--- /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;
+ }
+}