io.github.mtrevisan.boxon.internal.reflection.Reflections Maven / Gradle / Ivy
/**
* Copyright (c) 2020 Mauro Trevisan
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.mtrevisan.boxon.internal.reflection;
import io.github.mtrevisan.boxon.internal.JavaHelper;
import io.github.mtrevisan.boxon.internal.reflection.exceptions.ReflectionsException;
import io.github.mtrevisan.boxon.internal.reflection.helpers.ClasspathHelper;
import io.github.mtrevisan.boxon.internal.reflection.helpers.ReflectionHelper;
import io.github.mtrevisan.boxon.internal.reflection.scanners.AbstractScanner;
import io.github.mtrevisan.boxon.internal.reflection.scanners.ScannerInterface;
import io.github.mtrevisan.boxon.internal.reflection.scanners.SubTypesScanner;
import io.github.mtrevisan.boxon.internal.reflection.scanners.TypeAnnotationsScanner;
import io.github.mtrevisan.boxon.internal.reflection.vfs.VFSDirectory;
import io.github.mtrevisan.boxon.internal.reflection.vfs.VFSException;
import io.github.mtrevisan.boxon.internal.reflection.vfs.VFSFile;
import io.github.mtrevisan.boxon.internal.reflection.vfs.VirtualFileSystem;
import org.slf4j.Logger;
import java.lang.annotation.Annotation;
import java.lang.annotation.Inherited;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
/**
* @see Reflections
*/
public final class Reflections{
private static final Logger LOGGER = JavaHelper.getLoggerFor(Reflections.class);
private static final String DOT_CLASS = ".class";
private static final TypeAnnotationsScanner TYPE_ANNOTATIONS_SCANNER = new TypeAnnotationsScanner();
private static final SubTypesScanner SUB_TYPES_SCANNER = new SubTypesScanner();
private static final ScannerInterface[] SCANNERS = {TYPE_ANNOTATIONS_SCANNER, SUB_TYPES_SCANNER};
public static Reflections create(final URL... urls){
return new Reflections(false, urls);
}
public static Reflections create(final Class... classes){
final URL[] urls = extractDistinctURLs(classes);
return new Reflections(false, urls);
}
public static Reflections createExpandSuperTypes(final Class... classes){
final URL[] urls = extractDistinctURLs(classes);
return new Reflections(true, urls);
}
/**
* Sometimes simple calls have unexpected side effects. I wanted to update some plugins, but the update manager was
* hanging my UI. Looking at the stack trace reveals:
*
* at java.net.Inet4AddressImpl.lookupAllHostAddr(Native Method)
* at java.net.InetAddress$1.lookupAllHostAddr(Unknown Source)
* at java.net.InetAddress.getAddressFromNameService(Unknown Source)
* at java.net.InetAddress.getAllByName0(Unknown Source)
* at java.net.InetAddress.getAllByName0(Unknown Source)
* at java.net.InetAddress.getAllByName(Unknown Source)
* at java.net.InetAddress.getByName(Unknown Source)
* at java.net.URLStreamHandler.getHostAddress(Unknown Source)
* - locked <0x15ce1280> (a sun.net.www.protocol.http.Handler)
* at java.net.URLStreamHandler.hashCode(Unknown Source)
* at java.net.URL.hashCode(Unknown Source)
* - locked <0x1a3100d0> (a java.net.URL)
*
* Hmm, I must say that it is very dangerous that {@link URL#hashCode()} and {@link URL#equals(Object)} makes an
* Internet connection. {@link URL} has the worst equals/hasCode implementation I have ever seen: equality DEPENDS on the state
* of the Internet.
* Do not put {@link URL} into collections unless you can live with the fact that comparing makes calls to the Internet.
* Use {@link java.net.URI} instead.
* URL is an aggressive beast that can slow down and hang your application by making unexpected network traffic.
*
* @see java.net.URL.equals and hashCode make (blocking) Internet connections
*/
private static URL[] extractDistinctURLs(final Class[] classes){
final Set uris = convertIntoURI(classes);
final List urls = new ArrayList<>(uris.size());
try{
for(final URI uri : uris)
urls.add(uri.toURL());
}
catch(final MalformedURLException e){
//cannot happen
e.printStackTrace();
}
return urls.toArray(URL[]::new);
}
private static Set convertIntoURI(final Class[] classes){
final Set uris = new HashSet<>(classes.length);
for(final Class cls : classes){
final Collection urls = ClasspathHelper.forPackage(cls.getPackageName());
collectURIs(urls, uris);
}
return uris;
}
private static void collectURIs(final Collection urls, final Set uris){
for(final URL url : urls){
try{
uris.add(url.toURI());
}
catch(final URISyntaxException e){
LOGGER.warn("Invalid URL, cannot convert into URI", e);
}
}
}
public static Reflections createExpandSuperTypes(final URL... urls){
return new Reflections(true, urls);
}
private Reflections(final boolean expandSuperTypes, final URL... urls){
Objects.requireNonNull(urls);
if(urls.length == 0)
throw new IllegalArgumentException("Packages list cannot be empty");
for(final URL url : urls)
scan(url);
if(expandSuperTypes)
expandSuperTypes();
}
private void scan(final URL url){
try(final VFSDirectory directory = VirtualFileSystem.fromURL(url)){
//process each file in the directory
for(final VFSFile file : directory.getFiles())
scan(url, directory, file);
}
catch(final VFSException | ReflectionsException e){
if(LOGGER != null)
LOGGER.warn("Could not create VFSDirectory from URL, ignoring the exception and continuing", e);
}
catch(final Exception e){
if(LOGGER != null)
LOGGER.warn("Could not close directory", e);
}
}
private void scan(final URL url, final VFSDirectory directory, final VFSFile file){
final String relativePath = file.getRelativePath();
if(relativePath.endsWith(DOT_CLASS)){
final Object classObject = AbstractScanner.createClassObject(directory, file);
for(final ScannerInterface scanner : SCANNERS)
scan(scanner, url, relativePath, classObject);
}
}
private void scan(final ScannerInterface scanner, final URL url, final String relativePath, final Object classObject){
try{
scanner.scan(classObject);
if(LOGGER != null)
LOGGER.trace("Scanned file {} in URL {} with scanner {}", relativePath, url.toExternalForm(),
scanner.getClass().getSimpleName());
}
catch(final Exception e){
if(LOGGER != null)
LOGGER.debug("Could not scan file {} in URL {} with scanner {}", relativePath, url.toExternalForm(),
scanner.getClass().getSimpleName(), e);
}
}
/**
* Expand super types after scanning (for super types that were not scanned).
* This is helpful in finding the transitive closure without scanning all third party dependencies.
* It uses {@link ReflectionHelper#getSuperTypes(Class)}.
* For example, for classes {@code A, B, C} where {@code A} supertype of {@code B}, and {@code B} supertype of {@code C}:
*
* - if scanning {@code C} resulted in {@code B} ({@code B -> C} in class store), but {@code A} was not scanned
* (although {@code A} supertype of {@code B}) - then {@code getSubTypesOf(A)} will not return {@code C}.
* - if expanding supertypes, {@code B} will be expanded with {@code A} ({@code A -> B} in class store) - then
* {@code getSubTypesOf(A)} will return {@code C}.
*
*
*/
private void expandSuperTypes(){
final Set keys = SUB_TYPES_SCANNER.keys();
keys.removeAll(SUB_TYPES_SCANNER.values());
for(final String key : keys){
final Class type = ClasspathHelper.getClassFromName(key);
if(type != null)
expandSupertypes(SUB_TYPES_SCANNER, key, type);
}
}
private void expandSupertypes(final AbstractScanner scanner, final String key, final Class type){
for(final Class superType : ReflectionHelper.getSuperTypes(type))
if(scanner.put(superType.getName(), key)){
expandSupertypes(scanner, superType.getName(), superType);
if(LOGGER != null)
LOGGER.trace("Expanded subtype {} into {}", superType.getName(), key);
}
}
/**
* Gets all sub types in hierarchy of a given type.
*
* @param type The type to search for.
* @return The set of classes.
*/
public Set> getSubTypesOf(final Class type){
return ClasspathHelper.getClassFromNames(SUB_TYPES_SCANNER.getAll(type.getName()));
}
/**
* Get types annotated with a given annotation, both classes and annotations.
* {@link Inherited} is not honored by default.
* When honoring {@link Inherited}, meta-annotation should only effect annotated super classes and its sub types.
* Note that this ({@link Inherited}) meta-annotation type has no effect if the annotated type is used for
* anything other then a class. Also, this meta-annotation causes annotations to be inherited only from superclasses;
* annotations on implemented interfaces have no effect.
*
* @param annotation The annotation to search for.
* @return The set of classes.
*/
public Set> getTypesAnnotatedWith(final Class annotation){
return getTypesAnnotatedWith(annotation, false);
}
/**
* Get types annotated with a given annotation, both classes and annotations.
* {@link Inherited} is not honored by default.
* When honoring {@link Inherited}, meta-annotation should only effect annotated super classes and its sub types.
* When not honoring {@link Inherited}, meta annotation effects all subtypes, including annotations interfaces
* and classes.
* Note that this ({@link Inherited}) meta-annotation type has no effect if the annotated type is used for
* anything other then a class. Also, this meta-annotation causes annotations to be inherited only from superclasses;
* annotations on implemented interfaces have no effect.
*
* @param annotation The annotation to search for.
* @return The set of classes.
*/
public Set> getTypesAnnotatedWithHonorInherited(final Class annotation){
return getTypesAnnotatedWith(annotation, true);
}
private Set> getTypesAnnotatedWith(final Class annotation, final boolean honorInherited){
final Set annotated = TYPE_ANNOTATIONS_SCANNER.get(annotation.getName());
annotated.addAll(getAllAnnotatedClasses(annotated, annotation, honorInherited));
return ClasspathHelper.getClassFromNames(annotated);
}
/**
* Get types annotated with a given annotation, both classes and annotations, including annotation member values matching.
* {@link Inherited} is not honored by default.
* When honoring {@link Inherited}, meta-annotation should only effect annotated super classes and its sub types.
* Note that this ({@link Inherited}) meta-annotation type has no effect if the annotated type is used for
* anything other then a class. Also, this meta-annotation causes annotations to be inherited only from superclasses;
* annotations on implemented interfaces have no effect.
*
* @param annotation The annotation.
* @return The set of classes.
*/
public Set> getTypesAnnotatedWith(final Annotation annotation){
return getTypesAnnotatedWith(annotation, false);
}
/**
* Get types annotated with a given annotation, both classes and annotations, including annotation member values matching.
* {@link Inherited} is not honored by default.
* When honoring {@link Inherited}, meta-annotation should only effect annotated super classes and its sub types.
* When not honoring {@link Inherited}, meta annotation effects all subtypes, including annotations interfaces
* and classes.
* Note that this ({@link Inherited}) meta-annotation type has no effect if the annotated type is used for
* anything other then a class. Also, this meta-annotation causes annotations to be inherited only from superclasses;
* annotations on implemented interfaces have no effect.
*
* @param annotation The annotation.
* @return The set of classes.
*/
public Set> getTypesAnnotatedWithHonorInherited(final Annotation annotation){
return getTypesAnnotatedWith(annotation, true);
}
private Set> getTypesAnnotatedWith(final Annotation annotation, final boolean honorInherited){
final Set annotated = TYPE_ANNOTATIONS_SCANNER.get(annotation.annotationType().getName());
final Set> allAnnotated = JavaHelper.filter(ClasspathHelper.getClassFromNames(annotated),
getFilterOnAnnotation(annotation));
final Set filtered = JavaHelper.filter(getAllAnnotatedClasses(ClasspathHelper.getClassNames(allAnnotated),
annotation.annotationType(), honorInherited), Predicate.not(annotated::contains));
allAnnotated.addAll(ClasspathHelper.getClassFromNames(filtered));
return allAnnotated;
}
/**
* Returns a predicate that tell if the class is annotated with given {@code annotation}, including member matching.
*
* @param annotation The annotation.
* @return The predicate.
* @param The type of the returned predicate.
*/
private Predicate getFilterOnAnnotation(final Annotation annotation){
return input -> (input != null
&& input.isAnnotationPresent(annotation.annotationType())
&& areAnnotationMembersMatching(input.getAnnotation(annotation.annotationType()), annotation));
}
private boolean areAnnotationMembersMatching(final Annotation annotation1, final Annotation annotation2){
final boolean result = (annotation2 != null && annotation1.annotationType() == annotation2.annotationType());
if(result){
try{
for(final Method method : annotation1.annotationType().getDeclaredMethods())
if(!method.invoke(annotation1).equals(method.invoke(annotation2)))
return false;
}
catch(final Exception e){
throw new ReflectionsException("Could not invoke a method on annotation {} or {}", annotation1.annotationType(),
annotation2.annotationType())
.withCause(e);
}
}
return result;
}
private Collection getAllAnnotatedClasses(final Collection annotated,
final Class annotation, final boolean honorInherited){
if(!honorInherited){
final Collection subTypes = TYPE_ANNOTATIONS_SCANNER.getAllIncludingKeys(annotated);
return SUB_TYPES_SCANNER.getAllIncludingKeys(subTypes);
}
else if(annotation.isAnnotationPresent(Inherited.class)){
final Set subTypes = SUB_TYPES_SCANNER.get(JavaHelper.filter(annotated, input -> {
final Class type = ClasspathHelper.getClassFromName(input);
return (type != null && !type.isInterface());
}));
return SUB_TYPES_SCANNER.getAllIncludingKeys(subTypes);
}
else
return annotated;
}
}