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.editor.i18n.I18N;
  35 import com.oracle.javafx.scenebuilder.kit.editor.images.ImageUtils;
  36 import com.oracle.javafx.scenebuilder.kit.library.BuiltinLibrary;
  37 import com.oracle.javafx.scenebuilder.kit.library.LibraryItem;
  38 import com.oracle.javafx.scenebuilder.kit.library.util.JarExplorer;
  39 import com.oracle.javafx.scenebuilder.kit.library.util.JarReport;
  40 import com.oracle.javafx.scenebuilder.kit.library.util.JarReportEntry;
  41 import java.io.FileInputStream;
  42 import java.io.IOException;
  43 import java.io.InputStreamReader;
  44 import java.io.LineNumberReader;
  45 import java.net.MalformedURLException;
  46 import java.net.URL;
  47 import java.net.URLClassLoader;
  48 import java.nio.file.DirectoryStream;
  49 import java.nio.file.Files;
  50 import java.nio.file.Path;
  51 import java.nio.file.Paths;
  52 import java.nio.file.StandardWatchEventKinds;
  53 import java.nio.file.WatchEvent;
  54 import java.nio.file.WatchKey;
  55 import java.nio.file.WatchService;
  56 import java.util.ArrayList;
  57 import java.util.Collection;
  58 import java.util.Date;
  59 import java.util.HashSet;
  60 import java.util.List;
  61 import java.util.Locale;
  62 import java.util.Set;
  63 import java.util.logging.Logger;
  64 
  65 /**
  66  *
  67  *
  68  */
  69 class LibraryFolderWatcher implements Runnable {
  70 
  71     private final UserLibrary library;
  72     private enum FILE_TYPE {FXML, JAR};
  73 
  74     public LibraryFolderWatcher(UserLibrary library) {
  75         this.library = library;
  76     }
  77 
  78     /*
  79      * Runnable
  80      */
  81 
  82     @Override
  83     public void run() {
  84 
  85         try {
  86             library.updateExplorationCount(0);
  87             library.updateExplorationDate(new Date());
  88             runDiscovery();
  89             runWatching();
  90         } catch(InterruptedException x) {
  91             // Let's stop
  92         }
  93     }
  94 
  95 
  96     /*
  97      * Private
  98      */
  99 
 100 
 101     private void runDiscovery() throws InterruptedException {
 102         final Path folder = Paths.get(library.getPath());
 103 
 104         // First put the builtin items in the library
 105         library.setItems(BuiltinLibrary.getLibrary().getItems());
 106 
 107         // Now attempts to discover the user library folder
 108         boolean retry;
 109         do {
 110             try (DirectoryStream<Path> stream = Files.newDirectoryStream(folder)) {
 111                 final Set<Path> currentJars = new HashSet<>();
 112                 final Set<Path> currentFxmls = new HashSet<>();
 113                 for (Path entry: stream) {
 114                     if (isJarPath(entry)) {
 115                         currentJars.add(entry);
 116                     } else if (isFxmlPath(entry)) {
 117                         currentFxmls.add(entry);
 118                     }
 119                 }
 120 
 121                 updateLibrary(currentFxmls);
 122                 exploreAndUpdateLibrary(currentJars);
 123                 retry = false;
 124             } catch(IOException x) {
 125                 Thread.sleep(2000 /* ms */);
 126                 retry = true;
 127             } finally {
 128                 library.updateExplorationCount(library.getExplorationCount()+1);
 129             }
 130         }
 131         while (retry);
 132 
 133     }
 134 
 135     private void runWatching() throws InterruptedException {
 136         while (true) {
 137             final Path folder = Paths.get(library.getPath());
 138 
 139             WatchService watchService = null;
 140             while (watchService == null) {
 141                 try {
 142                     watchService = folder.getFileSystem().newWatchService();
 143                 } catch(IOException x) {
 144                     System.out.println("FileSystem.newWatchService() failed"); //NOI18N
 145                     System.out.println("Sleeping..."); //NOI18N
 146                     Thread.sleep(1000 /* ms */);
 147                 }
 148             }
 149 
 150             WatchKey watchKey = null;
 151             while ((watchKey == null) || (watchKey.isValid() == false)) {
 152                 try {
 153                     watchKey = folder.register(watchService,
 154                     StandardWatchEventKinds.ENTRY_CREATE,
 155                     StandardWatchEventKinds.ENTRY_DELETE,
 156                     StandardWatchEventKinds.ENTRY_MODIFY);
 157 
 158                     WatchKey wk;
 159                     do {
 160                         wk = watchService.take();
 161                         assert wk == watchKey;
 162 
 163                         boolean isDirty = false;
 164                         for (WatchEvent<?> e: wk.pollEvents()) {
 165                             final WatchEvent.Kind<?> kind = e.kind();
 166                             final Object context = e.context();
 167 
 168                             if (kind == StandardWatchEventKinds.ENTRY_CREATE
 169                                     || kind == StandardWatchEventKinds.ENTRY_DELETE
 170                                     || kind == StandardWatchEventKinds.ENTRY_MODIFY) {
 171                                 assert context instanceof Path;
 172                                 if (isJarPath((Path)context) || isFxmlPath((Path)context)) {
 173                                     isDirty = true;
 174                                 }
 175                             } else {
 176                                 assert kind == StandardWatchEventKinds.OVERFLOW;
 177                             }
 178                         }
 179 
 180                         // We reconstruct a full set from scratch as soon as the
 181                         // dirty flag is set.
 182                         if (isDirty) {
 183                             // First put the builtin items in the library
 184                             library.setItems(BuiltinLibrary.getLibrary().getItems());
 185 
 186                             final Set<Path> fxmls = new HashSet<>();
 187                             fxmls.addAll(getAllFiles(FILE_TYPE.FXML));
 188                             updateLibrary(fxmls);
 189 
 190                             final Set<Path> jars = new HashSet<>();
 191                             jars.addAll(getAllFiles(FILE_TYPE.JAR));
 192                             exploreAndUpdateLibrary(jars);
 193 
 194                             library.updateExplorationCount(library.getExplorationCount()+1);
 195                         }
 196                     } while (wk.reset());
 197                 } catch(IOException x) {
 198                     Thread.sleep(1000 /* ms */);
 199                 }
 200             }
 201 
 202         }
 203     }
 204 
 205 
 206     private Set<Path> getAllFiles(FILE_TYPE fileType) throws IOException {
 207         Set<Path> res = new HashSet<>();
 208         final Path folder = Paths.get(library.getPath());
 209 
 210         try (DirectoryStream<Path> ds = Files.newDirectoryStream(folder)) {
 211             for (Path p : ds) {
 212                 switch (fileType) {
 213                     case FXML:
 214                         if (isFxmlPath(p)) {
 215                             res.add(p);
 216                         }
 217                         break;
 218                     case JAR:
 219                         if (isJarPath(p)) {
 220                             res.add(p);
 221                         }
 222                         break;
 223                     default:
 224                         break;
 225                 }
 226             }
 227         }
 228 
 229         return res;
 230     }
 231 
 232 
 233     private static boolean isJarPath(Path path) {
 234         final String pathString = path.toString().toLowerCase(Locale.ROOT);
 235         return pathString.endsWith(".jar"); //NOI18N
 236     }
 237 
 238 
 239     private static boolean isFxmlPath(Path path) {
 240         final String pathString = path.toString().toLowerCase(Locale.ROOT);
 241         return pathString.endsWith(".fxml"); //NOI18N
 242     }
 243 
 244 
 245     private void updateLibrary(Collection<Path> paths) throws IOException {
 246         final List<LibraryItem> newItems = new ArrayList<>();
 247 
 248         for (Path path : paths) {
 249             newItems.add(makeLibraryItem(path));
 250         }
 251 
 252         library.addItems(newItems);
 253         library.updateFxmlFileReports(paths);
 254         library.updateExplorationDate(new Date());
 255     }
 256 
 257 
 258     private LibraryItem makeLibraryItem(Path path) throws IOException {
 259         final URL iconURL = ImageUtils.getNodeIconURL(null);
 260         String fileName = path.getFileName().toString();
 261         String itemName = fileName.substring(0, fileName.indexOf(".fxml")); //NOI18N
 262         String fxmlText = ""; //NOI18N
 263         StringBuilder buf = new StringBuilder();
 264 
 265         try (LineNumberReader reader = new LineNumberReader(new InputStreamReader(new FileInputStream(path.toFile()), "UTF-8"))) { //NOI18N
 266             String line;
 267             while ((line = reader.readLine()) != null) {
 268                 buf.append(line).append("\n"); //NOI18N
 269             }
 270 
 271             fxmlText = buf.toString();
 272         }
 273 
 274         final LibraryItem res = new LibraryItem(itemName, UserLibrary.TAG_USER_DEFINED, fxmlText, iconURL, library);
 275         return res;
 276     }
 277 
 278 
 279     private void exploreAndUpdateLibrary(Collection<Path> jars) throws IOException {
 280 
 281         //  1) we create a classloader
 282         //  2) we explore all the jars
 283         //  3) we construct a list of library items
 284         //  4) we update the user library with the class loader and items
 285 
 286         // 1)
 287         final ClassLoader classLoader;
 288         if (jars.isEmpty()) {
 289             classLoader = null;
 290         } else {
 291             classLoader = new URLClassLoader(makeURLArrayFromPaths(jars));
 292         }
 293 
 294         // 2)
 295         final List<JarReport> jarReports = new ArrayList<>();
 296         for (Path currentJar : jars) {
 297             Logger.getLogger(this.getClass().getSimpleName()).info(I18N.getString("log.info.explore.jar", currentJar));
 298             final JarExplorer explorer = new JarExplorer(currentJar);
 299             jarReports.add(explorer.explore(classLoader));
 300         }
 301 
 302         // 3)
 303         final List<LibraryItem> newItems = new ArrayList<>();
 304         for (JarReport jarReport : jarReports) {
 305             newItems.addAll(makeLibraryItems(jarReport));
 306         }
 307 
 308         // 4)
 309         library.updateClassLoader(classLoader);
 310         library.addItems(newItems);
 311         library.updateJarReports(new ArrayList<>(jarReports));
 312         library.updateExplorationDate(new Date());
 313     }
 314 
 315 
 316     private Collection<LibraryItem> makeLibraryItems(JarReport jarReport) throws IOException {
 317         final List<LibraryItem> result = new ArrayList<>();
 318         final URL iconURL = ImageUtils.getNodeIconURL(null);
 319         final List<String> excludedItems = library.getFilter();
 320 
 321         for (JarReportEntry e : jarReport.getEntries()) {
 322             if ((e.getStatus() == JarReportEntry.Status.OK) && e.isNode()) {
 323                 // We filter out items listed in the excluded list, based on canonical name of the class.
 324                 final String canonicalName = e.getKlass().getCanonicalName();
 325                 if (! excludedItems.contains(canonicalName)) {
 326                     final String name = e.getKlass().getSimpleName();
 327                     final String fxmlText = JarExplorer.makeFxmlText(e.getKlass());
 328                     result.add(new LibraryItem(name, UserLibrary.TAG_USER_DEFINED, fxmlText, iconURL, library));
 329                 }
 330             }
 331         }
 332 
 333         return result;
 334     }
 335 
 336 
 337     private URL[] makeURLArrayFromPaths(Collection<Path> paths) {
 338         final URL[] result = new URL[paths.size()];
 339         int i = 0;
 340         for (Path p : paths) {
 341             try {
 342                 result[i++] = p.toUri().toURL();
 343             } catch(MalformedURLException x) {
 344                 throw new RuntimeException("Bug in " + getClass().getSimpleName(), x); //NOI18N
 345             }
 346         }
 347 
 348         return result;
 349     }
 350 
 351 }