1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 2001, 2016, 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 
  28 package com.sun.javatest;
  29 
  30 import java.io.BufferedInputStream;
  31 import java.io.BufferedOutputStream;
  32 import java.io.DataInputStream;
  33 import java.io.DataOutputStream;
  34 import java.io.File;
  35 import java.io.FileInputStream;
  36 import java.io.FileOutputStream;
  37 import java.io.FileNotFoundException;
  38 import java.io.InputStream;
  39 import java.io.IOException;
  40 import java.io.OutputStream;
  41 import java.lang.ref.WeakReference;
  42 import java.util.ArrayList;
  43 import java.util.HashMap;
  44 import java.util.Map;
  45 import java.util.Properties;
  46 import java.util.TreeMap;
  47 
  48 import com.sun.javatest.logging.LoggerFactory;
  49 import com.sun.javatest.util.I18NResourceBundle;
  50 import com.sun.javatest.util.LogFile;
  51 
  52 /**
  53  * A class providing access to the working state of a test run, as embodied
  54  * in a work directory.
  55  */
  56 public class WorkDirectory {
  57 
  58     /**
  59      * This exception is used to report problems that arise when using
  60      * work directories.
  61      */
  62     public static class Fault extends Exception {
  63         Fault(I18NResourceBundle i18n, String s) {
  64             super(i18n.getString(s));
  65         }
  66 
  67         Fault(I18NResourceBundle i18n, String s, Object o) {
  68             super(i18n.getString(s, o));
  69         }
  70 
  71         Fault(I18NResourceBundle i18n, String s, Object[] o) {
  72             super(i18n.getString(s, o));
  73         }
  74     }
  75 
  76 
  77     /**
  78      * Signals that the template pointed to by that directory is missing.
  79      */
  80     public static class TemplateMissingFault extends Fault {
  81         TemplateMissingFault(I18NResourceBundle i18n, String key, File f, String template) {
  82             super(i18n, key, new Object[] {f.getPath(), template});
  83         }
  84 
  85         TemplateMissingFault(I18NResourceBundle i18n, String key, File f, String template, Throwable t) {
  86             super(i18n, key, new Object[] {f.getPath(), template, t.toString()});
  87         }
  88     }
  89 
  90     /**
  91      * Signals that there is a serious, unrecoverable problem when trying to
  92      * open or create a work directory.
  93      */
  94     public static class BadDirectoryFault extends Fault {
  95         BadDirectoryFault(I18NResourceBundle i18n, String key, File f) {
  96             super(i18n, key, f.getPath());
  97         }
  98 
  99         BadDirectoryFault(I18NResourceBundle i18n, String key, File f, Throwable t) {
 100             super(i18n, key, new Object[] {f.getPath(), t.toString()});
 101         }
 102     }
 103 
 104     /**
 105      * Signals that a directory (while valid in itself) is not a valid work directory.
 106      */
 107     public static class NotWorkDirectoryFault extends Fault {
 108         NotWorkDirectoryFault(I18NResourceBundle i18n, String key, File f) {
 109             super(i18n, key, f.getPath());
 110         }
 111     }
 112 
 113     /**
 114      * Signals that a work directory already exists when an attempt is made
 115      * to create one.
 116      */
 117     public static class WorkDirectoryExistsFault extends Fault {
 118         WorkDirectoryExistsFault(I18NResourceBundle i18n, String key, File f) {
 119             super(i18n, key, f.getPath());
 120         }
 121     }
 122 
 123     /**
 124      * Signals that a work directory does not match the given test suite.
 125      */
 126     public static class MismatchFault extends Fault {
 127         MismatchFault(I18NResourceBundle i18n, String key, File f) {
 128             super(i18n, key, f.getPath());
 129         }
 130     }
 131 
 132     /**
 133      * Signals that there is a problem trying to determine the test suite
 134      * appropriate for the work directory.
 135      */
 136     public static class TestSuiteFault extends Fault {
 137         TestSuiteFault(I18NResourceBundle i18n, String key, File f, Object o) {
 138             super(i18n, key, new Object[] {f.getPath(), o});
 139         }
 140     }
 141 
 142     /**
 143      * Signals that there is a problem trying to initialize from the data in
 144      * the work directory.
 145      */
 146     public static class InitializationFault extends Fault {
 147         InitializationFault(I18NResourceBundle i18n, String key, File f, Object o) {
 148             super(i18n, key, new Object[] {f.getPath(), o});
 149         }
 150     }
 151 
 152     /**
 153      * Signals that a problem occurred while trying to purge files in work directory.
 154      */
 155     public static class PurgeFault extends Fault {
 156         PurgeFault(I18NResourceBundle i18n, String key, File f, Object o) {
 157             super(i18n, key, new Object[] {f.getPath(), o});
 158         }
 159     }
 160 
 161     /**
 162      * Check if a directory is a work directory. This is intended to be a quick
 163      * check, rather than exhaustive one; as such, it simply checks for the
 164      * existence of the "jtData" subdirectory.
 165      * @param dir the directory to be checked
 166      * @return true if and only if the specified directory appears to be
 167      * a work directory
 168      */
 169     public static boolean isWorkDirectory(File dir) {
 170         //System.err.println("WorkDirectory.isWorkDirectory: " + dir);
 171         File jtData = new File(dir, JTDATA);
 172 
 173         if (jtData.exists() && jtData.isDirectory())
 174             // should consider checking for existence of test suite data
 175             return true;
 176         else
 177             return false;
 178     }
 179 
 180     /**
 181      * Check if a directory is an empty directory.
 182      * @param dir the directory to be checked
 183      * @return true if and only if the directory is empty
 184      */
 185     public static boolean isEmptyDirectory(File dir) {
 186         if (dir.exists() && dir.canRead() && dir.isDirectory()) {
 187             String[] list = dir.list();
 188             return (list == null || list.length == 0);
 189         } else
 190             return false;
 191     }
 192 
 193     /**
 194      * Do sanity check of workdir.  All critical areas must be
 195      * read-write.
 196      */
 197     public static boolean isUsableWorkDirectory(File dir) {
 198         if (dir == null || !isUsable(dir))
 199             return false;
 200 
 201         try {
 202             File canonDir = canonicalize(dir);
 203             File jtData = new File(canonDir, JTDATA);
 204 
 205             // could call isWorkDirectory(File)
 206             if (!isUsable(jtData))
 207                 return false;
 208 
 209             // all files in jtData must be read-write
 210             File[] content = jtData.listFiles();
 211 
 212             // could even look for key files while doing this loop
 213             if (content != null && content.length > 0)
 214                 for (int i = 0; i < content.length; i++)
 215                     if (!isUsable(content[i]))
 216                         return false;
 217         } catch (BadDirectoryFault f) {
 218             return false;
 219         }
 220 
 221         return true;
 222     }
 223 
 224     private static boolean isUsable(File f) {
 225         if (!f.exists())
 226             return false;
 227 
 228         if (!f.canRead())
 229             return false;
 230 
 231         if (!f.canWrite())
 232             return false;
 233 
 234         return true;
 235     }
 236 
 237     /**
 238      * Create a new work directory with a given name, and for a given test suite.
 239      * @param dir the directory to be created as a work directory.
 240      * This directory may (but need not) exist; if it does exist, it must be empty.
 241      * @param ts the test suite for which this will be a work directory
 242      * @return the WorkDirectory that was created
 243      * @throws WorkDirectory.WorkDirectoryExistsFault if the work directory
 244      *          could not be created because it already exists.
 245      *          If this exception is thrown, you may want to call {@link #open}
 246      *          instead.
 247      * @throws WorkDirectory.BadDirectoryFault is there was a problem creating
 248      *          the work directory.
 249      * @throws WorkDirectory.InitializationFault if there are unrecoverable problems encountered
 250      *         while reading the data present in the work directory
 251      * @see #convert
 252      * @see #open
 253      */
 254     public static WorkDirectory create(File dir, TestSuite ts)
 255     throws BadDirectoryFault, WorkDirectoryExistsFault, InitializationFault {
 256         //System.err.println("WD.create: " + dir);
 257         return createOrConvert(dir, ts, true);
 258     }
 259 
 260     /**
 261      * Convert an existing directory into a work directory.
 262      * @param dir the directory to be converted to a work directory
 263      * @param ts  the test suite for which this will be a work directory
 264      * @return the WorkDirectory that was created
 265      * @throws FileNotFoundException if the directory to be converted does
 266      *          not exist
 267      * @throws WorkDirectory.WorkDirectoryExistsFault if the work directory
 268      *          could not be created because it already exists.
 269      *          If this exception is thrown, you may want to call {@link #open}
 270      *          instead.
 271      * @throws WorkDirectory.BadDirectoryFault is there was a problem creating
 272      *          the work directory.
 273      * @throws WorkDirectory.InitializationFault if there are unrecoverable problems encountered
 274      *         while reading the data present in the work directory
 275      * @see #create
 276      * @see #open
 277      */
 278     public static WorkDirectory convert(File dir, TestSuite ts)
 279     throws BadDirectoryFault, WorkDirectoryExistsFault,
 280             FileNotFoundException, InitializationFault {
 281         if (!dir.exists())
 282             throw new FileNotFoundException(dir.getPath());
 283         return createOrConvert(dir, ts, false);
 284     }
 285 
 286     private static WorkDirectory createOrConvert(File dir, TestSuite ts, boolean checkEmpty)
 287     throws BadDirectoryFault, WorkDirectoryExistsFault, InitializationFault {
 288         File canonDir;
 289         File jtData;
 290         ArrayList<File> undoList = new ArrayList<>();
 291 
 292         try {
 293             if (dir.exists()) {
 294                 canonDir = canonicalize(dir);
 295                 jtData = new File(canonDir, JTDATA);
 296 
 297 
 298                 if (!canonDir.isDirectory())
 299                     throw new BadDirectoryFault(i18n, "wd.notDirectory", canonDir);
 300 
 301                 if (!canonDir.canRead())
 302                     throw new BadDirectoryFault(i18n, "wd.notReadable", canonDir);
 303 
 304                 if (jtData.exists() && jtData.isDirectory())
 305                     throw new WorkDirectoryExistsFault(i18n, "wd.alreadyExists", canonDir);
 306 
 307                 if (checkEmpty) {
 308                     String[] list = canonDir.list();
 309                     if (list != null && list.length > 0)
 310                         throw new BadDirectoryFault(i18n, "wd.notEmpty", canonDir);
 311                 }
 312 
 313                 // actively flush the dirMap for canonDir?
 314             } else {
 315                 if (!mkdirs(dir, undoList))
 316                     throw new BadDirectoryFault(i18n, "wd.cantCreate", dir);
 317                 canonDir = canonicalize(dir);
 318                 jtData = new File(canonDir, JTDATA);
 319             }
 320 
 321             if (!mkdirs(jtData, undoList))
 322                 throw new BadDirectoryFault(i18n, "wd.cantCreate", canonDir);
 323 
 324             try {
 325                 WorkDirectory wd;
 326 
 327                 synchronized (dirMap) {
 328                     wd = new WorkDirectory(canonDir, ts, null);
 329                     // dirMap.put(canonDir, new WeakReference(wd));
 330                 }
 331 
 332                 wd.saveTestSuiteInfo();
 333 
 334                 // create successful -- so zap the undoList
 335                 undoList = null;
 336 
 337                 return wd;
 338             } catch (IOException e) {
 339                 throw new BadDirectoryFault(i18n, "wd.cantWriteTestSuiteInfo", canonDir, e);
 340             }
 341         } finally {
 342             if (undoList != null)
 343                 undo(undoList);
 344         }
 345     }
 346 
 347     public String getLogFileName() {
 348         return logFileName;
 349     }
 350 
 351     private static boolean mkdirs(File dir, ArrayList<File> undoList) {
 352         File parent = dir.getParentFile();
 353         if (parent != null && !parent.exists()) {
 354             if (!mkdirs(parent, undoList))
 355                 return false;
 356         }
 357 
 358         if (dir.mkdir()) {
 359             //System.err.println("WD.mkdir " + dir);
 360             undoList.add(dir);
 361             return true;
 362         }
 363 
 364         return false;
 365     }
 366 
 367     private static void undo(ArrayList<File> undoList) {
 368         for (int i = undoList.size() - 1; i >= 0; i--) {
 369             File f = undoList.get(i);
 370             delete(f);
 371         }
 372     }
 373 
 374     private static void delete(File f) {
 375         //System.err.println("WD.delete " + f);
 376         if (f.isDirectory()) {
 377             File[] ff = f.listFiles();
 378             for (int i = 0; i < ff.length; i++) {
 379                 if (ff[i].isDirectory() || ff[i].isFile())
 380                     delete(ff[i]);
 381             }
 382         }
 383         f.delete();
 384     }
 385 
 386         public static void changeTemplate(File dir, File newTemplate) {
 387             File templateData = new File(dir, JTDATA + File.separator + "template.data");
 388             if (templateData.exists()) {
 389                 Properties p = new Properties();
 390                 final String absolutePath = templateData.getAbsolutePath();
 391                 FileInputStream fis = null;
 392                 FileOutputStream fos = null;
 393                 try {
 394                     fis = new FileInputStream(absolutePath);
 395                     p.load(fis);
 396                     p.setProperty("file", newTemplate.getCanonicalPath());
 397                     fos = new FileOutputStream(absolutePath);
 398                     p.store(fos, "template information file - do not modify");
 399                 } catch (IOException e) {
 400                         e.printStackTrace();
 401                 }
 402                 finally {
 403                     try { if (fis != null) fis.close(); } catch (IOException e) {}
 404                     try { if (fos != null) fos.close(); } catch (IOException e) {}
 405                 }
 406             }
 407         }
 408 
 409     private static void validateWD(File dir)
 410             throws FileNotFoundException,
 411             BadDirectoryFault,
 412             NotWorkDirectoryFault,
 413             TemplateMissingFault {
 414 
 415         if (!dir.exists()) {
 416             throw new FileNotFoundException(dir.getPath());
 417         }
 418 
 419         File canonDir = canonicalize(dir);
 420 
 421         if (!canonDir.isDirectory()) {
 422             throw new BadDirectoryFault(i18n, "wd.notDirectory", canonDir);
 423         }
 424 
 425         if (!canonDir.canRead()) {
 426             throw new BadDirectoryFault(i18n, "wd.notReadable", canonDir);
 427         }
 428 
 429         File jtData = new File(canonDir, JTDATA);
 430         if (!jtData.exists()) {
 431             throw new NotWorkDirectoryFault(i18n, "wd.notWorkDir", canonDir);
 432         }
 433 
 434         File templateData = new File(canonDir, JTDATA + File.separator + "template.data");
 435         if (templateData.exists()) {
 436             Properties p = new Properties();
 437             try (FileInputStream fis = new FileInputStream(templateData.getAbsolutePath())) {
 438                 p.load(fis);
 439             } catch (IOException e) {
 440                 e.printStackTrace();
 441             }
 442 
 443             String templateFile = p.getProperty("file");
 444             if (!new File(templateFile).exists()) {
 445 
 446                 try {
 447                     // try to calculate relocation
 448                     String oldWDpath = loadWdInfo(jtData).get(PREV_WD_PATH);
 449                     String[] begins = getDiffInPaths(dir.getPath(), oldWDpath);
 450                     if (begins != null) {
 451                         if (templateFile.startsWith(begins[1])) {
 452                             String candidat = begins[0] + templateFile.substring(begins[1].length());
 453                             if (!new File(candidat).exists()) {
 454                                 throw new TemplateMissingFault(i18n, "wd.templateMissing", canonDir, templateFile);
 455                             } else {
 456                                 // update WD info
 457                                 p.setProperty("file", candidat);
 458                                 FileOutputStream out = null;
 459                                 try {
 460                                     out = new FileOutputStream(templateData);
 461                                 } catch (FileNotFoundException e) {
 462                                     // should log the error
 463                                     // e.printStackTrace()
 464                                     return;
 465                                 }
 466 
 467                                 try {
 468                                     p.save(out, "template information file - do not modify");
 469                                 } finally {
 470                                     try { if (out != null) out.close(); } catch (IOException e) {}
 471                                 }
 472 
 473                             }
 474                         }
 475                     }
 476                 } catch (IOException ex) {
 477                     throw new TemplateMissingFault(i18n, "wd.templateMissing", canonDir, templateFile);
 478                 }
 479 
 480             }
 481         }
 482     }
 483 
 484     /*
 485      * This method calculates common tail and returns
 486      * different heads for two paths.
 487      * If no common tail it returns null.
 488      * For example:
 489      * getDiffInPaths("/aaa/bbb/cccc/dddd/rrrr", "/ccc/yyy/dddd/rrrr");
 490      * returns {"/aaa/bbb/cccc", "/ccc/yyy", "/dddd/rrrr"}
 491      */
 492     public static String[] getDiffInPaths(String newPath, String oldWDpath) {
 493         File nP = new File(newPath);
 494         File oP = new File(oldWDpath);
 495 
 496         while (nP.getParent() != null && oP.getParent() != null) {
 497             if (nP.getName().equals(oP.getName())) {
 498                 nP = nP.getParentFile();
 499                 oP = oP.getParentFile();
 500             } else {
 501                 break;
 502             }
 503         }
 504         if (!nP.getPath().equals(newPath) && !oP.getPath().equals(oldWDpath)) {
 505             return new String[] {nP.getPath(), oP.getPath(), newPath.substring(nP.getPath().length())};
 506         }
 507         return null;
 508     }
 509 
 510 
 511 
 512     /**
 513      * Open an existing work directory, using the default test suite associated with it.
 514      * @param dir the directory to be opened as a WorkDirectory
 515      * @return the WorkDirectory that is opened
 516      * @throws FileNotFoundException if the directory identified by <code>dir</code> does
 517      *          not exist. If this exception is thrown, you may want to call {@link #create}
 518      *          instead.
 519      * @throws WorkDirectory.BadDirectoryFault if there was a problem opening the
 520      *          work directory.
 521      * @throws WorkDirectory.NotWorkDirectoryFault if the directory identified
 522      *          by <code>dir</code> is a valid directory, but has not yet been
 523      *          initialized as a work directory. If this exception is thrown,
 524      *          you may want to call {@link #create} instead.
 525      * @throws WorkDirectory.MismatchFault if the test suite recorded in
 526      *          the work directory does not match the test suite's ID recorded
 527      *          in the work directory.
 528      * @throws WorkDirectory.TestSuiteFault if there was a problem determining
 529      *          the test suite for which this is a work directory.
 530      *          If this exception is thrown, you can override the test suite
 531      *          using the other version of {@link #open(File,TestSuite)}.
 532      * @throws WorkDirectory.InitializationFault if there are unrecoverable
 533      *         problems encountered while reading the data present in the
 534      *         work directory
 535      */
 536     public static WorkDirectory open(File dir)
 537     throws FileNotFoundException,
 538             BadDirectoryFault,
 539             NotWorkDirectoryFault,
 540             MismatchFault,
 541             TestSuiteFault,
 542             InitializationFault,
 543             TemplateMissingFault {
 544 
 545          validateWD(dir);
 546 
 547      File canonDir = canonicalize(dir);
 548         File jtData = new File(canonDir, JTDATA);
 549 
 550         WorkDirectory wd;
 551 
 552         synchronized (dirMap) {
 553             // sync-ed to make dirMap data consistent
 554             WeakReference<WorkDirectory> ref = dirMap.get(canonDir);
 555             wd = (ref == null ? null : ref.get());
 556 
 557             if (wd != null)
 558                 return wd;
 559 
 560             Map<String, String> tsInfo;
 561             TestSuite ts;
 562 
 563             try {
 564                 tsInfo = loadTestSuiteInfo(jtData);
 565                 String root = tsInfo.get(TESTSUITE_ROOT);
 566                 if (root == null)
 567                     throw new BadDirectoryFault(i18n, "wd.noTestSuiteRoot", canonDir);
 568 
 569                 File tsr = new File(root);
 570                 if (!tsr.exists())
 571                     throw new TestSuiteFault(i18n, "wd.cantFindTestSuite", canonDir, tsr.getPath());
 572 
 573                 ts = TestSuite.open(tsr);
 574 
 575                 String wdID = (tsInfo == null ? null : tsInfo.get(TESTSUITE_ID));
 576                 String tsID = ts.getID();
 577                 if (!(wdID == null ? "" : wdID).equals(tsID == null ? "" : tsID))
 578                     throw new MismatchFault(i18n, "wd.mismatchID", canonDir);
 579 
 580             }   // try
 581             catch (FileNotFoundException e) {
 582                 throw new BadDirectoryFault(i18n, "wd.noTestSuiteFile", canonDir);
 583             } catch (IOException e) {
 584                 throw new BadDirectoryFault(i18n, "wd.badTestSuiteFile", canonDir, e);
 585             } catch (TestSuite.Fault e) {
 586                 throw new TestSuiteFault(i18n, "wd.cantOpenTestSuite", canonDir, e.toString());
 587             }   // catch
 588 
 589             wd = new WorkDirectory(canonDir, ts, tsInfo);
 590 
 591             // dirMap.put(canonDir, new WeakReference(wd));
 592         }   // sync block
 593 
 594         return wd;
 595     }
 596 
 597     /**
 598      * Open an existing work directory, using an explicit test suite. Any information
 599      * about the test suite previously associated with this work directory is overwritten
 600      * and lost. Therefore this method should be used with care: normally, a work directory
 601      * should be opened with {@link #open(File)}.
 602      *
 603      * @param dir The directory to be opened as a WorkDirectory.
 604      * @param testSuite The test suite to be associated with this work directory.
 605      * @return The WorkDirectory that is opened.
 606      * @throws FileNotFoundException if the directory identified by <code>dir</code> does
 607      *          not exist. If this exception is thrown, you may want to call {@link #create}
 608      *          instead.
 609      * @throws WorkDirectory.BadDirectoryFault if there was a problem opening
 610      *          the work directory.
 611      * @throws WorkDirectory.NotWorkDirectoryFault if the directory identified by
 612      *          <code>dir</code> is a valid directory, but has not yet been
 613      *          initialized as a work directory. f this exception is thrown,
 614      *          you may want to call {@link #create} instead.
 615      * @throws WorkDirectory.MismatchFault if the specified test suite does not
 616      *          match the ID recorded in the work directory.
 617      * @throws WorkDirectory.InitializationFault if there are unrecoverable
 618      *         problems encountered while reading the data present in the
 619      *         work directory
 620      */
 621     public static WorkDirectory open(File dir, TestSuite testSuite)
 622     throws FileNotFoundException,
 623             BadDirectoryFault,
 624             NotWorkDirectoryFault,
 625             MismatchFault,
 626             InitializationFault,
 627             TemplateMissingFault {
 628 
 629          validateWD(dir);
 630 
 631         File canonDir = canonicalize(dir);
 632         File jtData = new File(canonDir, JTDATA);
 633 
 634         WorkDirectory wd = null;
 635         synchronized (dirMap) {
 636             WeakReference<WorkDirectory> ref = dirMap.get(canonDir);
 637             if (ref != null)
 638                 wd = ref.get();
 639 
 640             if (wd == null) {
 641                 Map<String, String> tsInfo;
 642                 try {
 643                     tsInfo = loadTestSuiteInfo(jtData);
 644                 } catch (IOException e) {
 645                     tsInfo = null;
 646                 }
 647 
 648                 String wdID = (tsInfo == null ? null : tsInfo.get(TESTSUITE_ID));
 649                 String tsID = testSuite.getID();
 650                 if (!(wdID == null ? "" : wdID).equals(tsID == null ? "" : tsID))
 651                     throw new MismatchFault(i18n, "wd.mismatchID", canonDir);
 652 
 653                 // no existing instance, create one
 654                 try {
 655                     wd = new WorkDirectory(canonDir, testSuite, tsInfo);
 656                     wd.saveTestSuiteInfo();
 657                     //dirMap.put(canonDir, new WeakReference(wd));
 658                 } catch (IOException e) {
 659                     throw new BadDirectoryFault(i18n, "wd.cantWriteTestSuiteInfo", canonDir, e);
 660                 }
 661             }   // if
 662         }   // sync
 663 
 664         return wd;
 665     }
 666 
 667     /**
 668      * Create a WorkDirectory object for a given directory and testsuite.
 669      * The directory is assumed to be valid (exists(), isDirectory(), canRead() etc)
 670      */
 671     private WorkDirectory(File root, TestSuite testSuite, Map<String, String> tsInfo) {
 672         if (root == null || testSuite == null)
 673             throw new NullPointerException();
 674         this.root = root;
 675         this.testSuite = testSuite;
 676         jtData = new File(root, JTDATA);
 677 
 678         if (jtData != null) {
 679             File loggerFile = getSystemFile(LoggerFactory.LOGFILE_NAME + "." + LoggerFactory.LOGFILE_EXTENSION);
 680             logFileName = loggerFile.getAbsolutePath();
 681             testSuite.setLogFilePath(this);
 682             try {
 683                 loggerFile.createNewFile();
 684             } catch (IOException ioe) {
 685                 testSuite.getNotificationLog(this).throwing("WorkDirectory", "WorkDirectory(File,TestSuite,Map)", ioe);
 686             }
 687 
 688         }
 689 
 690         // should consider saving parameter interview here;
 691         // -- possibly conditionally (don't need to write it in case of normal open)
 692 
 693         if (tsInfo != null) {
 694             String testC = (tsInfo.get(TESTSUITE_TESTCOUNT));
 695             int tc;
 696             if (testC == null)
 697                 tc = -1;
 698             else {
 699                 try {
 700                     tc = Integer.parseInt(testC);
 701                 } catch (NumberFormatException e) {
 702                     tc = -1;
 703                 }
 704             }
 705             testCount = tc;
 706         } else
 707             testCount = testSuite.getEstimatedTestCount();
 708 
 709         testSuiteID = testSuite.getID();
 710         if (testSuiteID == null)
 711             testSuiteID = "";
 712 
 713         doWDinfo(jtData, testSuite);
 714 
 715     }
 716 
 717 
 718     private void doWDinfo(File jtData, TestSuite testSuite) {
 719         try {
 720             oldWDpath = loadWdInfo(jtData).get(PREV_WD_PATH);
 721         } catch (IOException ex) {
 722             oldWDpath = null;
 723         }
 724         try {
 725             Properties p = new Properties();
 726             p.put(PREV_WD_PATH, root.getPath());
 727             saveInfo(p, WD_INFO, "WD information");
 728         } catch (IOException ex) {
 729             testSuite.getNotificationLog(this).throwing("WorkDirectory", "doWDinfo(File jtData, TestSuite testSuite)", ex);
 730         }
 731     }
 732 
 733     public String getPrevWDPath() {
 734         return oldWDpath;
 735     }
 736 
 737     /**
 738      * Get the root directory for this work directory.
 739      * @return the root directory for this work directory
 740      */
 741     public File getRoot() {
 742         return root;
 743     }
 744 
 745     /**
 746      * Get the root directory for this work directory.
 747      * @return the path of the root directory for this work directory
 748      */
 749     public String getPath() {
 750         return root.getPath();
 751     }
 752 
 753     /**
 754      * Get the data directory for this work directory.
 755      * @return the system (jtData) directory for this work directory
 756      */
 757     public File getJTData() {
 758         return jtData;
 759     }
 760 
 761     /**
 762      * Get a file in this work directory.
 763      * @param name the name of a file within this work directory
 764      * @return the full (absolute) name of the specified file
 765      */
 766     public File getFile(String name) {
 767         return new File(root, name);
 768     }
 769 
 770     /**
 771      * Get a file in the system directory for this work directory.
 772      * @param name the name of a file within the system (jtData) directory
 773      * @return the full (absolute) name of the specified file
 774      */
 775     public File getSystemFile(String name) {
 776         return new File(jtData, name);
 777     }
 778 
 779     /**
 780      * Get the test suite for this work directory.
 781      * @return the test suite for which this is a work directory
 782      */
 783     public TestSuite getTestSuite() {
 784         return testSuite;
 785     }
 786 
 787     /**
 788      * Find out the number of tests in the entire test suite.
 789      * This number is collected from either a previous iteration of the
 790      * testsuite or from the TestSuite object.
 791      * @return the number of tests in the test suite, -1 if not known.
 792      * @see #setTestSuiteTestCount
 793      * @see TestSuite#getEstimatedTestCount
 794      */
 795     public int getTestSuiteTestCount() {
 796         return testCount;
 797     }
 798 
 799     /**
 800      * Specify the total number of tests found in this testsuite.
 801      * When available, this class prefers to use this number rather
 802      * than that provided by a TestSuite object.
 803      * @param num the number of tests in the test suite
 804      * @see #getTestSuiteTestCount
 805      * @see TestSuite#getEstimatedTestCount
 806      */
 807     public void setTestSuiteTestCount(int num) {
 808         if (num != testCount) {
 809             testCount = num;
 810 
 811             try {
 812                 saveTestSuiteInfo();
 813             } catch (IOException e) {
 814                 // oh well, this isn't critical
 815             }
 816         }
 817     }
 818 
 819 
 820     /**
 821      * Get a test result table containing the test results in this work directory.
 822      * @return a test result table containing the test results in this work directory
 823      * @see #setTestResultTable
 824      */
 825     public TestResultTable getTestResultTable() {
 826         if (testResultTable != null) testResultTable.awakeCache();
 827         if (testResultTable == null ) {
 828             testResultTable = new TestResultTable(this);
 829         }
 830 
 831         return testResultTable;
 832     }
 833 
 834     /**
 835      * Set a test result table containing the test descriptions for the tests in this
 836      * test suite.
 837      * @param trt a test result table containing the test descriptions for the tests
 838      * in this work directory
 839      * @throws NullPointerException if trt is null.
 840      * @throws IllegalArgumentException if the test result table has been
 841      * initialized with a different work directory.
 842      * @see #getTestResultTable
 843      */
 844     public void setTestResultTable(TestResultTable trt) {
 845         if (trt == null)
 846             throw new NullPointerException();
 847 
 848         if (trt == testResultTable) {
 849             // already set to the correct value
 850             return;
 851         }
 852 
 853         if (testResultTable != null && testResultTable != trt) {
 854             // already set to something else
 855             throw new IllegalStateException();
 856         }
 857 
 858         WorkDirectory trt_wd = trt.getWorkDirectory();
 859         if (trt_wd != null && trt_wd != this)
 860             throw new IllegalArgumentException();
 861 
 862         if (trt_wd == null)
 863             trt.setWorkDirectory(this);
 864 
 865         testResultTable = trt;
 866     }
 867 
 868     public boolean isTRTSet() {
 869         return testResultTable != null;
 870     }
 871 
 872     /**
 873      * Print a text message to the workdir logfile.
 874      * A single line of text which is as short as possible is highly
 875      * recommended for readability purposes.
 876      *
 877      * @param i18n a resource bundle containing the localized messages
 878      * @param key a key into the resource bundle for the required message
 879      *
 880      * @since 3.0.1
 881      */
 882     public void log(I18NResourceBundle i18n, String key) {
 883         ensureLogFileInitialized();
 884         logFile.log(i18n, key);
 885     }
 886 
 887     /**
 888      * Print a text message to the workdir logfile.
 889      * A single line of text which is as short as possible is highly
 890      * recommended for readability purposes.
 891      *
 892      * @param i18n a resource bundle containing the localized messages
 893      * @param key a key into the resource bundle for the required message
 894      * @param arg An argument to be formatted into the specified message.
 895      *          If this is a <code>Throwable</code>, its stack trace
 896      *          will be included in the log.
 897      * @since 3.0.1
 898      */
 899     public void log(I18NResourceBundle i18n, String key, Object arg) {
 900         ensureLogFileInitialized();
 901         logFile.log(i18n, key, arg);
 902     }
 903 
 904     /**
 905      * Print a text message to the workdir logfile.
 906      * A single line of text which is as short as possible is highly
 907      * recommended for readability purposes.
 908      *
 909      * @param i18n a resource bundle containing the localized messages
 910      * @param key a key into the resource bundle for the required message
 911      * @param args An array of arguments to be formatted into the specified message.
 912      *          If the first arg is a <code>Throwable</code>, its stack
 913      *          trace will be included in the log.
 914      * @since 3.0.1
 915      */
 916     public void log(I18NResourceBundle i18n, String key, Object[] args) {
 917         ensureLogFileInitialized();
 918         logFile.log(i18n, key, args);
 919     }
 920 
 921     private void ensureLogFileInitialized() {
 922         if (logFile == null)
 923             logFile = new LogFile(getSystemFile("log.txt"));
 924     }
 925 
 926     /**
 927      * See <code>putTestAnnotation(String,String,String)</code>.
 928      *
 929      * @see #putTestAnnotation(String,String,String)
 930      */
 931     public synchronized void putTestAnnotation(TestResult tr, String key, String value) {
 932         putTestAnnotation(tr.getTestName(), key, value);
 933     }
 934 
 935     /**
 936      * Add an annotation for the given test.
 937      * @param testName Test for which the annotation should be added.  This is
 938      *     the value from <code>TestResult.getTestName()</code>.
 939      * @param key The name of the value to be entered.  The namespace for this
 940      *     value is unique for each <code>testName</code>.
 941      * @param value The value of the annotation.  Null removes the value from
 942      *     the map, an empty string should be used otherwise.
 943      */
 944     public synchronized void putTestAnnotation(String testName, String key, String value) {
 945         loadAnnotations();
 946 
 947         Map<String,String> map = null;
 948         if (annotationMap == null)
 949             annotationMap = new TreeMap<String, Map<String,String>>();
 950         else
 951             map = annotationMap.get(testName);
 952 
 953         if (map == null) {
 954             map = new HashMap<String,String>();
 955             annotationMap.put(testName, map);
 956         }
 957         // add/update in the first case, remove in the second case
 958         if (value != null)
 959             map.put(key, value);
 960         else
 961             map.remove(key);
 962 
 963         saveAnnotations();
 964     }
 965 
 966     /**
 967      * Get any annotations for the given test.
 968      *
 969      * @return Null if there are no annotations.  May also be null if the test does not exist.
 970      * @see #getTestAnnotations(TestResult)
 971      * @see #putTestAnnotation(String, String, String)
 972      * @see #putTestAnnotation(TestResult, String, String)
 973      * @throws NullPointerException if the parameter is null.
 974      */
 975     public synchronized Map<String,String> getTestAnnotations(String testName) {
 976         loadAnnotations();
 977         if (annotationMap == null)
 978             return null;
 979 
 980         return annotationMap.get(testName);
 981     }
 982 
 983     /**
 984      * Get any annotations for the given test in this work directory.
 985      * The annotations take the form of a map of strings for both the key and
 986      * value.
 987      * @param tr The test to get annotations for.
 988      * @throws NullPointerException if the parameter is null.
 989      * @return Null if there are no annotations.  May also be null if the test does not exist.
 990      */
 991     public synchronized Map<String,String> getTestAnnotations(TestResult tr) {
 992         if (tr == null)
 993             throw new NullPointerException();
 994 
 995         return getTestAnnotations(tr.getTestName());
 996     }
 997 
 998     /**
 999      * Clean the contents of the given path.  If <tt>path</tt> is a
1000      * directory, it is recursively deleted.  If it is a file, that file
1001      * is removed.
1002      * Any user confirmation should be done before calling this method.
1003      * If the path does not exist in this work directory, false is returned.
1004      * @param path Path to a directory in this work directory or a path to
1005      *        a jtr file.  A zero length string removes the root.
1006      * @return true is the purge occurred normally, false if the purge did not
1007      *         complete for some reason.  Most failures to purge will be
1008      *         announced by Faults.  A null parameter will result in
1009      *         false.
1010      * @throws WorkDirectory.PurgeFault If the file cannot be removed; the message field
1011      *         may not contain any useful information due to deficiencies in
1012      *         java.io.File.delete()..
1013      */
1014     public boolean purge(String path) throws PurgeFault {
1015         if (path == null)
1016             return false;
1017 
1018         boolean result = true;
1019 
1020         File f = (path.length() == 0 ? root : getFile(path));
1021 
1022         if (!f.exists())
1023             return false;
1024 
1025         if (f.isDirectory())
1026             result = recursivePurge(f, path);
1027         else {
1028             // single test
1029             result = f.delete();
1030             testResultTable.resetTest(path);
1031         }
1032 
1033         return result;
1034     }
1035 
1036     synchronized void clearAnnotations(TestResult tr) {
1037         loadAnnotations();
1038         if (annotationMap != null)
1039             annotationMap.remove(tr.getTestName());
1040         saveAnnotations();
1041     }
1042 
1043     // ------------ PRIVATE --------------
1044 
1045     /**
1046      * Load or save the annotations to secondary storage (disk).
1047      * Methods should call this before attempting to access annotations, in
1048      * particular because the annotations may be lazy loaded at startup.
1049      */
1050     private void loadAnnotations() {
1051         // could do file timestamp check
1052         if (annotationMap  == null)
1053             loadAnnotationsFromDisk();
1054     }
1055 
1056     private void loadAnnotationsFromDisk() {
1057         File aFile = getSystemFile(TEST_ANNOTATION_FILE);
1058         FileInputStream fis = null;
1059 
1060         if (aFile.exists() && aFile.canRead()) {
1061             try {
1062                 fis = new FileInputStream(aFile);
1063             } catch (FileNotFoundException e) {
1064                 // should never happen
1065                 e.printStackTrace();
1066             }
1067 
1068             DataInputStream reader = new DataInputStream(new BufferedInputStream(fis));
1069 
1070             annotationMap = new TreeMap<String, Map<String,String>>();
1071 
1072             try {
1073                 while(reader.available() > 0) {
1074                     try {
1075                         String s1 = reader.readUTF();
1076                         String s2 = reader.readUTF();
1077                         String s3 = reader.readUTF();
1078 
1079                         Map<String,String> map = annotationMap.get(s1);
1080                         if (map == null) {
1081                             map = new HashMap<String,String>();
1082                             annotationMap.put(s1, map);
1083                         }
1084                         map.put(s2,s3);
1085                     } catch (IOException e) {
1086                         e.printStackTrace();
1087                     }
1088                 }
1089                 reader.close();
1090             } catch(IOException ex) {
1091                 ex.printStackTrace();
1092                 try { if (reader != null) reader.close(); } catch (IOException e) {}
1093             }
1094 
1095             if (annotationMap.size() == 0)
1096                 annotationMap = null;
1097 
1098         }
1099     }
1100 
1101     private void saveAnnotations() {
1102         // must write even if the map is now empty
1103         // remove file if map empty
1104         File aFile = getSystemFile(TEST_ANNOTATION_FILE);
1105         if (annotationMap == null || annotationMap.size() == 0) {
1106             // should check aFile.canRead(), canWrite()
1107             // map may have been emptied since last write, so we can
1108             // delete the entire file now
1109             if (aFile.exists() && aFile.canWrite()) {
1110                 aFile.delete();
1111                 annotationMap = null;
1112             }
1113         } else {
1114             FileOutputStream fos = null;
1115 
1116             try {
1117                 fos = new FileOutputStream(aFile);
1118             } catch (FileNotFoundException e) {
1119                 // should not happen
1120                 e.printStackTrace();
1121             }
1122 
1123             DataOutputStream writer = new DataOutputStream(new BufferedOutputStream(fos));
1124             // writes a triplet
1125             for (String s: annotationMap.keySet()) {
1126                 Map<String,String> map = annotationMap.get(s);
1127                 try {
1128                     for (String key: map.keySet()) {
1129                         writer.writeUTF(s);    // 1
1130                         writer.writeUTF(key);      // 2
1131                         writer.writeUTF(map.get(key));  // 3
1132                     }
1133                 } catch (IOException e) {
1134                     e.printStackTrace();    // XXX
1135                 }
1136             }   // for
1137 
1138             try {
1139                 writer.close();
1140             } catch(IOException e) {
1141                 // should log
1142                 e.printStackTrace();
1143             }
1144         }
1145     }
1146 
1147     /**
1148      * @return False if any part of the removal process failed to complete.
1149      * @throws PurgeFault If the file cannot be removed; the message field
1150      *         may not contain any useful information due to deficiencies in
1151      *         java.io.File.delete()..
1152      */
1153     private boolean recursivePurge(File dir, String pathFromRoot) throws PurgeFault {
1154         boolean result = true;
1155         File[] files = dir.listFiles();
1156 
1157         if (files == null) {
1158             // make log entry
1159             // according to spec, this is not an empty directory, but a bad path
1160             // experienced with a symlink which went nowhere
1161             return false;
1162         }
1163 
1164         for (int i = 0; i < files.length; i++) {
1165             File f = files[i];
1166 
1167             String p; // root-relative path for f
1168             if (pathFromRoot.length() == 0)
1169                 p = f.getName();
1170             else
1171                 p = pathFromRoot + "/" + f.getName();
1172 
1173             if (f.isFile()) {
1174                 result &= f.delete();
1175                 if (f.getName().endsWith(TestResult.EXTN))
1176                     testResultTable.resetTest(p);
1177             } else if (!p.equals(JTDATA)) {
1178                 // directory, make sure not to delete jtData
1179                 result &= recursivePurge(f, p);
1180                 result &= f.delete();
1181             }
1182         }
1183 
1184         return result;
1185     }
1186 
1187     private static Map<String, String> loadTestSuiteInfo(File jtData) throws FileNotFoundException, IOException {
1188         return loadInfo(jtData, TESTSUITE);
1189     }
1190 
1191     private static Map<String, String> loadWdInfo(File jtData) throws FileNotFoundException, IOException {
1192         return loadInfo(jtData, WD_INFO);
1193     }
1194 
1195     private static Map<String, String> loadInfo(File jtData, String name) throws FileNotFoundException, IOException {
1196         try (InputStream in = new BufferedInputStream(new FileInputStream(new File(jtData, name)))) {
1197             return com.sun.javatest.util.Properties.load(in);
1198         }
1199     }
1200 
1201 
1202     private synchronized void saveTestSuiteInfo() throws IOException {
1203         Properties p = new Properties();
1204         p.put(TESTSUITE_ROOT, testSuite.getPath());
1205         String name = testSuite.getName();
1206         if (name != null)
1207             p.put(TESTSUITE_NAME, name);
1208 
1209         if (testCount > 0)
1210             p.put(TESTSUITE_TESTCOUNT, Integer.toString(testCount));
1211 
1212         if (testSuiteID != null && testSuiteID.length() > 0)
1213             p.put(TESTSUITE_ID, testSuiteID);
1214 
1215         saveInfo(p, TESTSUITE, "JT Harness Work Directory: Test Suite Info");
1216     }
1217 
1218 
1219 
1220     private synchronized void saveInfo(Properties p, String name, String descr) throws IOException {
1221         File f = File.createTempFile(name, ".new", jtData);
1222         OutputStream out = new BufferedOutputStream(new FileOutputStream(f));
1223 
1224         p.store(out, descr);
1225         out.close();
1226 
1227         // trying to get near-atomic updates to this file
1228         File to = new File(jtData, name);
1229         if (!f.renameTo(to)) {
1230             // it happend
1231             to.delete();
1232             f.renameTo(to);
1233         }
1234     }
1235 
1236     private static File canonicalize(File dir) throws BadDirectoryFault {
1237         try {
1238             return dir.getCanonicalFile();
1239         } catch (IOException e) {
1240             throw new BadDirectoryFault(i18n, "wd.cantCanonicalize", dir, e);
1241         }
1242     }
1243 
1244     private File root;
1245     private TestSuite testSuite;
1246     private String testSuiteID;
1247     private String oldWDpath;
1248     private int testCount = -1;
1249     private TestResultTable testResultTable;
1250     private Map<String, Map<String,String>> annotationMap;
1251     private File jtData;
1252     private String logFileName;
1253     private LogFile logFile;
1254     private static HashMap<File, WeakReference<WorkDirectory>> dirMap = new HashMap<>(2);     // must be manually synchronized
1255     public static final String JTDATA = "jtData";
1256     private static final String TESTSUITE = "testsuite";
1257     private static final String WD_INFO = "wdinfo";
1258     private static final String PREV_WD_PATH = "prev.wd.path";
1259     private static final String TESTSUITE_ID = "id";
1260     private static final String TESTSUITE_NAME = "name";
1261     private static final String TESTSUITE_ROOT = "root";
1262     private static final String TESTSUITE_TESTCOUNT = "testCount";
1263 
1264     private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(WorkDirectory.class);
1265 
1266     private static final String TEST_ANNOTATION_FILE = "test_annotations.dat";
1267 }