/*******************************************************************************
* Copyright (c) 2014 Salesforce.com, inc..
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Salesforce.com, inc. - initial API and implementation
******************************************************************************/
package com.salesforce.ide.ui.views.log;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.apache.log4j.Logger;
import org.eclipse.core.runtime.ILogListener;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.viewers.ITableFontProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IViewReference;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchPartReference;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.dialogs.FilteredTree;
import org.eclipse.ui.dialogs.PatternFilter;
import org.eclipse.ui.part.ViewPart;
import com.salesforce.ide.core.internal.utils.Constants;
import com.salesforce.ide.core.internal.utils.Utils;
import com.salesforce.ide.ui.internal.ForceImages;
import com.salesforce.ide.ui.internal.utils.UIMessages;
import com.salesforce.ide.ui.internal.utils.UIUtils;
/**
* Displays Force.com IDE logs found in the Platform log file
*
* @author cwall
*
*/
public class LogView extends ViewPart implements ILogListener {
private static final Logger logger = Logger.getLogger(LogView.class);
protected static final String EXCEPTION = "Exception";
protected static final String OPEN_LOG_EXCEPTION = "(Open log file for full message and/or stacktrace)";
protected static final String OPEN_LOG_EXCEPTION_FULL = "... " + OPEN_LOG_EXCEPTION;
protected static final String OPEN_LOG_FULL = "... (Open log file for full message)";
protected static final int MAX_LABEL_LENGTH = 200;
protected static final byte SEVERITY = 0x0;
protected static final byte MESSAGE = 0x1;
protected static final byte DATE = 0x2;
protected static final int ASCENDING = 1;
protected static final int DESCENDING = -1;
private static final int MAX_LOG_VIEW_ENTRIES = 500;
private int severityOrder;
private int messageOrder;
private int dateOrder;
private List<LogEntry> logEntries = null;
private List<LogEntry> batchedLogEntries = null;
private boolean batchEntries;
private File logFile = null;
private Comparator<? extends LogEntry> logComparator = null;
private boolean firstEvent = true;
private TreeColumn logSeverityColumn = null;
private TreeColumn logMessageColumn = null;
private TreeColumn logDateColumn = null;
private Tree logTree = null;
private FilteredTree logFilteredTree = null;
private final LogViewLabelProvider logViewLabelProvider = new LogViewLabelProvider();
private Action openLogFileAction = null;
private Action openLogFolderAction = null;
private LogView instance = null;
public LogView() {
this( Platform.getLogFileLocation().toFile());
}
public LogView(File logFile) {
logEntries = new ArrayList<LogEntry>();
batchedLogEntries = new ArrayList<LogEntry>();
this.logFile = logFile;
// maintain instance of self so that the perspective listener focus on the particular log view instance
instance = this;
}
protected File getLogFile() {
return logFile;
}
public boolean isBatchEntries() {
return batchEntries;
}
public List<LogEntry> getLogEntries() {
return logEntries;
}
public List<LogEntry> getBatchedLogEntries() {
return batchedLogEntries;
}
public int getBatchedLogEntriesCount() {
return Utils.isNotEmpty(batchedLogEntries) ? batchedLogEntries.size() : 0;
}
public TreeColumn getLogMessageColumn() {
return logMessageColumn;
}
public TreeColumn getLogDateColumn() {
return logDateColumn;
}
public TreeColumn getLogSeverityColumn() {
return logSeverityColumn;
}
public void setLogSeverityColumn(TreeColumn logSeverityColumn) {
this.logSeverityColumn = logSeverityColumn;
}
public Tree getLogTree() {
return logTree;
}
public FilteredTree getLogFilteredTree() {
return logFilteredTree;
}
public LogViewLabelProvider getLogViewLabelProvider() {
return logViewLabelProvider;
}
public Action getOpenLogFileAction() {
return openLogFileAction;
}
public Action getOpenLogFolderAction() {
return openLogFolderAction;
}
@Override
public void createPartControl(Composite parent) {
Composite composite = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout();
layout.horizontalSpacing = 0;
layout.verticalSpacing = 0;
layout.marginWidth = 0;
layout.marginHeight = 0;
composite.setLayout(layout);
readLogFile();
initViewer(composite);
initActions();
initializeViewerSorter();
getSite().setSelectionProvider(logFilteredTree.getViewer());
logTree.setToolTipText("View Force.com IDE Log");
Platform.addLogListener(this);
// batch entries when view is hidden; display batched entries when view is visible
getSite().getPage().addPartListener(new org.eclipse.ui.IPartListener2() {
public void partHidden(IWorkbenchPartReference partRef) {
if (!(partRef instanceof IViewReference)) {
return;
}
IWorkbenchPart part = partRef.getPart(false);
if (part == null || !part.equals(instance)) {
return;
}
batchEntries = true;
}
public void partVisible(IWorkbenchPartReference partRef) {
if (!(partRef instanceof IViewReference)) {
return;
}
IWorkbenchPart part = partRef.getPart(false);
if (part == null || !part.equals(instance)) {
return;
}
if (Utils.isNotEmpty(batchedLogEntries)) {
displayBatchedEntries();
}
batchEntries = false;
}
public void partActivated(IWorkbenchPartReference partRef) {}
public void partDeactivated(IWorkbenchPartReference partRef) {}
public void partBroughtToTop(IWorkbenchPartReference partRef) {}
public void partInputChanged(IWorkbenchPartReference partRef) {}
public void partOpened(IWorkbenchPartReference partRef) {}
public void partClosed(IWorkbenchPartReference partRef) {}
});
UIUtils.setHelpContext(logFilteredTree, this.getClass().getSimpleName());
}
private void initActions() {
IActionBars bars = getViewSite().getActionBars();
IToolBarManager toolBarManager = bars.getToolBarManager();
// init open file and folder actions
openLogFileAction = new OpenLogFileAction();
toolBarManager.add(openLogFileAction);
openLogFolderAction = new OpenLogFolderAction();
toolBarManager.add(openLogFolderAction);
}
private void initViewer(Composite parent) {
logFilteredTree = new FilteredTree(parent, SWT.FULL_SELECTION, new PatternFilter() {
@Override
protected boolean isLeafMatch(Viewer viewer, Object element) {
if (element instanceof LogEntry) {
LogEntry logEntry = (LogEntry) element;
String message = logEntry.getMessage();
String date = logEntry.getFormattedDate();
return wordMatches(message) || wordMatches(date);
}
return false;
}
});
logFilteredTree.setInitialText(UIMessages.getString("FilterInitialText"));
logTree = logFilteredTree.getViewer().getTree();
logTree.setLinesVisible(true);
logSeverityColumn = new TreeColumn(logTree, SWT.LEFT);
logSeverityColumn.setText("Severity");
logSeverityColumn.setWidth(70);
logSeverityColumn.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
severityOrder *= -1;
ViewerComparator comparator = getViewerComparator(SEVERITY);
logFilteredTree.getViewer().setComparator(comparator);
setComparator(SEVERITY);
setColumnSorting(logSeverityColumn, severityOrder);
}
});
logMessageColumn = new TreeColumn(logTree, SWT.LEFT);
logMessageColumn.setText("Message");
logMessageColumn.setWidth(500);
logMessageColumn.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
messageOrder *= -1;
ViewerComparator comparator = getViewerComparator(MESSAGE);
logFilteredTree.getViewer().setComparator(comparator);
setComparator(MESSAGE);
setColumnSorting(logMessageColumn, messageOrder);
}
});
logDateColumn = new TreeColumn(logTree, SWT.LEFT);
logDateColumn.setText("Date");
logDateColumn.setWidth(75);
logDateColumn.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
dateOrder *= -1;
ViewerComparator comparator = getViewerComparator(DATE);
logFilteredTree.getViewer().setComparator(comparator);
setComparator(DATE);
setColumnSorting(logDateColumn, dateOrder);
}
});
logTree.setHeaderVisible(true);
logFilteredTree.getViewer().setContentProvider(new LogViewContentProvider(this));
logFilteredTree.getViewer().setLabelProvider(logViewLabelProvider);
logFilteredTree.getViewer().setInput(this);
}
private void initializeViewerSorter() {
byte orderType = DATE;
ViewerComparator comparator = getViewerComparator(orderType);
logFilteredTree.getViewer().setComparator(comparator);
if (orderType == MESSAGE) {
setColumnSorting(logMessageColumn, messageOrder);
} else if (orderType == DATE) {
setColumnSorting(logDateColumn, dateOrder);
}
}
private void setColumnSorting(TreeColumn column, int order) {
logTree.setSortColumn(column);
logTree.setSortDirection(order == ASCENDING ? SWT.UP : SWT.DOWN);
}
@Override
public void dispose() {
Platform.removeLogListener(instance);
logViewLabelProvider.dispose();
logFilteredTree.dispose();
super.dispose();
}
public LogEntry[] getElements() {
return logEntries.toArray(new LogEntry[logEntries.size()]);
}
private void readLogFile() {
logEntries.clear();
try {
// parse log file and create log entries
List<LogEntry> result = new ArrayList<LogEntry>();
LogReader.parseLogFile(logFile, result);
logEntries.addAll(result);
limitEntriesCount();
} catch (Exception e) {
logger.error("Unable to read Force.com IDE log file", e);
Utils.openError(e, true, "Unable to read Force.com IDE log file");
}
}
private void limitEntriesCount() {
int entriesCount = getEntriesCount();
if (entriesCount <= MAX_LOG_VIEW_ENTRIES) {
return;
}
logEntries.subList(0, logEntries.size() - MAX_LOG_VIEW_ENTRIES).clear();
}
private int getEntriesCount() {
return logEntries.size();
}
public void logging(IStatus status, String plugin) {
// skip non-Force.com log events
if (Utils.isEmpty(status.getPlugin()) || !status.getPlugin().contains(Constants.FORCE_PLUGIN_PREFIX)) {
return;
}
if (batchEntries) {
LogEntry entry = new LogEntry(status);
batchedLogEntries.add(entry);
return;
}
if (firstEvent) {
readLogFile();
asyncRefresh(true);
firstEvent = false;
} else {
LogEntry entry = new LogEntry(status);
if (!batchedLogEntries.isEmpty()) {
batchedLogEntries.add(entry);
displayBatchedEntries();
} else {
pushEntry(entry);
}
}
}
private void displayBatchedEntries() {
Job job = new Job("Batching Force.com IDE log entries...") {
@Override
protected IStatus run(IProgressMonitor monitor) {
for (int i = 0; i < batchedLogEntries.size(); i++) {
if (!monitor.isCanceled()) {
LogEntry entry = batchedLogEntries.get(i);
pushEntry(entry);
batchedLogEntries.remove(i);
}
}
if (batchedLogEntries.size() > 0) {
batchedLogEntries.clear();
}
asyncRefresh(true);
return Status.OK_STATUS;
}
};
job.schedule();
}
private synchronized void pushEntry(LogEntry entry) {
logEntries.addAll(Collections.singletonList(entry));
limitEntriesCount();
asyncRefresh(true);
}
private void asyncRefresh(final boolean activate) {
if (logTree.isDisposed()) {
return;
}
Display display = logTree.getDisplay();
if (display != null) {
display.asyncExec(new Runnable() {
public void run() {
if (!logTree.isDisposed()) {
TreeViewer viewer = logFilteredTree.getViewer();
viewer.refresh();
}
}
});
}
}
@Override
public void setFocus() {
if (logFilteredTree != null && !logFilteredTree.isDisposed()) {
logFilteredTree.setFocus();
}
}
@Override
public void init(IViewSite site) throws PartInitException {
super.init(site);
dateOrder = DESCENDING;
messageOrder = DESCENDING;
severityOrder = DESCENDING;
setComparator(DATE);
}
public Comparator<? extends LogEntry> getComparator() {
return logComparator;
}
private void setComparator(byte sortType) {
if (sortType == DATE) {
logComparator = new Comparator<LogEntry>() {
public int compare(LogEntry entry1, LogEntry entry2) {
long date1 = (entry1).getDate().getTime();
long date2 = (entry2).getDate().getTime();
if (date1 == date2) {
int result = logEntries.indexOf(entry2) - logEntries.indexOf(entry1);
if (dateOrder == DESCENDING) {
result *= DESCENDING;
}
return result;
}
if (dateOrder == DESCENDING) {
return date1 > date2 ? DESCENDING : ASCENDING;
}
return date1 < date2 ? DESCENDING : ASCENDING;
}
};
} else if (sortType == MESSAGE) {
logComparator = new Comparator<LogEntry>() {
public int compare(LogEntry entry1, LogEntry entry2) {
return String.CASE_INSENSITIVE_ORDER.compare(entry1.getMessage(true), entry2.getMessage(true))
* messageOrder;
}
};
} else {
logComparator = new Comparator<LogEntry>() {
public int compare(LogEntry entry1, LogEntry entry2) {
if (entry1.getSeverity() == entry2.getSeverity()) {
return 0;
} else if (entry1.getSeverity() > entry2.getSeverity()) {
return 1 * severityOrder;
} else {
return -1 * severityOrder;
}
}
};
}
}
private ViewerComparator getViewerComparator(byte sortType) {
if (sortType == SEVERITY) {
return new ViewerComparator() {
@Override
public int compare(Viewer viewer, Object e1, Object e2) {
if ((e1 instanceof LogEntry) && (e2 instanceof LogEntry)) {
LogEntry entry1 = (LogEntry) e1;
LogEntry entry2 = (LogEntry) e2;
if (entry1.getSeverity() == entry2.getSeverity()) {
return 0;
} else if (entry1.getSeverity() > entry2.getSeverity()) {
return 1 * severityOrder;
} else {
return -1 * severityOrder;
}
}
return 0;
}
};
} else if (sortType == MESSAGE) {
return new ViewerComparator() {
@Override
public int compare(Viewer viewer, Object e1, Object e2) {
if ((e1 instanceof LogEntry) && (e2 instanceof LogEntry)) {
LogEntry entry1 = (LogEntry) e1;
LogEntry entry2 = (LogEntry) e2;
return String.CASE_INSENSITIVE_ORDER.compare(entry1.getMessage(true), entry2.getMessage(true))
* messageOrder;
}
return 0;
}
};
} else {
return new ViewerComparator() {
@Override
public int compare(Viewer viewer, Object e1, Object e2) {
long date1 = 0;
long date2 = 0;
if ((e1 instanceof LogEntry) && (e2 instanceof LogEntry)) {
date1 = ((LogEntry) e1).getDate().getTime();
date2 = ((LogEntry) e2).getDate().getTime();
}
if (date1 == date2) {
int result = logEntries.indexOf(e2) - logEntries.indexOf(e1);
if (dateOrder == DESCENDING)
result *= DESCENDING;
return result;
}
if (dateOrder == DESCENDING)
return date1 > date2 ? DESCENDING : ASCENDING;
return date1 < date2 ? DESCENDING : ASCENDING;
}
};
}
}
public void sortByDateDescending() {
setColumnSorting(logDateColumn, DESCENDING);
}
// content and label providers
class LogViewContentProvider implements ITreeContentProvider {
private LogView logView = null;
public LogViewContentProvider(LogView logView) {
this.logView = logView;
}
public void dispose() {}
public Object[] getChildren(Object element) {
return ((LogEntry) element).getChildren(element);
}
public Object[] getElements(Object element) {
return logView.getElements();
}
public Object getParent(Object element) {
return ((LogEntry) element).getParent(element);
}
public boolean hasChildren(Object element) {
return ((LogEntry) element).getChildren(element).length > 0;
}
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {}
public boolean isDeleted(Object element) {
return false;
}
}
class LogViewLabelProvider extends LabelProvider implements ITableLabelProvider, ITableFontProvider {
public LogViewLabelProvider() {}
@Override
public void dispose() {
super.dispose();
}
public Image getColumnImage(Object element, int columnIndex) {
LogEntry entry = (LogEntry) element;
if (columnIndex == 0) {
switch (entry.getSeverity()) {
case IStatus.INFO:
return ForceImages.get(ForceImages.INFO_ICON);
case IStatus.OK:
return ForceImages.get(ForceImages.DEBUG_ICON);
case IStatus.WARNING:
return ForceImages.get(ForceImages.WARNING_ICON);
case LogEntry.TRACE_SEVERITY:
return ForceImages.get(ForceImages.TRACE_ICON);
default:
return ForceImages.get(ForceImages.ERROR_ICON);
}
}
return null;
}
public String getColumnText(Object element, int columnIndex) {
if (!(element instanceof LogEntry)) {
return "Log statement not found";
}
LogEntry entry = (LogEntry) element;
switch (columnIndex) {
case 1:
String message = entry.getMessage(true);
if (Utils.isNotEmpty(message)) {
message = message.trim();
if (entry.getSeverity() == IStatus.WARNING || entry.getSeverity() == IStatus.ERROR) {
return getExceptionMessage(message, entry);
} else if (message.length() > MAX_LABEL_LENGTH) {
return getTruncatedMessage(message, OPEN_LOG_FULL);
} else {
return message;
}
}
return "No message found";
case 2:
return new SimpleDateFormat(Constants.STANDARD_DATE_FORMAT).format(entry.getDate());
default:
return Constants.EMPTY_STRING;
}
}
public String getExceptionMessage(String message, LogEntry entry) {
if (Utils.isNotEmpty(entry.getStack()) && entry.getStack().contains(EXCEPTION)) {
String exception =
entry.getStack().substring(0, entry.getStack().indexOf(EXCEPTION) + EXCEPTION.length());
if (exception.contains(Constants.DOT)) {
exception = exception.substring(exception.lastIndexOf(Constants.DOT) + 1);
}
StringBuffer sb = new StringBuffer("(");
sb.append(exception).append(") ");
message = sb.toString() + message;
}
if (message.length() > MAX_LABEL_LENGTH
|| (message.length() + OPEN_LOG_EXCEPTION_FULL.length()) > MAX_LABEL_LENGTH) {
message = getTruncatedMessage(message, OPEN_LOG_EXCEPTION_FULL);
} else {
message += " "+OPEN_LOG_EXCEPTION;
}
return message;
}
private String getTruncatedMessage(String message, String truncateStr) {
if (Utils.isEmpty(message) || message.length() < MAX_LABEL_LENGTH) {
return message;
}
StringBuffer sb = new StringBuffer(message.substring(0, MAX_LABEL_LENGTH - truncateStr.length()));
sb.append(truncateStr);
return sb.toString();
}
public Font getFont(Object element, int columnIndex) {
return null;
}
}
}