/* * Copyright (c) 2016 OBiBa. All rights reserved. * * This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.obiba.spring; import java.lang.annotation.Annotation; import java.util.Collection; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.FatalBeanException; import org.springframework.beans.factory.FactoryBean; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.util.StringUtils; /** * <p>This class is for automatic searching annotated classes (i.e. Hibernate entity classes with <code>Entity</code> * annotation). It is mostly for use with Hibernate's <code>SessionFactory</code> in the Spring application context, * but can be used to find classes that match any annotations. This code is based on William Mo's * <code>EntityBeanFinderFactoryBean</code>. * </p> * <p>Example bean definition: * <pre> * <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"> * <property name="dataSource"> * <ref bean="dataSource"/> * </property> * <property name="annotatedClasses"> * <bean class="org.obiba.onyx.spring.AnnotatedBeanFinderFactoryBean"> * <!-- Use Apache Ant Pattern --> * <property name="searchPatterns"> * <set> * <value>classpath*:org/obiba/**/*/*.class</value> * <value>**/foo-core-*.jar</value> * </set> * </property> * * <!-- Use Java regular expression to find all domain classes, default is .* --> * <property name="qualifiedClassNamePatterns"> * <set> * <value>^org\.obiba\..*\.domain\..*</value> * </set> * </property> * * <!-- Specify annotations to look for in classes --> * <property name="annotationClasses"> * <set> * <value>javax.persistence.Entity</value> * <value>javax.persistence.Embeddable</value> * <value>javax.persistence.MappedSuperclass</value> * </set> * </property> * * <value>test.package.Foo</value> * </bean> * </property> * <property name="annotatedPackages"> * <list> * <value>test.package</value> * </list> * </property> * </bean> * </pre> * </p> * <p>This class is for automatic searching annotated entity classes (i.e. classes with <code>Entity</code> annotation) * for Hibernate's <code>SessionFactory</code> in the Spring application context.</p> * * @author William Mo * @author Oren E. Livne * @version added supported for other Hibernate annotations, Jan 29, 2008 * @see http://forum.springframework.org/showthread.php?t=46630 */ @SuppressWarnings("UnusedDeclaration") public class AnnotatedBeanFinderFactoryBean implements ResourceLoaderAware, FactoryBean<Object> { /** * A logger that helps identify this class' printouts. */ private static final Logger log = LoggerFactory.getLogger(AnnotatedBeanFinderFactoryBean.class); /** * Resource resolver. */ private ResourcePatternResolver resolver; /** * A collection of resource search patterns. */ private final Set<String> searchPatterns = new HashSet<String>(); /** * A collection of qualified class name patterns to find in the selected resources. */ private final Set<String> qualifiedClassNamePatterns = new HashSet<String>(); /** * List of annotation types to match in classes. */ private final Set<Class<? extends Annotation>> annotationClasses = new HashSet<Class<? extends Annotation>>(); /** * The output set of annotated classes. */ private final Collection<Class<?>> annotatedClasses = new HashSet<Class<?>>(); public AnnotatedBeanFinderFactoryBean() { // default accepted class name pattern is all qualifiedClassNamePatterns.add(".*"); } /** * Inject the resource loader. * * @param resourceLoader * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) */ @Override public void setResourceLoader(ResourceLoader resourceLoader) { resolver = (ResourcePatternResolver) resourceLoader; } /** * Return an instance (possibly shared or independent) of the object managed by this factory. * <p> * As with a BeanFactory, this allows support for both the Singleton and Prototype design pattern. * </p> * @return instance of the object managed by this factory * @throws Exception in case of creation errors * @see org.springframework.beans.factory.FactoryBean#getObject() */ @Override public Object getObject() throws Exception { if(annotatedClasses.isEmpty()) { searchAnnotatedEntityClasses(); } return annotatedClasses; } /** * Return the type of product made by this factory. In this cases, a class. * * @return he type of object that this FactoryBean creates, in this case, a class * @see org.springframework.beans.factory.FactoryBean#getObjectType() */ @Override public Class<?> getObjectType() { return annotatedClasses.getClass(); } /** * Indicates that this bean is a singleton. * * @return true * @see org.springframework.beans.factory.FactoryBean#isSingleton() */ @Override public boolean isSingleton() { return true; } /** * The main method that searches for annotated classes in classpath resources. */ private void searchAnnotatedEntityClasses() { // Search resources by every search pattern. log.info("searchAnnotatedEntityClasses in {}", searchPatterns); for(String searchPattern : searchPatterns) { try { Resource[] resources = resolver.getResources(searchPattern); if(resources != null) { // Parse every resource. for(Resource res : resources) { dealWithResource(res); } } } catch(Exception ignore) { log.warn("Resource resolving failed", ignore); } } if(log.isInfoEnabled()) { log.info("Annotations to look for: {}", annotationClasses); log.info("Annotated classes found: {}", annotatedClasses); } } private void dealWithResource(Resource res) throws Exception { String path = res.getURL().getPath(); // Path name string should not be empty. if(!"".equals(path)) { if(path.endsWith(".class")) { dealWithClasses(path); } else if(path.endsWith(".jar")) { dealWithJars(res); } } } /** * @param path */ private void dealWithClasses(String path) { Set<String> qClassNames = listAllPossibleQualifiedClassNames(path); for(String qName : qClassNames) { // Apply the qualified class name pattern to improve the searching // performance. if(matchQualifiedClassNamePatterns(qName)) { addPossibleClasses(qName); } } } /** * @param qName */ private void addPossibleClasses(String qName) { Class<?> clazz; try { clazz = Class.forName(qName); // Add the class to the annotatedEntityClasses property. if(checkEntityAnnotation(clazz)) { annotatedClasses.add(clazz); } } catch(Exception ignore) { } catch(NoClassDefFoundError ignore) { } } /** * @param res * @throws Exception */ private void dealWithJars(Resource res) throws Exception { JarFile jarFile = null; try { jarFile = new JarFile(res.getFile()); // Enumerate all entries in this JAR file. Enumeration<JarEntry> jarEntries = jarFile.entries(); while(jarEntries.hasMoreElements()) { String name = jarEntries.nextElement().getName(); // If the entry is a class, deal with it. if(name.endsWith(".class") && !"".equals(name)) { // Format the path first. name = pathToQualifiedClassName(name); // Apply the qualified class name pattern to improve the // searching performance. if(matchQualifiedClassNamePatterns(name)) // This is the qualified class name, so add it. addPossibleClasses(name); } } } finally { if(jarFile != null) jarFile.close(); } } /** * @param classPath * @return */ private Set<String> listAllPossibleQualifiedClassNames(String classPath) { Set<String> qualifiedClassNames = new HashSet<String>(); // Format the path first. String path = pathToQualifiedClassName(classPath); // Split the QName by the dot (i.e. '.') character. String[] pathParts = path.split("\\."); // Add the path parts one by one from the end of the array to the // beginning. StringBuilder qName = new StringBuilder(); for(int i = pathParts.length - 1; i >= 0; i--) { qName.insert(0, pathParts[i]); qualifiedClassNames.add(qName.toString()); qName.insert(0, "."); } return qualifiedClassNames; } /** * @param path * @return */ @Nonnull private String pathToQualifiedClassName(@Nonnull String path) { return path.replaceAll("/", ".").replaceAll("\\\\", ".").substring(0, path.length() - ".class".length()); } /** * Match a path against a set of qualified class name patterns. * * @param path path to match * @return result of matching */ private boolean matchQualifiedClassNamePatterns(String path) { for(String pattern : qualifiedClassNamePatterns) { if(path.matches(pattern)) { return true; } } return false; } /** * Check whether the class implements at least one of the specified annotation types. * * @param clazz class to check * @return <code>true</code> if and only if the class implements at least one of the specified annotation types */ private boolean checkEntityAnnotation(Class<?> clazz) { for(Class<? extends Annotation> annotationClass : annotationClasses) { if(clazz.getAnnotation(annotationClass) != null) { if(log.isDebugEnabled()) { log.debug("Found class {} annotation @{}", clazz.getSimpleName(), annotationClass.getSimpleName()); } return true; } } return false; } /** * Clean and trim a string read from the context file. * * @param string string to clean * @return cleaned string */ private String cleanString(String string) { return StringUtils.trimAllWhitespace(string.replaceAll("[\t\n]", "")); } /** * Returns the set of resource search patterns. * * @return the set of resource search patterns */ public Set<String> getSearchPatterns() { return searchPatterns; } /** * Returns the the set of qualified class name patterns. * * @return the the set of qualified class name patterns */ public Set<String> getQualifiedClassNamePatterns() { return qualifiedClassNamePatterns; } /** * Returns the the set of annotation types. * * @return the the set of annotation types */ public Set<Class<? extends Annotation>> getAnnotationClasses() { return annotationClasses; } /** * Inject the set of resource search patterns. * * @param searchPatterns the set of resource search patterns to set */ public void setSearchPatterns(Iterable<String> searchPatterns) { // Regular expression are sensitive with special characters. for(String pattern : searchPatterns) { this.searchPatterns.add(cleanString(pattern)); } } /** * Inject the set of qualified class name patterns. * * @param qualifiedClassNamePatterns the set of qualified class name patterns to set */ public void setQualifiedClassNamePatterns(Collection<String> qualifiedClassNamePatterns) { // Regular expression are sensitive with special characters. // Clear default value if(qualifiedClassNamePatterns.size() > 0) this.qualifiedClassNamePatterns.clear(); for(String pattern : qualifiedClassNamePatterns) { this.qualifiedClassNamePatterns.add(cleanString(pattern)); } } /** * Injects the set of annotation types. * * @param annotationClasses the set of qualified annotation class names to set. */ @SuppressWarnings("unchecked") public void setAnnotationClasses(Iterable<String> annotationClasses) { // Filter tabs and new lines for(String annotationClass : annotationClasses) { Class<? extends Annotation> clazz; try { clazz = (Class<? extends Annotation>) Class.forName(cleanString(annotationClass)); this.annotationClasses.add(clazz); } catch(NoClassDefFoundError ignore) { throw new FatalBeanException("The class " + annotationClass + " in the annotatedClasses property of the sessionFactory declaration is not an annotation type."); } catch(ClassCastException e) { throw new FatalBeanException("Could not find annotation class " + annotationClass + " in the annotatedClasses property of the sessionFactory declaration."); } catch(Throwable throwable) { throw new FatalBeanException("Could not add annotation class " + annotationClass + " to the list of annotations in the annotatedClasses property of the sessionFactory declaration: " + throwable); } } } }