org.scijava.annotations.EclipseHelper Maven / Gradle / Ivy
Show all versions of scijava-common Show documentation
/*
* #%L
* SciJava Common shared library for SciJava software.
* %%
* Copyright (C) 2009 - 2017 Board of Regents of the University of
* Wisconsin-Madison, Broad Institute of MIT and Harvard, Max Planck
* Institute of Molecular Cell Biology and Genetics, University of
* Konstanz, and KNIME GmbH.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. 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.
*
* 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 THE COPYRIGHT HOLDERS OR CONTRIBUTORS 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.
* #L%
*/
package org.scijava.annotations;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.scijava.util.FileUtils;
/**
* Helps Eclipse's lack of support for annotation processing in incremental
* build mode.
*
* Eclipse has a very, let's say, "creative" way to interpret the Java
* specifications when it comes to annotation processing: while Java mandates
* that annotation processors need to be run after compiling Java classes,
* Eclipse cops out of that because it poses a challenge to its incremental
* compilation (and especially to Eclipse's attempt at compiling .class files
* even from .java sources that contain syntax errors).
*
*
* So we need to do something about this. Our strategy is to detect when the
* annotation index was not updated properly and just do it ourselves, whenever
* {@link Index#load(Class)} is called.
*
*
* Since our aim here is to compensate for Eclipse's shortcoming, we need only
* care about the scenario where the developer launches either a Java main class
* or a unit test from within Eclipse, and even then only when the annotation
* index is to be accessed.
*
*
* The way Eclipse launches Java main classes or unit tests, it makes a single
* {@link URLClassLoader} with all the necessary class path elements. Crucially,
* the class path elements corresponding to Eclipse projects will never point to
* {@code .jar} files but to directories. This allows us to assume that the
* annotation classes as well as the annotated classes can be loaded using that
* exact class loader, too.
*
*
* It is quite possible that a developer may launch a main class in a different
* project than the one which needs annotation indexing, therefore we need to
* inspect all class path elements.
*
*
* To provide at least a semblance of a performant component, before going all
* out and indexing the annotations, we verify that the {@code META-INF/json/}
* directory has an outdated timestamp relative to the {@code .class} files. If
* that is not the case, we may safely assume that the annotation indexes are
* up-to-date.
*
*
* To avoid indexing class path elements over and over again which simply do not
* contain indexable annotations, we make the {@code META-INF/json/} directory
* nevertheless, updating the timestamp to reflect that we indexed the
* annotations.
*
*
* @author Johannes Schindelin
*/
public class EclipseHelper extends DirectoryIndexer {
private static final String FORCE_ANNOTATION_INDEX_PROPERTY = "force.annotation.index";
static Set indexed = new HashSet<>();
private boolean bannerShown;
private static boolean debug =
"debug".equals(System.getProperty("scijava.log.level"));
private boolean autoDetectEclipse = true;
private static void debug(final String message) {
if (debug) {
System.err.println(message);
}
}
/**
* Updates the annotation index in the current Eclipse project.
*
* The assumption is that Eclipse -- after failing to run the annotation
* processors correctly -- will launch any tests or main classes with a class
* path that contains the project's output directory with the {@code .class}
* files (as opposed to a {@code .jar} file). We only need to update that
* first class path element (or for tests, the first two), and only if it is a
* local directory.
*
*
* @param loader the class loader whose class path to inspect
*/
public static void updateAnnotationIndex(final ClassLoader loader) {
debug("Checking class loader: " + loader);
if (loader == null ||
!(loader instanceof URLClassLoader))
{
debug("Not an URLClassLoader: " + loader);
return;
}
EclipseHelper helper = new EclipseHelper();
if (Boolean.getBoolean(FORCE_ANNOTATION_INDEX_PROPERTY)) {
helper.autoDetectEclipse = false;
}
boolean first = true;
for (final URL url : ((URLClassLoader) loader).getURLs()) {
debug("Checking URL: " + url);
if (helper.autoDetectEclipse && first) {
if (!"file".equals(url.getProtocol()) ||
(!url.getPath().endsWith("/") && !url.getPath().contains("surefire")))
{
debug("Not Eclipse because first entry is: " + url);
return;
}
first = false;
}
if (url.toString().endsWith("/./")) {
// Eclipse never adds "." to the class path
break;
}
helper.maybeIndex(url, loader);
}
updateAnnotationIndex(loader.getParent());
}
private void maybeIndex(final URL url, final ClassLoader loader) {
synchronized (indexed) {
if (indexed.contains(url)) {
return;
}
indexed.add(url);
}
if (!"file".equals(url.getProtocol())) {
debug("Not a file URL: " + url);
return;
}
File file = FileUtils.urlToFile(url);
if (autoDetectEclipse && !file.isAbsolute()) {
debug("Not an absolute file URL: " + url);
return;
}
final String name = file.getName();
if (name.endsWith(".jar")) {
if (!file.isFile()) {
debug("Not a file: " + file);
return;
}
/*
* To support mixed development with Eclipse and Maven, let's handle
* the case where Eclipse compiled classes, did not run the annotation
* processors, then the developer called "mvn test". In this case, we
* have a surefirebooter.jar whose manifest contains the dependencies,
* but crucially also the target/classes/ and target/test-classes/
* directories which may need to be indexed.
*/
if (!autoDetectEclipse || url.toString().matches(".*/target/surefire/surefirebooter[0-9]*\\.jar")) try {
final JarFile jar = new JarFile(file);
Manifest manifest = jar.getManifest();
if (manifest != null) {
final String classPath =
manifest.getMainAttributes().getValue(Name.CLASS_PATH);
if (classPath != null) {
for (final String element : classPath.split(" +"))
try {
maybeIndex(new URL(url, element), loader);
}
catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
}
catch (final IOException e) {
System.err.println("Warning: could not index annotations due to ");
e.printStackTrace();
}
return;
}
if (!file.isDirectory()) {
return;
}
index(file, loader);
}
private void index(File directory, ClassLoader loader) {
debug("Directory: " + directory);
if (!directory.canWrite() || upToDate(directory) || isIJ1(directory)) {
debug("can write: " + directory.canWrite() + ", up-to-date: " +
upToDate(directory) + ", : is IJ1: " + isIJ1(directory));
return;
}
final File jsonDirectory = new File(directory, Index.INDEX_PREFIX);
try {
discoverAnnotations(directory, "", loader);
if (!jsonDirectory.exists() && !foundAnnotations()) return;
if (!bannerShown) {
System.err.println("[ECLIPSE HELPER] Indexing annotations...");
bannerShown = true;
}
write(directory);
}
catch (IOException e) {
e.printStackTrace();
}
// update the timestamp of META-INF/json/
if (jsonDirectory.isDirectory()) {
jsonDirectory.setLastModified(System.currentTimeMillis());
}
else {
jsonDirectory.mkdirs();
}
}
/**
* A hacky way of detecting whether the given directory is the root of an
* ImageJ1 codebase containing unpacked ImageJ1 classes.
*/
private boolean isIJ1(File directory) {
return new File(directory, "IJ_Props.txt").exists();
}
private boolean upToDate(final File directory) {
final File jsonDirectory = new File(directory, Index.INDEX_PREFIX);
if (!jsonDirectory.isDirectory()) {
return false;
}
return upToDate(directory, jsonDirectory.lastModified());
}
private boolean upToDate(File directory, long lastModified) {
if (directory.lastModified() > lastModified) {
return false;
}
final File[] list = directory.listFiles();
if (list != null) {
for (final File file : list) {
if (file.isFile()) {
if (file.lastModified() > lastModified) {
return false;
}
}
else if (file.isDirectory()) {
if (!upToDate(file, lastModified)) {
return false;
}
}
}
}
return true;
}
/**
* Updates the annotation index in the current Eclipse project.
*
* The assumption is that Eclipse -- after failing to run the annotation
* processors correctly -- will launch any tests or main classes with a class
* path that contains the project's output directory with the {@code .class}
* files (as opposed to a {@code .jar} file). We only need to update that
* first class path element (or for tests, the first two), and only if it is a
* local directory.
*
*/
public static void main(final String... args) {
System.setProperty(FORCE_ANNOTATION_INDEX_PROPERTY, "true");
updateAnnotationIndex(Thread.currentThread().getContextClassLoader());
}
}