/* * Lilith - a log event viewer. * Copyright (C) 2007-2017 Joern Huxhorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.huxhorn.lilith.swing; import de.huxhorn.lilith.DateTimeFormatters; import de.huxhorn.lilith.data.eventsource.EventWrapper; import de.huxhorn.lilith.data.eventsource.SourceIdentifier; import de.huxhorn.lilith.data.logging.ExtendedStackTraceElement; import de.huxhorn.lilith.data.logging.LoggingEvent; import de.huxhorn.lilith.engine.EventSource; import de.huxhorn.lilith.swing.callables.CallableMetaData; import de.huxhorn.lilith.swing.callables.FindNextCallable; import de.huxhorn.lilith.swing.callables.FindPreviousCallable; import de.huxhorn.lilith.swing.linklistener.OpenUrlLinkListener; import de.huxhorn.lilith.swing.table.EventWrapperViewTable; import de.huxhorn.lilith.swing.table.model.EventWrapperTableModel; import de.huxhorn.sulky.buffers.Buffer; import de.huxhorn.sulky.buffers.DisposeOperation; import de.huxhorn.sulky.buffers.Flush; import de.huxhorn.sulky.buffers.FlushOperation; import de.huxhorn.sulky.buffers.SoftReferenceCachingBuffer; import de.huxhorn.sulky.buffers.filtering.FilteringBuffer; import de.huxhorn.sulky.codec.filebuffer.CodecFileBuffer; import de.huxhorn.sulky.conditions.And; import de.huxhorn.sulky.conditions.Condition; import de.huxhorn.sulky.formatting.HumanReadable; import de.huxhorn.sulky.swing.KeyStrokes; import de.huxhorn.sulky.tasks.ProgressingCallable; import de.huxhorn.sulky.tasks.Task; import de.huxhorn.sulky.tasks.TaskListener; import de.huxhorn.sulky.tasks.TaskManager; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.FocusTraversalPolicy; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Insets; import java.awt.KeyboardFocusManager; import java.awt.Point; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Serializable; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.ListSelectionModel; import javax.swing.border.MatteBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xhtmlrenderer.context.AWTFontResolver; import org.xhtmlrenderer.extend.FontResolver; import org.xhtmlrenderer.extend.TextRenderer; import org.xhtmlrenderer.layout.SharedContext; import org.xhtmlrenderer.simple.FSScrollPane; import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler; import org.xhtmlrenderer.swing.LinkListener; import org.xhtmlrenderer.swing.ScalableXHTMLPanel; import org.xhtmlrenderer.swing.SelectionHighlighter; public abstract class EventWrapperViewPanel<T extends Serializable> extends JPanel implements DisposeOperation, FlushOperation { private static final long serialVersionUID = -944900018575063575L; private final Logger logger = LoggerFactory.getLogger(EventWrapperViewPanel.class); static final String STATE_PROPERTY = "state"; private static final String FILTER_CONDITION_PROPERTY = "filterCondition"; static final String EVENT_SOURCE_PROPERTY = "eventSource"; private static final String SCROLLING_TO_BOTTOM_PROPERTY = "scrollingToBottom"; private static final String PAUSED_PROPERTY = "paused"; static final String SELECTED_EVENT_PROPERTY = "selectedEvent"; private final MainFrame mainFrame; private EventSource<T> eventSource; private LoggingViewState state; private boolean showingFilters; private Condition filterCondition; private TaskManager<Long> taskManager; private EventWrapperViewTable<T> table; private EventWrapperTableModel<T> tableModel; private JLabel statusLabel; private JScrollBar verticalLogScrollBar; private StatusTableModelListener tableModelListener; private MatteBorder focusedBorder; private MatteBorder unfocusedBorder; private DecimalFormat eventCountFormat; private FindResultListener findResultListener; private ScalableXHTMLPanel messagePane; private XhtmlNamespaceHandler xhtmlNamespaceHandler; private EventWrapper<T> selectedEvent; private SelectionHighlighter.CopyAction copyAction; private double scale; private JScrollPane tableScrollPane; private FindPanel<T> findPanel; private SoftReferenceCachingBuffer<EventWrapper<T>> cachedBuffer; public EventWrapperViewPanel(MainFrame mainFrame, EventSource<T> eventSource) { super(true); this.mainFrame = Objects.requireNonNull(mainFrame, "mainFrame must not be null!"); // can't use setEventSource(eventSource) at this point. this.eventSource = Objects.requireNonNull(eventSource, "eventSource must not be null!"); eventCountFormat = new DecimalFormat("#,###", new DecimalFormatSymbols(Locale.US)); this.taskManager = mainFrame.getLongWorkManager(); findResultListener = new FindResultListener(); taskManager.addTaskListener(findResultListener); showingFilters = false; tableModelListener = new StatusTableModelListener(); initUi(); } private void initUi() { ApplicationPreferences applicationPreferences = mainFrame.getApplicationPreferences(); scale = applicationPreferences.getScaleFactor(); Insets borderInsets = new Insets(2, 2, 2, 2); focusedBorder = new MatteBorder(borderInsets, Color.YELLOW); unfocusedBorder = new MatteBorder(borderInsets, Color.WHITE); cachedBuffer = createCachedBuffer(eventSource.getBuffer()); tableModel = createTableModel(cachedBuffer); tableModel.addTableModelListener(tableModelListener); table = createTable(tableModel); table.getSelectionModel().addListSelectionListener(new TableRowSelectionListener()); tableScrollPane = new JScrollPane(table); table.addMouseListener(new TableMouseListener()); tableScrollPane.addMouseListener(new ScrollPaneMouseListener()); tableScrollPane.setPreferredSize(new Dimension(400, 400)); verticalLogScrollBar = tableScrollPane.getVerticalScrollBar(); messagePane = new ScalableXHTMLPanel(); //noinspection Duplicates { SharedContext sharedContext = messagePane.getSharedContext(); TextRenderer textRenderer = sharedContext.getTextRenderer(); textRenderer.setSmoothingThreshold(RendererConstants.SMOOTHING_THRESHOLD); FontResolver fontResolver = sharedContext.getFontResolver(); if(fontResolver instanceof AWTFontResolver && RendererConstants.MENSCH_FONT != null) { AWTFontResolver awtFontResolver = (AWTFontResolver) fontResolver; awtFontResolver.setFontMapping(RendererConstants.MONOSPACED_FAMILY, RendererConstants.MENSCH_FONT); if(logger.isInfoEnabled()) logger.info("Installed '{}' font.", RendererConstants.MONOSPACED_FAMILY); } } { LinkListener originalLinkListener = null; List mouseTrackingList = messagePane.getMouseTrackingListeners(); //noinspection Duplicates if(mouseTrackingList != null) { for(Object o : mouseTrackingList) { if(logger.isDebugEnabled()) logger.debug("Before MTL {}", o); if(o instanceof LinkListener) { messagePane.removeMouseTrackingListener((LinkListener) o); originalLinkListener = (LinkListener) o; } } } messagePane.addMouseTrackingListener(new OpenUrlLinkListener(mainFrame, originalLinkListener)); } messagePane.setScale(scale); SelectionHighlighter messagePaneCaret = new SelectionHighlighter(); messagePaneCaret.install(messagePane); copyAction = new SelectionHighlighter.CopyAction(); copyAction.install(messagePaneCaret); messagePane.addMouseListener(new EventViewMouseListener()); messagePane.addFocusListener(new MessageFocusListener()); messagePane.setBorder(unfocusedBorder); xhtmlNamespaceHandler = new XhtmlNamespaceHandler(); FSScrollPane messageScrollPane = new FSScrollPane(messagePane); messageScrollPane.setPreferredSize(new Dimension(400, 400)); MouseWheelListener[] mwl = messageScrollPane.getMouseWheelListeners(); if(mwl != null) { for(MouseWheelListener current : mwl) { messageScrollPane.removeMouseWheelListener(current); } } messageScrollPane.addMouseWheelListener(new WrappingMouseWheelListener(mwl)); JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tableScrollPane, messageScrollPane); PropertyChangeListener splitPaneListener = new SplitPaneListener(); splitPane.addPropertyChangeListener(splitPaneListener); splitPane.setResizeWeight(0.5); // divide space equally in case of resize. splitPane.setOneTouchExpandable(true); setLayout(new BorderLayout()); add(splitPane, BorderLayout.CENTER); ScrollBarChangeListener scrollBarChangeListener = new ScrollBarChangeListener(); verticalLogScrollBar.getModel().addChangeListener(scrollBarChangeListener); table.addPropertyChangeListener(new EventWrapperViewChangeListener()); JPanel bottomPanel = new JPanel(new BorderLayout()); JPanel statusPanel = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); findPanel = new FindPanel<>(this); findPanel.addPropertyChangeListener(new FindPanelChangeListener()); bottomPanel.add(findPanel, BorderLayout.CENTER); bottomPanel.add(statusPanel, BorderLayout.SOUTH); statusLabel = new JLabel(); gbc.fill = GridBagConstraints.HORIZONTAL; gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 1.0; gbc.anchor = GridBagConstraints.CENTER; gbc.insets = new Insets(0, 5, 0, 0); statusPanel.add(statusLabel, gbc); add(bottomPanel, BorderLayout.SOUTH); setPaused(false); FocusTraversalPolicy focusTraversalPolicy = new MyFocusTraversalPolicy(); setFocusTraversalPolicy(focusTraversalPolicy); setFocusCycleRoot(true); setFocusTraversalPolicyProvider(true); if(logger.isDebugEnabled()) logger.debug("table.isFocusCycleRoot()={}", table.isFocusCycleRoot()); if(logger.isDebugEnabled()) { logger.debug("table.isFocusTraversalPolicyProvider()={}", table.isFocusTraversalPolicyProvider()); } table.setFocusTraversalPolicy(focusTraversalPolicy); table.setFocusCycleRoot(true); // setting table traversal back to "normal"... table .setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS)); table .setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, getFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS)); updateStatusText(); splitPane.setDividerLocation(0.5d); setShowingStatusBar(applicationPreferences.isShowingStatusBar()); setScrollingToBottom(applicationPreferences.isScrollingToBottom()); setScrollingSmoothly(applicationPreferences.isScrollingSmoothly()); } public EventWrapperViewTable<T> getTable() { return table; } LoggingViewState getState() { if(!EventQueue.isDispatchThread()) { if(logger.isWarnEnabled()) { //noinspection ThrowableInstanceNeverThrown logger.warn("!DispatchThread - getState: state=" + state, new Throwable()); } } return state; } public MainFrame getMainFrame() { return mainFrame; } void setState(LoggingViewState state) { Object oldValue = this.state; this.state = state; Object newValue = this.state; firePropertyChange(STATE_PROPERTY, oldValue, newValue); } private boolean isShowingFilters() { return showingFilters; } void setShowingFilters(@SuppressWarnings("SameParameterValue") boolean showingFilters) { if(logger.isDebugEnabled()) logger.debug("ShowingFilters: {}", showingFilters); this.showingFilters = showingFilters; if(showingFilters) { findPanel.updateUi(); } findPanel.setVisible(showingFilters); if(showingFilters) { findPanel.requestComboFocus(); applyFilter(); } scrollToEvent(); } /** * scrolls to bottom if it is enabled. Otherwise makes sure that an event is selected if available. */ void scrollToEvent() { if(table.isScrollingToBottom()) { EventQueue.invokeLater(new ScrollToBottomRunnable()); } else if(table.getSelectedRow() < 0) { EventQueue.invokeLater(new SelectFirstEventRunnable()); } } void setScaleFactor(double scale) { this.scale = scale; messagePane.setScale(scale); } void updateView() { // update HTML details view EventWrapper<T> selected = getSelectedEvent(); if(selected != null) { initMessage(selected); } else { resetMessage(); } } public void setShowingStatusBar(boolean showingStatusBar) { statusLabel.setVisible(showingStatusBar); } void setPreviousSearchStrings(List<String> previousSearchStrings) { findPanel.setPreviousSearchStrings(previousSearchStrings); } public void setConditionNames(List<String> conditionNames) { findPanel.setConditionNames(conditionNames); } private class ScrollToBottomRunnable implements Runnable { public void run() { // recheck because this is executed with invokeLater. if(table.isScrollingToBottom()) { table.scrollToBottom(); } } } private class SelectFirstEventRunnable implements Runnable { public void run() { table.scrollToFirst(); } } public void validate() { super.validate(); if(logger.isDebugEnabled()) logger.debug("Validate"); } private SoftReferenceCachingBuffer<EventWrapper<T>> createCachedBuffer(Buffer<EventWrapper<T>> buffer) { return new SoftReferenceCachingBuffer<>(buffer); } void setEventSource(EventSource<T> eventSource) { Objects.requireNonNull(eventSource, "eventSource must not be null!"); EventSource oldValue = this.eventSource; this.eventSource = eventSource; SoftReferenceCachingBuffer<EventWrapper<T>> cachedBuffer = createCachedBuffer(eventSource.getBuffer()); tableModel.setBuffer(cachedBuffer); EventSource newValue = this.eventSource; if(logger.isDebugEnabled()) logger.debug("EventSource\nOld: {}\nNew: {}", oldValue, newValue); firePropertyChange(EVENT_SOURCE_PROPERTY, oldValue, newValue); } public EventSource<T> getEventSource() { return eventSource; } void setScrollingSmoothly(boolean scrollingSmoothly) { table.setScrollingSmoothly(scrollingSmoothly); } void setScrollingToBottom(boolean scrollingToBottom) { Object oldValue = table.isScrollingToBottom(); table.setScrollingToBottom(scrollingToBottom); Object newValue = table.isScrollingToBottom(); firePropertyChange(SCROLLING_TO_BOTTOM_PROPERTY, oldValue, newValue); } boolean isScrollingToBottom() { return table.isScrollingToBottom(); } boolean isPaused() { return tableModel.isPaused(); } void setPaused(boolean paused) { Object oldValue = tableModel.isPaused(); tableModel.setPaused(paused); Object newValue = tableModel.isPaused(); firePropertyChange(PAUSED_PROPERTY, oldValue, newValue); } @SuppressWarnings("unchecked") ViewContainer<T> resolveContainer() { Container parent = getParent(); while(parent != null && !(parent instanceof ViewContainer)) { parent = parent.getParent(); } // not 100% type-safe return (ViewContainer<T>) parent; } public void addNotify() { super.addNotify(); if(logger.isDebugEnabled()) logger.debug("addNotify - parent: {}", getParent()); findPanel.setVisible(isShowingFilters()); } public void removeNotify() { super.removeNotify(); if(logger.isDebugEnabled()) logger.debug("removeNotify"); } protected abstract EventWrapperTableModel<T> createTableModel(Buffer<EventWrapper<T>> buffer); protected abstract EventWrapperViewTable<T> createTable(EventWrapperTableModel<T> tableModel); public void dispose() { tableModel.dispose(); taskManager.removeTaskListener(findResultListener); } public void flush() { Flush.flush(cachedBuffer); resetFind(); } public boolean isDisposed() { return tableModel.isDisposed(); } void resetFind() { findPanel.resetFind(); setFilterCondition(null); } private void setSelectedEvent(EventWrapper<T> selectedEvent) { Object oldValue = this.selectedEvent; this.selectedEvent = selectedEvent; Object newValue = this.selectedEvent; firePropertyChange(SELECTED_EVENT_PROPERTY, oldValue, newValue); } EventWrapper<T> getSelectedEvent() { return selectedEvent; } /** * Alright... The code in this class sucks big time. I'm fully aware of this fact. * One would expect that Focus Traversal would play well in an hierarchy, too, but it doesn't. * It's only supporting up- and down-cycling which isn't what I want/need. * I'd like to traverse into the findPanel and back out of it.... * * but..... * * if I implement that in a sophisticated, i.e. non-sucking, way I'd risk to infringe the * software patent "US Patent 6606106 - Hierarchical model for expressing focus traversal" * which you may review here: * http://www.patentstorm.us/patents/6606106/fulltext.html * * This is a hilarious example for the epic fail inherent to all software patents. * It's also a really good reason why you should vote for your local Pirate Party, or, * if your country doesn't have a Pirate Party yet, a reason to start one. * * If this is out of question then please try to vote for a party that opposes software patents, * assuming there is one in your country. * * kthxbai, Joern. */ class MyFocusTraversalPolicy extends FocusTraversalPolicy { private Component resolveComponent(Component component) { Container container = component.getParent(); //noinspection Duplicates while(container != null) { if(container == table) { return table; } if(container == messagePane) { return messagePane; } container = container.getParent(); } return null; } public Component getComponentAfter(Container aContainer, Component aComponent) { if(aComponent.equals(table)) { return messagePane; } //noinspection Duplicates if(aComponent.equals(messagePane)) { if(isShowingFilters()) { /* Attention, sucking code below! */ if(logger.isDebugEnabled()) logger.debug("Performing FocusTraversal-Voodoo..."); FocusTraversalPolicy policy = findPanel.getFocusTraversalPolicy(); return policy.getDefaultComponent(aContainer); /* Attention, sucking code above! */ } return table; } if(aComponent.equals(findPanel)) { return table; } /* Attention, sucking code below! */ if(logger.isDebugEnabled()) logger.debug("Performing FocusTraversal..."); Component c = resolveComponent(aComponent); if(table.equals(c)) { return messagePane; } if(messagePane.equals(c)) { if(isShowingFilters()) { return findPanel; } return table; } FocusTraversalPolicy policy = findPanel.getFocusTraversalPolicy(); Component result = policy.getComponentAfter(aContainer, aComponent); if(result == policy.getFirstComponent(aContainer)) { return table; } else if (result != null) { return result; } if(aContainer == aComponent) { // prevent useless warning return null; } /* Attention, sucking code above! */ if(logger.isWarnEnabled()) logger.warn("Moving focus forward was not explicitly handled.\ncontainer={}\ncomponent={}", aContainer, aComponent); return null; } public Component getComponentBefore(Container aContainer, Component aComponent) { if(aComponent.equals(messagePane)) { return table; } if(aComponent.equals(findPanel)) { return messagePane; } //noinspection Duplicates if(aComponent.equals(table)) { if(isShowingFilters()) { /* Attention, sucking code below! */ if(logger.isDebugEnabled()) logger.debug("Performing FocusTraversal-Voodoo..."); FocusTraversalPolicy policy = findPanel.getFocusTraversalPolicy(); return policy.getDefaultComponent(aContainer); /* Attention, sucking code above! */ } return messagePane; } /* Attention, sucking code below! */ if(logger.isDebugEnabled()) logger.debug("Performing FocusTraversal..."); Component c = resolveComponent(aComponent); if(table.equals(c)) { if(isShowingFilters()) { return findPanel; } return messagePane; } if(messagePane.equals(c)) { return table; } FocusTraversalPolicy policy = findPanel.getFocusTraversalPolicy(); Component result = policy.getComponentBefore(aContainer, aComponent); if(result == policy.getLastComponent(aContainer)) { return messagePane; } else if (result != null) { return result; } if(aContainer == aComponent) { // prevent useless warning return null; } /* Attention, sucking code above! */ if(logger.isWarnEnabled()) logger.warn("Moving focus backward was not explicitly handled.\ncontainer={}\ncomponent={}", aContainer, aComponent); return null; } public Component getFirstComponent(Container aContainer) { return table; } public Component getLastComponent(Container aContainer) { return messagePane; } public Component getDefaultComponent(Container aContainer) { return table; } } public EventWrapperTableModel<T> getTableModel() { return tableModel; } private void initMessage(EventWrapper wrapper) { String message = mainFrame.createMessage(wrapper); URL messageViewRootUrl = mainFrame.getApplicationPreferences().getDetailsViewRootUrl(); try { messagePane.setDocumentFromString(message, messageViewRootUrl.toExternalForm(), xhtmlNamespaceHandler); messagePane.setScale(scale); // this fixes a bug } catch(Throwable t) { if(logger.isWarnEnabled()) logger.warn("Exception while setting message {}!", message, t); writeErrorMessage(message); } } private void resetMessage() { String message = "<html><body>No event selected.</body></html>"; URL messageViewRootUrl = mainFrame.getApplicationPreferences().getDetailsViewRootUrl(); try { messagePane.setDocumentFromString(message, messageViewRootUrl.toExternalForm(), xhtmlNamespaceHandler); messagePane.setScale(scale); // this fixes a bug } catch(Throwable t) { if(logger.isWarnEnabled()) logger.warn("Exception while setting message!", t); writeErrorMessage(message); } } private void writeErrorMessage(String message) { File appPath = mainFrame.getApplicationPreferences().getStartupApplicationPath(); File errorPath = new File(appPath, "errors"); if(errorPath.mkdirs()) { if(logger.isDebugEnabled()) logger.debug("Created errors directory '{}'.", errorPath); } String filename = DateTimeFormatters.COMPACT_DATETIME_IN_SYSTEM_ZONE_T.format(Instant.now()); File errorFile = new File(errorPath, filename); try(OutputStream fos = Files.newOutputStream(errorFile.toPath())) { OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); osw.append(message); osw.flush(); if(logger.isInfoEnabled()) logger.info("Faulty message written to '{}'.", errorFile.getAbsolutePath()); } catch(Throwable e) { if(logger.isWarnEnabled()) logger.warn("Exception while writing faulty message to '{}'!", errorFile.getAbsolutePath(), e); } } /** * Does also disable scrollingToBottom and selects the clicked row, if any. * * @param p the point * @return the EventWrapper at the given point. */ private EventWrapper<T> getEventWrapper(Point p) { int row = table.rowAtPoint(p); if(-1 == row) { return null; } // row = table.convertRowIndexToModel(row); // 1.6, not needed table.setScrollingToBottom(false); table.selectRow(row); return tableModel.getValueAt(row); } private void applyFilter() { Condition condition=findPanel.getCondition(); if(logger.isDebugEnabled()) logger.debug("Setting condition: {}", condition); setFilterCondition(condition); } private void setFilterCondition(Condition condition) { Object old = this.filterCondition; this.filterCondition = condition; table.setFilterCondition(filterCondition); firePropertyChange(FILTER_CONDITION_PROPERTY, old, condition); } Condition getFilterCondition() { return filterCondition; } public void clear() { tableModel.clear(); table.requestFocusInWindow(); } /* public void findPrevious() { findPrevious(getSelectedRow(), getFilterCondition()); } */ void findPrevious(int currentRow, Condition condition) { if(condition != null) { ProgressingCallable<Long> callable = new FindPreviousCallable<>(this, currentRow, condition); executeFind(callable, "Find previous", currentRow, condition); } } /* public void findNext() { findNext(getSelectedRow(), getFilterCondition()); } */ void findNext(int currentRow, Condition condition) { if(condition != null) { ProgressingCallable<Long> callable = new FindNextCallable<>(this, currentRow, condition); executeFind(callable, "Find next", currentRow, condition); } } private void setSelectedRow(int row) { if(row > -1) { if(isScrollingToBottom()) { setScrollingToBottom(false); } table.selectRow(row); } } public int getSelectedRow() { ListSelectionModel selectionModel = table.getSelectionModel(); return selectionModel.getLeadSelectionIndex(); } private void updateStatusText() { int eventCount = tableModel.getRowCount(); StringBuilder statusText = new StringBuilder(); if(eventCount < 1) { statusText.append("No events."); } else { if(eventCount == 1) { statusText.append("One event."); } else { statusText.append(eventCountFormat.format(eventCount)).append(" events."); } long size = getSizeOnDisk(); if(size > 0) { statusText.append(" Size on disk: ") .append(HumanReadable.getHumanReadableSize(size, true, false)).append("bytes"); statusText.append(" Average event: ") .append(HumanReadable.getHumanReadableSize(size / eventCount, true, false)).append("bytes"); } } statusLabel.setText(statusText.toString()); } private long getSizeOnDisk() { Buffer buffer = getEventSource().getBuffer(); if(buffer instanceof CodecFileBuffer) { CodecFileBuffer cfb = (CodecFileBuffer) buffer; return cfb.getDataFile().length(); } return -1; } Buffer<EventWrapper<T>> getSourceBuffer() { Buffer<EventWrapper<T>> buffer = eventSource.getBuffer(); if(buffer instanceof FilteringBuffer) { FilteringBuffer<EventWrapper<T>> filteringBuffer = (FilteringBuffer<EventWrapper<T>>) buffer; return filteringBuffer.getSourceBuffer(); } return buffer; } Condition getBufferCondition() { Buffer<EventWrapper<T>> buffer = eventSource.getBuffer(); if(buffer instanceof FilteringBuffer) { FilteringBuffer<EventWrapper<T>> filteringBuffer = (FilteringBuffer<EventWrapper<T>>) buffer; return filteringBuffer.getCondition(); } return null; } void copySelection() { copyAction.actionPerformed(null); } /** * This method creates a new condition that is a combination of the current buffer condition and the given condition. * The conditions are combined using "and". Duplicate condition entries are prevented. * * @param condition the condition to be combined with the current buffer condition. * @return the combination of the given condition and the previous buffer condition. */ Condition getCombinedCondition(Condition condition) { Condition previousCondition = getBufferCondition(); if(previousCondition == null) { return condition; } try { // clone the previous condition so we don't change it while active Condition previousClone = previousCondition.clone(); if(condition == null) { return previousClone; } And and; if(previousClone instanceof And) { and = (And) previousClone; } else { and = new And(); ArrayList<Condition> conditions = new ArrayList<>(); conditions.add(previousClone); and.setConditions(conditions); } List<Condition> conditions = and.getConditions(); if(conditions == null) { conditions = new ArrayList<>(); } if(!conditions.contains(condition)) { // don't add duplicates conditions.add(condition); } if(conditions.size() > 1) { if(logger.isInfoEnabled()) logger.info("Setting and-conditions: {}", conditions); and.setConditions(conditions); return and; } else { return condition; } } catch(CloneNotSupportedException ex) { if(logger.isWarnEnabled()) logger.warn("Exception while cloning {}!", previousCondition, ex); } return null; } void createFilteredView() { ViewContainer<T> container = resolveContainer(); if(container == null) { return; } Condition condition = resolveCombinedCondition(); if(condition == null) { return; } container.addFilteredView(this, condition); } /** * If the currently selected event is in a filtered tab, the same event is displayed in the * unfiltered view. */ void showUnfilteredEvent() { int row = getSelectedRow(); if(row >= 0) { ViewContainer<T> container = resolveContainer(); if(container != null) { Buffer<EventWrapper<T>> buffer = eventSource.getBuffer(); if(buffer instanceof FilteringBuffer) { FilteringBuffer<EventWrapper<T>> filteringBuffer = (FilteringBuffer<EventWrapper<T>>) buffer; long unfilteredRow = filteringBuffer.getSourceIndex(row); if(unfilteredRow >= 0) { if(logger.isInfoEnabled()) { logger.info("Show unfiltered event {} for filtered event {}...", unfilteredRow, row); } EventWrapperViewPanel<T> defaultView = container.getDefaultView(); container.showDefaultView(); defaultView.setSelectedRow((int) unfilteredRow); } else if(logger.isWarnEnabled()) { logger.info("Can't show unfiltered event {} for filtered event {}...", unfilteredRow, row); } } } } } private void showPopup(Component component, Point p) { ViewContainer<T> container = resolveContainer(); if(container != null) { ViewWindow viewWindow = container.resolveViewWindow(); if(viewWindow != null) { ViewActions viewActions = viewWindow.getViewActions(); JPopupMenu popup = viewActions.getPopupMenu(); if(logger.isDebugEnabled()) logger.debug("Show popup at {}.", p); popup.show(component, p.x, p.y); } else { if(logger.isWarnEnabled()) logger.warn("Couldn't resolve viewWindow of viewContainer {}!", container); } } else { if(logger.isWarnEnabled()) logger.warn("Couldn't resolve viewContainer of viewPanel {}!", this); } } private class TableMouseListener implements MouseListener { TableMouseListener() { } public void mouseClicked(MouseEvent evt) { if(evt.isPopupTrigger()) { showPopup(evt); } else if(evt.getClickCount() > 1 && evt.getButton() == MouseEvent.BUTTON1) { if((evt.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK) != 0) { Point p = evt.getPoint(); EventWrapper<T> wrapper = getEventWrapper(p); if(wrapper != null) { T event = wrapper.getEvent(); if(event instanceof LoggingEvent) { LoggingEvent loggingEvent = (LoggingEvent) event; ExtendedStackTraceElement[] callStack = loggingEvent.getCallStack(); if(callStack != null && callStack.length > 0) { ExtendedStackTraceElement extendedStackTraceElement = callStack[0]; if(extendedStackTraceElement != null) { mainFrame.goToSource(extendedStackTraceElement.getStackTraceElement()); } } } } } else { Point p = evt.getPoint(); EventWrapper<T> wrapper = getEventWrapper(p); // To ensure that the event below the mouse is selected. if(logger.isDebugEnabled()) logger.debug("Show unfiltered event {}.", wrapper); showUnfilteredEvent(); } } } private void showPopup(MouseEvent evt) { Point p = evt.getPoint(); EventWrapper<T> wrapper = getEventWrapper(p); // To ensure that the event below the mouse is selected. if(logger.isDebugEnabled()) logger.debug("Show popup at {} for event {}.", p, wrapper); EventWrapperViewPanel.this.showPopup(table, p); } public void mousePressed(MouseEvent evt) { if(evt.isPopupTrigger()) { showPopup(evt); } } public void mouseReleased(MouseEvent evt) { if(evt.isPopupTrigger()) { showPopup(evt); } } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } } private class EventViewMouseListener implements MouseListener { EventViewMouseListener() { } private void showPopup(MouseEvent evt) { Point p = evt.getPoint(); EventWrapperViewPanel.this.showPopup(messagePane, p); } public void mouseClicked(MouseEvent evt) { messagePane.requestFocusInWindow(); if(evt.isPopupTrigger()) { showPopup(evt); } } public void mousePressed(MouseEvent evt) { if(evt.isPopupTrigger()) { showPopup(evt); } } public void mouseReleased(MouseEvent evt) { if(evt.isPopupTrigger()) { showPopup(evt); } } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } } private class ScrollPaneMouseListener implements MouseListener { ScrollPaneMouseListener() { } private void showPopup(MouseEvent evt) { Point p = evt.getPoint(); EventWrapperViewPanel.this.showPopup(tableScrollPane, p); } public void mouseClicked(MouseEvent evt) { tableScrollPane.requestFocusInWindow(); if(evt.isPopupTrigger()) { showPopup(evt); } } public void mousePressed(MouseEvent evt) { if(evt.isPopupTrigger()) { showPopup(evt); } } public void mouseReleased(MouseEvent evt) { if(evt.isPopupTrigger()) { showPopup(evt); } } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } } private class TableRowSelectionListener implements ListSelectionListener { private final Logger logger = LoggerFactory.getLogger(TableRowSelectionListener.class); public void valueChanged(ListSelectionEvent e) { int row = getSelectedRow(); if(logger.isDebugEnabled()) logger.debug("Selected row: {}.", row); if(row >= 0) { EventWrapper<T> event = tableModel.getValueAt(row); setSelectedEvent(event); initMessage(event); } else { setSelectedEvent(null); resetMessage(); } } } /** * Disables "Scroll to Bottom" in case of adjusting, i.e. if the scroll bar is dragged. */ private class ScrollBarChangeListener implements ChangeListener { private final Logger logger = LoggerFactory.getLogger(ScrollBarChangeListener.class); public void stateChanged(ChangeEvent evt) { if(logger.isDebugEnabled()) logger.debug("changeEvent: {}", evt); if(isScrollingToBottom()) { if(verticalLogScrollBar.getModel().getValueIsAdjusting()) { setScrollingToBottom(false); } } } } void focusTable() { table.requestFocusInWindow(); } void focusMessagePane() { messagePane.requestFocusInWindow(); } class EventWrapperViewChangeListener implements PropertyChangeListener { final Logger logger = LoggerFactory.getLogger(EventWrapperViewChangeListener.class); public void propertyChange(PropertyChangeEvent event) { String propertyName = event.getPropertyName(); if(EventWrapperViewTable.SCROLLING_TO_BOTTOM_PROPERTY.equals(propertyName)) { Object oldValue = event.getOldValue(); Object newValue = event.getNewValue(); firePropertyChange(SCROLLING_TO_BOTTOM_PROPERTY, oldValue, newValue); } } } class FindPanelChangeListener implements PropertyChangeListener { final Logger logger = LoggerFactory.getLogger(FindPanelChangeListener.class); public void propertyChange(PropertyChangeEvent event) { String propertyName = event.getPropertyName(); if(FindPanel.CONDITION_PROPERTY.equals(propertyName)) { applyFilter(); } } } private void executeFind(Callable<Long> callable, String name, int currentRow, Condition condition) { ViewContainer<T> container = resolveContainer(); if(container != null) { if(container.isSearching()) { return; // prevent scheduling of multiple searches... } } Map<String, String> metaData = CallableMetaData.createFindMetaData(condition, eventSource, currentRow); String description = "Executing '" + name + "' on " + metaData.get(CallableMetaData.FIND_TASK_META_SOURCE_IDENTIFIER) + " starting at row " + currentRow + ".\n\n" + metaData.get(CallableMetaData.FIND_TASK_META_CONDITION); findResultListener.setCallable(callable); Task<Long> task = taskManager.startTask(callable, name, description, metaData); if(container != null) { container.showSearchPanel(task); } } private class FindResultListener implements TaskListener<Long> { private Callable<Long> callable; public void taskCreated(Task<Long> longTask) { } public void executionFailed(Task<Long> task, ExecutionException exception) { if(logger.isDebugEnabled()) { logger.debug("in executionFailed:\n task: {}\nthis.callable: {}", task, this.callable); } if(this.callable == task.getCallable()) { if(logger.isInfoEnabled()) logger.info("Find execution failed!", exception); finished(); } } public void executionFinished(Task<Long> task, Long result) { if(logger.isDebugEnabled()) { logger.debug("in executionFinished:\n task: {}\nthis.callable: {}", task, this.callable); } if(this.callable == task.getCallable()) { if(logger.isInfoEnabled()) logger.info("Find execution finished: {}!", result); if(result != null && result >= 0) { int row = result.intValue(); // this will always work setSelectedRow(row); } finished(); } } public void executionCanceled(Task<Long> task) { if(logger.isDebugEnabled()) { logger.debug("in executionCanceled:\n task: {}\nthis.callable: {}", task, this.callable); } if(this.callable == task.getCallable()) { if(logger.isInfoEnabled()) logger.info("Find execution canceled."); finished(); } } public void progressUpdated(Task<Long> task, int progress) { if(logger.isDebugEnabled()) { logger .debug("in progressUpdated:\task: {}\n this.callable: {}", task, this.callable); } if(this.callable == task.getCallable()) { // to catch updates after cancel. if(logger.isDebugEnabled()) logger.debug("Progress update: {}", progress); ViewContainer<T> container = resolveContainer(); if(container != null) { ProgressGlassPane progressPanel = container.getProgressPanel(); progressPanel.setProgress(progress); } } } private void finished() { if(logger.isDebugEnabled()) logger.debug("Executing FindResultListener.finished()."); ViewContainer<T> container = resolveContainer(); if(container != null) { ProgressGlassPane progressPanel = container.getProgressPanel(); progressPanel.getFindCancelAction().setTask(null); setCallable(null); container.hideSearchPanel(); } } void setCallable(Callable<Long> callable) { if(logger.isDebugEnabled()) { //noinspection ThrowableInstanceNeverThrown logger.debug("Setting task...\n newCallable: " + callable + "\npreviousCallable: " + this.callable, new Throwable()); } this.callable = callable; } } protected abstract void closeConnection(SourceIdentifier sourceIdentifier); /** * Returns a new combined condition of this view and the current condition of its table if it differs and the table has a condition. * Otherwise, null is returned. * * @return the combined condition */ Condition resolveCombinedCondition() { Condition currentFilter = getTable().getFilterCondition(); if (currentFilter == null) { return null; } Condition originalBufferCondition = getBufferCondition(); Condition filter = getCombinedCondition(currentFilter); if (filter == null || filter.equals(originalBufferCondition)) { return null; } return filter; } private class StatusTableModelListener implements TableModelListener { public void tableChanged(TableModelEvent e) { if(logger.isDebugEnabled()) logger.debug("TableModelEvent: {}", e); updateStatusText(); } } private class SplitPaneListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { if(logger.isDebugEnabled()) logger.debug("SplitPane change!"); String propertyName = evt.getPropertyName(); if("dividerLocation".equals(propertyName)) { scrollToEvent(); } } } private class MessageFocusListener implements FocusListener { public void focusGained(FocusEvent e) { messagePane.setBorder(focusedBorder); } public void focusLost(FocusEvent e) { messagePane.setBorder(unfocusedBorder); } } private class WrappingMouseWheelListener implements MouseWheelListener { private MouseWheelListener[] wrapped; private WrappingMouseWheelListener(MouseWheelListener[] wrapped) { this.wrapped = wrapped; } public void mouseWheelMoved(MouseWheelEvent e) { //noinspection MagicConstant if(e.getModifiers() == KeyStrokes.COMMAND_KEYMASK) { // special handling, i.e. zoom in, zoom out int rotation = e.getWheelRotation(); boolean up = false; if(rotation < 0) { up = true; rotation = -rotation; } for(int i = 0; i < rotation; i++) { if(up) { mainFrame.zoomIn(); } else { mainFrame.zoomOut(); } } } else { if(wrapped != null) { for(MouseWheelListener current : wrapped) { current.mouseWheelMoved(e); } } } } } }