/*******************************************************************************
* Copyright 2011 Google Inc. All Rights Reserved.
*
* 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
*
* 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.
*******************************************************************************/
package com.google.gwt.eclipse.oophm.model;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.eclipse.oophm.LogSniffer;
import com.google.gwt.eclipse.oophm.model.BrowserTab.ModuleHandle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* A log entry. Log entries can be nested.
*
* This class is thread-safe.
*
* @param <T> the entity associated with the log entry({@link BrowserTab} or
* {@link Server})
*/
public class LogEntry<T extends IModelNode> {
/**
* Detailed information associated with a log entry.
*
* TODO: This class is very similar to the LogData protobuf class. Consider
* some sort of unification.
*/
public static class Data {
private static String returnDefaultIfEmpty(String str, String defaultVal) {
if (str == null || str.length() == 0) {
return defaultVal;
}
return str;
}
private String attentionLevel;
private final String details;
private final String helpInfoText;
private final String helpInfoURL;
private final String label;
private final String logLevel;
private final boolean needsAttention;
private final long timestamp;
/**
* Create a new instance.
*
* @param label the label for the log entry
* @param details any details (such as a stack trace) associated with the
* entry
* @param logLevel the level (ERROR, WARN, etc..) for the log entry
* @param helpInfoURL a URL that points to helpful information associated
* with the log entry
* @param helpInfoText some helpful text associated with the log entry
* @param timestamp the time that this entry was created
*/
public Data(String label, String details, String logLevel,
String helpInfoURL, String helpInfoText, long timestamp,
boolean needsAttention) {
this.label = returnDefaultIfEmpty(label, "Unknown");
this.details = returnDefaultIfEmpty(details, "");
this.logLevel = returnDefaultIfEmpty(logLevel, "INFO");
this.helpInfoText = returnDefaultIfEmpty(helpInfoText, "");
this.helpInfoURL = returnDefaultIfEmpty(helpInfoURL, "");
this.timestamp = timestamp;
this.needsAttention = needsAttention;
}
/**
* Returns the max log level of a child node who needs attention or
* <code>null</code> if there isn't one.
*/
public String getAttentionLevel() {
return attentionLevel;
}
public String getDetails() {
return details;
}
public String getHelpInfoText() {
return helpInfoText;
}
public String getHelpInfoURL() {
return helpInfoURL;
}
public String getLabel() {
return label;
}
public String getLogLevel() {
return logLevel;
}
public boolean getNeedsAttention() {
return needsAttention;
}
public long getTimestamp() {
return timestamp;
}
public void setAttentionLevel(String attentionLevel) {
this.attentionLevel = attentionLevel;
}
}
enum DisclosureFilter {
DISCLOSED {
@Override
public boolean matches(LogEntry<?> logEntry) {
return logEntry.isDisclosed();
}
},
/**
* No entries excluded.
*/
NONE {
@Override
public boolean matches(LogEntry<?> logEntry) {
return true;
}
},
UNDISCLOSED {
@Override
public boolean matches(LogEntry<?> logEntry) {
return !logEntry.isDisclosed();
}
};
public abstract boolean matches(LogEntry<?> logEntry);
}
/**
* A comparator that can be used to compare two different log entries. The log
* entries must have the same parent, and they must not be children of a root
* log entry.
*/
private static final Comparator<LogEntry<?>> NON_ROOT_LOG_ENTRY_COMPARATOR = new Comparator<LogEntry<?>>() {
public int compare(LogEntry<?> o1, LogEntry<?> o2) {
if (o1 == null || o2 == null) {
throw new IllegalArgumentException("Cannot compare null objects.");
}
if (o1 == o2) {
return 0;
}
if (o1.getParent() != o2.getParent()) {
throw new IllegalArgumentException(
"Cannot compare log entries that have different parents.");
}
if ((o1.getParent() == o1.getLog().getRootLogEntry())) {
throw new IllegalArgumentException(
"Cannot compare log entries that are direct children of a root log entry.");
}
return o1.index - o2.index;
}
};
/**
* Returns the {@link TreeLogger.Type} enum value corresponding to the
* <code>treeLoggerTypeName</code> or null if there isn't one.
*/
public static TreeLogger.Type toTreeLoggerType(String treeLoggerTypeName) {
assert (treeLoggerTypeName != null);
try {
return TreeLogger.Type.valueOf(treeLoggerTypeName);
} catch (IllegalArgumentException e) {
// Ignored
}
return null;
}
private final List<LogEntry<T>> children = new ArrayList<LogEntry<T>>();
private final ModuleHandle moduleHandle;
private boolean disclosed = true;
private final int index;
private Log<T> log = null;
private final Data logData;
private LogEntry<T> parent = null;
/**
* Create a log entry.
*
* @param logData detailed information for this log entry
* @param index the index of the log entry among the parent entry's children
* @param moduleHandle the handle to the module that created this log entry
*/
public LogEntry(Data logData, int index, ModuleHandle moduleHandle) {
this.moduleHandle = moduleHandle;
this.logData = logData;
this.index = index;
}
/**
* Add a log entry as a child of this entry. Fires an event to all listeners
* of the log associated with this entry. May also set the attention level on
* the entity associated with this entry's {@link Log}, which in turn will
* cause events to be fired. See
* {@link INeedsAttention#setNeedsAttentionLevel(String)}.
*/
public void addChild(LogEntry<T> child) {
boolean shouldUpdateParents = false;
boolean childNeedsAttention = false;
int insertionIndex;
// Only fires the event once the lock has been released. Holding on to
// the lock while firing the event may lead to deadlock.
synchronized (log.instanceLock) {
child.setLog(log);
child.setParent(this);
if (getLog().getRootLogEntry() != this) {
insertionIndex = insertIntoNonRootEntry(child);
} else {
insertionIndex = insertIntoRootEntry(child);
}
LogSniffer.log("inserted at index {0,number,#}", insertionIndex);
if (insertionIndex == -1) {
return;
}
Data childLogData = child.getLogData();
childNeedsAttention = childLogData.getNeedsAttention();
LogEntry<T> parent = this;
while (parent != null) {
if (!parent.isDisclosed()) {
// Force the parent to be disclosed since it received a new child
parent.setDisclosed(true);
shouldUpdateParents = true;
}
if (childNeedsAttention) {
shouldUpdateParents = true;
Data parentLogData = parent.getLogData();
if (parentLogData != null) {
if (shouldPropageLogLevelToParent(parentLogData, childLogData)) {
parentLogData.setAttentionLevel(childLogData.getLogLevel());
}
} else {
// Looking at the root
}
}
parent = parent.getParent();
}
}
log.fireNewEntryAdded(insertionIndex, child, childNeedsAttention,
shouldUpdateParents);
if (childNeedsAttention) {
// TODO: Improve type safety
T entity = log.getEntity();
if (entity instanceof INeedsAttention) {
((INeedsAttention) entity).setNeedsAttentionLevel(child.getLogData().getLogLevel());
}
}
}
/**
* Returns all children of this node including undisclosed entries.
*/
public List<LogEntry<T>> getAllChildren() {
return getFilteredChildren(DisclosureFilter.NONE);
}
/**
* Returns children that are disclosed by the model.
*/
public List<LogEntry<T>> getDisclosedChildren() {
return getFilteredChildren(DisclosureFilter.DISCLOSED);
}
/**
* Return the log associated with this entry.
*/
public Log<T> getLog() {
return log;
}
/**
* Return the log entry's data.
*/
public Data getLogData() {
return logData;
}
/**
* Return the name of the module that generated this log entry.
*/
public ModuleHandle getModuleHandle() {
return moduleHandle;
}
/**
* Return the parent of this entry, or <code>null
* </code> if this is a top-level log
* entry.
*/
public LogEntry<T> getParent() {
return parent;
}
/**
* Returns the disclosure state of this element.
*/
public boolean isDisclosed() {
return disclosed;
}
/**
* Sets the disclosure state of this element.
*/
public void setDisclosed(boolean disclosed) {
this.disclosed = disclosed;
}
@Override
public String toString() {
String logLevel;
String label;
if (logData != null) {
label = logData.getLabel();
logLevel = logData.getLogLevel();
} else {
label = "Root LogEntry";
logLevel = "Root entry has no log level";
}
return String.format("[%1$s] [%3$s] - %2$s", logLevel, label,
moduleHandle.getName());
}
void setLog(Log<T> log) {
this.log = log;
}
void setParent(LogEntry<T> parent) {
this.parent = parent;
}
/**
* Returns a list of children excluding the elements according to the
* {@link DisclosureFilter}.
*/
private List<LogEntry<T>> getFilteredChildren(
DisclosureFilter disclosureFilter) {
synchronized (log.instanceLock) {
List<LogEntry<T>> filteredChildren = new ArrayList<LogEntry<T>>();
for (LogEntry<T> logEntry : children) {
if (disclosureFilter.matches(logEntry)) {
filteredChildren.add(logEntry);
}
}
return filteredChildren;
}
}
/**
* Insert a new child of a non-root log entry. In order to call this method,
* this entry must NOT be the root log entry.
*
* The log's instance lock should be held for the duration of this method.
*
* @param newChild the child to insert
* @return the position where the child was inserted among the entry's
* children, or -1 if this entry is already in the set of children
*/
private int insertIntoNonRootEntry(LogEntry<T> newChild) {
assert (this != this.getLog().getRootLogEntry());
/*
* At the non-root level, the children are all from the same module logger.
* It is safe to use a binary insertion algorithm that compares the entry
* indexes.
*/
int insertionIndex = Collections.binarySearch(children, newChild,
NON_ROOT_LOG_ENTRY_COMPARATOR);
if (insertionIndex >= 0) {
// This child is already in the set of children...
return -1;
}
// Convert the index into the real insertion index and add the child
insertionIndex = -(insertionIndex + 1);
children.add(insertionIndex, newChild);
return insertionIndex;
}
/**
* Insert a new child of a root log entry. In order to call this method, this
* entry must be the root log entry.
*
* The log's instance lock should be held for the duration of this method.
*
* @param newChild the child to insert
* @return the position where the child was inserted among the root entry's
* children, or -1 if this entry is already in the set of children
*/
private int insertIntoRootEntry(LogEntry<T> newChild) {
assert (this == this.getLog().getRootLogEntry());
assert (newChild.getModuleHandle() != null);
/*
* This insertion algorithm differs from the non-root case because the root
* log entry may have children from different module loggers. As a result,
* their indexes can only be interpreted in the context of the module that
* generated them.
*/
int insertionIndex = children.size();
for (int i = children.size() - 1; i > -1; i--) {
LogEntry<T> curChild = children.get(i);
if (newChild.getModuleHandle() == curChild.getModuleHandle()) {
if (newChild.index > curChild.index) {
insertionIndex = i + 1;
break;
} else if (newChild.index == curChild.index) {
// This child is already in the set of children
insertionIndex = -1;
break;
}
}
}
if (insertionIndex != -1) {
children.add(insertionIndex, newChild);
}
return insertionIndex;
}
private boolean shouldPropageLogLevelToParent(Data parentLogData,
Data childLogData) {
TreeLogger.Type parentLogLevel = toTreeLoggerType(parentLogData.logLevel);
TreeLogger.Type childLogLevel = toTreeLoggerType(childLogData.logLevel);
return parentLogLevel == null || childLogLevel == null
|| parentLogLevel.isLowerPriorityThan(childLogLevel);
}
}