View Javadoc
1   /*
2    * Copyright (C) 2012-2024 RRiBbit.org
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.rribbit.creation;
17  
18  import org.rribbit.Listener;
19  import org.rribbit.ListenerObject;
20  import org.slf4j.Logger;
21  import org.slf4j.LoggerFactory;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.lang.reflect.Method;
26  import java.net.URL;
27  import java.net.URLDecoder;
28  import java.nio.charset.StandardCharsets;
29  import java.util.ArrayList;
30  import java.util.Collection;
31  import java.util.Enumeration;
32  import java.util.concurrent.CopyOnWriteArrayList;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  import java.util.zip.ZipEntry;
36  import java.util.zip.ZipFile;
37  
38  /**
39   * 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
40   * 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
41   * 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
42   * superclass/superinterface separately to this {@link ListenerObjectCreator}.
43   * <p />
44   * 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
45   * 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.
46   * <p />
47   * 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.
48   *
49   * @author G.J. Schouten
50   *
51   */
52  public abstract class AbstractClassBasedListenerObjectCreator extends ObjectBasedListenerObjectCreator {
53  
54  	private static final Logger log = LoggerFactory.getLogger(AbstractClassBasedListenerObjectCreator.class);
55  
56  	protected Collection<Class<?>> excludedClasses;
57  
58  	/**
59  	 * Calls {@link #addClass(Class)} on each given class.
60  	 *
61  	 * @param classes
62  	 */
63  	public AbstractClassBasedListenerObjectCreator(Class<?>... classes) {
64  
65  		excludedClasses = new CopyOnWriteArrayList<>();
66  		for(Class<?> clazz : classes) {
67  			this.addClass(clazz);
68  		}
69  	}
70  
71  	/**
72  	 * Calls {@link #addPackage(String, boolean)} on each given package name.
73  	 *
74  	 * @param excludedClasses	the classes to be excluded from scanning, can be null if not needed
75  	 * @param scanSubPackages	whether to scan the subpackages in the given packages
76  	 * @param packageNames		the packages to be scanned
77  	 */
78  	public AbstractClassBasedListenerObjectCreator(Collection<Class<?>> excludedClasses, boolean scanSubPackages, String... packageNames) {
79  
80  		this.excludedClasses = new CopyOnWriteArrayList<>();
81  		if(excludedClasses != null) {
82  			this.excludedClasses.addAll(excludedClasses);
83  		}
84  
85  		for(String packageName : packageNames) {
86  			this.addPackage(packageName, scanSubPackages);
87  		}
88  	}
89  
90  	/**
91  	 * 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)},
92  	 * excluding it beforehand has NO effect. Excluded classes are only for package scanning and have NO effect on manual class scanning.
93  	 *
94  	 * @param excludedClass
95  	 */
96  	public void excludeClass(Class<?> excludedClass) {
97  		excludedClasses.add(excludedClass);
98  	}
99  
100 	/**
101 	 * 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
102 	 * {@link #getTargetObjectForClass(Class)} can provide a suitable execution target. Otherwise, the {@link Class} is ignored. The created {@link ListenerObject}s are added
103 	 * to the {@link Collection} of {@link ListenerObject}s that this instance keeps.
104 	 *
105 	 * @param clazz
106 	 */
107 	public void addClass(Class<?> clazz) {
108 
109 		log.debug("Processing class '{}'", clazz.getName());
110 		Object targetObject = this.getTargetObjectForClass(clazz);
111 		if(targetObject != null) {
112 			log.debug("Found target object for class '{}', getting Listener methods", clazz.getName());
113 			Collection<ListenerObject> incompleteListenerObjects = this.getIncompleteListenerObjectsFromClass(clazz);
114 			for(ListenerObject listenerObject : incompleteListenerObjects) {
115 				listenerObject.setTarget(targetObject);
116 			}
117 			listenerObjects.addAll(incompleteListenerObjects);
118 			this.notifyObserversOnClassAdded(clazz);
119 		}
120 	}
121 
122 	/**
123 	 * 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.
124 	 *
125 	 * @param packageName		the package to be scanned
126 	 * @param scanSubPackages	whether to scan the subpackages in the given package
127 	 */
128 	public void addPackage(String packageName, boolean scanSubPackages) {
129 
130 		log.debug("Scanning package '{}' for classes", packageName);
131 		Collection<Class<?>> classes;
132 		try {
133 			classes = this.getClasses(packageName, scanSubPackages);
134 		} catch(Exception e) {
135 			throw new RuntimeException("Error during reading of package '" + packageName + "'", e);
136 		}
137 		for(Class<?> clazz : classes) {
138 			if(!excludedClasses.contains(clazz)) {
139 				this.addClass(clazz);
140 			}
141 		}
142 	}
143 
144 	/**
145 	 * 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}.
146 	 * The default is the Context {@link ClassLoader}.
147 	 */
148 	protected ClassLoader getClassLoader() {
149 		return Thread.currentThread().getContextClassLoader();
150 	}
151 
152 	/**
153 	 * Scans all classes accessible in the given package, using the {@link ClassLoader} associated with this {@link AbstractClassBasedListenerObjectCreator}.
154 	 *
155 	 * @param packageName		the package
156 	 * @param scanSubPackages	whether to scan the subpackages in the given package
157 	 * @return					the classes, or an empty {@link Collection} if none were found
158 	 * @throws ClassNotFoundException
159 	 * @throws IOException
160 	 */
161 	protected Collection<Class<?>> getClasses(String packageName, boolean scanSubPackages) throws ClassNotFoundException, IOException {
162 
163 		String path = packageName.replace('.', '/');
164 		Enumeration<URL> resources = this.getClassLoader().getResources(path);
165 
166 		Collection<Class<?>> classes = new ArrayList<>();
167 		while(resources.hasMoreElements()) {
168 			URL resource = resources.nextElement();
169 			String filename = URLDecoder.decode(resource.getFile(), StandardCharsets.UTF_8);
170 			log.debug("Processing resource '{}' with filename '{}'", resource, filename);
171 
172 			if(resource.getProtocol().equals("jar") || resource.getProtocol().equals("zip")) {
173 				String zipFilename = filename.substring(5, filename.indexOf("!"));
174 				try (ZipFile zipFile = new ZipFile(zipFilename)) {
175 					classes.addAll(this.findClassesInZipFile(zipFile, path, scanSubPackages));
176 				}
177 			} else {
178 				File directory = new File(filename);
179 				classes.addAll(this.findClassesInDirectory(directory, packageName, scanSubPackages));
180 			}
181 		}
182 		return classes;
183 	}
184 
185 	/**
186 	 * Method used to find all classes in a given zipFile.
187 	 *
188 	 * @param zipFile			the zipFile
189 	 * @param path				the package name in path format for classes found inside the zipFile
190 	 * @param scanSubPackages	whether to scan the subpackages in the given path
191 	 * @return					the classes, or an empty {@link Collection} if none were found
192 	 * @throws ClassNotFoundException
193 	 */
194 	protected Collection<Class<?>> findClassesInZipFile(ZipFile zipFile, String path, boolean scanSubPackages) throws ClassNotFoundException {
195 
196 		log.debug("Scanning zipFile '{}' for classes in package '{}'", zipFile.getName(), path);
197 		Collection<Class<?>> classes = new ArrayList<>();
198 		Enumeration<ZipEntry> zipEntries = (Enumeration<ZipEntry>) zipFile.entries();
199 		while(zipEntries.hasMoreElements()) {
200 			String entryName = zipEntries.nextElement().getName();
201 			log.debug("Entry name: '{}'", entryName);
202 			Pattern p;
203 			if(scanSubPackages) {
204 				p = Pattern.compile("(" + path + "/[\\w/]+)\\.class");
205 			} else {
206 				p = Pattern.compile("(" + path + "/\\w+)\\.class");
207 			}
208 			Matcher m = p.matcher(entryName);
209 			if(m.matches()) {
210 				String className = m.group(1).replaceAll("/", ".");
211 				log.debug("Adding Class {}", className);
212 				classes.add(Class.forName(className));
213 			}
214 		}
215 		return classes;
216 	}
217 
218 	/**
219 	 * Method used to find all classes in a given directory.
220 	 *
221 	 * @param directory			the directory
222 	 * @param packageName		the package name for classes found inside the directory
223 	 * @param scanSubPackages	whether to scan the subpackages in the given package
224 	 * @return					the classes, or an empty {@link Collection} if none were found
225 	 * @throws ClassNotFoundException
226 	 */
227 	protected Collection<Class<?>> findClassesInDirectory(File directory, String packageName, boolean scanSubPackages) throws ClassNotFoundException {
228 
229 		log.debug("Scanning directory '{}' for classes in package '{}'", directory, packageName);
230 		Collection<Class<?>> classes = new ArrayList<>();
231 		if(!directory.exists()) {
232 			return classes;
233 		}
234 		File[] files = directory.listFiles();
235 		if (files != null) {
236 			for(File file : files) {
237 				if(file.isFile() && file.getName().endsWith(".class")) {
238 					classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
239 				} else if(file.isDirectory() && scanSubPackages) {
240 					classes.addAll(this.findClassesInDirectory(file, packageName + "." + file.getName(), true));
241 				}
242 			}
243 		}
244 		return classes;
245 	}
246 
247 	/**
248 	 * Gets a target execution {@link Object} for the given class to be used by a {@link ListenerObject} to execute its {@link Method}.
249 	 *
250 	 * @param clazz
251 	 * @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
252 	 */
253 	protected abstract Object getTargetObjectForClass(Class<?> clazz);
254 }