Relocate projects to spring-boot-project

Move projects to better reflect the way that Spring Boot is released.

The following projects are under `spring-boot-project`:

  - `spring-boot`
  - `spring-boot-autoconfigure`
  - `spring-boot-tools`
  - `spring-boot-starters`
  - `spring-boot-actuator`
  - `spring-boot-actuator-autoconfigure`
  - `spring-boot-test`
  - `spring-boot-test-autoconfigure`
  - `spring-boot-devtools`
  - `spring-boot-cli`
  - `spring-boot-docs`

See gh-9316
This commit is contained in:
Phillip Webb
2017-09-19 14:29:46 -07:00
parent 0419d42b7c
commit 0ba4830b4f
4023 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
loader.path: target,target/lib,.
loader.main: org.springframework.boot.load.it.jar.EmbeddedJarStarter

View File

@@ -0,0 +1,106 @@
<?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.boot.launcher.it</groupId>
<artifactId>executable-dir</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>@maven-compiler-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>@maven-dependency-plugin.version@</version>
<executions>
<execution>
<id>unpack</id>
<phase>prepare-package</phase>
<goals>
<goal>copy</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}/lib</outputDirectory>
</configuration>
</execution>
<execution>
<id>copy</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>@maven-jar-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>@maven-surefire-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>@exec-maven-plugin.version@</version>
<configuration>
<executable>java</executable>
<arguments>
<argument>-cp</argument>
<argument>${project.build.directory}/lib/@project.artifactId@-@project.version@.jar</argument>
<argument>org.springframework.boot.loader.PropertiesLauncher</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>@javax-annotation.version@</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>@jetty.version@</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-annotations</artifactId>
<version>@jetty.version@</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>@spring.version@</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,47 @@
/*
* 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.boot.load.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();
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.boot.load.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!";
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.boot.load.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 {
}

View File

@@ -0,0 +1,120 @@
<?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.boot.launcher.it</groupId>
<artifactId>executable-jar</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>@maven-assembly-plugin.version@</version>
<configuration>
<descriptors>
<descriptor>src/main/assembly/jar-with-dependencies.xml</descriptor>
</descriptors>
<recompressZippedFiles>false</recompressZippedFiles>
<archive>
<manifest>
<mainClass>org.springframework.boot.loader.JarLauncher</mainClass>
</manifest>
<manifestEntries>
<Start-Class>org.springframework.boot.load.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>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>@maven-compiler-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>@maven-dependency-plugin.version@</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>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>@maven-jar-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>@maven-surefire-plugin.version@</version>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>@javax-annotation.version@</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>@jetty.version@</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-annotations</artifactId>
<version>@jetty.version@</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>@spring.version@</version>
</dependency>
</dependencies>
</project>

View File

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

View File

@@ -0,0 +1,47 @@
/*
* 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.boot.load.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();
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.boot.load.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!";
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.boot.load.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 {
}

View File

@@ -0,0 +1 @@
loader.path=jar:file:target/executable-props-lib-0.0.1.BUILD-SNAPSHOT-dependencies.jar/!BOOT-INF/lib

View File

@@ -0,0 +1,114 @@
<?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.boot.launcher.it</groupId>
<artifactId>executable-props-lib</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>@maven-assembly-plugin.version@</version>
<executions>
<execution>
<id>app</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>src/main/assembly/app.xml</descriptor>
</descriptors>
<recompressZippedFiles>false</recompressZippedFiles>
<archive>
<manifest>
<mainClass>org.springframework.boot.loader.PropertiesLauncher</mainClass>
</manifest>
<manifestEntries>
<Start-Class>org.springframework.boot.launcher.it.props.EmbeddedJarStarter</Start-Class>
</manifestEntries>
</archive>
</configuration>
</execution>
<execution>
<id>depedendencies</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>src/main/assembly/dependencies.xml</descriptor>
</descriptors>
<recompressZippedFiles>false</recompressZippedFiles>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>@maven-compiler-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>@maven-dependency-plugin.version@</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}/app-assembly</outputDirectory>
</configuration>
</execution>
<execution>
<id>copy</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/dependencies-assembly/BOOT-INF/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>@maven-jar-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>@maven-surefire-plugin.version@</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>@spring.version@</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,27 @@
<?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>app</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<useProjectArtifact/>
<includes>
<include>${project.groupId}:${project.artifactId}</include>
</includes>
<outputDirectory>BOOT-INF/classes</outputDirectory>
<unpack>true</unpack>
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>${project.build.directory}/app-assembly</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,17 @@
<?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>dependencies</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.build.directory}/dependencies-assembly</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2012-2017 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.boot.launcher.it.props;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* Main class to start the embedded server.
*
* @author Dave Syer
*/
public final class EmbeddedJarStarter {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
context.getBean(SpringConfiguration.class).run(args);
context.close();
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2017 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.boot.launcher.it.props;
import java.io.IOException;
import java.util.Properties;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
/**
* Spring configuration.
*
* @author Dave Syer
*/
@Configuration
@ComponentScan
public class SpringConfiguration implements InitializingBean {
private String message = "Jar";
@Override
public void afterPropertiesSet() throws IOException {
Properties props = new Properties();
props.load(new ClassPathResource("application.properties").getInputStream());
String value = props.getProperty("message");
if (value!=null) {
this.message = value;
}
}
public void run(String... args) {
System.err.println("Hello Embedded " + this.message + "!");
}
}

View File

@@ -0,0 +1,17 @@
def jarfile = './target/executable-props-lib-0.0.1.BUILD-SNAPSHOT-app.jar'
new File("${basedir}/application.properties").delete()
String exec(String command) {
def proc = command.execute([], basedir)
proc.waitFor()
proc.err.text
}
String out = exec("java -jar ${jarfile}")
assert out.contains('Hello Embedded World!'),
'Using -jar my.jar should load dependencies from separate jar and use the application.properties from the jar\n' + out
out = exec("java -cp ${jarfile} org.springframework.boot.loader.PropertiesLauncher")
assert out.contains('Hello Embedded World!'),
'Using -cp my.jar with PropertiesLauncher should load dependencies from separate jar and use the application.properties from the jar\n' + out

View File

@@ -0,0 +1,101 @@
<?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.boot.launcher.it</groupId>
<artifactId>executable-props</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>@maven-assembly-plugin.version@</version>
<configuration>
<descriptors>
<descriptor>src/main/assembly/jar-with-dependencies.xml</descriptor>
</descriptors>
<recompressZippedFiles>false</recompressZippedFiles>
<archive>
<manifest>
<mainClass>org.springframework.boot.loader.PropertiesLauncher</mainClass>
</manifest>
<manifestEntries>
<Start-Class>org.springframework.boot.load.it.props.EmbeddedJarStarter</Start-Class>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>jar-with-dependencies</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>@maven-compiler-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>@maven-dependency-plugin.version@</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/BOOT-INF/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>@maven-jar-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>@maven-surefire-plugin.version@</version>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>@spring.version@</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,27 @@
<?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>
<outputDirectory>BOOT-INF/classes</outputDirectory>
<unpack>true</unpack>
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>${project.build.directory}/assembly</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2012-2015 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.boot.load.it.props;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
* Main class to start the embedded server.
*
* @author Dave Syer
*/
public final class EmbeddedJarStarter {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
context.getBean(SpringConfiguration.class).run(args);
context.close();
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2017 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.boot.load.it.props;
import java.io.IOException;
import java.util.Properties;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
/**
* Spring configuration.
*
* @author Dave Syer
*/
@Configuration
@ComponentScan
public class SpringConfiguration implements InitializingBean {
private String message = "Jar";
@Override
public void afterPropertiesSet() throws IOException {
Properties props = new Properties();
props.load(new ClassPathResource("application.properties").getInputStream());
String value = props.getProperty("message");
if (value!=null) {
this.message = value;
}
}
public void run(String... args) {
System.err.println("Hello Embedded " + this.message + "!");
}
}

View File

@@ -0,0 +1,31 @@
def jarfile = './target/executable-props-0.0.1.BUILD-SNAPSHOT-full.jar'
new File("${basedir}/application.properties").delete()
String exec(String command) {
def proc = command.execute([], basedir)
proc.waitFor()
proc.err.text
}
String out = exec("java -jar ${jarfile}")
assert out.contains('Hello Embedded World!'),
'Using -jar my.jar should use the application.properties from the jar\n' + out
out = exec("java -cp ${jarfile} org.springframework.boot.loader.PropertiesLauncher")
assert out.contains('Hello Embedded World!'),
'Using -cp my.jar with PropertiesLauncher should use the application.properties from the jar\n' + out
new File("${basedir}/application.properties").withWriter { it -> it << "message: Foo" }
out = exec("java -jar ${jarfile}")
assert out.contains('Hello Embedded World!'),
'Should use the application.properties from the jar in preference to local filesystem\n' + out
out = exec("java -Dloader.path=.,lib -jar ${jarfile}")
assert out.contains('Hello Embedded Foo!'),
'With loader.path=.,lib should use the application.properties from the local filesystem\n' + out
new File("${basedir}/target/application.properties").withWriter { it -> it << "message: Spam" }
out = exec("java -Dloader.path=target,.,lib -jar ${jarfile}")
assert out.contains('Hello Embedded Spam!'),
'With loader.path=target,.,lib should use the application.properties from the target directory\n' + out

View File

@@ -0,0 +1,99 @@
<?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.boot.launcher.it</groupId>
<artifactId>executable-war</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>@maven-compiler-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>@maven-dependency-plugin.version@</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>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>@maven-surefire-plugin.version@</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>@maven-war-plugin.version@</version>
<configuration>
<archive>
<manifest>
<mainClass>org.springframework.boot.loader.WarLauncher</mainClass>
</manifest>
<manifestEntries>
<Start-Class>org.springframework.boot.load.it.war.embedded.EmbeddedWarStarter</Start-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>@javax-annotation.version@</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>@jetty.version@</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-plus</artifactId>
<version>@jetty.version@</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-annotations</artifactId>
<version>@jetty.version@</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>@spring.version@</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,37 @@
/*
* 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.boot.load.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!";
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.boot.load.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 {
}

View File

@@ -0,0 +1,41 @@
/*
* 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.boot.load.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("/*");
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.boot.load.it.war.embedded;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.Configuration;
import org.eclipse.jetty.webapp.WebAppContext;
import org.springframework.boot.load.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();
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.boot.load.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 ex) {
throw new RuntimeException(ex);
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
}
});
}
}

View File

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

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2012-2017 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.boot.loader;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import org.springframework.boot.loader.archive.Archive;
/**
* Base class for executable archive {@link Launcher}s.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected ExecutableArchiveLauncher(Archive archive) {
this.archive = archive;
}
protected final Archive getArchive() {
return this.archive;
}
@Override
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException(
"No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>(
this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives);
return archives;
}
/**
* Determine if the specified {@link JarEntry} is a nested item that should be added
* to the classpath. The method is called once for each entry.
* @param entry the jar entry
* @return {@code true} if the entry is a nested item (jar or folder)
*/
protected abstract boolean isNestedArchive(Archive.Entry entry);
/**
* Called to post-process archive entries before they are used. Implementations can
* add and remove entries.
* @param archives the archives
* @throws Exception if the post processing fails
*/
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2012-2016 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.boot.loader;
import org.springframework.boot.loader.archive.Archive;
/**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
* included inside a {@code /BOOT-INF/lib} directory and that application classes are
* included inside a {@code /BOOT-INF/classes} directory.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2012-2017 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.boot.loader;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.util.Enumeration;
import java.util.jar.JarFile;
import org.springframework.boot.loader.jar.Handler;
/**
* {@link ClassLoader} used by the {@link Launcher}.
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
*/
public class LaunchedURLClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
/**
* Create a new {@link LaunchedURLClassLoader} instance.
* @param urls the URLs from which to load classes and resources
* @param parent the parent class loader for delegation
*/
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
public URL findResource(String name) {
Handler.setUseFastConnectionExceptions(true);
try {
return super.findResource(name);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
@Override
public Enumeration<URL> findResources(String name) throws IOException {
Handler.setUseFastConnectionExceptions(true);
try {
return super.findResources(name);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Handler.setUseFastConnectionExceptions(true);
try {
try {
definePackageIfNecessary(name);
}
catch (IllegalArgumentException ex) {
// Tolerate race condition due to being parallel capable
if (getPackage(name) == null) {
// This should never happen as the IllegalArgumentException indicates
// that the package has already been defined and, therefore,
// getPackage(name) should not return null.
throw new AssertionError("Package " + name + " has already been "
+ "defined but it could not be found");
}
}
return super.loadClass(name, resolve);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
/**
* Define a package before a {@code findClass} call is made. This is necessary to
* ensure that the appropriate manifest for nested JARs is associated with the
* package.
* @param className the class name being found
*/
private void definePackageIfNecessary(String className) {
int lastDot = className.lastIndexOf('.');
if (lastDot >= 0) {
String packageName = className.substring(0, lastDot);
if (getPackage(packageName) == null) {
try {
definePackage(className, packageName);
}
catch (IllegalArgumentException ex) {
// Tolerate race condition due to being parallel capable
if (getPackage(packageName) == null) {
// This should never happen as the IllegalArgumentException
// indicates that the package has already been defined and,
// therefore, getPackage(name) should not have returned null.
throw new AssertionError(
"Package " + packageName + " has already been defined "
+ "but it could not be found");
}
}
}
}
}
private void definePackage(final String className, final String packageName) {
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
String packageEntryName = packageName.replace('.', '/') + "/";
String classEntryName = className.replace('.', '/') + ".class";
for (URL url : getURLs()) {
try {
URLConnection connection = url.openConnection();
if (connection instanceof JarURLConnection) {
JarFile jarFile = ((JarURLConnection) connection)
.getJarFile();
if (jarFile.getEntry(classEntryName) != null
&& jarFile.getEntry(packageEntryName) != null
&& jarFile.getManifest() != null) {
definePackage(packageName, jarFile.getManifest(), url);
return null;
}
}
}
catch (IOException ex) {
// Ignore
}
}
return null;
}, AccessController.getContext());
}
catch (java.security.PrivilegedActionException ex) {
// Ignore
}
}
/**
* Clear URL caches.
*/
public void clearCache() {
for (URL url : getURLs()) {
try {
URLConnection connection = url.openConnection();
if (connection instanceof JarURLConnection) {
clearCache(connection);
}
}
catch (IOException ex) {
// Ignore
}
}
}
private void clearCache(URLConnection connection) throws IOException {
Object jarFile = ((JarURLConnection) connection).getJarFile();
if (jarFile instanceof org.springframework.boot.loader.jar.JarFile) {
((org.springframework.boot.loader.jar.JarFile) jarFile).clearCache();
}
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2012-2017 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.boot.loader;
import java.io.File;
import java.net.URI;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.boot.loader.jar.JarFile;
/**
* Base class for launchers that can start an application with a fully configured
* classpath backed by one or more {@link Archive}s.
*
* @author Phillip Webb
* @author Dave Syer
*/
public abstract class Launcher {
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
/**
* Create a classloader for the specified archives.
* @param archives the archives
* @return the classloader
* @throws Exception if the classloader cannot be created
*/
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader(urls.toArray(new URL[urls.size()]));
}
/**
* Create a classloader for the specified URLs.
* @param urls the URLs
* @return the classloader
* @throws Exception if the classloader cannot be created
*/
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
/**
* Launch the application given the archive file and a fully configured classloader.
* @param args the incoming arguments
* @param mainClass the main class to run
* @param classLoader the classloader
* @throws Exception if the launch fails
*/
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
/**
* Create the {@code MainMethodRunner} used to launch the application.
* @param mainClass the main class
* @param args the incoming arguments
* @param classLoader the classloader
* @return the main method runner
*/
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
/**
* Returns the main class that should be launched.
* @return the name of the main class
* @throws Exception if the main class cannot be obtained
*/
protected abstract String getMainClass() throws Exception;
/**
* Returns the archives that will be used to construct the class path.
* @return the class path archives
* @throws Exception if the class path archives cannot be obtained
*/
protected abstract List<Archive> getClassPathArchives() throws Exception;
protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
String path = (location == null ? null : location.getSchemeSpecificPart());
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException(
"Unable to determine code source archive from " + root);
}
return (root.isDirectory() ? new ExplodedArchive(root)
: new JarFileArchive(root));
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2016 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.boot.loader;
import java.lang.reflect.Method;
/**
* Utility class that is used by {@link Launcher}s to call a main method. The class
* containing the main method is loaded using the thread context class loader.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
/**
* Create a new {@link MainMethodRunner} instance.
* @param mainClass the main class
* @param args incoming arguments
*/
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args == null ? null : args.clone());
}
public void run() throws Exception {
Class<?> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, new Object[] { this.args });
}
}

View File

