usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/Navigator.java
changeset 0 62ac12e07fc0
child 19 9c1f74f86687
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/usr/src/java/vpanels/panel/org/opensolaris/os/vp/panel/Navigator.java	Mon Jul 14 20:29:30 2008 -0700
@@ -0,0 +1,526 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License (the "License").
+ * You may not use this file except in compliance with the License.
+ *
+ * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+ * or http://www.opensolaris.org/os/licensing.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ */
+
+/*
+ * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
+ * Use is subject to license terms.
+ *
+ * ident	"@(#)Navigator.java	1.19	08/06/26 SMI"
+ */
+
+package org.opensolaris.os.vp.panel;
+
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.regex.Pattern;
+import org.opensolaris.os.vp.panel.Navigable.Util;
+import org.opensolaris.os.vp.util.*;
+
+public class Navigator {
+    //
+    // Static data
+    //
+
+    public static final String PATH_SEPARATOR = "/";
+    public static final String PARENT_ID = "..";
+
+    // Use a thread pool to autmatically handle uncaught exceptions and queued
+    // requests
+    private static final ThreadPoolExecutor navThreadPool;
+    static {
+	// Unbounded
+	BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
+
+	navThreadPool = new ThreadPoolExecutor(
+	    1, 1, 10, TimeUnit.MINUTES, queue);
+
+	navThreadPool.allowCoreThreadTimeOut(true);
+    }
+
+    //
+    // Instance data
+    //
+
+    private StackList<Control> stack = new StackList<Control>();
+    private List<Control> roStack = Collections.unmodifiableList(stack);
+
+    private List<NavigationListener> listeners =
+	new ArrayList<NavigationListener>();
+
+    private List<NavigationListener> roListeners =
+	Collections.unmodifiableList(listeners);
+
+    //
+    // Constructors
+    //
+
+    /**
+     * Constructs a {@code Navigator} with the given initial root.
+     *
+     * @param	    root
+     *		    the intial root {@link Control}
+     *
+     * @param	    parameters
+     *		    initialization parameters for the root {@link Control}, or
+     *		    {@code null} if no parameters apply
+     *
+     * @exception   ActionAbortedException
+     *		    see {@link #goTo(Control,Navigable...)}
+     *
+     * @exception   InvalidAddressException
+     *		    see {@link #goTo(Control,Navigable...)}
+     *
+     * @exception   MissingParameterException
+     *		    see {@link #goTo(Control,Navigable...)}
+     *
+     * @exception   InvalidParameterException
+     *		    see {@link #goTo(Control,Navigable...)}
+     *
+     * @exception   IllegalArgumentException
+     *		    see {@link #goTo(Control,Navigable...)}
+     */
+    public Navigator(Control root, Map<String, String> parameters)
+	throws ActionAbortedException, InvalidAddressException,
+	MissingParameterException, InvalidParameterException {
+
+	if (parameters == null) {
+	    parameters = Collections.emptyMap();
+	} else {
+	    parameters = Collections.unmodifiableMap(parameters);
+	}
+
+	root.start(this, parameters);
+	stack.push(root);
+
+	String forward = root.getForwardingPath();
+	if (forward != null) {
+	    SimpleNavigable[] path = toArray(forward);
+	    Control relativeTo = isAbsolute(forward) ? null : root;
+	    goTo(relativeTo, path);
+	}
+    }
+
+    //
+    // Navigator methods
+    //
+
+    /**
+     * Adds a {@link NavigationListener} to be notified when navigation is
+     * stopped and started.
+     */
+    public void addNavigationListener(NavigationListener l) {
+	listeners.add(l);
+    }
+
+    /**
+     * Runs the given {@code Runnable} on this {@code Navigator}'s navigation
+     * thread.	This thread is intended to be used specifically for
+     * navigation; the given {@code Runnable} should thus be limited to calling
+     * {@link #goTo(Control,Navigable...)} and (briefly) handling any thrown
+     * exceptions.
+     * <p>
+     * Notes:
+     * <ol>
+     *	 <li>
+     *	   If this method is called when the navigation thread is busy, the
+     *	   given {@code Runnable} will be queued and run when the thread is
+     *	   available.
+     *	 </li>
+     *	 <li>
+     *	   Navigation does not <i>have</i> to occur on this thread, of course.
+     *	   It is provided here solely as a convenience for asynchronous
+     *	   navigation.
+     *	 </li>
+     * </ol>
+     * <p>
+     *
+     * @param	    r
+     *		    the {@code Runnable} to navigate and handle any resulting
+     *		    exceptions
+     */
+    public void asynchExec(Runnable r) {
+	navThreadPool.execute(r);
+    }
+
+    /**
+     * Notifies all registered {@link NavigationListener}s that a navigation has
+     * begun.
+     */
+    protected void fireNavigationStarted(NavigationEvent e) {
+	// Iterate backwards to allow listener removal during iteration
+	for (int i = listeners.size() - 1; i >= 0; i--) {
+	    listeners.get(i).navigationStarted(e);
+	}
+    }
+
+    /**
+     * Notifies all registered {@link NavigationListener}s that a navigation has
+     * stopped.
+     */
+    protected void fireNavigationStopped(NavigationEvent e) {
+	// Iterate backwards to allow listener removal during iteration
+	for (int i = listeners.size() - 1; i >= 0; i--) {
+	    listeners.get(i).navigationStopped(e);
+	}
+    }
+
+    public List<NavigationListener> getNavigationListeners() {
+	return roListeners;
+    }
+
+    /**
+     * Gets the current path.
+     */
+    public List<Control> getPath() {
+	synchronized (stack) {
+	    return roStack;
+	}
+    }
+
+    /**
+     * Gets the current path as a {@code String}.
+     */
+    public String getPathString() {
+	synchronized (stack) {
+	    return getPathString(stack);
+	}
+    }
+
+    /**
+     * Navigates to the {@link Control} identified by the given path.
+     *
+     * @param	    relativeTo
+     *		    a {@link Control} within the navigation stack to which
+     *		    {@code path} is relative, or {@code null} if {@code path} is
+     *		    absolute
+     *
+     * @param	    path
+     *		    the path (relative to {@code relativeTo}), or unspecified to
+     *		    navigate up the stack to {@code relativeTo}
+     *
+     * @return	    the previously current {@link Control}
+     *
+     * @exception   ActionAbortedException
+     *		    if the navigation is vetoed
+     *
+     * @exception   InvalidAddressException
+     *		    if {@code path} does not refer to a valid address
+     *
+     * @exception   MissingParameterException
+     *		    if some {@link Control} in the process of navigation could
+     *		    not be {@link Control#start started} due to a missing
+     *		    intialization parameter
+     *
+     * @exception   InvalidParameterException
+     *		    if some {@link Control} in the process of navigation could
+     *		    not be {@link Control#start started} due to an invalid
+     *		    intialization parameter
+     *
+     * @exception   IllegalArgumentException
+     *		    if {@code relativeTo} is not in the navigation stack
+     */
+    public Control goTo(Control relativeTo, Navigable... path)
+	throws ActionAbortedException, InvalidAddressException,
+	MissingParameterException, InvalidParameterException {
+
+	synchronized (stack) {
+	    // Last absolute path
+	    StackList<Navigable> laPath = new StackList<Navigable>();
+	    laPath.addAll(stack);
+
+	    while (true) {
+		// Current absolute path
+		StackList<Navigable> caPath = new StackList<Navigable>();
+
+		if (relativeTo != null) {
+		    caPath.addAll(laPath);
+		    try {
+			// Remove path elements until relativeTo is at the top
+			while (!((HasControl)caPath.peek()).getControl().
+			    equals(relativeTo)) {
+			    caPath.pop();
+			}
+		    } catch (EmptyStackException e) {
+			throw new IllegalArgumentException(String.format(
+			    "Control not in navigation path: %s (%s)",
+			    relativeTo.getClass().getName(),
+			    relativeTo.getId()));
+		    }
+		}
+
+		if (path != null && path.length != 0) {
+		    CollectionUtil.addAll(caPath, path);
+
+		    // Remove unnecessary ".." segments
+		    normalize(caPath);
+		}
+
+		List<Navigable> rPath = getRelativePath(laPath, caPath);
+		if (rPath.isEmpty()) {
+		    break;
+		}
+
+		// Iterate through rPath, adding/removing Controls
+		// to/from laPath as appropriate
+		for (Navigable hIP : rPath) {
+		    if (hIP.getId().equals(PARENT_ID)) {
+			switch (laPath.size()) {
+			    case 0:
+				// Should be impossible
+				throw new IllegalArgumentException(
+				    "navigation stack has no root");
+
+			    case 1:
+				// Tried to pop root off navigation stack
+				throw new InvalidAddressException(
+				    getPathString(caPath));
+			}
+
+			laPath.pop();
+		    } else {
+			String id = hIP.getId();
+
+			Object last = laPath.peek();
+
+			// laPath elements are Controls or PendingControls
+			Control curControl = ((HasControl)last).getControl();
+			Control newControl = curControl.getChildControl(id);
+
+			if (newControl == null) {
+			    throw new InvalidAddressException(
+				String.format("%s%s%s", getPathString(laPath),
+				PATH_SEPARATOR, id));
+			}
+
+			PendingControl pair =
+			    new PendingControl(newControl, hIP.getParameters());
+
+			laPath.push(pair);
+		    }
+		}
+
+		Control control = ((HasControl)laPath.peek()).getControl();
+		String forward = control.getForwardingPath();
+		if (forward == null) {
+		    break;
+		}
+
+		path = toArray(forward);
+		relativeTo = isAbsolute(forward) ? null : control;
+	    }
+
+	    // The original Control, before navigation
+	    Control orig = peek();
+
+	    // Mildly hackish
+	    List tmp = getRelativePath(stack, laPath);
+
+	    @SuppressWarnings({"unchecked"})
+	    List<PendingControl> rPath = (List<PendingControl>)tmp;
+
+	    if (!rPath.isEmpty()) {
+		NavigationEvent event = new NavigationEvent(this,
+		    Collections.unmodifiableList(rPath));
+		fireNavigationStarted(event);
+
+		try {
+		    for (PendingControl pend : rPath) {
+			Control curControl = peek();
+
+			if (pend.getId().equals(PARENT_ID)) {
+			    curControl.stop();
+			    stack.pop();
+			    peek().childStopped(curControl);
+			} else {
+			    Control newControl = pend.getControl();
+
+			    newControl.start(this, pend.getParameters());
+			    stack.push(newControl);
+
+			    curControl.childStarted(newControl);
+			}
+
+			if (System.getProperty("vpanels.debug.navigator") !=
+			    null) {
+			    System.out.printf("%s\n", getPathString());
+			}
+		    }
+		} finally {
+		    fireNavigationStopped(event);
+		}
+	    }
+
+	    return orig;
+	}
+    }
+
+    /**
+     * Shortcut for:
+     * <p>
+     *	 <code>
+     *	   {@link #goTo(Control,Navigable...) goTo}(relativeTo,
+     *	   new {@link SimpleNavigable#SimpleNavigable(String,Map)
+     *	   SimpleNavigable}(id, {@link Navigable.Util#toMap
+     *	   Navigable.Util.toMap}(parameters)));
+     *	 </code>
+     * </p>
+     */
+    public Control goTo(Control relativeTo, String id,
+	String... parameters) throws ActionAbortedException,
+	InvalidAddressException, MissingParameterException,
+	InvalidParameterException {
+
+	return goTo(relativeTo,
+	    new SimpleNavigable(id, Navigable.Util.toMap(parameters)));
+    }
+
+    /**
+     * Returns the current {@link Control}.
+     */
+    public Control peek() {
+	synchronized (stack) {
+	    return stack.peek();
+	}
+    }
+
+    /**
+     * Navigate to the parent of the current {@link Control}.
+     *
+     * @return	    the previously current {@link Control}
+     *
+     * @exception   ActionAbortedException
+     *		    if the navigation is vetoed
+     *
+     * @exception   IllegalArgumentException
+     *		    if the navigation stack consists only of the root element
+     */
+    public Control pop() throws ActionAbortedException {
+	try {
+	    return goTo(peek(), PendingControl.PARENT);
+	} catch (InvalidAddressException ignore) {
+	    // Should not be possible
+	} catch (InvalidParameterException ignore) {
+	    // Should not be possible
+	}
+	return null;
+    }
+
+    /**
+     * Removes a {@link NavigationListener} from notification of when navigation
+     * is stopped and started.
+     */
+    public boolean removeNavigationListener(NavigationListener l) {
+	return listeners.remove(l);
+    }
+
+    //
+    // Private methods
+    //
+
+    /**
+     * Gets the relative path between the given absolute paths.
+     *
+     * @param	    fromPath
+     *		    an absolute source path
+     *
+     * @param	    toPath
+     *		    an absolute destination path
+     *
+     * @return	    a relative path from the current path to the given path
+     */
+    private List<Navigable> getRelativePath(
+	List<? extends Navigable> fromPath,
+	List<? extends Navigable> toPath) {
+
+	List<Navigable> rPath = new ArrayList<Navigable>();
+
+	// Determine branching index
+	int branch = 0;
+	while (branch < fromPath.size() && branch < toPath.size() &&
+	    Navigable.Util.equals(fromPath.get(branch), toPath.get(branch))) {
+
+	    branch++;
+	}
+
+	for (int i = fromPath.size() - 1; i >= branch; i--) {
+	    rPath.add(PendingControl.PARENT);
+	}
+
+	for (int i = branch; i < toPath.size(); i++) {
+	    rPath.add(toPath.get(i));
+	}
+
+	return rPath;
+    }
+
+    /**
+     * Removes each ".." segment and preceding non-".." segment the given path.
+     */
+    private void normalize(List<? extends HasId> path) {
+	for (int i = 1; i < path.size(); i++) {
+	    if (path.get(i).getId().equals(PARENT_ID) &&
+		!path.get(i - 1).getId().equals(PARENT_ID)) {
+		path.remove(i--);
+		path.remove(i--);
+	    }
+	}
+    }
+
+    //
+    // Static methods
+    //
+
+    /**
+     * Gets the given path as a {@code String}.
+     */
+    public static String getPathString(Collection<? extends Navigable> path) {
+	StringBuffer buffer = new StringBuffer();
+
+	for (Navigable hIP : path) {
+	    buffer.append(PATH_SEPARATOR).append(Control.encode(
+		hIP.getId(), hIP.getParameters()));
+	}
+
+	return buffer.toString();
+    }
+
+    /**
+     * Determines whether the given path is absolute.  If the given path starts
+     * with PATH_SEPARATOR, it is considered absolute.
+     */
+    public static boolean isAbsolute(String path) {
+	return path.startsWith(PATH_SEPARATOR);
+    }
+
+    public static SimpleNavigable[] toArray(String path) {
+	path = path.replaceFirst(
+	    "^(" + Pattern.quote(PATH_SEPARATOR) + ")+", "");
+
+	String[] parts = path.split(PATH_SEPARATOR, 0);
+	SimpleNavigable[] elements = new SimpleNavigable[parts.length];
+
+	for (int i = 0; i < parts.length; i++) {
+	    elements[i] = Control.decode(parts[i]);
+	}
+
+	return elements;
+    }
+}