From 0acabf820e5603d59233849cdf4c6e51ea0da087 Mon Sep 17 00:00:00 2001 From: Richard Vowles Date: Sat, 2 Nov 2013 20:07:35 +1300 Subject: [PATCH] Add support for bang paths --- pom.xml | 7 + .../server/core/XmlConfigLoader.java | 191 ++++-------------- .../server/util/ClassPathSearch.java | 60 +++++- .../server/util/ClassPathSearchFilter.java | 2 +- .../server/util/ClassPathSearchTests.java | 114 +++++++++++ .../server/util/SimpleJarBangClass.java | 9 + .../server/util/SimpleJarClass.java | 9 + .../unitinternal/TestXmlConfigLoader.java | 78 ++++++- 8 files changed, 309 insertions(+), 161 deletions(-) create mode 100644 src/test/java/com/avaje/ebeaninternal/server/util/ClassPathSearchTests.java create mode 100644 src/test/java/com/avaje/ebeaninternal/server/util/SimpleJarBangClass.java create mode 100644 src/test/java/com/avaje/ebeaninternal/server/util/SimpleJarClass.java diff --git a/pom.xml b/pom.xml index 6fa075129..501d6e3ef 100644 --- a/pom.xml +++ b/pom.xml @@ -147,6 +147,13 @@ test + + commons-io + commons-io + 2.4 + test + + diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/XmlConfigLoader.java b/src/main/java/com/avaje/ebeaninternal/server/core/XmlConfigLoader.java index 4f69efa59..be1ba7be0 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/XmlConfigLoader.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/XmlConfigLoader.java @@ -1,176 +1,71 @@ package com.avaje.ebeaninternal.server.core; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.URL; -import java.net.URLDecoder; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; -import java.util.jar.JarFile; -import java.util.zip.ZipEntry; - -import com.avaje.ebean.config.GlobalProperties; -import com.avaje.ebeaninternal.api.ClassUtil; import com.avaje.ebeaninternal.server.lib.util.Dnode; import com.avaje.ebeaninternal.server.lib.util.DnodeReader; -import com.avaje.ebeaninternal.server.util.ClassPathReader; -import com.avaje.ebeaninternal.server.util.DefaultClassPathReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + /** * Used to read the orm.xml and ebean-orm.xml configuration files. - * + * * @author rbygrave + * @author Richard Vowles - http://plus.google.com/RichardVowles */ public class XmlConfigLoader { - private static final Logger logger = LoggerFactory.getLogger(XmlConfigLoader.class); - - private final ClassPathReader classPathReader; + private static final Logger logger = LoggerFactory.getLogger(XmlConfigLoader.class); - private final Object[] classPaths; - - - public XmlConfigLoader(ClassLoader classLoader){ - - if (classLoader == null) { - classLoader = getClass().getClassLoader(); - } - - String cn = GlobalProperties.get("ebean.classpathreader", null); - if (cn != null){ - // use a user defined classPathReader - logger.info("Using ["+cn+"] to read the searchable class path"); - this.classPathReader = (ClassPathReader)ClassUtil.newInstance(cn, this.getClass()); - } else { - this.classPathReader = new DefaultClassPathReader(); - } - - this.classPaths = classPathReader.readPath(classLoader); - } - - public XmlConfig load() { - List ormXml = search("META-INF/orm.xml"); - List ebeanOrmXml = search("META-INF/ebean-orm.xml"); - - return new XmlConfig(ormXml, ebeanOrmXml); - } - - public List search(String searchFor) { - - ArrayList xmlList = new ArrayList(); + private final ClassLoader classLoader; - String charsetName = Charset.defaultCharset().name(); + public XmlConfigLoader(ClassLoader classLoader) { - for (int h = 0; h < classPaths.length; h++) { + if (classLoader == null) { + classLoader = getClass().getClassLoader(); + } - try { - // for each class path ... - File classPath; - if (URL.class.isInstance(classPaths[h])) { - classPath = new File(((URL) classPaths[h]).getFile()); - } else { - classPath = new File(classPaths[h].toString()); - } + this.classLoader = classLoader; + } - // URL Decode the path replacing %20 to space characters. - String path = URLDecoder.decode(classPath.getAbsolutePath(), charsetName); + public XmlConfig load() { + List ormXml = search("META-INF/orm.xml"); + List ebeanOrmXml = search("META-INF/ebean-orm.xml"); - classPath = new File(path); + return new XmlConfig(ormXml, ebeanOrmXml); + } - if (classPath.isDirectory()) { - checkDir(searchFor, xmlList, classPath); + public List search(String resourceName) { + ArrayList xmlList = new ArrayList(); - } else if (classPath.getName().endsWith(".jar") || classPath.getName().endsWith(".war") || classPath.getName().endsWith(".war!/WEB-INF/classes")) { - checkJar(searchFor, xmlList, classPath); - - } else { - // this is not expected - String msg = "Not a Jar or Directory? " + classPath.getAbsolutePath(); - logger.error(msg); - } + try { + Enumeration resources = classLoader.getResources(resourceName); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); - return xmlList; - - } + InputStream is = url.openStream(); + processInputStream(xmlList, is); + is.close(); + } + } catch (IOException e) { + logger.error("Unable to find resources {}", resourceName); + } - private void processInputStream(ArrayList xmlList, InputStream is) throws IOException { - - DnodeReader reader = new DnodeReader(); - Dnode xmlDoc = reader.parseXml(is); - is.close(); - - xmlList.add(xmlDoc); - } - - private void checkFile(String searchFor, ArrayList xmlList, File dir) throws IOException { + return xmlList; + } - File f = new File(dir, searchFor); - if (f.exists()){ - FileInputStream fis = new FileInputStream(f); - BufferedInputStream is = new BufferedInputStream(fis); - processInputStream(xmlList, is); - } - } - - private void checkDir(String searchFor, ArrayList xmlList, File dir) throws IOException { + private void processInputStream(ArrayList xmlList, InputStream is) throws IOException { - checkFile(searchFor, xmlList, dir); - - if (dir.getPath().endsWith("classes")) { - // see if this is part of webapp and look for META-INF/searchFor - // relative to the WEB-INF/classes directory - File parent = dir.getParentFile(); - if (parent != null && parent.getPath().endsWith("WEB-INF")){ - parent = parent.getParentFile(); - if (parent != null){ - File metaInf = new File(parent, "META-INF"); - if (metaInf.exists()){ - checkFile(searchFor, xmlList, metaInf); - } - } - } - } - } - - private void checkJar(String searchFor, ArrayList xmlList, File classPath) throws IOException { - - String fileName = classPath.getName(); - if (fileName.toLowerCase().startsWith("surefire")){ - return; - } - if (classPath.getAbsolutePath().endsWith(".war!/WEB-INF/classes")) { - classPath = new File(classPath.getAbsolutePath().substring(0, classPath.getAbsolutePath().lastIndexOf('!'))); - } - JarFile module = null; - try { - module = new JarFile(classPath); - ZipEntry entry = module.getEntry(searchFor); - if (entry != null){ - InputStream is = module.getInputStream(entry); - processInputStream(xmlList, is); - } - } catch (Exception e) { - logger.info("Unable to check jar file "+fileName+" for ebean-orm.xml"); - } finally { - if (module != null){ - module.close(); - } - } - } + DnodeReader reader = new DnodeReader(); + Dnode xmlDoc = reader.parseXml(is); + is.close(); - + xmlList.add(xmlDoc); + } } diff --git a/src/main/java/com/avaje/ebeaninternal/server/util/ClassPathSearch.java b/src/main/java/com/avaje/ebeaninternal/server/util/ClassPathSearch.java index 9a2e6ffb5..5513e1da3 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/util/ClassPathSearch.java +++ b/src/main/java/com/avaje/ebeaninternal/server/util/ClassPathSearch.java @@ -3,6 +3,7 @@ package com.avaje.ebeaninternal.server.util; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.lang.instrument.ClassDefinition; import java.net.MalformedURLException; import java.net.URL; import java.net.URLDecoder; @@ -44,6 +45,11 @@ public class ClassPathSearch { ArrayList> matchList = new ArrayList>(); + /** + * For class-reloading, forcing them through the transformer + */ + ArrayList matchDefinitions = new ArrayList(); + HashSet jarHits = new HashSet(); HashSet packageHits = new HashSet(); @@ -139,8 +145,20 @@ public class ClassPathSearch { // for each class path ... File classPath; + String jarOffset = null; // used for war files with bang paths, e.g. !/WEB-INF/classes if (URL.class.isInstance(classPaths[h])){ - classPath = new File(((URL)classPaths[h]).getFile()); + URL fileUrl = (URL)classPaths[h]; + if (fileUrl.getPath().contains("!")) { + String[] parts = fileUrl.getPath().split("!"); + if (parts[0].startsWith("file:")) { // jar:file:..../file.war!/WEB-INF/classes typically + classPath = new File(parts[0].substring("file:".length())); + } else { + classPath = new File(parts[0]); + } + jarOffset = parts[1]; + } else { + classPath = new File(fileUrl.getFile()); + } } else { classPath = new File(classPaths[h].toString()); } @@ -157,9 +175,11 @@ public class ClassPathSearch { if (classPath.isDirectory()) { files = getDirectoryEnumeration(classPath); - } else if (classPath.getName().endsWith(".jar")) { + } else if (classPath.getName().endsWith(".jar") || classPath.getName().endsWith(".war")) { jarFileName = classPath.getName(); - if (!filter.isSearchJar(jarFileName)) { + + // search name needs to include the offset if it is there, in case it contains interesting info + if (!filter.isSearchJar(jarFileName + ((jarOffset == null) ? "" : ("!" + jarOffset)))) { // skip any jars not list in the filter continue; } @@ -183,7 +203,7 @@ public class ClassPathSearch { logger.error(msg); } - searchFiles(files, jarFileName); + searchFiles(files, jarFileName, jarOffset); if (module != null) { try { @@ -232,14 +252,41 @@ public class ClassPathSearch { return Collections.enumeration(fileNameList); } - private void searchFiles(Enumeration files, String jarFileName) { + /** + * Searches through the Java Archive (jar or war file) looking for classes that match our requirements. + * + * @param files - all of the files 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. file:///myfile.war!/WEB-INF/classes) + */ + private void searchFiles(Enumeration files, String jarFileName, String 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 (jarOffset != null) { + if (jarOffset.startsWith("/")) { + jarOffset = jarOffset.substring(1); + } + + if (!jarOffset.endsWith("/")) { + jarOffset += "/"; + } + } while (files != null && files.hasMoreElements()) { String fileName = files.nextElement().toString(); // we only want the class files - if (fileName.endsWith(".class")) { + if (fileName.endsWith(".class") && (jarOffset == null || fileName.startsWith(jarOffset))) { + + 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("."); @@ -261,6 +308,7 @@ public class ClassPathSearch { theClass = Class.forName(className, false, classLoader); if (matcher.isMatch(theClass)) { + matchList.add(theClass); registerHit(jarFileName, theClass); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/util/ClassPathSearchFilter.java b/src/main/java/com/avaje/ebeaninternal/server/util/ClassPathSearchFilter.java index 45a7983c2..95c455203 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/util/ClassPathSearchFilter.java +++ b/src/main/java/com/avaje/ebeaninternal/server/util/ClassPathSearchFilter.java @@ -181,7 +181,7 @@ public class ClassPathSearchFilter { Iterator incIt = set.iterator(); while (incIt.hasNext()) { String val = incIt.next(); - if (match.startsWith(val)) { + if (match.contains(val)) { return true; } } diff --git a/src/test/java/com/avaje/ebeaninternal/server/util/ClassPathSearchTests.java b/src/test/java/com/avaje/ebeaninternal/server/util/ClassPathSearchTests.java new file mode 100644 index 000000000..7b167e653 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/util/ClassPathSearchTests.java @@ -0,0 +1,114 @@ +package com.avaje.ebeaninternal.server.util; + +import junit.framework.Assert; +import org.apache.commons.io.IOUtils; +import org.junit.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +/** + * This ensures that the ClassPathSearch supports normal file:file.jar files as well as jar/war files + * with bang paths. Bang paths typically look like this as a url: + * + * jar:file:/path/to/file.war!/WEB-INF/classes + * + * author: Richard Vowles - http://plus.google.com/RichardVowles + */ +public class ClassPathSearchTests { + + private static final String WEB_INF_CLASSES = "WEB-INF/classes/"; + + private void runAsserts(List> found, URLClassLoader cl, ClassPathSearch search, + URL jar, URL jarBang) throws ClassNotFoundException { + + assert found.size() == 2; + assert found.contains(cl.loadClass(SimpleJarBangClass.class.getName())); + assert found.contains(cl.loadClass(SimpleJarClass.class.getName())); + + assert search.getJarHits().contains(new File(jar.getFile()).getName()); + assert search.getJarHits().contains(new File(jarBang.getFile().split("!")[0]).getName()); + } + + @Test + public void ensureClassPathFindsClassesInBangPaths() throws IOException, ClassNotFoundException { + URL jar = setupJar(); + URL jarBang = setupJarBang(); + + URLClassLoader cl = new URLClassLoader(new URL[] { jar, jarBang}); + + ClassPathSearchFilter filter = new ClassPathSearchFilter(); + filter.includePackage("com.avaje.ebeaninternal.server"); + filter.includeJar("WEB-INF"); + + ClassPathSearch search = new ClassPathSearch(cl, filter, new ClassPathSearchMatcher() { + @Override + public boolean isMatch(Class cls) { + return true; + } + }); + + List> found = search.findClasses(); + + Assert.assertEquals(1, found.size()); + assert found.contains(cl.loadClass(SimpleJarBangClass.class.getName())); + + assert search.getJarHits().contains(new File(jarBang.getFile().split("!")[0]).getName()); + + filter = new ClassPathSearchFilter(); + filter.includePackage("com.avaje.ebeaninternal.server"); + filter.includeJar("bang"); + + search = new ClassPathSearch(cl, filter, new ClassPathSearchMatcher() { + @Override + public boolean isMatch(Class cls) { + return true; + } + }); + + runAsserts(search.findClasses(), cl, search, jar, jarBang); + } + + private URL createJar(Class clazz, File jarFile, String offset) throws IOException { + FileOutputStream stream = new FileOutputStream(jarFile); + JarOutputStream jarOutputStream = new JarOutputStream(stream); + String clazzPath = clazz.getPackage().getName().replace(".", "/") + "/" + clazz.getSimpleName() + ".class"; + JarEntry entry = new JarEntry(offset + clazzPath); + jarOutputStream.putNextEntry(entry); + InputStream classStream = getClass().getResourceAsStream("/" + clazzPath); + IOUtils.copy(classStream, jarOutputStream); + + if (offset != null) { // copy the same file in, with a different offset + entry = new JarEntry("random/" + clazzPath); + jarOutputStream.putNextEntry(entry); + classStream = getClass().getResourceAsStream("/" + clazzPath); + IOUtils.copy(classStream, jarOutputStream); + } + + jarOutputStream.close(); + stream.close(); + + if (offset.length() > 0) { + return new URL("jar:" + jarFile.toURI().toString() + "!/" + offset); + } else { + return jarFile.toURI().toURL(); + } + } + + private URL setupJarBang() throws IOException { + File jarFile = File.createTempFile("bang", ".war"); + return createJar(SimpleJarBangClass.class, jarFile, WEB_INF_CLASSES); + } + + private URL setupJar() throws IOException { + File jarFile = File.createTempFile("nobang", ".jar"); + return createJar(SimpleJarClass.class, jarFile, ""); + } +} diff --git a/src/test/java/com/avaje/ebeaninternal/server/util/SimpleJarBangClass.java b/src/test/java/com/avaje/ebeaninternal/server/util/SimpleJarBangClass.java new file mode 100644 index 000000000..70d2c9a9a --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/util/SimpleJarBangClass.java @@ -0,0 +1,9 @@ +package com.avaje.ebeaninternal.server.util; + +/** + * Exists only for including in the jar for the Class Path Search Tests. + * + * author: Richard Vowles - http://plus.google.com/RichardVowles + */ +public class SimpleJarBangClass { +} diff --git a/src/test/java/com/avaje/ebeaninternal/server/util/SimpleJarClass.java b/src/test/java/com/avaje/ebeaninternal/server/util/SimpleJarClass.java new file mode 100644 index 000000000..1e506cef5 --- /dev/null +++ b/src/test/java/com/avaje/ebeaninternal/server/util/SimpleJarClass.java @@ -0,0 +1,9 @@ +package com.avaje.ebeaninternal.server.util; + +/** + * Exists only for the purpose of putting in a jar for the ClassPathSearchTests. + * + * author: Richard Vowles - http://plus.google.com/RichardVowles + */ +public class SimpleJarClass { +} diff --git a/src/test/java/com/avaje/tests/unitinternal/TestXmlConfigLoader.java b/src/test/java/com/avaje/tests/unitinternal/TestXmlConfigLoader.java index fc2b8e5e1..0d9442c5c 100644 --- a/src/test/java/com/avaje/tests/unitinternal/TestXmlConfigLoader.java +++ b/src/test/java/com/avaje/tests/unitinternal/TestXmlConfigLoader.java @@ -1,14 +1,28 @@ package com.avaje.tests.unitinternal; -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; - import com.avaje.ebean.BaseTestCase; import com.avaje.ebeaninternal.server.core.XmlConfigLoader; import com.avaje.ebeaninternal.server.lib.util.Dnode; +import org.apache.commons.io.IOUtils; +import org.junit.Assert; +import org.junit.Test; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +/** + * Tests basic xml loading functionality across multiple jars and inside our own test structure. + * + * @author Rob Bygrave + * @author Richard Vowles - http://plus.google.com/RichardVowles + */ public class TestXmlConfigLoader extends BaseTestCase { @Test @@ -16,10 +30,62 @@ public class TestXmlConfigLoader extends BaseTestCase { XmlConfigLoader xmlConfigLoader = new XmlConfigLoader(null); - List ebeanOrmXml = xmlConfigLoader.search("META-INF/ebean-orm.xml"); + List ebeanOrmXml = xmlConfigLoader.search(META_INF_EBEAN_ORM_XML); Assert.assertNotNull(ebeanOrmXml); Assert.assertTrue("Found ebean-orm.xml", ebeanOrmXml.size() > 0); } + + private static final String META_INF_EBEAN_ORM_XML = "META-INF/ebean-orm.xml"; + private static final String META_INF_ORM_XML = "META-INF/orm.xml"; + private static final String WEB_INF_CLASSES = "WEB-INF/classes/"; + + @Test + public void ensureEbeanOrmStillLoads() throws IOException { + URL jar = setupJar(); + URL jarBang = setupJarBang(); + + URLClassLoader cl = new URLClassLoader(new URL[] { jar, jarBang}, getClass().getClassLoader()); + + XmlConfigLoader loader = new XmlConfigLoader(cl); + + Assert.assertEquals(3, loader.search(META_INF_EBEAN_ORM_XML).size()); // 1 in parent, 2 in ours + Assert.assertEquals(2, loader.search(META_INF_ORM_XML).size()); // 2 in ours + + } + + private URL createJar(File jarFile, String offset) throws IOException { + FileOutputStream stream = new FileOutputStream(jarFile); + JarOutputStream jarOutputStream = new JarOutputStream(stream); + + JarEntry entry = new JarEntry(offset + META_INF_EBEAN_ORM_XML); + jarOutputStream.putNextEntry(entry); + InputStream classStream = getClass().getResourceAsStream("/" + META_INF_EBEAN_ORM_XML); + IOUtils.copy(classStream, jarOutputStream); + + entry = new JarEntry(offset + META_INF_ORM_XML); + jarOutputStream.putNextEntry(entry); + classStream = getClass().getResourceAsStream("/" + META_INF_EBEAN_ORM_XML); + IOUtils.copy(classStream, jarOutputStream); + + jarOutputStream.close(); + stream.close(); + + if (offset.length() > 0) { + return new URL("jar:" + jarFile.toURI().toString() + "!/" + offset); + } else { + return jarFile.toURI().toURL(); + } + } + + private URL setupJarBang() throws IOException { + File jarFile = File.createTempFile("bang", ".war"); + return createJar(jarFile, WEB_INF_CLASSES); + } + + private URL setupJar() throws IOException { + File jarFile = File.createTempFile("nobang", ".jar"); + return createJar(jarFile, ""); + } }