001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.dev.util.log;
017:
018: import com.google.gwt.core.ext.TreeLogger;
019:
020: import org.eclipse.swt.SWT;
021: import org.eclipse.swt.graphics.Color;
022: import org.eclipse.swt.graphics.Image;
023: import org.eclipse.swt.widgets.Display;
024: import org.eclipse.swt.widgets.Tree;
025: import org.eclipse.swt.widgets.TreeItem;
026:
027: import java.io.IOException;
028: import java.io.InputStream;
029: import java.util.Iterator;
030: import java.util.LinkedList;
031: import java.util.List;
032: import java.util.Stack;
033:
034: /**
035: * Tree logger built on an SWT tree item.
036: */
037: public final class TreeItemLogger extends AbstractTreeLogger {
038:
039: /**
040: * Represents an individual log event.
041: */
042: public static class LogEvent {
043: public final Throwable caught;
044:
045: public final int index;
046:
047: public final boolean isBranchCommit;
048:
049: public final TreeItemLogger logger;
050:
051: public final String message;
052:
053: public final TreeLogger.Type type;
054:
055: public LogEvent(TreeItemLogger logger, boolean isBranchCommit,
056: int index, Type type, String message, Throwable caught) {
057: this .logger = logger;
058: this .isBranchCommit = isBranchCommit;
059: this .index = index;
060: this .type = type;
061: this .message = message;
062: this .caught = caught;
063: }
064:
065: @Override
066: public String toString() {
067: String s = "";
068: s += "[logger " + logger.toString();
069: s += ", " + (isBranchCommit ? "BRANCH" : "LOG");
070: s += ", index " + index;
071: s += ", type " + type.toString();
072: s += ", msg '" + message + "'";
073: s += "]";
074: return s;
075: }
076:
077: /**
078: * Can only be called by the UI thread.
079: */
080: public void uiFlush(Tree tree) {
081: // Get or create the tree item associated with this logger.
082: //
083: TreeItem treeItem = createTreeItem(tree);
084:
085: if (treeItem == null) {
086: // The logger associated with this log event is dead, so it should
087: // show no ui at all.
088: //
089: return;
090: }
091:
092: // Style all ancestors.
093: //
094: uiStyleChildAndAncestors(type, treeItem);
095: }
096:
097: /**
098: * Creates a tree item in a way that is sensitive to the log event and its
099: * position in the tree.
100: */
101: private TreeItem createTreeItem(Tree tree) {
102: TreeItem treeItem = null;
103:
104: if (isBranchCommit) {
105: // A child logger is committing.
106: // 'logger' is the logger that needs a tree item.
107: // We can be sure that the child logger's parent is non-null
108: // and that either (1) it is has a TreeItem meaning it is not a
109: // top-level entry or (2) it is a top-level entry and gets attached to
110: // the Tree.
111: //
112: TreeItemLogger parentLogger = (TreeItemLogger) logger
113: .getParentLogger();
114: if (parentLogger.lazyTreeItem == null) {
115: // Is top level.
116: //
117: treeItem = new TreeItem(tree, SWT.NONE);
118: logger.lazyTreeItem = treeItem;
119: } else if (!parentLogger.lazyTreeItem.isDisposed()) {
120: // Is not top level, but still valid to write to.
121: //
122: treeItem = new TreeItem(parentLogger.lazyTreeItem,
123: SWT.NONE);
124: logger.lazyTreeItem = treeItem;
125: } else {
126: // The tree item associated with this logger's parent has been
127: // disposed, so we simply ignore all pending updates related to it.
128: // We also mark that logger dead to avoid adding log events for it.
129: //
130: parentLogger.markLoggerDead();
131: return null;
132: }
133: } else {
134: // Create a regular log item on 'logger'.
135: // The logger may be the root logger, in which case we create TreeItems
136: // directly underneath Tree, or it may be a branched logger, in which
137: // case we create TreeItems underneath the branched logger's TreeItem
138: // (which cannot be null because of the careful ordering of log events).
139: //
140: if (logger.lazyTreeItem == null) {
141: // Is top level.
142: //
143: treeItem = new TreeItem(tree, SWT.NONE);
144: } else if (!logger.lazyTreeItem.isDisposed()) {
145: // Is not top level, but still valid to write to.
146: //
147: treeItem = new TreeItem(logger.lazyTreeItem,
148: SWT.NONE);
149: } else {
150: // The tree item associated with this logger's parent has been
151: // disposed, so we simply ignore all pending updates related to it.
152: // We also mark that logger dead to avoid adding log events for it.
153: //
154: logger.markLoggerDead();
155: return null;
156: }
157: }
158:
159: // Set the text of the new tree item.
160: //
161: String label = message;
162: if (label == null) {
163: if (caught != null) {
164: label = caught.getMessage();
165:
166: if (label == null || label.trim().length() == 0) {
167: label = caught.toString();
168: }
169: }
170: }
171: treeItem.setText(label);
172:
173: // This LogEvent object becomes the tree item's custom data.
174: //
175: treeItem.setData(this );
176:
177: return treeItem;
178: }
179:
180: /**
181: * Can only be called by the UI thread.
182: */
183: private void uiStyleChildAndAncestors(TreeLogger.Type type,
184: TreeItem child) {
185: Display display = child.getDisplay();
186: Color color;
187:
188: Image image = null;
189: if (type == TreeLogger.ERROR) {
190: color = display.getSystemColor(SWT.COLOR_RED);
191: image = imageError;
192: } else if (type == TreeLogger.WARN) {
193: color = display.getSystemColor(SWT.COLOR_DARK_YELLOW);
194: image = imageWarning;
195: } else if (type == TreeLogger.INFO) {
196: color = display.getSystemColor(SWT.COLOR_BLACK);
197: image = imageInfo;
198: } else if (type == TreeLogger.TRACE) {
199: color = display.getSystemColor(SWT.COLOR_DARK_GRAY);
200: image = imageTrace;
201: } else if (type == TreeLogger.DEBUG) {
202: color = display.getSystemColor(SWT.COLOR_DARK_CYAN);
203: image = imageDebug;
204: } else {
205: // if (type == TreeLogger.SPAM)
206: color = display.getSystemColor(SWT.COLOR_DARK_GREEN);
207: image = imageSpam;
208: }
209:
210: if (image != null) {
211: child.setImage(image);
212: }
213:
214: // Set this item's color.
215: //
216: child.setForeground(color);
217:
218: // For types needing attention, set all parents to the warning color.
219: //
220: if (type.needsAttention()) {
221: /*
222: * Originally, this code would expand TreeItems from this child up to
223: * its top level ancestor. However, on Mac and Linux, the TreeItems fail
224: * to expand if its parent is not already expanded. It appears to be an
225: * interaction between SWT and GTK, specifically bug
226: * https://bugs.eclipse.org/bugs/show_bug.cgi?id=97757. The following
227: * loop stores the ancestors and the second loop will set the expanded
228: * attribute on from the top level ancestor down to this child.
229: */
230: Stack<TreeItem> parents = new Stack<TreeItem>();
231:
232: boolean propagateColor = true;
233: TreeItem parent = child.getParentItem();
234: while (parent != null) {
235: parents.push(parent);
236:
237: LogEvent parentEvent = (LogEvent) parent.getData();
238: if (propagateColor) {
239: if (parentEvent.type.isLowerPriorityThan(type)) {
240: parent.setForeground(color);
241: } else {
242: propagateColor = false;
243: }
244: }
245:
246: parent = parent.getParentItem();
247: }
248:
249: while (!parents.isEmpty()) {
250: parent = parents.pop();
251: parent.setExpanded(true);
252: }
253: }
254: }
255: }
256:
257: /**
258: * One object that is shared across all logger instances in the same tree.
259: * This class is the synchronization choke point that prevents the ui thread
260: * from flushing events while other threads are adding them, and it also
261: * provides tree-wide shared objects such as log item images.
262: */
263: private static class PendingUpdates {
264: private List<LogEvent> updates = new LinkedList<LogEvent>();
265:
266: private final Object updatesLock = new Object();
267:
268: public void add(LogEvent update) {
269: synchronized (updatesLock) {
270: updates.add(update);
271: }
272: }
273:
274: /**
275: * Flushes any pending log entries.
276: *
277: * @return <code>true</code> if any new entries were written
278: */
279: public synchronized boolean uiFlush(Tree tree) {
280: // Move the list to flush into a local copy then release the udpate
281: // lock so log events can keep coming in while we flush.
282: //
283: List<LogEvent> toFlush = null;
284: synchronized (updatesLock) {
285: if (updates.isEmpty()) {
286: // Nothing to do.
287: //
288: return false;
289: }
290: toFlush = updates;
291: updates = new LinkedList<LogEvent>();
292: }
293:
294: for (Iterator<LogEvent> iter = toFlush.iterator(); iter
295: .hasNext();) {
296: LogEvent update = iter.next();
297: // Loggers can be die while flushing, so we have to be sure never
298: // to try to flush an entry to a dead logger.
299: //
300: if (!update.logger.isLoggerDead()) {
301: update.uiFlush(tree);
302: }
303: }
304:
305: return true;
306: }
307: }
308:
309: // These don't get disposed, but they do last for the entire process, so
310: // not a big deal.
311: //
312: private static final Image imageDebug = tryLoadImage("log-item-debug.gif");
313: private static final Image imageError = tryLoadImage("log-item-error.gif");
314: private static final Image imageInfo = tryLoadImage("log-item-info.gif");
315: private static final Image imageSpam = tryLoadImage("log-item-spam.gif");
316: private static final Image imageTrace = tryLoadImage("log-item-trace.gif");
317: private static final Image imageWarning = tryLoadImage("log-item-warning.gif");
318:
319: private static Image tryLoadImage(String simpleName) {
320: InputStream is = TreeItemLogger.class
321: .getResourceAsStream(simpleName);
322: if (is != null) {
323: try {
324: Image image = new Image(null, is);
325: return image;
326: } finally {
327: try {
328: is.close();
329: } catch (IOException e) {
330: }
331: }
332: } else {
333: // Bad image.
334: //
335: return null;
336: }
337: }
338:
339: private boolean dead;
340:
341: private TreeItem lazyTreeItem;
342:
343: private final PendingUpdates sharedPendingUpdates;
344:
345: /**
346: * Constructs the top-level TreeItemLogger.
347: */
348: public TreeItemLogger() {
349: sharedPendingUpdates = new PendingUpdates();
350: }
351:
352: /**
353: * Constructs an internal logger.
354: */
355: private TreeItemLogger(PendingUpdates sharedPendingUpdates) {
356: // Inherit the one and only update list from my parent.
357: this .sharedPendingUpdates = sharedPendingUpdates;
358: }
359:
360: public void markLoggerDead() {
361: // Cannot kill the root logger, even if attempted.
362: //
363: if (getParentLogger() != null) {
364: dead = true;
365: }
366: }
367:
368: /**
369: * Flushes log records to the UI; must only be called by the UI thread.
370: *
371: * @return <code>true</code> if any new entries were written
372: */
373: public boolean uiFlush(Tree tree) {
374: return sharedPendingUpdates.uiFlush(tree);
375: }
376:
377: @Override
378: protected AbstractTreeLogger doBranch() {
379: return new TreeItemLogger(sharedPendingUpdates);
380: }
381:
382: @Override
383: protected void doCommitBranch(
384: AbstractTreeLogger childBeingCommitted, Type type,
385: String msg, Throwable caught) {
386: if (isLoggerDead()) {
387: return;
388: }
389:
390: TreeItemLogger commitChild = (TreeItemLogger) childBeingCommitted;
391: sharedPendingUpdates.add(new LogEvent(commitChild, true,
392: commitChild.getBranchedIndex(), type, msg, caught));
393: }
394:
395: @Override
396: protected void doLog(int index, TreeLogger.Type type, String msg,
397: Throwable caught) {
398: if (isLoggerDead()) {
399: return;
400: }
401:
402: sharedPendingUpdates.add(new LogEvent(this , false, index, type,
403: msg, caught));
404: }
405:
406: /**
407: * Used for an extra check to avoid creating log events for dead loggers. A
408: * dead logger is one that can no longer interact with the UI.
409: */
410: private boolean isLoggerDead() {
411: // Deadness was cached.
412: //
413: if (dead) {
414: return true;
415: }
416:
417: // Check upward in the parent chain for any dead parent.
418: //
419: TreeItemLogger parentLogger = (TreeItemLogger) getParentLogger();
420: if (parentLogger == null) {
421: // This is the root logger, which cannot die.
422: //
423: return false;
424: }
425:
426: // Otherwise, this logger is dead if its parent is dead (recursively).
427: //
428: if (parentLogger.isLoggerDead()) {
429: // This logger is dead because my parent is dead.
430: //
431: markLoggerDead();
432: return true;
433: }
434:
435: // I'm not quite dead yet.
436: //
437: return false;
438: }
439: }
|