1 /* 2 * $Id$ 3 * 4 * Copyright (c) 2006, 2013, Oracle and/or its affiliates. All rights reserved. 5 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 6 * 7 * This code is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License version 2 only, as 9 * published by the Free Software Foundation. Oracle designates this 10 * particular file as subject to the "Classpath" exception as provided 11 * by Oracle in the LICENSE file that accompanied this code. 12 * 13 * This code is distributed in the hope that it will be useful, but WITHOUT 14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 16 * version 2 for more details (a copy is included in the LICENSE file that 17 * accompanied this code). 18 * 19 * You should have received a copy of the GNU General Public License version 20 * 2 along with this work; if not, write to the Free Software Foundation, 21 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 22 * 23 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 24 * or visit www.oracle.com if you need additional information or have any 25 * questions. 26 */ 27 package com.sun.interview; 28 29 import java.io.File; 30 import java.io.IOException; 31 import java.io.StringReader; 32 import java.io.StringWriter; 33 import java.util.*; 34 35 import com.sun.interview.Interview.Fault; 36 37 // todo: 38 39 // how to sort values 40 // how to sort groups for display 41 // table headers - default, with set methods 42 // improper use of default value (not used) 43 44 /** 45 * A {@link Question question} which consists of many key-value pairs. The 46 * values are altered by the user, the key is immutable. The output of this 47 * question is a Properties object. The key in the properties object is always 48 * the internal name, not the internationalized name. If internationalized 49 * key names are supplied, they are used only for presentation. 50 * 51 * The presentation info is store here instead of in a renderer class because 52 * multiple clients need to render this question. 53 * 54 * @since 4.0 55 */ 56 public abstract class PropertiesQuestion extends CompositeQuestion 57 { 58 /** 59 * Create a question with a nominated tag. 60 * If this constructor is used, the choices must be supplied separately. 61 * @param interview The interview containing this question. 62 * @param tag A unique tag to identify this specific question. 63 */ 64 protected PropertiesQuestion(Interview interview, String tag) { 65 super(interview, tag); 66 clear(); 67 setDefaultValue(value); 68 } 69 70 /** 71 * Create a question with a nominated tag. Not recommended since this is 72 * not internationalized. 73 * @param interview The interview containing this question. 74 * @param tag A unique tag to identify this specific question. 75 * @param props The literal keys and values. A shallow copy of this is used. 76 * @throws NullPointerException if choices is null 77 */ 78 protected PropertiesQuestion(Interview interview, String tag, Properties props) { 79 super(interview, tag); 80 81 setProperties(props); 82 setDefaultValue(value); 83 } 84 85 /** 86 * Create a question with a nominated tag. The keys must be registered in the 87 * resource bundle for this question for internationalization purposes. They 88 * will be looked up by this questions tag, plus each of the key values. 89 * @param interview The interview containing this question. 90 * @param tag A unique tag to identify this specific question. 91 * @param keys Internal name of the keys 92 * @throws NullPointerException if choices is null 93 */ 94 protected PropertiesQuestion(Interview interview, String tag, String[] keys) { 95 super(interview, tag); 96 97 String blank = ""; 98 setKeys(keys, true); 99 setDefaultValue(value); 100 } 101 102 /** 103 * The current value will be set to all false; 104 * @param props The set of names for the choices for this question. 105 * @see #getValue 106 * @throws NullPointerException if choices is null 107 */ 108 protected void setProperties(Properties props) { 109 value = ((Properties)props.clone()); 110 // clobber display values? 111 } 112 113 /** 114 * Set the keys to be shown in the properties table. Previous properties 115 * are removed, and the new values are all the empty string. 116 * The current value will be set to an empty string. 117 * @param keys The set of names of the choices for this question. 118 * @param localize if false, the choices will be used directly 119 * as the display choices; otherwise the choices will be used 120 * to construct keys to get localized values from the interview's 121 * resource bundle. 122 * @see #getKeys 123 * @throws NullPointerException if choices is null 124 */ 125 protected void setKeys(String[] keys, boolean localize) { 126 value = new Properties(); 127 String blank = ""; 128 129 // populate the properties object 130 for (int i = 0; i < keys.length; i++) { 131 value.put(keys[i], blank); 132 } 133 134 ResourceBundle b = interview.getResourceBundle(); 135 if (!localize || b == null) // will use literal keys 136 presentationKeys = null; 137 else { 138 presentationKeys = new HashMap<>(); 139 for (int i = 0; i < keys.length; i++) { 140 String c = keys[i]; 141 String rn = tag + "." + c; 142 try { 143 presentationKeys.put(keys[i], (c == null ? null : b.getString(rn))); 144 } 145 catch (MissingResourceException e) { 146 System.err.println("WARNING: missing resource " + rn); 147 presentationKeys.put(keys[i], c); 148 } 149 } // for 150 } // else 151 } 152 153 /** 154 * Get the set of keys currently used this question. Includes all hidden and 155 * read-only values as well. 156 * @return The set of keys (internal non-i18n value) 157 * @see #setKeys 158 */ 159 public Enumeration getKeys() { 160 if (value != null) 161 return value.keys(); 162 else 163 return null; 164 } 165 166 /** 167 * Get the default response for this question. 168 * @return the default response for this question. 169 * 170 * @see #setDefaultValue 171 */ 172 public Properties getDefaultValue() { 173 return defaultValue; 174 } 175 176 /** 177 * Set the default response for this question, 178 * used by the clear method. 179 * @param props the default response for this question. 180 * 181 * @see #getDefaultValue 182 */ 183 public void setDefaultValue(Properties props) { 184 defaultValue = props; 185 } 186 187 /** 188 * Get the current (default or latest) response to this question. 189 * @return The current value - a cloned copy. 190 * @see #setValue 191 * @throws IllegalStateException if no choices have been set, defining 192 * the set of responses to this question 193 */ 194 public Properties getValue() { 195 if (value == null) 196 return null; 197 198 return (Properties)(value.clone()); 199 } 200 201 /** 202 * Verify this question is on the current path, and if it is, 203 * return the current value. 204 * @return the current value of this question 205 * @throws Interview.NotOnPathFault if this question is not on the 206 * current path 207 * @see #getValue 208 */ 209 public Properties getValueOnPath() throws Interview.NotOnPathFault { 210 interview.verifyPathContains(this); 211 return getValue(); 212 } 213 214 public String getStringValue() { 215 StringBuffer result = new StringBuffer(); 216 if (value != null) { 217 String sep = System.getProperty("line.separator"); 218 219 Enumeration names = value.propertyNames(); 220 ArrayList list = Collections.list(names); 221 Collections.sort(list); 222 for (Object o : list) { 223 String key = (String)o; 224 result.append(key); 225 result.append("="); 226 result.append(value.getProperty(key)); 227 result.append(sep); 228 } 229 } 230 231 return result.toString(); 232 } 233 234 /** 235 * Set the current value. 236 * @param newValue Value represented as a single string. May not be null. May be 237 * an empty string. 238 * @see #getValue 239 */ 240 public void setValue(String newValue) { 241 if (value == null || value.size() == 0) 242 return; 243 244 // parse newValue and inject into properties object 245 if (newValue == null) 246 throw new NullPointerException(); 247 248 setValue(load(newValue)); 249 } 250 251 /** 252 * Set a specific property within this question. 253 * The property must exist before it can be set, else a 254 * Fault is thrown to prevent unauthorized additions of 255 * properties to the question value. 256 * @param key the key of the property to set, must not be null 257 * @param v the new value for the property, must not be null 258 * @throws Interview.Fault if the key does not already exist 259 * in the question's value. 260 * @throws NullPointerException if either parameter is null 261 * @throws Interview.Fault if the specified key does not exist in the question 262 */ 263 public void setValue(String key, String v) throws Interview.Fault { 264 if (key == null || v == null) 265 throw new NullPointerException(); 266 267 String check = value.getProperty(key); 268 if (check == null) 269 throw new Fault(Interview.i18n, "props.badSubval"); 270 value.put(key, v); 271 272 interview.updatePath(this); 273 interview.setEdited(true); 274 } 275 276 public boolean isValueValid() { 277 String[][] badVals = getInvalidKeys(); 278 279 if (badVals == null) 280 return true; 281 else 282 return false; 283 } 284 285 public boolean isValueAlwaysValid() { 286 return false; 287 } 288 289 /** 290 * Clear any response to this question, resetting the value 291 * back to its initial state. 292 */ 293 public void clear() { 294 setValue(defaultValue); 295 } 296 297 /** 298 * Load the value for this question from a dictionary, using 299 * the tag as the key. 300 * @param data The map from which to load the value for this question. 301 */ 302 protected void load(Map<String, String> data) { 303 String o = data.get(tag); 304 if (o != null) { 305 setValue(load(o)); 306 } 307 } 308 309 protected static Properties load(String s) { 310 Properties2 p2 = new Properties2(); 311 312 try { 313 p2.load(new StringReader(s)); 314 } catch (IOException e) { 315 //e.printStackTrace(); 316 // what to do?! source should be really stable since it is a 317 // String 318 } 319 320 // repopulate a J2SE properties object 321 Properties p = new Properties(); 322 323 Enumeration e = p2.propertyNames(); 324 while(e.hasMoreElements()) { 325 Object next = e.nextElement(); 326 p.put( next, p2.get(next) ); 327 } // while 328 329 return p; 330 } 331 332 /** 333 * Save the value for this question in a dictionary, using 334 * the tag as the key. 335 * @param data The map in which to save the value for this question. 336 */ 337 protected void save(Map<String, String> data) { 338 if (value == null) 339 return; 340 341 Properties2 p2 = new Properties2(); 342 p2.load(value); 343 StringWriter sw = new StringWriter(); 344 p2.save(sw, null); 345 data.put(tag, sw.toString()); 346 } 347 348 // extra special features 349 350 // ------------- MISC METHODS ------------- 351 352 /** 353 * Make the given value read-only. It may be viewed, and used for 354 * export, but not be modified by the user. 355 * @throws IllegalArgumentException If the given key does not exist 356 * in the quuestion's property table. 357 */ 358 /* 359 public void setReadOnlyValue(String key) { 360 if (value == null) 361 throw new IllegalArgumentException("Question does not have a value yet."); 362 363 if (readOnlyKeys == null) 364 readOnlyKeys = new HashSet(); 365 366 if (value.getProperty(key) == null) 367 throw new IllegalArgumentException("No such key: " + key); 368 369 readOnlyKeys.add(key); 370 } 371 */ 372 373 /** 374 * Determine if a value is read-only. 375 */ 376 public boolean isReadOnlyValue(String key) { 377 ValueConstraints c = getConstraints(key); 378 if (c == null || !c.isReadOnly()) 379 return false; 380 else 381 return true; 382 } 383 384 /** 385 * Determine if the given property is visible to the user. 386 * If it is not, the value is not presented in the GUI for editing and 387 * is hidden in any reports which show the state of the interview. Nonetheless, 388 * it is still present and can be altered manually on the command line or by 389 * editing the configuration file. So it is truly invisible, yet real. 390 * 391 * @return True if the entry is visible (default), false otherwise. 392 */ 393 public boolean isEntryVisible(String key) { 394 ValueConstraints c = getConstraints(key); 395 if (c == null || c.isVisible()) 396 return true; 397 else 398 return false; 399 } 400 401 /** 402 * Get the keys which are currently invalid and blocking the question 403 * (getNext() returning null). It is recommended but not required that 404 * this method return null if the question is not blocked (getNext() != 405 * null). This default implementation of this method is to check any 406 * ValueConstraint objects for each key and return those results. If you 407 * override this method, it is highly recommended that you allow this to 408 * take place, then add in any additional checking to the results provided 409 * by this base implementation. 410 * @return Invalid key in index zero, <em>localized</em> explanation 411 * in index one. Null means there are no invalid keys. 412 */ 413 public String[][] getInvalidKeys() { 414 Enumeration names = value.propertyNames(); 415 List<String> badKeys = new ArrayList<>(); 416 List<String> reasons = new ArrayList<>(); 417 418 while (names.hasMoreElements()) { 419 String curr = (String)(names.nextElement()); 420 ValueConstraints rules = getConstraints(curr); 421 422 if (rules == null) { 423 continue; // no checks, next key 424 } 425 else { 426 String reason = rules.isValid(value.getProperty(curr)); 427 if (reason != null) { 428 badKeys.add(curr); 429 reasons.add(reason); 430 } 431 } 432 } 433 434 // repack data for return 435 if (badKeys.size() > 0) { 436 String[][] ret = new String[badKeys.size()][2]; 437 for (int i = 0; i < badKeys.size(); i++) { 438 ret[i][0] = badKeys.get(i); 439 ret[i][1] = reasons.get(i); 440 } // for 441 442 return ret; 443 } 444 else 445 return null; 446 } 447 448 /** 449 * Convenience method for finding out the status of a particular value. 450 * This method is final because subclasses should implement getInvalidKeys(). 451 * @param key The key to query. Must not be null. 452 * @return The explanation for the value being invalid. Null if the value 453 * is reported as valid. 454 * @see #getInvalidKeys 455 */ 456 public final String isValueValid(String key) { 457 if (key == null) 458 throw new IllegalArgumentException("Key parameter null!"); 459 460 String[][] badVals = getInvalidKeys(); 461 462 if (badVals == null) 463 return null; 464 465 for (int i = 0; i < badVals.length; i++) 466 if (badVals[i][0].equals(key)) 467 return badVals[i][1]; 468 469 return null; 470 } 471 472 // ------------- UPDATE METHODS ------------- 473 /** 474 * Private because we need to maintain internal consistency, especially with 475 * the i18n info. 476 */ 477 public void setValue(Properties props) { 478 if (props == null) { 479 value = null; 480 } 481 else { 482 value = ((Properties)props.clone()); 483 } 484 // what to do about read-only and other tagging values? flush? 485 // remove the extra ones? 486 // should work ok for now if we just leave it, probably safer to leave 487 // it 488 489 interview.updatePath(this); 490 interview.setEdited(true); 491 } 492 493 /** 494 * Update the given properties. New properties cannot be added this way. 495 * @param props Properties to update, keys in first index, values in the second. 496 * @throws IllegalArgumentException If a property in <code>props</code> does not 497 * exist. 498 */ 499 public void updateProperties(String[][] props) { 500 if (props == null || props.length == 0) 501 throw new IllegalArgumentException("Argument is null or zero length."); 502 503 for (int i = 0; i < props.length; i++) { 504 updateProperty(props[i][0], props[i][1]); 505 } 506 } 507 508 /** 509 * Update the given property. New properties cannot be added this way. 510 * @param key Property to update. 511 * @param val Value for the property. 512 * @throws IllegalArgumentException If the property does not exist. 513 */ 514 public void updateProperty(String key, String val) { 515 if (!value.containsKey(key)) 516 throw new IllegalArgumentException("Key " + key + " does not exist"); 517 518 String strVal = val; 519 ValueConstraints rule = this.getConstraints(key); 520 521 // if rules is FloatConstraint it needs to correct value if resolution is set 522 if (rule instanceof PropertiesQuestion.FloatConstraints) { 523 try { 524 float propertyValue = Float.parseFloat(strVal); 525 float res = ((PropertiesQuestion.FloatConstraints)rule).getResolution(); 526 if(!Float.isNaN(res)) { 527 res = Math.round(1/res); 528 float k = propertyValue * res; 529 if (Math.abs(k - (int)k) >= 0.5) 530 k += 1.0f; 531 strVal = Float.toString(((int)k) / res); 532 } 533 } 534 catch(NumberFormatException e) { 535 // do nothing 536 } 537 } 538 539 Object old = value.setProperty(key, strVal); 540 if (!old.equals(val)) { 541 interview.updatePath(this); 542 interview.setEdited(true); 543 } 544 } 545 546 // ------------- GROUP MANAGEMENT ------------- 547 548 /** 549 * Create a new group. 550 * @throws IllegalStateException If the group requested already exists. 551 */ 552 public void createGroup(String name) { 553 if (keyGroups == null) 554 keyGroups = new HashMap<>(); 555 556 Object o = keyGroups.get(name); 557 if (o != null) 558 throw new IllegalStateException("Group " + name + " already exists."); 559 560 ArrayList<String> al = new ArrayList<>(); 561 keyGroups.put(name, al); 562 } 563 564 /** 565 * Set the presentation group to which the key(s) should belong. 566 * If the key is in another group, it will be removed from that one. 567 * The 568 * @param group internal name for the group. Internationalized version 569 * must be available in the resource bundle as tag+group. 570 * @param key Which keys to add to the group. 571 * @throws IllegalArgumentException If an attempt is made to add to a group which 572 * does not exist. 573 * @throws IllegalStateException If an attempt is made to group a key which is not 574 * present. 575 * @see #createGroup 576 */ 577 public void setGroup(String group, String key) { 578 if (value == null) 579 throw new IllegalStateException( 580 "Question has no values, cannot group non-existant key"); 581 if (!value.containsKey(key)) 582 throw new IllegalArgumentException("Key " + key + " does not exist"); 583 if (keyGroups == null) 584 throw new IllegalArgumentException("No such group: " + group); 585 586 // find existing group or create 587 ArrayList<String> l = keyGroups.get(group); 588 if (l == null) 589 throw new IllegalArgumentException("No such group: " + group); 590 591 // remove key from all groups 592 Iterator<ArrayList<String>> vals = keyGroups.values().iterator(); 593 while (vals.hasNext()) { 594 ArrayList<String> al = vals.next(); 595 for (int i = 0; i < al.size(); i++) 596 if (al.get(i).equals(key)) 597 al.remove(i); 598 } 599 600 // add to group 601 for (int i = 0; i < l.size(); i++) { 602 if (l.get(i).equals(key)) 603 return; // already there 604 } 605 l.add(key); 606 } 607 608 public void setGroup(String group, String[] keys) { 609 if (keys == null || keys.length == 0) 610 return; 611 612 for (int i = 0; i < keys.length; i++) 613 setGroup(group, keys[i]); 614 } 615 616 /** 617 * Get the names of the groups being used by the current set of values. 618 * Groups which are empty are not listed; groups may become empty if client 619 * code attempts to put a key in more than one group. 620 * @return Group names, null if no grouping is in use. 621 * @see #setGroup(String,String) 622 * @see #setGroup(String,String[]) 623 */ 624 public String[] getGroups() { 625 if (keyGroups == null) 626 return null; 627 628 ArrayList<String> result = new ArrayList<>(); 629 Set<String> keys = keyGroups.keySet(); 630 if (keys != null) { 631 Iterator<String> it = keys.iterator(); 632 while(it.hasNext()) { 633 String key = it.next(); 634 ArrayList<String> al = keyGroups.get(key); 635 if (al == null || al.size() == 0) 636 continue; // empty group 637 638 result.add(key); 639 } // while 640 } 641 642 if (result.size() == 0) 643 return null; 644 else { 645 String[] ret = new String[result.size()]; 646 ret = result.toArray(ret); 647 return ret; 648 } 649 } 650 651 /** 652 * Get the keys which are registered with the given group. 653 * @param group Group name to query. Null returns the groupless keys. 654 * @return Null if the group is empty or does not exist, else the keys and 655 * values in this array. 656 * @see #getGroups 657 * @see #setGroup 658 */ 659 public String[][] getGroup(String group) { 660 if (value == null || value.size() == 0) 661 return null; 662 663 if (group == null) 664 return getUngrouped(); 665 666 ArrayList<String> al = keyGroups.get(group); 667 668 if (al == null || al.size() == 0) 669 return null; 670 else { 671 Iterator<String> it = al.iterator(); 672 String[][] data = new String[al.size()][2]; 673 for(int i = 0; it.hasNext(); i++) { 674 data[i][0] = it.next(); 675 data[i][1] = (String)(value.get(data[i][0])); 676 } // for 677 678 return data; 679 } 680 } 681 682 /** 683 * @return The keys and values which are not allocated to any group. 684 * Null if there are no ungrouped values. 685 * @see #getGroup 686 * @see #setGroup 687 */ 688 public String[][] getUngrouped() { 689 if (value == null || value.size() == 0) 690 return null; 691 692 if (keyGroups != null) { 693 Set keys = keyGroups.keySet(); 694 if (keys != null) { 695 String[] gps = getGroups(); 696 Properties copy = (Properties)(value.clone()); 697 698 // remove all grouped entries from the copied 699 // question value 700 for (int i = 0; i < gps.length; i++) { 701 String[][] vals = getGroup(gps[i]); 702 703 for (int j = 0; j < vals.length; j++) 704 copy.remove(vals[j][0]); 705 } 706 707 if (copy.size() > 0) { 708 Enumeration en = copy.propertyNames(); 709 String[][] ret = new String[copy.size()][2]; 710 int i = 0; 711 712 while (en.hasMoreElements()) { 713 String key = (String)(en.nextElement()); 714 ret[i][0] = key; 715 ret[i][1] = copy.getProperty(key); 716 i++; 717 } // while 718 719 return ret; 720 } 721 else 722 return null; 723 } 724 } 725 // no groups, return the entire value set 726 String[][] ret = new String[value.size()][2]; 727 Enumeration en = value.propertyNames(); 728 int i = 0; 729 730 while (en.hasMoreElements()) { 731 String key = (String)(en.nextElement()); 732 ret[i][0] = key; 733 ret[i][1] = value.getProperty(key); 734 i++; 735 } // while 736 737 return ret; // no groups in use 738 } 739 740 // ------------ PRESENTATION --------------- 741 742 /** 743 * Get the display (localized) name of the group. 744 * The resource necessary is the question tag, with the group name and ".group" 745 * appended. That is <code>jck.qTable.basic.group</code> and 746 * <code>jck.qTable.advanced.group</code>. 747 * @param group The internal group name, as is used in the rest of this API. 748 * @return The localized group name, the generated bundle key if that 749 * cannot be found. 750 */ 751 public String getGroupDisplayName(String group) { 752 ResourceBundle b = interview.getResourceBundle(); 753 String rn = tag + "." + group + ".group"; 754 755 try { 756 return b.getString(rn); 757 } 758 catch (MissingResourceException e) { 759 System.err.println("WARNING: missing resource " + rn); 760 return rn; 761 } 762 } 763 764 /** 765 * Get the header string for the column in the table that contains the 766 * key names. A short name is recommended for onscreen space considerations. 767 * @return A string describing the key column in the table. 768 */ 769 public String getKeyHeaderName() { 770 // XXX upgrade to be customizable 771 return Interview.i18n.getString("props.key.name"); 772 } 773 774 /** 775 * Get the header string for the column in the table that contains the 776 * value names. A short name is recommended for onscreen space considerations. 777 * @return A string describing the value column in the table. 778 */ 779 public String getValueHeaderName() { 780 // XXX upgrade to be customizable 781 return Interview.i18n.getString("props.value.name"); 782 } 783 784 785 // ------------ TYPING and CONSTRAINTS --------------- 786 787 /** 788 * Apply constraints to a value. 789 * @throws IllegalArgumentException If the key supplied does not 790 * exist in the current data. 791 * @see PropertiesQuestion.ValueConstraints 792 * @see PropertiesQuestion.IntConstraints 793 * @see PropertiesQuestion.FloatConstraints 794 * @see PropertiesQuestion.BooleanConstraints 795 */ 796 public void setConstraints(String key, ValueConstraints c) { 797 if (constraints == null) 798 constraints = new HashMap<>(); 799 800 if (value == null || value.getProperty(key) == null) 801 throw new IllegalArgumentException("No such key: " + key); 802 803 if (c instanceof BooleanConstraints) { 804 if (value.getProperty(key) == null || value.getProperty(key).isEmpty()) { 805 if (defaultValue.getProperty(key) != null && !defaultValue.getProperty(key).isEmpty()) { 806 value.put(key, defaultValue.getProperty(key)); 807 } 808 else{ 809 if (((BooleanConstraints) c).isYesNo()){ 810 value.put(key, BooleanConstraints.NO); 811 } 812 else{ 813 value.put(key, BooleanConstraints.FALSE); 814 } 815 } 816 } 817 } 818 constraints.put(key, c); 819 } 820 821 /** 822 * Calculates constraint key for table row. 823 * By default constraint key is a value of first column 824 * @param values Array of table row data 825 * @return a key 826 */ 827 public String getConstraintKeyFromRow(Object[] values) { 828 if (values != null && values.length > 0 ) 829 return getKeyPropertyName(values[0].toString()); 830 else 831 return ""; 832 } 833 834 /** 835 * Get the property name for displayed key value in the table 836 * @param key value of the key in the table 837 * @return key or the property name for the key 838 */ 839 public String getKeyPropertyName(String key){ 840 if (presentationKeys != null){ 841 for (Object keyProperty : presentationKeys.keySet()){ 842 if (key.equals(presentationKeys.get(keyProperty))){ 843 return (String) keyProperty; 844 } 845 } 846 } 847 848 return key; 849 } 850 851 /** 852 * Returns the localized key values to display 853 */ 854 public Map<String, String> getPresentationKeys(){ 855 return presentationKeys; 856 } 857 858 /** 859 * @param key The key for the value to get constraints for. 860 * @return Constraints object for the specified key, null if there are no 861 * known constraints. 862 */ 863 public ValueConstraints getConstraints(String key) { 864 if (constraints == null) 865 return null; 866 else 867 return constraints.get(key); 868 } 869 870 /** 871 * The localized values to display, corresponding 1-1 to the 872 * set of property keys. Null indicates that no i18n presentation values 873 * are provided. 874 */ 875 private Map<String, String> presentationKeys; 876 877 /** 878 * Indexed like everything else, by property key, the value is a 879 * ValueConstraint. 880 */ 881 private Map<String, ValueConstraints> constraints; 882 883 /** 884 * The current (default or latest) response to this question. 885 */ 886 protected Properties value; 887 888 /** 889 * The default response for this question. 890 */ 891 private Properties defaultValue; 892 893 /** 894 * Record of key groupings. Key is the group name string, the value is 895 * a ArrayList. 896 */ 897 private Map<String, ArrayList<String>> keyGroups; 898 899 // these are now represented in ValueConstraints 900 //private HashSet readOnlyKeys; 901 //private HashSet hiddenKeys; 902 903 public static class ValueConstraints { 904 public ValueConstraints() { this(false, true); } 905 906 public ValueConstraints(boolean readonly, boolean visible) { 907 this.readonly = readonly; 908 this.visible = visible; 909 } 910 911 /** 912 * Determine whether this value should be readable only, by the 913 * interview user. The default state is false. 914 * @param state True if readonly, false otherwise. 915 */ 916 public void setReadOnly(boolean state) { readonly = state; } 917 918 /** 919 * Make value outwardly visible or invisible. This does not 920 * mean it is not accessible, just that it is not shown when 921 * possible in the user interfaces. The default state is true. 922 * @param state True if the property at constrained by this object 923 * should be visible. 924 */ 925 public void setVisible(boolean state) { visible = state; } 926 927 /** 928 * Determine if this value is a read-only value. The default is false. 929 * @return True if read-only, false otherwise. 930 */ 931 public boolean isReadOnly() { return readonly; } 932 933 /** 934 * Is this property (and value) visible? True by default. 935 * @return True if it should be visible, false otherwise. 936 */ 937 public boolean isVisible() { return visible; } 938 939 /** 940 * May the answer be set to an unanswered state. If false, the 941 * question will always be answered. If true, the question may 942 * be set to an affirmative, negative or unset response. An unset 943 * response is considered an incomplete answer by default. 944 * @param state True if the user is allowed to make this value unset. 945 * @see #isUnsetAllowed 946 */ 947 public void setUnsetAllowed(boolean state) { allowUnset = state; } 948 949 /** 950 * Is an unset response allowed. The default is true, unless indicated 951 * otherwise by a subclass. 952 * @return True if the unsetting the answer is allowed. 953 * @see #setUnsetAllowed 954 */ 955 public boolean isUnsetAllowed() { return allowUnset; } 956 957 /** 958 * Is the given value valid for this field? Since this constraint 959 * class has no particular typing, the default only check that the 960 * value is non-empty. You may override this method to do custom 961 * checking, or you may do your checking in getInvalidKeys() which 962 * by default defers to the associated constraint object (if any). 963 * @param v The value to check. 964 * @return Null if the valid is valid, a localized reason string 965 * otherwise. 966 * @see #getInvalidKeys 967 */ 968 public String isValid(String v) { 969 if (v == null || v.length() == 0) 970 return "Value is not set"; 971 else 972 return null; 973 } 974 975 private boolean visible = true; 976 private boolean readonly = false; 977 private boolean allowUnset = true; 978 } 979 980 public static class IntConstraints extends ValueConstraints { 981 public IntConstraints() { super(); } 982 983 /** 984 * Construct with defined upper and lower value boundaries. 985 * @param min Minimum valid response value (inclusive). 986 * @param max Maximum valid response value (inclusive). 987 */ 988 public IntConstraints(int min, int max) { 989 this(); 990 setBounds(min, max); 991 } 992 993 /** 994 * Construct with suggested values for the user. 995 * @param suggestions Predefined values for the user to choose from. 996 * @see #setCustomValuesAllowed(boolean) 997 */ 998 public IntConstraints(int[] suggestions) { 999 this(); 1000 setSuggestions(suggestions); 1001 } 1002 1003 /** 1004 * @param min Minimum valid response value (inclusive). 1005 * @param max Maximum valid response value (inclusive). 1006 * @param suggestions Predefined values for the user to choose from. 1007 * @see #setBounds 1008 */ 1009 public IntConstraints(int min, int max, int[] suggestions) { 1010 this(); 1011 setBounds(min, max); 1012 setSuggestions(suggestions); 1013 } 1014 1015 /** 1016 * Set the max/min possible value that should be considered 1017 * valid. The range in inclusive. The defaults are the 1018 * MIN and MAX values for the integer type, except the minimum 1019 * value itself, which is reserved. 1020 * @see #getLowerBound 1021 * @see #getUpperBound 1022 */ 1023 public void setBounds(int min, int max) { 1024 this.max = max; 1025 this.min = min; 1026 } 1027 1028 /** 1029 * Get the lower bound which specifies the minimum possible value to be 1030 * considered a valid response from the user. 1031 * @return Minimum boundary (inclusive). 1032 */ 1033 public int getLowerBound() { return min; } 1034 1035 /** 1036 * Get the upper bound which specifies the maximum possible value to be 1037 * considered a valid response from the user. 1038 * @return Maximum boundary (inclusive). 1039 */ 1040 public int getUpperBound() { return max; } 1041 1042 /** 1043 * Get the suggested values. Not a copy, do not alter the array. 1044 */ 1045 public int[] getSuggestions() { return suggestions; } 1046 1047 /** 1048 * Are user specified values allowed? If not, there must be 1049 * suggestions present. 1050 * @throws IllegalStateException If no suggestions have been 1051 * provided. 1052 * @see #setSuggestions 1053 */ 1054 public void setCustomValuesAllowed(boolean state) { 1055 custom = state; 1056 } 1057 1058 /** 1059 * Are custom user values allowed? 1060 * @see #setCustomValuesAllowed 1061 * @see #setSuggestions 1062 */ 1063 public boolean isCustomValuesAllowed() { 1064 return custom; 1065 } 1066 1067 /** 1068 * Supply some possible values that the user may want to 1069 * select from. 1070 */ 1071 public void setSuggestions(int[] sugs) { 1072 suggestions = new int[sugs.length]; 1073 System.arraycopy(sugs, 0, suggestions, 0, sugs.length); 1074 } 1075 1076 /** 1077 * Is the given value valid for this field? The basic check for 1078 * validity is to see if the given string can be parsed as an 1079 * integer value in the current locale. 1080 * @param v The value to check. 1081 * @return Null if the valid is valid, a localized reason string 1082 * otherwise. 1083 */ 1084 public String isValid(String v) { 1085 try { 1086 int number = Integer.parseInt(v); 1087 return isValid(number); 1088 } 1089 catch (NumberFormatException e) { 1090 return "Not an integer."; // XXX i18n 1091 } 1092 } 1093 1094 /** 1095 * Is the given value valid for this field? 1096 * @return Null if the valid is valid, a localized reason string 1097 * otherwise. 1098 */ 1099 public String isValid(int v) { 1100 if (v < min || v > max) 1101 return "Value out of range (" + v + "), must be between " + 1102 min + " and " + max; 1103 else 1104 return null; 1105 } 1106 1107 /** 1108 * Suggested values for this value's response. 1109 */ 1110 protected int[] suggestions; 1111 1112 /** 1113 * Is the user allowed to supply their own value or are they required 1114 * to use one of the suggestions? 1115 */ 1116 protected boolean custom = true; 1117 1118 /** 1119 * The lower bound for responses to this value. 1120 */ 1121 private int min = Integer.MIN_VALUE + 1; 1122 1123 /** 1124 * The upper bound for responses to this value. 1125 */ 1126 private int max = Integer.MAX_VALUE; 1127 } 1128 1129 /** 1130 * Constraints specifying a floating point type. 1131 */ 1132 public static class FloatConstraints extends ValueConstraints { 1133 public FloatConstraints() { 1134 super(); 1135 } 1136 1137 /** 1138 * @param min Minimum valid response value. 1139 * @param max Maximum valid response value 1140 * @see #setBounds 1141 */ 1142 public FloatConstraints(float min, float max) { 1143 this(); 1144 setBounds(min, max); 1145 } 1146 1147 /** 1148 * Construct with suggestions for the user. 1149 * @param suggestions Values to suggest to the user. Array should be 1150 * of length greater than zero. 1151 */ 1152 public FloatConstraints(float[] suggestions) { 1153 this(); 1154 setSuggestions(suggestions); 1155 } 1156 1157 /** 1158 * Construct with both min, max and suggested values. 1159 * @param min Minimum valid response value. 1160 * @param max Maximum valid response value 1161 * @param suggestions Values to suggest to the user. Array should be 1162 * of length greater than zero. 1163 * @see #getSuggestions() 1164 */ 1165 public FloatConstraints(float min, float max, float[] suggestions) { 1166 this(); 1167 setBounds(min, max); 1168 setSuggestions(suggestions); 1169 } 1170 1171 /** 1172 * Set the max/min possible value that should be considered 1173 * valid. The range in inclusive. The defaults are the 1174 * MIN and MAX values for the float datatype. 1175 * @param min Minimum valid response value. 1176 * @param max Maximum valid response value. 1177 */ 1178 public void setBounds(float min, float max) { 1179 this.max = max; 1180 this.min = min; 1181 } 1182 1183 /** 1184 * Get the lower bound which specifies the minimum possible value to be 1185 * considered a valid response from the user. 1186 * @return Minimum boundary (inclusive). 1187 */ 1188 public float getLowerBound() { return min; } 1189 1190 /** 1191 * Get the upper bound which specifies the maximum possible value to be 1192 * considered a valid response from the user. 1193 * @return Maximum boundary (inclusive). 1194 */ 1195 public float getUpperBound() { return max; } 1196 1197 /** 1198 * Get the suggested values. Not a copy, do not alter the array. 1199 * @return Suggested response values currently set for this question. 1200 * Null if none have been set. 1201 * @see #setSuggestions 1202 */ 1203 public float[] getSuggestions() { return suggestions; } 1204 1205 /** 1206 * Are user specified values allowed? If not, there must be 1207 * suggestions present. 1208 * @throws IllegalStateException If no suggestions have been 1209 * provided. 1210 * @see #setSuggestions 1211 */ 1212 public void setCustomValuesAllowed(boolean state) { 1213 custom = state; 1214 } 1215 1216 /** 1217 * Are custom user values allowed? 1218 * @see #setCustomValuesAllowed 1219 * @see #setSuggestions 1220 */ 1221 public boolean isCustomValuesAllowed() { 1222 return custom; 1223 } 1224 1225 /** 1226 * Supply some possible values that the user may want to 1227 * select from. 1228 * @param sugs Suggested values to present the user for this question. 1229 * Should be an array of length greater than zero. 1230 * @see #getSuggestions() 1231 */ 1232 public void setSuggestions(float[] sugs) { 1233 suggestions = new float[sugs.length]; 1234 System.arraycopy(sugs, 0, suggestions, 0, sugs.length); 1235 } 1236 1237 /** 1238 * Set the resolution for responses to this question. Responses 1239 * may be rounded to the nearest multiple of the resolution. 1240 * @param resolution the resolution for responses to this question 1241 * @see #getResolution 1242 * @see #setValue 1243 */ 1244 public void setResolution(float resolution) { 1245 this.resolution = resolution; 1246 } 1247 1248 /** 1249 * Get the resolution for responses to this question. Responses 1250 * may be rounded to the nearest multiple of the resolution. 1251 * @return the resolution for responses to this question 1252 * @see #setResolution 1253 * @see #setValue 1254 */ 1255 public float getResolution() { 1256 return resolution; 1257 } 1258 1259 /** 1260 * Is the given value valid for this field? The basic check for 1261 * validity is to see if the given string can be parsed as an 1262 * floating point value in the current locale. 1263 * @param v The value to check. 1264 * @return Null if the valid is valid, a localized reason string 1265 * otherwise. 1266 */ 1267 public String isValid(String v) { 1268 try { 1269 float number = Float.parseFloat(v); 1270 return isValid(number); 1271 } 1272 catch (NumberFormatException e) { 1273 return "Not an floating point number."; // XXX i18n 1274 } 1275 } 1276 1277 /** 1278 * Is the given value valid for this field? 1279 * @return Null if the valid is valid, a localized reason string 1280 * otherwise. 1281 */ 1282 public String isValid(float v) { 1283 if (v < min || v > max) 1284 return "Value out of range ( " + v + "), must be between " + 1285 min + " and " + max; 1286 else 1287 return null; 1288 } 1289 1290 /** 1291 * Current value set for the suggested response values. 1292 * @see #setSuggestions(float[]) 1293 * @see #getSuggestions() 1294 */ 1295 protected float[] suggestions; 1296 1297 /** 1298 * Is the user allowed to supply their own value or are they required 1299 * to use one of the suggestions? 1300 */ 1301 protected boolean custom = true; 1302 1303 /** 1304 * The lower bound for responses to this value. 1305 */ 1306 private float min = Float.MIN_VALUE; 1307 1308 /** 1309 * The upper bound for responses to this value. 1310 */ 1311 private float max = Float.MAX_VALUE; 1312 1313 /** 1314 * The resolution for responses to this question 1315 */ 1316 private float resolution = Float.NaN; 1317 } 1318 1319 /** 1320 * Value restrictions for string type responses. 1321 */ 1322 public static class StringConstraints extends ValueConstraints { 1323 public StringConstraints() { 1324 super(); 1325 } 1326 1327 public StringConstraints(String[] suggestions) { 1328 this(); 1329 setSuggestions(suggestions); 1330 } 1331 1332 /** 1333 * Construct with max string length restriction. 1334 * @param maxLen Maximum length string for the response. 1335 */ 1336 public StringConstraints(int maxLen) { 1337 this(); 1338 setNominalMaxLength(maxLen); 1339 } 1340 1341 /** 1342 * Construct with max string length restriction and suggested 1343 * responses. 1344 * @param maxLen Maximum length string for the response. 1345 * @param suggestions The suggested responses to present the user with. 1346 * Should be an array of greater than zero length. 1347 */ 1348 public StringConstraints(String[] suggestions, int maxLen) { 1349 this(); 1350 setSuggestions(suggestions); 1351 setNominalMaxLength(maxLen); 1352 } 1353 1354 /** 1355 * Supply some possible values that the user may want to 1356 * select from. 1357 * @param sugs The suggested responses to present the user with. 1358 * Should be an array of greater than zero length. Can be null if 1359 * you wish to remove the setting completely. 1360 * @see #isCustomValuesAllowed 1361 * @see #getSuggestions 1362 */ 1363 public void setSuggestions(String[] sugs) { 1364 suggestions = new String[sugs.length]; 1365 System.arraycopy(sugs, 0, suggestions, 0, sugs.length); 1366 } 1367 1368 /** 1369 * Determine what the current value suggestions are. 1370 * @return Null if there are no suggested values, otherwise an array of 1371 * length greater than zero. 1372 */ 1373 public String[] getSuggestions() { 1374 return suggestions; 1375 } 1376 1377 /** 1378 * Are user specified values allowed? If not, there must be 1379 * suggestions present. 1380 * @throws IllegalStateException If no suggestions have been 1381 * provided. 1382 * @see #setSuggestions 1383 */ 1384 public void setCustomValuesAllowed(boolean state) { 1385 custom = state; 1386 // XXX need to throw exception which javadoc threatens 1387 } 1388 1389 /** 1390 * Can the user provide whatever string answer they wish, or must they 1391 * choose only from the suggested values. An assumption is that if 1392 * this value is false, then there are available suggestions for this 1393 * value. 1394 * @see #setCustomValuesAllowed 1395 * @see #setSuggestions 1396 */ 1397 public boolean isCustomValuesAllowed() { 1398 return custom; 1399 } 1400 1401 /** 1402 * Get the nominal maximum length for the string. 1403 * @return the nominal maximum length for the string. 1404 * @see #setNominalMaxLength 1405 */ 1406 public int getNominalMaxLength() { 1407 return nominalMaxLength; 1408 } 1409 1410 /** 1411 * Set the expected maximum length for the string. 1412 * @param nominalMaxLength the nominal maximum length for the string. 1413 * @see #getNominalMaxLength 1414 */ 1415 public void setNominalMaxLength(int nominalMaxLength) { 1416 this.nominalMaxLength = nominalMaxLength; 1417 } 1418 1419 /** 1420 * Current value set for the suggested response values. 1421 * @see #setSuggestions(String[]) 1422 * @see #getSuggestions() 1423 */ 1424 protected String[] suggestions; 1425 protected boolean custom = true; 1426 1427 /** 1428 * The nominal maximum length for the string. 1429 */ 1430 protected int nominalMaxLength; 1431 } 1432 1433 /** 1434 * Constraints allowing a value to be either a boolean or Yes/No response. 1435 */ 1436 public static class BooleanConstraints extends ValueConstraints { 1437 1438 public final static String YES = "Yes"; 1439 public final static String NO = "No"; 1440 public final static String TRUE = "True"; 1441 public final static String FALSE = "False"; 1442 1443 public BooleanConstraints() { 1444 super(); 1445 } 1446 1447 /** 1448 * @param isYesNo Should this question be presented as a Yes/No question 1449 * rather than a True/False. 1450 * @see #setUseYesNo 1451 */ 1452 public BooleanConstraints(boolean isYesNo) { 1453 this(); 1454 setUseYesNo(isYesNo); 1455 } 1456 1457 /** 1458 * @param isYesNo Should this question be presented as a Yes/No question 1459 * rather than a True/False. 1460 * @param unsetAllowed Can the question be set to have no value. 1461 * @see #setUnsetAllowed 1462 * @see #setUseYesNo 1463 */ 1464 public BooleanConstraints(boolean isYesNo, boolean unsetAllowed) { 1465 this(); 1466 setUseYesNo(isYesNo); 1467 setUnsetAllowed(unsetAllowed); 1468 } 1469 1470 /** 1471 * Indicate whether this should be a Yes/No question or 1472 * True/False. 1473 * @param state True if this should be rendered as a Yes/No question, 1474 * false if it should be a boolean true/false. 1475 * @see #isYesNo 1476 */ 1477 public void setUseYesNo(boolean state) { 1478 yesno = state; 1479 } 1480 1481 /** 1482 * Is this a yes/no field, instead of the default true/false? 1483 * @return True if this is a Yes/No question, false if it is a 1484 * True/False question. 1485 * @see #setUseYesNo 1486 */ 1487 public boolean isYesNo() { 1488 return yesno; 1489 } 1490 1491 private boolean yesno = false; 1492 } 1493 1494 /** 1495 * Constrains the response to filenames or paths, and allows chooser 1496 * widgets to be rendered for the user when appropriate. 1497 */ 1498 public static class FilenameConstraints extends ValueConstraints { 1499 public FilenameConstraints() { 1500 super(); 1501 } 1502 1503 @Override 1504 public String isValid(String v) { 1505 if (v == null || v.length() == 0) 1506 return "Value is not set"; 1507 1508 if (baseRelativeOnly && baseDir != null && !v.startsWith(baseDir.getPath())) 1509 return "Path is not relative to " + baseDir.getPath(); 1510 1511 if (filters != null) { 1512 File fl = new File(v); 1513 for (FileFilter f : filters) { 1514 if (!f.accept(fl)) { 1515 return "File is not valid"; 1516 } 1517 } 1518 } 1519 return null; 1520 } 1521 1522 /** 1523 * @param baseDir Base directory where selection should begin from. 1524 * @param relativeOnly Force the result of this value to be relative 1525 * to the base location. This is limited on some filesystem types 1526 * of course, where relative paths from one place to another are not 1527 * always possible. 1528 */ 1529 public FilenameConstraints(File baseDir, boolean relativeOnly) { 1530 this(); 1531 this.baseDir = baseDir; 1532 this.baseRelativeOnly = relativeOnly; 1533 } 1534 1535 /** 1536 * Get the filters used to select valid files for a response 1537 * to this question. 1538 * @return An array of filters 1539 * @see #setFilter 1540 * @see #setFilters 1541 */ 1542 public FileFilter[] getFilters() { 1543 return filters; 1544 } 1545 1546 /** 1547 * Set a filter used to select valid files for a response 1548 * to this question. Use this method, or setFilters(), not both. 1549 * @param filter a filter used to select valid files for a response 1550 * to this question 1551 * @see #getFilters 1552 * @see #setFilters 1553 */ 1554 public void setFilter(FileFilter filter) { 1555 filters = new FileFilter[] { filter }; 1556 } 1557 1558 /** 1559 * Set the filters used to select valid files for a response 1560 * to this question. The first element in the array is selected by 1561 * default. Use this method, or setFilter(), not both. 1562 * @param filters An array of filters used to select valid files for a response 1563 * to this question 1564 * @see #getFilters 1565 * @see #setFilter 1566 */ 1567 public void setFilters(FileFilter[] filters) { 1568 this.filters = filters; 1569 } 1570 1571 /** 1572 * Get the default directory for files for a response to this question. 1573 * @return the default directory in which files should be found/placed 1574 * @see #setBaseDirectory 1575 * @see #isBaseRelativeOnly 1576 */ 1577 public File getBaseDirectory() { 1578 return baseDir; 1579 } 1580 1581 /** 1582 * Set the default directory for files for a response to this question. 1583 * @param dir the default directory in which files should be found/placed 1584 * @see #getBaseDirectory 1585 */ 1586 public void setBaseDirectory(File dir) { 1587 baseDir = dir; 1588 } 1589 1590 /** 1591 * Determine whether all valid responses to this question should be 1592 * relative to the base directory (in or under it). False by 1593 * default. 1594 * @return true if all valid responses to this question should be 1595 * relative to the base directory 1596 * @see #setBaseRelativeOnly 1597 */ 1598 public boolean isBaseRelativeOnly() { 1599 return baseRelativeOnly; 1600 } 1601 1602 /** 1603 * Specify whether all valid responses to this question should be 1604 * relative to the base directory (i.e. in or under it.) 1605 * @param b this parameter should be true if all valid responses 1606 * to this question should be relative to the base directory 1607 * @see #setBaseRelativeOnly 1608 */ 1609 public void setBaseRelativeOnly(boolean b) { 1610 baseRelativeOnly = b; 1611 } 1612 1613 /** 1614 * Supply some possible values that the user may want to 1615 * select from. The <code>getPath()</code> string will be used for 1616 * presentation and persistent storage of the value. 1617 */ 1618 public void setSuggestions(File[] sugs) { 1619 // validate sugs 1620 suggestions = new File[sugs.length]; 1621 System.arraycopy(sugs, 0, suggestions, 0, sugs.length); 1622 } 1623 1624 public File[] getSuggestions() { 1625 return suggestions; 1626 } 1627 1628 private File baseDir; 1629 private boolean baseRelativeOnly; 1630 private FileFilter[] filters; 1631 1632 /** 1633 * Current value set for the suggested response values. 1634 * @see #setSuggestions(File[]) 1635 * @see #getSuggestions() 1636 */ 1637 protected File[] suggestions; 1638 } 1639 }