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