001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.atteo.classindex;
015
016import java.io.BufferedReader;
017import java.io.FileNotFoundException;
018import java.io.IOException;
019import java.io.InputStreamReader;
020import java.lang.annotation.Annotation;
021import java.net.URL;
022import java.nio.charset.StandardCharsets;
023import java.util.ArrayList;
024import java.util.Enumeration;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Set;
028
029import org.atteo.classindex.processor.ClassIndexProcessor;
030
031/**
032 * Access to the compile-time generated index of classes.
033 * <p/>
034 * <p>
035 * Use &#064;{@link IndexAnnotated} and &#064;{@link IndexSubclasses} annotations to force the classes to be indexed.
036 * </p>
037 * <p/>
038 * <p>
039 * Keep in mind that the class is indexed only when it is compiled with
040 * classindex.jar file in classpath.
041 * </p>
042 * <p/>
043 * <p>
044 * Also to preserve class-index data when creating shaded jar you should use the following
045 * Maven configuration:
046 * <pre>
047 * {@code
048 * <build>
049 *   <plugins>
050 *     <plugin>
051 *       <groupId>org.apache.maven.plugins</groupId>
052 *       <artifactId>maven-shade-plugin</artifactId>
053 *       <version>1.4</version>
054 *       <executions>
055 *         <execution>
056 *           <phase>package</phase>
057 *           <goals>
058 *             <goal>shade</goal>
059 *           </goals>
060 *           <configuration>
061 *             <transformers>
062 *               <transformer implementation="org.atteo.classindex.ClassIndexTransformer"/>
063 *             </transformers>
064 *           </configuration>
065 *         </execution>
066 *       </executions>
067 *       <dependencies>
068 *         <groupId>org.atteo.classindex</groupId>
069 *         <artifactId>classindex-transformer</artifactId>
070 *       </dependencies>
071 *     </plugin>
072 *   </plugins>
073 * </build>
074 * }
075 * </pre>
076 * </p>
077 */
078public class ClassIndex {
079    public static final String SUBCLASS_INDEX_PREFIX = "META-INF/services/";
080    public static final String ANNOTATED_INDEX_PREFIX = "META-INF/annotations/";
081    public static final String PACKAGE_INDEX_NAME = "jaxb.index";
082    public static final String JAVADOC_PREFIX = "META-INF/javadocs/";
083
084    private ClassIndex() {
085
086    }
087
088    /**
089     * Retrieves a list of subclasses of the given class.
090     * <p/>
091     * <p>
092     * The class must be annotated with {@link IndexSubclasses} for it's subclasses to be indexed
093     * at compile-time by {@link ClassIndexProcessor}.
094     * </p>
095     *
096     * @param superClass class to find subclasses for
097     * @return list of subclasses
098     */
099    @SuppressWarnings("unchecked")
100    public static <T> Iterable<Class<? extends T>> getSubclasses(Class<T> superClass) {
101        return getSubclasses(superClass, Thread.currentThread().getContextClassLoader());
102    }
103
104    /**
105     * Retrieves a list of subclasses of the given class.
106     * <p/>
107     * <p>
108     * The class must be annotated with {@link IndexSubclasses} for it's subclasses to be indexed
109     * at compile-time by {@link ClassIndexProcessor}.
110     * </p>
111     *
112     * @param superClass class to find subclasses for
113     * @param classLoader classloader for loading classes
114     * @return list of subclasses
115     */
116    @SuppressWarnings("unchecked")
117    public static <T> Iterable<Class<? extends T>> getSubclasses(Class<T> superClass, ClassLoader classLoader) {
118        Iterable<String> entries = readIndexFile(classLoader, SUBCLASS_INDEX_PREFIX + superClass.getCanonicalName());
119        Set<Class<?>> classes = new HashSet<>();
120        findClasses(classLoader, classes, entries);
121        List<Class<? extends T>> subclasses = new ArrayList<>();
122
123        for (Class<?> klass : classes) {
124            if (superClass.isAssignableFrom(klass)) {
125                subclasses.add((Class<? extends T>) klass);
126            }
127        }
128
129        return subclasses;
130    }
131
132    /**
133     * Retrieves a list of classes from given package.
134     * <p/>
135     * <p>
136     * The package must be annotated with {@link IndexSubclasses} for the classes inside
137     * to be indexed at compile-time by {@link ClassIndexProcessor}.
138     * </p>
139     *
140     * @param packageName name of the package to search classes for
141     * @return list of classes from package
142     */
143    public static Iterable<Class<?>> getPackageClasses(String packageName) {
144        return getPackageClasses(packageName, Thread.currentThread().getContextClassLoader());
145    }
146
147    /**
148     * Retrieves a list of classes from given package.
149     * <p/>
150     * <p>
151     * The package must be annotated with {@link IndexSubclasses} for the classes inside
152     * to be indexed at compile-time by {@link ClassIndexProcessor}.
153     * </p>
154     *
155     * @param packageName name of the package to search classes for
156     * @param classLoader classloader for loading classes
157     * @return list of classes from package
158     */
159    public static Iterable<Class<?>> getPackageClasses(String packageName, ClassLoader classLoader) {
160        Iterable<String> entries = readIndexFile(classLoader, packageName.replace(".", "/") + "/" + PACKAGE_INDEX_NAME);
161
162        Set<Class<?>> classes = new HashSet<>();
163        findClassesInPackage(classLoader, packageName, classes, entries);
164        findClasses(classLoader, classes, entries);
165        return classes;
166    }
167
168    /**
169     * Retrieves a list of classes annotated by given annotation.
170     * <p/>
171     * <p>
172     * The annotation must be annotated with {@link IndexAnnotated} for annotated classes
173     * to be indexed at compile-time by {@link ClassIndexProcessor}.
174     * </p>
175     *
176     * @param annotation annotation to search class for
177     * @return list of annotated classes
178     */
179    public static Iterable<Class<?>> getAnnotated(Class<? extends Annotation> annotation) {
180        return getAnnotated(annotation, Thread.currentThread().getContextClassLoader());
181    }
182
183    /**
184     * Retrieves a list of classes annotated by given annotation.
185     * <p/>
186     * <p>
187     * The annotation must be annotated with {@link IndexAnnotated} for annotated classes
188     * to be indexed at compile-time by {@link ClassIndexProcessor}.
189     * </p>
190     *
191     * @param annotation  annotation to search class for
192     * @param classLoader classloader for loading classes
193     * @return list of annotated classes
194     */
195    public static Iterable<Class<?>> getAnnotated(Class<? extends Annotation> annotation, ClassLoader classLoader) {
196        Iterable<String> entries = readIndexFile(classLoader, ANNOTATED_INDEX_PREFIX + annotation.getCanonicalName());
197        Set<Class<?>> classes = new HashSet<>();
198        findClasses(classLoader, classes, entries);
199        return classes;
200    }
201
202    /**
203     * Returns the Javadoc summary for given class.
204     * <p>
205     * Javadoc summary is the first sentence of a Javadoc.
206     * </p>
207     * <p>
208     * You need to use {@link IndexSubclasses} or {@link IndexAnnotated} with {@link IndexAnnotated#storeJavadoc()}
209     * set to true.
210     * </p>
211     *
212     * @param klass class to retrieve summary for
213     * @return summary for given class, or null if it does not exists
214     * @see <a href="http://www.oracle.com/technetwork/java/javase/documentation/index-137868.html#writingdoccomments">Writing doc comments</a>
215     */
216    public static String getClassSummary(Class<?> klass) {
217        return getClassSummary(klass, Thread.currentThread().getContextClassLoader());
218    }
219
220    /**
221     * Returns the Javadoc summary for given class.
222     * <p>
223     * Javadoc summary is the first sentence of a Javadoc.
224     * </p>
225     * <p>
226     * You need to use {@link IndexSubclasses} or {@link IndexAnnotated} with {@link IndexAnnotated#storeJavadoc()}
227     * set to true.
228     * </p>
229     *
230     * @param klass       class to retrieve summary for
231     * @param classLoader classloader for loading classes
232     * @return summary for given class, or null if it does not exists
233     * @see <a href="http://www.oracle.com/technetwork/java/javase/documentation/index-137868.html#writingdoccomments">Writing doc comments</a>
234     */
235    public static String getClassSummary(Class<?> klass, ClassLoader classLoader) {
236        URL resource = classLoader.getResource(JAVADOC_PREFIX + klass.getCanonicalName());
237        if (resource == null) {
238            return null;
239        }
240        try {
241            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8))) {
242                StringBuilder builder = new StringBuilder();
243                String line = reader.readLine();
244                while (line != null) {
245                    int dotIndex = line.indexOf('.');
246                    if (dotIndex == -1) {
247                        builder.append(line);
248                    } else {
249                        builder.append(line.subSequence(0, dotIndex));
250                        return builder.toString().trim();
251                    }
252                    line = reader.readLine();
253                }
254                return builder.toString().trim();
255            } catch (FileNotFoundException e) {
256                // catch this just in case some compiler actually throws that
257                return null;
258            }
259        } catch (IOException e) {
260            throw new RuntimeException("ClassIndex: Cannot read Javadoc index", e);
261        }
262    }
263
264    private static Iterable<String> readIndexFile(ClassLoader classLoader, String resourceFile) {
265        Set<String> entries = new HashSet<>();
266
267        try {
268            Enumeration<URL> resources = classLoader.getResources(resourceFile);
269
270            while (resources.hasMoreElements()) {
271                URL resource = resources.nextElement();
272                try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8))) {
273
274                    String line = reader.readLine();
275                    while (line != null) {
276                        entries.add(line);
277                        line = reader.readLine();
278                    }
279                } catch (FileNotFoundException e) {
280                    // When executed under Tomcat started from Eclipse with "Serve modules without
281                    // publishing" option turned on, getResources() method above returns the same
282                    // resource two times: first with incorrect path and second time with correct one.
283                    // So ignore the one which does not exist.
284                    // See: https://github.com/atteo/classindex/issues/5
285                }
286            }
287        } catch (IOException e) {
288            throw new RuntimeException("ClassIndex: Cannot read class index", e);
289        }
290        return entries;
291    }
292
293    private static void findClasses(ClassLoader classLoader, Set<Class<?>> classes, Iterable<String> entries) {
294        for (String entry : entries) {
295            Class<?> klass;
296            try {
297                klass = classLoader.loadClass(entry);
298            } catch (ClassNotFoundException e) {
299                continue;
300            }
301            classes.add(klass);
302        }
303    }
304
305    private static void findClassesInPackage(ClassLoader classLoader, String packageName, Set<Class<?>> classes,
306            Iterable<String> entries) {
307        for (String entry : entries) {
308            if (entry.contains(".")) {
309                continue;
310            }
311            Class<?> klass;
312            try {
313                klass = classLoader.loadClass(packageName + "." + entry);
314            } catch (ClassNotFoundException e) {
315                continue;
316            }
317            classes.add(klass);
318        }
319    }
320}