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:
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user