/**
* SampleScanner.java
*
* Copyright (c) 2013-2016, F(X)yz
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of F(X)yz, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL F(X)yz BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.fxyz3d.util;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import org.fxyz3d.FXyzSample;
import org.fxyz3d.FXyzSamplerProject;
import org.fxyz3d.model.EmptySample;
import org.fxyz3d.model.Project;
/**
* All the code related to classpath scanning, etc for samples.
*/
public class SampleScanner {
private static final List<String> ILLEGAL_CLASS_NAMES = new ArrayList<>();
static {
ILLEGAL_CLASS_NAMES.add("/com/javafx/main/Main.class");
ILLEGAL_CLASS_NAMES.add("/com/javafx/main/NoJavaFXFallback.class");
}
private static final Map<String, FXyzSamplerProject> packageToProjectMap = new HashMap<>();
static {
System.out.println("Initialising FXyz-Sampler sample scanner...");
System.out.println("\tDiscovering projects...");
// find all projects on the classpath that expose a FXyzSamplerProject
// service. These guys are our friends....
ServiceLoader<FXyzSamplerProject> loader = ServiceLoader.load(FXyzSamplerProject.class);
for (FXyzSamplerProject project : loader) {
final String projectName = project.getProjectName();
final String basePackage = project.getSampleBasePackage();
packageToProjectMap.put(basePackage, project);
System.out.println("\t\tFound project '" + projectName +
"', with sample base package '" + basePackage + "'");
}
if (packageToProjectMap.isEmpty()) {
System.out.println("\tError: Did not find any projects!");
}
}
private final Map<String, Project> projectsMap = new HashMap<>();
/**
* Gets the list of sample classes to load
*
* @return The classes
*/
public Map<String, Project> discoverSamples() {
Class<?>[] results = new Class[] { };
try {
results = loadFromPathScanning();
} catch (ClassNotFoundException | IOException e) {
e.printStackTrace();
}
for (Class<?> sampleClass : results) {
if (! FXyzSample.class.isAssignableFrom(sampleClass)) continue;
if (sampleClass.isInterface()) continue;
if (Modifier.isAbstract(sampleClass.getModifiers())) continue;
// if (FXyzSample.class.isAssignableFrom(EmptySample.class)) continue;
if (sampleClass == EmptySample.class) continue;
FXyzSample sample = null;
try {
sample = (FXyzSample)sampleClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
if (sample == null || ! sample.isVisible()) continue;
final String packageName = sampleClass.getPackage().getName();
for (String key : packageToProjectMap.keySet()) {
if (packageName.contains(key)) {
final String prettyProjectName = packageToProjectMap.get(key).getProjectName();
Project project;
if (! projectsMap.containsKey(prettyProjectName)) {
project = new Project(prettyProjectName, key);
project.setWelcomePage(packageToProjectMap.get(key).getWelcomePage());
projectsMap.put(prettyProjectName, project);
} else {
project = projectsMap.get(prettyProjectName);
}
project.addSample(packageName, sample);
}
}
}
return projectsMap;
}
/**
* Scans all classes.
*
* @return The classes
* @throws ClassNotFoundException
* @throws IOException
*/
private Class<?>[] loadFromPathScanning() throws ClassNotFoundException, IOException {
final List<File> dirs = new ArrayList<>();
final List<File> jars = new ArrayList<>();
// scan the classpath
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String path = "";
Enumeration<URL> resources = classLoader.getResources(path);
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
if (url.toExternalForm().contains("/jre/")) continue;
// Only "file" and "jar" URLs are recognized, other schemes will be ignored.
String protocol = url.getProtocol().toLowerCase();
if (null != protocol) switch (protocol) {
case "file":
dirs.add(new File(url.getFile()));
break;
case "jar":
String fileName = new URL(url.getFile()).getFile();
// JAR URL specs must contain the string "!/" which separates the name
// of the JAR file from the path of the resource contained in it, even
// if the path is empty.
int sep = fileName.indexOf("!/");
if (sep > 0) {
jars.add(new File(fileName.substring(0, sep)));
} break;
}
}
// and also scan the current working directory
final Path workingDirectory = new File("").toPath();
scanPath(workingDirectory, dirs, jars);
// process directories first, then jars, so that classes take precedence
// over built jars (it makes rapid development easier in the IDE)
final Set<Class<?>> classes = new LinkedHashSet<>();
for (File directory : dirs) {
classes.addAll(findClassesInDirectory(directory));
}
for (File jar : jars) {
String fullPath = jar.getAbsolutePath();
if (fullPath.endsWith("jfxrt.jar")) continue;
classes.addAll(findClassesInJar(new File(fullPath)));
}
return classes.toArray(new Class[classes.size()]);
}
private void scanPath(Path workingDirectory, final List<File> dirs, final List<File> jars) throws IOException {
Files.walkFileTree(workingDirectory, new SimpleFileVisitor<Path>() {
@Override public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
final File file = path.toFile();
final String fullPath = file.getAbsolutePath();
final String name = file.toString();
if (fullPath.endsWith("jfxrt.jar") || name.contains("jre")) {
return FileVisitResult.CONTINUE;
}
if (file.isDirectory()) {
dirs.add(file);
} else if (name.toLowerCase().endsWith(".jar")) {
jars.add(file);
}
return FileVisitResult.CONTINUE;
}
});
}
private List<Class<?>> findClassesInDirectory(File directory) throws IOException {
List<Class<?>> classes = new ArrayList<>();
if (!directory.exists()) {
System.out.println("Directory does not exist: " + directory.getAbsolutePath());
return classes;
}
processPath(directory.toPath(), classes);
return classes;
}
private List<Class<?>> findClassesInJar(File jarFile) throws IOException, ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();
if (!jarFile.exists()) {
System.out.println("Jar file does not exist here: " + jarFile.getAbsolutePath());
return classes;
}
FileSystem jarFileSystem = FileSystems.newFileSystem(jarFile.toPath(), null);
processPath(jarFileSystem.getPath("/"), classes);
return classes;
}
private void processPath(Path path, final List<Class<?>> classes) throws IOException {
final String root = path.toString();
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String name = file.toString();
if (name.endsWith(".class") && ! ILLEGAL_CLASS_NAMES.contains(name)) {
// remove root path to make class name correct in all cases
name = name.substring(root.length());
Class<?> clazz = processClassName(name);
if (clazz != null) {
classes.add(clazz);
}
}
return FileVisitResult.CONTINUE;
}
});
}
private Class<?> processClassName(final String name) {
String className = name.replace("\\", ".");
className = className.replace("/", ".");
// some cleanup code
if (className.contains("$")) {
// we don't care about samples as inner classes, so
// we jump out
return null;
}
if (className.contains(".bin")) {
className = className.substring(className.indexOf(".bin") + 4);
className = className.replace(".bin", "");
}
if (className.startsWith(".")) {
className = className.substring(1);
}
if (className.endsWith(".class")) {
className = className.substring(0, className.length() - 6);
}
Class<?> clazz = null;
try {
clazz = Class.forName(className);
} catch (Throwable e) {
// Throwable, could be all sorts of bad reasons the class won't instantiate
System.out.println("ERROR: Class name: " + className);
System.out.println("ERROR: Initial filename: " + name);
// e.printStackTrace();
}
return clazz;
}
}