Skip to content

Commit ab0396e

Browse files
committed
Add a helper for Eclipse
Eclipse is a bit challenged and challenging when it comes to following the Java standard. For example, in the default mode Eclipse does not run any annotation processors (as it would be mandated by the Java specification). To compensate for this, we simply run a custom annotation processor whenever we encounter at Index.load() time to handle the case that there might be some annotation index that is out-of-date. Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
1 parent 4b03603 commit ab0396e

3 files changed

Lines changed: 340 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* #%L
3+
* Annotation index (processor and index access library).
4+
* %%
5+
* Copyright (C) 2009 - 2013 Board of Regents of the University of Wisconsin-Madison.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
*
28+
* The views and conclusions contained in the software and documentation are
29+
* those of the authors and should not be interpreted as representing official
30+
* policies, either expressed or implied, of any organization.
31+
* #L%
32+
*/
33+
34+
package org.scijava.annotations;
35+
36+
import java.io.File;
37+
import java.io.IOException;
38+
import java.net.URL;
39+
import java.net.URLClassLoader;
40+
41+
/**
42+
* Helps Eclipse's lack of support for annotation processing in incremental
43+
* build mode.
44+
* <p>
45+
* Eclipse has a very, let's say, "creative" way to interpret the Java
46+
* specifications when it comes to annotation processing: while Java mandates
47+
* that annotation processors need to be run after compiling Java classes,
48+
* Eclipse cops out of that because it poses a challenge to its incremental
49+
* compilation (and especially to Eclipse's attempt at compiling .class files
50+
* even from .java sources that contain syntax errors).
51+
* </p>
52+
* <p>
53+
* So we need to do something about this. Our strategy is to detect when the
54+
* annotation index was not updated properly and just do it ourselves, whenever
55+
* {@link Index#load(Class)} is called.
56+
* </p>
57+
* <p>
58+
* Since our aim here is to compensate for Eclipse's shortcoming, we need only
59+
* care about the scenario where the developer launches either a Java main class
60+
* or a unit test from within Eclipse, and even then only when the annotation
61+
* index is to be accessed.
62+
* </p>
63+
* <p>
64+
* The way Eclipse launches Java main classes or unit tests, it makes a single
65+
* {@link URLClassLoader} with all the necessary class path elements. Crucially,
66+
* the class path elements corresponding to Eclipse projects will never point to
67+
* {@code .jar} files but to directories. This allows us to assume that the
68+
* annotation classes as well as the annotated classes can be loaded using that
69+
* exact class loader, too.
70+
* </p>
71+
* <p>
72+
* It is quite possible that a developer may launch a main class in a different
73+
* project than the one which needs annotation indexing, therefore we need to
74+
* inspect all class path elements.
75+
* </p>
76+
* <p>
77+
* To provide at least a semblance of a performant component, before going all
78+
* out and indexing the annotations, we verify that the {@code META-INF/json/}
79+
* directory has an outdated timestamp relative to the {@code .class} files. If
80+
* that is not the case, we may safely assume that the annotation indexes are
81+
* up-to-date.
82+
* </p>
83+
* <p>
84+
* To avoid indexing class path elements over and over again which simply do not
85+
* contain indexable annotations, we make the {@code META-INF/json/} directory
86+
* nevertheless, updating the timestamp to reflect that we indexed the
87+
* annotations.
88+
* </p>
89+
*
90+
* @author Johannes Schindelin
91+
*/
92+
public class EclipseHelper extends DirectoryIndexer {
93+
94+
/**
95+
* Updates the annotation index in the current Eclipse project.
96+
* <p>
97+
* The assumption is that Eclipse -- after failing to run the annotation
98+
* processors correctly -- will launch any tests or main classes with a class
99+
* path that contains the project's output directory with the {@code .class}
100+
* files (as opposed to a {@code .jar} file). We only need to update that
101+
* first class path element (or for tests, the first two), and only if it is a
102+
* local directory.
103+
* </p>
104+
*
105+
* @param loader the class loader whose class path to inspect
106+
* @throws IOException
107+
*/
108+
public static void updateAnnotationIndex(final ClassLoader loader) {
109+
if (!(loader instanceof URLClassLoader)) {
110+
return;
111+
}
112+
EclipseHelper helper = null;
113+
for (final URL url : ((URLClassLoader) loader).getURLs()) {
114+
if (!"file".equals(url.getProtocol())) {
115+
continue;
116+
}
117+
String path = url.getFile();
118+
if (path.indexOf(':') >= 0) {
119+
continue;
120+
}
121+
File directory = new File(path);
122+
if (!directory.isDirectory()) {
123+
continue;
124+
}
125+
if (helper == null) {
126+
helper = new EclipseHelper();
127+
}
128+
helper.index(directory, loader);
129+
}
130+
}
131+
132+
private void index(File directory, ClassLoader loader) {
133+
if (!directory.canWrite() || upToDate(directory)) {
134+
return;
135+
}
136+
System.err.println("[ECLIPSE HELPER] Indexing annotations...");
137+
try {
138+
discoverAnnotations(directory, "", loader);
139+
write(directory);
140+
}
141+
catch (IOException e) {
142+
e.printStackTrace();
143+
}
144+
// update the timestamp of META-INF/json/
145+
final File jsonDirectory = new File(directory, Index.INDEX_PREFIX);
146+
if (jsonDirectory.isDirectory()) {
147+
jsonDirectory.setLastModified(System.currentTimeMillis());
148+
}
149+
else {
150+
jsonDirectory.mkdirs();
151+
}
152+
}
153+
154+
private boolean upToDate(final File directory) {
155+
final File jsonDirectory = new File(directory, Index.INDEX_PREFIX);
156+
if (!jsonDirectory.isDirectory()) {
157+
return false;
158+
}
159+
return upToDate(directory, jsonDirectory.lastModified());
160+
}
161+
162+
private boolean upToDate(File directory, long lastModified) {
163+
if (directory.lastModified() > lastModified) {
164+
return false;
165+
}
166+
final File[] list = directory.listFiles();
167+
if (list != null) {
168+
for (final File file : list) {
169+
if (file.isFile()) {
170+
if (file.lastModified() > lastModified) {
171+
return false;
172+
}
173+
}
174+
else if (file.isDirectory()) {
175+
if (!upToDate(file, lastModified)) {
176+
return false;
177+
}
178+
}
179+
}
180+
}
181+
return true;
182+
}
183+
}

