1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 2001, 2015, 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.util.ArrayList;
  31 import java.util.Map;
  32 
  33 /**
  34  * A {@link Question question} to which the response is one or more filenames.
  35  */
  36 public abstract class FileListQuestion extends Question
  37 {
  38     /**
  39      * Create a question with a nominated tag.
  40      * @param interview The interview containing this question.
  41      * @param tag A unique tag to identify this specific question.
  42      */
  43     protected FileListQuestion(Interview interview, String tag) {
  44         super(interview, tag);
  45 
  46         if (interview.getInterviewSemantics() > Interview.SEMANTIC_PRE_32)
  47             clear();
  48 
  49         setDefaultValue(value);
  50     }
  51 
  52     /**
  53      * Get the default response for this question.
  54      * @return the default response for this question.
  55      *
  56      * @see #setDefaultValue
  57      */
  58     public File[] getDefaultValue() {
  59         return defaultValue;
  60     }
  61 
  62     /**
  63      * Set the default response for this question,
  64      * used by the clear method.
  65      * @param v the default response for this question.
  66      *
  67      * @see #getDefaultValue
  68      */
  69     public void setDefaultValue(File[] v) {
  70         defaultValue = v;
  71     }
  72 
  73     /**
  74      * Specify whether or not duplicates should be allowed in the list.
  75      * By default, duplicates are allowed.
  76      * @param b true if duplicates should be allowed, and false otherwise
  77      * @see #isDuplicatesAllowed
  78      */
  79     public void setDuplicatesAllowed(boolean b) {
  80         duplicatesAllowed = b;
  81     }
  82 
  83     /**
  84      * Check whether or not duplicates should be allowed in the list.
  85      * @return true if duplicates should be allowed, and false otherwise
  86      * @see #setDuplicatesAllowed
  87      */
  88     public boolean isDuplicatesAllowed() {
  89         return duplicatesAllowed;
  90     }
  91 
  92     /**
  93      * Get the current (default or latest) response to this question.
  94      * @return The current value.
  95      * @see #setValue
  96      */
  97     public File[] getValue() {
  98         return value;
  99     }
 100 
 101     /**
 102      * Verify this question is on the current path, and if it is,
 103      * return the current value.
 104      * @return the current value of this question
 105      * @throws Interview.NotOnPathFault if this question is not on the
 106      * current path
 107      * @see #getValue
 108      */
 109     public File[] getValueOnPath()
 110         throws Interview.NotOnPathFault
 111     {
 112         interview.verifyPathContains(this);
 113         return getValue();
 114     }
 115 
 116     @Override
 117     public String getStringValue() {
 118         return join(value);
 119     }
 120 
 121     /**
 122      * Set the response to this question to the value represented by
 123      * a string-valued argument.
 124      * @param paths The new value for the question, can be null to set no value.
 125      * @see #getValue
 126      */
 127     @Override
 128     public void setValue(String paths) {
 129         setValue(paths == null ? null : split(paths));
 130     }
 131 
 132     /**
 133      * Set the current value.
 134      * @param newValue The value to be set.
 135      * @see #getValue
 136      */
 137     public void setValue(File[] newValue) {
 138         File[] oldValue = value;
 139         value = newValue;
 140         if (!equal(value, oldValue)) {
 141             interview.updatePath(this);
 142             interview.setEdited(true);
 143         }
 144     }
 145 
 146     /**
 147      * Simple validation, upgrade if needed.
 148      * Iterates values, checks against filters, except if interview semantics
 149      * are set to an pre-50 version, in which case true is always returned.
 150      * Using semantics greater than 50 is highly recommended and recommended if
 151      * an old interview is being modernized.
 152      * @return False if any values are rejected by filters, true otherwise.
 153      *        True if there are no values or no filters.
 154      * @see com.sun.interview.Interview#getInterviewSemantics
 155      * @see com.sun.interview.Interview#SEMANTIC_VERSION_50
 156      */
 157     @Override
 158     public boolean isValueValid() {
 159         if (interview.getInterviewSemantics() < Interview.SEMANTIC_VERSION_50) {
 160             // not that useful, but it's how the original question behaved
 161             return true;
 162         }
 163 
 164         if (value == null || value.length == 0 ||
 165             filters == null || filters.length == 0) {
 166             return true;
 167         }
 168 
 169         for (File f: value) {
 170             if (f == null) {
 171                 continue;
 172             }
 173 
 174             for (FileFilter fs: filters) {
 175                 if (fs != null && !fs.accept(f)) {
 176                     return false;
 177                 }
 178             }
 179         }   // for
 180 
 181         return true;
 182     }
 183 
 184     @Override
 185     public boolean isValueAlwaysValid() {
 186         return false;
 187     }
 188 
 189     /**
 190      * Get the filters used to select valid files for a response
 191      * to this question.
 192      * @return An array of filters
 193      * @see #setFilter
 194      * @see #setFilters
 195      */
 196     public FileFilter[] getFilters() {
 197         return filters;
 198     }
 199 
 200     /**
 201      * Set a filter used to select valid files for a response
 202      * to this question.
 203      * @param filter a filter used to select valid files for a response
 204      * to this question
 205      * @see #getFilters
 206      * @see #setFilters
 207      */
 208     public void setFilter(FileFilter filter) {
 209         filters = new FileFilter[] { filter };
 210     }
 211 
 212     /**
 213      * Set the filters used to select valid files for a response
 214      * to this question.  For pre-50 behavior, both the filters and the hint
 215      * filter values are treated the same, and neither is used for validation
 216      * (e.g. <code>isValid()</code>.
 217      * @param fs An array of filters used to select valid files for a response
 218      * to this question
 219      * @see #getFilters
 220      * @see #setFilters
 221      * @see #getHintFilters
 222      */
 223     public void setFilters(FileFilter[] fs) {
 224         if (interview.getInterviewSemantics() >= Interview.SEMANTIC_VERSION_50) {
 225             filters = fs;
 226         }
 227         else {
 228             // old behavior, the fitlers act as hint filters, not validation
 229             // filters
 230             filters = hintFilters = fs;
 231         }
 232     }
 233 
 234     /**
 235      * Set the filters which the user can use to help find files among a list
 236      * of files - this is somewhat exposing of the fact that there is a user
 237      * interface.  This should not be confused with setFilters(), which in
 238      * version 5.0 or later of the harness, are used to do validity checks on
 239      * the actual value (e.g.. in <code>isValid()</code>.
 240      * @param fs Filters which might be offered to the user.
 241      * @see #setFilters
 242      * @see #isValueValid
 243      * @since 5.0
 244      */
 245     public void setHintFilters(FileFilter[] fs) {
 246         hintFilters = fs;
 247     }
 248 
 249     /**
 250      * A set of filters to help users locate the right file/dir.
 251      * These filters are not used for validating the question value.
 252      * @see #setHintFilters(com.sun.interview.FileFilter[])
 253      * @see #getFilters
 254      * @since 5.0
 255      */
 256     public FileFilter[] getHintFilters() {
 257         if (interview.getInterviewSemantics() >= Interview.SEMANTIC_VERSION_50) {
 258             return hintFilters;
 259         } else {
 260             return filters;
 261         }
 262     }
 263 
 264     /**
 265      * Get the default directory for files for a response to this question.
 266      * @return the default directory in which files should be found/placed
 267      * @see #setBaseDirectory
 268      * @see #isBaseRelativeOnly
 269      */
 270     public File getBaseDirectory() {
 271         return baseDir;
 272     }
 273 
 274     /**
 275      * Set the default directory for files for a response to this question.
 276      * @param dir the default directory in which files should be found/placed
 277      * @see #getBaseDirectory
 278      */
 279     public void setBaseDirectory(File dir) {
 280         baseDir = dir;
 281     }
 282 
 283     /**
 284      * Determine whether all valid responses to this question should be
 285      * relative to the base directory (i.e. in or under it.)
 286      * @return true if all valid responses to this question should be
 287      * relative to the base directory
 288      * @see #setBaseRelativeOnly
 289      */
 290     public boolean isBaseRelativeOnly() {
 291         return baseRelativeOnly;
 292     }
 293 
 294     /**
 295      * Specify whether all valid responses to this question should be
 296      * relative to the base directory (i.e. in or under it.)
 297      * @param b this parameter should be true if all valid responses
 298      * to this question should be relative to the base directory
 299      * @see #setBaseRelativeOnly
 300      */
 301     public void setBaseRelativeOnly(boolean b) {
 302         baseRelativeOnly = b;
 303     }
 304 
 305     /**
 306      * Clear any response to this question, resetting the value
 307      * back to its initial state.
 308      */
 309     public void clear() {
 310         setValue(defaultValue);
 311     }
 312 
 313     /**
 314      * Load the value for this question from a dictionary, using
 315      * the tag as the key.
 316      * @param data The map from which to load the value for this question.
 317      */
 318     protected void load(Map<String, String> data) {
 319         Object o = data.get(tag);
 320         if (o instanceof File[])
 321             setValue((File[])o);
 322         else if (o instanceof String)
 323             setValue(split((String)o));
 324     }
 325 
 326     /**
 327      * Break apart a string containing a white-space separate list of file
 328      * names into an array of individual files.
 329      * If the string is null or empty, an empty array is returned.
 330      * The preferred separator is a newline character;
 331      * if there are no newline characters in the string, then
 332      * (for backward compatibility) space is accepted instead.
 333      * @param s The string to be broken apart
 334      * @return An array of files determined from the parameter string.
 335      * @see #join
 336      */
 337     public static File[] split(String s) {
 338         if (s == null)
 339             return empty;
 340 
 341         char sep = (s.indexOf('\n') == -1 ? ' ' : '\n');
 342 
 343         ArrayList<File> v = new ArrayList<>();
 344         int start = -1;
 345         for (int i = 0; i < s.length(); i++) {
 346             if (s.charAt(i) == sep) {
 347                 if (start != -1)
 348                     v.add(new File(s.substring(start, i)));
 349                 start = -1;
 350             } else
 351                 if (start == -1)
 352                     start = i;
 353         }
 354         if (start != -1)
 355             v.add(new File(s.substring(start)));
 356         if (v.size() == 0)
 357             return empty;
 358         File[] a = new File[v.size()];
 359         v.toArray(a);
 360         return a;
 361     }
 362 
 363     private static final File[] empty = { };
 364 
 365     /**
 366      * Save the value for this question in a dictionary, using
 367      * the tag as the key.
 368      * @param data The map in which to save the value for this question.
 369      */
 370     protected void save(Map<String, String> data) {
 371         if (value != null)
 372             data.put(tag, join(value));
 373     }
 374 
 375     /**
 376      * Convert a list of filenames to a newline separated string.
 377      * @param ff an array of filenames
 378      * @return a string containing the filenames separated by newline
 379      * characters.
 380      * If there is just one filename, and if it contains space characters
 381      * in its path,
 382      * the list is terminated by a newline as well.
 383      * If the parameter array is null or empty, an empty string is returned.
 384      * @see #split
 385      */
 386     public static String join(File[] ff) {
 387         if (ff == null || ff.length == 0)
 388             return "";
 389 
 390         int l = ff.length - 1; // allow for spaces between words
 391         for (int i = 0; i < ff.length; i++)
 392             l += ff[i].getPath().length();
 393 
 394         StringBuffer sb = new StringBuffer(l);
 395 
 396         String ff0p = ff[0].getPath();
 397         sb.append(ff0p);
 398 
 399         if (ff.length == 1 && ff0p.indexOf(' ') != -1) {
 400             // if there is just one file, and if it contains space characters,
 401             // then force a newline character for subsequent split to recognize
 402             sb.append('\n');
 403         }
 404         else {
 405             // if there is more than one file, separate them with newlines
 406             for (int i = 1; i < ff.length; i++) {
 407                 sb.append('\n');
 408                 sb.append(ff[i].getPath());
 409             }
 410         }
 411 
 412         return sb.toString();
 413     }
 414 
 415     /**
 416      * Determine if two arrays of filenames are equal.
 417      * @param f1 the first array to be compared
 418      * @param f2 the other array to be compared
 419      * @return true if both arrays are null, or if neither are null and if
 420      * their contents match, element for element, in order
 421      */
 422     protected static boolean equal(File[] f1, File[] f2) {
 423         if (f1 == null || f2 == null)
 424             return (f1 == f2);
 425 
 426         if (f1.length != f2.length)
 427             return false;
 428 
 429         for (int i = 0; i < f1.length; i++) {
 430             if (f1[i] != f2[i])
 431                 return false;
 432         }
 433 
 434         return true;
 435     }
 436 
 437     /**
 438      * The current (default or latest) response to this question.
 439      */
 440     protected File[] value;
 441 
 442     /**
 443      * The default response for this question.
 444      */
 445     private File[] defaultValue;
 446 
 447     private File baseDir;
 448 
 449     private boolean baseRelativeOnly;
 450 
 451     private FileFilter[] filters;
 452     private FileFilter[] hintFilters;
 453 
 454     private boolean duplicatesAllowed = true;
 455 }