package org.grails.plugins.jpa.util import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.core.io.support.PathMatchingResourcePatternResolver import javax.persistence.Entity import grails.util.GrailsUtil /** * Scan for EntityBeans in classpath. Depends on Spring and JPA. Design for thread-safety to allow multiple scanner be * run concurrently. * * The following are constructor arguments: * * includes - a single or a list of class(es) to be included. It supports '*' and '**'. e.g. * new EntityBeanScanner(includes:['foo.Bar']) * new EntityBeanScanner(includes:['foo.*']), covers: foo.Bar but not foo.bar.Bar * new EntityBeanScanner(includes:['foo.**']), covers: foo.bar.Bar * new EntityBeanScanner().with\{ it.setTargets(['foo.Bar']\}* * * excludes - a single or a list of String to override the includes list. Unlike includes, each String is a evaluated * as regular expression to match against full class name without '.class', e.g. foo.bar.Bar * * baseClass - the class that will be used to determine the base class path directory in the file system. For example, * in Intellij IDEA, the test cases are compiled to a different output directory than the source classes, * and the JPA Plugin library may be from another class path directory. * You'll need to set the baseClass to any class in the same output directory as your entity classes * * classLoader - the classloader to for classpath scanning. It is passed to the Spring PathMatchingResourcePatternResolver * * innerClass - a boolean that determine if innerClass with @Entity should be loaded. It is false by default, and it is * not tested if it will work when set to 'true'. Please report if you find it is or isn't working. */ public class EntityBeanScanner { static Logger logger = LoggerFactory.getLogger(EntityBeanScanner.class); private boolean scanned = false def includes, excludes, classLoader, results = [], sameClassPath = true, innerClass = false def basePath; Class baseClass; def EntityBeanScanner(Map cfg = null) { includes = cfg && cfg?.'includes' instanceof String ? [cfg.'includes'] : cfg?.'includes' excludes = cfg && cfg?.'excludes' instanceof String ? [cfg.'excludes'] : cfg?.'excludes' classLoader = cfg?.'classLoader' ?: this.class.classLoader baseClass = cfg.'baseClass' ?: this.class if (cfg?.containsKey('innerClass')) this.innerClass = cfg?.'innerClass' } /** * Scan the classpath, load every class that match the 'includes' criteria to prepare a list of Entity Beans */ EntityBeanScanner scan() { if (logger.isTraceEnabled()) logger.trace("scan() - includes: $includes, excludes: $excludes, classLoader: $classLoader") try { def resourceFinder = new PathMatchingResourcePatternResolver(this.classLoader) def scannerFullClassName = baseClass.name.replaceAll('\\.', '/') + '.class' def scannerResource = resourceFinder.getResource('classpath:/' + scannerFullClassName) //org.springframework.core.io.ClassPathResource basePath = scannerResource.file.absolutePath.with {it.substring(0, it.size() - scannerFullClassName.size())} def baseURL = scannerResource?.URL.toString().with {url -> url.substring(0, url.size() - scannerFullClassName.size())} if (logger.isTraceEnabled()) logger.trace("scan() - baseURL: $baseURL, scannerFullClassName: $scannerFullClassName, scannerResource.URL: ${scannerResource?.URL}") includes?.each {String target -> boolean isWildCard = target.indexOf('*') != -1, isDoubleWildCard = target.indexOf('**') != -1 def prefix = isWildCard ? 'classpath*:/' : 'classpath:/' def targetPath = prefix + target.replaceAll('\\.', '/') + ((isDoubleWildCard) ? '' : '.class') def targetResources = resourceFinder.getResources(targetPath) if (logger.isTraceEnabled()) logger.trace("scan() - processing - targetPath: ${targetPath}, targetResources.size(): ${targetResources?.size()}") targetResources?.eachWithIndex {resource, i -> def url = resource.URL.toString(), isClass = url.endsWith('.class') if (this.'sameClassPath' && !url.startsWith(baseURL)) return; if (this.'innerClass' && url.contains('$')) return; if (url.endsWith('/')) return; // package is not processed //TODO see if it can support scanning outside the classpath that load this Scanner if (logger.isTraceEnabled()) logger.trace("scan() - $i - url: $url, baseURL: ${baseURL}") String entityClass = url.substring(baseURL.toString().size(), url.size() - '.class'.size()).replaceAll('/', '.') if (this.'excludes' && this.'excludes'.any {(entityClass =~ it).matches()}) return; if (logger.isTraceEnabled()) logger.trace("scan() - - loading class - class: $entityClass") //TODO review if it is a good idea to load every Entity Class Class clazz try {clazz = Class.forName(entityClass)} catch (e) {logger.error("scan() - - fail to load class: ${entityClass}")} if (clazz?.isAnnotationPresent(Entity.class)) { if (logger.isTraceEnabled()) logger.trace("scan() - - matched - entityClass: $entityClass, url: ${url}, i: $i") this.results << entityClass } } } if (logger.isDebugEnabled()) logger.debug("scan() - found ${results.size()} entity classes - includes: $includes, excludes: $excludes, innerClass: $innerClass") return this; } catch (e) { //logger.error("scan() - e: ${e.message}", e) logger.error("scan() - error - e: ${e.message}, classloader: ${this.class.classLoader}") throw GrailsUtil.sanitize(e) } } List getClassList() { return results; } }