@@ -0,0 +1,645 @@
/*
* Copyright 2012-2017 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.boot.loader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.boot.loader.archive.Archive.EntryFilter;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.boot.loader.util.SystemPropertyUtils;
/**
* {@link Launcher} for archives with user-configured classpath and main class via a
* properties file. This model is often more flexible and more amenable to creating
* well-behaved OS-level services than a model based on executable jars.
* <p>
* Looks in various places for a properties file to extract loader settings, defaulting to
* {@code loader.properties} either on the current classpath or in the current working
* directory. The name of the properties file can be changed by setting a System property
* {@code loader.config.name} (e.g. {@code -Dloader.config.name=foo} will look for
* {@code foo.properties}. If that file doesn't exist then tries
* {@code loader.config.location} (with allowed prefixes {@code classpath:} and
* {@code file:} or any valid URL). Once that file is located turns it into Properties and
* extracts optional values (which can also be provided overridden as System properties in
* case the file doesn't exist):
* <ul>
* <li>{@code loader.path}: a comma-separated list of directories (containing file
* resources and/or nested archives in *.jar or *.zip or archives) or archives to append
* to the classpath. {@code BOOT-INF/classes,BOOT-INF/lib} in the application archive are
* always used</li>
* <li>{@code loader.main}: the main method to delegate execution to once the class loader
* is set up. No default, but will fall back to looking for a {@code Start-Class} in a
* {@code MANIFEST.MF}, if there is one in <code>${loader.home}/META-INF</code>.</li>
* </ul>
*
* @author Dave Syer
* @author Janne Valkealahti
* @author Andy Wilkinson
*/
public class PropertiesLauncher extends Launcher {
private static final String DEBUG = "loader.debug";
/**
* Properties key for main class. As a manifest entry can also be specified as
* {@code Start-Class}.
*/
public static final String MAIN = "loader.main";
/**
* Properties key for classpath entries (directories possibly containing jars or
* jars). Multiple entries can be specified using a comma-separated list. {@code
* BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used.
*/
public static final String PATH = "loader.path";
/**
* Properties key for home directory. This is the location of external configuration
* if not on classpath, and also the base path for any relative paths in the
* {@link #PATH loader path}. Defaults to current working directory (
* <code>${user.dir}</code>).
*/
public static final String HOME = "loader.home";
/**
* Properties key for default command line arguments. These arguments (if present) are
* prepended to the main method arguments before launching.
*/
public static final String ARGS = "loader.args";
/**
* Properties key for name of external configuration file (excluding suffix). Defaults
* to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is
* provided instead.
*/
public static final String CONFIG_NAME = "loader.config.name";
/**
* Properties key for config file location (including optional classpath:, file: or
* URL prefix).
*/
public static final String CONFIG_LOCATION = "loader.config.location";
/**
* Properties key for boolean flag (default false) which if set will cause the
* external configuration properties to be copied to System properties (assuming that
* is allowed by Java security).
*/
public static final String SET_SYSTEM_PROPERTIES = "loader.system";
private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+");
private final File home;
private List<String> paths = new ArrayList<>();
private final Properties properties = new Properties();
private Archive parent;
public PropertiesLauncher() {
try {
this.home = getHomeDirectory();
initializeProperties();
initializePaths();
this.parent = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected File getHomeDirectory() {
try {
return new File(getPropertyWithDefault(HOME, "${user.dir}"));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private void initializeProperties() throws Exception, IOException {
List<String> configs = new ArrayList<>();
if (getProperty(CONFIG_LOCATION) != null) {
configs.add(getProperty(CONFIG_LOCATION));
}
else {
String[] names = getPropertyWithDefault(CONFIG_NAME, "loader").split(",");
for (String name : names) {
configs.add("file:" + getHomeDirectory() + "/" + name + ".properties");
configs.add("classpath:" + name + ".properties");
configs.add("classpath:BOOT-INF/classes/" + name + ".properties");
}
}
for (String config : configs) {
try (InputStream resource = getResource(config)) {
if (resource != null) {
debug("Found: " + config);
loadResource(resource);
// Load the first one we find
return;
}
else {
debug("Not found: " + config);
}
}
}
}
private void loadResource(InputStream resource) throws IOException, Exception {
this.properties.load(resource);
for (Object key : Collections.list(this.properties.propertyNames())) {
String text = this.properties.getProperty((String) key);
String value = SystemPropertyUtils.resolvePlaceholders(this.properties, text);
if (value != null) {
this.properties.put(key, value);
}
}
if ("true".equals(getProperty(SET_SYSTEM_PROPERTIES))) {
debug("Adding resolved properties to System properties");
for (Object key : Collections.list(this.properties.propertyNames())) {
String value = this.properties.getProperty((String) key);
System.setProperty((String) key, value);
}
}
}
private InputStream getResource(String config) throws Exception {
if (config.startsWith("classpath:")) {
return getClasspathResource(config.substring("classpath:".length()));
}
config = stripFileUrlPrefix(config);
if (isUrl(config)) {
return getURLResource(config);
}
return getFileResource(config);
}
private String stripFileUrlPrefix(String config) {
if (config.startsWith("file:")) {
config = config.substring("file:".length());
if (config.startsWith("//")) {
config = config.substring(2);
}
}
return config;
}
private boolean isUrl(String config) {
return config.contains("://");
}
private InputStream getClasspathResource(String config) {
while (config.startsWith("/")) {
config = config.substring(1);
}
config = "/" + config;
debug("Trying classpath: " + config);
return getClass().getResourceAsStream(config);
}
private InputStream getFileResource(String config) throws Exception {
File file = new File(config);
debug("Trying file: " + config);
if (file.canRead()) {
return new FileInputStream(file);
}
return null;
}
private InputStream getURLResource(String config) throws Exception {
URL url = new URL(config);
if (exists(url)) {
URLConnection con = url.openConnection();
try {
return con.getInputStream();
}
catch (IOException ex) {
// Close the HTTP connection (if applicable).
if (con instanceof HttpURLConnection) {
((HttpURLConnection) con).disconnect();
}
throw ex;
}
}
return null;
}
private boolean exists(URL url) throws IOException {
// Try a URL connection content-length header...
URLConnection connection = url.openConnection();
try {
connection.setUseCaches(
connection.getClass().getSimpleName().startsWith("JNLP"));
if (connection instanceof HttpURLConnection) {
HttpURLConnection httpConnection = (HttpURLConnection) connection;
httpConnection.setRequestMethod("HEAD");
int responseCode = httpConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
return true;
}
else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
}
return (connection.getContentLength() >= 0);
}
finally {
if (connection instanceof HttpURLConnection) {
((HttpURLConnection) connection).disconnect();
}
}
}
private void initializePaths() throws Exception {
String path = getProperty(PATH);
if (path != null) {
this.paths = parsePathsProperty(path);
}
debug("Nested archive paths: " + this.paths);
}
private List<String> parsePathsProperty(String commaSeparatedPaths) {
List<String> paths = new ArrayList<>();
for (String path : commaSeparatedPaths.split(",")) {
path = cleanupPath(path);
// "" means the user wants root of archive but not current directory
path = ("".equals(path) ? "/" : path);
paths.add(path);
}
if (paths.isEmpty()) {
paths.add("lib");
}
return paths;
}
protected String[] getArgs(String... args) throws Exception {
String loaderArgs = getProperty(ARGS);
if (loaderArgs != null) {
String[] defaultArgs = loaderArgs.split("\\s+");
String[] additionalArgs = args;
args = new String[defaultArgs.length + additionalArgs.length];
System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length);
System.arraycopy(additionalArgs, 0, args, defaultArgs.length,
additionalArgs.length);
}
return args;
}
@Override
protected String getMainClass() throws Exception {
String mainClass = getProperty(MAIN, "Start-Class");
if (mainClass == null) {
throw new IllegalStateException(
"No '" + MAIN + "' or 'Start-Class' specified");
}
return mainClass;
}
@Override
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
Set<URL> urls = new LinkedHashSet<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(new URL[0]),
getClass().getClassLoader());
debug("Classpath: " + urls);
String customLoaderClassName = getProperty("loader.classLoader");
if (customLoaderClassName != null) {
loader = wrapWithCustomClassLoader(loader, customLoaderClassName);
debug("Using custom class loader: " + customLoaderClassName);
}
return loader;
}
@SuppressWarnings("unchecked")
private ClassLoader wrapWithCustomClassLoader(ClassLoader parent,
String loaderClassName) throws Exception {
Class<ClassLoader> loaderClass = (Class<ClassLoader>) Class
.forName(loaderClassName, true, parent);
try {
return loaderClass.getConstructor(ClassLoader.class).newInstance(parent);
}
catch (NoSuchMethodException ex) {
// Ignore and try with URLs
}
try {
return loaderClass.getConstructor(URL[].class, ClassLoader.class)
.newInstance(new URL[0], parent);
}
catch (NoSuchMethodException ex) {
// Ignore and try without any arguments
}
return loaderClass.newInstance();
}
private String getProperty(String propertyKey) throws Exception {
return getProperty(propertyKey, null, null);
}
private String getProperty(String propertyKey, String manifestKey) throws Exception {
return getProperty(propertyKey, manifestKey, null);
}
private String getPropertyWithDefault(String propertyKey, String defaultValue)
throws Exception {
return getProperty(propertyKey, null, defaultValue);
}
private String getProperty(String propertyKey, String manifestKey,
String defaultValue) throws Exception {
if (manifestKey == null) {
manifestKey = propertyKey.replace('.', '-');
manifestKey = toCamelCase(manifestKey);
}
String property = SystemPropertyUtils.getProperty(propertyKey);
if (property != null) {
String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
property);
debug("Property '" + propertyKey + "' from environment: " + value);
return value;
}
if (this.properties.containsKey(propertyKey)) {
String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
this.properties.getProperty(propertyKey));
debug("Property '" + propertyKey + "' from properties: " + value);
return value;
}
try {
if (this.home != null) {
// Prefer home dir for MANIFEST if there is one
Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
if (manifest != null) {
String value = manifest.getMainAttributes().getValue(manifestKey);
if (value != null) {
debug("Property '" + manifestKey
+ "' from home directory manifest: " + value);
return SystemPropertyUtils.resolvePlaceholders(this.properties,
value);
}
}
}
}
catch (IllegalStateException ex) {
// Ignore
}
// Otherwise try the parent archive
Manifest manifest = createArchive().getManifest();
if (manifest != null) {
String value = manifest.getMainAttributes().getValue(manifestKey);
if (value != null) {
debug("Property '" + manifestKey + "' from archive manifest: " + value);
return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
}
}
return defaultValue == null ? defaultValue
: SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue);
}
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> lib = new ArrayList<>();
for (String path : this.paths) {
for (Archive archive : getClassPathArchives(path)) {
if (archive instanceof ExplodedArchive) {
List<Archive> nested = new ArrayList<>(
archive.getNestedArchives(new ArchiveEntryFilter()));
nested.add(0, archive);
lib.addAll(nested);
}
else {
lib.add(archive);
}
}
}
addNestedEntries(lib);
return lib;
}
private List<Archive> getClassPathArchives(String path) throws Exception {
String root = cleanupPath(stripFileUrlPrefix(path));
List<Archive> lib = new ArrayList<>();
File file = new File(root);
if (!"/".equals(root)) {
if (!isAbsolutePath(root)) {
file = new File(this.home, root);
}
if (file.isDirectory()) {
debug("Adding classpath entries from " + file);
Archive archive = new ExplodedArchive(file, false);
lib.add(archive);
}
}
Archive archive = getArchive(file);
if (archive != null) {
debug("Adding classpath entries from archive " + archive.getUrl() + root);
lib.add(archive);
}
List<Archive> nestedArchives = getNestedArchives(root);
if (nestedArchives != null) {
debug("Adding classpath entries from nested " + root);
lib.addAll(nestedArchives);
}
return lib;
}
private boolean isAbsolutePath(String root) {
// Windows contains ":" others start with "/"
return root.contains(":") || root.startsWith("/");
}
private Archive getArchive(File file) throws IOException {
String name = file.getName().toLowerCase();
if (name.endsWith(".jar") || name.endsWith(".zip")) {
return new JarFileArchive(file);
}
return null;
}
private List<Archive> getNestedArchives(String path) throws Exception {
Archive parent = this.parent;
String root = path;
if (!root.equals("/") && root.startsWith("/")
|| parent.getUrl().equals(this.home.toURI().toURL())) {
// If home dir is same as parent archive, no need to add it twice.
return null;
}
if (root.contains("!")) {
int index = root.indexOf("!");
File file = new File(this.home, root.substring(0, index));
if (root.startsWith("jar:file:")) {
file = new File(root.substring("jar:file:".length(), index));
}
parent = new JarFileArchive(file);
root = root.substring(index + 1, root.length());
while (root.startsWith("/")) {
root = root.substring(1);
}
}
if (root.endsWith(".jar")) {
File file = new File(this.home, root);
if (file.exists()) {
parent = new JarFileArchive(file);
root = "";
}
}
if (root.equals("/") || root.equals("./") || root.equals(".")) {
// The prefix for nested jars is actually empty if it's at the root
root = "";
}
EntryFilter filter = new PrefixMatchingArchiveFilter(root);
List<Archive> archives = new ArrayList<>(parent.getNestedArchives(filter));
if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar")
&& parent != this.parent) {
// You can't find the root with an entry filter so it has to be added
// explicitly. But don't add the root of the parent archive.
archives.add(parent);
}
return archives;
}
private void addNestedEntries(List<Archive> lib) {
// The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/"
// directories, meaning we are running from an executable JAR. We add nested
// entries from there with low priority (i.e. at end).
try {
lib.addAll(this.parent.getNestedArchives((entry) -> {
if (entry.isDirectory()) {
return entry.getName().equals(JarLauncher.BOOT_INF_CLASSES);
}
return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB);
}));
}
catch (IOException ex) {
// Ignore
}
}
private String cleanupPath(String path) {
path = path.trim();
// No need for current dir path
if (path.startsWith("./")) {
path = path.substring(2);
}
if (path.toLowerCase().endsWith(".jar") || path.toLowerCase().endsWith(".zip")) {
return path;
}
if (path.endsWith("/*")) {
path = path.substring(0, path.length() - 1);
}
else {
// It's a directory
if (!path.endsWith("/") && !path.equals(".")) {
path = path + "/";
}
}
return path;
}
public static void main(String[] args) throws Exception {
PropertiesLauncher launcher = new PropertiesLauncher();
args = launcher.getArgs(args);
launcher.launch(args);
}
public static String toCamelCase(CharSequence string) {
if (string == null) {
return null;
}
StringBuilder builder = new StringBuilder();
Matcher matcher = WORD_SEPARATOR.matcher(string);
int pos = 0;
while (matcher.find()) {
builder.append(capitalize(string.subSequence(pos, matcher.end()).toString()));
pos = matcher.end();
}
builder.append(capitalize(string.subSequence(pos, string.length()).toString()));
return builder.toString();
}
private static String capitalize(String str) {
return Character.toUpperCase(str.charAt(0)) + str.substring(1);
}
private void debug(String message) {
if (Boolean.getBoolean(DEBUG)) {
System.out.println(message);
}
}
/**
* Convenience class for finding nested archives that have a prefix in their file path
* (e.g. "lib/").
*/
private static final class PrefixMatchingArchiveFilter implements EntryFilter {
private final String prefix;
private final ArchiveEntryFilter filter = new ArchiveEntryFilter();
private PrefixMatchingArchiveFilter(String prefix) {
this.prefix = prefix;
}
@Override
public boolean matches(Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(this.prefix);
}
return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
}
}
/**
* Convenience class for finding nested archives (archive entries that can be
* classpath entries).
*/
private static final class ArchiveEntryFilter implements EntryFilter {
private static final String DOT_JAR = ".jar";
private static final String DOT_ZIP = ".zip";
@Override
public boolean matches(Entry entry) {
return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP);
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2012-2016 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.boot.loader;
import org.springframework.boot.loader.archive.Archive;
/**
* {@link Launcher} for WAR based archives. This launcher for standard WAR archives.
* Supports dependencies in {@code WEB-INF/lib} as well as {@code WEB-INF/lib-provided},
* classes are loaded from {@code WEB-INF/classes}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class WarLauncher extends ExecutableArchiveLauncher {
private static final String WEB_INF = "WEB-INF/";
private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
private static final String WEB_INF_LIB = WEB_INF + "lib/";
private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";
public WarLauncher() {
super();
}
protected WarLauncher(Archive archive) {
super(archive);
}
@Override
public boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(WEB_INF_CLASSES);
}
else {
return entry.getName().startsWith(WEB_INF_LIB)
|| entry.getName().startsWith(WEB_INF_LIB_PROVIDED);
}
}
public static void main(String[] args) throws Exception {
new WarLauncher().launch(args);
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2012-2016 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.boot.loader.archive;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.jar.Manifest;
import org.springframework.boot.loader.Launcher;
/**
* An archive that can be launched by the {@link Launcher}.
*
* @author Phillip Webb
* @see JarFileArchive
*/
public interface Archive extends Iterable<Archive.Entry> {
/**
* Returns a URL that can be used to load the archive.
* @return the archive URL
* @throws MalformedURLException if the URL is malformed
*/
URL getUrl() throws MalformedURLException;
/**
* Returns the manifest of the archive.
* @return the manifest
* @throws IOException if the manifest cannot be read
*/
Manifest getManifest() throws IOException;
/**
* Returns nested {@link Archive}s for entries that match the specified filter.
* @param filter the filter used to limit entries
* @return nested archives
* @throws IOException if nested archives cannot be read
*/
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
/**
* Represents a single entry in the archive.
*/
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();
}
/**
* Strategy interface to filter {@link Entry Entries}.
*/
interface EntryFilter {
/**
* Apply the jar entry filter.
* @param entry the entry to filter
* @return {@code true} if the filter matches
*/
boolean matches(Entry entry);
}
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2012-2017 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.boot.loader.archive;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.jar.Manifest;
/**
* {@link Archive} implementation backed by an exploded archive directory.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class ExplodedArchive implements Archive {
private static final Set<String> SKIPPED_NAMES = new HashSet<>(
Arrays.asList(".", ".."));
private final File root;
private final boolean recursive;
private File manifestFile;
private Manifest manifest;
/**
* Create a new {@link ExplodedArchive} instance.
* @param root the root folder
*/
public ExplodedArchive(File root) {
this(root, true);
}
/**
* Create a new {@link ExplodedArchive} instance.
* @param root the root folder
* @param recursive if recursive searching should be used to locate the manifest.
* Defaults to {@code true}, folders with a large tree might want to set this to
* {@code
* false}.
*/
public ExplodedArchive(File root, boolean recursive) {
if (!root.exists() || !root.isDirectory()) {
throw new IllegalArgumentException("Invalid source folder " + root);
}
this.root = root;
this.recursive = recursive;
this.manifestFile = getManifestFile(root);
}
private File getManifestFile(File root) {
File metaInf = new File(root, "META-INF");
return new File(metaInf, "MANIFEST.MF");
}
@Override
public URL getUrl() throws MalformedURLException {
return this.root.toURI().toURL();
}
@Override
public Manifest getManifest() throws IOException {
if (this.manifest == null && this.manifestFile.exists()) {
try (FileInputStream inputStream = new FileInputStream(this.manifestFile)) {
this.manifest = new Manifest(inputStream);
}
}
return this.manifest;
}
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
@Override
public Iterator<Entry> iterator() {
return new FileEntryIterator(this.root, this.recursive);
}
protected Archive getNestedArchive(Entry entry) throws IOException {
File file = ((FileEntry) entry).getFile();
return (file.isDirectory() ? new ExplodedArchive(file)
: new JarFileArchive(file));
}
@Override
public String toString() {
try {
return getUrl().toString();
}
catch (Exception ex) {
return "exploded archive";
}
}
/**
* File based {@link Entry} {@link Iterator}.
*/
private static class FileEntryIterator implements Iterator<Entry> {
private final Comparator<File> entryComparator = new EntryComparator();
private final File root;
private final boolean recursive;
private final Deque<Iterator<File>> stack = new LinkedList<>();
private File current;
FileEntryIterator(File root, boolean recursive) {
this.root = root;
this.recursive = recursive;
this.stack.add(listFiles(root));
this.current = poll();
}
@Override
public boolean hasNext() {
return this.current != null;
}
@Override
public Entry next() {
if (this.current == null) {
throw new NoSuchElementException();
}
File file = this.current;
if (file.isDirectory()
&& (this.recursive || file.getParentFile().equals(this.root))) {
this.stack.addFirst(listFiles(file));
}
this.current = poll();
String name = file.toURI().getPath()
.substring(this.root.toURI().getPath().length());
return new FileEntry(name, file);
}
private Iterator<File> listFiles(File file) {
File[] files = file.listFiles();
if (files == null) {
return Collections.<File>emptyList().iterator();
}
Arrays.sort(files, this.entryComparator);
return Arrays.asList(files).iterator();
}
private File poll() {
while (!this.stack.isEmpty()) {
while (this.stack.peek().hasNext()) {
File file = this.stack.peek().next();
if (!SKIPPED_NAMES.contains(file.getName())) {
return file;
}
}
this.stack.poll();
}
return null;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
/**
* {@link Comparator} that orders {@link File} entries by their absolute paths.
*/
private static class EntryComparator implements Comparator<File> {
@Override
public int compare(File o1, File o2) {
return o1.getAbsolutePath().compareTo(o2.getAbsolutePath());
}
}
}
/**
* {@link Entry} backed by a File.
*/
private static class FileEntry implements Entry {
private final String name;
private final File file;
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;
}
}
}

View File

