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, "");
+ }
}