001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.openide.explorer.view;
042:
043: import java.util.logging.Level;
044: import java.util.logging.Logger;
045: import org.openide.nodes.*;
046: import org.openide.util.Mutex;
047: import org.openide.util.Utilities;
048:
049: import java.awt.Image;
050:
051: import java.beans.BeanInfo;
052:
053: import java.lang.ref.Reference;
054: import java.lang.ref.WeakReference;
055:
056: import java.util.*;
057:
058: import javax.swing.Icon;
059: import javax.swing.ImageIcon;
060: import javax.swing.SwingUtilities;
061: import javax.swing.event.EventListenerList;
062: import javax.swing.tree.TreeNode;
063:
064: /** Visual representation of one node. Holds necessary information about nodes
065: * like icon, name, description and also list of its children.
066: * <P>
067: * There is at most one VisualizerNode for one node. All of them are held in a cache.
068: * <P>
069: * VisualizerNode provides a thread-safe layer between Nodes, which may fire
070: * property changes on any thread, and the AWT dispatch thread
071: * thread.
072: *
073: * @author Jaroslav Tulach
074: */
075: final class VisualizerNode extends EventListenerList implements
076: NodeListener, TreeNode, Runnable {
077: /** one template to use for searching for visualizers */
078: private static final VisualizerNode TEMPLATE = new VisualizerNode(0);
079: /** a shared logger for the visualizer functionality */
080: static final Logger LOG = Logger.getLogger(VisualizerNode.class
081: .getName());
082:
083: /** constant holding empty reference to children */
084: private static final Reference<VisualizerChildren> NO_REF = new WeakReference<VisualizerChildren>(
085: null);
086:
087: /** cache of visualizers */
088: private static WeakHashMap<VisualizerNode, Reference<VisualizerNode>> cache = new WeakHashMap<VisualizerNode, Reference<VisualizerNode>>();
089:
090: /** empty visualizer */
091: public static final VisualizerNode EMPTY = getVisualizer(null,
092: Node.EMPTY);
093:
094: /** queue processor to transfer requests to event queue */
095: private static final QP QUEUE = new QP();
096: private static final String UNKNOWN = new String();
097: static final long serialVersionUID = 3726728244698316872L;
098: private static final String NO_HTML_DISPLAYNAME = "noHtmlDisplayName"; //NOI18N
099:
100: /** loaded default icon */
101: private static Icon defaultIcon;
102:
103: /** default icon to use when none is present */
104: private static final String DEFAULT_ICON = "org/openide/nodes/defaultNode.png"; // NOI18N
105:
106: /** Cached icon - pre-html, there was a separate cache in NodeRenderer, but
107: * if we're keeping a weak cache of VisualizerNodes, there's no reason not
108: * to keep it here */
109: private Icon icon = null;
110:
111: /** node. Do not modify!!! */
112: Node node;
113:
114: /** system hashcode of the node */
115: private int hashCode;
116:
117: /** visualizer children attached thru weak references Reference (VisualizerChildren) */
118: private Reference<VisualizerChildren> children = NO_REF;
119:
120: /** the VisualizerChildren that contains this VisualizerNode or null */
121: private VisualizerChildren parent;
122:
123: /** cached name */
124: private String name;
125:
126: /** cached display name */
127: private String displayName;
128:
129: /** cached short description */
130: private String shortDescription;
131: private transient boolean inRead;
132: private String htmlDisplayName = null;
133: private int cachedIconType = -1;
134:
135: /** Constructor that creates template for the node.
136: */
137: private VisualizerNode(int hashCode) {
138: this .hashCode = hashCode;
139: this .node = null;
140: }
141:
142: /** Creates new VisualizerNode
143: * @param n node to refer to
144: */
145: private VisualizerNode(Node n) {
146: node = n;
147: hashCode = System.identityHashCode(node);
148:
149: // attach as a listener
150: node.addNodeListener(NodeOp.weakNodeListener(this , node));
151:
152: // uiListener = WeakListener.propertyChange (this, null);
153: // UIManager.addPropertyChangeListener (uiListener);
154: name = UNKNOWN;
155: displayName = UNKNOWN;
156: shortDescription = UNKNOWN;
157: }
158:
159: // bugfix #29435, getVisualizer is synchronized in place of be called only from EventQueue
160:
161: /** Finds VisualizerNode for given node.
162: * @param ch the children this visualizer should belong to
163: * @param n the node
164: * @return the visualizer
165: */
166: public static VisualizerNode getVisualizer(VisualizerChildren ch,
167: Node n) {
168: return getVisualizer(ch, n, true);
169: }
170:
171: /** Finds VisualizerNode for given node.
172: * @param ch the children this visualizer should belong to
173: * @param n the node
174: * @return the visualizer or null
175: */
176: public static synchronized VisualizerNode getVisualizer(
177: VisualizerChildren ch, Node n, boolean create) {
178: TEMPLATE.hashCode = System.identityHashCode(n);
179: TEMPLATE.node = n;
180:
181: Reference<VisualizerNode> r = cache.get(TEMPLATE);
182:
183: TEMPLATE.hashCode = 0;
184: TEMPLATE.node = null;
185:
186: VisualizerNode v = (r == null) ? null : r.get();
187:
188: if (v == null) {
189: if (!create) {
190: return null;
191: }
192:
193: v = new VisualizerNode(n);
194: cache.put(v, new WeakReference<VisualizerNode>(v));
195: }
196:
197: if (ch != null) {
198: v.parent = ch;
199: }
200:
201: return v;
202: }
203:
204: /** Returns cached short description.
205: * @return short description of represented node
206: */
207: public String getShortDescription() {
208: String desc = shortDescription;
209:
210: if (desc == UNKNOWN) {
211: shortDescription = desc = node.getShortDescription();
212: }
213:
214: return desc;
215: }
216:
217: /** Returns cached display name.
218: * @return display name of represented node
219: */
220: public String getDisplayName() {
221: if (displayName == UNKNOWN) {
222: displayName = (node == null) ? null : node.getDisplayName();
223: }
224:
225: return displayName;
226: }
227:
228: /** Returns cached name.
229: * @return name of represented node
230: */
231: public String getName() {
232: if (name == UNKNOWN) {
233: name = (node == null) ? null : node.getName();
234: }
235:
236: return name;
237: }
238:
239: /** Getter for list of children of this visualizer.
240: * @return list of VisualizerNode objects
241: */
242: public List<VisualizerNode> getChildren() {
243: VisualizerChildren ch = children.get();
244:
245: if ((ch == null) && !node.isLeaf()) {
246: // initialize the nodes children before we enter
247: // the readAccess section
248: Node[] tmpInit = node.getChildren().getNodes();
249:
250: // go into lock to ensure that no childrenAdded, childrenRemoved,
251: // childrenReordered notifications occures and that is why we do
252: // not loose any changes
253: ch = Children.MUTEX
254: .readAccess(new Mutex.Action<VisualizerChildren>() {
255: public VisualizerChildren run() {
256: Node[] nodes = node.getChildren()
257: .getNodes();
258: VisualizerChildren vc = new VisualizerChildren(
259: VisualizerNode.this , nodes);
260: notifyVisualizerChildrenChange(
261: nodes.length, vc);
262:
263: return vc;
264: }
265: });
266: }
267:
268: if (LOG.isLoggable(Level.FINER)) {
269: // this assert is too expensive during the performance tests:
270: assert (ch == null) || !ch.list.contains(null) : ch.list
271: + " from " + node;
272: }
273:
274: return (ch == null) ? Collections.<VisualizerNode> emptyList()
275: : ch.list;
276: }
277:
278: //
279: // TreeNode interface (callable only from AWT-Event-Queue)
280: //
281: public int getIndex(final javax.swing.tree.TreeNode p1) {
282: return getChildren().indexOf(p1);
283: }
284:
285: public boolean getAllowsChildren() {
286: return !isLeaf();
287: }
288:
289: public javax.swing.tree.TreeNode getChildAt(int p1) {
290: List ch = getChildren();
291: VisualizerNode vn = (VisualizerNode) ch.get(p1);
292: assert vn != null : "Null child in " + ch + " from " + node;
293:
294: return vn;
295: }
296:
297: public int getChildCount() {
298: return getChildren().size();
299: }
300:
301: public java.util.Enumeration<VisualizerNode> children() {
302: List<VisualizerNode> l = getChildren();
303: assert !l.contains(null) : "Null child in " + l + " from "
304: + node;
305:
306: return java.util.Collections.enumeration(l);
307: }
308:
309: public boolean isLeaf() {
310: return node.isLeaf();
311: }
312:
313: public javax.swing.tree.TreeNode getParent() {
314: Node parent = node.getParentNode();
315:
316: return (parent == null) ? null : getVisualizer(null, parent);
317: }
318:
319: // **********************************************
320: // Can be called under Children.MUTEX.writeAccess
321: // **********************************************
322:
323: /** Fired when a set of new children is added.
324: * @param ev event describing the action
325: */
326: public void childrenAdded(NodeMemberEvent ev) {
327: VisualizerChildren ch = children.get();
328:
329: LOG.log(Level.FINER, "childrenAdded {0}", ev); // NOI18N
330: if (ch == null) {
331: LOG.log(Level.FINER, "childrenAdded - exit"); // NOI18N
332: return;
333: }
334:
335: QUEUE.runSafe(new VisualizerEvent.Added(ch, ev.getDelta(), ev
336: .getDeltaIndices()));
337: LOG.log(Level.FINER, "childrenAdded - end"); // NOI18N
338: }
339:
340: /** Fired when a set of children is removed.
341: * @param ev event describing the action
342: */
343: public void childrenRemoved(NodeMemberEvent ev) {
344: VisualizerChildren ch = children.get();
345:
346: LOG.log(Level.FINER, "childrenRemoved {0}", ev); // NOI18N
347: if (ch == null) {
348: LOG.log(Level.FINER, "childrenRemoved - exit"); // NOI18N
349: return;
350: }
351:
352: QUEUE.runSafe(new VisualizerEvent.Removed(ch, ev.getDelta()));
353: LOG.log(Level.FINER, "childrenRemoved - end"); // NOI18N
354: }
355:
356: /** Fired when the order of children is changed.
357: * @param ev event describing the change
358: */
359: public void childrenReordered(NodeReorderEvent ev) {
360: doChildrenReordered(ev.getPermutation());
361: }
362:
363: // helper method (called from TreeTableView.sort)
364: void doChildrenReordered(int[] perm) {
365: VisualizerChildren ch = children.get();
366:
367: LOG.log(Level.FINER, "childrenReordered {0}", perm); // NOI18N
368: if (ch == null) {
369: LOG.log(Level.FINER, "childrenReordered - exit"); // NOI18N
370: return;
371: }
372:
373: QUEUE.runSafe(new VisualizerEvent.Reordered(ch, perm));
374: LOG.log(Level.FINER, "childrenReordered - end"); // NOI18N
375: }
376:
377: void reorderChildren(Comparator<VisualizerNode> c) {
378: assert SwingUtilities.isEventDispatchThread();
379:
380: VisualizerChildren ch = children.get();
381:
382: if (ch == null) {
383: return;
384: }
385:
386: new VisualizerEvent.Reordered(ch, c).run();
387: }
388:
389: void naturalOrder() {
390: //force new creation of the children list in the natural order
391: children.clear();
392: getChildren();
393:
394: //sort the children list with a dummy comparator to throw events needed
395: reorderChildren(new Comparator<VisualizerNode>() {
396: public int compare(VisualizerNode o1, VisualizerNode o2) {
397: return 0;
398: }
399: });
400: }
401:
402: /** Fired when the node is deleted.
403: * @param ev event describing the node
404: */
405: public void nodeDestroyed(NodeEvent ev) {
406: // ignore for now
407: }
408:
409: /** Change in the node properties (icon, etc.)
410: */
411: public void propertyChange(final java.beans.PropertyChangeEvent evt) {
412: String name = evt.getPropertyName();
413: boolean isIconChange = Node.PROP_ICON.equals(name)
414: || Node.PROP_OPENED_ICON.equals(name);
415:
416: if (Node.PROP_NAME.equals(name)
417: || Node.PROP_DISPLAY_NAME.equals(name) || isIconChange) {
418: if (isIconChange) {
419: //Ditch the cached icon type so the next call to getIcon() will
420: //recreate the ImageIcon
421: cachedIconType = -1;
422: }
423:
424: if (Node.PROP_DISPLAY_NAME.equals(name)) {
425: htmlDisplayName = null;
426: }
427:
428: SwingUtilities.invokeLater(this );
429:
430: return;
431: }
432:
433: // bugfix #37748, VisualizerNode ignores change of short desc if it is not read yet (set to UNKNOWN)
434: if (Node.PROP_SHORT_DESCRIPTION.equals(name)
435: && (shortDescription != UNKNOWN)) {
436: SwingUtilities.invokeLater(this );
437:
438: return;
439: }
440:
441: if (Node.PROP_LEAF.equals(name)) {
442: SwingUtilities.invokeLater(new Runnable() {
443: public void run() {
444: children = NO_REF;
445:
446: // notify models
447: VisualizerNode parent = VisualizerNode.this ;
448:
449: while (parent != null) {
450: Object[] listeners = parent.getListenerList();
451:
452: for (int i = listeners.length - 1; i >= 0; i -= 2) {
453: ((NodeModel) listeners[i])
454: .structuralChange(VisualizerNode.this );
455: }
456:
457: parent = (VisualizerNode) parent.getParent();
458: }
459: }
460: });
461: }
462: }
463:
464: /** Update the state of this class by retrieving new name, etc.
465: * And fire change to all listeners. Only by AWT-Event-Queue
466: */
467: public void run() {
468: if (!inRead) {
469: try {
470: // call the foreign code under the read lock
471: // so all potential structure modifications
472: // are queued until after we finish.
473: // see issue #48993
474: inRead = true;
475: Children.MUTEX.readAccess(this );
476: } finally {
477: inRead = false;
478: }
479:
480: return;
481: }
482:
483: name = node.getName();
484: displayName = node.getDisplayName();
485: shortDescription = UNKNOWN;
486:
487: //
488: // notify models
489: //
490: VisualizerNode parent = this ;
491:
492: while (parent != null) {
493: Object[] listeners = parent.getListenerList();
494:
495: for (int i = listeners.length - 1; i >= 0; i -= 2) {
496: ((NodeModel) listeners[i]).update(this );
497: }
498:
499: parent = (VisualizerNode) parent.getParent();
500: }
501: }
502:
503: //
504: // Access to VisualizerChildren
505: //
506:
507: /** Notifies change in the amount of children. This is used to distinguish between
508: * weak and hard reference. Called from VisualizerChildren
509: * @param size amount of children
510: * @param ch the children
511: */
512: void notifyVisualizerChildrenChange(int size, VisualizerChildren ch) {
513: if (size == 0) {
514: // hold the children hard
515: children = new StrongReference<VisualizerChildren>(ch);
516: } else {
517: children = new WeakReference<VisualizerChildren>(ch);
518: }
519: }
520:
521: // ********************************
522: // This can be called from anywhere
523: // ********************************
524:
525: /** Adds visualizer listener.
526: */
527: public synchronized void addNodeModel(NodeModel l) {
528: add(NodeModel.class, l);
529: }
530:
531: /** Removes visualizer listener.
532: */
533: public synchronized void removeNodeModel(NodeModel l) {
534: remove(NodeModel.class, l);
535: }
536:
537: /** Hash code
538: */
539: public int hashCode() {
540: return hashCode;
541: }
542:
543: /** Equals two objects are equal if they have the same hash code
544: */
545: public boolean equals(Object o) {
546: if (!(o instanceof VisualizerNode)) {
547: return false;
548: }
549:
550: VisualizerNode v = (VisualizerNode) o;
551:
552: return v.node == node;
553: }
554:
555: /** String name is taken from the node.
556: */
557: public String toString() {
558: return getDisplayName();
559: }
560:
561: public String getHtmlDisplayName() {
562: if (htmlDisplayName == null) {
563: htmlDisplayName = node.getHtmlDisplayName();
564:
565: if (htmlDisplayName == null) {
566: htmlDisplayName = NO_HTML_DISPLAYNAME;
567: }
568: }
569:
570: return (htmlDisplayName == NO_HTML_DISPLAYNAME) ? null
571: : htmlDisplayName;
572: }
573:
574: Icon getIcon(boolean opened, boolean large) {
575: int newCacheType = getCacheType(opened, large);
576:
577: if (cachedIconType != newCacheType) {
578: int iconType = large ? BeanInfo.ICON_COLOR_32x32
579: : BeanInfo.ICON_COLOR_16x16;
580:
581: Image image = opened ? node.getOpenedIcon(iconType) : node
582: .getIcon(iconType);
583:
584: // bugfix #28515, check if getIcon contract isn't broken
585: if (image == null) {
586: String method = opened ? "getOpenedIcon" : "getIcon"; // NOI18N
587: LOG.warning("Node \"" + node.getName() + "\" ["
588: + node.getClass().getName()
589: + "] cannot return null from " + method
590: + "(). See Node." + method + " contract."); // NOI18N
591:
592: icon = getDefaultIcon();
593: } else {
594: icon = new ImageIcon(image);
595: }
596: }
597:
598: cachedIconType = newCacheType;
599:
600: return icon;
601: }
602:
603: /** Some simple bitmasking to determine the type of the cached icon.
604: * Generally, it's worth caching one, but not a bunch - generally one will
605: * be used repeatedly. */
606: private static final int getCacheType(boolean opened, boolean large) {
607: return (opened ? 2 : 0) | (large ? 1 : 0);
608: }
609:
610: /** Loads default icon if not loaded. */
611: private static Icon getDefaultIcon() {
612: if (defaultIcon == null) {
613: defaultIcon = new ImageIcon(Utilities
614: .loadImage(DEFAULT_ICON));
615: }
616:
617: return defaultIcon;
618: }
619:
620: static void runQueue() {
621: QUEUE.run();
622: }
623:
624: /** Strong reference.
625: */
626: private static final class StrongReference<T> extends
627: WeakReference<T> {
628: private T o;
629:
630: public StrongReference(T o) {
631: super (null);
632: this .o = o;
633: }
634:
635: public T get() {
636: return o;
637: }
638: }
639:
640: /** Class that processes runnables in event queue. It guarantees that
641: * the order of processed objects will be exactly the same as they
642: * arrived.
643: */
644: private static final class QP extends Object implements Runnable {
645: /** queue of all requests (Runnable) that should be processed
646: * AWT-Event queue.
647: */
648: private LinkedList<Runnable> queue = null;
649:
650: QP() {
651: }
652:
653: /** Runs the runnable in event thread.
654: * @param run what should run
655: */
656: public void runSafe(Runnable run) {
657: boolean isNew = false;
658:
659: synchronized (this ) {
660: // access to queue variable is synchronized
661: if (queue == null) {
662: queue = new LinkedList<Runnable>();
663: isNew = true;
664: }
665:
666: queue.add(run);
667: }
668:
669: if (isNew) {
670: // either starts the processing of the queue immediatelly
671: // (if we are in AWT-Event thread) or uses
672: // SwingUtilities.invokeLater to do so
673: Mutex.EVENT.writeAccess(this );
674: }
675: }
676:
677: /** Processes the queue.
678: */
679: public void run() {
680: Enumeration<Runnable> en;
681:
682: synchronized (this ) {
683: // access to queue variable is synchronized
684: if (queue == null) {
685: LOG.log(Level.FINER, "Queue empty"); // NOI18N
686: return;
687: }
688:
689: en = Collections.enumeration(queue);
690: queue = null;
691: LOG.log(Level.FINER, "Queue emptied"); // NOI18N
692: }
693:
694: while (en.hasMoreElements()) {
695: Runnable r = en.nextElement();
696: LOG.log(Level.FINER, "Running {0}", r); // NOI18N
697: Children.MUTEX.readAccess(r); // run the update under Children.MUTEX
698: LOG.log(Level.FINER, "Finished {0}", r); // NOI18N
699: }
700: LOG.log(Level.FINER, "Queue processing over"); // NOI18N
701: }
702: }
703: }
|