@@ -0,0 +1,225 @@
/*
* Copyright 2012-2017 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.boot.loader.archive;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.jar.JarEntry;
import java.util.jar.Manifest;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.boot.loader.jar.JarFile;
/**
* {@link Archive} implementation backed by a {@link JarFile}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class JarFileArchive implements Archive {
private static final String UNPACK_MARKER = "UNPACK:";
private static final int BUFFER_SIZE = 32 * 1024;
private final JarFile jarFile;
private URL url;
private File tempUnpackFolder;
public JarFileArchive(File file) throws IOException {
this(file, null);
}
public JarFileArchive(File file, URL url) throws IOException {
this(new JarFile(file));
this.url = url;
}
public JarFileArchive(JarFile jarFile) {
this.jarFile = jarFile;
}
@Override
public URL getUrl() throws MalformedURLException {
if (this.url != null) {
return this.url;
}
return this.jarFile.getUrl();
}
@Override
public Manifest getManifest() throws IOException {
return this.jarFile.getManifest();
}
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
@Override
public Iterator<Entry> iterator() {
return new EntryIterator(this.jarFile.entries());
}
protected Archive getNestedArchive(Entry entry) throws IOException {
JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry();
if (jarEntry.getComment().startsWith(UNPACK_MARKER)) {
return getUnpackedNestedArchive(jarEntry);
}
try {
JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
return new JarFileArchive(jarFile);
}
catch (Exception ex) {
throw new IllegalStateException(
"Failed to get nested archive for entry " + entry.getName(), ex);
}
}
private Archive getUnpackedNestedArchive(JarEntry jarEntry) throws IOException {
String name = jarEntry.getName();
if (name.lastIndexOf("/") != -1) {
name = name.substring(name.lastIndexOf("/") + 1);
}
File file = new File(getTempUnpackFolder(), name);
if (!file.exists() || file.length() != jarEntry.getSize()) {
unpack(jarEntry, file);
}
return new JarFileArchive(file, file.toURI().toURL());
}
private File getTempUnpackFolder() {
if (this.tempUnpackFolder == null) {
File tempFolder = new File(System.getProperty("java.io.tmpdir"));
this.tempUnpackFolder = createUnpackFolder(tempFolder);
}
return this.tempUnpackFolder;
}
private File createUnpackFolder(File parent) {
int attempts = 0;
while (attempts++ < 1000) {
String fileName = new File(this.jarFile.getName()).getName();
File unpackFolder = new File(parent,
fileName + "-spring-boot-libs-" + UUID.randomUUID());
if (unpackFolder.mkdirs()) {
return unpackFolder;
}
}
throw new IllegalStateException(
"Failed to create unpack folder in directory '" + parent + "'");
}
private void unpack(JarEntry entry, File file) throws IOException {
try (InputStream inputStream = this.jarFile.getInputStream(entry,
ResourceAccess.ONCE);
OutputStream outputStream = new FileOutputStream(file)) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead = -1;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
}
@Override
public String toString() {
try {
return getUrl().toString();
}
catch (Exception ex) {
return "jar archive";
}
}
/**
* {@link Archive.Entry} iterator implementation backed by {@link JarEntry}.
*/
private static class EntryIterator implements Iterator<Entry> {
private final Enumeration<JarEntry> enumeration;
EntryIterator(Enumeration<JarEntry> enumeration) {
this.enumeration = enumeration;
}
@Override
public boolean hasNext() {
return this.enumeration.hasMoreElements();
}
@Override
public Entry next() {
return new JarFileEntry(this.enumeration.nextElement());
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
}
/**
* {@link Archive.Entry} implementation backed by a {@link JarEntry}.
*/
private static class JarFileEntry implements Entry {
private final JarEntry jarEntry;
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();
}
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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.
*/
/**
* Abstraction over logical Archives be they backed by a JAR file or unpacked into a
* folder.
*
* @see org.springframework.boot.loader.archive.Archive
*/
package org.springframework.boot.loader.archive;

View File

@@ -0,0 +1,60 @@
/*
* 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.boot.loader.data;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* {@link RandomAccessData} implementation backed by a byte array.
*
* @author Phillip Webb
*/
public class ByteArrayRandomAccessData implements RandomAccessData {
private final byte[] bytes;
private final long offset;
private final long length;
public ByteArrayRandomAccessData(byte[] bytes) {
this(bytes, 0, (bytes == null ? 0 : bytes.length));
}
public ByteArrayRandomAccessData(byte[] bytes, long offset, long length) {
this.bytes = (bytes == null ? new byte[0] : bytes);
this.offset = offset;
this.length = length;
}
@Override
public InputStream getInputStream(ResourceAccess access) {
return new ByteArrayInputStream(this.bytes, (int) this.offset, (int) this.length);
}
@Override
public RandomAccessData getSubsection(long offset, long length) {
return new ByteArrayRandomAccessData(this.bytes, this.offset + offset, length);
}
@Override
public long getSize() {
return this.length;
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2012-2015 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.boot.loader.data;
import java.io.IOException;
import java.io.InputStream;
/**
* Interface that provides read-only random access to some underlying data.
* Implementations must allow concurrent reads in a thread-safe manner.
*
* @author Phillip Webb
*/
public interface RandomAccessData {
/**
* Returns an {@link InputStream} that can be used to read the underlying data. The
* caller is responsible close the underlying stream.
* @param access hint indicating how the underlying data should be accessed
* @return a new input stream that can be used to read the underlying data.
* @throws IOException if the stream cannot be opened
*/
InputStream getInputStream(ResourceAccess access) throws IOException;
/**
* Returns a new {@link RandomAccessData} for a specific subsection of this data.
* @param offset the offset of the subsection
* @param length the length of the subsection
* @return the subsection data
*/
RandomAccessData getSubsection(long offset, long length);
/**
* Returns the size of the data.
* @return the size
*/
long getSize();
/**
* Lock modes for accessing the underlying resource.
*/
enum ResourceAccess {
/**
* Obtain access to the underlying resource once and keep it until the stream is
* closed.
*/
ONCE,
/**
* Obtain access to the underlying resource on each read, releasing it when done.
*/
PER_READ
}
}

View File

@@ -0,0 +1,279 @@
/*
* Copyright 2012-2017 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.boot.loader.data;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
/**
* {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}.
*
* @author Phillip Webb
*/
public class RandomAccessDataFile implements RandomAccessData {
private static final int DEFAULT_CONCURRENT_READS = 4;
private final File file;
private final FilePool filePool;
private final long offset;
private final long length;
/**
* Create a new {@link RandomAccessDataFile} backed by the specified file.
* @param file the underlying file
* @throws IllegalArgumentException if the file is null or does not exist
* @see #RandomAccessDataFile(File, int)
*/
public RandomAccessDataFile(File file) {
this(file, DEFAULT_CONCURRENT_READS);
}
/**
* Create a new {@link RandomAccessDataFile} backed by the specified file.
* @param file the underlying file
* @param concurrentReads the maximum number of concurrent reads allowed on the
* underlying file before blocking
* @throws IllegalArgumentException if the file is null or does not exist
* @see #RandomAccessDataFile(File)
*/
public RandomAccessDataFile(File file, int concurrentReads) {
if (file == null) {
throw new IllegalArgumentException("File must not be null");
}
if (!file.exists()) {
throw new IllegalArgumentException("File must exist");
}
this.file = file;
this.filePool = new FilePool(file, concurrentReads);
this.offset = 0L;
this.length = file.length();
}
/**
* Private constructor used to create a {@link #getSubsection(long, long) subsection}.
* @param file the underlying file
* @param pool the underlying pool
* @param offset the offset of the section
* @param length the length of the section
*/
private RandomAccessDataFile(File file, FilePool pool, long offset, long length) {
this.file = file;
this.filePool = pool;
this.offset = offset;
this.length = length;
}
/**
* Returns the underlying File.
* @return the underlying file
*/
public File getFile() {
return this.file;
}
@Override
public InputStream getInputStream(ResourceAccess access) throws IOException {
return new DataInputStream(access);
}
@Override
public RandomAccessData getSubsection(long offset, long length) {
if (offset < 0 || length < 0 || offset + length > this.length) {
throw new IndexOutOfBoundsException();
}
return new RandomAccessDataFile(this.file, this.filePool, this.offset + offset,
length);
}
@Override
public long getSize() {
return this.length;
}
public void close() throws IOException {
this.filePool.close();
}
/**
* {@link RandomAccessDataInputStream} implementation for the
* {@link RandomAccessDataFile}.
*/
private class DataInputStream extends InputStream {
private RandomAccessFile file;
private int position;
DataInputStream(ResourceAccess access) throws IOException {
if (access == ResourceAccess.ONCE) {
this.file = new RandomAccessFile(RandomAccessDataFile.this.file, "r");
this.file.seek(RandomAccessDataFile.this.offset);
}
}
@Override
public int read() throws IOException {
return doRead(null, 0, 1);
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b == null ? 0 : b.length);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (b == null) {
throw new NullPointerException("Bytes must not be null");
}
return doRead(b, off, len);
}
/**
* Perform the actual read.
* @param b the bytes to read or {@code null} when reading a single byte
* @param off the offset of the byte array
* @param len the length of data to read
* @return the number of bytes read into {@code b} or the actual read byte if
* {@code b} is {@code null}. Returns -1 when the end of the stream is reached
* @throws IOException in case of I/O errors
*/
public int doRead(byte[] b, int off, int len) throws IOException {
if (len == 0) {
return 0;
}
int cappedLen = cap(len);
if (cappedLen <= 0) {
return -1;
}
RandomAccessFile file = this.file;
try {
if (file == null) {
file = RandomAccessDataFile.this.filePool.acquire();
file.seek(RandomAccessDataFile.this.offset + this.position);
}
if (b == null) {
int rtn = file.read();
moveOn(rtn == -1 ? 0 : 1);
return rtn;
}
else {
return (int) moveOn(file.read(b, off, cappedLen));
}
}
finally {
if (this.file == null && file != null) {
RandomAccessDataFile.this.filePool.release(file);
}
}
}
@Override
public long skip(long n) throws IOException {
return (n <= 0 ? 0 : moveOn(cap(n)));
}
@Override
public void close() throws IOException {
if (this.file != null) {
this.file.close();
}
}
/**
* Cap the specified value such that it cannot exceed the number of bytes
* remaining.
* @param n the value to cap
* @return the capped value
*/
private int cap(long n) {
return (int) Math.min(RandomAccessDataFile.this.length - this.position, n);
}
/**
* Move the stream position forwards the specified amount.
* @param amount the amount to move
* @return the amount moved
*/
private long moveOn(int amount) {
this.position += amount;
return amount;
}
}
/**
* Manage a pool that can be used to perform concurrent reads on the underlying
* {@link RandomAccessFile}.
*/
static class FilePool {
private final File file;
private final int size;
private final Semaphore available;
private final Queue<RandomAccessFile> files;
FilePool(File file, int size) {
this.file = file;
this.size = size;
this.available = new Semaphore(size);
this.files = new ConcurrentLinkedQueue<>();
}
public RandomAccessFile acquire() throws IOException {
this.available.acquireUninterruptibly();
RandomAccessFile file = this.files.poll();
if (file != null) {
return file;
}
return new RandomAccessFile(this.file, "r");
}
public void release(RandomAccessFile file) {
this.files.add(file);
this.available.release();
}
public void close() throws IOException {
this.available.acquireUninterruptibly(this.size);
try {
RandomAccessFile pooledFile = this.files.poll();
while (pooledFile != null) {
pooledFile.close();
pooledFile = this.files.poll();
}
}
finally {
this.available.release(this.size);
}
}
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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.
*/
/**
* Classes and interfaces to allows random access to a block of data.
*
* @see org.springframework.boot.loader.data.RandomAccessData
*/
package org.springframework.boot.loader.data;

View File

@@ -0,0 +1,233 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.nio.charset.Charset;
/**
* Simple wrapper around a byte array that represents an ASCII. Used for performance
* reasons to save constructing Strings for ZIP data.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
final class AsciiBytes {
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final byte[] bytes;
private final int offset;
private final int length;
private String string;
private int hash;
/**
* Create a new {@link AsciiBytes} from the specified String.
* @param string the source string
*/
AsciiBytes(String string) {
this(string.getBytes(UTF_8));
this.string = string;
}
/**
* Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes
* are not expected to change.
* @param bytes the source bytes
*/
AsciiBytes(byte[] bytes) {
this(bytes, 0, bytes.length);
}
/**
* Create a new {@link AsciiBytes} from the specified bytes. NOTE: underlying bytes
* are not expected to change.
* @param bytes the source bytes
* @param offset the offset
* @param length the length
*/
AsciiBytes(byte[] bytes, int offset, int length) {
if (offset < 0 || length < 0 || (offset + length) > bytes.length) {
throw new IndexOutOfBoundsException();
}
this.bytes = bytes;
this.offset = offset;
this.length = length;
}
public int length() {
return this.length;
}
public boolean startsWith(AsciiBytes prefix) {
if (this == prefix) {
return true;
}
if (prefix.length > this.length) {
return false;
}
for (int i = 0; i < prefix.length; i++) {
if (this.bytes[i + this.offset] != prefix.bytes[i + prefix.offset]) {
return false;
}
}
return true;
}
public boolean endsWith(AsciiBytes postfix) {
if (this == postfix) {
return true;
}
if (postfix.length > this.length) {
return false;
}
for (int i = 0; i < postfix.length; i++) {
if (this.bytes[this.offset + (this.length - 1)
- i] != postfix.bytes[postfix.offset + (postfix.length - 1) - i]) {
return false;
}
}
return true;
}
public AsciiBytes substring(int beginIndex) {
return substring(beginIndex, this.length);
}
public AsciiBytes substring(int beginIndex, int endIndex) {
int length = endIndex - beginIndex;
if (this.offset + length > this.bytes.length) {
throw new IndexOutOfBoundsException();
}
return new AsciiBytes(this.bytes, this.offset + beginIndex, length);
}
public AsciiBytes append(String string) {
if (string == null || string.isEmpty()) {
return this;
}
return append(string.getBytes(UTF_8));
}
public AsciiBytes append(AsciiBytes asciiBytes) {
if (asciiBytes == null || asciiBytes.length() == 0) {
return this;
}
return append(asciiBytes.bytes);
}
public AsciiBytes append(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return this;
}
byte[] combined = new byte[this.length + bytes.length];
System.arraycopy(this.bytes, this.offset, combined, 0, this.length);
System.arraycopy(bytes, 0, combined, this.length, bytes.length);
return new AsciiBytes(combined);
}
@Override
public String toString() {
if (this.string == null) {
this.string = new String(this.bytes, this.offset, this.length, UTF_8);
}
return this.string;
}
@Override
public int hashCode() {
int hash = this.hash;
if (hash == 0 && this.bytes.length > 0) {
for (int i = this.offset; i < this.offset + this.length; i++) {
int b = this.bytes[i];
if (b < 0) {
b = b & 0x7F;
int limit;
int excess = 0x80;
if (b < 96) {
limit = 1;
excess += 0x40 << 6;
}
else if (b < 112) {
limit = 2;
excess += (0x60 << 12) + (0x80 << 6);
}
else {
limit = 3;
excess += (0x70 << 18) + (0x80 << 12) + (0x80 << 6);
}
for (int j = 0; j < limit; j++) {
b = (b << 6) + (this.bytes[++i] & 0xFF);
}
b -= excess;
}
if (b <= 0xFFFF) {
hash = 31 * hash + b;
}
else {
hash = 31 * hash + ((b >> 0xA) + 0xD7C0);
hash = 31 * hash + ((b & 0x3FF) + 0xDC00);
}
}
this.hash = hash;
}
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj.getClass().equals(AsciiBytes.class)) {
AsciiBytes other = (AsciiBytes) obj;
if (this.length == other.length) {
for (int i = 0; i < this.length; i++) {
if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) {
return false;
}
}
return true;
}
}
return false;
}
static String toString(byte[] bytes) {
return new String(bytes, UTF_8);
}
public static int hashCode(String string) {
// We're compatible with String's hashCode().
return string.hashCode();
}
public static int hashCode(int hash, String string) {
for (int i = 0; i < string.length(); i++) {
hash = 31 * hash + string.charAt(i);
}
return hash;
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.io.IOException;
import java.io.InputStream;
import org.springframework.boot.loader.data.RandomAccessData;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
/**
* Utilities for dealing with bytes from ZIP files.
*
* @author Phillip Webb
*/
final class Bytes {
private static final byte[] EMPTY_BYTES = new byte[] {};
private Bytes() {
}
public static byte[] get(RandomAccessData data) throws IOException {
try (InputStream inputStream = data.getInputStream(ResourceAccess.ONCE)) {
return get(inputStream, data.getSize());
}
}
public static byte[] get(InputStream inputStream, long length) throws IOException {
if (length == 0) {
return EMPTY_BYTES;
}
byte[] bytes = new byte[(int) length];
if (!fill(inputStream, bytes)) {
throw new IOException("Unable to read bytes");
}
return bytes;
}
public static boolean fill(InputStream inputStream, byte[] bytes) throws IOException {
return fill(inputStream, bytes, 0, bytes.length);
}
private static boolean fill(InputStream inputStream, byte[] bytes, int offset,
int length) throws IOException {
while (length > 0) {
int read = inputStream.read(bytes, offset, length);
if (read == -1) {
return false;
}
offset += read;
length = -read;
}
return true;
}
public static long littleEndianValue(byte[] bytes, int offset, int length) {
long value = 0;
for (int i = length - 1; i >= 0; i--) {
value = ((value << 8) | (bytes[offset + i] & 0xFF));
}
return value;
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.io.IOException;
import org.springframework.boot.loader.data.RandomAccessData;
/**
* A ZIP File "End of central directory record" (EOCD).
*
* @author Phillip Webb
* @author Andy Wilkinson
* @see <a href="http://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
*/
class CentralDirectoryEndRecord {
private static final int MINIMUM_SIZE = 22;
private static final int MAXIMUM_COMMENT_LENGTH = 0xFFFF;
private static final int MAXIMUM_SIZE = MINIMUM_SIZE + MAXIMUM_COMMENT_LENGTH;
private static final int SIGNATURE = 0x06054b50;
private static final int COMMENT_LENGTH_OFFSET = 20;
private static final int READ_BLOCK_SIZE = 256;
private byte[] block;
private int offset;
private int size;
/**
* Create a new {@link CentralDirectoryEndRecord} instance from the specified
* {@link RandomAccessData}, searching backwards from the end until a valid block is
* located.
* @param data the source data
* @throws IOException in case of I/O errors
*/
CentralDirectoryEndRecord(RandomAccessData data) throws IOException {
this.block = createBlockFromEndOfData(data, READ_BLOCK_SIZE);
this.size = MINIMUM_SIZE;
this.offset = this.block.length - this.size;
while (!isValid()) {
this.size++;
if (this.size > this.block.length) {
if (this.size >= MAXIMUM_SIZE || this.size > data.getSize()) {
throw new IOException("Unable to find ZIP central directory "
+ "records after reading " + this.size + " bytes");
}
this.block = createBlockFromEndOfData(data, this.size + READ_BLOCK_SIZE);
}
this.offset = this.block.length - this.size;
}
}
private byte[] createBlockFromEndOfData(RandomAccessData data, int size)
throws IOException {
int length = (int) Math.min(data.getSize(), size);
return Bytes.get(data.getSubsection(data.getSize() - length, length));
}
private boolean isValid() {
if (this.block.length < MINIMUM_SIZE
|| Bytes.littleEndianValue(this.block, this.offset + 0, 4) != SIGNATURE) {
return false;
}
// Total size must be the structure size + comment
long commentLength = Bytes.littleEndianValue(this.block,
this.offset + COMMENT_LENGTH_OFFSET, 2);
return this.size == MINIMUM_SIZE + commentLength;
}
/**
* Returns the location in the data that the archive actually starts. For most files
* the archive data will start at 0, however, it is possible to have prefixed bytes
* (often used for startup scripts) at the beginning of the data.
* @param data the source data
* @return the offset within the data where the archive begins
*/
public long getStartOfArchive(RandomAccessData data) {
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
long specifiedOffset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
long actualOffset = data.getSize() - this.size - length;
return actualOffset - specifiedOffset;
}
/**
* Return the bytes of the "Central directory" based on the offset indicated in this
* record.
* @param data the source data
* @return the central directory data
*/
public RandomAccessData getCentralDirectory(RandomAccessData data) {
long offset = Bytes.littleEndianValue(this.block, this.offset + 16, 4);
long length = Bytes.littleEndianValue(this.block, this.offset + 12, 4);
return data.getSubsection(offset, length);
}
/**
* Return the number of ZIP entries in the file.
* @return the number of records in the zip
*/
public int getNumberOfRecords() {
long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2);
if (numberOfRecords == 0xFFFF) {
throw new IllegalStateException("Zip64 archives are not supported");
}
return (int) numberOfRecords;
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2012-2016 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.boot.loader.jar;
import java.io.IOException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import org.springframework.boot.loader.data.RandomAccessData;
/**
* A ZIP File "Central directory file header record" (CDFH).
*
* @author Phillip Webb
* @author Andy Wilkinson
* @see <a href="http://en.wikipedia.org/wiki/Zip_%28file_format%29">Zip File Format</a>
*/
final class CentralDirectoryFileHeader implements FileHeader {
private static final AsciiBytes SLASH = new AsciiBytes("/");
private static final byte[] NO_EXTRA = {};
private static final AsciiBytes NO_COMMENT = new AsciiBytes("");
private byte[] header;
private int headerOffset;
private AsciiBytes name;
private byte[] extra;
private AsciiBytes comment;
private long localHeaderOffset;
CentralDirectoryFileHeader() {
}
CentralDirectoryFileHeader(byte[] header, int headerOffset, AsciiBytes name,
byte[] extra, AsciiBytes comment, long localHeaderOffset) {
super();
this.header = header;
this.headerOffset = headerOffset;
this.name = name;
this.extra = extra;
this.comment = comment;
this.localHeaderOffset = localHeaderOffset;
}
void load(byte[] data, int dataOffset, RandomAccessData variableData,
int variableOffset, JarEntryFilter filter) throws IOException {
// Load fixed part
this.header = data;
this.headerOffset = dataOffset;
long nameLength = Bytes.littleEndianValue(data, dataOffset + 28, 2);
long extraLength = Bytes.littleEndianValue(data, dataOffset + 30, 2);
long commentLength = Bytes.littleEndianValue(data, dataOffset + 32, 2);
this.localHeaderOffset = Bytes.littleEndianValue(data, dataOffset + 42, 4);
// Load variable part
dataOffset += 46;
if (variableData != null) {
data = Bytes.get(variableData.getSubsection(variableOffset + 46,
nameLength + extraLength + commentLength));
dataOffset = 0;
}
this.name = new AsciiBytes(data, dataOffset, (int) nameLength);
if (filter != null) {
this.name = filter.apply(this.name);
}
this.extra = NO_EXTRA;
this.comment = NO_COMMENT;
if (extraLength > 0) {
this.extra = new byte[(int) extraLength];
System.arraycopy(data, (int) (dataOffset + nameLength), this.extra, 0,
this.extra.length);
}
if (commentLength > 0) {
this.comment = new AsciiBytes(data,
(int) (dataOffset + nameLength + extraLength), (int) commentLength);
}
}
public AsciiBytes getName() {
return this.name;
}
@Override
public boolean hasName(String name, String suffix) {
return this.name.equals(new AsciiBytes(suffix == null ? name : name + suffix));
}
public boolean isDirectory() {
return this.name.endsWith(SLASH);
}
@Override
public int getMethod() {
return (int) Bytes.littleEndianValue(this.header, this.headerOffset + 10, 2);
}
public long getTime() {
long date = Bytes.littleEndianValue(this.header, this.headerOffset + 14, 2);
long time = Bytes.littleEndianValue(this.header, this.headerOffset + 12, 2);
return decodeMsDosFormatDateTime(date, time).getTimeInMillis();
}
/**
* Decode MS-DOS Date Time details. See
* <a href="http://mindprod.com/jgloss/zip.html">mindprod.com/jgloss/zip.html</a> for
* more details of the format.
* @param date the date part
* @param time the time part
* @return a {@link Calendar} containing the decoded date.
*/
private Calendar decodeMsDosFormatDateTime(long date, long time) {
int year = (int) ((date >> 9) & 0x7F) + 1980;
int month = (int) ((date >> 5) & 0xF) - 1;
int day = (int) (date & 0x1F);
int hours = (int) ((time >> 11) & 0x1F);
int minutes = (int) ((time >> 5) & 0x3F);
int seconds = (int) ((time << 1) & 0x3E);
return new GregorianCalendar(year, month, day, hours, minutes, seconds);
}
public long getCrc() {
return Bytes.littleEndianValue(this.header, this.headerOffset + 16, 4);
}
@Override
public long getCompressedSize() {
return Bytes.littleEndianValue(this.header, this.headerOffset + 20, 4);
}
@Override
public long getSize() {
return Bytes.littleEndianValue(this.header, this.headerOffset + 24, 4);
}
public byte[] getExtra() {
return this.extra;
}
public AsciiBytes getComment() {
return this.comment;
}
@Override
public long getLocalHeaderOffset() {
return this.localHeaderOffset;
}
@Override
public CentralDirectoryFileHeader clone() {
byte[] header = new byte[46];
System.arraycopy(this.header, this.headerOffset, header, 0, header.length);
return new CentralDirectoryFileHeader(header, 0, this.name, header, this.comment,
this.localHeaderOffset);
}
public static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data,
int offset, JarEntryFilter filter) throws IOException {
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
byte[] bytes = Bytes.get(data.getSubsection(offset, 46));
fileHeader.load(bytes, 0, data, offset, filter);
return fileHeader;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.loader.data.RandomAccessData;
/**
* Parses the central directory from a JAR file.
*
* @author Phillip Webb
* @see CentralDirectoryVisitor
*/
class CentralDirectoryParser {
private int CENTRAL_DIRECTORY_HEADER_BASE_SIZE = 46;
private final List<CentralDirectoryVisitor> visitors = new ArrayList<>();
public <T extends CentralDirectoryVisitor> T addVisitor(T visitor) {
this.visitors.add(visitor);
return visitor;
}
/**
* Parse the source data, triggering {@link CentralDirectoryVisitor visitors}.
* @param data the source data
* @param skipPrefixBytes if prefix bytes should be skipped
* @return The actual archive data without any prefix bytes
* @throws IOException on error
*/
public RandomAccessData parse(RandomAccessData data, boolean skipPrefixBytes)
throws IOException {
CentralDirectoryEndRecord endRecord = new CentralDirectoryEndRecord(data);
if (skipPrefixBytes) {
data = getArchiveData(endRecord, data);
}
RandomAccessData centralDirectoryData = endRecord.getCentralDirectory(data);
visitStart(endRecord, centralDirectoryData);
parseEntries(endRecord, centralDirectoryData);
visitEnd();
return data;
}
private void parseEntries(CentralDirectoryEndRecord endRecord,
RandomAccessData centralDirectoryData) throws IOException {
byte[] bytes = Bytes.get(centralDirectoryData);
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
int dataOffset = 0;
for (int i = 0; i < endRecord.getNumberOfRecords(); i++) {
fileHeader.load(bytes, dataOffset, null, 0, null);
visitFileHeader(dataOffset, fileHeader);
dataOffset += this.CENTRAL_DIRECTORY_HEADER_BASE_SIZE
+ fileHeader.getName().length() + fileHeader.getComment().length()
+ fileHeader.getExtra().length;
}
}
private RandomAccessData getArchiveData(CentralDirectoryEndRecord endRecord,
RandomAccessData data) {
long offset = endRecord.getStartOfArchive(data);
if (offset == 0) {
return data;
}
return data.getSubsection(offset, data.getSize() - offset);
}
private void visitStart(CentralDirectoryEndRecord endRecord,
RandomAccessData centralDirectoryData) {
for (CentralDirectoryVisitor visitor : this.visitors) {
visitor.visitStart(endRecord, centralDirectoryData);
}
}
private void visitFileHeader(int dataOffset, CentralDirectoryFileHeader fileHeader) {
for (CentralDirectoryVisitor visitor : this.visitors) {
visitor.visitFileHeader(fileHeader, dataOffset);
}
}
private void visitEnd() {
for (CentralDirectoryVisitor visitor : this.visitors) {
visitor.visitEnd();
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2016 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.boot.loader.jar;
import org.springframework.boot.loader.data.RandomAccessData;
/**
* Callback visitor triggered by {@link CentralDirectoryParser}.
*
* @author Phillip Webb
*/
interface CentralDirectoryVisitor {
void visitStart(CentralDirectoryEndRecord endRecord,
RandomAccessData centralDirectoryData);
void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset);
void visitEnd();
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2012-2016 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.boot.loader.jar;
import java.util.zip.ZipEntry;
/**
* A file header record that has been loaded from a Jar file.
*
* @author Phillip Webb
* @see JarEntry
* @see CentralDirectoryFileHeader
*/
interface FileHeader {
/**
* Returns {@code true} if the header has the given name.
* @param name the name to test
* @param suffix an additional suffix (or {@code null})
* @return {@code true} if the header has the given name
*/
boolean hasName(String name, String suffix);
/**
* Return the offset of the load file header within the archive data.
* @return the local header offset
*/
long getLocalHeaderOffset();
/**
* Return the compressed size of the entry.
* @return the compressed size.
*/
long getCompressedSize();
/**
* Return the uncompressed size of the entry.
* @return the uncompressed size.
*/
long getSize();
/**
* Return the method used to compress the data.
* @return the zip compression method
* @see ZipEntry#STORED
* @see ZipEntry#DEFLATED
*/
int getMethod();
}

View File

@@ -0,0 +1,348 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.io.File;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.net.URLStreamHandler;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* {@link URLStreamHandler} for Spring Boot loader {@link JarFile}s.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @see JarFile#registerUrlProtocolHandler()
*/
public class Handler extends URLStreamHandler {
// NOTE: in order to be found as a URL protocol handler, this class must be public,
// must be named Handler and must be in a package ending '.jar'
private static final String JAR_PROTOCOL = "jar:";
private static final String FILE_PROTOCOL = "file:";
private static final String SEPARATOR = "!/";
private static final String[] FALLBACK_HANDLERS = {
"sun.net.www.protocol.jar.Handler" };
private static final Method OPEN_CONNECTION_METHOD;
static {
Method method = null;
try {
method = URLStreamHandler.class.getDeclaredMethod("openConnection",
URL.class);
}
catch (Exception ex) {
// Swallow and ignore
}
OPEN_CONNECTION_METHOD = method;
}
private static SoftReference<Map<File, JarFile>> rootFileCache;
static {
rootFileCache = new SoftReference<>(null);
}
private final JarFile jarFile;
private URLStreamHandler fallbackHandler;
public Handler() {
this(null);
}
public Handler(JarFile jarFile) {
this.jarFile = jarFile;
}
@Override
protected URLConnection openConnection(URL url) throws IOException {
if (this.jarFile != null) {
return JarURLConnection.get(url, this.jarFile);
}
try {
return JarURLConnection.get(url, getRootJarFileFromUrl(url));
}
catch (Exception ex) {
return openFallbackConnection(url, ex);
}
}
private URLConnection openFallbackConnection(URL url, Exception reason)
throws IOException {
try {
return openConnection(getFallbackHandler(), url);
}
catch (Exception ex) {
if (reason instanceof IOException) {
log(false, "Unable to open fallback handler", ex);
throw (IOException) reason;
}
log(true, "Unable to open fallback handler", ex);
if (reason instanceof RuntimeException) {
throw (RuntimeException) reason;
}
throw new IllegalStateException(reason);
}
}
private void log(boolean warning, String message, Exception cause) {
try {
Logger.getLogger(getClass().getName())
.log((warning ? Level.WARNING : Level.FINEST), message, cause);
}
catch (Exception ex) {
if (warning) {
System.err.println("WARNING: " + message);
}
}
}
private URLStreamHandler getFallbackHandler() {
if (this.fallbackHandler != null) {
return this.fallbackHandler;
}
for (String handlerClassName : FALLBACK_HANDLERS) {
try {
Class<?> handlerClass = Class.forName(handlerClassName);
this.fallbackHandler = (URLStreamHandler) handlerClass.newInstance();
return this.fallbackHandler;
}
catch (Exception ex) {
// Ignore
}
}
throw new IllegalStateException("Unable to find fallback handler");
}
private URLConnection openConnection(URLStreamHandler handler, URL url)
throws Exception {
if (OPEN_CONNECTION_METHOD == null) {
throw new IllegalStateException(
"Unable to invoke fallback open connection method");
}
OPEN_CONNECTION_METHOD.setAccessible(true);
return (URLConnection) OPEN_CONNECTION_METHOD.invoke(handler, url);
}
@Override
protected void parseURL(URL context, String spec, int start, int limit) {
if (spec.toLowerCase().startsWith(JAR_PROTOCOL)) {
setFile(context, getFileFromSpec(spec.substring(start, limit)));
}
else {
setFile(context, getFileFromContext(context, spec.substring(start, limit)));
}
}
private String getFileFromSpec(String spec) {
int separatorIndex = spec.lastIndexOf("!/");
if (separatorIndex == -1) {
throw new IllegalArgumentException("No !/ in spec '" + spec + "'");
}
try {
new URL(spec.substring(0, separatorIndex));
return spec;
}
catch (MalformedURLException ex) {
throw new IllegalArgumentException("Invalid spec URL '" + spec + "'", ex);
}
}
private String getFileFromContext(URL context, String spec) {
String file = context.getFile();
if (spec.startsWith("/")) {
return trimToJarRoot(file) + SEPARATOR + spec.substring(1);
}
if (file.endsWith("/")) {
return file + spec;
}
int lastSlashIndex = file.lastIndexOf('/');
if (lastSlashIndex == -1) {
throw new IllegalArgumentException(
"No / found in context URL's file '" + file + "'");
}
return file.substring(0, lastSlashIndex + 1) + spec;
}
private String trimToJarRoot(String file) {
int lastSeparatorIndex = file.lastIndexOf(SEPARATOR);
if (lastSeparatorIndex == -1) {
throw new IllegalArgumentException(
"No !/ found in context URL's file '" + file + "'");
}
return file.substring(0, lastSeparatorIndex);
}
private void setFile(URL context, String file) {
setURL(context, JAR_PROTOCOL, null, -1, null, null, normalize(file), null, null);
}
private String normalize(String file) {
int afterLastSeparatorIndex = file.lastIndexOf(SEPARATOR) + SEPARATOR.length();
String afterSeparator = file.substring(afterLastSeparatorIndex);
afterSeparator = replaceParentDir(afterSeparator);
afterSeparator = replaceCurrentDir(afterSeparator);
return file.substring(0, afterLastSeparatorIndex) + afterSeparator;
}
private String replaceParentDir(String file) {
int parentDirIndex;
while ((parentDirIndex = file.indexOf("/../")) >= 0) {
int precedingSlashIndex = file.lastIndexOf('/', parentDirIndex - 1);
if (precedingSlashIndex >= 0) {
file = file.substring(0, precedingSlashIndex)
+ file.substring(parentDirIndex + 3);
}
else {
file = file.substring(parentDirIndex + 4);
}
}
return file;
}
private String replaceCurrentDir(String file) {
return file.replace("/./", "/");
}
@Override
protected int hashCode(URL u) {
return hashCode(u.getProtocol(), u.getFile());
}
private int hashCode(String protocol, String file) {
int result = (protocol == null ? 0 : protocol.hashCode());
int separatorIndex = file.indexOf(SEPARATOR);
if (separatorIndex == -1) {
return result + file.hashCode();
}
String source = file.substring(0, separatorIndex);
String entry = canonicalize(file.substring(separatorIndex + 2));
try {
result += new URL(source).hashCode();
}
catch (MalformedURLException ex) {
result += source.hashCode();
}
result += entry.hashCode();
return result;
}
@Override
protected boolean sameFile(URL u1, URL u2) {
if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar")) {
return false;
}
int separator1 = u1.getFile().indexOf(SEPARATOR);
int separator2 = u2.getFile().indexOf(SEPARATOR);
if (separator1 == -1 || separator2 == -1) {
return super.sameFile(u1, u2);
}
String nested1 = u1.getFile().substring(separator1 + SEPARATOR.length());
String nested2 = u2.getFile().substring(separator2 + SEPARATOR.length());
if (!nested1.equals(nested2)) {
String canonical1 = canonicalize(nested1);
String canonical2 = canonicalize(nested2);
if (!canonical1.equals(canonical2)) {
return false;
}
}
String root1 = u1.getFile().substring(0, separator1);
String root2 = u2.getFile().substring(0, separator2);
try {
return super.sameFile(new URL(root1), new URL(root2));
}
catch (MalformedURLException ex) {
// Continue
}
return super.sameFile(u1, u2);
}
private String canonicalize(String path) {
return path.replace(SEPARATOR, "/");
}
public JarFile getRootJarFileFromUrl(URL url) throws IOException {
String spec = url.getFile();
int separatorIndex = spec.indexOf(SEPARATOR);
if (separatorIndex == -1) {
throw new MalformedURLException("Jar URL does not contain !/ separator");
}
String name = spec.substring(0, separatorIndex);
return getRootJarFile(name);
}
private JarFile getRootJarFile(String name) throws IOException {
try {
if (!name.startsWith(FILE_PROTOCOL)) {
throw new IllegalStateException("Not a file URL");
}
String path = name.substring(FILE_PROTOCOL.length());
File file = new File(URLDecoder.decode(path, "UTF-8"));
Map<File, JarFile> cache = rootFileCache.get();
JarFile result = (cache == null ? null : cache.get(file));
if (result == null) {
result = new JarFile(file);
addToRootFileCache(file, result);
}
return result;
}
catch (Exception ex) {
throw new IOException("Unable to open root Jar file '" + name + "'", ex);
}
}
/**
* Add the given {@link JarFile} to the root file cache.
* @param sourceFile the source file to add
* @param jarFile the jar file.
*/
static void addToRootFileCache(File sourceFile, JarFile jarFile) {
Map<File, JarFile> cache = rootFileCache.get();
if (cache == null) {
cache = new ConcurrentHashMap<>();
rootFileCache = new SoftReference<>(cache);
}
cache.put(sourceFile, jarFile);
}
/**
* Set if a generic static exception can be thrown when a URL cannot be connected.
* This optimization is used during class loading to save creating lots of exceptions
* which are then swallowed.
* @param useFastConnectionExceptions if fast connection exceptions can be used.
*/
public static void setUseFastConnectionExceptions(
boolean useFastConnectionExceptions) {
JarURLConnection.setUseFastExceptions(useFastConnectionExceptions);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2012-2016 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.boot.loader.jar;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.CodeSigner;
import java.security.cert.Certificate;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
/**
* Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s.
*
* @author Phillip Webb
*/
class JarEntry extends java.util.jar.JarEntry implements FileHeader {
private Certificate[] certificates;
private CodeSigner[] codeSigners;
private final JarFile jarFile;
private long localHeaderOffset;
JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) {
super(header.getName().toString());
this.jarFile = jarFile;
this.localHeaderOffset = header.getLocalHeaderOffset();
setCompressedSize(header.getCompressedSize());
setMethod(header.getMethod());
setCrc(header.getCrc());
setSize(header.getSize());
setExtra(header.getExtra());
setComment(header.getComment().toString());
setSize(header.getSize());
setTime(header.getTime());
}
@Override
public boolean hasName(String name, String suffix) {
return getName().length() == name.length() + suffix.length()
&& getName().startsWith(name) && getName().endsWith(suffix);
}
/**
* Return a {@link URL} for this {@link JarEntry}.
* @return the URL for the entry
* @throws MalformedURLException if the URL is not valid
*/
URL getUrl() throws MalformedURLException {
return new URL(this.jarFile.getUrl(), getName());
}
@Override
public Attributes getAttributes() throws IOException {
Manifest manifest = this.jarFile.getManifest();
return (manifest == null ? null : manifest.getAttributes(getName()));
}
@Override
public Certificate[] getCertificates() {
if (this.jarFile.isSigned() && this.certificates == null) {
this.jarFile.setupEntryCertificates(this);
}
return this.certificates;
}
@Override
public CodeSigner[] getCodeSigners() {
if (this.jarFile.isSigned() && this.codeSigners == null) {
this.jarFile.setupEntryCertificates(this);
}
return this.codeSigners;
}
void setCertificates(java.util.jar.JarEntry entry) {
this.certificates = entry.getCertificates();
this.codeSigners = entry.getCodeSigners();
}
@Override
public long getLocalHeaderOffset() {
return this.localHeaderOffset;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
/**
* Interface that can be used to filter and optionally rename jar entries.
*
* @author Phillip Webb
*/
interface JarEntryFilter {
/**
* Apply the jar entry filter.
* @param name the current entry name. This may be different that the original entry
* name if a previous filter has been applied
* @return the new name of the entry or {@code null} if the entry should not be
* included.
*/
AsciiBytes apply(AsciiBytes name);
}

View File

@@ -0,0 +1,400 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import org.springframework.boot.loader.data.RandomAccessData;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.boot.loader.data.RandomAccessDataFile;
/**
* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.
* <ul>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
* on any directory entry.</li>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
* embedded JAR files (as long as their entry is not compressed).</li>
* </ul>
*
* @author Phillip Webb
*/
public class JarFile extends java.util.jar.JarFile {
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
private static final AsciiBytes META_INF = new AsciiBytes("META-INF/");
private static final AsciiBytes SIGNATURE_FILE_EXTENSION = new AsciiBytes(".SF");
private final RandomAccessDataFile rootFile;
private final String pathFromRoot;
private final RandomAccessData data;
private final JarFileType type;
private URL url;
private JarFileEntries entries;
private SoftReference<Manifest> manifest;
private boolean signed;
/**
* Create a new {@link JarFile} backed by the specified file.
* @param file the root jar file
* @throws IOException if the file cannot be read
*/
public JarFile(File file) throws IOException {
this(new RandomAccessDataFile(file));
}
/**
* Create a new {@link JarFile} backed by the specified file.
* @param file the root jar file
* @throws IOException if the file cannot be read
*/
JarFile(RandomAccessDataFile file) throws IOException {
this(file, "", file, JarFileType.DIRECT);
}
/**
* Private constructor used to create a new {@link JarFile} either directly or from a
* nested entry.
* @param rootFile the root jar file
* @param pathFromRoot the name of this file
* @param data the underlying data
* @param type the type of the jar file
* @throws IOException if the file cannot be read
*/
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
RandomAccessData data, JarFileType type) throws IOException {
this(rootFile, pathFromRoot, data, null, type);
}
private JarFile(RandomAccessDataFile rootFile, String pathFromRoot,
RandomAccessData data, JarEntryFilter filter, JarFileType type)
throws IOException {
super(rootFile.getFile());
this.rootFile = rootFile;
this.pathFromRoot = pathFromRoot;
CentralDirectoryParser parser = new CentralDirectoryParser();
this.entries = parser.addVisitor(new JarFileEntries(this, filter));
parser.addVisitor(centralDirectoryVisitor());
this.data = parser.parse(data, filter == null);
this.type = type;
}
private CentralDirectoryVisitor centralDirectoryVisitor() {
return new CentralDirectoryVisitor() {
@Override
public void visitStart(CentralDirectoryEndRecord endRecord,
RandomAccessData centralDirectoryData) {
}
@Override
public void visitFileHeader(CentralDirectoryFileHeader fileHeader,
int dataOffset) {
AsciiBytes name = fileHeader.getName();
if (name.startsWith(META_INF)
&& name.endsWith(SIGNATURE_FILE_EXTENSION)) {
JarFile.this.signed = true;
}
}
@Override
public void visitEnd() {
}
};
}
protected final RandomAccessDataFile getRootJarFile() {
return this.rootFile;
}
RandomAccessData getData() {
return this.data;
}
@Override
public Manifest getManifest() throws IOException {
Manifest manifest = (this.manifest == null ? null : this.manifest.get());
if (manifest == null) {
if (this.type == JarFileType.NESTED_DIRECTORY) {
manifest = new JarFile(this.getRootJarFile()).getManifest();
}
else {
try (InputStream inputStream = getInputStream(MANIFEST_NAME,
ResourceAccess.ONCE)) {
if (inputStream == null) {
return null;
}
manifest = new Manifest(inputStream);
}
}
this.manifest = new SoftReference<>(manifest);
}
return manifest;
}
@Override
public Enumeration<java.util.jar.JarEntry> entries() {
final Iterator<JarEntry> iterator = this.entries.iterator();
return new Enumeration<java.util.jar.JarEntry>() {
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public java.util.jar.JarEntry nextElement() {
return iterator.next();
}
};
}
@Override
public JarEntry getJarEntry(String name) {
return (JarEntry) getEntry(name);
}
public boolean containsEntry(String name) {
return this.entries.containsEntry(name);
}
@Override
public ZipEntry getEntry(String name) {
return this.entries.getEntry(name);
}
@Override
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
return getInputStream(ze, ResourceAccess.PER_READ);
}
public InputStream getInputStream(ZipEntry ze, ResourceAccess access)
throws IOException {
if (ze instanceof JarEntry) {
return this.entries.getInputStream((JarEntry) ze, access);
}
return getInputStream(ze == null ? null : ze.getName(), access);
}
InputStream getInputStream(String name, ResourceAccess access) throws IOException {
return this.entries.getInputStream(name, access);
}
/**
* Return a nested {@link JarFile} loaded from the specified entry.
* @param entry the zip entry
* @return a {@link JarFile} for the entry
* @throws IOException if the nested jar file cannot be read
*/
public synchronized JarFile getNestedJarFile(final ZipEntry entry)
throws IOException {
return getNestedJarFile((JarEntry) entry);
}
/**
* Return a nested {@link JarFile} loaded from the specified entry.
* @param entry the zip entry
* @return a {@link JarFile} for the entry
* @throws IOException if the nested jar file cannot be read
*/
public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException {
try {
return createJarFileFromEntry(entry);
}
catch (Exception ex) {
throw new IOException(
"Unable to open nested jar file '" + entry.getName() + "'", ex);
}
}
private JarFile createJarFileFromEntry(JarEntry entry) throws IOException {
if (entry.isDirectory()) {
return createJarFileFromDirectoryEntry(entry);
}
return createJarFileFromFileEntry(entry);
}
private JarFile createJarFileFromDirectoryEntry(JarEntry entry) throws IOException {
final AsciiBytes sourceName = new AsciiBytes(entry.getName());
JarEntryFilter filter = (name) -> {
if (name.startsWith(sourceName) && !name.equals(sourceName)) {
return name.substring(sourceName.length());
}
return null;
};
return new JarFile(this.rootFile,
this.pathFromRoot + "!/"
+ entry.getName().substring(0, sourceName.length() - 1),
this.data, filter, JarFileType.NESTED_DIRECTORY);
}
private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
if (entry.getMethod() != ZipEntry.STORED) {
throw new IllegalStateException("Unable to open nested entry '"
+ entry.getName() + "'. It has been compressed and nested "
+ "jar files must be stored without compression. Please check the "
+ "mechanism used to create your executable jar file");
}
RandomAccessData entryData = this.entries.getEntryData(entry.getName());
return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(),
entryData, JarFileType.NESTED_JAR);
}
@Override
public int size() {
return (int) this.data.getSize();
}
@Override
public void close() throws IOException {
super.close();
this.rootFile.close();
}
/**
* Return a URL that can be used to access this JAR file. NOTE: the specified URL
* cannot be serialized and or cloned.
* @return the URL
* @throws MalformedURLException if the URL is malformed
*/
public URL getUrl() throws MalformedURLException {
if (this.url == null) {
Handler handler = new Handler(this);
String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/";
file = file.replace("file:////", "file://"); // Fix UNC paths
this.url = new URL("jar", "", -1, file, handler);
}
return this.url;
}
@Override
public String toString() {
return getName();
}
@Override
public String getName() {
return this.rootFile.getFile() + this.pathFromRoot;
}
boolean isSigned() {
return this.signed;
}
void setupEntryCertificates(JarEntry entry) {
// Fallback to JarInputStream to obtain certificates, not fast but hopefully not
// happening that often.
try {
try (JarInputStream inputStream = new JarInputStream(
getData().getInputStream(ResourceAccess.ONCE))) {
java.util.jar.JarEntry certEntry = inputStream.getNextJarEntry();
while (certEntry != null) {
inputStream.closeEntry();
if (entry.getName().equals(certEntry.getName())) {
setCertificates(entry, certEntry);
}
setCertificates(getJarEntry(certEntry.getName()), certEntry);
certEntry = inputStream.getNextJarEntry();
}
}
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private void setCertificates(JarEntry entry, java.util.jar.JarEntry certEntry) {
if (entry != null) {
entry.setCertificates(certEntry);
}
}
public void clearCache() {
this.entries.clearCache();
}
protected String getPathFromRoot() {
return this.pathFromRoot;
}
JarFileType getType() {
return this.type;
}
/**
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
* {@link URLStreamHandler} will be located to deal with jar URLs.
*/
public static void registerUrlProtocolHandler() {
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
: handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
/**
* Reset any cached handlers just in case a jar protocol has already been used. We
* reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
* should have no effect other than clearing the handlers cache.
*/
private static void resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null);
}
catch (Error ex) {
// Ignore
}
}
/**
* The type of a {@link JarFile}.
*/
enum JarFileType {
DIRECT, NESTED_DIRECTORY, NESTED_JAR
}
}

View File

@@ -0,0 +1,306 @@
/*
* Copyright 2012-2016 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.boot.loader.jar;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.zip.ZipEntry;
import org.springframework.boot.loader.data.RandomAccessData;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
/**
* Provides access to entries from a {@link JarFile}. In order to reduce memory
* consumption entry details are stored using int arrays. The {@code hashCodes} array
* stores the hash code of the entry name, the {@code centralDirectoryOffsets} provides
* the offset to the central directory record and {@code positions} provides the original
* order position of the entry. The arrays are stored in hashCode order so that a binary
* search can be used to find a name.
* <p>
* A typical Spring Boot application will have somewhere in the region of 10,500 entries
* which should consume about 122K.
*
* @author Phillip Webb
*/
class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
private static final long LOCAL_FILE_HEADER_SIZE = 30;
private static final String SLASH = "/";
private static final String NO_SUFFIX = "";
protected static final int ENTRY_CACHE_SIZE = 25;
private final JarFile jarFile;
private final JarEntryFilter filter;
private RandomAccessData centralDirectoryData;
private int size;
private int[] hashCodes;
private int[] centralDirectoryOffsets;
private int[] positions;
private final Map<Integer, FileHeader> entriesCache = Collections
.synchronizedMap(new LinkedHashMap<Integer, FileHeader>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<Integer, FileHeader> eldest) {
if (JarFileEntries.this.jarFile.isSigned()) {
return false;
}
return size() >= ENTRY_CACHE_SIZE;
}
});
JarFileEntries(JarFile jarFile, JarEntryFilter filter) {
this.jarFile = jarFile;
this.filter = filter;
}
@Override
public void visitStart(CentralDirectoryEndRecord endRecord,
RandomAccessData centralDirectoryData) {
int maxSize = endRecord.getNumberOfRecords();
this.centralDirectoryData = centralDirectoryData;
this.hashCodes = new int[maxSize];
this.centralDirectoryOffsets = new int[maxSize];
this.positions = new int[maxSize];
}
@Override
public void visitFileHeader(CentralDirectoryFileHeader fileHeader, int dataOffset) {
AsciiBytes name = applyFilter(fileHeader.getName());
if (name != null) {
add(name, fileHeader, dataOffset);
}
}
private void add(AsciiBytes name, CentralDirectoryFileHeader fileHeader,
int dataOffset) {
this.hashCodes[this.size] = name.hashCode();
this.centralDirectoryOffsets[this.size] = dataOffset;
this.positions[this.size] = this.size;
this.size++;
}
@Override
public void visitEnd() {
sort(0, this.size - 1);
int[] positions = this.positions;
this.positions = new int[positions.length];
for (int i = 0; i < this.size; i++) {
this.positions[positions[i]] = i;
}
}
private void sort(int left, int right) {
// Quick sort algorithm, uses hashCodes as the source but sorts all arrays
if (left < right) {
int pivot = this.hashCodes[left + (right - left) / 2];
int i = left;
int j = right;
while (i <= j) {
while (this.hashCodes[i] < pivot) {
i++;
}
while (this.hashCodes[j] > pivot) {
j--;
}
if (i <= j) {
swap(i, j);
i++;
j--;
}
}
if (left < j) {
sort(left, j);
}
if (right > i) {
sort(i, right);
}
}
}
private void swap(int i, int j) {
swap(this.hashCodes, i, j);
swap(this.centralDirectoryOffsets, i, j);
swap(this.positions, i, j);
}
private void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
@Override
public Iterator<JarEntry> iterator() {
return new EntryIterator();
}
public boolean containsEntry(String name) {
return getEntry(name, FileHeader.class, true) != null;
}
public JarEntry getEntry(String name) {
return getEntry(name, JarEntry.class, true);
}
public InputStream getInputStream(String name, ResourceAccess access)
throws IOException {
FileHeader entry = getEntry(name, FileHeader.class, false);
return getInputStream(entry, access);
}
public InputStream getInputStream(FileHeader entry, ResourceAccess access)
throws IOException {
if (entry == null) {
return null;
}
InputStream inputStream = getEntryData(entry).getInputStream(access);
if (entry.getMethod() == ZipEntry.DEFLATED) {
inputStream = new ZipInflaterInputStream(inputStream, (int) entry.getSize());
}
return inputStream;
}
public RandomAccessData getEntryData(String name) throws IOException {
FileHeader entry = getEntry(name, FileHeader.class, false);
if (entry == null) {
return null;
}
return getEntryData(entry);
}
private RandomAccessData getEntryData(FileHeader entry) throws IOException {
// aspectjrt-1.7.4.jar has a different ext bytes length in the
// local directory to the central directory. We need to re-read
// here to skip them
RandomAccessData data = this.jarFile.getData();
byte[] localHeader = Bytes.get(
data.getSubsection(entry.getLocalHeaderOffset(), LOCAL_FILE_HEADER_SIZE));
long nameLength = Bytes.littleEndianValue(localHeader, 26, 2);
long extraLength = Bytes.littleEndianValue(localHeader, 28, 2);
return data.getSubsection(entry.getLocalHeaderOffset() + LOCAL_FILE_HEADER_SIZE
+ nameLength + extraLength, entry.getCompressedSize());
}
private <T extends FileHeader> T getEntry(String name, Class<T> type,
boolean cacheEntry) {
int hashCode = AsciiBytes.hashCode(name);
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry);
if (entry == null) {
hashCode = AsciiBytes.hashCode(hashCode, SLASH);
entry = getEntry(hashCode, name, SLASH, type, cacheEntry);
}
return entry;
}
private <T extends FileHeader> T getEntry(int hashCode, String name, String suffix,
Class<T> type, boolean cacheEntry) {
int index = getFirstIndex(hashCode);
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
T entry = getEntry(index, type, cacheEntry);
if (entry.hasName(name, suffix)) {
return entry;
}
index++;
}
return null;
}
@SuppressWarnings("unchecked")
private <T extends FileHeader> T getEntry(int index, Class<T> type,
boolean cacheEntry) {
try {
FileHeader cached = this.entriesCache.get(index);
FileHeader entry = (cached != null ? cached
: CentralDirectoryFileHeader.fromRandomAccessData(
this.centralDirectoryData,
this.centralDirectoryOffsets[index], this.filter));
if (CentralDirectoryFileHeader.class.equals(entry.getClass())
&& type.equals(JarEntry.class)) {
entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry);
}
if (cacheEntry && cached != entry) {
this.entriesCache.put(index, entry);
}
return (T) entry;
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private int getFirstIndex(int hashCode) {
int index = Arrays.binarySearch(this.hashCodes, 0, this.size, hashCode);
if (index < 0) {
return -1;
}
while (index > 0 && this.hashCodes[index - 1] == hashCode) {
index--;
}
return index;
}
public void clearCache() {
this.entriesCache.clear();
}
private AsciiBytes applyFilter(AsciiBytes name) {
return (this.filter == null ? name : this.filter.apply(name));
}
/**
* Iterator for contained entries.
*/
private class EntryIterator implements Iterator<JarEntry> {
private int index = 0;
@Override
public boolean hasNext() {
return this.index < JarFileEntries.this.size;
}
@Override
public JarEntry next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
int entryIndex = JarFileEntries.this.positions[this.index];
this.index++;
return getEntry(entryIndex, JarEntry.class, false);
}
}
}

View File

@@ -0,0 +1,405 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.net.URLStreamHandler;
import java.security.Permission;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
/**
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @author Rostyslav Dudka
*/
final class JarURLConnection extends java.net.JarURLConnection {
private static ThreadLocal<Boolean> useFastExceptions = new ThreadLocal<>();
private static final FileNotFoundException FILE_NOT_FOUND_EXCEPTION = new FileNotFoundException(
"Jar file or entry not found");
private static final IllegalStateException NOT_FOUND_CONNECTION_EXCEPTION = new IllegalStateException(
FILE_NOT_FOUND_EXCEPTION);
private static final String SEPARATOR = "!/";
private static final URL EMPTY_JAR_URL;
static {
try {
EMPTY_JAR_URL = new URL("jar:", null, 0, "file:!/", new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL u) throws IOException {
// Stub URLStreamHandler to prevent the wrong JAR Handler from being
// Instantiated and cached.
return null;
}
});
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
private static final JarEntryName EMPTY_JAR_ENTRY_NAME = new JarEntryName("");
private static final String READ_ACTION = "read";
private static final JarURLConnection NOT_FOUND_CONNECTION = JarURLConnection
.notFound();
private final JarFile jarFile;
private Permission permission;
private URL jarFileUrl;
private final JarEntryName jarEntryName;
private JarEntry jarEntry;
private JarURLConnection(URL url, JarFile jarFile, JarEntryName jarEntryName)
throws IOException {
// What we pass to super is ultimately ignored
super(EMPTY_JAR_URL);
this.url = url;
this.jarFile = jarFile;
this.jarEntryName = jarEntryName;
}
@Override
public void connect() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (!this.jarEntryName.isEmpty() && this.jarEntry == null) {
this.jarEntry = this.jarFile.getJarEntry(getEntryName());
if (this.jarEntry == null) {
throwFileNotFound(this.jarEntryName, this.jarFile);
}
}
this.connected = true;
}
@Override
public JarFile getJarFile() throws IOException {
connect();
return this.jarFile;
}
@Override
public URL getJarFileURL() {
if (this.jarFile == null) {
throw NOT_FOUND_CONNECTION_EXCEPTION;
}
if (this.jarFileUrl == null) {
this.jarFileUrl = buildJarFileUrl();
}
return this.jarFileUrl;
}
private URL buildJarFileUrl() {
try {
String spec = this.jarFile.getUrl().getFile();
if (spec.endsWith(SEPARATOR)) {
spec = spec.substring(0, spec.length() - SEPARATOR.length());
}
if (spec.indexOf(SEPARATOR) == -1) {
return new URL(spec);
}
return new URL("jar:" + spec);
}
catch (MalformedURLException ex) {
throw new IllegalStateException(ex);
}
}
@Override
public JarEntry getJarEntry() throws IOException {
if (this.jarEntryName == null || this.jarEntryName.isEmpty()) {
return null;
}
connect();
return this.jarEntry;
}
@Override
public String getEntryName() {
if (this.jarFile == null) {
throw NOT_FOUND_CONNECTION_EXCEPTION;
}
return this.jarEntryName.toString();
}
@Override
public InputStream getInputStream() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (this.jarEntryName.isEmpty()
&& this.jarFile.getType() == JarFile.JarFileType.DIRECT) {
throw new IOException("no entry name specified");
}
connect();
InputStream inputStream = (this.jarEntryName.isEmpty()
? this.jarFile.getData().getInputStream(ResourceAccess.ONCE)
: this.jarFile.getInputStream(this.jarEntry));
if (inputStream == null) {
throwFileNotFound(this.jarEntryName, this.jarFile);
}
return inputStream;
}
private void throwFileNotFound(Object entry, JarFile jarFile)
throws FileNotFoundException {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
throw FILE_NOT_FOUND_EXCEPTION;
}
throw new FileNotFoundException(
"JAR entry " + entry + " not found in " + jarFile.getName());
}
@Override
public int getContentLength() {
long length = getContentLengthLong();
if (length > Integer.MAX_VALUE) {
return -1;
}
return (int) length;
}
@Override
public long getContentLengthLong() {
if (this.jarFile == null) {
return -1;
}
try {
if (this.jarEntryName.isEmpty()) {
return this.jarFile.size();
}
JarEntry entry = getJarEntry();
return (entry == null ? -1 : (int) entry.getSize());
}
catch (IOException ex) {
return -1;
}
}
@Override
public Object getContent() throws IOException {
connect();
return (this.jarEntryName.isEmpty() ? this.jarFile : super.getContent());
}
@Override
public String getContentType() {
return (this.jarEntryName == null ? null : this.jarEntryName.getContentType());
}
@Override
public Permission getPermission() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (this.permission == null) {
this.permission = new FilePermission(
this.jarFile.getRootJarFile().getFile().getPath(), READ_ACTION);
}
return this.permission;
}
@Override
public long getLastModified() {
if (this.jarFile == null || this.jarEntryName.isEmpty()) {
return 0;
}
try {
JarEntry entry = getJarEntry();
return (entry == null ? 0 : entry.getTime());
}
catch (IOException ex) {
return 0;
}
}
static void setUseFastExceptions(boolean useFastExceptions) {
JarURLConnection.useFastExceptions.set(useFastExceptions);
}
static JarURLConnection get(URL url, JarFile jarFile) throws IOException {
String spec = extractFullSpec(url, jarFile.getPathFromRoot());
int separator;
int index = 0;
while ((separator = spec.indexOf(SEPARATOR, index)) > 0) {
String entryName = spec.substring(index, separator);
JarEntry jarEntry = jarFile.getJarEntry(entryName);
if (jarEntry == null) {
return JarURLConnection.notFound(jarFile, JarEntryName.get(entryName));
}
jarFile = jarFile.getNestedJarFile(jarEntry);
index += separator + SEPARATOR.length();
}
JarEntryName jarEntryName = JarEntryName.get(spec, index);
if (Boolean.TRUE.equals(useFastExceptions.get())) {
if (!jarEntryName.isEmpty()
&& !jarFile.containsEntry(jarEntryName.toString())) {
return NOT_FOUND_CONNECTION;
}
}
return new JarURLConnection(url, jarFile, jarEntryName);
}
private static String extractFullSpec(URL url, String pathFromRoot) {
String file = url.getFile();
int separatorIndex = file.indexOf(SEPARATOR);
if (separatorIndex < 0) {
return "";
}
int specIndex = separatorIndex + SEPARATOR.length() + pathFromRoot.length();
return file.substring(specIndex);
}
private static JarURLConnection notFound() {
try {
return notFound(null, null);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private static JarURLConnection notFound(JarFile jarFile, JarEntryName jarEntryName)
throws IOException {
if (Boolean.TRUE.equals(useFastExceptions.get())) {
return NOT_FOUND_CONNECTION;
}
return new JarURLConnection(null, jarFile, jarEntryName);
}
/**
* A JarEntryName parsed from a URL String.
*/
static class JarEntryName {
private final String name;
private String contentType;
JarEntryName(String spec) {
this.name = decode(spec);
}
private String decode(String source) {
if (source.isEmpty() || (source.indexOf('%') < 0)) {
return source;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream(source.length());
write(source, bos);
// AsciiBytes is what is used to store the JarEntries so make it symmetric
return AsciiBytes.toString(bos.toByteArray());
}
private void write(String source, ByteArrayOutputStream outputStream) {
int length = source.length();
for (int i = 0; i < length; i++) {
int c = source.charAt(i);
if (c > 127) {
try {
String encoded = URLEncoder.encode(String.valueOf((char) c),
"UTF-8");
write(encoded, outputStream);
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
else {
if (c == '%') {
if ((i + 2) >= length) {
throw new IllegalArgumentException(
"Invalid encoded sequence \"" + source.substring(i)
+ "\"");
}
c = decodeEscapeSequence(source, i);
i += 2;
}
outputStream.write(c);
}
}
}
private char decodeEscapeSequence(String source, int i) {
int hi = Character.digit(source.charAt(i + 1), 16);
int lo = Character.digit(source.charAt(i + 2), 16);
if (hi == -1 || lo == -1) {
throw new IllegalArgumentException(
"Invalid encoded sequence \"" + source.substring(i) + "\"");
}
return ((char) ((hi << 4) + lo));
}
@Override
public String toString() {
return this.name;
}
public boolean isEmpty() {
return this.name.isEmpty();
}
public String getContentType() {
if (this.contentType == null) {
this.contentType = deduceContentType();
}
return this.contentType;
}
private String deduceContentType() {
// Guess the content type, don't bother with streams as mark is not supported
String type = (isEmpty() ? "x-java/jar" : null);
type = (type != null ? type : guessContentTypeFromName(toString()));
type = (type != null ? type : "content/unknown");
return type;
}
public static JarEntryName get(String spec) {
return get(spec, 0);
}
public static JarEntryName get(String spec, int beginIndex) {
if (spec.length() <= beginIndex) {
return EMPTY_JAR_ENTRY_NAME;
}
return new JarEntryName(spec.substring(beginIndex));
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2014 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.boot.loader.jar;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/**
* {@link InflaterInputStream} that supports the writing of an extra "dummy" byte (which
* is required with JDK 6) and returns accurate available() results.
*
* @author Phillip Webb
*/
class ZipInflaterInputStream extends InflaterInputStream {
private boolean extraBytesWritten;
private int available;
ZipInflaterInputStream(InputStream inputStream, int size) {
super(inputStream, new Inflater(true), getInflaterBufferSize(size));
this.available = size;
}
@Override
public int available() throws IOException {
if (this.available < 0) {
return super.available();
}
return this.available;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int result = super.read(b, off, len);
if (result != -1) {
this.available -= result;
}
return result;
}
@Override
protected void fill() throws IOException {
try {
super.fill();
}
catch (EOFException ex) {
if (this.extraBytesWritten) {
throw ex;
}
this.len = 1;
this.buf[0] = 0x0;
this.extraBytesWritten = true;
this.inf.setInput(this.buf, 0, this.len);
}
}
private static int getInflaterBufferSize(long size) {
size += 2; // inflater likes some space
size = (size > 65536 ? 8192 : size);
size = (size <= 0 ? 4096 : size);
return (int) size;
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Support for loading and manipulating JAR/WAR files.
*/
package org.springframework.boot.loader.jar;

View File

@@ -0,0 +1,26 @@
/*
* 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.
*/
/**
* System that allows self-contained JAR/WAR archives to be launched using
* {@code java -jar}. Archives can include nested packaged dependency JARs (there is 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.boot.loader.JarLauncher
* @see org.springframework.boot.loader.WarLauncher
*/
package org.springframework.boot.loader;

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2012-2017 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.boot.loader.util;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
/**
* Helper class for resolving placeholders in texts. Usually applied to file paths.
* <p>
* A text may contain {@code $ ...} placeholders, to be resolved as system properties:
* e.g. {@code $ user.dir} . Default values can be supplied using the ":" separator
* between key and value.
* <p>
* Adapted from Spring.
*
* @author Juergen Hoeller
* @author Rob Harrop
* @author Dave Syer
* @see System#getProperty(String)
*/
public abstract class SystemPropertyUtils {
/**
* Prefix for system property placeholders: "${".
*/
public static final String PLACEHOLDER_PREFIX = "${";
/**
* Suffix for system property placeholders: "}".
*/
public static final String PLACEHOLDER_SUFFIX = "}";
/**
* Value separator for system property placeholders: ":".
*/
public static final String VALUE_SEPARATOR = ":";
private static final String SIMPLE_PREFIX = PLACEHOLDER_PREFIX.substring(1);
/**
* Resolve ${...} placeholders in the given text, replacing them with corresponding
* system property values.
* @param text the String to resolve
* @return the resolved String
* @throws IllegalArgumentException if there is an unresolvable placeholder
* @see #PLACEHOLDER_PREFIX
* @see #PLACEHOLDER_SUFFIX
*/
public static String resolvePlaceholders(String text) {
if (text == null) {
return text;
}
return parseStringValue(null, text, text, new HashSet<>());
}
/**
* Resolve ${...} placeholders in the given text, replacing them with corresponding
* system property values.
* @param properties a properties instance to use in addition to System
* @param text the String to resolve
* @return the resolved String
* @throws IllegalArgumentException if there is an unresolvable placeholder
* @see #PLACEHOLDER_PREFIX
* @see #PLACEHOLDER_SUFFIX
*/
public static String resolvePlaceholders(Properties properties, String text) {
if (text == null) {
return text;
}
return parseStringValue(properties, text, text, new HashSet<>());
}
private static String parseStringValue(Properties properties, String value,
String current, Set<String> visitedPlaceholders) {
StringBuilder buf = new StringBuilder(current);
int startIndex = current.indexOf(PLACEHOLDER_PREFIX);
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(buf, startIndex);
if (endIndex != -1) {
String placeholder = buf
.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
String originalPlaceholder = placeholder;
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException("Circular placeholder reference '"
+ originalPlaceholder + "' in property definitions");
}
// Recursive invocation, parsing placeholders contained in the
// placeholder
// key.
placeholder = parseStringValue(properties, value, placeholder,
visitedPlaceholders);
// Now obtain the value for the fully resolved key...
String propVal = resolvePlaceholder(properties, value, placeholder);
if (propVal == null && VALUE_SEPARATOR != null) {
int separatorIndex = placeholder.indexOf(VALUE_SEPARATOR);
if (separatorIndex != -1) {
String actualPlaceholder = placeholder.substring(0,
separatorIndex);
String defaultValue = placeholder
.substring(separatorIndex + VALUE_SEPARATOR.length());
propVal = resolvePlaceholder(properties, value,
actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
}
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(properties, value, propVal,
visitedPlaceholders);
buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(),
propVal);
startIndex = buf.indexOf(PLACEHOLDER_PREFIX,
startIndex + propVal.length());
}
else {
// Proceed with unprocessed value.
startIndex = buf.indexOf(PLACEHOLDER_PREFIX,
endIndex + PLACEHOLDER_SUFFIX.length());
}
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
}
}
return buf.toString();
}
private static String resolvePlaceholder(Properties properties, String text,
String placeholderName) {
String propVal = getProperty(placeholderName, null, text);
if (propVal != null) {
return propVal;
}
return properties == null ? null : properties.getProperty(placeholderName);
}
public static String getProperty(String key) {
return getProperty(key, null, "");
}
public static String getProperty(String key, String defaultValue) {
return getProperty(key, defaultValue, "");
}
/**
* Search the System properties and environment variables for a value with the
* provided key. Environment variables in {@code UPPER_CASE} style are allowed where
* System properties would normally be {@code lower.case}.
* @param key the key to resolve
* @param defaultValue the default value
* @param text optional extra context for an error message if the key resolution fails
* (e.g. if System properties are not accessible)
* @return a static property value or null of not found
*/
public static String getProperty(String key, String defaultValue, String text) {
try {
String propVal = System.getProperty(key);
if (propVal == null) {
// Fall back to searching the system environment.
propVal = System.getenv(key);
}
if (propVal == null) {
// Try with underscores.
propVal = System.getenv(key.replace('.', '_'));
}
if (propVal == null) {
// Try uppercase with underscores as well.
propVal = System.getenv(key.toUpperCase().replace('.', '_'));
}
if (propVal != null) {
return propVal;
}
}
catch (Throwable ex) {
System.err.println("Could not resolve key '" + key + "' in '" + text
+ "' as system property or in environment: " + ex);
}
return defaultValue;
}
private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
int index = startIndex + PLACEHOLDER_PREFIX.length();
int withinNestedPlaceholder = 0;
while (index < buf.length()) {
if (substringMatch(buf, index, PLACEHOLDER_SUFFIX)) {
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + PLACEHOLDER_SUFFIX.length();
}
else {
return index;
}
}
else if (substringMatch(buf, index, SIMPLE_PREFIX)) {
withinNestedPlaceholder++;
index = index + SIMPLE_PREFIX.length();
}
else {
index++;
}
}
return -1;
}
private static boolean substringMatch(CharSequence str, int index,
CharSequence substring) {
for (int j = 0; j < substring.length(); j++) {
int i = index + j;
if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2015 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.
*/
/**
* Utilities used by Spring Boot's JAR loading.
*/
package org.springframework.boot.loader.util;

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2012-2017 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.boot.loader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.util.FileCopyUtils;
/**
* Base class for testing {@link ExecutableArchiveLauncher} implementations.
*
* @author Andy Wilkinson
*/
public class AbstractExecutableArchiveLauncherTests {
@Rule
public TemporaryFolder temp = new TemporaryFolder();
protected File createJarArchive(String name, String entryPrefix) throws IOException {
File archive = this.temp.newFile(name);
JarOutputStream jarOutputStream = new JarOutputStream(
new FileOutputStream(archive));
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/"));
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/classes/"));
jarOutputStream.putNextEntry(new JarEntry(entryPrefix + "/lib/"));
JarEntry libFoo = new JarEntry(entryPrefix + "/lib/foo.jar");
libFoo.setMethod(ZipEntry.STORED);
ByteArrayOutputStream fooJarStream = new ByteArrayOutputStream();
new JarOutputStream(fooJarStream).close();
libFoo.setSize(fooJarStream.size());
CRC32 crc32 = new CRC32();
crc32.update(fooJarStream.toByteArray());
libFoo.setCrc(crc32.getValue());
jarOutputStream.putNextEntry(libFoo);
jarOutputStream.write(fooJarStream.toByteArray());
jarOutputStream.close();
return archive;
}
protected File explode(File archive) throws IOException {
File exploded = this.temp.newFolder("exploded");
JarFile jarFile = new JarFile(archive);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
File entryFile = new File(exploded, entry.getName());
if (entry.isDirectory()) {
entryFile.mkdirs();
}
else {
FileCopyUtils.copy(jarFile.getInputStream(entry),
new FileOutputStream(entryFile));
}
}
jarFile.close();
return exploded;
}
protected Set<URL> getUrls(List<Archive> archives) throws MalformedURLException {
Set<URL> urls = new HashSet<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return urls;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.boot.loader;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
/**
* Hamcrest matcher to tests that a byte array starts with specific bytes.
*
* @author Phillip Webb
*/
public class ByteArrayStartsWith extends TypeSafeMatcher<byte[]> {
private final byte[] bytes;
public ByteArrayStartsWith(byte[] bytes) {
this.bytes = bytes;
}
@Override
public void describeTo(Description description) {
description.appendText("a byte array starting with ").appendValue(this.bytes);
}
@Override
protected boolean matchesSafely(byte[] item) {
if (item.length < this.bytes.length) {
return false;
}
for (int i = 0; i < this.bytes.length; i++) {
if (item[i] != this.bytes[i]) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2012-2016 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.boot.loader;
import java.io.File;
import java.net.URL;
import java.util.List;
import org.junit.Test;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JarLauncher}.
*
* @author Andy Wilkinson
*/
public class JarLauncherTests extends AbstractExecutableArchiveLauncherTests {
@Test
public void explodedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath()
throws Exception {
File explodedRoot = explode(createJarArchive("archive.jar", "BOOT-INF"));
JarLauncher launcher = new JarLauncher(new ExplodedArchive(explodedRoot, true));
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).hasSize(2);
assertThat(getUrls(archives)).containsOnly(
new File(explodedRoot, "BOOT-INF/classes").toURI().toURL(),
new URL("jar:"
+ new File(explodedRoot, "BOOT-INF/lib/foo.jar").toURI().toURL()
+ "!/"));
}
@Test
public void archivedJarHasOnlyBootInfClassesAndContentsOfBootInfLibOnClasspath()
throws Exception {
File jarRoot = createJarArchive("archive.jar", "BOOT-INF");
JarLauncher launcher = new JarLauncher(new JarFileArchive(jarRoot));
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).hasSize(2);
assertThat(getUrls(archives)).containsOnly(
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/classes!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/BOOT-INF/lib/foo.jar!/"));
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2012-2016 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.boot.loader;
import java.io.File;
import java.net.URL;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.jar.JarFile;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link LaunchedURLClassLoader}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
*/
@SuppressWarnings("resource")
public class LaunchedURLClassLoaderTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void resolveResourceFromArchive() throws Exception {
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") },
getClass().getClassLoader());
assertThat(loader.getResource("demo/Application.java")).isNotNull();
}
@Test
public void resolveResourcesFromArchive() throws Exception {
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") },
getClass().getClassLoader());
assertThat(loader.getResources("demo/Application.java").hasMoreElements())
.isTrue();
}
@Test
public void resolveRootPathFromArchive() throws Exception {
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") },
getClass().getClassLoader());
assertThat(loader.getResource("")).isNotNull();
}
@Test
public void resolveRootResourcesFromArchive() throws Exception {
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(
new URL[] { new URL("jar:file:src/test/resources/jars/app.jar!/") },
getClass().getClassLoader());
assertThat(loader.getResources("").hasMoreElements()).isTrue();
}
@Test
public void resolveFromNested() throws Exception {
File file = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(file);
JarFile jarFile = new JarFile(file);
URL url = jarFile.getUrl();
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url },
null);
URL resource = loader.getResource("nested.jar!/3.dat");
assertThat(resource.toString()).isEqualTo(url + "nested.jar!/3.dat");
assertThat(resource.openConnection().getInputStream().read()).isEqualTo(3);
}
@Test
public void resolveFromNestedWhileThreadIsInterrupted() throws Exception {
File file = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(file);
JarFile jarFile = new JarFile(file);
URL url = jarFile.getUrl();
LaunchedURLClassLoader loader = new LaunchedURLClassLoader(new URL[] { url },
null);
try {
Thread.currentThread().interrupt();
URL resource = loader.getResource("nested.jar!/3.dat");
assertThat(resource.toString()).isEqualTo(url + "nested.jar!/3.dat");
assertThat(resource.openConnection().getInputStream().read()).isEqualTo(3);
}
finally {
Thread.interrupted();
}
}
}

View File

@@ -0,0 +1,375 @@
/*
* Copyright 2012-2017 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.boot.loader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.assertj.core.api.Condition;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.boot.testsupport.rule.OutputCapture;
import org.springframework.core.io.FileSystemResource;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PropertiesLauncher}.
*
* @author Dave Syer
* @author Andy Wilkinson
*/
public class PropertiesLauncherTests {
@Rule
public OutputCapture output = new OutputCapture();
@Rule
public ExpectedException expected = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private ClassLoader contextClassLoader;
@Before
public void setup() throws IOException {
this.contextClassLoader = Thread.currentThread().getContextClassLoader();
MockitoAnnotations.initMocks(this);
System.setProperty("loader.home",
new File("src/test/resources").getAbsolutePath());
}
@After
public void close() {
Thread.currentThread().setContextClassLoader(this.contextClassLoader);
System.clearProperty("loader.home");
System.clearProperty("loader.path");
System.clearProperty("loader.main");
System.clearProperty("loader.config.name");
System.clearProperty("loader.config.location");
System.clearProperty("loader.system");
System.clearProperty("loader.classLoader");
}
@Test
public void testDefaultHome() {
System.clearProperty("loader.home");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getHomeDirectory())
.isEqualTo(new File(System.getProperty("user.dir")));
}
@Test
public void testAlternateHome() throws Exception {
System.setProperty("loader.home", "src/test/resources/home");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getHomeDirectory())
.isEqualTo(new File(System.getProperty("loader.home")));
assertThat(launcher.getMainClass()).isEqualTo("demo.HomeApplication");
}
@Test
public void testNonExistentHome() throws Exception {
System.setProperty("loader.home", "src/test/resources/nonexistent");
this.expected.expectMessage("Invalid source folder");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getHomeDirectory())
.isNotEqualTo(new File(System.getProperty("loader.home")));
}
@Test
public void testUserSpecifiedMain() throws Exception {
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getMainClass()).isEqualTo("demo.Application");
assertThat(System.getProperty("loader.main")).isNull();
}
@Test
public void testUserSpecifiedConfigName() throws Exception {
System.setProperty("loader.config.name", "foo");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getMainClass()).isEqualTo("my.Application");
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[etc/]");
}
@Test
public void testRootOfClasspathFirst() throws Exception {
System.setProperty("loader.config.name", "bar");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getMainClass()).isEqualTo("my.BarApplication");
}
@Test
public void testUserSpecifiedDotPath() throws Exception {
System.setProperty("loader.path", ".");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[.]");
}
@Test
public void testUserSpecifiedSlashPath() throws Exception {
System.setProperty("loader.path", "jars/");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[jars/]");
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, endingWith("app.jar!/"));
}
@Test
public void testUserSpecifiedWildcardPath() throws Exception {
System.setProperty("loader.path", "jars/*");
System.setProperty("loader.main", "demo.Application");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[jars/]");
launcher.launch(new String[0]);
waitFor("Hello World");
}
@Test
public void testUserSpecifiedJarPath() throws Exception {
System.setProperty("loader.path", "jars/app.jar");
System.setProperty("loader.main", "demo.Application");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[jars/app.jar]");
launcher.launch(new String[0]);
waitFor("Hello World");
}
@Test
public void testUserSpecifiedRootOfJarPath() throws Exception {
System.setProperty("loader.path",
"jar:file:./src/test/resources/nested-jars/app.jar!/");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[jar:file:./src/test/resources/nested-jars/app.jar!/]");
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
assertThat(archives).areExactly(1, endingWith("app.jar!/"));
}
@Test
public void testUserSpecifiedRootOfJarPathWithDot() throws Exception {
System.setProperty("loader.path", "nested-jars/app.jar!/./");
PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
assertThat(archives).areExactly(1, endingWith("app.jar!/"));
}
@Test
public void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception {
System.setProperty("loader.path",
"jar:file:./src/test/resources/nested-jars/app.jar!/./");
PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
}
@Test
public void testUserSpecifiedJarFileWithNestedArchives() throws Exception {
System.setProperty("loader.path", "nested-jars/app.jar");
System.setProperty("loader.main", "demo.Application");
PropertiesLauncher launcher = new PropertiesLauncher();
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
assertThat(archives).areExactly(1, endingWith("app.jar!/"));
}
@Test
public void testUserSpecifiedDirectoryContainingJarFileWithNestedArchives()
throws Exception {
System.setProperty("loader.path", "nested-jars");
System.setProperty("loader.main", "demo.Application");
PropertiesLauncher launcher = new PropertiesLauncher();
launcher.launch(new String[0]);
waitFor("Hello World");
}
@Test
public void testUserSpecifiedJarPathWithDot() throws Exception {
System.setProperty("loader.path", "./jars/app.jar");
System.setProperty("loader.main", "demo.Application");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[jars/app.jar]");
launcher.launch(new String[0]);
waitFor("Hello World");
}
@Test
public void testUserSpecifiedClassLoader() throws Exception {
System.setProperty("loader.path", "jars/app.jar");
System.setProperty("loader.classLoader", URLClassLoader.class.getName());
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[jars/app.jar]");
launcher.launch(new String[0]);
waitFor("Hello World");
}
@Test
public void testUserSpecifiedClassPathOrder() throws Exception {
System.setProperty("loader.path", "more-jars/app.jar,jars/app.jar");
System.setProperty("loader.classLoader", URLClassLoader.class.getName());
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
.isEqualTo("[more-jars/app.jar, jars/app.jar]");
launcher.launch(new String[0]);
waitFor("Hello Other World");
}
@Test
public void testCustomClassLoaderCreation() throws Exception {
System.setProperty("loader.classLoader", TestLoader.class.getName());
PropertiesLauncher launcher = new PropertiesLauncher();
ClassLoader loader = launcher.createClassLoader(archives());
assertThat(loader).isNotNull();
assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName());
}
private List<Archive> archives() throws Exception {
List<Archive> archives = new ArrayList<>();
String path = System.getProperty("java.class.path");
for (String url : path.split(File.pathSeparator)) {
archives.add(archive(url));
}
return archives;
}
private Archive archive(String url) throws IOException {
File file = new FileSystemResource(url).getFile();
if (url.endsWith(".jar")) {
return new JarFileArchive(file);
}
return new ExplodedArchive(file);
}
@Test
public void testUserSpecifiedConfigPathWins() throws Exception {
System.setProperty("loader.config.name", "foo");
System.setProperty("loader.config.location", "classpath:bar.properties");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getMainClass()).isEqualTo("my.BarApplication");
}
@Test
public void testSystemPropertySpecifiedMain() throws Exception {
System.setProperty("loader.main", "foo.Bar");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getMainClass()).isEqualTo("foo.Bar");
}
@Test
public void testSystemPropertiesSet() throws Exception {
System.setProperty("loader.system", "true");
new PropertiesLauncher();
assertThat(System.getProperty("loader.main")).isEqualTo("demo.Application");
}
@Test
public void testArgsEnhanced() throws Exception {
System.setProperty("loader.args", "foo");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(Arrays.asList(launcher.getArgs("bar")).toString())
.isEqualTo("[foo, bar]");
}
@SuppressWarnings("unchecked")
@Test
public void testLoadPathCustomizedUsingManifest() throws Exception {
System.setProperty("loader.home",
this.temporaryFolder.getRoot().getAbsolutePath());
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
manifest.getMainAttributes().putValue("Loader-Path", "/foo.jar, /bar");
File manifestFile = new File(this.temporaryFolder.getRoot(),
"META-INF/MANIFEST.MF");
manifestFile.getParentFile().mkdirs();
manifest.write(new FileOutputStream(manifestFile));
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat((List<String>) ReflectionTestUtils.getField(launcher, "paths"))
.containsExactly("/foo.jar", "/bar/");
}
@Test
public void testManifestWithPlaceholders() throws Exception {
System.setProperty("loader.home", "src/test/resources/placeholders");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getMainClass()).isEqualTo("demo.FooApplication");
}
private void waitFor(String value) throws Exception {
int count = 0;
boolean timeout = false;
while (!timeout && count < 100) {
count++;
Thread.sleep(50L);
timeout = this.output.toString().contains(value);
}
assertThat(timeout).as("Timed out waiting for (" + value + ")").isTrue();
}
private Condition<Archive> endingWith(final String value) {
return new Condition<Archive>() {
@Override
public boolean matches(Archive archive) {
return archive.toString().endsWith(value);
}
};
}
public static class TestLoader extends URLClassLoader {
public TestLoader(ClassLoader parent) {
super(new URL[0], parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return super.findClass(name);
}
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2012-2017 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.boot.loader;
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 {
createTestJar(file, false);
}
public static void createTestJar(File file, boolean unpackNested) throws Exception {
FileOutputStream fileOutputStream = new FileOutputStream(file);
try (JarOutputStream jarOutputStream = new JarOutputStream(fileOutputStream)) {
writeManifest(jarOutputStream, "j1");
writeEntry(jarOutputStream, "1.dat", 1);
writeEntry(jarOutputStream, "2.dat", 2);
writeDirEntry(jarOutputStream, "d/");
writeEntry(jarOutputStream, "d/9.dat", 9);
writeDirEntry(jarOutputStream, "special/");
writeEntry(jarOutputStream, "special/\u00EB.dat", '\u00EB');
writeNestedEntry("nested.jar", unpackNested, jarOutputStream);
writeNestedEntry("another-nested.jar", unpackNested, jarOutputStream);
}
}
private static void writeNestedEntry(String name, boolean unpackNested,
JarOutputStream jarOutputStream) throws Exception, IOException {
JarEntry nestedEntry = new JarEntry(name);
byte[] nestedJarData = getNestedJarData();
nestedEntry.setSize(nestedJarData.length);
nestedEntry.setCompressedSize(nestedJarData.length);
if (unpackNested) {
nestedEntry.setComment("UNPACK:0000000000000000000000000000000000000000");
}
CRC32 crc32 = new CRC32();
crc32.update(nestedJarData);
nestedEntry.setCrc(crc32.getValue());
nestedEntry.setMethod(ZipEntry.STORED);
jarOutputStream.putNextEntry(nestedEntry);
jarOutputStream.write(nestedJarData);
jarOutputStream.closeEntry();
}
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);
writeEntry(jarOutputStream, "\u00E4.dat", '\u00E4');
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();
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2012-2016 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.boot.loader;
import java.io.File;
import java.net.URL;
import java.util.List;
import org.junit.Test;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WarLauncher}.
*
* @author Andy Wilkinson
*/
public class WarLauncherTests extends AbstractExecutableArchiveLauncherTests {
@Test
public void explodedWarHasOnlyWebInfClassesAndContentsOfWebInfLibOnClasspath()
throws Exception {
File explodedRoot = explode(createJarArchive("archive.war", "WEB-INF"));
WarLauncher launcher = new WarLauncher(new ExplodedArchive(explodedRoot, true));
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).hasSize(2);
assertThat(getUrls(archives)).containsOnly(
new File(explodedRoot, "WEB-INF/classes").toURI().toURL(),
new URL("jar:"
+ new File(explodedRoot, "WEB-INF/lib/foo.jar").toURI().toURL()
+ "!/"));
}
@Test
public void archivedWarHasOnlyWebInfClassesAndContentsOWebInfLibOnClasspath()
throws Exception {
File jarRoot = createJarArchive("archive.war", "WEB-INF");
WarLauncher launcher = new WarLauncher(new JarFileArchive(jarRoot));
List<Archive> archives = launcher.getClassPathArchives();
assertThat(archives).hasSize(2);
assertThat(getUrls(archives)).containsOnly(
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/classes!/"),
new URL("jar:" + jarRoot.toURI().toURL() + "!/WEB-INF/lib/foo.jar!/"));
}
}

