All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.avaje.ebeaninternal.server.util.ClassPathSearch Maven / Gradle / Ivy

package com.avaje.ebeaninternal.server.util;

import com.avaje.ebeaninternal.api.ClassPathSearchService;
import com.avaje.ebeaninternal.api.ClassUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.loader.jar.JarEntryData;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

/**
 * Can search the class path for classes using a ClassPathSearchMatcher. A
 * ClassPathSearch should only be used once in a single threaded manor. It is
 * not safe for multithreaded use.
 * 

* For example, used to find all the Entity beans and ScalarTypes for Ebean. *

*/ public class ClassPathSearch implements ClassPathSearchService { private static final Logger logger = LoggerFactory.getLogger(ClassPathSearch.class); private ClassLoader classLoader; private final List classPath = new ArrayList(); private ClassPathSearchFilter filter; private ClassPathSearchMatcher matcher; private final ArrayList> matchList = new ArrayList>(); private final HashSet jarHits = new HashSet(); private final HashSet packageHits = new HashSet(); private ClassPathReader classPathReader = new DefaultClassPathReader(); private final ArrayList scannedUris = new ArrayList(); public ClassPathSearch() { // Default Construct } @Override public void init(ClassLoader classLoader, ClassPathSearchFilter filter, ClassPathSearchMatcher matcher, String classPathReaderClassName) { this.classLoader = classLoader; this.filter = filter; this.matcher = matcher; initClassPaths(classPathReaderClassName); } private void initClassPaths(String classPathReaderCN) { try { if (classPathReaderCN != null) { // use a user defined classPathReader logger.info("Using [" + classPathReaderCN + "] to read the searchable class path"); classPathReader = (ClassPathReader) ClassUtil.newInstance(classPathReaderCN); } Object[] rawClassPaths = classPathReader.readPath(classLoader); if (rawClassPaths == null || rawClassPaths.length == 0) { logger.warn("ClassPath is EMPTY using ClassPathReader [" + classPathReader + "]"); return; } for (int i = 0; i < rawClassPaths.length; i++) { // check for a jarfile with a manifest classpath (e.g. maven surefire) List classPathFromManifest = getClassPathFromManifest(rawClassPaths[i]); if (classPathFromManifest.isEmpty()) { classPath.add(rawClassPaths[i]); } else { classPath.addAll(classPathFromManifest); } } if (rawClassPaths.length == 1) { // look to add an 'outer' jar when it contains a manifest classpath if (!classPath.contains(rawClassPaths[0])) { classPath.add(rawClassPaths[0]); } } if (logger.isDebugEnabled()) { for (Object entry : classPath) { logger.debug("Classpath Entry: {}", entry); } } } catch (Exception e) { throw new RuntimeException("Error trying to read the classpath entries", e); } } /** * Return the set of jars that contained classes that matched. */ @Override public Set getJarHits() { return jarHits; } /** * Return the set of packages that contained classes that matched. */ @Override public Set getPackageHits() { return packageHits; } /** * Register where matching classes where found. *

* Could use this info to speed up future searches. *

*/ private void registerHit(String jarFileName, Class cls) { if (jarFileName != null) { jarHits.add(jarFileName); } Package pkg = cls.getPackage(); if (pkg != null) { packageHits.add(pkg.getName()); } else { packageHits.add(""); } } /** * Searches the class path for all matching classes. */ @Override public List> findClasses() throws IOException { if (classPath.isEmpty()) { // returning an empty list return matchList; } int classPathSize = classPath.size(); for (int i = 0; i < classPathSize; i++) { ClassPathElement element = getClassPathElement(classPath.get(i)); if (element.isDirectory()) { scanDirectory(element); } else if (element.isJarOrWar()) { // search name including the ! offset if it is there if (classPathSize == 1 || filter.isSearchJar(element.getJarNameWithOffset(), element.getJarOffset())) { scanJar(element); } } else { logger.error("Error: expected classPath entry [" + element + "] to be a directory or a .jar file but it is not either of those?"); } } if (matchList.isEmpty()) { logger.warn("No Entities found in ClassPath using ClassPathReader [" + classPathReader + "] Classpath Searched[" + classPath + "]"); } return matchList; } private ClassPathElement getClassPathElement(Object classPathEntry) throws MalformedURLException { URL fileUrl; if (URI.class.isInstance(classPathEntry)) { fileUrl = ((URI) classPathEntry).toURL(); } else if (!URL.class.isInstance(classPathEntry)) { // assumed to be a file path return new ClassPathElement(classPathEntry.toString()); } else { fileUrl = (URL) classPathEntry; } if (!fileUrl.getPath().contains("!")) { return new ClassPathElement(new File(fileUrl.getFile())); } // jar:file:..../file.war!/WEB-INF/classes typically String[] parts = fileUrl.getPath().split("!"); String fileName = parts[0]; String jarOffset = parts[1]; if (fileName.startsWith("file:")) { fileName = fileName.substring("file:".length()); } return new ClassPathElement(new File(fileName), jarOffset); } private void scanDirectory(ClassPathElement classPathEntry) { scanDirectory(classPathEntry.classPath); } private void scanDirectory(File directory) { List directoryFiles = getDirectoryFiles(directory); searchFiles(Collections.enumeration(directoryFiles), null, null, null); } private void scanUri(URI uri) throws IOException { if (uri.getScheme().equals("file") && scannedUris.add(uri)) { File file = new File(uri); if (file.exists()) { if (file.isDirectory()) { scanDirectory(file); } else { scanJar(new ClassPathElement(file)); } } } } private void scanJar(ClassPathElement classPathEntry) throws IOException { JarFile module = null; try { // our resource is a jar File file = classPathEntry.classPath; module = new JarFile(file); logger.trace("scanJar file:{}", file); List classPathFromManifest = getClassPathFromManifest(file, module.getManifest()); for (URI uri : classPathFromManifest) { scanUri(uri); } searchFiles(module.entries(), classPathEntry.getJarName(), classPathEntry.jarOffset, file); } catch (MalformedURLException ex) { throw new IOException("Bad classpath error: ", ex); } finally { if (module != null) { try { // close the jar if it was used module.close(); } catch (IOException e) { logger.error("Error closing jar", e); } } } } private List getDirectoryFiles(File classPath) { // list of file names (latter checked as Classes) ArrayList fileNameList = new ArrayList(); Set includePkgs = filter.getIncludePackages(); if (includePkgs.size() > 0) { // just search the relevant directories based on the // list of included packages for (String pkg : includePkgs) { String relativePath = pkg.replace('.', '/'); File dir = new File(classPath, relativePath); if (dir.exists()) { recursivelyListDir(fileNameList, dir, new StringBuilder(relativePath)); } } } else { // get a recursive listing of this classPath recursivelyListDir(fileNameList, classPath, new StringBuilder()); } return fileNameList; } /** * Searches through the Java Archive (jar or war file) looking for classes * that match our requirements. * @param entries - all of the entries in the Java Archive, this is an enumeration * provided by the Jar file * @param jarFileName - the name of the java archive * @param jarOffset - an offset inside the archive to chop off the name of the class - * this is used when we have bang path offsets (e.g. * @param module the containing jar/war file (used for spring boot embedded jar scanning) */ private void searchFiles(Enumeration entries, String jarFileName, String jarOffset, File module) { if (entries == null) { return; } logger.debug("searchFiles jarFileName:{} jarOffset:{}", jarFileName, jarOffset); // Strips the first character off as all entries in a jar file have no / // prefix. We want to come out with a name like WEB-INF/classes/ to ensure // we filter the contents of the war/jar file by this. if ("/".equals(jarOffset)) { // root level for runnable jar (spring boot etc) jarOffset = null; } else if (jarOffset != null) { if (jarOffset.startsWith("/")) { jarOffset = jarOffset.substring(1); } if (!jarOffset.endsWith("/")) { jarOffset += "/"; } } while (entries.hasMoreElements()) { Object element = entries.nextElement(); String entryName = element.toString(); if (isEntryEmbeddedJar(module, entryName)) { scanSpringBootEmbeddedJar(jarFileName, module, entryName); } if (isEntryClass(jarOffset, entryName)) { // check if it an 'interesting' class - entity etc registerScannedClass(jarFileName, jarOffset, entryName); } } } /** * Return true if this is an embedded jar that should be scanned. */ private boolean isEntryEmbeddedJar(File module, String entryName) { return entryName.endsWith(".jar") && module != null && filter.isSearchJar(entryName, null); } /** * Return true if this is a class that should be checked (for entity, interesting interface etc). */ private boolean isEntryClass(String jarOffset, String entryName) { return entryName.endsWith(".class") && (jarOffset == null || entryName.startsWith(jarOffset)); } private void scanSpringBootEmbeddedJar(String jarFileName, File module, String fileName) { // spring boot embedded jar logger.debug("spring boot embedded:{} : module:{}", fileName, module.getAbsoluteFile()); try { org.springframework.boot.loader.jar.JarFile jarFile = new org.springframework.boot.loader.jar.JarFile(module); org.springframework.boot.loader.jar.JarFile jarEntryFile = jarFile.getNestedJarFile(jarFile.getJarEntryData(fileName)); Iterator iterator = jarEntryFile.iterator(); while (iterator.hasNext()) { JarEntryData jarEntryData = iterator.next(); if (jarEntryData.getName().toString().endsWith(".class")) { logger.debug("... spring boot class entry:{}", jarEntryData.getName().toString()); registerScannedClass(jarFileName, null, jarEntryData.getName().toString()); } } } catch (IOException e) { throw new RuntimeException(e); } } private void registerScannedClass(String jarFileName, String jarOffset, String fileName) { if (jarOffset != null) { // we got through here only if there is an offset and we // matched it, so strip it off the file // as we are trying to find the className fileName = fileName.substring(jarOffset.length()); } String className = fileName.replace('/', '.').substring(0, fileName.length() - 6); int lastPeriod = className.lastIndexOf("."); String pckgName; if (lastPeriod > 0) { pckgName = className.substring(0, lastPeriod); } else { pckgName = ""; } if (filter.isSearchPackage(pckgName)) { // get the class for our class name try { Class theClass = Class.forName(className, false, classLoader); if (matcher.isMatch(theClass)) { matchList.add(theClass); registerHit(jarFileName, theClass); } } catch (ClassNotFoundException e) { // expected to get this hence trace logger.trace("Error searching classpath" + e.getMessage()); } catch (NoClassDefFoundError e) { // expected to get this hence trace logger.trace("Error searching classpath" + e.getMessage()); } } } private void recursivelyListDir(List fileNameList, File dir, StringBuilder relativePath) { if (!dir.isDirectory()) { // add class fileName to the list fileNameList.add(relativePath.toString()); } else { File[] files = dir.listFiles(); for (int i = 0; i < files.length; i++) { // store our original relative path string length int prevLen = relativePath.length(); relativePath.append(prevLen == 0 ? "" : "/").append(files[i].getName()); recursivelyListDir(fileNameList, files[i], relativePath); // delete sub directory from our relative path relativePath.delete(prevLen, relativePath.length()); } } } /** * If URL and actually a jarfile with manifest return the derived classpath. */ private static List getClassPathFromManifest(Object classPathElement) { try { if (classPathElement instanceof URL) { File file = new File(((URL) classPathElement).getFile()); if (file.isDirectory()) { return Collections.emptyList(); } JarFile jarFile = new JarFile(file); try { return getClassPathFromManifest(file, jarFile.getManifest()); } finally { jarFile.close(); } } return Collections.emptyList(); } catch (IOException e) { return Collections.emptyList(); } } /** * If a jarfile with a manifest classpath return that. */ private static List getClassPathFromManifest(File jarFile, Manifest manifest) { if (manifest == null) { return Collections.emptyList(); } List list = new ArrayList(); String classpathAttribute = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH.toString()); if (classpathAttribute != null) { String[] split = classpathAttribute.split(" "); for (String path : split) { try { path = path.trim(); if (path.length() > 0) { URI uri = getClassPathEntry(jarFile, path); list.add(uri); } } catch (URISyntaxException e) { // Ignore bad entry logger.warn("Invalid Class-Path entry: " + path); } } } return list; } private static URI getClassPathEntry(File jarFile, String path) throws URISyntaxException { URI uri = new URI(path); if (uri.isAbsolute()) { return uri; } else { return new File(jarFile.getParentFile(), path.replace('/', File.separatorChar)).toURI(); } } private static File decodePath(File classPath) { try { String charsetName = Charset.defaultCharset().name(); // URL Decode the path replacing %20 to space characters. String path = URLDecoder.decode(classPath.getAbsolutePath(), charsetName); return new File(path); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } /** * Element that has both underlying file and ! jarOffset. */ private static class ClassPathElement { private final File classPath; private final String jarOffset; ClassPathElement(String path) { this(new File(path)); } ClassPathElement(File file) { this(file, null); } ClassPathElement(File file, String jarOffset) { classPath = decodePath(file); this.jarOffset = jarOffset; } public String toString() { return classPath.getAbsolutePath(); } boolean isDirectory() { return classPath.isDirectory(); } boolean isJarOrWar() { return classPath.getName().endsWith(".jar") || classPath.getName().endsWith(".war"); } String getJarName() { return classPath.getName(); } String getJarNameWithOffset() { return (jarOffset == null) ? classPath.getName() : classPath.getName() + "!" + jarOffset; } String getJarOffset() { return jarOffset; } } }