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.util;
  33 
  34 import java.io.ByteArrayInputStream;
  35 import java.io.IOException;
  36 import java.nio.charset.Charset;
  37 import java.nio.file.Path;
  38 import java.util.Enumeration;
  39 import java.util.jar.JarEntry;
  40 import java.util.jar.JarFile;
  41 import javafx.fxml.FXMLLoader;
  42 
  43 /**
  44  *
  45  *
  46  */
  47 public class JarExplorer {
  48 
  49     private final Path jar;
  50 
  51     public JarExplorer(Path jar) {
  52         assert jar != null;
  53         assert jar.isAbsolute();
  54 
  55         this.jar = jar;
  56     }
  57 
  58     public JarReport explore(ClassLoader classLoader) throws IOException {
  59         final JarReport result = new JarReport(jar);
  60 
  61         try (JarFile jarFile = new JarFile(jar.toFile())) {
  62             final Enumeration<JarEntry> e = jarFile.entries();
  63             while (e.hasMoreElements()) {
  64                 final JarEntry entry = e.nextElement();
  65                 result.getEntries().add(exploreEntry(entry, classLoader));
  66             }
  67         }
  68 
  69         return result;
  70     }
  71 
  72     public static String makeFxmlText(Class<?> klass) {
  73         final StringBuilder result = new StringBuilder();
  74 
  75         /*
  76          * <?xml version="1.0" encoding="UTF-8"?> //NOI18N
  77          *
  78          * <?import a.b.C?>
  79          *
  80          * <C/>
  81          */
  82 
  83         result.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); //NOI18N
  84 
  85         result.append("<?import "); //NOI18N
  86         result.append(klass.getCanonicalName());
  87         result.append("?>"); //NOI18N
  88         result.append("<"); //NOI18N
  89         result.append(klass.getSimpleName());
  90         result.append("/>\n"); //NOI18N
  91 
  92         return result.toString();
  93     }
  94 
  95 
  96     public static Object instantiateWithFXMLLoader(Class<?> klass, ClassLoader classLoader) throws IOException {
  97         Object result;
  98 
  99         final String fxmlText = makeFxmlText(klass);
 100         final byte[] fxmlBytes = fxmlText.getBytes(Charset.forName("UTF-8")); //NOI18N
 101 
 102         final FXMLLoader fxmlLoader = new FXMLLoader();
 103         try {
 104             fxmlLoader.setClassLoader(classLoader);
 105             result = fxmlLoader.load(new ByteArrayInputStream(fxmlBytes));
 106         } catch(IOException x) {
 107             throw x;
 108         } catch(RuntimeException|Error x) {
 109             throw new IOException(x);
 110         }
 111 
 112         return result;
 113     }
 114 
 115     /*
 116      * Private
 117      */
 118 
 119     private JarReportEntry exploreEntry(JarEntry entry, ClassLoader classLoader) {
 120         JarReportEntry.Status status;
 121         Throwable entryException;
 122         Class<?> entryClass;
 123 
 124         if (entry.isDirectory()) {
 125             status = JarReportEntry.Status.IGNORED;
 126             entryClass = null;
 127             entryException = null;
 128         } else {
 129             final String className = makeClassName(entry.getName());
 130             // Filtering out what starts with com.javafx. is bound to DTL-6378.
 131             if (className == null || className.startsWith("java.") //NOI18N
 132                     || className.startsWith("javax.") || className.startsWith("javafx.") //NOI18N
 133                     || className.startsWith("com.oracle.javafx.scenebuilder.") //NOI18N
 134                     || className.startsWith("com.javafx.")) { //NOI18N
 135                 status = JarReportEntry.Status.IGNORED;
 136                 entryClass = null;
 137                 entryException = null;
 138             } else {
 139                 try {
 140                     // Some reading explaining why using Class.forName is not appropriate:
 141                     // http://blog.osgi.org/2011/05/what-you-should-know-about-class.html
 142                     // http://blog.bjhargrave.com/2007/09/classforname-caches-defined-class-in.html
 143                     // http://stackoverflow.com/questions/8100376/class-forname-vs-classloader-loadclass-which-to-use-for-dynamic-loading
 144                     entryClass = classLoader.loadClass(className); // Note: static intializers of entryClass are not run, this doesn't seem to be an issue
 145                     try {
 146                         instantiateWithFXMLLoader(entryClass, classLoader);
 147                         status = JarReportEntry.Status.OK;
 148                         entryException = null;
 149                     } catch(RuntimeException|IOException x) {
 150                         status = JarReportEntry.Status.CANNOT_INSTANTIATE;
 151                         entryException = x;
 152                     }
 153                 } catch(Error | ClassNotFoundException x) {
 154                     status = JarReportEntry.Status.CANNOT_LOAD;
 155                     entryClass = null;
 156                     entryException = x;
 157                 }
 158             }
 159         }
 160 
 161         return new JarReportEntry(entry.getName(), status, entryException, entryClass);
 162     }
 163 
 164 
 165     private String makeClassName(String entryName) {
 166         final String result;
 167 
 168         if (entryName.endsWith(".class") == false) { //NOI18N
 169             result = null;
 170         } else if (entryName.contains("$")) { //NOI18N
 171             // We skip inner classes for now
 172             result = null;
 173         } else {
 174             final int endIndex = entryName.length()-6; // ".class" -> 6 //NOI18N
 175             result = entryName.substring(0, endIndex).replace("/", "."); //NOI18N
 176         }
 177 
 178         return result;
 179     }
 180 }