View File

@@ -0,0 +1,198 @@
/*
* Copyright 2012-2017 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.boot.loader.archive;
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.boot.loader.TestJarCreator;
import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ExplodedArchive}.
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
*/
public class ExplodedArchiveTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File rootFolder;
private ExplodedArchive archive;
@Before
public void setup() throws Exception {
createArchive();
}
private void createArchive() throws Exception {
createArchive(null);
}
private void createArchive(String folderName) throws Exception {
File file = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(file);
this.rootFolder = StringUtils.hasText(folderName)
? this.temporaryFolder.newFolder(folderName)
: 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);
jarFile.close();
}
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"))
.isEqualTo("j1");
}
@Test
public void getEntries() throws Exception {
Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
assertThat(entries.size()).isEqualTo(10);
}
@Test
public void getUrl() throws Exception {
assertThat(this.archive.getUrl()).isEqualTo(this.rootFolder.toURI().toURL());
}
@Test
public void getUrlWithSpaceInPath() throws Exception {
createArchive("spaces in the name");
assertThat(this.archive.getUrl()).isEqualTo(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())
.isEqualTo("jar:" + this.rootFolder.toURI() + "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()).isEqualTo(1);
assertThat(nested.getUrl().toString())
.isEqualTo("file:" + this.rootFolder.toURI().getPath() + "d/");
}
@Test
public void getNonRecursiveEntriesForRoot() throws Exception {
ExplodedArchive archive = new ExplodedArchive(new File("/"), false);
Map<String, Archive.Entry> entries = getEntriesMap(archive);
assertThat(entries.size()).isGreaterThan(1);
}
@Test
public void getNonRecursiveManifest() throws Exception {
ExplodedArchive archive = new ExplodedArchive(
new File("src/test/resources/root"));
assertThat(archive.getManifest()).isNotNull();
Map<String, Archive.Entry> entries = getEntriesMap(archive);
assertThat(entries.size()).isEqualTo(4);
}
@Test
public void getNonRecursiveManifestEvenIfNonRecursive() throws Exception {
ExplodedArchive archive = new ExplodedArchive(new File("src/test/resources/root"),
false);
assertThat(archive.getManifest()).isNotNull();
Map<String, Archive.Entry> entries = getEntriesMap(archive);
assertThat(entries.size()).isEqualTo(3);
}
@Test
public void getResourceAsStream() throws Exception {
ExplodedArchive archive = new ExplodedArchive(
new File("src/test/resources/root"));
assertThat(archive.getManifest()).isNotNull();
URLClassLoader loader = new URLClassLoader(new URL[] { archive.getUrl() });
assertThat(loader.getResourceAsStream("META-INF/spring/application.xml"))
.isNotNull();
loader.close();
}
@Test
public void getResourceAsStreamNonRecursive() throws Exception {
ExplodedArchive archive = new ExplodedArchive(new File("src/test/resources/root"),
false);
assertThat(archive.getManifest()).isNotNull();
URLClassLoader loader = new URLClassLoader(new URL[] { archive.getUrl() });
assertThat(loader.getResourceAsStream("META-INF/spring/application.xml"))
.isNotNull();
loader.close();
}
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
Map<String, Archive.Entry> entries = new HashMap<>();
for (Archive.Entry entry : archive) {
entries.put(entry.getName(), entry);
}
return entries;
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2012-2017 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.boot.loader.archive;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
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.boot.loader.TestJarCreator;
import org.springframework.boot.loader.archive.Archive.Entry;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.CoreMatchers.equalTo;
/**
* Tests for {@link JarFileArchive}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class JarFileArchiveTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public ExpectedException thrown = ExpectedException.none();
private File rootJarFile;
private JarFileArchive archive;
private String rootJarFileUrl;
@Before
public void setup() throws Exception {
setup(false);
}
private void setup(boolean unpackNested) throws Exception {
this.rootJarFile = this.temporaryFolder.newFile();
this.rootJarFileUrl = this.rootJarFile.toURI().toString();
TestJarCreator.createTestJar(this.rootJarFile, unpackNested);
this.archive = new JarFileArchive(this.rootJarFile);
}
@Test
public void getManifest() throws Exception {
assertThat(this.archive.getManifest().getMainAttributes().getValue("Built-By"))
.isEqualTo("j1");
}
@Test
public void getEntries() throws Exception {
Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
assertThat(entries.size()).isEqualTo(10);
}
@Test
public void getUrl() throws Exception {
URL url = this.archive.getUrl();
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFileUrl + "!/");
}
@Test
public void getNestedArchive() throws Exception {
Entry entry = getEntriesMap(this.archive).get("nested.jar");
Archive nested = this.archive.getNestedArchive(entry);
assertThat(nested.getUrl().toString())
.isEqualTo("jar:" + this.rootJarFileUrl + "!/nested.jar!/");
}
@Test
public void getNestedUnpackedArchive() throws Exception {
setup(true);
Entry entry = getEntriesMap(this.archive).get("nested.jar");
Archive nested = this.archive.getNestedArchive(entry);
assertThat(nested.getUrl().toString()).startsWith("file:");
assertThat(nested.getUrl().toString()).endsWith("/nested.jar");
}
@Test
public void unpackedLocationsAreUniquePerArchive() throws Exception {
setup(true);
Entry entry = getEntriesMap(this.archive).get("nested.jar");
URL firstNested = this.archive.getNestedArchive(entry).getUrl();
setup(true);
entry = getEntriesMap(this.archive).get("nested.jar");
URL secondNested = this.archive.getNestedArchive(entry).getUrl();
assertThat(secondNested).isNotEqualTo(firstNested);
}
@Test
public void unpackedLocationsFromSameArchiveShareSameParent() throws Exception {
setup(true);
File nested = new File(this.archive
.getNestedArchive(getEntriesMap(this.archive).get("nested.jar")).getUrl()
.toURI());
File anotherNested = new File(
this.archive
.getNestedArchive(
getEntriesMap(this.archive).get("another-nested.jar"))
.getUrl().toURI());
assertThat(nested.getParent()).isEqualTo(anotherNested.getParent());
}
@Test
public void zip64ArchivesAreHandledGracefully() throws IOException {
File file = this.temporaryFolder.newFile("test.jar");
FileCopyUtils.copy(writeZip64Jar(), file);
this.thrown.expectMessage(equalTo("Zip64 archives are not supported"));
new JarFileArchive(file);
}
@Test
public void nestedZip64ArchivesAreHandledGracefully() throws IOException {
File file = this.temporaryFolder.newFile("test.jar");
JarOutputStream output = new JarOutputStream(new FileOutputStream(file));
JarEntry zip64JarEntry = new JarEntry("nested/zip64.jar");
output.putNextEntry(zip64JarEntry);
byte[] zip64JarData = writeZip64Jar();
zip64JarEntry.setSize(zip64JarData.length);
zip64JarEntry.setCompressedSize(zip64JarData.length);
zip64JarEntry.setMethod(ZipEntry.STORED);
CRC32 crc32 = new CRC32();
crc32.update(zip64JarData);
zip64JarEntry.setCrc(crc32.getValue());
output.write(zip64JarData);
output.closeEntry();
output.close();
JarFileArchive jarFileArchive = new JarFileArchive(file);
this.thrown.expectMessage(
equalTo("Failed to get nested archive for entry nested/zip64.jar"));
jarFileArchive
.getNestedArchive(getEntriesMap(jarFileArchive).get("nested/zip64.jar"));
}
private byte[] writeZip64Jar() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
JarOutputStream jarOutput = new JarOutputStream(bytes);
for (int i = 0; i < 65537; i++) {
jarOutput.putNextEntry(new JarEntry(i + ".dat"));
jarOutput.closeEntry();
}
jarOutput.close();
return bytes.toByteArray();
}
private Map<String, Archive.Entry> getEntriesMap(Archive archive) {
Map<String, Archive.Entry> entries = new HashMap<>();
for (Archive.Entry entry : archive) {
entries.put(entry.getName(), entry);
}
return entries;
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2016 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.boot.loader.data;
import java.io.InputStream;
import org.junit.Test;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ByteArrayRandomAccessData}.
*
* @author Phillip Webb
*/
public class ByteArrayRandomAccessDataTests {
@Test
public void testGetInputStream() throws Exception {
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
RandomAccessData data = new ByteArrayRandomAccessData(bytes);
InputStream inputStream = data.getInputStream(ResourceAccess.PER_READ);
assertThat(FileCopyUtils.copyToByteArray(inputStream)).isEqualTo(bytes);
assertThat(data.getSize()).isEqualTo(bytes.length);
}
@Test
public void testGetSubsection() throws Exception {
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
RandomAccessData data = new ByteArrayRandomAccessData(bytes);
data = data.getSubsection(1, 4).getSubsection(1, 2);
InputStream inputStream = data.getInputStream(ResourceAccess.PER_READ);
assertThat(FileCopyUtils.copyToByteArray(inputStream))
.isEqualTo(new byte[] { 2, 3 });
assertThat(data.getSize()).isEqualTo(2L);
}
}

View File

@@ -0,0 +1,344 @@
/*
* Copyright 2012-2017 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.boot.loader.data;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.boot.loader.data.RandomAccessDataFile.FilePool;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.spy;
/**
* Tests for {@link RandomAccessDataFile}.
*
* @author Phillip Webb
*/
public class RandomAccessDataFileTests {
private static final byte[] BYTES;
static {
BYTES = new byte[256];
for (int i = 0; i < BYTES.length; i++) {
BYTES[i] = (byte) i;
}
}
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File tempFile;
private RandomAccessDataFile file;
private InputStream inputStream;
@Before
public void setup() throws Exception {
this.tempFile = this.temporaryFolder.newFile();
FileOutputStream outputStream = new FileOutputStream(this.tempFile);
outputStream.write(BYTES);
outputStream.close();
this.file = new RandomAccessDataFile(this.tempFile);
this.inputStream = this.file.getInputStream(ResourceAccess.PER_READ);
}
@After
public void cleanup() throws Exception {
this.inputStream.close();
this.file.close();
}
@Test
public void fileNotNull() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be null");
new RandomAccessDataFile(null);
}
@Test
public void fileExists() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must exist");
new RandomAccessDataFile(new File("/does/not/exist"));
}
@Test
public void fileNotNullWithConcurrentReads() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be null");
new RandomAccessDataFile(null, 1);
}
@Test
public void fileExistsWithConcurrentReads() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must exist");
new RandomAccessDataFile(new File("/does/not/exist"), 1);
}
@Test
public void inputStreamRead() throws Exception {
for (int i = 0; i <= 255; i++) {
assertThat(this.inputStream.read()).isEqualTo(i);
}
}
@Test
public void inputStreamReadNullBytes() throws Exception {
this.thrown.expect(NullPointerException.class);
this.thrown.expectMessage("Bytes must not be null");
this.inputStream.read(null);
}
@Test
public void inputStreamReadNullBytesWithOffset() throws Exception {
this.thrown.expect(NullPointerException.class);
this.thrown.expectMessage("Bytes must not be null");
this.inputStream.read(null, 0, 1);
}
@Test
public void inputStreamReadBytes() throws Exception {
byte[] b = new byte[256];
int amountRead = this.inputStream.read(b);
assertThat(b).isEqualTo(BYTES);
assertThat(amountRead).isEqualTo(256);
}
@Test
public void inputStreamReadOffsetBytes() throws Exception {
byte[] b = new byte[7];
this.inputStream.skip(1);
int amountRead = this.inputStream.read(b, 2, 3);
assertThat(b).isEqualTo(new byte[] { 0, 0, 1, 2, 3, 0, 0 });
assertThat(amountRead).isEqualTo(3);
}
@Test
public void inputStreamReadMoreBytesThanAvailable() throws Exception {
byte[] b = new byte[257];
int amountRead = this.inputStream.read(b);
assertThat(b).startsWith(BYTES);
assertThat(amountRead).isEqualTo(256);
}
@Test
public void inputStreamReadPastEnd() throws Exception {
this.inputStream.skip(255);
assertThat(this.inputStream.read()).isEqualTo(0xFF);
assertThat(this.inputStream.read()).isEqualTo(-1);
assertThat(this.inputStream.read()).isEqualTo(-1);
}
@Test
public void inputStreamReadZeroLength() throws Exception {
byte[] b = new byte[] { 0x0F };
int amountRead = this.inputStream.read(b, 0, 0);
assertThat(b).isEqualTo(new byte[] { 0x0F });
assertThat(amountRead).isEqualTo(0);
assertThat(this.inputStream.read()).isEqualTo(0);
}
@Test
public void inputStreamSkip() throws Exception {
long amountSkipped = this.inputStream.skip(4);
assertThat(this.inputStream.read()).isEqualTo(4);
assertThat(amountSkipped).isEqualTo(4L);
}
@Test
public void inputStreamSkipMoreThanAvailable() throws Exception {
long amountSkipped = this.inputStream.skip(257);
assertThat(this.inputStream.read()).isEqualTo(-1);
assertThat(amountSkipped).isEqualTo(256L);
}
@Test
public void inputStreamSkipPastEnd() throws Exception {
this.inputStream.skip(256);
long amountSkipped = this.inputStream.skip(1);
assertThat(amountSkipped).isEqualTo(0L);
}
@Test
public void subsectionNegativeOffset() throws Exception {
this.thrown.expect(IndexOutOfBoundsException.class);
this.file.getSubsection(-1, 1);
}
@Test
public void subsectionNegativeLength() throws Exception {
this.thrown.expect(IndexOutOfBoundsException.class);
this.file.getSubsection(0, -1);
}
@Test
public void subsectionZeroLength() throws Exception {
RandomAccessData subsection = this.file.getSubsection(0, 0);
assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read())
.isEqualTo(-1);
}
@Test
public void subsectionTooBig() throws Exception {
this.file.getSubsection(0, 256);
this.thrown.expect(IndexOutOfBoundsException.class);
this.file.getSubsection(0, 257);
}
@Test
public void subsectionTooBigWithOffset() throws Exception {
this.file.getSubsection(1, 255);
this.thrown.expect(IndexOutOfBoundsException.class);
this.file.getSubsection(1, 256);
}
@Test
public void subsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 1);
assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read())
.isEqualTo(1);
}
@Test
public void inputStreamReadPastSubsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
assertThat(inputStream.read()).isEqualTo(1);
assertThat(inputStream.read()).isEqualTo(2);
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
public void inputStreamReadBytesPastSubsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
byte[] b = new byte[3];
int amountRead = inputStream.read(b);
assertThat(b).isEqualTo(new byte[] { 1, 2, 0 });
assertThat(amountRead).isEqualTo(2);
}
@Test
public void inputStreamSkipPastSubsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
assertThat(inputStream.skip(3)).isEqualTo(2L);
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
public void inputStreamSkipNegative() throws Exception {
assertThat(this.inputStream.skip(-1)).isEqualTo(0L);
}
@Test
public void getFile() throws Exception {
assertThat(this.file.getFile()).isEqualTo(this.tempFile);
}
@Test
public void concurrentReads() throws Exception {
ExecutorService executorService = Executors.newFixedThreadPool(20);
List<Future<Boolean>> results = new ArrayList<>();
for (int i = 0; i < 100; i++) {
results.add(executorService.submit(() -> {
InputStream subsectionInputStream = RandomAccessDataFileTests.this.file
.getSubsection(0, 256).getInputStream(ResourceAccess.PER_READ);
byte[] b = new byte[256];
subsectionInputStream.read(b);
return Arrays.equals(b, BYTES);
}));
}
for (Future<Boolean> future : results) {
assertThat(future.get()).isTrue();
}
}
@Test
public void close() throws Exception {
this.file.getInputStream(ResourceAccess.PER_READ).read();
this.file.close();
Field filePoolField = RandomAccessDataFile.class.getDeclaredField("filePool");
filePoolField.setAccessible(true);
Object filePool = filePoolField.get(this.file);
Field filesField = filePool.getClass().getDeclaredField("files");
filesField.setAccessible(true);
Queue<?> queue = (Queue<?>) filesField.get(filePool);
assertThat(queue.size()).isEqualTo(0);
}
@Test
public void seekFailuresDoNotPreventSubsequentReads() throws Exception {
FilePool filePool = (FilePool) ReflectionTestUtils.getField(this.file,
"filePool");
FilePool spiedPool = spy(filePool);
ReflectionTestUtils.setField(this.file, "filePool", spiedPool);
willAnswer((invocation) -> {
RandomAccessFile originalFile = (RandomAccessFile) invocation
.callRealMethod();
if (Mockito.mockingDetails(originalFile).isSpy()) {
return originalFile;
}
RandomAccessFile spiedFile = spy(originalFile);
willThrow(new IOException("Seek failed")).given(spiedFile).seek(anyLong());
return spiedFile;
}).given(spiedPool).acquire();
for (int i = 0; i < 5; i++) {
try {
this.file.getInputStream(ResourceAccess.PER_READ).read();
fail("Read should fail due to exception from seek");
}
catch (IOException ex) {
}
}
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2012-2016 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.boot.loader.jar;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AsciiBytes}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class AsciiBytesTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void createFromBytes() throws Exception {
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66 });
assertThat(bytes.toString()).isEqualTo("AB");
}
@Test
public void createFromBytesWithOffset() throws Exception {
AsciiBytes bytes = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
assertThat(bytes.toString()).isEqualTo("BC");
}
@Test
public void createFromString() throws Exception {
AsciiBytes bytes = new AsciiBytes("AB");
assertThat(bytes.toString()).isEqualTo("AB");
}
@Test
public void length() throws Exception {
AsciiBytes b1 = new AsciiBytes(new byte[] { 65, 66 });
AsciiBytes b2 = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
assertThat(b1.length()).isEqualTo(2);
assertThat(b2.length()).isEqualTo(2);
}
@Test
public void startWith() throws Exception {
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
assertThat(abc.startsWith(abc)).isTrue();
assertThat(abc.startsWith(ab)).isTrue();
assertThat(abc.startsWith(bc)).isFalse();
assertThat(abc.startsWith(abcd)).isFalse();
}
@Test
public void endsWith() throws Exception {
AsciiBytes abc = new AsciiBytes(new byte[] { 65, 66, 67 });
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67 }, 1, 2);
AsciiBytes ab = new AsciiBytes(new byte[] { 65, 66 });
AsciiBytes aabc = new AsciiBytes(new byte[] { 65, 65, 66, 67 });
assertThat(abc.endsWith(abc)).isTrue();
assertThat(abc.endsWith(bc)).isTrue();
assertThat(abc.endsWith(ab)).isFalse();
assertThat(abc.endsWith(aabc)).isFalse();
}
@Test
public void substringFromBeingIndex() throws Exception {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
assertThat(abcd.substring(0).toString()).isEqualTo("ABCD");
assertThat(abcd.substring(1).toString()).isEqualTo("BCD");
assertThat(abcd.substring(2).toString()).isEqualTo("CD");
assertThat(abcd.substring(3).toString()).isEqualTo("D");
assertThat(abcd.substring(4).toString()).isEqualTo("");
this.thrown.expect(IndexOutOfBoundsException.class);
abcd.substring(5);
}
@Test
public void substring() throws Exception {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
assertThat(abcd.substring(0, 4).toString()).isEqualTo("ABCD");
assertThat(abcd.substring(1, 3).toString()).isEqualTo("BC");
assertThat(abcd.substring(3, 4).toString()).isEqualTo("D");
assertThat(abcd.substring(3, 3).toString()).isEqualTo("");
this.thrown.expect(IndexOutOfBoundsException.class);
abcd.substring(3, 5);
}
@Test
public void appendString() throws Exception {
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
AsciiBytes appended = bc.append("D");
assertThat(bc.toString()).isEqualTo("BC");
assertThat(appended.toString()).isEqualTo("BCD");
}
@Test
public void appendBytes() throws Exception {
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
AsciiBytes appended = bc.append(new byte[] { 68 });
assertThat(bc.toString()).isEqualTo("BC");
assertThat(appended.toString()).isEqualTo("BCD");
}
@Test
public void hashCodeAndEquals() throws Exception {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
AsciiBytes bc = new AsciiBytes(new byte[] { 66, 67 });
AsciiBytes bc_substring = new AsciiBytes(new byte[] { 65, 66, 67, 68 })
.substring(1, 3);
AsciiBytes bc_string = new AsciiBytes("BC");
assertThat(bc.hashCode()).isEqualTo(bc.hashCode());
assertThat(bc.hashCode()).isEqualTo(bc_substring.hashCode());
assertThat(bc.hashCode()).isEqualTo(bc_string.hashCode());
assertThat(bc).isEqualTo(bc);
assertThat(bc).isEqualTo(bc_substring);
assertThat(bc).isEqualTo(bc_string);
assertThat(bc.hashCode()).isNotEqualTo(abcd.hashCode());
assertThat(bc).isNotEqualTo(abcd);
}
@Test
public void hashCodeSameAsString() throws Exception {
hashCodeSameAsString("abcABC123xyz!");
}
@Test
public void hashCodeSameAsStringWithSpecial() throws Exception {
hashCodeSameAsString("special/\u00EB.dat");
}
@Test
public void hashCodeSameAsStringWithCyrillicCharacters() throws Exception {
hashCodeSameAsString("\u0432\u0435\u0441\u043D\u0430");
}
@Test
public void hashCodeSameAsStringWithEmoji() throws Exception {
hashCodeSameAsString("\ud83d\udca9");
}
private void hashCodeSameAsString(String input) {
assertThat(new AsciiBytes(input).hashCode()).isEqualTo(input.hashCode());
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.data.RandomAccessData;
import org.springframework.boot.loader.data.RandomAccessDataFile;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CentralDirectoryParser}.
*
* @author Phillip Webb
*/
public class CentralDirectoryParserTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File jarFile;
private RandomAccessData jarData;
@Before
public void setup() throws Exception {
this.jarFile = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(this.jarFile);
this.jarData = new RandomAccessDataFile(this.jarFile);
}
@Test
public void visitsInOrder() throws Exception {
MockCentralDirectoryVisitor visitor = new MockCentralDirectoryVisitor();
CentralDirectoryParser parser = new CentralDirectoryParser();
parser.addVisitor(visitor);
parser.parse(this.jarData, false);
List<String> invocations = visitor.getInvocations();
assertThat(invocations).startsWith("visitStart").endsWith("visitEnd")
.contains("visitFileHeader");
}
@Test
public void visitRecords() throws Exception {
Collector collector = new Collector();
CentralDirectoryParser parser = new CentralDirectoryParser();
parser.addVisitor(collector);
parser.parse(this.jarData, false);
Iterator<CentralDirectoryFileHeader> headers = collector.getHeaders().iterator();
assertThat(headers.next().getName().toString()).isEqualTo("META-INF/");
assertThat(headers.next().getName().toString()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(headers.next().getName().toString()).isEqualTo("1.dat");
assertThat(headers.next().getName().toString()).isEqualTo("2.dat");
assertThat(headers.next().getName().toString()).isEqualTo("d/");
assertThat(headers.next().getName().toString()).isEqualTo("d/9.dat");
assertThat(headers.next().getName().toString()).isEqualTo("special/");
assertThat(headers.next().getName().toString()).isEqualTo("special/\u00EB.dat");
assertThat(headers.next().getName().toString()).isEqualTo("nested.jar");
assertThat(headers.next().getName().toString()).isEqualTo("another-nested.jar");
assertThat(headers.hasNext()).isFalse();
}
private static class Collector implements CentralDirectoryVisitor {
private List<CentralDirectoryFileHeader> headers = new ArrayList<>();
@Override
public void visitStart(CentralDirectoryEndRecord endRecord,
RandomAccessData centralDirectoryData) {
}
@Override
public void visitFileHeader(CentralDirectoryFileHeader fileHeader,
int dataOffset) {
this.headers.add(fileHeader.clone());
}
@Override
public void visitEnd() {
}
public List<CentralDirectoryFileHeader> getHeaders() {
return this.headers;
}
}
private static class MockCentralDirectoryVisitor implements CentralDirectoryVisitor {
private final List<String> invocations = new ArrayList<>();
@Override
public void visitStart(CentralDirectoryEndRecord endRecord,
RandomAccessData centralDirectoryData) {
this.invocations.add("visitStart");
}
@Override
public void visitFileHeader(CentralDirectoryFileHeader fileHeader,
int dataOffset) {
this.invocations.add("visitFileHeader");
}
@Override
public void visitEnd() {
this.invocations.add("visitEnd");
}
public List<String> getInvocations() {
return this.invocations;
}
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.net.MalformedURLException;
import java.net.URL;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Handler}.
*
* @author Andy Wilkinson
*/
public class HandlerTests {
private final Handler handler = new Handler();
@Test
public void parseUrlWithJarRootContextAndAbsoluteSpecThatUsesContext()
throws MalformedURLException {
String spec = "/entry.txt";
URL context = createUrl("file:example.jar!/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
}
@Test
public void parseUrlWithDirectoryEntryContextAndAbsoluteSpecThatUsesContext()
throws MalformedURLException {
String spec = "/entry.txt";
URL context = createUrl("file:example.jar!/dir/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
}
@Test
public void parseUrlWithJarRootContextAndRelativeSpecThatUsesContext()
throws MalformedURLException {
String spec = "entry.txt";
URL context = createUrl("file:example.jar!/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm()).isEqualTo("jar:file:example.jar!/entry.txt");
}
@Test
public void parseUrlWithDirectoryEntryContextAndRelativeSpecThatUsesContext()
throws MalformedURLException {
String spec = "entry.txt";
URL context = createUrl("file:example.jar!/dir/");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm())
.isEqualTo("jar:file:example.jar!/dir/entry.txt");
}
@Test
public void parseUrlWithFileEntryContextAndRelativeSpecThatUsesContext()
throws MalformedURLException {
String spec = "entry.txt";
URL context = createUrl("file:example.jar!/dir/file");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm())
.isEqualTo("jar:file:example.jar!/dir/entry.txt");
}
@Test
public void parseUrlWithSpecThatIgnoresContext() throws MalformedURLException {
JarFile.registerUrlProtocolHandler();
String spec = "jar:file:/other.jar!/nested!/entry.txt";
URL context = createUrl("file:example.jar!/dir/file");
this.handler.parseURL(context, spec, 0, spec.length());
assertThat(context.toExternalForm())
.isEqualTo("jar:jar:file:/other.jar!/nested!/entry.txt");
}
@Test
public void sameFileReturnsFalseForUrlsWithDifferentProtocols()
throws MalformedURLException {
assertThat(this.handler.sameFile(new URL("jar:file:foo.jar!/content.txt"),
new URL("file:/foo.jar"))).isFalse();
}
@Test
public void sameFileReturnsFalseForDifferentFileInSameJar()
throws MalformedURLException {
assertThat(this.handler.sameFile(
new URL("jar:file:foo.jar!/the/path/to/the/first/content.txt"),
new URL("jar:file:/foo.jar!/content.txt"))).isFalse();
}
@Test
public void sameFileReturnsFalseForSameFileInDifferentJars()
throws MalformedURLException {
assertThat(this.handler.sameFile(
new URL("jar:file:/the/path/to/the/first.jar!/content.txt"),
new URL("jar:file:/second.jar!/content.txt"))).isFalse();
}
@Test
public void sameFileReturnsTrueForSameFileInSameJar() throws MalformedURLException {
assertThat(this.handler.sameFile(
new URL("jar:file:/the/path/to/the/first.jar!/content.txt"),
new URL("jar:file:/the/path/to/the/first.jar!/content.txt"))).isTrue();
}
@Test
public void sameFileReturnsTrueForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar()
throws MalformedURLException {
assertThat(this.handler.sameFile(
new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt"),
new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt"))).isTrue();
}
@Test
public void hashCodesAreEqualForUrlsThatReferenceSameFileViaNestedArchiveAndFromRootOfJar()
throws MalformedURLException {
assertThat(this.handler
.hashCode(new URL("jar:file:/test.jar!/BOOT-INF/classes!/foo.txt")))
.isEqualTo(this.handler.hashCode(
new URL("jar:file:/test.jar!/BOOT-INF/classes/foo.txt")));
}
@Test
public void urlWithSpecReferencingParentDirectory() throws MalformedURLException {
assertStandardAndCustomHandlerUrlsAreEqual(
"file:/test.jar!/BOOT-INF/classes!/xsd/folderA/a.xsd",
"../folderB/b.xsd");
}
@Test
public void urlWithSpecReferencingAncestorDirectoryOutsideJarStopsAtJarRoot()
throws MalformedURLException {
assertStandardAndCustomHandlerUrlsAreEqual(
"file:/test.jar!/BOOT-INF/classes!/xsd/folderA/a.xsd",
"../../../../../../folderB/b.xsd");
}
@Test
public void urlWithSpecReferencingCurrentDirectory() throws MalformedURLException {
assertStandardAndCustomHandlerUrlsAreEqual(
"file:/test.jar!/BOOT-INF/classes!/xsd/folderA/a.xsd",
"./folderB/./b.xsd");
}
private void assertStandardAndCustomHandlerUrlsAreEqual(String context, String spec)
throws MalformedURLException {
URL standardUrl = new URL(new URL("jar:" + context), spec);
URL customHandlerUrl = new URL(new URL("jar", null, -1, context, this.handler),
spec);
assertThat(customHandlerUrl.toString()).isEqualTo(standardUrl.toString());
}
private URL createUrl(String file) throws MalformedURLException {
return new URL("jar", null, -1, file, this.handler);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2016 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.boot.loader.jar;
import java.io.UnsupportedEncodingException;
import org.junit.Test;
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JarEntryName}.
*
* @author Andy Wilkinson
*/
public class JarEntryNameTests {
@Test
public void basicName() {
assertThat(new JarEntryName("a/b/C.class").toString()).isEqualTo("a/b/C.class");
}
@Test
public void nameWithSingleByteEncodedCharacters() {
assertThat(new JarEntryName("%61/%62/%43.class").toString())
.isEqualTo("a/b/C.class");
}
@Test
public void nameWithDoubleByteEncodedCharacters() {
assertThat(new JarEntryName("%c3%a1/b/C.class").toString())
.isEqualTo("\u00e1/b/C.class");
}
@Test
public void nameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters()
throws UnsupportedEncodingException {
assertThat(new JarEntryName("%c3%a1/b/\u00c7.class").toString())
.isEqualTo("\u00e1/b/\u00c7.class");
}
}

View File

@@ -0,0 +1,481 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilePermission;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
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.boot.loader.TestJarCreator;
import org.springframework.boot.loader.data.RandomAccessDataFile;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link JarFile}.
*
* @author Phillip Webb
* @author Martin Lau
* @author Andy Wilkinson
*/
public class JarFileTests {
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private File rootJarFile;
private JarFile jarFile;
@Before
public void setup() throws Exception {
this.rootJarFile = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(this.rootJarFile);
this.jarFile = new JarFile(this.rootJarFile);
}
@Test
public void jdkJarFile() throws Exception {
// Sanity checks to see how the default jar file operates
java.util.jar.JarFile jarFile = new java.util.jar.JarFile(this.rootJarFile);
Enumeration<java.util.jar.JarEntry> entries = jarFile.entries();
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(entries.nextElement().getName()).isEqualTo("1.dat");
assertThat(entries.nextElement().getName()).isEqualTo("2.dat");
assertThat(entries.nextElement().getName()).isEqualTo("d/");
assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat");
assertThat(entries.nextElement().getName()).isEqualTo("special/");
assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat");
assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
assertThat(entries.hasMoreElements()).isFalse();
URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl });
assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull();
assertThat(urlClassLoader.getResource("d/9.dat")).isNotNull();
jarFile.close();
urlClassLoader.close();
}
@Test
public void createFromFile() throws Exception {
JarFile jarFile = new JarFile(this.rootJarFile);
assertThat(jarFile.getName()).isNotNull();
jarFile.close();
}
@Test
public void getManifest() throws Exception {
assertThat(this.jarFile.getManifest().getMainAttributes().getValue("Built-By"))
.isEqualTo("j1");
}
@Test
public void getManifestEntry() throws Exception {
ZipEntry entry = this.jarFile.getJarEntry("META-INF/MANIFEST.MF");
Manifest manifest = new Manifest(this.jarFile.getInputStream(entry));
assertThat(manifest.getMainAttributes().getValue("Built-By")).isEqualTo("j1");
}
@Test
public void getEntries() throws Exception {
Enumeration<java.util.jar.JarEntry> entries = this.jarFile.entries();
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(entries.nextElement().getName()).isEqualTo("1.dat");
assertThat(entries.nextElement().getName()).isEqualTo("2.dat");
assertThat(entries.nextElement().getName()).isEqualTo("d/");
assertThat(entries.nextElement().getName()).isEqualTo("d/9.dat");
assertThat(entries.nextElement().getName()).isEqualTo("special/");
assertThat(entries.nextElement().getName()).isEqualTo("special/\u00EB.dat");
assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
assertThat(entries.hasMoreElements()).isFalse();
}
@Test
public void getSpecialResourceViaClassLoader() throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(
new URL[] { this.jarFile.getUrl() });
assertThat(urlClassLoader.getResource("special/\u00EB.dat")).isNotNull();
urlClassLoader.close();
}
@Test
public void getJarEntry() throws Exception {
java.util.jar.JarEntry entry = this.jarFile.getJarEntry("1.dat");
assertThat(entry).isNotNull();
assertThat(entry.getName()).isEqualTo("1.dat");
}
@Test
public void getInputStream() throws Exception {
InputStream inputStream = this.jarFile
.getInputStream(this.jarFile.getEntry("1.dat"));
assertThat(inputStream.available()).isEqualTo(1);
assertThat(inputStream.read()).isEqualTo(1);
assertThat(inputStream.available()).isEqualTo(0);
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
public void getName() throws Exception {
assertThat(this.jarFile.getName()).isEqualTo(this.rootJarFile.getPath());
}
@Test
public void getSize() throws Exception {
assertThat(this.jarFile.size()).isEqualTo((int) this.rootJarFile.length());
}
@Test
public void getEntryTime() throws Exception {
java.util.jar.JarFile jdkJarFile = new java.util.jar.JarFile(this.rootJarFile);
assertThat(this.jarFile.getEntry("META-INF/MANIFEST.MF").getTime())
.isEqualTo(jdkJarFile.getEntry("META-INF/MANIFEST.MF").getTime());
jdkJarFile.close();
}
@Test
public void close() throws Exception {
RandomAccessDataFile randomAccessDataFile = spy(
new RandomAccessDataFile(this.rootJarFile, 1));
JarFile jarFile = new JarFile(randomAccessDataFile);
jarFile.close();
verify(randomAccessDataFile).close();
}
@Test
public void getUrl() throws Exception {
URL url = this.jarFile.getUrl();
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/");
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
assertThat(jarURLConnection.getJarFile()).isSameAs(this.jarFile);
assertThat(jarURLConnection.getJarEntry()).isNull();
assertThat(jarURLConnection.getContentLength()).isGreaterThan(1);
assertThat(jarURLConnection.getContent()).isSameAs(this.jarFile);
assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar");
assertThat(jarURLConnection.getJarFileURL().toURI())
.isEqualTo(this.rootJarFile.toURI());
}
@Test
public void createEntryUrl() throws Exception {
URL url = new URL(this.jarFile.getUrl(), "1.dat");
assertThat(url.toString())
.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/1.dat");
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
assertThat(jarURLConnection.getJarFile()).isSameAs(this.jarFile);
assertThat(jarURLConnection.getJarEntry())
.isSameAs(this.jarFile.getJarEntry("1.dat"));
assertThat(jarURLConnection.getContentLength()).isEqualTo(1);
assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class);
assertThat(jarURLConnection.getContentType()).isEqualTo("content/unknown");
assertThat(jarURLConnection.getPermission()).isInstanceOf(FilePermission.class);
FilePermission permission = (FilePermission) jarURLConnection.getPermission();
assertThat(permission.getActions()).isEqualTo("read");
assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath());
}
@Test
public void getMissingEntryUrl() throws Exception {
URL url = new URL(this.jarFile.getUrl(), "missing.dat");
assertThat(url.toString())
.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/missing.dat");
this.thrown.expect(FileNotFoundException.class);
((JarURLConnection) url.openConnection()).getJarEntry();
}
@Test
public void getUrlStream() throws Exception {
URL url = this.jarFile.getUrl();
url.openConnection();
this.thrown.expect(IOException.class);
url.openStream();
}
@Test
public void getEntryUrlStream() throws Exception {
URL url = new URL(this.jarFile.getUrl(), "1.dat");
url.openConnection();
InputStream stream = url.openStream();
assertThat(stream.read()).isEqualTo(1);
assertThat(stream.read()).isEqualTo(-1);
}
@Test
public void getNestedJarFile() throws Exception {
JarFile nestedJarFile = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/");
assertThat(entries.nextElement().getName()).isEqualTo("META-INF/MANIFEST.MF");
assertThat(entries.nextElement().getName()).isEqualTo("3.dat");
assertThat(entries.nextElement().getName()).isEqualTo("4.dat");
assertThat(entries.nextElement().getName()).isEqualTo("\u00E4.dat");
assertThat(entries.hasMoreElements()).isFalse();
InputStream inputStream = nestedJarFile
.getInputStream(nestedJarFile.getEntry("3.dat"));
assertThat(inputStream.read()).isEqualTo(3);
assertThat(inputStream.read()).isEqualTo(-1);
URL url = nestedJarFile.getUrl();
assertThat(url.toString())
.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/");
JarURLConnection conn = (JarURLConnection) url.openConnection();
assertThat(conn.getJarFile()).isSameAs(nestedJarFile);
assertThat(conn.getJarFileURL().toString())
.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
assertThat(conn.getInputStream()).isNotNull();
JarInputStream jarInputStream = new JarInputStream(conn.getInputStream());
assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("3.dat");
assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("4.dat");
assertThat(jarInputStream.getNextJarEntry().getName()).isEqualTo("\u00E4.dat");
jarInputStream.close();
assertThat(conn.getPermission()).isInstanceOf(FilePermission.class);
FilePermission permission = (FilePermission) conn.getPermission();
assertThat(permission.getActions()).isEqualTo("read");
assertThat(permission.getName()).isEqualTo(this.rootJarFile.getPath());
}
@Test
public void getNestedJarDirectory() throws Exception {
JarFile nestedJarFile = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("d/"));
Enumeration<java.util.jar.JarEntry> entries = nestedJarFile.entries();
assertThat(entries.nextElement().getName()).isEqualTo("9.dat");
assertThat(entries.hasMoreElements()).isFalse();
InputStream inputStream = nestedJarFile
.getInputStream(nestedJarFile.getEntry("9.dat"));
assertThat(inputStream.read()).isEqualTo(9);
assertThat(inputStream.read()).isEqualTo(-1);
URL url = nestedJarFile.getUrl();
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/d!/");
assertThat(((JarURLConnection) url.openConnection()).getJarFile())
.isSameAs(nestedJarFile);
}
@Test
public void getNestedJarEntryUrl() throws Exception {
JarFile nestedJarFile = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
URL url = nestedJarFile.getJarEntry("3.dat").getUrl();
assertThat(url.toString())
.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat");
InputStream inputStream = url.openStream();
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(3);
}
@Test
public void createUrlFromString() throws Exception {
JarFile.registerUrlProtocolHandler();
String spec = "jar:" + this.rootJarFile.toURI() + "!/nested.jar!/3.dat";
URL url = new URL(spec);
assertThat(url.toString()).isEqualTo(spec);
InputStream inputStream = url.openStream();
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(3);
JarURLConnection connection = (JarURLConnection) url.openConnection();
assertThat(connection.getURL().toString()).isEqualTo(spec);
assertThat(connection.getJarFileURL().toString())
.isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
assertThat(connection.getEntryName()).isEqualTo("3.dat");
}
@Test
public void createNonNestedUrlFromString() throws Exception {
nonNestedJarFileFromString("jar:" + this.rootJarFile.toURI() + "!/2.dat");
}
@Test
public void createNonNestedUrlFromPathString() throws Exception {
nonNestedJarFileFromString(
"jar:" + this.rootJarFile.toPath().toUri() + "!/2.dat");
}
private void nonNestedJarFileFromString(String spec) throws Exception {
JarFile.registerUrlProtocolHandler();
URL url = new URL(spec);
assertThat(url.toString()).isEqualTo(spec);
InputStream inputStream = url.openStream();
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(2);
JarURLConnection connection = (JarURLConnection) url.openConnection();
assertThat(connection.getURL().toString()).isEqualTo(spec);
assertThat(connection.getJarFileURL().toURI())
.isEqualTo(this.rootJarFile.toURI());
assertThat(connection.getEntryName()).isEqualTo("2.dat");
}
@Test
public void getDirectoryInputStream() throws Exception {
InputStream inputStream = this.jarFile
.getInputStream(this.jarFile.getEntry("d/"));
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
public void getDirectoryInputStreamWithoutSlash() throws Exception {
InputStream inputStream = this.jarFile.getInputStream(this.jarFile.getEntry("d"));
assertThat(inputStream).isNotNull();
assertThat(inputStream.read()).isEqualTo(-1);
}
@Test
public void sensibleToString() throws Exception {
assertThat(this.jarFile.toString()).isEqualTo(this.rootJarFile.getPath());
assertThat(this.jarFile.getNestedJarFile(this.jarFile.getEntry("nested.jar"))
.toString()).isEqualTo(this.rootJarFile.getPath() + "!/nested.jar");
}
@Test
public void verifySignedJar() throws Exception {
String classpath = System.getProperty("java.class.path");
String[] entries = classpath.split(System.getProperty("path.separator"));
String signedJarFile = null;
for (String entry : entries) {
if (entry.contains("bcprov")) {
signedJarFile = entry;
}
}
assertThat(signedJarFile).isNotNull();
java.util.jar.JarFile jarFile = new JarFile(new File(signedJarFile));
jarFile.getManifest();
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
InputStream inputStream = jarFile.getInputStream(jarEntry);
inputStream.skip(Long.MAX_VALUE);
inputStream.close();
if (!jarEntry.getName().startsWith("META-INF") && !jarEntry.isDirectory()
&& !jarEntry.getName().endsWith("TigerDigest.class")) {
assertThat(jarEntry.getCertificates()).isNotNull();
}
}
jarFile.close();
}
@Test
public void jarFileWithScriptAtTheStart() throws Exception {
File file = this.temporaryFolder.newFile();
InputStream sourceJarContent = new FileInputStream(this.rootJarFile);
FileOutputStream outputStream = new FileOutputStream(file);
StreamUtils.copy("#/bin/bash", Charset.defaultCharset(), outputStream);
FileCopyUtils.copy(sourceJarContent, outputStream);
this.rootJarFile = file;
this.jarFile = new JarFile(file);
// Call some other tests to verify
getEntries();
getNestedJarFile();
}
@Test
public void cannotLoadMissingJar() throws Exception {
// relates to gh-1070
JarFile nestedJarFile = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
URL nestedUrl = nestedJarFile.getUrl();
URL url = new URL(nestedUrl, nestedJarFile.getUrl() + "missing.jar!/3.dat");
this.thrown.expect(FileNotFoundException.class);
url.openConnection().getInputStream();
}
@Test
public void registerUrlProtocolHandlerWithNoExistingRegistration() {
String original = System.getProperty(PROTOCOL_HANDLER);
try {
System.clearProperty(PROTOCOL_HANDLER);
JarFile.registerUrlProtocolHandler();
String protocolHandler = System.getProperty(PROTOCOL_HANDLER);
assertThat(protocolHandler).isEqualTo(HANDLERS_PACKAGE);
}
finally {
if (original == null) {
System.clearProperty(PROTOCOL_HANDLER);
}
else {
System.setProperty(PROTOCOL_HANDLER, original);
}
}
}
@Test
public void registerUrlProtocolHandlerAddsToExistingRegistration() {
String original = System.getProperty(PROTOCOL_HANDLER);
try {
System.setProperty(PROTOCOL_HANDLER, "com.example");
JarFile.registerUrlProtocolHandler();
String protocolHandler = System.getProperty(PROTOCOL_HANDLER);
assertThat(protocolHandler).isEqualTo("com.example|" + HANDLERS_PACKAGE);
}
finally {
if (original == null) {
System.clearProperty(PROTOCOL_HANDLER);
}
else {
System.setProperty(PROTOCOL_HANDLER, original);
}
}
}
@Test
public void jarFileCanBeDeletedOnceItHasBeenClosed() throws Exception {
File temp = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(temp);
JarFile jf = new JarFile(temp);
jf.close();
assertThat(temp.delete()).isTrue();
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2012-2017 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.boot.loader.jar;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.net.URL;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.TestJarCreator;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JarURLConnection}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Rostyslav Dudka
*/
public class JarURLConnectionTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder(new File("target"));
private File rootJarFile;
private JarFile jarFile;
@Before
public void setup() throws Exception {
this.rootJarFile = this.temporaryFolder.newFile();
TestJarCreator.createTestJar(this.rootJarFile);
this.jarFile = new JarFile(this.rootJarFile);
}
@Test
public void connectionToRootUsingAbsoluteUrl() throws Exception {
URL url = new URL("jar:file:" + getAbsolutePath() + "!/");
assertThat(JarURLConnection.get(url, this.jarFile).getContent())
.isSameAs(this.jarFile);
}
@Test
public void connectionToRootUsingRelativeUrl() throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/");
assertThat(JarURLConnection.get(url, this.jarFile).getContent())
.isSameAs(this.jarFile);
}
@Test
public void connectionToEntryUsingAbsoluteUrl() throws Exception {
URL url = new URL("jar:file:" + getAbsolutePath() + "!/1.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 1 }));
}
@Test
public void connectionToEntryUsingRelativeUrl() throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/1.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 1 }));
}
@Test
public void connectionToEntryUsingAbsoluteUrlWithFileColonSlashSlashPrefix()
throws Exception {
URL url = new URL("jar:file:/" + getAbsolutePath() + "!/1.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 1 }));
}
@Test
public void connectionToEntryUsingAbsoluteUrlForNestedEntry() throws Exception {
URL url = new URL("jar:file:" + getAbsolutePath() + "!/nested.jar!/3.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 }));
}
@Test
public void connectionToEntryUsingRelativeUrlForNestedEntry() throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
assertThat(JarURLConnection.get(url, this.jarFile).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 }));
}
@Test
public void connectionToEntryUsingAbsoluteUrlForEntryFromNestedJarFile()
throws Exception {
URL url = new URL("jar:file:" + getAbsolutePath() + "!/nested.jar!/3.dat");
JarFile nested = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
assertThat(JarURLConnection.get(url, nested).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 }));
}
@Test
public void connectionToEntryUsingRelativeUrlForEntryFromNestedJarFile()
throws Exception {
URL url = new URL("jar:file:" + getRelativePath() + "!/nested.jar!/3.dat");
JarFile nested = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
assertThat(JarURLConnection.get(url, nested).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 }));
}
@Test
public void connectionToEntryInNestedJarFromUrlThatUsesExistingUrlAsContext()
throws Exception {
URL url = new URL(new URL("jar", null, -1,
"file:" + getAbsolutePath() + "!/nested.jar!/", new Handler()), "/3.dat");
JarFile nested = this.jarFile
.getNestedJarFile(this.jarFile.getEntry("nested.jar"));
assertThat(JarURLConnection.get(url, nested).getInputStream())
.hasSameContentAs(new ByteArrayInputStream(new byte[] { 3 }));
}
@Test
public void getContentLengthReturnsLengthOfUnderlyingEntry() throws Exception {
URL url = new URL(new URL("jar", null, -1,
"file:" + getAbsolutePath() + "!/nested.jar!/", new Handler()), "/3.dat");
assertThat(url.openConnection().getContentLength()).isEqualTo(1);
}
@Test
public void getContentLengthLongReturnsLengthOfUnderlyingEntry() throws Exception {
URL url = new URL(new URL("jar", null, -1,
"file:" + getAbsolutePath() + "!/nested.jar!/", new Handler()), "/3.dat");
assertThat(url.openConnection().getContentLengthLong()).isEqualTo(1);
}
@Test
public void getLastModifiedReturnsLastModifiedTimeOfJarEntry() throws Exception {
URL url = new URL("jar:file:" + getAbsolutePath() + "!/1.dat");
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
assertThat(connection.getLastModified())
.isEqualTo(connection.getJarEntry().getTime());
}
private String getAbsolutePath() {
return this.rootJarFile.getAbsolutePath().replace('\\', '/');
}
private String getRelativePath() {
return this.rootJarFile.getPath().replace('\\', '/');
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2012-2016 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.boot.loader.util;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SystemPropertyUtils}.
*
* @author Dave Syer
*/
public class SystemPropertyUtilsTests {
@BeforeClass
public static void init() {
System.setProperty("foo", "bar");
}
@AfterClass
public static void close() {
System.clearProperty("foo");
}
@Test
public void testVanillaPlaceholder() {
assertThat(SystemPropertyUtils.resolvePlaceholders("${foo}")).isEqualTo("bar");
}
@Test
public void testDefaultValue() {
assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:foo}"))
.isEqualTo("foo");
}
@Test
public void testNestedPlaceholder() {
assertThat(SystemPropertyUtils.resolvePlaceholders("${bar:${spam:foo}}"))
.isEqualTo("foo");
}
@Test
public void testEnvVar() {
assertThat(SystemPropertyUtils.getProperty("lang"))
.isEqualTo(System.getenv("LANG"));
}
}

View File

@@ -0,0 +1 @@
loader.main: my.BootInfBarApplication

View File

@@ -0,0 +1,3 @@
foo: Application
loader.main: my.${foo}
loader.path: etc

View File

@@ -0,0 +1 @@
loader.main: my.BarApplication

View File

@@ -0,0 +1 @@
loader.main: demo.HomeApplication

View File

@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Start-Class: ${foo.main}

View File

@@ -0,0 +1 @@
foo.main: demo.FooApplication

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>