1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package com.oracle.javafx.scenebuilder.kit.library.user;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.library.BuiltinSectionComparator;
  35 import com.oracle.javafx.scenebuilder.kit.library.Library;
  36 import com.oracle.javafx.scenebuilder.kit.library.LibraryItem;
  37 import com.oracle.javafx.scenebuilder.kit.library.util.JarReport;
  38 import java.io.File;
  39 import java.io.FileInputStream;
  40 import java.io.FileNotFoundException;
  41 import java.io.IOException;
  42 import java.io.InputStreamReader;
  43 import java.io.LineNumberReader;
  44 import java.io.PrintWriter;
  45 import java.net.URLClassLoader;
  46 import java.nio.file.Files;
  47 import java.nio.file.Path;
  48 import java.nio.file.Paths;
  49 import java.nio.file.StandardCopyOption;
  50 import java.util.ArrayList;
  51 import java.util.Collection;
  52 import java.util.Comparator;
  53 import java.util.Date;
  54 import java.util.List;
  55 import java.util.TreeSet;
  56 import javafx.application.Platform;
  57 import javafx.beans.property.ReadOnlyIntegerProperty;
  58 import javafx.beans.property.ReadOnlyObjectProperty;
  59 import javafx.beans.property.SimpleIntegerProperty;
  60 import javafx.beans.property.SimpleObjectProperty;
  61 import javafx.collections.FXCollections;
  62 import javafx.collections.ObservableList;
  63 
  64 /**
  65  *
  66  *
  67  */
  68 public class UserLibrary extends Library {
  69 
  70     public enum State { READY, WATCHING };
  71     public static final String TAG_USER_DEFINED = "Custom"; //NOI18N
  72 
  73     private final String path;
  74     private final BuiltinSectionComparator sectionComparator
  75             = new BuiltinSectionComparator();
  76 
  77     private final ObservableList<JarReport> jarReports = FXCollections.observableArrayList();
  78     private final ObservableList<JarReport> previousJarReports = FXCollections.observableArrayList();
  79     private final ObservableList<Path> fxmlFileReports = FXCollections.observableArrayList();
  80     private final ObservableList<Path> previousFxmlFileReports = FXCollections.observableArrayList();
  81     private final SimpleIntegerProperty explorationCountProperty = new SimpleIntegerProperty();
  82     private final SimpleObjectProperty<Date> explorationDateProperty = new SimpleObjectProperty<>();
  83 
  84     private State state = State.READY;
  85     private Exception exception;
  86     private LibraryFolderWatcher watcher;
  87     private Thread watcherThread;
  88     // Where we store canonical class names of items we want to exclude from
  89     // the user defined one displayed in the Library panel.
  90     // As a consequence an empty file means we display all items.
  91     private final String filterFileName = "filter.txt"; //NOI18N
  92 
  93 
  94     /*
  95      * Public
  96      */
  97 
  98     public UserLibrary(String path) {
  99         this.path = path;
 100     }
 101 
 102     public String getPath() {
 103         return path;
 104     }
 105 
 106     public ObservableList<JarReport> getJarReports() {
 107         return jarReports;
 108     }
 109 
 110     public ObservableList<JarReport> getPreviousJarReports() {
 111         return previousJarReports;
 112     }
 113 
 114     public ObservableList<Path> getFxmlFileReports() {
 115         return fxmlFileReports;
 116     }
 117 
 118     public ObservableList<Path> getPreviousFxmlFileReports() {
 119         return previousFxmlFileReports;
 120     }
 121 
 122     public synchronized State getState() {
 123         return state;
 124     }
 125 
 126     public synchronized void startWatching() {
 127         assert state == State.READY;
 128 
 129         if (state == State.READY) {
 130             assert watcher == null;
 131             assert watcherThread == null;
 132 
 133             watcher = new LibraryFolderWatcher(this);
 134             watcherThread = new Thread(watcher);
 135             watcherThread.setName(watcher.getClass().getSimpleName() + "(" + path  + ")"); //NOI18N
 136             watcherThread.setDaemon(true);
 137             watcherThread.start();
 138             state = State.WATCHING;
 139         }
 140     }
 141 
 142     public synchronized void stopWatching() {
 143         assert state == State.WATCHING;
 144 
 145         if (state == State.WATCHING) {
 146             assert watcher != null;
 147             assert watcherThread != null;
 148             assert exception == null;
 149 
 150             watcherThread.interrupt();
 151 
 152             try {
 153                 watcherThread.join();
 154             } catch(InterruptedException x) {
 155                 x.printStackTrace();
 156             } finally {
 157                 watcher = null;
 158                 watcherThread = null;
 159                 state = State.READY;
 160 
 161                 // In READY state, we release the class loader.
 162                 // This enables library import to manipulate jar files.
 163                 changeClassLoader(null);
 164                 previousJarReports.clear();
 165             }
 166         }
 167     }
 168 
 169     public int getExplorationCount() {
 170         return explorationCountProperty.get();
 171     }
 172 
 173     public ReadOnlyIntegerProperty explorationCountProperty() {
 174         return explorationCountProperty;
 175     }
 176 
 177     public Object getExplorationDate() {
 178         return explorationDateProperty.get();
 179     }
 180 
 181     public ReadOnlyObjectProperty<Date> explorationDateProperty() {
 182         return explorationDateProperty;
 183     }
 184 
 185     public void setFilter(List<String> classnames) throws FileNotFoundException, IOException {
 186         if (classnames != null && classnames.size() > 0) {
 187             File filterFile = new File(getFilterFileName());
 188             // TreeSet to get natural order sorting and no duplicates
 189             TreeSet<String> allClassnames = new TreeSet<>();
 190 
 191             for (String classname : classnames) {
 192                 allClassnames.add(classname);
 193             }
 194 
 195             Path filterFilePath = Paths.get(getPath(), filterFileName);
 196             Path formerFilterFilePath = Paths.get(getPath(), filterFileName + ".tmp"); //NOI18N
 197             Files.deleteIfExists(formerFilterFilePath);
 198 
 199             try {
 200                 // Rename already existing filter file so that we can rollback
 201                 if (Files.exists(filterFilePath)) {
 202                     Files.move(filterFilePath, formerFilterFilePath, StandardCopyOption.ATOMIC_MOVE);
 203                 }
 204 
 205                 // Create the new filter file
 206                 Files.createFile(filterFilePath);
 207 
 208                 // Write content of the new filter file
 209                 try (PrintWriter writer = new PrintWriter(filterFile, "UTF-8")) { //NOI18N
 210                     for (String classname : allClassnames) {
 211                         writer.write(classname + "\n"); //NOI18N
 212                     }
 213                 }
 214 
 215                 // Delete the former filter file
 216                 if (Files.exists(formerFilterFilePath)) {
 217                     Files.delete(formerFilterFilePath);
 218                 }
 219             } catch (IOException ioe) {
 220                 // Rollback
 221                 if (Files.exists(formerFilterFilePath)) {
 222                     Files.move(formerFilterFilePath, filterFilePath, StandardCopyOption.ATOMIC_MOVE);
 223                 }
 224                 throw (ioe);
 225             }
 226         }
 227     }
 228 
 229     public List<String> getFilter() throws FileNotFoundException, IOException {
 230         List<String> res = new ArrayList<>();
 231         File filterFile = new File(getFilterFileName());
 232 
 233         if (filterFile.exists()) {
 234             try (LineNumberReader reader = new LineNumberReader(new InputStreamReader(new FileInputStream(filterFile), "UTF-8"))) { //NOI18N
 235                 String line;
 236                 while ((line = reader.readLine()) != null) {
 237                     res.add(line);
 238                 }
 239             }
 240         }
 241 
 242         return res;
 243     }
 244 
 245     /*
 246      * Package
 247      */
 248 
 249     String getFilterFileName() {
 250         return getPath() + File.separator + filterFileName;
 251     }
 252 
 253     void updateJarReports(Collection<JarReport> newJarReports) {
 254         if (Platform.isFxApplicationThread()) {
 255             previousJarReports.setAll(jarReports);
 256             jarReports.setAll(newJarReports);
 257         } else {
 258             Platform.runLater(() -> {
 259                 previousJarReports.setAll(jarReports);
 260                 jarReports.setAll(newJarReports);
 261             });
 262         }
 263     }
 264 
 265     void updateFxmlFileReports(Collection<Path> newFxmlFileReports) {
 266         if (Platform.isFxApplicationThread()) {
 267             previousFxmlFileReports.setAll(fxmlFileReports);
 268             fxmlFileReports.setAll(newFxmlFileReports);
 269         } else {
 270             Platform.runLater(() -> {
 271                 previousFxmlFileReports.setAll(fxmlFileReports);
 272                 fxmlFileReports.setAll(newFxmlFileReports);
 273             });
 274         }
 275     }
 276 
 277     void setItems(Collection<LibraryItem> items) {
 278         if (Platform.isFxApplicationThread()) {
 279             itemsProperty.setAll(items);
 280         } else {
 281             Platform.runLater(() -> itemsProperty.setAll(items));
 282         }
 283     }
 284 
 285     void addItems(Collection<LibraryItem> items) {
 286         if (Platform.isFxApplicationThread()) {
 287             itemsProperty.addAll(items);
 288         } else {
 289             Platform.runLater(() -> itemsProperty.addAll(items));
 290         }
 291     }
 292 
 293     void updateClassLoader(ClassLoader newClassLoader) {
 294         if (Platform.isFxApplicationThread()) {
 295             changeClassLoader(newClassLoader);
 296         } else {
 297             Platform.runLater(() -> changeClassLoader(newClassLoader));
 298         }
 299     }
 300 
 301     void updateExplorationCount(int count) {
 302         if (Platform.isFxApplicationThread()) {
 303             explorationCountProperty.set(count);
 304         } else {
 305             Platform.runLater(() -> explorationCountProperty.set(count));
 306         }
 307     }
 308 
 309     void updateExplorationDate(Date date) {
 310         if (Platform.isFxApplicationThread()) {
 311             explorationDateProperty.set(date);
 312         } else {
 313             Platform.runLater(() -> explorationDateProperty.set(date));
 314         }
 315     }
 316 
 317     /*
 318      * Library
 319      */
 320     @Override
 321     public Comparator<String> getSectionComparator() {
 322         return sectionComparator;
 323     }
 324 
 325     /*
 326      * Private
 327      */
 328 
 329     private void changeClassLoader(ClassLoader newClassLoader) {
 330         assert Platform.isFxApplicationThread();
 331 
 332         /*
 333          * Before changing to the new class loader,
 334          * we invoke URLClassLoader.close() on the existing one
 335          * so that it releases its associated jar files.
 336          */
 337         final ClassLoader classLoader = classLoaderProperty.get();
 338         if (classLoader instanceof URLClassLoader) {
 339             final URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
 340             try {
 341                 urlClassLoader.close();
 342             } catch(IOException x) {
 343                 x.printStackTrace();
 344             }
 345         }
 346 
 347         // Now moves to the new class loader
 348         classLoaderProperty.set(newClassLoader);
 349     }
 350 
 351     /*
 352      * Debug
 353      */
 354 
 355     public static void main(String[] args) throws Exception {
 356         final String path = "/Users/elp/Desktop/MyLib"; //NOI18N
 357         final UserLibrary lib = new UserLibrary(path);
 358         lib.startWatching();
 359         System.out.println("Starting to watch for 20 s"); //NOI18N
 360         Thread.sleep(20 * 1000);
 361         System.out.println("Stopping to watch for 20 s"); //NOI18N
 362         lib.stopWatching();
 363         Thread.sleep(20 * 1000);
 364         System.out.println("Exiting"); //NOI18N
 365     }
 366 }