src/main/java/org/scijava/annotations/Index.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public static <A extends Annotation> Index<A> load(final Class<A> annotation)
8585
public static <A extends Annotation> Index<A> load(final Class<A> annotation,
8686
final ClassLoader loader)
8787
{
88+
EclipseHelper.updateAnnotationIndex(loader);
8889
return new Index<A>(annotation, loader);
8990
}
9091

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* #%L
3+
* Annotation index (processor and index access library).
4+
* %%
5+
* Copyright (C) 2009 - 2013 Board of Regents of the University of Wisconsin-Madison.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
*
28+
* The views and conclusions contained in the software and documentation are
29+
* those of the authors and should not be interpreted as representing official
30+
* policies, either expressed or implied, of any organization.
31+
* #L%
32+
*/
33+
34+
package org.scijava.annotations;
35+
36+
import static org.junit.Assert.assertEquals;
37+
import static org.junit.Assert.assertFalse;
38+
import static org.junit.Assert.assertTrue;
39+
40+
import java.io.File;
41+
import java.io.FileOutputStream;
42+
import java.io.IOException;
43+
import java.io.InputStream;
44+
import java.io.OutputStream;
45+
import java.net.URL;
46+
import java.net.URLClassLoader;
47+
48+
import org.junit.Test;
49+
50+
/**
51+
* Verifies that the {@link EclipseHelper} does its job correctly.
52+
*
53+
* @author Johannes Schindelin
54+
*/
55+
public class EclipseHelperTest {
56+
57+
@Test
58+
public void testIndexing() throws Exception {
59+
final File dir = createTempDirectory();
60+
copyClasses(dir, Complex.class, Simple.class, Fruit.class,
61+
AnnotatedA.class, AnnotatedB.class, AnnotatedC.class);
62+
final File jsonDir = new File(dir, Index.INDEX_PREFIX);
63+
for (final Class<?> clazz : new Class<?>[] { Complex.class, Simple.class })
64+
{
65+
assertFalse(new File(jsonDir, clazz.getName()).exists());
66+
}
67+
final URLClassLoader loader =
68+
new URLClassLoader(new URL[] { dir.toURI().toURL() }, getClass()
69+
.getClassLoader().getParent())
70+
{
71+
72+
@Override
73+
public Class<?> loadClass(final String className)
74+
throws ClassNotFoundException
75+
{
76+
if (className.equals(Indexable.class.getName())) {
77+
return Indexable.class;
78+
}
79+
return super.loadClass(className);
80+
}
81+
};
82+
EclipseHelper.updateAnnotationIndex(loader);
83+
for (final Class<?> clazz : new Class<?>[] { Complex.class, Simple.class })
84+
{
85+
assertTrue(new File(jsonDir, clazz.getName()).exists());
86+
}
87+
assertEquals(2, jsonDir.list().length);
88+
}
89+
90+
private void copyClasses(final File dir, final Class<?>... classes)
91+
throws IOException
92+
{
93+
final byte[] buffer = new byte[16384];
94+
for (final Class<?> clazz : classes) {
95+
final String classPath = DirectoryIndexerTest.getResourcePath(clazz);
96+
final InputStream in =
97+
getClass().getResource("/" + classPath).openStream();
98+
final File outFile = new File(dir, classPath);
99+
final File parent = outFile.getParentFile();
100+
assertTrue(parent.isDirectory() || parent.mkdirs());
101+
final OutputStream out = new FileOutputStream(outFile);
102+
for (;;) {
103+
int count = in.read(buffer);
104+
if (count < 0) {
105+
break;
106+
}
107+
out.write(buffer, 0, count);
108+
}
109+
in.close();
110+
out.close();
111+
}
112+
}
113+
114+
private File createTempDirectory() throws IOException {
115+
// if running from .../target/test-classes/, let's make a directory next to
116+
// it
117+
final String classPath =
118+
"/" + DirectoryIndexerTest.getResourcePath(getClass());
119+
final String url = getClass().getResource(classPath).toString();
120+
if (url.startsWith("file:") && url.endsWith(classPath)) {
121+
final String directory =
122+
url.substring(5, url.length() - classPath.length());
123+
if (directory.endsWith("/target/test-classes")) {
124+
final File testClassesDirectory = new File(directory);
125+
if (testClassesDirectory.isDirectory()) {
126+
final File result =
127+
new File(testClassesDirectory.getParentFile(), "eclipse-test");
128+
if (result.exists()) {
129+
rmRF(result);
130+
}
131+
return result;
132+
}
133+
}
134+
}
135+
// fall back to /tmp/
136+
final File result = File.createTempFile("eclipse-test", "");
137+
result.delete();
138+
result.mkdir();
139+
return result;
140+
}
141+
142+
private static boolean rmRF(final File directory) {
143+
final File[] list = directory.listFiles();
144+
if (list != null) {
145+
for (final File file : list) {
146+
if (file.isFile() && !file.delete()) {
147+
return false;
148+
}
149+
else if (file.isDirectory() && !rmRF(file)) {
150+
return false;
151+
}
152+
}
153+
}
154+
return directory.delete();
155+
}
156+
}

0 commit comments

Comments
 (0)