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