package io.micronaut.core.io.scan;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.reflect.ClassUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarFile;
import java.util.stream.Stream;
@Internal
public class ClassPathAnnotationScanner implements AnnotationScanner {
private static final Logger LOG = LoggerFactory.getLogger(ClassPathAnnotationScanner.class);
private final ClassLoader classLoader;
private boolean includeJars;
public ClassPathAnnotationScanner(ClassLoader classLoader) {
this.classLoader = classLoader;
this.includeJars = true;
}
public ClassPathAnnotationScanner() {
this(ClassPathAnnotationScanner.class.getClassLoader());
}
protected ClassPathAnnotationScanner includeJars(boolean includeJars) {
this.includeJars = includeJars;
return this;
}
@Override
public Stream<Class> scan(String annotation, String pkg) {
if (pkg == null) {
return Stream.empty();
}
List<Class> classes = doScan(annotation, pkg);
return classes.stream();
}
protected List<Class> doScan(String annotation, String pkg) {
try {
String packagePath = pkg.replace('.', '/').concat("/");
List<Class> classes = new ArrayList<>();
Enumeration<URL> resources = classLoader.getResources(packagePath);
if (!resources.hasMoreElements() && LOG.isDebugEnabled()) {
LOG.debug("No resources found under package path: {}", packagePath);
}
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
try {
traverseFile(annotation, classes, Paths.get(url.toURI()));
} catch (URISyntaxException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring file [" + url + "] due to URI error: " + e.getMessage(), e);
}
}
} else if (includeJars && Arrays.asList("jar", "zip", "war").contains(protocol)) {
URLConnection con = url.openConnection();
if (con instanceof JarURLConnection) {
JarURLConnection jarCon = (JarURLConnection) con;
JarFile jarFile = jarCon.getJarFile();
jarFile.stream()
.filter(entry -> {
String name = entry.getName();
return name.startsWith(packagePath) && name.endsWith(ClassUtils.CLASS_EXTENSION) && name.indexOf('$') == -1;
})
.forEach(entry -> {
try (InputStream inputStream = jarFile.getInputStream(entry)) {
scanInputStream(annotation, inputStream, classes);
} catch (IOException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring JAR entry [" + entry.getName() + "] due to I/O error: " + e.getMessage(), e);
}
} catch (ClassNotFoundException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring JAR entry [" + entry.getName() + "]. Class not found: " + e.getMessage(), e);
}
}
});
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring JAR URI entry [" + url + "]. No JarURLConnection found.");
}
}
}
}
return classes;
} catch (IOException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring I/O Exception scanning package: " + pkg, e);
}
return Collections.emptyList();
}
}
protected void traverseFile(String annotation, List<Class> classes, Path filePath) {
if (Files.isDirectory(filePath)) {
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(filePath)) {
dirs.forEach(path -> {
if (Files.isDirectory(path)) {
traverseFile(annotation, classes, path);
} else {
scanFile(annotation, path, classes);
}
});
} catch (IOException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring directory [" + filePath + "] due to I/O error: " + e.getMessage(), e);
}
}
} else {
scanFile(annotation, filePath, classes);
}
}
protected void scanFile(String annotation, Path filePath, List<Class> classes) {
String fileName = filePath.getFileName().toString();
if (fileName.endsWith(".class") && fileName.indexOf('$') == -1) {
try (InputStream inputStream = Files.newInputStream(filePath)) {
scanInputStream(annotation, inputStream, classes);
} catch (IOException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring file [" + fileName + "] due to I/O error: " + e.getMessage(), e);
}
} catch (ClassNotFoundException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Ignoring file [" + fileName + "]. Class not found: " + e.getMessage(), e);
}
}
}
}
private void scanInputStream(String annotation, InputStream inputStream, List<Class> classes) throws IOException, ClassNotFoundException {
AnnotationClassReader annotationClassReader = new AnnotationClassReader(inputStream);
AnnotatedTypeInfoVisitor classVisitor = new AnnotatedTypeInfoVisitor();
annotationClassReader.accept(classVisitor, AnnotationClassReader.SKIP_DEBUG);
if (classVisitor.hasAnnotation(annotation)) {
classes.add(classLoader.loadClass(classVisitor.getTypeName()));
}
}
}