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 }