Log browser based on osgi LogService

Signed-off-by: Lukasz Dywicki <luke@code-house.org>

git-svn-id: https://svn.apache.org/repos/asf/karaf/webconsole/trunk@1166781 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/LogEntriesDataProvider.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/LogEntriesDataProvider.java
new file mode 100644
index 0000000..cdca34c
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/LogEntriesDataProvider.java
@@ -0,0 +1,72 @@
+package org.apache.karaf.webconsole.osgi.internal.log;
+
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.karaf.webconsole.osgi.internal.log.search.Matcher;
+import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
+import org.apache.wicket.model.IModel;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogReaderService;
+
+final class LogEntriesDataProvider extends SortableDataProvider<LogEntry> {
+
+    private LogReaderService logReader;
+
+    private List<LogEntry> entries;
+
+    private Options options;
+
+    private transient List<Matcher> matchers;
+
+    public LogEntriesDataProvider(LogReaderService logReader, Options options, List<Matcher> matchers) {
+        this.logReader = logReader;
+        this.options = options;
+        this.matchers = matchers;
+    }
+
+    public Iterator<? extends LogEntry> iterator(int first, int count) {
+        return getEntries().subList(first, first + count).iterator();
+    }
+
+    private List<LogEntry> getEntries() {
+        if (entries == null) {
+            entries = new LinkedList<LogEntry>();
+            @SuppressWarnings("unchecked")
+            Enumeration<LogEntry> logEntries = logReader.getLog();
+            while (logEntries.hasMoreElements()) {
+                LogEntry entry = logEntries.nextElement();
+
+                boolean matchesAll = true;
+                for (Matcher matcher : matchers) {
+                    if (!matcher.matches(entry, options)) {
+                        matchesAll = false;
+                        break;
+                    }
+                }
+
+                if (matchesAll) entries.add(entry);
+            }
+        }
+        return entries;
+    }
+
+    public IModel<LogEntry> model(LogEntry object) {
+        return new LogEntryModel(this.logReader, object);
+    }
+
+    public int size() {
+        return getEntries().size();
+    }
+
+    public void setOptions(Options options) {
+        this.options = options;
+    }
+
+    @Override
+    public void detach() {
+        entries = null;
+    }
+}
\ No newline at end of file
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/LogEntryModel.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/LogEntryModel.java
new file mode 100644
index 0000000..9b881cb
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/LogEntryModel.java
@@ -0,0 +1,30 @@
+package org.apache.karaf.webconsole.osgi.internal.log;
+
+import java.util.Collections;
+import java.util.Enumeration;
+
+import org.apache.wicket.model.LoadableDetachableModel;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogReaderService;
+
+public class LogEntryModel extends LoadableDetachableModel<LogEntry> {
+
+    private int hashCode;
+    private LogReaderService logReader;
+
+    public LogEntryModel(LogReaderService logReader, LogEntry object) {
+        super(object);
+        this.logReader = logReader;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    protected LogEntry load() {
+        for (LogEntry entry : Collections.list((Enumeration<LogEntry>) logReader.getLog())) {
+            if (hashCode == entry.hashCode()) {
+                return entry;
+            }
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/LogsPage.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/LogsPage.java
new file mode 100644
index 0000000..414680f
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/LogsPage.java
@@ -0,0 +1,77 @@
+package org.apache.karaf.webconsole.osgi.internal.log;
+
+import java.text.DateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.karaf.webconsole.core.table.PropertyColumnExt;
+import org.apache.karaf.webconsole.osgi.internal.OsgiPage;
+import org.apache.karaf.webconsole.osgi.internal.log.search.BundleMatcher;
+import org.apache.karaf.webconsole.osgi.internal.log.search.DateFromMatcher;
+import org.apache.karaf.webconsole.osgi.internal.log.search.DateToMatcher;
+import org.apache.karaf.webconsole.osgi.internal.log.search.Matcher;
+import org.apache.karaf.webconsole.osgi.internal.log.search.MessageMatcher;
+import org.apache.karaf.webconsole.osgi.internal.log.search.PriorityMatcher;
+import org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.DefaultDataTable;
+import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.PropertyModel;
+import org.ops4j.pax.wicket.api.PaxWicketBean;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogReaderService;
+
+public class LogsPage extends OsgiPage {
+
+    @PaxWicketBean(name = "logReader")
+    private LogReaderService logReader;
+
+    private Options options = new Options();
+
+    public LogsPage() {
+        CompoundPropertyModel<Options> model = new CompoundPropertyModel<Options>(new PropertyModel<Options>(this, "options"));
+        setDefaultModel(model);
+
+        @SuppressWarnings("unchecked")
+        IColumn<LogEntry>[] columns = new IColumn[] {
+            new AbstractColumn<LogEntry>(Model.of("time")) {
+                public void populateItem(Item<ICellPopulator<LogEntry>> cellItem, String componentId, IModel<LogEntry> rowModel) {
+                    long time = rowModel.getObject().getTime();
+                    DateFormat format = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.FULL);
+                    cellItem.add(new Label(componentId, format.format(new Date(time))));
+                }
+            },
+            new AbstractColumn<LogEntry>(Model.of("level")) {
+                public void populateItem(Item<ICellPopulator<LogEntry>> cellItem, String componentId, IModel<LogEntry> rowModel) {
+                    cellItem.add(new Label(componentId, Priority.valueOf(rowModel.getObject()).name()));
+                }
+            },
+            new PropertyColumnExt<LogEntry>("Bundle", "bundle.symbolicName"),
+            new PropertyColumnExt<LogEntry>("Version", "bundle.version"),
+            new PropertyColumnExt<LogEntry>("Message", "message"),
+            new PropertyColumnExt<LogEntry>("Exception", "exception"),
+        };
+
+        OptionsForm form = new OptionsForm("filters", model);
+
+        List<Matcher> matchers = Arrays.asList(
+            new PriorityMatcher(),
+            new MessageMatcher(),
+            new BundleMatcher(),
+            new DateFromMatcher(),
+            new DateToMatcher()
+        );
+
+        LogEntriesDataProvider provider = new LogEntriesDataProvider(logReader, options, matchers);
+        DefaultDataTable<LogEntry> table = new DefaultDataTable<LogEntry>("logs", columns, provider, 20);
+
+        add(table);
+        add(form);
+    }
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/Options.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/Options.java
new file mode 100644
index 0000000..93917cf
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/Options.java
@@ -0,0 +1,66 @@
+package org.apache.karaf.webconsole.osgi.internal.log;
+
+import java.io.Serializable;
+
+public class Options implements Serializable {
+
+    private Priority priority = Priority.Any;
+
+    private String bundleNameFragment;
+
+    private String messageFragment;
+
+    private Long dateFrom = null;
+
+    private Long dateTo = null;
+
+    public Priority getPriority() {
+        return priority;
+    }
+
+    public void setPriority(Priority priority) {
+        this.priority = priority;
+    }
+
+    public String getBundleNameFragment() {
+        return bundleNameFragment;
+    }
+
+    public void setBundleNameFragment(String bundleNameFragment) {
+        this.bundleNameFragment = bundleNameFragment;
+    }
+
+    public String getMessageFragment() {
+        return messageFragment;
+    }
+
+    public void setMessageFragment(String messageFragment) {
+        this.messageFragment = messageFragment;
+    }
+
+    public Long getDateFrom() {
+        return dateFrom;
+    }
+
+    public void setDateFrom(Long dateFrom) {
+        this.dateFrom = dateFrom;
+    }
+
+    public Long getDateTo() {
+        return dateTo;
+    }
+
+    public void setDateTo(Long dateTo) {
+        this.dateTo = dateTo;
+    }
+
+    @Override
+    public String toString() {
+        return "Options [priority=" + priority + ", bundleNameFragment="
+                + bundleNameFragment + ", messageFragment=" + messageFragment
+                + ", dateFrom=" + dateFrom + ", dateTo=" + dateTo + "]";
+    }
+
+    
+
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/OptionsForm.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/OptionsForm.java
new file mode 100644
index 0000000..20f9898
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/OptionsForm.java
@@ -0,0 +1,42 @@
+package org.apache.karaf.webconsole.osgi.internal.log;
+
+import java.util.Arrays;
+
+import org.apache.wicket.extensions.markup.html.form.select.IOptionRenderer;
+import org.apache.wicket.extensions.markup.html.form.select.Select;
+import org.apache.wicket.extensions.markup.html.form.select.SelectOptions;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.SubmitLink;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+
+public class OptionsForm extends Form<Options> {
+
+    public OptionsForm(String id, CompoundPropertyModel<Options> model) {
+        super(id, model);
+
+        IModel<Priority> priority = model.bind("priority");
+        Select<Priority> select = new Select<Priority>("priority", priority);
+        select.add(new SelectOptions<Priority>("options", Arrays.asList(Priority.values()), new IOptionRenderer<Priority>() {
+            public String getDisplayValue(Priority object) {
+                return object.name();
+            }
+
+            public IModel<Priority> getModel(Priority value) {
+                return Model.of(value);
+            }
+        }));
+
+        add(select);
+        add(new TextField<Long>("dateFrom", Long.class));
+        add(new TextField<Long>("dateTo", Long.class));
+        add(new TextField<String>("messageFragment",String.class));
+        add(new TextField<String>("bundleNameFragment",String.class));
+
+
+        add(new SubmitLink("submit"));
+    }
+
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/Priority.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/Priority.java
new file mode 100644
index 0000000..e88145d
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/Priority.java
@@ -0,0 +1,40 @@
+package org.apache.karaf.webconsole.osgi.internal.log;
+
+import static org.osgi.service.log.LogService.LOG_DEBUG;
+import static org.osgi.service.log.LogService.LOG_ERROR;
+import static org.osgi.service.log.LogService.LOG_INFO;
+import static org.osgi.service.log.LogService.LOG_WARNING;
+
+import org.osgi.service.log.LogEntry;
+
+
+public enum Priority {
+
+    Error(LOG_ERROR),
+    Warning(LOG_WARNING),
+    Info(LOG_INFO),
+    Debug(LOG_DEBUG),
+
+    Any(5);
+
+    private final int level;
+
+    private Priority(int priority) {
+        this.level = priority;
+    }
+
+    public int getLevel() {
+        return level;
+    }
+
+    public static Priority valueOf(LogEntry entry) {
+        for (Priority priority : values()) {
+            if (priority.level == entry.getLevel()) {
+                return priority;
+            }
+        }
+
+        return Any;
+    }
+
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/BundleMatcher.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/BundleMatcher.java
new file mode 100644
index 0000000..a872d99
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/BundleMatcher.java
@@ -0,0 +1,15 @@
+package org.apache.karaf.webconsole.osgi.internal.log.search;
+
+import org.apache.karaf.webconsole.osgi.internal.log.Options;
+import org.osgi.service.log.LogEntry;
+
+public class BundleMatcher implements Matcher {
+
+    public boolean matches(LogEntry entry, Options options) {
+        if (options.getBundleNameFragment() != null && !options.getBundleNameFragment().isEmpty()) {
+            return entry.getBundle().getSymbolicName().contains(options.getBundleNameFragment());
+        }
+        return true;
+    }
+
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/DateFromMatcher.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/DateFromMatcher.java
new file mode 100644
index 0000000..0fe414c
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/DateFromMatcher.java
@@ -0,0 +1,15 @@
+package org.apache.karaf.webconsole.osgi.internal.log.search;
+
+import org.apache.karaf.webconsole.osgi.internal.log.Options;
+import org.osgi.service.log.LogEntry;
+
+public class DateFromMatcher implements Matcher {
+
+    public boolean matches(LogEntry entry, Options options) {
+        if (options.getDateFrom() != null && options.getDateFrom() > 0) {
+            return entry.getTime() >= options.getDateFrom();
+        }
+        return true;
+    }
+
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/DateToMatcher.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/DateToMatcher.java
new file mode 100644
index 0000000..4a37e65
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/DateToMatcher.java
@@ -0,0 +1,15 @@
+package org.apache.karaf.webconsole.osgi.internal.log.search;
+
+import org.apache.karaf.webconsole.osgi.internal.log.Options;
+import org.osgi.service.log.LogEntry;
+
+public class DateToMatcher implements Matcher {
+
+    public boolean matches(LogEntry entry, Options options) {
+        if (options.getDateTo() != null && options.getDateTo() > 0) {
+            return entry.getTime() <= options.getDateTo();
+        }
+        return true;
+    }
+
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/Matcher.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/Matcher.java
new file mode 100644
index 0000000..26c08a3
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/Matcher.java
@@ -0,0 +1,10 @@
+package org.apache.karaf.webconsole.osgi.internal.log.search;
+
+import org.apache.karaf.webconsole.osgi.internal.log.Options;
+import org.osgi.service.log.LogEntry;
+
+public interface Matcher {
+
+    boolean matches(LogEntry entry, Options options);
+
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/MessageMatcher.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/MessageMatcher.java
new file mode 100644
index 0000000..07e0734
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/MessageMatcher.java
@@ -0,0 +1,15 @@
+package org.apache.karaf.webconsole.osgi.internal.log.search;
+
+import org.apache.karaf.webconsole.osgi.internal.log.Options;
+import org.osgi.service.log.LogEntry;
+
+public class MessageMatcher implements Matcher {
+
+    public boolean matches(LogEntry entry, Options options) {
+        if (options.getMessageFragment() != null && !options.getMessageFragment().isEmpty()) {
+            return entry.getMessage().contains(options.getMessageFragment());
+        }
+        return true;
+    }
+
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/PriorityMatcher.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/PriorityMatcher.java
new file mode 100644
index 0000000..fe1af86
--- /dev/null
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/log/search/PriorityMatcher.java
@@ -0,0 +1,12 @@
+package org.apache.karaf.webconsole.osgi.internal.log.search;
+
+import org.apache.karaf.webconsole.osgi.internal.log.Options;
+import org.osgi.service.log.LogEntry;
+
+public class PriorityMatcher implements Matcher {
+
+    public boolean matches(LogEntry entry, Options options) {
+        return options.getPriority().getLevel() >= entry.getLevel();
+    }
+
+}
diff --git a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/sidebar/OsgiSidebar.java b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/sidebar/OsgiSidebar.java
index b2d1b9f..0804da2 100644
--- a/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/sidebar/OsgiSidebar.java
+++ b/osgi/src/main/java/org/apache/karaf/webconsole/osgi/internal/sidebar/OsgiSidebar.java
@@ -27,6 +27,7 @@
 import org.apache.karaf.webconsole.osgi.internal.bundle.BundlesPage;
 import org.apache.karaf.webconsole.osgi.internal.configuration.ConfigurationsPage;
 import org.apache.karaf.webconsole.osgi.internal.event.EventsPage;
+import org.apache.karaf.webconsole.osgi.internal.log.LogsPage;
 import org.apache.wicket.Page;
 import org.apache.wicket.markup.html.link.Link;
 
@@ -39,7 +40,8 @@
     public List<Link<Page>> getItems(String componentId, String labelId) {
         return Arrays.asList(
             createPageLink(componentId, labelId, "Configuration", ConfigurationsPage.class),
-            createPageLink(componentId, labelId, "Events", EventsPage.class)
+            createPageLink(componentId, labelId, "Events", EventsPage.class),
+            createPageLink(componentId, labelId, "Log entries", LogsPage.class)
         );
     }
 
diff --git a/osgi/src/main/resources/OSGI-INF/blueprint/osgi.xml b/osgi/src/main/resources/OSGI-INF/blueprint/osgi.xml
index 28837a7..5608796 100644
--- a/osgi/src/main/resources/OSGI-INF/blueprint/osgi.xml
+++ b/osgi/src/main/resources/OSGI-INF/blueprint/osgi.xml
@@ -50,6 +50,7 @@
     <reference id="startLevel" interface="org.osgi.service.startlevel.StartLevel" />
     <reference id="packageAdmin" interface="org.osgi.service.packageadmin.PackageAdmin" />
     <reference id="metaTypeService" interface="org.osgi.service.metatype.MetaTypeService" />
+    <reference id="logReader" interface="org.osgi.service.log.LogReaderService" />
 
     <reference-list id="columnProviders" interface="org.apache.karaf.webconsole.osgi.bundle.IColumnProvider" availability="optional" />
     <reference-list id="actionProviders" interface="org.apache.karaf.webconsole.osgi.bundle.IActionProvider" availability="optional" />
diff --git a/osgi/src/main/resources/org/apache/karaf/webconsole/osgi/internal/log/LogsPage.html b/osgi/src/main/resources/org/apache/karaf/webconsole/osgi/internal/log/LogsPage.html
new file mode 100644
index 0000000..eef6050
--- /dev/null
+++ b/osgi/src/main/resources/org/apache/karaf/webconsole/osgi/internal/log/LogsPage.html
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.4-strict.dtd" >
+    <wicket:head>
+        <title>Karaf WebConsole</title>
+    </wicket:head>
+
+    <body>
+        <wicket:extend>
+            <h1>Log entries</h1>
+
+            <div class="grid_9">
+                <form wicket:id="filters">
+
+                    <label for="priority">
+                        Priority
+                        <select wicket:id="priority" id="priority">
+                            <wicket:container wicket:id="options">
+                                <option wicket:id="option">label</option>
+                            </wicket:container>
+                        </select>
+                    </label>
+
+                    <label for="dateFrom">
+                        Date from
+                        <input type="text" id="dateFrom" wicket:id="dateFrom" />
+                    </label>
+
+                    <label for="dateTo">
+                        Date to
+                        <input type="text" id="dateTo" wicket:id="dateTo" />
+                    </label>
+
+                    <label for="messageFragment">
+                        Message
+                        <input type="text" id="messageFragment" wicket:id="messageFragment" />
+                    </label>
+
+                    <label for="bundleNameFragment">
+                        Bundle
+                        <input type="text" id="bundleNameFragment" wicket:id="bundleNameFragment" />
+                    </label>
+
+                    <input wicket:id="submit" type="submit" value="Filter" />
+                </form>
+            </div>
+
+            <table wicket:id="logs" class="dataview" />
+
+        </wicket:extend>
+    </body>
+</html>