Support running executable-jars exploded

Allow the Launcher to detect ans run exploded (unpacked) jar/war files.
This change is primarily driven by the fact that Cloud Foundry will
unpack uploaded files.

Issue: #53066255
pull/7/head
Phillip Webb 12 years ago
parent 3ba700a861
commit 324abe88b4

@ -12,4 +12,27 @@
<properties>
<main.basedir>${basedir}/..</main.basedir>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-invoker-plugin</artifactId>
<configuration>
<cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
<settingsFile>src/it/settings.xml</settingsFile>
<localRepositoryPath>${project.build.directory}/local-repo</localRepositoryPath>
<postBuildHookScript>verify</postBuildHookScript>
<addTestClassPath>true</addTestClassPath>
</configuration>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>install</goal>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.zero.launcher.it</groupId>
<artifactId>executable-jar</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>unpack</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<type>jar</type>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/assembly</outputDirectory>
</configuration>
</execution>
<execution>
<id>copy</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/assembly/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<descriptors>
<descriptor>src/main/assembly/jar-with-dependencies.xml</descriptor>
</descriptors>
<archive>
<manifest>
<mainClass>org.springframework.launcher.JarLauncher</mainClass>
</manifest>
<manifestEntries>
<Start-Class>org.springframework.launcher.it.jar.EmbeddedJarStarter</Start-Class>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>jar-with-dependencies</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>8.1.8.v20121106</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-annotations</artifactId>
<version>8.1.8.v20121106</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>3.2.0.RELEASE</version>
</dependency>
</dependencies>
</project>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>full</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<useProjectArtifact/>
<includes>
<include>${project.groupId}:${project.artifactId}</include>
</includes>
<unpack>true</unpack>
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>${project.build.directory}/assembly</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>

@ -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();
}
}

@ -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!";
}
}

@ -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 {
}

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.zero.launcher.it</groupId>
<artifactId>executable-war</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>war</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<archive>
<manifest>
<mainClass>org.springframework.launcher.WarLauncher</mainClass>
</manifest>
<manifestEntries>
<Start-Class>org.springframework.launcher.it.war.embedded.EmbeddedWarStarter</Start-Class>
</manifestEntries>
</archive>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>unpack</id>
<phase>prepare-package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<type>jar</type>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/${project.artifactId}-${project.version}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>8.1.8.v20121106</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-plus</artifactId>
<version>8.1.8.v20121106</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-annotations</artifactId>
<version>8.1.8.v20121106</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>3.2.0.RELEASE</version>
</dependency>
</dependencies>
</project>

@ -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!";
}
}

@ -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 {
}

@ -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("/*");
}
}

@ -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();
}
}

@ -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) {
}
});
}
}

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<profiles>
<profile>
<id>it-repo</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<repositories>
<repository>
<id>local.central</id>
<url>@localRepositoryUrl@</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>local.central</id>
<url>@localRepositoryUrl@</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
</settings>

@ -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<Entry> 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);
}
}

@ -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<String> SKIPPED_NAMES = new HashSet<String>(Arrays.asList(
".", ".."));
private static final Object MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF";
private File root;
private Map<String, Entry> entries = new LinkedHashMap<String, Entry>();
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<String, Entry> 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<Entry> 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<String, Entry> filteredEntries = new LinkedHashMap<String, Archive.Entry>();
for (Map.Entry<String, 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);
}
}
}

@ -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<Entry> entries;
public JarFileArchive(File file) throws IOException {
this(new RandomAccessJarFile(file));
}
public JarFileArchive(RandomAccessJarFile jarFile) {
this.jarFile = jarFile;
ArrayList<Entry> jarFileEntries = new ArrayList<Entry>();
Enumeration<JarEntry> 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<Entry> 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();
}
}
}

@ -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<RandomAccessJarFile> lib) throws Exception {
lib.add(0, jarFile);
protected void postProcessLib(Archive archive, List<Archive> lib) throws Exception {
lib.add(0, archive);
}
public static void main(String[] args) {

@ -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<RandomAccessJarFile> lib = new ArrayList<RandomAccessJarFile>();
Enumeration<JarEntry> 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<Archive> lib = new ArrayList<Archive>();
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<RandomAccessJarFile> lib) throws Exception {
protected void postProcessLib(Archive archive, List<Archive> lib) throws Exception {
}
/**
@ -133,8 +129,7 @@ public abstract class Launcher {
* @return the classloader
* @throws Exception
*/
protected ClassLoader createClassLoader(List<RandomAccessJarFile> lib)
throws Exception {
protected ClassLoader createClassLoader(List<Archive> 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");

@ -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<RandomAccessJarFile> lib) throws Exception {
lib.add(0, filterJarFile(jarFile));
protected void postProcessLib(Archive archive, List<Archive> 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;
}

@ -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 {

@ -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

@ -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<JarEntry> 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<String, Archive.Entry> 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<String, Entry> 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<String, Entry> 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<String, Archive.Entry> getEntriesMap(Archive archive) {
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
for (Archive.Entry entry : archive.getEntries()) {
entries.put(entry.getName(), entry);
}
return entries;
}
}

@ -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<String, Archive.Entry> 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<String, Entry> entries = getEntriesMap(filteredArchive);
assertThat(entries.size(), equalTo(1));
}
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
Map<String, Archive.Entry> entries = new HashMap<String, Archive.Entry>();
for (Archive.Entry entry : archive.getEntries()) {
entries.put(entry.getName(), entry);
}
return entries;
}
}

@ -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();
}
}

@ -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 {

@ -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();

@ -16,31 +16,15 @@
package org.springframework.launcher.jar;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
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;
@ -48,16 +32,25 @@ 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 org.springframework.launcher.jar.JarEntryFilter;
import org.springframework.launcher.jar.RandomAccessJarFile;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
/**
* 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<JarEntry> entries = jarFile.entries();
Enumeration<JarEntry> 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<JarEntry> 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<JarEntry> 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"));
}
}
Loading…
Cancel
Save