alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *                 Sun Public License Notice
 * 
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 * 
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2004 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.openide.explorer;

import java.awt.Component;
import java.beans.*;
import java.io.*;
import java.util.*;
import java.text.MessageFormat;
import javax.swing.Action;
import javax.swing.SwingUtilities;

import org.openide.nodes.*;
import org.openide.ErrorManager;
import org.openide.util.*;
import org.openide.util.datatransfer.*;
import org.openide.util.io.SafeException;

/**
 * Manages a selection and root context for a (set of) Explorer view(s).  The
 * views should register their {@link java.beans.VetoableChangeListener}s and
 * {@link java.beans.PropertyChangeListener}s at the
 * ExplorerManager of the Explorer they belong to. The manager
 * listens on changes to the node hierarchy and updates the selection and root
 * node.
 *
 * 

Deserialization may throw {@link SafeException} if the contexts cannot be * restored correctly, but the stream is uncorrupted. * * * @author Ian Formanek, Petr Hamernik, Jaroslav Tulach, Jan Jancura, * Jesse Glick * @see ExplorerUtils */ public final class ExplorerManager extends Object implements Serializable, Cloneable { /** generated Serialized Version UID */ static final long serialVersionUID = -4330330689803575792L; /** Name of property for the root context. */ public static final String PROP_ROOT_CONTEXT = "rootContext"; // NOI18N /** Name of property for the explored context. */ public static final String PROP_EXPLORED_CONTEXT = "exploredContext"; // NOI18N /** Name of property for the node selection. */ public static final String PROP_SELECTED_NODES = "selectedNodes"; // NOI18N /** Name of property for change in a node. */ public static final String PROP_NODE_CHANGE = "nodeChange"; // NOI18N /** The support for VetoableChangeEvent */ private transient VetoableChangeSupport vetoableSupport; /** The support for PropertyChangeEvent */ private transient PropertyChangeSupport propertySupport; /** The current root context */ private Node rootContext; /** The current explored context */ private Node exploredContext; /** The currently selected beans */ private Node[] selectedNodes; /** listener to destroy of root node */ private transient Listener listener; /** weak listener */ private transient NodeListener weakListener; /** Request processor for managing selections. */ static RequestProcessor selectionProcessor; /** Delay for coalescing events before removing destroyed nodes from the selection. */ private static final int SELECTION_SYNC_DELAY = 200; /** Task that removes manages node selection issues. */ private RequestProcessor.Task selectionSyncTask; /** Actions factory provided for this explorer manager */ private ExplorerActionsImpl actions; /** Construct a new manager. */ public ExplorerManager () { init (); } /** Initializes the nodes. */ private void init () { exploredContext = rootContext = Node.EMPTY; selectedNodes = new Node[0]; listener = new Listener (); weakListener = NodeOp.weakNodeListener (listener, null); } /** Clones the manager. * @return manager with the same settings like this one */ public Object clone () { ExplorerManager em = new ExplorerManager (); em.rootContext = rootContext; em.exploredContext = exploredContext; em.selectedNodes = selectedNodes; return em; } /** Get the set of selected nodes. * @return the selected nodes; empty (not null) if none are selected */ public Node[] getSelectedNodes () { return selectedNodes; } // compare two arrays of nodes in respect to a path to root private boolean equalNodes (Node[] arr1, Node[] arr2) { // generic tests if (!Arrays.equals (arr1, arr2)) return false; if (arr1 == null || arr1.length == 0) return true; // compare paths from each node to root int i = 0; while ( i < arr1.length && Arrays.equals ( NodeOp.createPath (arr1[i], null), NodeOp.createPath (arr2[i], null))) { i++; } return i == arr1.length; } /** Set the set of selected nodes. * @param value the nodes to select; empty (not null) if none are to be selected * @exception PropertyVetoException when the given nodes cannot be selected * @throws IllegalArgumentException if null is given, or if any elements * of the selection are not within the current root context */ public final void setSelectedNodes (Node[] value) throws PropertyVetoException { Node[] oldValue; synchronized (this) { if (value == null) throw new IllegalArgumentException(getString("EXC_NodeCannotBeNull")); if (equalNodes (value, selectedNodes)) { return; } for (int i = 0; i < value.length; i++) { if (value[i] == null) { throw new IllegalArgumentException(getString("EXC_NoElementOfNodeSelectionMayBeNull")); } if (! isUnderRoot (value[i])) { throw new IllegalArgumentException( MessageFormat.format( getString("EXC_NodeSelectionCannotContainNodes"), new Object[] { value[i].getDisplayName(), rootContext.getDisplayName(), } ) ); } } if (value.length != 0 && vetoableSupport != null) { // we send the vetoable change event only for non-empty selections vetoableSupport.fireVetoableChange(PROP_SELECTED_NODES, selectedNodes, value); } oldValue = selectedNodes; Collection nodesToRemove; Collection nodesToAdd; Collection newSelection = new LinkedList(); Collection currentNodes = Arrays.asList(oldValue); for (int i = 0; i < value.length; i++) { newSelection.add(value[i]); } nodesToAdd = new LinkedList(newSelection); nodesToAdd.removeAll(currentNodes); nodesToRemove = new LinkedList(currentNodes); nodesToRemove.removeAll(newSelection); // PENDING: filter out duplicities from the selection if (newSelection.size() == value.length) { selectedNodes = value; } else { selectedNodes = new Node[value.length]; newSelection.toArray(selectedNodes); } Iterator it; // remove listeners from nodes that are being deselected for (it = nodesToRemove.iterator(); it.hasNext(); ) { Node n = (Node)it.next(); n.removeNodeListener(weakListener); } // and add listeners to nodes that become selected for (it = nodesToAdd.iterator(); it.hasNext(); ) { Node n = (Node)it.next(); n.removeNodeListener(weakListener); n.addNodeListener(weakListener); } } // synchronized (this) if (propertySupport != null) { // replan fire of prop event to AWT-queue to get correctly visual-reactions if (SwingUtilities.isEventDispatchThread ()) { propertySupport.firePropertyChange (PROP_SELECTED_NODES, oldValue, selectedNodes); } else { final Node[] tempOldValue = oldValue; SwingUtilities.invokeLater(new Runnable () { public void run () { propertySupport.firePropertyChange (PROP_SELECTED_NODES, tempOldValue, selectedNodes); } }); } } } /** Get the explored context. *

The "explored context" is not as frequently used as the node selection; * generally it refers to a parent node which contains all of the things * being displayed at this moment. For BeanTreeView this is * irrelevant, but ContextTreeView uses it (in lieu of the node * selection) and for IconView it is important (the node * whose children are visible, i.e. the "background" of the icon view). * @return the node being explored, or null */ public final Node getExploredContext() { return exploredContext; } /** Set the explored context. * The node selection will be cleared as well. * @param value the new node to explore, or null if none should be explored. * @throws IllegalArgumentException if the node is not within the current root context in the node hierarchy */ public final void setExploredContext(Node value) { setExploredContext(value, new Node[0]); } /** Set the explored context. * The node selection will be changed as well. Note: node selection cannot be * vetoed if calling this method. It is generally better to call setExploredContextAndSelection. * @param value the new node to explore, or null if none should be explored. * @throws IllegalArgumentException if the node is not within the current root context in the node hierarchy */ public final void setExploredContext(Node value, Node[] selection) { // handles nulls correctly: if (Utilities.compareObjects (value, exploredContext)) { setSelectedNodes0(selection); return; } if (value != null && ! isUnderRoot (value)) { throw new IllegalArgumentException( MessageFormat.format( getString("EXC_ContextMustBeWithinRootContext"), new Object[] { value.getDisplayName (), rootContext.getDisplayName () } ) ); } setSelectedNodes0(selection); final Node oldValue = exploredContext; exploredContext = value; if (propertySupport != null) { // replan fire of prop event to AWT-queue to get correctly visual-reactions if (SwingUtilities.isEventDispatchThread ()) { propertySupport.firePropertyChange (PROP_EXPLORED_CONTEXT, oldValue, exploredContext); } else { SwingUtilities.invokeLater(new Runnable () { public void run () { propertySupport.firePropertyChange (PROP_EXPLORED_CONTEXT, oldValue, exploredContext); } }); } } } /** Set the explored context and selected nodes. If the change in selected nodes is vetoed, * PropertyVetoException is rethrown from here. * @param value the new node to explore, or null if none should be explored. * @param selection the new nodes to be selected * @throws IllegalArgumentException if the node is not within the current root context in the node hierarchy * @throws PropertyVetoExcepion if listeners attached to this explorer manager do so */ public final void setExploredContextAndSelection(Node value, Node[] selection) throws PropertyVetoException { // handles nulls correctly: if (Utilities.compareObjects (value, exploredContext)) { setSelectedNodes1(selection); return; } if (value != null && ! isUnderRoot (value)) { throw new IllegalArgumentException( MessageFormat.format( getString("EXC_ContextMustBeWithinRootContext"), new Object[] { value.getDisplayName (), rootContext.getDisplayName () } ) ); } setSelectedNodes1(selection); final Node oldValue = exploredContext; exploredContext = value; if (propertySupport != null) { // replan fire of prop event to AWT-queue to get correctly visual-reactions if (SwingUtilities.isEventDispatchThread ()) { propertySupport.firePropertyChange (PROP_EXPLORED_CONTEXT, oldValue, exploredContext); } else { SwingUtilities.invokeLater(new Runnable () { public void run () { propertySupport.firePropertyChange (PROP_EXPLORED_CONTEXT, oldValue, exploredContext); } }); } } } /** Sets selected nodes and handles PropertyVetoException */ final void setSelectedNodes0(Node[] nodes) { try { setSelectedNodes(nodes); } catch (PropertyVetoException e) { if (nodes.length == 0) { IllegalStateException err = new IllegalStateException(getString("EXC_MustNotVetoEmptySelection")); ErrorManager.getDefault ().annotate(err, e); // Should be impossible regardless of view: throw err; } } } /** Sets selected nodes and handles PropertyVetoException */ private void setSelectedNodes1(Node[] nodes) throws PropertyVetoException { try { setSelectedNodes(nodes); } catch (PropertyVetoException e) { if (nodes.length == 0) { IllegalStateException err = new IllegalStateException(getString("EXC_MustNotVetoEmptySelection")); ErrorManager.getDefault ().annotate(err, e); // Should be impossible regardless of view: throw err; } throw e; // this is the difference to setSelectedNodes0 } } /** Get the root context. *

The "root context" is simply the topmost node that this explorer can * display or manipulate. For BeanTreeView, this would mean * the root node of the tree. For e.g. IconView, this would * mean the uppermost possible node that that icon view could display; * while the explored context would change at user prompting via the * up button and clicking on subfolders, the root context would be fixed * by the code displaying the explorer. * @return the root context node */ public final Node getRootContext() { return rootContext; } /** Set the root context. * The explored context will be set to the new root context as well. * If any of the selected nodes are not inside it, the selection will be cleared. * @param value the new node to serve as a root * @throws IllegalArgumentException if it is null */ public final void setRootContext(Node value) { if (value == null) throw new IllegalArgumentException(getString("EXC_CannotHaveNullRootContext")); if (rootContext.equals (value)) return; final Node oldValue = rootContext; rootContext = value; oldValue.removeNodeListener (weakListener); rootContext.addNodeListener (weakListener); if (propertySupport != null) { // replan fire of prop event to AWT-queue to get correctly visual-reactions if (SwingUtilities.isEventDispatchThread ()) { propertySupport.firePropertyChange (PROP_ROOT_CONTEXT, oldValue, rootContext); } else { SwingUtilities.invokeLater(new Runnable () { public void run () { propertySupport.firePropertyChange (PROP_ROOT_CONTEXT, oldValue, rootContext); } }); } } Node[] newselection = getSelectedNodes(); if (!areUnderTarget(newselection, rootContext)) { newselection = new Node[0]; } setExploredContext(rootContext, newselection); } /** @return true iff all nodes are under the target node */ private boolean areUnderTarget(Node[] nodes, Node target) { bigloop: for (int i = 0; i < nodes.length; i++) { Node node = nodes[i]; while (node != null) { if (node.equals(target)) { continue bigloop; } node = node.getParentNode(); } return false; } return true; } /** Add a PropertyChangeListener to the listener list. * @param l the listener to add */ public synchronized void addPropertyChangeListener(PropertyChangeListener l) { if (propertySupport == null) propertySupport = new PropertyChangeSupport(this); propertySupport.addPropertyChangeListener(l); } /** Remove a PropertyChangeListener from the listener list. * @param l the listener to remove */ public synchronized void removePropertyChangeListener(PropertyChangeListener l) { if (propertySupport != null) propertySupport.removePropertyChangeListener(l); } /** Add a VetoableListener to the listener list. * @param l the listener to add */ public synchronized void addVetoableChangeListener(VetoableChangeListener l) { if (vetoableSupport == null) vetoableSupport = new VetoableChangeSupport(this); vetoableSupport.addVetoableChangeListener(l); } /** Remove a VetoableChangeListener from the listener list. * @param l the listener to remove */ public synchronized void removeVetoableChangeListener(VetoableChangeListener l) { if (vetoableSupport != null) vetoableSupport.removeVetoableChangeListener(l); } /** Checks whether given Node is a subnode of rootContext. * @return true if specified Node is under current rootContext */ private boolean isUnderRoot(Node node) { while (node != null) { if (node.equals(rootContext)) return true; node = node.getParentNode(); } return false; } /** defines serialized fields for the manager. */ private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField ("root", Node.Handle.class), // NOI18N new ObjectStreamField ("rootName", String.class), // NOI18N new ObjectStreamField ("explored", String[].class), // NOI18N // XXX(-ttran) this should be String[][].class, but cannot be changed // now because of backward compatibility new ObjectStreamField ("selected", Object[].class) // NOI18N }; /** serializes object * @serialData the following objects are written in sequence: *

    *
  1. a Node.Handle for the root context; may be null if root context * is not persistable *
  2. the display name of the root context (to give nicer error messages * later on) *
  3. the path from root context to explored context; null if no explored * context or no such path *
  4. for every element of node selection, path from root context to that node; * null if no such path *
  5. null to terminate *
* Note that if the root context handle is null, the display name is still written * but the paths to explored context and node selection are not written, the stream * ends there. */ private void writeObject (ObjectOutputStream os) throws IOException { // indication that we gonna use put fields and not the old method. os.writeObject (this); ObjectOutputStream.PutField fields = os.putFields (); // [PENDING] is this method (and readObject) always called from within // the Nodes mutex? It should be! //System.err.println("rootContext: " + rootContext); Node.Handle rCH = rootContext.getHandle (); fields.put ("root", rCH); // NOI18N //System.err.println("writing: " + rCH); fields.put ("rootName", rootContext.getDisplayName ()); // NOI18N //System.err.println("writing: " + rootContext.getDisplayName ()); // If root cannot be stored, we just stop right there, of course. if (rCH != null) { // Note that explored context may be null (this is valid). // Also, it may have happened that the hierarchy changed so that // the explored context is *no longer* under the root (though it was at // the time these things were set up). In this case, we cannot store the // path. Caution: NodeOp.createPath will create a path to a root (parentless) // node even if you specify a non-null root, if the first arg is not a child! String[] explored; if (exploredContext == null) explored = null; else if (isUnderRoot (exploredContext)) explored = NodeOp.createPath (exploredContext, rootContext); else explored = null; fields.put ("explored", explored); // NOI18N // Now do the same for each node selection, with the same caveats. // Null terminates, so bad elements are simply skipped. LinkedList selected = new LinkedList (); for (int i = 0; i < selectedNodes.length; i++) { if (isUnderRoot (selectedNodes[i])) { selected.add (NodeOp.createPath (selectedNodes[i], rootContext)); } } fields.put ("selected", selected.toArray ()); // NOI18N } os.writeFields(); } /** Deserializes the view and initializes it * @serialData see writeObject */ private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { // perform initialization init(); // read the first object in the stream Object firstObject = ois.readObject (); if (firstObject != this) { // use old version of deserialization readObjectOld ((Node.Handle)firstObject, ois); return; } // work with get fields ObjectInputStream.GetField fields = ois.readFields(); // read root handle Node.Handle h = (Node.Handle)fields.get ("root", null); // NOI18N //System.err.println("reading: " + h); final String rootName = (String)fields.get ("rootName", null); // NOI18N //System.err.println("reading: " + rootName); if (h == null) { // Cancel deserialization (e.g. of the ExplorerPanel window) in case the // root handle was not persistent: throw new SafeException(new IOException("Could not restore Explorer window; the root node \"" + rootName + "\" is not persistent; override Node.getHandle to fix")); // NOI18N } else { String[] exploredCtx = (String[]) fields.get ("explored", null); // NOI18N Object[] selPaths = (Object[]) fields.get ("selected", null); // NOI18N try { Node root = h.getNode (); if (root == null) throw new IOException("Node.Handle.getNode (for " + rootName + ") should not return null"); // NOI18N restoreSelection(root, exploredCtx, Arrays.asList(selPaths)); } catch (IOException ioe) { if (! Utilities.compareObjects (ioe.getMessage (), ioe.getLocalizedMessage ())) { // already localized throw new SafeException (ioe); } else { throw new SafeException (ioe) { public String getLocalizedMessage () { return NbBundle.getMessage (ExplorerManager.class, "EXC_handle_failed", rootName); } }; } } } } private void readObjectOld (Node.Handle h, ObjectInputStream ois) throws java.io.IOException, ClassNotFoundException { if (h == null) { // do nothing => should not occur to often and moreover this is also // dead code replaced by new version return; } else { String[] rootCtx = (String[]) ois.readObject(); String[] exploredCtx = (String[]) ois.readObject (); LinkedList ll = new LinkedList (); for (;;) { String[] path = (String[]) ois.readObject(); if (path == null) break; ll.add(path); } Node root = findPath (h.getNode (), rootCtx); restoreSelection(root, exploredCtx, ll); } } private void restoreSelection(final Node root, final String[] exploredCtx, final List selectedPaths /* of String[] */) { setRootContext(root); // XXX(-ttran) findPath() can take a long time and employs DataSystems // and others. We cannot call it synchrorously, in the past deadlocks // have happened because of this. OTOH as we call setSelectedNodes // asynchonously someone else can change the root context or the Node // hierarchy in between, which causes setSelectedNodes to throw // IllegalArgumentException. There seems to be no simple good // solution. For now we just catch IllegalArgumentException and be // decently silent about the fact. //RequestProcessor.getDefault().post(new Runnable() { // bugfix #38207, select nodes has to be called in AWT queue here Mutex.EVENT.readAccess (new Runnable() { public void run() { // convert paths to Nodes List selNodes = new ArrayList(selectedPaths.size()); for (Iterator iter = selectedPaths.iterator(); iter.hasNext(); ) { String[] path = (String[]) iter.next(); selNodes.add(findPath(root, path)); } // set the selection try { Node[] newSelection = (Node[]) selNodes.toArray(new Node[selNodes.size ()]); if (exploredCtx != null) { setExploredContext(findPath(root, exploredCtx), newSelection); } else { setSelectedNodes0(newSelection); } } catch (IllegalArgumentException ignore) {} } }); } /** * Finds the proper Explorer manager for a given component. This is done * by traversing the component hierarchy and finding the first ancestor * that implements {@link Provider}.

This method should be used in * {@link Component#addNotify} of each component that works with the * Explorer manager, e.g.: *

     * private transient ExplorerManager explorer;
     * 
     * public void addNotify () {
     *   super.addNotify ();
     *   explorer = ExplorerManager.find (this);
     * }
     * 
* * @param comp component to find the manager for * @return the manager, or a new empty manager if no ancestor implements * Provider * * @see Provider */ public static ExplorerManager find (Component comp) { // start looking for manager from parent, not the component itself for (;;) { comp = comp.getParent (); if (comp == null) { // create new explorer because nothing has been found return new ExplorerManager (); } if (comp instanceof Provider) { // ok, found a provider, return its manager return ((Provider)comp).getExplorerManager (); } } } /** Finds node by given path */ static Node findPath(Node r, String[] path) { try { return NodeOp.findPath(r, path); } catch (NodeNotFoundException ex) { return ex.getClosestNode(); } } /** Creates or retrieves RequestProcessor for selection updates. */ static synchronized RequestProcessor getSelectionProcessor() { if (selectionProcessor == null) { selectionProcessor = new RequestProcessor("ExplorerManager-selection"); //NOI18N } return selectionProcessor; } /** Finds ExplorerActionsImpl for a explorer manager. * @param em the manager * @return ExplorerActionsImpl */ static synchronized ExplorerActionsImpl findExplorerActionsImpl (ExplorerManager em) { if (em.actions == null) { em.actions = new ExplorerActionsImpl (); em.actions.attach (em); } return em.actions; } // // inner classes // /** Interface for components wishing to provide their own ExplorerManager. * @see ExplorerManager#find */ public static interface Provider { /** Get the explorer manager. * @return the manager */ public ExplorerManager getExplorerManager (); } /** Listener to be notified when root node has been destroyed. * Then the root node is changed to Node.EMPTY */ private class Listener extends NodeAdapter implements Runnable { Collection removeList = new HashSet(); Listener() {} /** Fired when the node is deleted. * @param ev event describing the node */ public void nodeDestroyed(NodeEvent ev) { if (ev.getNode ().equals (getRootContext ())) { // node has been deleted // [PENDING] better to show a node with a label such as "" // and a tool tip explaining the situation setRootContext (Node.EMPTY); } else { // assume that the node is among currently selected nodes scheduleRemove(ev.getNode()); } } /* Change in a node. * @param ev the event */ public void propertyChange (java.beans.PropertyChangeEvent ev) { if (propertySupport != null) { // replan fire of prop event to AWT-queue to get correctly visual-reactions if (SwingUtilities.isEventDispatchThread ()) { propertySupport.firePropertyChange (PROP_NODE_CHANGE, null, null); } else { SwingUtilities.invokeLater(new Runnable () { public void run () { propertySupport.firePropertyChange (PROP_NODE_CHANGE, null, null); } }); } } } /** Schedules removal of a node */ private void scheduleRemove(Node n) { synchronized (ExplorerManager.this) { if (selectionSyncTask == null) { selectionSyncTask = getSelectionProcessor().create(this); } else { selectionSyncTask.cancel(); } } synchronized (this) { removeList.add(n); } // invariant: selectionSyncTask != null && is not running yet. selectionSyncTask.schedule(SELECTION_SYNC_DELAY); } public void run() { Collection remove; synchronized (this) { // atomically clears the list while keeping a copy. // if another node is removed after this point, the selection // will be updated later. remove = removeList; removeList = new HashSet(); } LinkedList newSel = new LinkedList(Arrays.asList(getSelectedNodes())); Iterator it = remove.iterator (); while (it.hasNext ()) { Node n_remove = (Node)it.next (); if (newSel.contains (n_remove)) { // compare paths to root Node n_selection = (Node)newSel.get (newSel.indexOf (n_remove)); if (!Arrays.equals (NodeOp.createPath (n_remove, null), NodeOp.createPath (n_selection, null))) { it.remove (); } } } newSel.removeAll(remove); Node[] selNodes = (Node[]) newSel.toArray(new Node[newSel.size()]); setSelectedNodes0(selNodes); } } private static String getString(String key) { return NbBundle.getMessage(ExplorerManager.class,key); } }
... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.