diff --git a/spring-launcher/pom.xml b/spring-launcher/pom.xml index 7d3e5c0f34..50e9462292 100644 --- a/spring-launcher/pom.xml +++ b/spring-launcher/pom.xml @@ -12,4 +12,27 @@ ${basedir}/.. + + + + maven-invoker-plugin + + ${project.build.directory}/it + src/it/settings.xml + ${project.build.directory}/local-repo + verify + true + + + + integration-test + + install + run + + + + + + diff --git a/spring-launcher/src/it/executable-jar/pom.xml b/spring-launcher/src/it/executable-jar/pom.xml new file mode 100644 index 0000000000..c9b4c7a6ae --- /dev/null +++ b/spring-launcher/src/it/executable-jar/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + org.springframework.zero.launcher.it + executable-jar + 0.0.1.BUILD-SNAPSHOT + jar + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.6 + + + unpack + prepare-package + + unpack + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + jar + + + ${project.build.directory}/assembly + + + + copy + prepare-package + + copy-dependencies + + + ${project.build.directory}/assembly/lib + + + + + + maven-assembly-plugin + 2.4 + + + src/main/assembly/jar-with-dependencies.xml + + + + org.springframework.launcher.JarLauncher + + + org.springframework.launcher.it.jar.EmbeddedJarStarter + + + + + + jar-with-dependencies + package + + single + + + + + + + + + org.eclipse.jetty + jetty-webapp + 8.1.8.v20121106 + + + org.eclipse.jetty + jetty-annotations + 8.1.8.v20121106 + + + org.springframework + spring-webmvc + 3.2.0.RELEASE + + + diff --git a/spring-launcher/src/it/executable-jar/src/main/assembly/jar-with-dependencies.xml b/spring-launcher/src/it/executable-jar/src/main/assembly/jar-with-dependencies.xml new file mode 100644 index 0000000000..44626f91aa --- /dev/null +++ b/spring-launcher/src/it/executable-jar/src/main/assembly/jar-with-dependencies.xml @@ -0,0 +1,26 @@ + + + full + + jar + + false + + + + + ${project.groupId}:${project.artifactId} + + true + + + + + ${project.build.directory}/assembly + / + + + diff --git a/spring-launcher/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/EmbeddedJarStarter.java b/spring-launcher/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/EmbeddedJarStarter.java new file mode 100644 index 0000000000..8bafbd9f8b --- /dev/null +++ b/spring-launcher/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/EmbeddedJarStarter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher.it.jar; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * Main class to start the embedded server. + * + * @author Phillip Webb + */ +public final class EmbeddedJarStarter { + + public static void main(String[] args) throws Exception { + Server server = new Server(8080); + + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); + server.setHandler(context); + + AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext(); + webApplicationContext.register(SpringConfiguration.class); + DispatcherServlet dispatcherServlet = new DispatcherServlet(webApplicationContext); + context.addServlet(new ServletHolder(dispatcherServlet), "/*"); + + server.start(); + server.join(); + } +} diff --git a/spring-launcher/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/ExampleController.java b/spring-launcher/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/ExampleController.java new file mode 100644 index 0000000000..2ad0474a43 --- /dev/null +++ b/spring-launcher/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/ExampleController.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher.it.jar; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Simple example Spring MVC Controller. + * + * @author Phillip Webb + */ +@Controller +public class ExampleController { + + @RequestMapping("/") + @ResponseBody + public String helloWorld() { + return "Hello Embedded Jar World!"; + } + +} diff --git a/spring-launcher/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/SpringConfiguration.java b/spring-launcher/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/SpringConfiguration.java new file mode 100644 index 0000000000..2d533998db --- /dev/null +++ b/spring-launcher/src/it/executable-jar/src/main/java/org/springframework/launcher/it/jar/SpringConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher.it.jar; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * Spring configuration. + * + * @author Phillip Webb + */ +@Configuration +@EnableWebMvc +@ComponentScan +public class SpringConfiguration { + +} diff --git a/spring-launcher/src/it/executable-war/pom.xml b/spring-launcher/src/it/executable-war/pom.xml new file mode 100644 index 0000000000..ff2b1118c8 --- /dev/null +++ b/spring-launcher/src/it/executable-war/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + org.springframework.zero.launcher.it + executable-war + 0.0.1.BUILD-SNAPSHOT + war + + + + org.apache.maven.plugins + maven-war-plugin + 2.3 + + + + org.springframework.launcher.WarLauncher + + + org.springframework.launcher.it.war.embedded.EmbeddedWarStarter + + + false + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.6 + + + unpack + prepare-package + + unpack + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + jar + + + ${project.build.directory}/${project.artifactId}-${project.version} + + + + + + + + + org.eclipse.jetty + jetty-webapp + 8.1.8.v20121106 + + + org.eclipse.jetty + jetty-plus + 8.1.8.v20121106 + + + org.eclipse.jetty + jetty-annotations + 8.1.8.v20121106 + + + org.springframework + spring-webmvc + 3.2.0.RELEASE + + + diff --git a/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/ExampleController.java b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/ExampleController.java new file mode 100644 index 0000000000..3cef4c506a --- /dev/null +++ b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/ExampleController.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher.it.war; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * Simple example Spring MVC Controller. + * + * @author Phillip Webb + */ +@Controller +public class ExampleController { + + @RequestMapping("/") + @ResponseBody + public String helloWorld() { + return "Hello Embedded WAR World!"; + } + +} diff --git a/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringConfiguration.java b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringConfiguration.java new file mode 100644 index 0000000000..d1fdd10d95 --- /dev/null +++ b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher.it.war; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * Spring configuration. + * + * @author Phillip Webb + */ +@Configuration +@EnableWebMvc +@ComponentScan +public class SpringConfiguration { + +} diff --git a/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringInitializer.java b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringInitializer.java new file mode 100644 index 0000000000..8099eb77c7 --- /dev/null +++ b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/SpringInitializer.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher.it.war; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * Spring {@link WebApplicationInitializer} for classic WAR deployment. + * + * @author Phillip Webb + */ +public class SpringInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext(); + webApplicationContext.register(SpringConfiguration.class); + servletContext.addServlet("dispatcherServlet", + new DispatcherServlet(webApplicationContext)).addMapping("/*"); + } + +} diff --git a/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/EmbeddedWarStarter.java b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/EmbeddedWarStarter.java new file mode 100644 index 0000000000..a3d54083ab --- /dev/null +++ b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/EmbeddedWarStarter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher.it.war.embedded; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.WebAppContext; +import org.springframework.launcher.it.war.SpringInitializer; + +/** + * Starter to launch the embedded server. NOTE: Jetty annotation scanning is not + * compatible with executable WARs so we must specify the {@link SpringInitializer}. + * + * @author Phillip Webb + */ +public final class EmbeddedWarStarter { + + public static void main(String[] args) throws Exception { + Server server = new Server(8080); + + WebAppContext webAppContext = new WebAppContext(); + webAppContext.setContextPath("/"); + webAppContext.setConfigurations(new Configuration[] { + new WebApplicationInitializersConfiguration(SpringInitializer.class) }); + + webAppContext.setParentLoaderPriority(true); + server.setHandler(webAppContext); + server.start(); + + server.join(); + } +} diff --git a/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/WebApplicationInitializersConfiguration.java b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/WebApplicationInitializersConfiguration.java new file mode 100644 index 0000000000..9fefb25391 --- /dev/null +++ b/spring-launcher/src/it/executable-war/src/main/java/org/springframework/launcher/it/war/embedded/WebApplicationInitializersConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher.it.war.embedded; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +import org.eclipse.jetty.webapp.AbstractConfiguration; +import org.eclipse.jetty.webapp.Configuration; +import org.eclipse.jetty.webapp.WebAppContext; +import org.springframework.util.Assert; +import org.springframework.web.WebApplicationInitializer; + +/** + * Jetty {@link Configuration} that allows Spring {@link WebApplicationInitializer} to be + * started. This is required because Jetty annotation scanning does not work with packaged + * WARs. + * + * @author Phillip Webb + */ +public class WebApplicationInitializersConfiguration extends AbstractConfiguration { + + private Class[] webApplicationInitializers; + + public WebApplicationInitializersConfiguration(Class webApplicationInitializer, + Class... webApplicationInitializers) { + this.webApplicationInitializers = new Class[webApplicationInitializers.length + 1]; + this.webApplicationInitializers[0] = webApplicationInitializer; + System.arraycopy(webApplicationInitializers, 0, this.webApplicationInitializers, + 1, webApplicationInitializers.length); + for (Class i : webApplicationInitializers) { + Assert.notNull(i, "WebApplicationInitializer must not be null"); + Assert.isAssignable(WebApplicationInitializer.class, i); + } + } + + @Override + public void configure(WebAppContext context) throws Exception { + context.getServletContext().addListener(new ServletContextListener() { + + @Override + public void contextInitialized(ServletContextEvent sce) { + try { + for (Class webApplicationInitializer : webApplicationInitializers) { + WebApplicationInitializer initializer = (WebApplicationInitializer) webApplicationInitializer.newInstance(); + initializer.onStartup(sce.getServletContext()); + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + } + }); + } + +} diff --git a/spring-launcher/src/it/settings.xml b/spring-launcher/src/it/settings.xml new file mode 100644 index 0000000000..e1e0ace341 --- /dev/null +++ b/spring-launcher/src/it/settings.xml @@ -0,0 +1,35 @@ + + + + + it-repo + + true + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + diff --git a/spring-launcher/src/main/java/org/springframework/launcher/Archive.java b/spring-launcher/src/main/java/org/springframework/launcher/Archive.java new file mode 100644 index 0000000000..61e2142cdb --- /dev/null +++ b/spring-launcher/src/main/java/org/springframework/launcher/Archive.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.jar.Manifest; + +/** + * An archive that can be launched by the {@link Launcher}. + * + * @author Phillip Webb + * @see JarFileArchive + */ +public interface Archive { + + /** + * Returns the manifest of the archive. + * @return the manifest + * @throws IOException + */ + Manifest getManifest() throws IOException; + + /** + * Returns archive entries. + * @return the archive entries + */ + Iterable getEntries(); + + /** + * Returns a URL that can be used to load the archive. + * @return the archive URL + * @throws MalformedURLException + */ + URL getUrl() throws MalformedURLException; + + /** + * Returns a nest archive from on the the contained entries. + * @param entry the entry (may be a directory or file) + * @return the nested archive + * @throws IOException + */ + Archive getNestedArchive(Entry entry) throws IOException; + + /** + * Returns a filtered version of the archive. + * @param filter the filter to apply + * @return a filter archive + * @throws IOException + */ + Archive getFilteredArchive(EntryFilter filter) throws IOException; + + /** + * Represents a single entry in the archive. + */ + public static interface Entry { + + /** + * Returns {@code true} if the entry represents a directory. + * @return if the entry is a directory + */ + boolean isDirectory(); + + /** + * Returns the name of the entry + * @return the name of the entry + */ + String getName(); + + } + + /** + * A filter for archive entries. + */ + public static interface EntryFilter { + + /** + * Apply the jar entry filter. + * @param entryName the current entry name. This may be different that the + * original entry name if a previous filter has been applied + * @param entry the entry to filter + * @return the new name of the entry or {@code null} if the entry should not be + * included. + */ + String apply(String entryName, Entry entry); + + } + +} diff --git a/spring-launcher/src/main/java/org/springframework/launcher/ExplodedArchive.java b/spring-launcher/src/main/java/org/springframework/launcher/ExplodedArchive.java new file mode 100644 index 0000000000..91d221c72c --- /dev/null +++ b/spring-launcher/src/main/java/org/springframework/launcher/ExplodedArchive.java @@ -0,0 +1,194 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.jar.Manifest; + +/** + * {@link Archive} implementation backed by an exploded archive directory. + * + * @author Phillip Webb + */ +public class ExplodedArchive implements Archive { + + private static final Set SKIPPED_NAMES = new HashSet(Arrays.asList( + ".", "..")); + + private static final Object MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + + private File root; + + private Map entries = new LinkedHashMap(); + + private Manifest manifest; + + public ExplodedArchive(File root) { + if (!root.exists() || !root.isDirectory()) { + throw new IllegalArgumentException("Invalid source folder " + root); + } + this.root = root; + buildEntries(root); + this.entries = Collections.unmodifiableMap(this.entries); + } + + private ExplodedArchive(File root, Map entries) { + this.root = root; + this.entries = Collections.unmodifiableMap(entries); + } + + private void buildEntries(File file) { + if (!file.equals(this.root)) { + String name = file.getAbsolutePath().substring( + this.root.getAbsolutePath().length() + 1); + if (file.isDirectory()) { + name += "/"; + } + this.entries.put(name, new FileEntry(name, file)); + } + if (file.isDirectory()) { + for (File child : file.listFiles()) { + if (!SKIPPED_NAMES.contains(child.getName())) { + buildEntries(child); + } + } + } + } + + @Override + public Manifest getManifest() throws IOException { + if (this.manifest == null && this.entries.containsKey(MANIFEST_ENTRY_NAME)) { + FileEntry entry = (FileEntry) this.entries.get(MANIFEST_ENTRY_NAME); + FileInputStream inputStream = new FileInputStream(entry.getFile()); + try { + this.manifest = new Manifest(inputStream); + } + finally { + inputStream.close(); + } + } + return this.manifest; + } + + @Override + public Iterable getEntries() { + return this.entries.values(); + } + + @Override + public URL getUrl() throws MalformedURLException { + FilteredURLStreamHandler handler = new FilteredURLStreamHandler(); + return new URL("file", "", -1, this.root.getAbsolutePath() + "/", handler); + // return this.root.toURI().toURL(); + } + + @Override + public Archive getNestedArchive(Entry entry) throws IOException { + File file = ((FileEntry) entry).getFile(); + return (file.isDirectory() ? new ExplodedArchive(file) : new JarFileArchive(file)); + } + + @Override + public Archive getFilteredArchive(EntryFilter filter) throws IOException { + Map filteredEntries = new LinkedHashMap(); + for (Map.Entry entry : this.entries.entrySet()) { + String filteredName = filter.apply(entry.getKey(), entry.getValue()); + if (filteredName != null) { + filteredEntries.put(filteredName, new FileEntry(filteredName, + ((FileEntry) entry.getValue()).getFile())); + } + } + return new ExplodedArchive(this.root, filteredEntries); + } + + private class FileEntry implements Entry { + + private final String name; + private final File file; + + public FileEntry(String name, File file) { + this.name = name; + this.file = file; + } + + public File getFile() { + return this.file; + } + + @Override + public boolean isDirectory() { + return this.file.isDirectory(); + } + + @Override + public String getName() { + return this.name; + } + } + + /** + * {@link URLStreamHandler} that respects filtered entries. + */ + private class FilteredURLStreamHandler extends URLStreamHandler { + + public FilteredURLStreamHandler() { + } + + @Override + protected URLConnection openConnection(URL url) throws IOException { + String name = url.getPath().substring( + ExplodedArchive.this.root.getAbsolutePath().length() + 1); + if (ExplodedArchive.this.entries.containsKey(name)) { + return new URL(url.toString()).openConnection(); + } + return new FileNotFoundURLConnection(url, name); + } + } + + /** + * {@link URLConnection} used to represent a filtered file. + */ + private static class FileNotFoundURLConnection extends URLConnection { + + private String name; + + public FileNotFoundURLConnection(URL url, String name) { + super(url); + this.name = name; + } + + @Override + public void connect() throws IOException { + throw new FileNotFoundException(this.name); + } + + } + +} diff --git a/spring-launcher/src/main/java/org/springframework/launcher/JarFileArchive.java b/spring-launcher/src/main/java/org/springframework/launcher/JarFileArchive.java new file mode 100644 index 0000000000..6b9c648bcf --- /dev/null +++ b/spring-launcher/src/main/java/org/springframework/launcher/JarFileArchive.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.Manifest; + +import org.springframework.launcher.jar.JarEntryFilter; +import org.springframework.launcher.jar.RandomAccessJarFile; + +/** + * {@link Archive} implementation backed by a {@link RandomAccessJarFile}. + * + * @author Phillip Webb + */ +public class JarFileArchive implements Archive { + + private final RandomAccessJarFile jarFile; + + private final List entries; + + public JarFileArchive(File file) throws IOException { + this(new RandomAccessJarFile(file)); + } + + public JarFileArchive(RandomAccessJarFile jarFile) { + this.jarFile = jarFile; + ArrayList jarFileEntries = new ArrayList(); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + jarFileEntries.add(new JarFileEntry(entries.nextElement())); + } + this.entries = Collections.unmodifiableList(jarFileEntries); + } + + @Override + public Manifest getManifest() throws IOException { + return this.jarFile.getManifest(); + } + + @Override + public Iterable getEntries() { + return this.entries; + } + + @Override + public URL getUrl() throws MalformedURLException { + return this.jarFile.getUrl(); + } + + @Override + public Archive getNestedArchive(Entry entry) throws IOException { + JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry(); + RandomAccessJarFile jarFile = this.jarFile.getNestedJarFile(jarEntry); + return new JarFileArchive(jarFile); + } + + @Override + public Archive getFilteredArchive(final EntryFilter filter) throws IOException { + RandomAccessJarFile filteredJar = this.jarFile + .getFilteredJarFile(new JarEntryFilter() { + @Override + public String apply(String name, JarEntry entry) { + return filter.apply(name, new JarFileEntry(entry)); + } + }); + return new JarFileArchive(filteredJar); + } + + /** + * {@link Archive.Entry} implementation backed by a {@link JarEntry}. + */ + private static class JarFileEntry implements Entry { + + private final JarEntry jarEntry; + + public JarFileEntry(JarEntry jarEntry) { + this.jarEntry = jarEntry; + } + + public JarEntry getJarEntry() { + return this.jarEntry; + } + + @Override + public boolean isDirectory() { + return this.jarEntry.isDirectory(); + } + + @Override + public String getName() { + return this.jarEntry.getName(); + } + + } + +} diff --git a/spring-launcher/src/main/java/org/springframework/launcher/JarLauncher.java b/spring-launcher/src/main/java/org/springframework/launcher/JarLauncher.java index db3732a254..6e07532ea3 100644 --- a/spring-launcher/src/main/java/org/springframework/launcher/JarLauncher.java +++ b/spring-launcher/src/main/java/org/springframework/launcher/JarLauncher.java @@ -17,9 +17,6 @@ package org.springframework.launcher; import java.util.List; -import java.util.jar.JarEntry; - -import org.springframework.launcher.jar.RandomAccessJarFile; /** * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are @@ -30,14 +27,13 @@ import org.springframework.launcher.jar.RandomAccessJarFile; public class JarLauncher extends Launcher { @Override - protected boolean isNestedJarFile(JarEntry jarEntry) { - return !jarEntry.isDirectory() && jarEntry.getName().startsWith("lib/"); + protected boolean isNestedArchive(Archive.Entry entry) { + return !entry.isDirectory() && entry.getName().startsWith("lib/"); } @Override - protected void postProcessLib(RandomAccessJarFile jarFile, - List lib) throws Exception { - lib.add(0, jarFile); + protected void postProcessLib(Archive archive, List lib) throws Exception { + lib.add(0, archive); } public static void main(String[] args) { diff --git a/spring-launcher/src/main/java/org/springframework/launcher/Launcher.java b/spring-launcher/src/main/java/org/springframework/launcher/Launcher.java index 84072d7aab..2dc17e997f 100644 --- a/spring-launcher/src/main/java/org/springframework/launcher/Launcher.java +++ b/spring-launcher/src/main/java/org/springframework/launcher/Launcher.java @@ -23,13 +23,10 @@ import java.net.URLClassLoader; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.ArrayList; -import java.util.Enumeration; import java.util.List; import java.util.jar.JarEntry; import java.util.logging.Logger; -import org.springframework.launcher.jar.RandomAccessJarFile; - /** * Base class for launchers that can start an application with a fully configured * classpath. @@ -77,11 +74,14 @@ public abstract class Launcher { if (codeSourcePath == null) { throw new IllegalStateException("Unable to determine code source archive"); } - if (codeSourcePath.endsWith("/")) { - throw new IllegalStateException("The specified code source path '" - + codeSourcePath + "' is not an archive"); + File root = new File(codeSourcePath); + if (!root.exists()) { + throw new IllegalStateException( + "Unable to determine code source archive from " + root); } - launch(args, new File(codeSourcePath)); + Archive archive = (root.isDirectory() ? new ExplodedArchive(root) + : new JarFileArchive(root)); + launch(args, archive); } /** @@ -90,22 +90,19 @@ public abstract class Launcher { * @param archive the underlying (zip/war/jar) archive * @throws Exception */ - protected void launch(String[] args, File archive) throws Exception { - RandomAccessJarFile jarFile = new RandomAccessJarFile(archive); - - List lib = new ArrayList(); - Enumeration jarEntries = jarFile.entries(); - while (jarEntries.hasMoreElements()) { - JarEntry jarEntry = jarEntries.nextElement(); - if (isNestedJarFile(jarEntry)) { - this.logger.fine("Adding: " + jarEntry.getName()); - lib.add(jarFile.getNestedJarFile(jarEntry)); + protected void launch(String[] args, Archive archive) throws Exception { + List lib = new ArrayList(); + for (Archive.Entry entry : archive.getEntries()) { + if (isNestedArchive(entry)) { + this.logger.fine("Adding: " + entry.getName()); + lib.add(archive.getNestedArchive(entry)); } } + this.logger.fine("Added " + lib.size() + " entries"); - postProcessLib(jarFile, lib); + postProcessLib(archive, lib); ClassLoader classLoader = createClassLoader(lib); - launch(args, jarFile, classLoader); + launch(args, archive, classLoader); } /** @@ -114,17 +111,16 @@ public abstract class Launcher { * @param jarEntry the jar entry * @return {@code true} if the entry is a nested item (jar or folder) */ - protected abstract boolean isNestedJarFile(JarEntry jarEntry); + protected abstract boolean isNestedArchive(Archive.Entry jarEntry); /** * Called to post-process lib entries before they are used. Implementations can add * and remove entries. - * @param jarFile the jar file + * @param archive the archive * @param lib the existing lib * @throws Exception */ - protected void postProcessLib(RandomAccessJarFile jarFile, - List lib) throws Exception { + protected void postProcessLib(Archive archive, List lib) throws Exception { } /** @@ -133,8 +129,7 @@ public abstract class Launcher { * @return the classloader * @throws Exception */ - protected ClassLoader createClassLoader(List lib) - throws Exception { + protected ClassLoader createClassLoader(List lib) throws Exception { URL[] urls = new URL[lib.size()]; for (int i = 0; i < urls.length; i++) { urls[i] = lib.get(i).getUrl(); @@ -155,13 +150,13 @@ public abstract class Launcher { /** * Launch the application given the archive file and a fully configured classloader. * @param args the incoming arguments - * @param jarFile the jar file + * @param archive the archive * @param classLoader the classloader * @throws Exception */ - protected void launch(String[] args, RandomAccessJarFile jarFile, - ClassLoader classLoader) throws Exception { - String mainClass = getMainClass(jarFile); + protected void launch(String[] args, Archive archive, ClassLoader classLoader) + throws Exception { + String mainClass = getMainClass(archive); Runnable runner = createMainMethodRunner(mainClass, args, classLoader); Thread runnerThread = new Thread(runner); runnerThread.setContextClassLoader(classLoader); @@ -172,12 +167,12 @@ public abstract class Launcher { /** * Obtain the main class that should be used to launch the application. By default * this method uses a {@code Start-Class} manifest entry. - * @param jarFile the jar file + * @param archive the archive * @return the main class * @throws Exception */ - protected String getMainClass(RandomAccessJarFile jarFile) throws Exception { - String mainClass = jarFile.getManifest().getMainAttributes() + protected String getMainClass(Archive archive) throws Exception { + String mainClass = archive.getManifest().getMainAttributes() .getValue("Start-Class"); if (mainClass == null) { throw new IllegalStateException("No 'Start-Class' manifest entry specified"); diff --git a/spring-launcher/src/main/java/org/springframework/launcher/WarLauncher.java b/spring-launcher/src/main/java/org/springframework/launcher/WarLauncher.java index e152424af3..478dc7e914 100644 --- a/spring-launcher/src/main/java/org/springframework/launcher/WarLauncher.java +++ b/spring-launcher/src/main/java/org/springframework/launcher/WarLauncher.java @@ -18,10 +18,6 @@ package org.springframework.launcher; import java.io.IOException; import java.util.List; -import java.util.jar.JarEntry; - -import org.springframework.launcher.jar.JarEntryFilter; -import org.springframework.launcher.jar.RandomAccessJarFile; /** * {@link Launcher} for WAR based archives. This launcher for standard WAR archives. @@ -33,35 +29,33 @@ import org.springframework.launcher.jar.RandomAccessJarFile; public class WarLauncher extends Launcher { @Override - protected boolean isNestedJarFile(JarEntry jarEntry) { - if (jarEntry.isDirectory()) { - return jarEntry.getName().equals("WEB-INF/classes/"); + protected boolean isNestedArchive(Archive.Entry entry) { + if (entry.isDirectory()) { + return entry.getName().equals("WEB-INF/classes/"); } else { - return jarEntry.getName().startsWith("WEB-INF/lib/") - || jarEntry.getName().startsWith("WEB-INF/lib-provided/"); + return entry.getName().startsWith("WEB-INF/lib/") + || entry.getName().startsWith("WEB-INF/lib-provided/"); } } @Override - protected void postProcessLib(RandomAccessJarFile jarFile, - List lib) throws Exception { - lib.add(0, filterJarFile(jarFile)); + protected void postProcessLib(Archive archive, List lib) throws Exception { + lib.add(0, filterArchive(archive)); } /** * Filter the specified WAR file to exclude elements that should not appear on the * classpath. - * @param jarFile the source file - * @return the filtered file + * @param archive the source archive + * @return the filtered archive * @throws IOException on error */ - protected RandomAccessJarFile filterJarFile(RandomAccessJarFile jarFile) - throws IOException { - return jarFile.getFilteredJarFile(new JarEntryFilter() { + protected Archive filterArchive(Archive archive) throws IOException { + return archive.getFilteredArchive(new Archive.EntryFilter() { @Override - public String apply(String entryName, JarEntry entry) { + public String apply(String entryName, Archive.Entry entry) { if (entryName.startsWith("META-INF/") || entryName.startsWith("WEB-INF/")) { return null; } diff --git a/spring-launcher/src/main/java/org/springframework/launcher/jar/RandomAccessJarFile.java b/spring-launcher/src/main/java/org/springframework/launcher/jar/RandomAccessJarFile.java index ac8b46b34c..ffb6368dcb 100644 --- a/spring-launcher/src/main/java/org/springframework/launcher/jar/RandomAccessJarFile.java +++ b/spring-launcher/src/main/java/org/springframework/launcher/jar/RandomAccessJarFile.java @@ -353,8 +353,7 @@ public class RandomAccessJarFile extends JarFile { } /** - * {@link RandomAccessJarURLConnection} used to support - * {@link RandomAccessJarFile#getUrl()}. + * {@link JarURLConnection} used to support {@link RandomAccessJarFile#getUrl()}. */ private static class RandomAccessJarURLConnection extends JarURLConnection { diff --git a/spring-launcher/src/main/java/org/springframework/launcher/package-info.java b/spring-launcher/src/main/java/org/springframework/launcher/package-info.java index a878456639..c05c211eea 100644 --- a/spring-launcher/src/main/java/org/springframework/launcher/package-info.java +++ b/spring-launcher/src/main/java/org/springframework/launcher/package-info.java @@ -17,7 +17,7 @@ /** * System that allows self contained JAR/WAR archives to be launched using * {@code java -jar}. Archives can include nested packaged dependency JARs (there is - * not need to create shade style jars) and are executed without unpacking. The only + * no need to create shade style jars) and are executed without unpacking. The only * constraint is that nested JARs must be stored in the archive uncompressed. * * @see org.springframework.launcher.JarLauncher diff --git a/spring-launcher/src/test/java/org/springframework/launcher/ExplodedArchiveTests.java b/spring-launcher/src/test/java/org/springframework/launcher/ExplodedArchiveTests.java new file mode 100644 index 0000000000..1c6e2967c5 --- /dev/null +++ b/spring-launcher/src/test/java/org/springframework/launcher/ExplodedArchiveTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.launcher.Archive.Entry; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link ExplodedArchive}. + * + * @author Phillip Webb + */ +public class ExplodedArchiveTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private File rootFolder; + + private ExplodedArchive archive; + + @Before + public void setup() throws Exception { + File file = this.temporaryFolder.newFile(); + TestJarCreator.createTestJar(file); + + this.rootFolder = this.temporaryFolder.newFolder(); + JarFile jarFile = new JarFile(file); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + File destination = new File(this.rootFolder.getAbsolutePath() + + File.separator + entry.getName()); + destination.getParentFile().mkdirs(); + if (entry.isDirectory()) { + destination.mkdir(); + } + else { + copy(jarFile.getInputStream(entry), new FileOutputStream(destination)); + } + } + this.archive = new ExplodedArchive(this.rootFolder); + } + + private void copy(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024]; + int len = in.read(buffer); + while (len != -1) { + out.write(buffer, 0, len); + len = in.read(buffer); + } + } + + @Test + public void getManifest() throws Exception { + assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By"), + equalTo("j1")); + } + + @Test + public void getEntries() throws Exception { + Map entries = getEntriesMap(this.archive); + assertThat(entries.size(), equalTo(7)); + } + + @Test + public void getUrl() throws Exception { + URL url = this.archive.getUrl(); + assertThat(url, equalTo(this.rootFolder.toURI().toURL())); + } + + @Test + public void getNestedArchive() throws Exception { + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + Archive nested = this.archive.getNestedArchive(entry); + assertThat(nested.getUrl().toString(), + equalTo("jar:file:" + this.rootFolder.getPath() + "/nested.jar!/")); + } + + @Test + public void nestedDirArchive() throws Exception { + Entry entry = getEntriesMap(this.archive).get("d/"); + Archive nested = this.archive.getNestedArchive(entry); + Map nestedEntries = getEntriesMap(nested); + assertThat(nestedEntries.size(), equalTo(1)); + assertThat(nested.getUrl().toString(), + equalTo("file:" + this.rootFolder.getPath() + "/d/")); + } + + @Test + public void getFilteredArchive() throws Exception { + Archive filteredArchive = this.archive + .getFilteredArchive(new Archive.EntryFilter() { + @Override + public String apply(String entryName, Entry entry) { + if (entryName.equals("1.dat")) { + return entryName; + } + return null; + } + }); + Map entries = getEntriesMap(filteredArchive); + assertThat(entries.size(), equalTo(1)); + URLClassLoader classLoader = new URLClassLoader( + new URL[] { filteredArchive.getUrl() }); + assertThat(classLoader.getResourceAsStream("1.dat").read(), equalTo(1)); + assertThat(classLoader.getResourceAsStream("2.dat"), nullValue()); + } + + private Map getEntriesMap(Archive archive) { + Map entries = new HashMap(); + for (Archive.Entry entry : archive.getEntries()) { + entries.put(entry.getName(), entry); + } + return entries; + } +} diff --git a/spring-launcher/src/test/java/org/springframework/launcher/JarFileArchiveTests.java b/spring-launcher/src/test/java/org/springframework/launcher/JarFileArchiveTests.java new file mode 100644 index 0000000000..d3c3b732b5 --- /dev/null +++ b/spring-launcher/src/test/java/org/springframework/launcher/JarFileArchiveTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher; + +import java.io.File; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.launcher.Archive.Entry; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link JarFileArchive}. + * + * @author Phillip Webb + */ +public class JarFileArchiveTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + private File rootJarFile; + + private JarFileArchive archive; + + @Before + public void setup() throws Exception { + this.rootJarFile = this.temporaryFolder.newFile(); + TestJarCreator.createTestJar(this.rootJarFile); + this.archive = new JarFileArchive(this.rootJarFile); + } + + @Test + public void getManifest() throws Exception { + assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By"), + equalTo("j1")); + } + + @Test + public void getEntries() throws Exception { + Map entries = getEntriesMap(this.archive); + assertThat(entries.size(), equalTo(7)); + } + + @Test + public void getUrl() throws Exception { + URL url = this.archive.getUrl(); + assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() + + "!/")); + } + + @Test + public void getNestedArchive() throws Exception { + Entry entry = getEntriesMap(this.archive).get("nested.jar"); + Archive nested = this.archive.getNestedArchive(entry); + assertThat(nested.getUrl().toString(), + equalTo("jar:file:" + this.rootJarFile.getPath() + "!/nested.jar!/")); + } + + @Test + public void getFilteredArchive() throws Exception { + Archive filteredArchive = this.archive + .getFilteredArchive(new Archive.EntryFilter() { + @Override + public String apply(String entryName, Entry entry) { + if (entryName.equals("1.dat")) { + return entryName; + } + return null; + } + }); + Map entries = getEntriesMap(filteredArchive); + assertThat(entries.size(), equalTo(1)); + } + + private Map getEntriesMap(Archive archive) { + Map entries = new HashMap(); + for (Archive.Entry entry : archive.getEntries()) { + entries.put(entry.getName(), entry); + } + return entries; + } +} diff --git a/spring-launcher/src/test/java/org/springframework/launcher/TestJarCreator.java b/spring-launcher/src/test/java/org/springframework/launcher/TestJarCreator.java new file mode 100644 index 0000000000..362da85e09 --- /dev/null +++ b/spring-launcher/src/test/java/org/springframework/launcher/TestJarCreator.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.launcher; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; + +/** + * Creates a simple test jar. + * + * @author Phillip Webb + */ +public abstract class TestJarCreator { + + public static void createTestJar(File file) throws Exception { + FileOutputStream fileOutputStream = new FileOutputStream(file); + JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream); + try { + writeManifest(jarOutputStream, "j1"); + writeEntry(jarOutputStream, "1.dat", 1); + writeEntry(jarOutputStream, "2.dat", 2); + writeDirEntry(jarOutputStream, "d/"); + writeEntry(jarOutputStream, "d/9.dat", 9); + + JarEntry nestedEntry = new JarEntry("nested.jar"); + byte[] nestedJarData = getNestedJarData(); + nestedEntry.setSize(nestedJarData.length); + nestedEntry.setCompressedSize(nestedJarData.length); + CRC32 crc32 = new CRC32(); + crc32.update(nestedJarData); + nestedEntry.setCrc(crc32.getValue()); + + nestedEntry.setMethod(ZipEntry.STORED); + jarOutputStream.putNextEntry(nestedEntry); + jarOutputStream.write(nestedJarData); + jarOutputStream.closeEntry(); + } + finally { + jarOutputStream.close(); + } + } + + private static byte[] getNestedJarData() throws Exception { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream); + writeManifest(jarOutputStream, "j2"); + writeEntry(jarOutputStream, "3.dat", 3); + writeEntry(jarOutputStream, "4.dat", 4); + jarOutputStream.close(); + return byteArrayOutputStream.toByteArray(); + } + + private static void writeManifest(JarOutputStream jarOutputStream, String name) + throws Exception { + writeDirEntry(jarOutputStream, "META-INF/"); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().putValue("Built-By", name); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); + manifest.write(jarOutputStream); + jarOutputStream.closeEntry(); + } + + private static void writeDirEntry(JarOutputStream jarOutputStream, String name) + throws IOException { + jarOutputStream.putNextEntry(new JarEntry(name)); + jarOutputStream.closeEntry(); + } + + private static void writeEntry(JarOutputStream jarOutputStream, String name, int data) + throws IOException { + jarOutputStream.putNextEntry(new JarEntry(name)); + jarOutputStream.write(new byte[] { (byte) data }); + jarOutputStream.closeEntry(); + } + +} diff --git a/spring-launcher/src/test/java/org/springframework/launcher/data/RandomAccessDataFileTest.java b/spring-launcher/src/test/java/org/springframework/launcher/data/RandomAccessDataFileTests.java similarity index 99% rename from spring-launcher/src/test/java/org/springframework/launcher/data/RandomAccessDataFileTest.java rename to spring-launcher/src/test/java/org/springframework/launcher/data/RandomAccessDataFileTests.java index 959508c65c..245b5e866a 100644 --- a/spring-launcher/src/test/java/org/springframework/launcher/data/RandomAccessDataFileTest.java +++ b/spring-launcher/src/test/java/org/springframework/launcher/data/RandomAccessDataFileTests.java @@ -48,7 +48,7 @@ import org.springframework.launcher.data.RandomAccessDataFile; * * @author Phillip Webb */ -public class RandomAccessDataFileTest { +public class RandomAccessDataFileTests { private static final byte[] BYTES; static { diff --git a/spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessDataZipInputStreamTest.java b/spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessDataZipInputStreamTests.java similarity index 98% rename from spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessDataZipInputStreamTest.java rename to spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessDataZipInputStreamTests.java index b6e7a9fc58..bba954ec11 100644 --- a/spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessDataZipInputStreamTest.java +++ b/spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessDataZipInputStreamTests.java @@ -40,7 +40,7 @@ import org.springframework.launcher.jar.RandomAccessDataZipInputStream; * * @author Phillip Webb */ -public class RandomAccessDataZipInputStreamTest { +public class RandomAccessDataZipInputStreamTests { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); diff --git a/spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessJarFileTest.java b/spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessJarFileTests.java similarity index 64% rename from spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessJarFileTest.java rename to spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessJarFileTests.java index b50ddd5a43..e374f12ad0 100644 --- a/spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessJarFileTest.java +++ b/spring-launcher/src/test/java/org/springframework/launcher/jar/RandomAccessJarFileTests.java @@ -16,6 +16,25 @@ package org.springframework.launcher.jar; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TemporaryFolder; +import org.springframework.launcher.TestJarCreator; +import org.springframework.launcher.data.RandomAccessDataFile; + import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.instanceOf; @@ -26,38 +45,12 @@ import static org.junit.Assert.assertThat; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.JarURLConnection; -import java.net.URL; -import java.util.Enumeration; -import java.util.jar.Attributes; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.util.jar.Manifest; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.rules.TemporaryFolder; -import org.springframework.launcher.data.RandomAccessDataFile; -import org.springframework.launcher.jar.JarEntryFilter; -import org.springframework.launcher.jar.RandomAccessJarFile; - /** * Tests for {@link RandomAccessJarFile}. * * @author Phillip Webb */ -public class RandomAccessJarFileTest { +public class RandomAccessJarFileTests { @Rule public ExpectedException thrown = ExpectedException.none(); @@ -71,69 +64,11 @@ public class RandomAccessJarFileTest { @Before public void setup() throws Exception { - this.rootJarFile = temporaryFolder.newFile(); - FileOutputStream fileOutputStream = new FileOutputStream(this.rootJarFile); - JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream); - try { - writeManifest(jarOutputStream, "j1"); - writeEntry(jarOutputStream, "1.dat", 1); - writeEntry(jarOutputStream, "2.dat", 2); - writeDirEntry(jarOutputStream, "d/"); - writeEntry(jarOutputStream, "d/9.dat", 9); - - JarEntry nestedEntry = new JarEntry("nested.jar"); - byte[] nestedJarData = getNestedJarData(); - nestedEntry.setSize(nestedJarData.length); - nestedEntry.setCompressedSize(nestedJarData.length); - CRC32 crc32 = new CRC32(); - crc32.update(nestedJarData); - nestedEntry.setCrc(crc32.getValue()); - - nestedEntry.setMethod(ZipEntry.STORED); - jarOutputStream.putNextEntry(nestedEntry); - jarOutputStream.write(nestedJarData); - jarOutputStream.closeEntry(); - } - finally { - jarOutputStream.close(); - } + this.rootJarFile = this.temporaryFolder.newFile(); + TestJarCreator.createTestJar(this.rootJarFile); this.jarFile = new RandomAccessJarFile(this.rootJarFile); } - private byte[] getNestedJarData() throws Exception { - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream); - writeManifest(jarOutputStream, "j2"); - writeEntry(jarOutputStream, "3.dat", 3); - writeEntry(jarOutputStream, "4.dat", 4); - jarOutputStream.close(); - return byteArrayOutputStream.toByteArray(); - } - - private void writeManifest(JarOutputStream jarOutputStream, String name) - throws Exception { - writeDirEntry(jarOutputStream, "META-INF/"); - Manifest manifest = new Manifest(); - manifest.getMainAttributes().putValue("Built-By", name); - manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); - jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); - manifest.write(jarOutputStream); - jarOutputStream.closeEntry(); - } - - private void writeDirEntry(JarOutputStream jarOutputStream, String name) - throws IOException { - jarOutputStream.putNextEntry(new JarEntry(name)); - jarOutputStream.closeEntry(); - } - - private void writeEntry(JarOutputStream jarOutputStream, String name, int data) - throws IOException { - jarOutputStream.putNextEntry(new JarEntry(name)); - jarOutputStream.write(new byte[] { (byte) data }); - jarOutputStream.closeEntry(); - } - @Test public void createFromFile() throws Exception { RandomAccessJarFile jarFile = new RandomAccessJarFile(this.rootJarFile); @@ -150,13 +85,13 @@ public class RandomAccessJarFileTest { @Test public void getManifest() throws Exception { - assertThat(jarFile.getManifest().getMainAttributes().getValue("Built-By"), + assertThat(this.jarFile.getManifest().getMainAttributes().getValue("Built-By"), equalTo("j1")); } @Test public void getEntries() throws Exception { - Enumeration entries = jarFile.entries(); + Enumeration entries = this.jarFile.entries(); assertThat(entries.nextElement().getName(), equalTo("META-INF/")); assertThat(entries.nextElement().getName(), equalTo("META-INF/MANIFEST.MF")); assertThat(entries.nextElement().getName(), equalTo("1.dat")); @@ -169,26 +104,27 @@ public class RandomAccessJarFileTest { @Test public void getJarEntry() throws Exception { - JarEntry entry = jarFile.getJarEntry("1.dat"); + JarEntry entry = this.jarFile.getJarEntry("1.dat"); assertThat(entry, notNullValue(ZipEntry.class)); assertThat(entry.getName(), equalTo("1.dat")); } @Test public void getInputStream() throws Exception { - InputStream inputStream = jarFile.getInputStream(jarFile.getEntry("1.dat")); + InputStream inputStream = this.jarFile.getInputStream(this.jarFile + .getEntry("1.dat")); assertThat(inputStream.read(), equalTo(1)); assertThat(inputStream.read(), equalTo(-1)); } @Test public void getName() throws Exception { - assertThat(jarFile.getName(), equalTo(rootJarFile.getPath())); + assertThat(this.jarFile.getName(), equalTo(this.rootJarFile.getPath())); } @Test public void getSize() throws Exception { - assertThat(jarFile.size(), equalTo((int) rootJarFile.length())); + assertThat(this.jarFile.size(), equalTo((int) this.rootJarFile.length())); } @Test @@ -202,25 +138,26 @@ public class RandomAccessJarFileTest { @Test public void getUrl() throws Exception { - URL url = jarFile.getUrl(); - assertThat(url.toString(), equalTo("jar:file:" + rootJarFile.getPath() + "!/")); + URL url = this.jarFile.getUrl(); + assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() + + "!/")); JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); - assertThat(jarURLConnection.getJarFile(), sameInstance((JarFile) jarFile)); + assertThat(jarURLConnection.getJarFile(), sameInstance((JarFile) this.jarFile)); assertThat(jarURLConnection.getJarEntry(), nullValue()); assertThat(jarURLConnection.getContentLength(), greaterThan(1)); - assertThat(jarURLConnection.getContent(), sameInstance((Object) jarFile)); + assertThat(jarURLConnection.getContent(), sameInstance((Object) this.jarFile)); assertThat(jarURLConnection.getContentType(), equalTo("x-java/jar")); } @Test public void getEntryUrl() throws Exception { - URL url = new URL(jarFile.getUrl(), "1.dat"); - assertThat(url.toString(), equalTo("jar:file:" + rootJarFile.getPath() + URL url = new URL(this.jarFile.getUrl(), "1.dat"); + assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() + "!/1.dat")); JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection(); - assertThat(jarURLConnection.getJarFile(), sameInstance((JarFile) jarFile)); + assertThat(jarURLConnection.getJarFile(), sameInstance((JarFile) this.jarFile)); assertThat(jarURLConnection.getJarEntry(), - sameInstance(jarFile.getJarEntry("1.dat"))); + sameInstance(this.jarFile.getJarEntry("1.dat"))); assertThat(jarURLConnection.getContentLength(), equalTo(1)); assertThat(jarURLConnection.getContent(), instanceOf(InputStream.class)); assertThat(jarURLConnection.getContentType(), equalTo("content/unknown")); @@ -228,24 +165,24 @@ public class RandomAccessJarFileTest { @Test public void getMissingEntryUrl() throws Exception { - URL url = new URL(jarFile.getUrl(), "missing.dat"); - assertThat(url.toString(), equalTo("jar:file:" + rootJarFile.getPath() + URL url = new URL(this.jarFile.getUrl(), "missing.dat"); + assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() + "!/missing.dat")); - thrown.expect(FileNotFoundException.class); + this.thrown.expect(FileNotFoundException.class); ((JarURLConnection) url.openConnection()).getJarEntry(); } @Test public void getUrlStream() throws Exception { - URL url = jarFile.getUrl(); + URL url = this.jarFile.getUrl(); url.openConnection(); - thrown.expect(IOException.class); + this.thrown.expect(IOException.class); url.openStream(); } @Test public void getEntryUrlStream() throws Exception { - URL url = new URL(jarFile.getUrl(), "1.dat"); + URL url = new URL(this.jarFile.getUrl(), "1.dat"); url.openConnection(); InputStream stream = url.openStream(); assertThat(stream.read(), equalTo(1)); @@ -254,7 +191,7 @@ public class RandomAccessJarFileTest { @Test public void getNestedJarFile() throws Exception { - RandomAccessJarFile nestedJarFile = jarFile.getNestedJarFile(jarFile + RandomAccessJarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile .getEntry("nested.jar")); Enumeration entries = nestedJarFile.entries(); @@ -270,7 +207,7 @@ public class RandomAccessJarFileTest { assertThat(inputStream.read(), equalTo(-1)); URL url = nestedJarFile.getUrl(); - assertThat(url.toString(), equalTo("jar:file:" + rootJarFile.getPath() + assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() + "!/nested.jar!/")); assertThat(((JarURLConnection) url.openConnection()).getJarFile(), sameInstance((JarFile) nestedJarFile)); @@ -278,7 +215,7 @@ public class RandomAccessJarFileTest { @Test public void getNestedJarDirectory() throws Exception { - RandomAccessJarFile nestedJarFile = jarFile.getNestedJarFile(jarFile + RandomAccessJarFile nestedJarFile = this.jarFile.getNestedJarFile(this.jarFile .getEntry("d/")); Enumeration entries = nestedJarFile.entries(); @@ -291,28 +228,30 @@ public class RandomAccessJarFileTest { assertThat(inputStream.read(), equalTo(-1)); URL url = nestedJarFile.getUrl(); - assertThat(url.toString(), equalTo("jar:file:" + rootJarFile.getPath() + "!/d!/")); + assertThat(url.toString(), equalTo("jar:file:" + this.rootJarFile.getPath() + + "!/d!/")); assertThat(((JarURLConnection) url.openConnection()).getJarFile(), sameInstance((JarFile) nestedJarFile)); } @Test public void getDirectoryInputStream() throws Exception { - InputStream inputStream = jarFile.getInputStream(jarFile.getEntry("d/")); + InputStream inputStream = this.jarFile + .getInputStream(this.jarFile.getEntry("d/")); assertThat(inputStream, notNullValue()); assertThat(inputStream.read(), equalTo(-1)); } @Test public void getDirectoryInputStreamWithoutSlash() throws Exception { - InputStream inputStream = jarFile.getInputStream(jarFile.getEntry("d")); + InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d")); assertThat(inputStream, notNullValue()); assertThat(inputStream.read(), equalTo(-1)); } @Test public void getFilteredJarFile() throws Exception { - RandomAccessJarFile filteredJarFile = jarFile + RandomAccessJarFile filteredJarFile = this.jarFile .getFilteredJarFile(new JarEntryFilter() { @Override public String apply(String entryName, JarEntry entry) { @@ -334,8 +273,8 @@ public class RandomAccessJarFileTest { @Test public void sensibleToString() throws Exception { - assertThat(jarFile.toString(), equalTo(rootJarFile.getPath())); - assertThat(jarFile.getNestedJarFile(jarFile.getEntry("nested.jar")).toString(), - equalTo(rootJarFile.getPath() + "!/nested.jar")); + assertThat(this.jarFile.toString(), equalTo(this.rootJarFile.getPath())); + assertThat(this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar")) + .toString(), equalTo(this.rootJarFile.getPath() + "!/nested.jar")); } }