1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 2002, 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.javatest;
  28 
  29 import java.io.*;
  30 import java.lang.reflect.InvocationTargetException;
  31 import java.net.URL;
  32 import java.net.URLClassLoader;
  33 import java.nio.charset.StandardCharsets;
  34 import java.text.MessageFormat;
  35 import java.util.Properties;
  36 import java.util.ResourceBundle;
  37 
  38 import com.sun.interview.Interview;
  39 import com.sun.interview.Question;
  40 import com.sun.interview.WizPrint;
  41 import com.sun.javatest.util.BackupPolicy;
  42 import com.sun.javatest.util.I18NResourceBundle;
  43 
  44 /**
  45  * This class provides a utility for command-line editing of configuration (.jti) files.
  46  * It is intended to be invoked from the command line, as in: <pre>
  47  * java com.sun.javatest.EditJIT options...
  48  * </pre>
  49  * For details of the options, use the <code>-help</code> option.
  50  */
  51 public class EditJTI
  52 {
  53     /**
  54      * This exception is used to indicate a problem with the command line arguments.
  55      */
  56     public static class BadArgs extends Exception
  57     {
  58         /**
  59          * Create a BadArgs exception.
  60          * @param i18n A resource bundle in which to find the detail message.
  61          * @param s The key for the detail message.
  62          */
  63         BadArgs(ResourceBundle i18n, String s) {
  64             super(i18n.getString(s));
  65         }
  66 
  67         /**
  68          * Create a BadArgs exception.
  69          * @param i18n A resource bundle in which to find the detail message.
  70          * @param s The key for the detail message.
  71          * @param o An argument to be formatted with the detail message by
  72          * {@link java.text.MessageFormat#format}
  73          */     BadArgs(ResourceBundle i18n, String s, Object o) {
  74             super(MessageFormat.format(i18n.getString(s), new Object[] {o}));
  75         }
  76 
  77 
  78         /**
  79          * Create a BadArgs exception.
  80          * @param i18n A resource bundle in which to find the detail message.
  81          * @param s The key for the detail message.
  82          * @param o An array of arguments to be formatted with the detail message by
  83          * {@link java.text.MessageFormat#format}
  84          */
  85         BadArgs(ResourceBundle i18n, String s, Object[] o) {
  86             super(MessageFormat.format(i18n.getString(s), o));
  87         }
  88     }
  89 
  90     /**
  91      * This exception is used to report problems that arise when using this API.
  92      */
  93     public static class Fault extends Exception
  94     {
  95         Fault(I18NResourceBundle i18n, String s) {
  96             super(i18n.getString(s));
  97         }
  98 
  99         Fault(I18NResourceBundle i18n, String s, Object o) {
 100             super(i18n.getString(s, o));
 101         }
 102 
 103         Fault(I18NResourceBundle i18n, String s, Object[] o) {
 104             super(i18n.getString(s, o));
 105         }
 106     }
 107 
 108     /**
 109      * Command line entry point. Run with <code>-help</code> to get
 110      * brief command line help.  Warning: this method uses System.exit
 111      * and so does not return if called directly.
 112      * @param args Comamnd line arguments.
 113      */
 114     public static void main(String[] args) {
 115         try {
 116             EditJTI e = new EditJTI();
 117             boolean ok = e.run(args);
 118             System.exit(ok ? 0 : 1);
 119         }
 120         catch (BadArgs e) {
 121             System.err.println(e.getMessage());
 122             usage(System.err);
 123             System.exit(2);
 124         }
 125         catch (Fault e) {
 126             System.err.println(e.getMessage());
 127             System.exit(3);
 128         }
 129     }
 130 
 131     /**
 132      * Print out brief command line help.
 133      * @param out the stream to which to write the command line help.
 134      */
 135     public static void usage(PrintStream out) {
 136         String prog = System.getProperty("program", "java " + EditJTI.class.getName());
 137         out.println(i18n.getString("editJTI.usage.title"));
 138         out.print("  ");
 139         out.print(prog);
 140         out.println(i18n.getString("editJTI.usage.summary"));
 141 
 142         out.println(i18n.getString("editJTI.usage.options"));
 143         out.println(i18n.getString("editJTI.usage.help1"));
 144         out.println(i18n.getString("editJTI.usage.help2"));
 145         out.println(i18n.getString("editJTI.usage.help3"));
 146         out.println(i18n.getString("editJTI.usage.classpath1"));
 147         out.println(i18n.getString("editJTI.usage.classpath2"));
 148         out.println(i18n.getString("editJTI.usage.log1"));
 149         out.println(i18n.getString("editJTI.usage.log2"));
 150         out.println(i18n.getString("editJTI.usage.outfile1"));
 151         out.println(i18n.getString("editJTI.usage.outfile2"));
 152         out.println(i18n.getString("editJTI.usage.path1"));
 153         out.println(i18n.getString("editJTI.usage.path2"));
 154         out.println(i18n.getString("editJTI.usage.preview1"));
 155         out.println(i18n.getString("editJTI.usage.preview2"));
 156         out.println(i18n.getString("editJTI.usage.ts1"));
 157         out.println(i18n.getString("editJTI.usage.ts2"));
 158         out.println(i18n.getString("editJTI.usage.verbose1"));
 159         out.println(i18n.getString("editJTI.usage.verbose2"));
 160         out.println("");
 161         out.println(i18n.getString("editJTI.usage.edit"));
 162         out.println(i18n.getString("editJTI.usage.set"));
 163         out.println(i18n.getString("editJTI.usage.search"));
 164         out.println("");
 165     }
 166 
 167     /**
 168      * Run the utility, without exiting.  Any messages are written to
 169      * the standard output stream.
 170      * @param args command line args
 171      * @return true if the resulting configuration is valid (complete),
 172      * and false otherwise.
 173      * @throws EditJTI.BadArgs if there is an error analysing the args
 174      * @throws EditJTI.Fault if there is an error executing the args
 175      */
 176     public boolean run(String[] args) throws BadArgs, Fault {
 177         PrintWriter out = new PrintWriter(System.out);
 178         try {
 179             return run(args, out);
 180         }
 181         finally {
 182             out.flush();
 183         }
 184     }
 185 
 186 
 187     /**
 188      * Run the utility, without exiting, writing any messages to a specified stream.
 189      * @param args command line args
 190      * @param out the stream to which to write any messages
 191      * @return true if the resulting configuration is valid (complete),
 192      * and false otherwise.
 193      * @throws EditJTI.BadArgs if there is an error analysing the args
 194      * @throws EditJTI.Fault if there is an error executing the args
 195      */
 196     public boolean run(String[] args, PrintWriter out) throws BadArgs, Fault {
 197         File inFile = null;
 198         File outFile = null;
 199         File logFile = null;
 200         File classPath = null;
 201         File testSuitePath = null;
 202         File workDirPath = null;
 203         String[] editCmds = null;
 204         boolean helpFlag = false;
 205         boolean previewFlag = false;
 206         boolean showPathFlag = false;
 207         boolean verboseFlag = false;
 208 
 209         for (int i = 0; i < args.length; i++) {
 210             if ((args[i].equals("-o") || args[i].equals("-out"))
 211                 && i + 1 < args.length) {
 212                 checkUnset(outFile, args[i]);
 213                 outFile = new File(args[++i]);
 214             }
 215             else if ((args[i].equals("-i") || args[i].equals("-in"))
 216                 && i + 1 < args.length) {
 217                 checkUnset(inFile, args[i]);
 218                 inFile = new File(args[++i]);
 219             }
 220             else if ((args[i].equals("-l") || args[i].equals("-log"))
 221                      && i + 1 < args.length) {
 222                 checkUnset(logFile, args[i]);
 223                 logFile = new File(args[++i]);
 224             }
 225             else if (args[i].equals("-n") || args[i].equals("-preview"))
 226                 previewFlag = true;
 227             else if (args[i].equals("-p") || args[i].equals("-path"))
 228                 showPathFlag = true;
 229             else if (args[i].equals("-v") || args[i].equals("-verbose")  )
 230                 verboseFlag = true;
 231             else if ((args[i].equals("-cp") || args[i].equals("-classpath")) && i + 1 < args.length) {
 232                 checkUnset(classPath, args[i]);
 233                 classPath = new File(args[++i]);
 234             }
 235             else if ((args[i].equals("-ts") || args[i].equals("-testsuite")) && i + 1 < args.length) {
 236                 checkUnset(testSuitePath, args[i]);
 237                 testSuitePath = new File(args[++i]);
 238             }
 239             else if ((args[i].equals("-wd") || args[i].equals("-workdir")) && i + 1 < args.length) {
 240                 checkUnset(testSuitePath, args[i]);
 241                 workDirPath = new File(args[++i]);
 242             }
 243             else if (args[i].equals("-help") || args[i].equals("-usage") || args[i].equals("/?") )
 244                 helpFlag = true;
 245             else if (args[i].startsWith("-"))
 246                 throw new BadArgs(i18n, "editJTI.badOption", args[i]);
 247             else if (i <= args.length - 1) {
 248                 if (inFile == null) {
 249                     editCmds = new String[args.length - 1 - i];
 250                     System.arraycopy(args, i, editCmds, 0, editCmds.length);
 251                     inFile = new File(args[args.length - 1]);
 252                 }
 253                 else {
 254                     editCmds = new String[args.length - i];
 255                     System.arraycopy(args, i, editCmds, 0, editCmds.length);
 256                 }
 257                 i = args.length - 1;
 258             }
 259             else
 260                 throw new BadArgs(i18n, "editJTI.badOption", args[i]);
 261         }
 262 
 263         if (args.length == 0 || helpFlag) {
 264             usage(System.out);
 265             if (inFile == null)
 266                 return true;
 267         }
 268 
 269         if (classPath != null && testSuitePath != null)
 270             throw new BadArgs(i18n, "editJTI.cantHaveClassPathAndTestSuite");
 271 
 272         if (inFile == null)
 273             throw new BadArgs(i18n, "editJTI.noInterview");
 274 
 275         // if (editCmds.length == 0 && outFile == null && logFile == null && !showPathFlag)
 276         //     throw new BadArgs(...no.actions....);
 277 
 278         verbose = verboseFlag;
 279         this.out = out;
 280 
 281         try {
 282             /* the following looks nice and simple, but breaks compatibility
 283                with 3.1.4, because InterviewParameters.open will try and open
 284                the wd in the .jti file if not given explicitly -- and previously,
 285                this was not required/done.  So, only use the simple code if wd
 286                is set, and use the old 3.1.4 code if wd is not set.
 287             */
 288             /* See comment above
 289             if (workDirPath != null || testSuitePath != null) {
 290                 interview = InterviewParameters.open(testSuitePath, workDirPath, inFile);
 291             }
 292             */
 293             if (workDirPath != null)
 294                 interview = InterviewParameters.open(testSuitePath, workDirPath, inFile);
 295             else if (testSuitePath != null) {
 296                 // only open the test suite, not the work dir
 297                 TestSuite ts;
 298                 try {
 299                     ts = TestSuite.open(testSuitePath);
 300                 }
 301                 catch (FileNotFoundException e) {
 302                     throw new Fault(i18n, "editJTI.cantFindTestSuite", testSuitePath);
 303                 }
 304                 catch (TestSuite.NotTestSuiteFault e) {
 305                     throw new Fault(i18n, "editJTI.notATestSuite", testSuitePath);
 306                 }
 307                 catch (TestSuite.Fault e) {
 308                     throw new Fault(i18n, "editJTI.cantOpenTestSuite",
 309                                     new Object[] { testSuitePath, e });
 310                 }
 311                 load(inFile, ts);
 312             }
 313             // End of patches for 3.1.4 compatibility
 314             else if (classPath != null) {
 315                 URLClassLoader loader = new URLClassLoader(new URL[] { classPath.toURL() });
 316                 load(inFile, loader);
 317             }
 318             else
 319                 load(inFile);
 320 
 321         }
 322         catch (Interview.Fault e) {
 323             throw new Fault(i18n, "editJTI.cantOpenFile",
 324                             new Object[] { inFile.getPath(), e.getMessage() });
 325         }
 326         catch (FileNotFoundException e) {
 327             throw new Fault(i18n, "editJTI.cantFindFile", inFile.getPath());
 328         }
 329         catch (IOException e) {
 330             throw new Fault(i18n, "editJTI.cantOpenFile",
 331                             new Object[] { inFile.getPath(), e });
 332         }
 333         catch (IllegalStateException e) {
 334             // only occurs if keywords are being used in the config, and the
 335             // test suite is not available.  user needs to specify -wd or -ts
 336             if (verbose)
 337                 e.printStackTrace();
 338 
 339             throw new Fault(i18n, "editJTI.badState", e.getMessage());
 340         }
 341 
 342         if (NUM_BACKUPS > 0)
 343             interview.setBackupPolicy(BackupPolicy.simpleBackups(NUM_BACKUPS));
 344 
 345         if (editCmds != null)
 346             edit(editCmds);
 347 
 348 
 349         if (showPathFlag)
 350             showPath();
 351 
 352         try {
 353             if (logFile != null) {
 354                 if (previewFlag) {
 355                     String msg = i18n.getString("editJTI.wouldWriteLog", logFile);
 356                     out.println(msg);
 357                 }
 358                 else
 359                     writeLog(logFile);
 360             }
 361         }
 362         catch (IOException e) {
 363             throw new Fault(i18n, "editJTI.cantWriteLog",
 364                             new Object[] { logFile.getPath(), e });
 365         }
 366 
 367 
 368         try {
 369             if (previewFlag) {
 370                 String msg;
 371                 if (interview.isEdited())
 372                     msg = i18n.getString("editJTI.wouldSaveEdited",
 373                                 (outFile != null ? outFile : inFile));
 374                 else if (outFile != null)
 375                     msg = i18n.getString("editJTI.wouldSaveNotEdited", outFile);
 376                 else
 377                     msg = i18n.getString("editJTI.wouldNotSave");
 378                 out.println(msg);
 379             }
 380             else {
 381                 if (outFile != null)
 382                     save(outFile);
 383                 else if (interview.isEdited())
 384                     save(inFile);
 385             }
 386         }
 387         catch (Interview.Fault e) {
 388             throw new Fault(i18n, "editJTI.cantOpenFile",
 389                             new Object[] {
 390                                 (outFile == null || outFile.getPath() == null ?
 391                                  "??": outFile.getPath()), e });
 392         }
 393         catch (IOException e) {
 394             File f = (outFile == null ? interview.getFile() : outFile);
 395             throw new Fault(i18n, "editJTI.cantSaveFile",
 396                             new Object[] { f.getPath(), e });
 397         }
 398 
 399         return (interview.isFinishable());
 400     }
 401 
 402     /**
 403      * Load a configuration file to be edited.
 404      * @param inFile the file to be loaded
 405      * @throws IOException if there is a problem reading the file
 406      * @throws Interview.Fault if there is a problem loading the interview data from the file
 407      */
 408     public void load(File inFile) throws IOException, Interview.Fault {
 409         // this opens the interview via the work directory and test suite;
 410         // the test suite implicitly knows its classpath via the .jtt file
 411         interview = InterviewParameters.open(inFile);
 412         interview.setEdited(false);
 413     }
 414 
 415     /**
 416      * Load a configuration file to be edited.
 417      * @param inFile the file to be loaded
 418      * @param ts the test suite for which the interview is to be loaded
 419      * @throws IOException if there is a problem reading the file
 420      * @throws Interview.Fault if there is a problem loading the interview data from the file
 421      * @throws EditJTI.Fault if there is a problem creating the interview for the testsuite
 422      */
 423     public void load(File inFile, TestSuite ts)
 424         throws IOException, Interview.Fault, Fault
 425     {
 426         // this opens the interview via the work directory and test suite;
 427         // the test suite implicitly knows its classpath via the .jtt file
 428         try {
 429             interview = ts.createInterview();
 430         }
 431         catch (TestSuite.Fault e) {
 432             throw new Fault(i18n, "editJTI.cantCreateInterviewForTestSuite",
 433                             new Object[] { ts.getPath(), e.getMessage() });
 434         }
 435         interview.load(inFile);
 436         interview.setEdited(false);
 437     }
 438 
 439     /**
 440      * Load a configuration file to be edited, using a specified class loader
 441      * to load the interview class.
 442      * @param inFile the file to be loaded
 443      * @param loader the class loader to be used to load the interview class
 444      * @throws IOException if there is a problem reading the file
 445      * @throws Interview.Fault if there is a problem loading the interview data from the file
 446      * @throws EditJTI.Fault if there is a problem creating the interview for the testsuite
 447      */
 448     public void load(File inFile, URLClassLoader loader)
 449         throws IOException, Interview.Fault, Fault
 450     {
 451         InputStream in = new BufferedInputStream(new FileInputStream(inFile));
 452         Properties p = new Properties();
 453         p.load(in);
 454         in.close();
 455 
 456         String interviewClassName = (String) (p.get("INTERVIEW"));
 457         try {
 458             Class<? extends InterviewParameters> interviewClass =
 459                     loader.loadClass(interviewClassName).asSubclass(InterviewParameters.class);
 460 
 461             interview = interviewClass.getDeclaredConstructor().newInstance();
 462         }
 463         catch (ClassCastException e) {
 464             throw new Fault(i18n, "editJTI.invalidInterview", inFile);
 465         }
 466         catch (ClassNotFoundException e) {
 467             throw new Fault(i18n, "editJTI.cantFindClass",
 468                             new Object[] { interviewClassName, inFile });
 469         }
 470         catch (NoSuchMethodException | InstantiationException | InvocationTargetException e) {
 471             throw new Fault(i18n, "editJTI.cantInstantiateClass",
 472                             new Object[] { interviewClassName, inFile });
 473         }
 474         catch (IllegalAccessException e) {
 475             throw new Fault(i18n, "editJTI.cantAccessClass",
 476                             new Object[] { interviewClassName, inFile });
 477         }
 478         finally {
 479             try { if (in != null) in.close(); } catch (IOException e) {}
 480         }
 481 
 482         interview.load(inFile);
 483         interview.setEdited(false);
 484     }
 485 
 486     /**
 487      * Save the edited configuration in a specified file.
 488      * @param file The file in which to save the configuration
 489      * @throws IOException if there is a problem while writing the file
 490      * @throws Interview.Fault if there is a problem while saving the interview data
 491      */
 492     public void save(File file) throws IOException, Interview.Fault {
 493         interview.save(file);
 494     }
 495 
 496     /**
 497      * Show the current question path for the configuration.
 498      */
 499     public void showPath() {
 500         Question[] path = interview.getPath();
 501 
 502         int indent = 0;
 503         for (int i = 0; i < path.length; i++)
 504             indent = Math.max(indent, path[i].getTag().length());
 505         indent = Math.min(indent, MAX_INDENT);
 506 
 507         for (int i = 0; i < path.length; i++) {
 508             Question q = path[i];
 509             String tag = q.getTag();
 510             String value = q.getStringValue();
 511             out.print(tag);
 512             int l = tag.length();
 513             if (l > MAX_INDENT && value != null && value.length() > 0) {
 514                 out.println();
 515                 l = 0;
 516             }
 517             for (int x = l; x < indent; x++)
 518                 out.print(' ');
 519             out.print(' ');
 520             out.println(value == null ? "" : value);
 521         }
 522     }
 523 
 524     /**
 525      * Write a log of the questions that determine the current configuration.
 526      * @param logFile the file to which to write the log
 527      * @throws IOException if there is a problem while writing the log file
 528      */
 529     public void writeLog(File logFile) throws IOException {
 530         WizPrint wp = new WizPrint(interview);
 531         wp.setShowResponses(true);
 532         wp.setShowResponseTypes(false);
 533         wp.setShowTags(true);
 534         BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(logFile), StandardCharsets.UTF_8));
 535         wp.write(out);
 536     }
 537 
 538     /**
 539      * Apply a series of edits to the current configuration.
 540      * @param cmds the editing commands to be applied
 541      * @throws EditJTI.Fault if there is a problem while applying the edit commands.
 542      * @see #edit(String)
 543      */
 544     public void edit(String[] cmds) throws Fault {
 545         for (int i = 0; i < cmds.length; i++) {
 546             edit(cmds[i]);
 547         }
 548     }
 549 
 550     /**
 551      * Apply an edit to the current configuration.
 552      * @param cmd the editing command to be applied
 553      * Currently, two forms of command are supported: <dl>
 554      * <dt><em>tag-name=value</em>
 555      * <dd>Set the response to the question whose value is <em>tag-name</em> to <em>value</em>
 556      * <dt><em>/search/replace/</em>
 557      * <dd>For all questions on the current path, change instances of <em>search</em> to  <em>replace</em>
 558      * </dl>
 559      * @throws EditJTI.Fault if there is a problem while applying the edit commands.
 560      * @see #edit(String[])
 561      */
 562     public void edit(String cmd) throws Fault {
 563         if (cmd == null || cmd.length() == 0)
 564             return;
 565 
 566         int eqIndex = cmd.indexOf('=');
 567         if (Character.isJavaIdentifierStart(cmd.charAt(0)) && eqIndex > 0)
 568             setValue(cmd.substring(0, eqIndex), cmd.substring(eqIndex + 1));
 569         else if (cmd.toLowerCase().startsWith("import:")) {
 570             importFile(new File(cmd.substring("import:".length())));
 571         }
 572         else {
 573             int left = 0;
 574             // could support a command letter in front?
 575             char delim = cmd.charAt(left);
 576             int center = cmd.indexOf(delim, left + 1);
 577             if (center == -1)
 578                 throw new Fault(i18n, "editJTI.badCmd", cmd);
 579             // could support trailing flags?
 580             int right = cmd.length() - 1;
 581             if (cmd.charAt(right) != delim)
 582                 throw new Fault(i18n, "editJTI.badCmd", cmd);
 583             String searchText = cmd.substring(left + 1, center);
 584             String replaceText = cmd.substring(center + 1, right);
 585             if (searchText.length() == 0)
 586                 throw new Fault(i18n, "editJTI.badCmd", cmd);
 587             setMatchingValues(searchText, replaceText);
 588         }
 589     }
 590 
 591     private void importFile(File file) throws Fault {
 592 
 593         InputStream in;
 594         try {
 595             in = new BufferedInputStream(new FileInputStream(file));
 596         }
 597         catch (FileNotFoundException e) {
 598             throw new Fault(i18n, "editJTI.cantFindImport", new Object[] { file, e });
 599         }
 600 
 601         Properties p;
 602         try {
 603             p = new Properties();
 604             p.load(in);
 605             in.close();
 606         }
 607         catch (IOException e) {
 608             throw new Fault(i18n, "editJTI.cantReadImport",
 609                             new Object[] { file, e });
 610         }
 611         finally {
 612             try { if (in != null) in.close(); } catch (IOException e) {}
 613         }
 614 
 615         // for each question on the path, see if there is a corresponding
 616         // imported value
 617         Question[] path = interview.getPath();
 618         for (int i = 0; i < path.length; i++) {
 619             Question q = path[i];
 620             String v = p.getProperty(q.getTag());
 621             if (v != null) {
 622                 setValue(q, v);
 623                 path = interview.getPath(); // update path in case tail has changed
 624             }
 625         }
 626     }
 627 
 628     private void setMatchingValues(String searchText, String replaceText) throws Fault {
 629         boolean found = false;
 630 
 631         Question[] path = interview.getPath();
 632         for (int i = 0; i < path.length; i++) {
 633             Question q = path[i];
 634             String currValue = q.getStringValue();
 635             if (currValue == null)
 636                 continue;
 637             // currently hardwired: considerCase: false; word match: false
 638             int pos = match(searchText, currValue, false, false);
 639             if (pos >= 0) {
 640                 String newValue = currValue.substring(0, pos)
 641                     + replaceText
 642                     + currValue.substring(pos + searchText.length());
 643                 setValue(q, newValue);
 644                 found = true;
 645                 path = interview.getPath(); // update path in case tail has changed
 646             }
 647         }
 648         if (!found)
 649             throw new Fault(i18n, "editJTI.cantFindMatch", searchText);
 650     }
 651 
 652     private void setValue(String tag, String value) throws Fault {
 653         Question[] path = interview.getPath();
 654         for (int i = 0; i < path.length; i++) {
 655             Question q = path[i];
 656             if (q.getTag().equals(tag)) {
 657                 setValue(q, value);
 658                 return;
 659             }
 660         }
 661         throw new Fault(i18n, "editJTI.cantFindQuestion", tag);
 662     }
 663 
 664     private void setValue(Question q, String value) throws Fault {
 665         try {
 666             String oldValue = q.getStringValue();
 667             q.setValue(value);
 668             if (verbose)
 669                 out.println(i18n.getString("editJTI.update",
 670                     new Object[] { q.getTag(), oldValue, q.getStringValue() }));
 671         }
 672         catch (Interview.Fault e) {
 673             throw new Fault(i18n, "editJTI.cantSetValue", new Object[] { q.getTag(), e.getMessage() } );
 674         }
 675     }
 676 
 677     private static int match(String s1, String s2, boolean considerCase, boolean word) {
 678         int s1len = s1.length();
 679         int s2len = s2.length();
 680         for (int i = 0; i <= s2len - s1len; i++) {
 681             if (s1.regionMatches(!considerCase, 0, s2, i, s1len)) {
 682                 if (!word || (word &&
 683                               ( (i == 0 || isBoundaryCh(s2.charAt(i-1)))
 684                                 && (i+s1len == s2.length() || isBoundaryCh(s2.charAt(i+s1len))) )))
 685                     return i;
 686             }
 687         }
 688         return -1;
 689     }
 690 
 691     private static boolean isBoundaryCh(char c) {
 692         return !(Character.isUnicodeIdentifierStart(c)
 693                  || Character.isUnicodeIdentifierPart(c));
 694     }
 695 
 696     private static void checkUnset(Object item, String option)
 697         throws BadArgs
 698     {
 699         if (item != null)
 700             throw new BadArgs(i18n, "editJTI.dupOption",option);
 701     }
 702 
 703     private InterviewParameters interview;
 704     private boolean verbose;
 705     private PrintWriter out;
 706 
 707     private static int MAX_INDENT = Integer.getInteger("EditJTI.maxIndent", 32).intValue();
 708     private static int NUM_BACKUPS = Integer.getInteger("EditJTI.numBackups", 2).intValue();
 709 
 710     private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(EditJTI.class);
 711 }