Home | History | Annotate | Download | only in control
      1 /*
      2  * CDDL HEADER START
      3  *
      4  * The contents of this file are subject to the terms of the
      5  * Common Development and Distribution License (the "License").
      6  * You may not use this file except in compliance with the License.
      7  *
      8  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
      9  * or http://www.opensolaris.org/os/licensing.
     10  * See the License for the specific language governing permissions
     11  * and limitations under the License.
     12  *
     13  * When distributing Covered Code, include this CDDL HEADER in each
     14  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
     15  * If applicable, add the following below this CDDL HEADER, with the
     16  * fields enclosed by brackets "[]" replaced with your own identifying
     17  * information: Portions Copyright [yyyy] [name of copyright owner]
     18  *
     19  * CDDL HEADER END
     20  */
     21 
     22 /*
     23  * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
     24  * Use is subject to license terms.
     25  */
     26 
     27 package org.opensolaris.os.vp.panel.common.control;
     28 
     29 import java.io.UnsupportedEncodingException;
     30 import java.net.*;
     31 import java.util.*;
     32 import org.opensolaris.os.vp.panel.common.action.*;
     33 import org.opensolaris.os.vp.util.misc.Finder;
     34 
     35 /**
     36  * The {@code Control} class encapsulates the control over all aspects of a
     37  * single point in a navigation hierarchy.
     38  */
     39 public abstract class Control implements Navigable, HasControl {
     40     //
     41     // Enums
     42     //
     43 
     44     public enum UnsavedChangesAction {
     45 	CANCEL, DISCARD, SAVE
     46     }
     47 
     48     //
     49     // Static data
     50     //
     51 
     52     /**
     53      * The encoding used to encode/decode Control IDs and parameters in a path
     54      * string.
     55      */
     56     public static final String ENCODING = "UTF-8";
     57 
     58     //
     59     // Instance data
     60     //
     61 
     62     private String id;
     63     private String name;
     64     private Navigator navigator;
     65     private Map<String, String> parameters;
     66     private Control child;
     67 
     68     private StructuredAction<?, ?, ?> resetAction =
     69 	new StructuredAction<Object, Object, Object>(null) {
     70 	    @Override
     71 	    public Object work(Object pInput, Object rtInput)
     72 		throws ActionAbortedException, ActionFailedException {
     73 
     74 		resetAll();
     75 		return null;
     76 	    }
     77 	};
     78 
     79     private StructuredAction<?, ?, ?> saveAction =
     80 	new StructuredAction<Object, Object, Object>(null) {
     81 	    @Override
     82 	    public Object work(Object pInput, Object rtInput)
     83 		throws ActionAbortedException, ActionFailedException {
     84 
     85 		try {
     86 		    saveAll();
     87 		} catch (ActionUnauthorizedException e) {
     88 		    throw new ActionFailedException(e);
     89 		}
     90 		return null;
     91 	    }
     92 	};
     93 
     94     //
     95     // Constructors
     96     //
     97 
     98     /**
     99      * Constructs a {@code Control} with the given identifier and name.
    100      */
    101     public Control(String id, String name) {
    102 	setId(id);
    103 	setName(name);
    104     }
    105 
    106     /**
    107      * Constructs a {@code Control} with a {@code null} identifier and name.
    108      */
    109     public Control() {
    110 	this(null, null);
    111     }
    112 
    113     //
    114     // HasId methods
    115     //
    116 
    117     /**
    118      * Gets an identifier for this {@code Control}, sufficiently unique as to
    119      * distinguish itself from its siblings.
    120      */
    121     @Override
    122     public String getId() {
    123 	return id;
    124     }
    125 
    126     //
    127     // Navigable methods
    128     //
    129 
    130     /**
    131      * Gets the localized name of this {@code Control}.
    132      */
    133     @Override
    134     public String getName() {
    135 	return name;
    136     }
    137 
    138     /**
    139      * Gets the initialization parameters passed to the {@link #start} method,
    140      * if this {@code Control} is started, or {@code null} if this {@code
    141      * Control} is stopped.
    142      */
    143     @Override
    144     public Map<String, String> getParameters() {
    145 	return parameters;
    146     }
    147 
    148     //
    149     // HasControl methods
    150     //
    151 
    152     @Override
    153     public Control getControl() {
    154 	return this;
    155     }
    156 
    157     //
    158     // Object methods
    159     //
    160 
    161     @Override
    162     public String toString() {
    163 	return getName();
    164     }
    165 
    166     //
    167     // Control methods
    168     //
    169 
    170     /**
    171      * Saves the given child as the {@link #getRunningChild running child}.
    172      * Called by {@link #descendantStarted} when a child of this {@code Control}
    173      * is started.
    174      *
    175      * @exception   IllegalStateException
    176      *		    if the running child has already been set
    177      */
    178     public void childStarted(Control child) {
    179 	if (this.child != null) {
    180 	    throw new IllegalStateException("child already started");
    181 	}
    182 
    183 	this.child = child;
    184     }
    185 
    186     /**
    187      * Removes the given child as the {@link #getRunningChild running child}.
    188      * Called by {@link #descendantStopped} when a child of this {@code Control}
    189      * is stopped.
    190      *
    191      * @exception   IllegalStateException
    192      *		    if the given control is not the running child
    193      */
    194     public void childStopped(Control child) {
    195 	if (this.child != child) {
    196 	    throw new IllegalStateException("not running child");
    197 	}
    198 
    199 	this.child = null;
    200     }
    201 
    202     /**
    203      * Calls {@link #childStarted} iff the given path refers to an immediate
    204      * child of this {@code Control}.
    205      * <p/>
    206      * Called by the {@link Navigator} just after a descendant {@code Control}
    207      * of this {@code Control} has been started and pushed onto the {@link
    208      * Navigator}'s {@code Control} stack.
    209      *
    210      * @param	    path
    211      *		    the path to the descendant {@code Control}, relative to this
    212      *		    {@code Control} (with the just-started {@code Control} as
    213      *		    the last element)
    214      */
    215     public void descendantStarted(Control[] path) {
    216 	if (path.length == 1) {
    217 	    childStarted(path[0]);
    218 	}
    219     }
    220 
    221     /**
    222      * Calls {@link #childStopped} iff the given path refers to an immediate
    223      * child of this {@code Control}.
    224      * <p/>
    225      * Called by the {@link Navigator} just after a descendant {@code Control}
    226      * of this {@code Control} has been stopped and popped off the {@link
    227      * Navigator}'s {@code Control} stack.
    228      *
    229      * @param	    path
    230      *		    the path to the descendant {@code Control}, relative to this
    231      *		    {@code Control} (with the just-stopped {@code Control} as
    232      *		    the last element)
    233      */
    234     public void descendantStopped(Control[] path) {
    235 	if (path.length == 1) {
    236 	    childStopped(path[0]);
    237 	}
    238     }
    239 
    240     /**
    241      * Asynchronously navigates up one level above this {@code Control} in the
    242      * navigation stack.  The {@link #stop stop methods} of all affected {@code
    243      * Control}s are called with a {@code true} argument.
    244      */
    245     public void doCancel() {
    246 	getNavigator().goToAsync(true, this, Navigator.PARENT_NAVIGABLE);
    247     }
    248 
    249     /**
    250      * Asynchronously invokes this {@link Control}'s save action, then navigates
    251      * up one level in the navigation stack.
    252      */
    253     public void doOkay() {
    254 	final StructuredAction<?, ?, ?> saveAction = getSaveAction();
    255 	saveAction.asyncExec(
    256 	    new Runnable() {
    257 		@Override
    258 		public void run() {
    259 		    try {
    260 			saveAction.invoke();
    261 
    262 			getNavigator().goToAsync(false, Control.this,
    263 			    Navigator.PARENT_NAVIGABLE);
    264 		    } catch (ActionException ignore) {
    265 		    }
    266 		}
    267 	    });
    268     }
    269 
    270     /**
    271      * Gets a list of {@code Navigable}s that resolve to a child {@code Control}
    272      * of this {@code Control}.
    273      *
    274      * @return	    a non-{@code null} (but possibly empty) {@code Collection}
    275      */
    276     public abstract List<Navigable> getBrowsable();
    277 
    278     /**
    279      * Gets the child {@code Control} with the given identifier, creating it if
    280      * necessary.
    281      *
    282      * @param	    id
    283      *		    a unique identifier, as reported by the child {@code
    284      *		    Control}'s {@link #getId} method.
    285      *
    286      * @return	    a {@code Control} object, or {@code null} if no such child
    287      *		    is known
    288      */
    289     public abstract Control getChildControl(String id);
    290 
    291     /**
    292      * Gets a {@link Navigable} path to navigate to automatically when this
    293      * {@code Control} is the final destination of a navigation (not an
    294      * intermediate stop to another {@code Control}).  This method is called by
    295      * the {@link Navigator} <strong>after</strong> this {@code Control} has
    296      * been started.
    297      * <p/>
    298      * If the first element is {@code null}, the returned path is considered
    299      * absolute.  Otherwise, it is relative to this {@code Control}.
    300      * <p/>
    301      * This default implementation returns {@code null}.
    302      *
    303      * @return	    a {@link Navigable} array, or {@code null} if no automatic
    304      *		    forwarding should occur
    305      */
    306     public Navigable[] getForwardingPath() {
    307 	return null;
    308     }
    309 
    310     /**
    311      * Gets the help URL for this {@code DefaultControl}.
    312      * <p/>
    313      * This default implementation returns {@code null}.
    314      *
    315      * @return	    a URL, or {@code null} if no URL applies
    316      */
    317     public URL getHelpURL() {
    318 	return null;
    319     }
    320 
    321     /**
    322      * Gets the {@link Navigator} passed to the {@link #start} method, if
    323      * this {@code Control} is started, or {@code null} if this {@code Control}
    324      * is stopped.
    325      */
    326     public Navigator getNavigator() {
    327 	return navigator;
    328     }
    329 
    330     /**
    331      * Gets a {@link StructuredAction} that invokes {@link #resetAll}.
    332      */
    333     public StructuredAction<?, ?, ?> getResetAction() {
    334 	return resetAction;
    335     }
    336 
    337     /**
    338      * Gets the child {@code Control} currently running, or {@code null} if
    339      * there is none.
    340      */
    341     public Control getRunningChild() {
    342 	return child;
    343     }
    344 
    345     /**
    346      * Gets a {@link StructuredAction} that invokes {@link #saveAll}.
    347      */
    348     public StructuredAction<?, ?, ?> getSaveAction() {
    349 	return saveAction;
    350     }
    351 
    352     /**
    353      * Called by {@link #stop} when there are unsaved changes, gets the action
    354      * that should be taken to handle them.
    355      * <p/>
    356      * This default implementation returns {@link UnsavedChangesAction#DISCARD}.
    357      * Subclasses may wish to prompt the user to determine the appropriate
    358      * action to take.
    359      */
    360     protected UnsavedChangesAction getUnsavedChangesAction() {
    361 	return UnsavedChangesAction.DISCARD;
    362     }
    363 
    364     /**
    365      * Gets a hint as to whether this {@code Control} should be returned by a
    366      * parent {@code Control}'s {@link #getBrowsable} method.  The parent may
    367      * choose to ignore this hint.
    368      * <p/>
    369      * This default implementation returns {@code true}.
    370      */
    371     public boolean isBrowsable() {
    372 	return true;
    373     }
    374 
    375     /**
    376      * Indicates whether there are any unsaved changes in this {@code Control}.
    377      * <p/>
    378      * This default implementation returns {@code false}.
    379      */
    380     protected boolean isChanged() {
    381 	return false;
    382     }
    383 
    384     public boolean isStarted() {
    385 	return navigator != null;
    386     }
    387 
    388     /**
    389      * If appropriate, resets this {@code Control}, discarding any pending
    390      * changes.
    391      * <p/>
    392      * This default implementation does nothing.
    393      *
    394      * @exception   ActionAbortedException
    395      *		    if this operation is cancelled
    396      *
    397      * @exception   ActionFailedException
    398      *		    if this operation fails
    399      */
    400     protected void reset() throws ActionAbortedException, ActionFailedException
    401     {
    402     }
    403 
    404     /**
    405      * {@link #reset Reset}s all {@code Control}s from the top of the navigation
    406      * stack to this {@link Control}, discarding any pending changes.
    407      *
    408      * @exception   ActionAbortedException
    409      *		    see {@link #reset}
    410      *
    411      * @exception   ActionFailedException
    412      *		    see {@link #reset}
    413      *
    414      * @exception   IllegalStateException
    415      *		    if this {@link Control} is not started
    416      */
    417     protected void resetAll() throws ActionAbortedException,
    418 	ActionFailedException {
    419 
    420 	assertStartState(true);
    421 
    422 	List<Control> path = navigator.getPath();
    423 	if (!path.contains(this)) {
    424 	    throw new IllegalStateException();
    425 	}
    426 
    427 	for (int i = path.size() - 1; i >= 0; i--) {
    428 	    Control control = path.get(i);
    429 	    control.reset();
    430 	    if (control == this) {
    431 		break;
    432 	    }
    433 	}
    434     }
    435 
    436     /**
    437      * If appropriate, saves any changes made while this {@code Control} is
    438      * running.
    439      * <p/>
    440      * This default implementation does nothing.
    441      *
    442      * @exception   ActionAbortedException
    443      *		    if this operation is cancelled
    444      *
    445      * @exception   ActionFailedException
    446      *		    if this operation fails
    447      *
    448      * @exception   ActionUnauthorizedException
    449      *		    if the current user has insufficient privileges for this
    450      *		    operation
    451      */
    452     protected void save() throws ActionAbortedException, ActionFailedException,
    453 	ActionUnauthorizedException {
    454     }
    455 
    456     /**
    457      * {@link #save Save}s all {@code Control}s from the top of the navigation
    458      * stack to this {@link Control}.
    459      *
    460      * @exception   ActionAbortedException
    461      *		    see {@link #save}
    462      *
    463      * @exception   ActionFailedException
    464      *		    see {@link #save}
    465      *
    466      * @exception   ActionUnauthorizedException
    467      *		    see {@link #save}
    468      *
    469      * @exception   IllegalStateException
    470      *		    if this {@link Control} is not started
    471      */
    472     protected void saveAll() throws ActionAbortedException,
    473 	ActionFailedException, ActionUnauthorizedException {
    474 
    475 	assertStartState(true);
    476 
    477 	List<Control> path = navigator.getPath();
    478 	if (!path.contains(this)) {
    479 	    throw new IllegalStateException();
    480 	}
    481 
    482 	for (int i = path.size() - 1; i >= 0; i--) {
    483 	    Control control = path.get(i);
    484 	    if (control.isChanged()) {
    485 		control.save();
    486 	    }
    487 	    if (control == this) {
    488 		break;
    489 	    }
    490 	}
    491     }
    492 
    493     /**
    494      * Sets the identifier for this {@code Control}.
    495      */
    496     protected void setId(String id) {
    497 	this.id = id;
    498     }
    499 
    500     /**
    501      * Sets the name for this {@code Control}.
    502      */
    503     protected void setName(String name) {
    504 	this.name = name;
    505     }
    506 
    507     /**
    508      * Saves references to the given {@link #getNavigator Navigator} and {@link
    509      * #getParameters initialization parameters}.
    510      * <p/>
    511      * Called by the {@link Navigator} when this {@code Control} is pushed onto
    512      * the {@code Control} stack.
    513      *
    514      * @param	    navigator
    515      *		    the {@link Navigator} that handles navigation to/from this
    516      *		    {@code Controls}
    517      *
    518      * @param	    parameters
    519      *		    non-{@code null}, but optional (may be empty) initialization
    520      *		    parameters
    521      *
    522      * @exception   NavigationAbortedException
    523      *		    if this {@code Control} could not be started due to the
    524      *		    action being cancelled or vetoed
    525      *
    526      * @exception   InvalidParameterException
    527      *		    if this {@code Control} could not be started due to invalid
    528      *		    intialization parameters
    529      *
    530      * @exception   IllegalStateException
    531      *		    if this {@link Control} is already started
    532      */
    533     public void start(Navigator navigator, Map<String, String> parameters)
    534 	throws NavigationAbortedException, InvalidParameterException {
    535 
    536 	assertStartState(false);
    537 	this.navigator = navigator;
    538 	this.parameters = parameters;
    539     }
    540 
    541     /**
    542      * If {@code isCancel} is {@code false}, saves, resets, or cancels changes
    543      * {@link #isChanged if necessary}, based on the return value of {@link
    544      * #getUnsavedChangesAction}.  Then resets the references to the {@link
    545      * #getNavigator Navigator} and {@link #getParameters initialization
    546      * parameters}.
    547      * <p/>
    548      * Called by the {@link Navigator} prior to this {@code Control} being
    549      * removed as the current {@code Control}.
    550      *
    551      * @param	    isCancel
    552      *		    {@code true} if this {@code Control} is being stopped as
    553      *		    part of a {@code cancel} operation, {@code false} otherwise
    554      *
    555      * @exception   NavigationAbortedException
    556      *		    if this {@code Control} should remain the current {@code
    557      *		    Control}
    558      *
    559      * @exception   IllegalStateException
    560      *		    if this {@link Control} is not started
    561      */
    562     public void stop(boolean isCancel) throws NavigationAbortedException {
    563 	assertStartState(true);
    564 
    565 	if (!isCancel && isChanged()) {
    566 	    try {
    567 		switch (getUnsavedChangesAction()) {
    568 		    case SAVE:
    569 			getSaveAction().invoke();
    570 			break;
    571 
    572 		    case DISCARD:
    573 			getResetAction().invoke();
    574 			break;
    575 
    576 		    default:
    577 		    case CANCEL:
    578 			throw new NavigationAbortedException();
    579 		}
    580 
    581 	    // Thrown by invoke()
    582 	    } catch (ActionException e) {
    583 		throw new NavigationAbortedException(e);
    584 	    }
    585 	}
    586 
    587 	this.navigator = null;
    588 	this.parameters = null;
    589     }
    590 
    591     //
    592     // Private methods
    593     //
    594 
    595     private void assertStartState(boolean started) {
    596 	if (isStarted() != started) {
    597 	    throw new IllegalStateException(started ? "control started" :
    598 		"control not started");
    599 	}
    600     }
    601 
    602     //
    603     // Static methods
    604     //
    605 
    606     /**
    607      * Builds a help URL from a page and section, falling back to the
    608      * page if the section is invalid.
    609      */
    610     protected static URL buildHelpURL(String page, String section) {
    611 	URL url = Finder.getResource(page);
    612 	try {
    613 	    url = new URL(url, section);
    614 	} catch (MalformedURLException ignore) {
    615 	}
    616 	return url;
    617     }
    618 
    619     /**
    620      * Decodes the given encoded {@code String} into an identifier and
    621      * parameters, encapsulated by a {@link SimpleNavigable}.
    622      *
    623      * @param	    encoded
    624      *		    an {@link #encode encode}d {@code String}
    625      *
    626      * @return	    a {@link SimpleNavigable}
    627      */
    628     public static SimpleNavigable decode(String encoded) {
    629 	String[] elements = encoded.split("\\?", 2);
    630 	String id = elements[0];
    631 	Map<String, String> parameters = new HashMap<String, String>();
    632 
    633 	try {
    634 	    id = URLDecoder.decode(elements[0], ENCODING);
    635 	} catch (UnsupportedEncodingException ignore) {
    636 	}
    637 
    638 	if (elements.length >= 2 && !elements[1].isEmpty()) {
    639 	    String[] keyEqVals = elements[1].split("&");
    640 
    641 	    for (String keyEqVal : keyEqVals) {
    642 		String[] nvPair = keyEqVal.split("=", 2);
    643 		String key = nvPair[0];
    644 		String value = nvPair.length < 2 ? "" : nvPair[1];
    645 
    646 		try {
    647 		    key = URLDecoder.decode(key, ENCODING);
    648 		    value = URLDecoder.decode(value, ENCODING);
    649 		} catch (UnsupportedEncodingException ignore) {
    650 		}
    651 
    652 		parameters.put(key, value);
    653 	    }
    654 	}
    655 
    656 	return new SimpleNavigable(id, null, parameters);
    657     }
    658 
    659     /**
    660      * Encodes the given identifier and parameters.
    661      *
    662      * @param	    id
    663      *		    a {@link Control#getId Control identifier}
    664      *
    665      * @param	    parameters
    666      *		    initialization parameters, or {@code null} if no parameters
    667      *		    apply
    668      *
    669      * @return	    an encoded String
    670      */
    671     public static String encode(String id, Map<String, String> parameters) {
    672 	StringBuffer buffer = new StringBuffer();
    673 
    674 	try {
    675 	    buffer.append(URLEncoder.encode(id, ENCODING));
    676 
    677 	    if (parameters != null && !parameters.isEmpty()) {
    678 		buffer.append("?");
    679 		boolean first = true;
    680 
    681 		for (String key : parameters.keySet()) {
    682 		    if (first) {
    683 			first = false;
    684 		    } else {
    685 			buffer.append("&");
    686 		    }
    687 
    688 		    String value = parameters.get(key);
    689 		    if (value == null) {
    690 			value = "";
    691 		    } else {
    692 			value = URLEncoder.encode(value, ENCODING);
    693 		    }
    694 
    695 		    key = URLEncoder.encode(key, ENCODING);
    696 
    697 		    buffer.append(key).append("=").append(value);
    698 		}
    699 	    }
    700 	} catch (UnsupportedEncodingException ignore) {
    701 	}
    702 
    703 	return buffer.toString();
    704     }
    705 }
    706