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 @{@link IndexAnnotated} and @{@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}