AbstractClassBasedListenerObjectCreator.java
/*
* Copyright (C) 2012-2024 RRiBbit.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.rribbit.creation;
import org.rribbit.Listener;
import org.rribbit.ListenerObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* This {@link ListenerObjectCreator} creates {@link ListenerObject}s from classes. Users can pass in {@link Class}es or packagenames and this class will scan the {@link Class}es
* and create {@link ListenerObject}s for the public methods that are annotated with {@link Listener}. Note that public methods inherited from superclasses and superinterfaces will also be
* scanned. This means that users must take care not to scan a method twice, once as a method of a class and once as a method of a superclass, by passing a class/interface and its
* superclass/superinterface separately to this {@link ListenerObjectCreator}.
* <p />
* Please note that in Java, method annotations are NOT inherited. This means that, if you override/implement a method in a subclass or subinterface, and the overriding/implementing method
* does not have the annotation, then that method will not inherit it. If a class or interface just inherits a method, without overriding it, then the annotation WILL exist.
* <p />
* This class does NOT set the execution target for the created {@link ListenerObject}s. That is the responsibility of the implementations of this abstract class.
*
* @author G.J. Schouten
*
*/
public abstract class AbstractClassBasedListenerObjectCreator extends ObjectBasedListenerObjectCreator {
private static final Logger log = LoggerFactory.getLogger(AbstractClassBasedListenerObjectCreator.class);
protected Collection<Class<?>> excludedClasses;
/**
* Calls {@link #addClass(Class)} on each given class.
*
* @param classes
*/
public AbstractClassBasedListenerObjectCreator(Class<?>... classes) {
excludedClasses = new CopyOnWriteArrayList<>();
for(Class<?> clazz : classes) {
this.addClass(clazz);
}
}
/**
* Calls {@link #addPackage(String, boolean)} on each given package name.
*
* @param excludedClasses the classes to be excluded from scanning, can be null if not needed
* @param scanSubPackages whether to scan the subpackages in the given packages
* @param packageNames the packages to be scanned
*/
public AbstractClassBasedListenerObjectCreator(Collection<Class<?>> excludedClasses, boolean scanSubPackages, String... packageNames) {
this.excludedClasses = new CopyOnWriteArrayList<>();
if(excludedClasses != null) {
this.excludedClasses.addAll(excludedClasses);
}
for(String packageName : packageNames) {
this.addPackage(packageName, scanSubPackages);
}
}
/**
* Adds 'excludedClass' to the list of classes that are excluded from scanning when package are scanned. When classes are manually scanned with {@link #addClass(Class)},
* excluding it beforehand has NO effect. Excluded classes are only for package scanning and have NO effect on manual class scanning.
*
* @param excludedClass
*/
public void excludeClass(Class<?> excludedClass) {
excludedClasses.add(excludedClass);
}
/**
* Scans all public methods in the given {@link Class} and creates {@link ListenerObject}s for them if they have a {@link Listener} annotation, provided that
* {@link #getTargetObjectForClass(Class)} can provide a suitable execution target. Otherwise, the {@link Class} is ignored. The created {@link ListenerObject}s are added
* to the {@link Collection} of {@link ListenerObject}s that this instance keeps.
*
* @param clazz
*/
public void addClass(Class<?> clazz) {
log.debug("Processing class '{}'", clazz.getName());
Object targetObject = this.getTargetObjectForClass(clazz);
if(targetObject != null) {
log.debug("Found target object for class '{}', getting Listener methods", clazz.getName());
Collection<ListenerObject> incompleteListenerObjects = this.getIncompleteListenerObjectsFromClass(clazz);
for(ListenerObject listenerObject : incompleteListenerObjects) {
listenerObject.setTarget(targetObject);
}
listenerObjects.addAll(incompleteListenerObjects);
this.notifyObserversOnClassAdded(clazz);
}
}
/**
* Scans all classes in the given package and calls {@link #addClass(Class)} on each of them if they're not contained in the collection of excluded classes.
*
* @param packageName the package to be scanned
* @param scanSubPackages whether to scan the subpackages in the given package
*/
public void addPackage(String packageName, boolean scanSubPackages) {
log.debug("Scanning package '{}' for classes", packageName);
Collection<Class<?>> classes;
try {
classes = this.getClasses(packageName, scanSubPackages);
} catch(Exception e) {
throw new RuntimeException("Error during reading of package '" + packageName + "'", e);
}
for(Class<?> clazz : classes) {
if(!excludedClasses.contains(clazz)) {
this.addClass(clazz);
}
}
}
/**
* This method gets the {@link ClassLoader} that is used to get the classes in a package. Please override it if you want to use a different {@link ClassLoader}.
* The default is the Context {@link ClassLoader}.
*/
protected ClassLoader getClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
/**
* Scans all classes accessible in the given package, using the {@link ClassLoader} associated with this {@link AbstractClassBasedListenerObjectCreator}.
*
* @param packageName the package
* @param scanSubPackages whether to scan the subpackages in the given package
* @return the classes, or an empty {@link Collection} if none were found
* @throws ClassNotFoundException
* @throws IOException
*/
protected Collection<Class<?>> getClasses(String packageName, boolean scanSubPackages) throws ClassNotFoundException, IOException {
String path = packageName.replace('.', '/');
Enumeration<URL> resources = this.getClassLoader().getResources(path);
Collection<Class<?>> classes = new ArrayList<>();
while(resources.hasMoreElements()) {
URL resource = resources.nextElement();
String filename = URLDecoder.decode(resource.getFile(), StandardCharsets.UTF_8);
log.debug("Processing resource '{}' with filename '{}'", resource, filename);
if(resource.getProtocol().equals("jar") || resource.getProtocol().equals("zip")) {
String zipFilename = filename.substring(5, filename.indexOf("!"));
try (ZipFile zipFile = new ZipFile(zipFilename)) {
classes.addAll(this.findClassesInZipFile(zipFile, path, scanSubPackages));
}
} else {
File directory = new File(filename);
classes.addAll(this.findClassesInDirectory(directory, packageName, scanSubPackages));
}
}
return classes;
}
/**
* Method used to find all classes in a given zipFile.
*
* @param zipFile the zipFile
* @param path the package name in path format for classes found inside the zipFile
* @param scanSubPackages whether to scan the subpackages in the given path
* @return the classes, or an empty {@link Collection} if none were found
* @throws ClassNotFoundException
*/
protected Collection<Class<?>> findClassesInZipFile(ZipFile zipFile, String path, boolean scanSubPackages) throws ClassNotFoundException {
log.debug("Scanning zipFile '{}' for classes in package '{}'", zipFile.getName(), path);
Collection<Class<?>> classes = new ArrayList<>();
Enumeration<ZipEntry> zipEntries = (Enumeration<ZipEntry>) zipFile.entries();
while(zipEntries.hasMoreElements()) {
String entryName = zipEntries.nextElement().getName();
log.debug("Entry name: '{}'", entryName);
Pattern p;
if(scanSubPackages) {
p = Pattern.compile("(" + path + "/[\\w/]+)\\.class");
} else {
p = Pattern.compile("(" + path + "/\\w+)\\.class");
}
Matcher m = p.matcher(entryName);
if(m.matches()) {
String className = m.group(1).replaceAll("/", ".");
log.debug("Adding Class {}", className);
classes.add(Class.forName(className));
}
}
return classes;
}
/**
* Method used to find all classes in a given directory.
*
* @param directory the directory
* @param packageName the package name for classes found inside the directory
* @param scanSubPackages whether to scan the subpackages in the given package
* @return the classes, or an empty {@link Collection} if none were found
* @throws ClassNotFoundException
*/
protected Collection<Class<?>> findClassesInDirectory(File directory, String packageName, boolean scanSubPackages) throws ClassNotFoundException {
log.debug("Scanning directory '{}' for classes in package '{}'", directory, packageName);
Collection<Class<?>> classes = new ArrayList<>();
if(!directory.exists()) {
return classes;
}
File[] files = directory.listFiles();
if (files != null) {
for(File file : files) {
if(file.isFile() && file.getName().endsWith(".class")) {
classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
} else if(file.isDirectory() && scanSubPackages) {
classes.addAll(this.findClassesInDirectory(file, packageName + "." + file.getName(), true));
}
}
}
return classes;
}
/**
* Gets a target execution {@link Object} for the given class to be used by a {@link ListenerObject} to execute its {@link Method}.
*
* @param clazz
* @return an {@link Object} that has the type 'clazz' and can be used as an execution target, or 'null' if no such {@link Object} can be found
*/
protected abstract Object getTargetObjectForClass(Class<?> clazz);
}