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