Loader changes

This commit is contained in:
Phillip Webb 2013-09-19 10:51:15 -07:00 committed by Dave Syer
parent 053c072155
commit e9fd7c96b8
4 changed files with 252 additions and 280 deletions

View File

@ -195,8 +195,4 @@ public abstract class Launcher {
return (Runnable) constructor.newInstance(mainClass, args); return (Runnable) constructor.newInstance(mainClass, args);
} }
protected boolean isArchive(String name) {
return name.endsWith(".jar") || name.endsWith(".zip");
}
} }

View File

@ -23,22 +23,19 @@ import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.security.ProtectionDomain;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.springframework.boot.loader.util.SystemPropertyUtils; import org.springframework.boot.loader.util.SystemPropertyUtils;
import org.springframework.util.ResourceUtils;
/** /**
* <p>
* {@link Launcher} for archives with user-configured classpath and main class via a * {@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 * 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. * well-behaved OS-level services than a model based on executable jars.
* </p>
* *
* <p> * <p>
* Looks in various places for a properties file to extract loader settings, defaulting to * Looks in various places for a properties file to extract loader settings, defaulting to
@ -48,7 +45,7 @@ import org.springframework.util.ResourceUtils;
* will look for <code>foo.properties</code>. If that file doesn't exist then tries * will look for <code>foo.properties</code>. If that file doesn't exist then tries
* <code>loader.config.location</code> (with allowed prefixes <code>classpath:</code> and * <code>loader.config.location</code> (with allowed prefixes <code>classpath:</code> and
* <code>file:</code> or any valid URL). Once that file is located turns it into * <code>file:</code> or any valid URL). Once that file is located turns it into
* Properties and extracts optional values (which can also be provided oroverridden as * Properties and extracts optional values (which can also be provided overridden as
* System properties in case the file doesn't exist): * System properties in case the file doesn't exist):
* *
* <ul> * <ul>
@ -59,16 +56,13 @@ import org.springframework.util.ResourceUtils;
* loader is set up. No default, but will fall back to looking in a * loader is set up. No default, but will fall back to looking in a
* <code>MANIFEST.MF</code> if there is one.</li> * <code>MANIFEST.MF</code> if there is one.</li>
* </ul> * </ul>
* </p>
*
* <p>
*
* </p>
* *
* @author Dave Syer * @author Dave Syer
*/ */
public class PropertiesLauncher extends Launcher { public class PropertiesLauncher extends Launcher {
private Logger logger = Logger.getLogger(Launcher.class.getName());
/** /**
* Properties key for main class * Properties key for main class
*/ */
@ -81,11 +75,9 @@ public class PropertiesLauncher extends Launcher {
public static final String HOME = "loader.home"; public static final String HOME = "loader.home";
public static String CONFIG_NAME = "loader.config.name"; public static final String CONFIG_NAME = "loader.config.name";
public static String CONFIG_LOCATION = "loader.config.location"; public static final String CONFIG_LOCATION = "loader.config.location";
private Logger logger = Logger.getLogger(Launcher.class.getName());
private static final List<String> DEFAULT_PATHS = Arrays.asList("lib/"); private static final List<String> DEFAULT_PATHS = Arrays.asList("lib/");
@ -94,51 +86,20 @@ public class PropertiesLauncher extends Launcher {
private Properties properties = new Properties(); private Properties properties = new Properties();
@Override @Override
public void launch(String[] args) { protected void launch(String[] args, ProtectionDomain protectionDomain)
try { throws Exception {
launch(args, new ExplodedArchive(new File(getHomeDirectory()))); launch(args, new ExplodedArchive(getHomeDirectory()));
}
catch (Exception ex) {
ex.printStackTrace();
System.exit(1);
}
} }
protected String getHomeDirectory() { protected File getHomeDirectory() {
return SystemPropertyUtils.resolvePlaceholders(System.getProperty(HOME, return new File(SystemPropertyUtils.resolvePlaceholders(System.getProperty(HOME,
"${user.dir}")); "${user.dir}")));
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
String name = entry.getName();
if (entry.isDirectory()) {
for (String path : this.paths) {
if (path.length() > 0 && name.equals(path)) {
return true;
}
}
}
else {
for (String path : this.paths) {
if (path.length() > 0 && name.startsWith(path) && isArchive(name)) {
return true;
}
}
}
return false;
}
@Override
protected void postProcessLib(Archive archive, List<Archive> lib) throws Exception {
lib.add(0, archive);
} }
/** /**
* Look in various places for a properties file to extract loader settings. Default to * Look in various places for a properties file to extract loader settings. Default to
* <code>application.properties</code> either on the current classpath or in the * <code>application.properties</code> either on the current classpath or in the
* current working directory. * current working directory.
*
* @see org.springframework.boot.loader.Launcher#launch(java.lang.String[], * @see org.springframework.boot.loader.Launcher#launch(java.lang.String[],
* org.springframework.boot.loader.Archive) * org.springframework.boot.loader.Archive)
*/ */
@ -149,83 +110,24 @@ public class PropertiesLauncher extends Launcher {
} }
protected void initialize() throws Exception { protected void initialize() throws Exception {
initializeProperties();
initializePaths();
}
private void initializeProperties() throws Exception, IOException {
String config = SystemPropertyUtils.resolvePlaceholders(System.getProperty( String config = SystemPropertyUtils.resolvePlaceholders(System.getProperty(
CONFIG_NAME, "application")) + ".properties"; CONFIG_NAME, "application")) + ".properties";
while (config.startsWith("/")) { InputStream resource = getClasspathResource(config);
config = config.substring(1);
}
this.logger.fine("Trying default location: " + config);
InputStream resource = getClass().getResourceAsStream("/" + config);
if (resource == null) { if (resource == null) {
config = SystemPropertyUtils.resolvePlaceholders(System.getProperty( config = SystemPropertyUtils.resolvePlaceholders(System.getProperty(
CONFIG_LOCATION, config)); CONFIG_LOCATION, config));
resource = getResource(config);
if (config.startsWith("classpath:")) {
config = config.substring("classpath:".length());
while (config.startsWith("/")) {
config = config.substring(1);
}
config = "/" + config;
this.logger.fine("Trying classpath: " + config);
resource = getClass().getResourceAsStream(config);
}
else {
if (config.startsWith("file:")) {
config = config.substring("file:".length());
if (config.startsWith("//")) {
config = config.substring(2);
}
}
if (!config.contains(":")) {
File file = new File(config);
this.logger.fine("Trying file: " + config);
if (file.canRead()) {
resource = new FileInputStream(file);
}
}
else {
URL url = new URL(config);
if (exists(url)) {
URLConnection con = url.openConnection();
try {
resource = con.getInputStream();
}
catch (IOException ex) {
// Close the HTTP connection (if applicable).
if (con instanceof HttpURLConnection) {
((HttpURLConnection) con).disconnect();
}
throw ex;
}
}
}
}
} }
if (resource != null) { if (resource != null) {
this.logger.info("Found: " + config); this.logger.info("Found: " + config);
this.properties.load(resource);
try { try {
String path = System.getProperty(PATH); this.properties.load(resource);
if (path == null) {
path = this.properties.getProperty(PATH);
}
if (path != null) {
path = SystemPropertyUtils.resolvePlaceholders(path);
this.paths = new ArrayList<String>(Arrays.asList(path.split(",")));
for (int i = 0; i < this.paths.size(); i++) {
this.paths.set(i, this.paths.get(i).trim());
}
}
} }
finally { finally {
resource.close(); resource.close();
@ -234,53 +136,153 @@ public class PropertiesLauncher extends Launcher {
else { else {
this.logger.info("Not found: " + config); this.logger.info("Not found: " + config);
} }
for (int i = 0; i < this.paths.size(); i++) { }
if (!this.paths.get(i).endsWith("/")) {
// Always a directory private InputStream getResource(String config) throws Exception {
this.paths.set(i, this.paths.get(i) + "/"); if (config.startsWith("classpath:")) {
} return getClasspathResource(config.substring("classpath:".length()));
if (this.paths.get(i).startsWith("./")) { }
// No need for current dir path config = stripFileUrlPrefix(config);
this.paths.set(i, this.paths.get(i).substring(2)); 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);
} }
} }
for (Iterator<String> iter = this.paths.iterator(); iter.hasNext();) { return config;
String path = iter.next(); }
if (path.equals(".") || path.equals("")) {
// Empty path is always on the classpath so no need for it to be private boolean isUrl(String config) {
// explicitly listed here return config.contains("://");
iter.remove(); }
private InputStream getClasspathResource(String config) {
while (config.startsWith("/")) {
config = config.substring(1);
}
config = "/" + config;
this.logger.fine("Trying classpath: " + config);
return getClass().getResourceAsStream(config);
}
private InputStream getFileResource(String config) throws Exception {
File file = new File(config);
this.logger.fine("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 IOException {
String path = System.getProperty(PATH);
if (path == null) {
path = this.properties.getProperty(PATH);
}
if (path != null) {
this.paths = parsePathsProperty(SystemPropertyUtils.resolvePlaceholders(path));
} }
this.logger.info("Nested archive paths: " + this.paths); this.logger.info("Nested archive paths: " + this.paths);
} }
private boolean exists(URL url) throws IOException { private List<String> parsePathsProperty(String commaSeparatedPaths) {
List<String> paths = new ArrayList<String>();
for (String path : commaSeparatedPaths.split(",")) {
path = cleanupPath(path);
// Empty path is always on the classpath so no need for it to be explicitly
// listed here
if (!(path.equals(".") || path.equals(""))) {
paths.add(path);
}
}
return paths;
}
// Try a URL connection content-length header... private String cleanupPath(String path) {
URLConnection con = url.openConnection(); path = path.trim();
ResourceUtils.useCachesIfNecessary(con); // Always a directory
HttpURLConnection httpCon = (con instanceof HttpURLConnection ? (HttpURLConnection) con if (!path.endsWith("/")) {
: null); path = path + "/";
if (httpCon != null) {
httpCon.setRequestMethod("HEAD");
int code = httpCon.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
return true;
}
else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
} }
if (con.getContentLength() >= 0) { // No need for current dir path
return true; if (path.startsWith("./")) {
path = path.substring(2);
} }
if (httpCon != null) { return path;
// no HTTP OK status, and no content-length header: give up }
httpCon.disconnect();
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
String name = entry.getName();
for (String path : this.paths) {
if (path.length() > 0) {
if ((entry.isDirectory() && name.equals(path))
|| (!entry.isDirectory() && name.startsWith(path) && isArchive(name))) {
return true;
}
}
} }
return false; return false;
}
private boolean isArchive(String name) {
return name.endsWith(".jar") || name.endsWith(".zip");
}
@Override
protected void postProcessLib(Archive archive, List<Archive> lib) throws Exception {
lib.add(0, archive);
} }
@Override @Override

View File

@ -19,8 +19,6 @@ package org.springframework.boot.loader.util;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
/** /**
* Helper class for resolving placeholders in texts. Usually applied to file paths. * Helper class for resolving placeholders in texts. Usually applied to file paths.
* *
@ -50,7 +48,7 @@ public abstract class SystemPropertyUtils {
/** Value separator for system property placeholders: ":" */ /** Value separator for system property placeholders: ":" */
public static final String VALUE_SEPARATOR = ":"; public static final String VALUE_SEPARATOR = ":";
private static final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(); private static final String SIMPLE_PREFIX = PLACEHOLDER_PREFIX.substring(1);
/** /**
* Resolve ${...} placeholders in the given text, replacing them with corresponding * Resolve ${...} placeholders in the given text, replacing them with corresponding
@ -62,147 +60,120 @@ public abstract class SystemPropertyUtils {
* @throws IllegalArgumentException if there is an unresolvable placeholder * @throws IllegalArgumentException if there is an unresolvable placeholder
*/ */
public static String resolvePlaceholders(String text) { public static String resolvePlaceholders(String text) {
return helper.replacePlaceholders(text); if (text == null) {
throw new IllegalArgumentException("Argument 'value' must not be null.");
}
return parseStringValue(text, text, new HashSet<String>());
} }
static protected class PropertyPlaceholderHelper { private static String parseStringValue(String value, String current,
Set<String> visitedPlaceholders) {
private static final String simplePrefix = PLACEHOLDER_PREFIX.substring(1); StringBuilder buf = new StringBuilder(current);
/** int startIndex = current.indexOf(PLACEHOLDER_PREFIX);
* Replaces all placeholders of format {@code $ name} with the value returned from while (startIndex != -1) {
* the supplied {@link PlaceholderResolver}. int endIndex = findPlaceholderEndIndex(buf, startIndex);
* @param value the value containing the placeholders to be replaced. if (endIndex != -1) {
* @return the supplied value with placeholders replaced inline. String placeholder = buf.substring(
*/ startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
public String replacePlaceholders(String value) { String originalPlaceholder = placeholder;
Assert.notNull(value, "Argument 'value' must not be null."); if (!visitedPlaceholders.add(originalPlaceholder)) {
return parseStringValue(value, value, new HashSet<String>()); throw new IllegalArgumentException("Circular placeholder reference '"
} + originalPlaceholder + "' in property definitions");
}
private String parseStringValue(String value, String current, // Recursive invocation, parsing placeholders contained in the
Set<String> visitedPlaceholders) { // placeholder
// key.
StringBuilder buf = new StringBuilder(current); placeholder = parseStringValue(value, placeholder, visitedPlaceholders);
// Now obtain the value for the fully resolved key...
int startIndex = current.indexOf(PLACEHOLDER_PREFIX); String propVal = resolvePlaceholder(value, placeholder);
while (startIndex != -1) { if (propVal == null && VALUE_SEPARATOR != null) {
int endIndex = findPlaceholderEndIndex(buf, startIndex); int separatorIndex = placeholder.indexOf(VALUE_SEPARATOR);
if (endIndex != -1) { if (separatorIndex != -1) {
String placeholder = buf.substring( String actualPlaceholder = placeholder.substring(0,
startIndex + PLACEHOLDER_PREFIX.length(), endIndex); separatorIndex);
String originalPlaceholder = placeholder; String defaultValue = placeholder.substring(separatorIndex
if (!visitedPlaceholders.add(originalPlaceholder)) { + VALUE_SEPARATOR.length());
throw new IllegalArgumentException( propVal = resolvePlaceholder(value, actualPlaceholder);
"Circular placeholder reference '" + originalPlaceholder if (propVal == null) {
+ "' in property definitions"); propVal = defaultValue;
}
// Recursive invocation, parsing placeholders contained in the
// placeholder
// key.
placeholder = parseStringValue(value, placeholder,
visitedPlaceholders);
// Now obtain the value for the fully resolved key...
String propVal = resolvePlaceholder(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(value, actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
} }
} }
if (propVal != null) { }
// Recursive invocation, parsing placeholders contained in the if (propVal != null) {
// previously resolved placeholder value. // Recursive invocation, parsing placeholders contained in the
propVal = parseStringValue(value, propVal, visitedPlaceholders); // previously resolved placeholder value.
buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal = parseStringValue(value, propVal, visitedPlaceholders);
propVal); buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(),
startIndex = buf.indexOf(PLACEHOLDER_PREFIX, propVal);
startIndex + propVal.length()); 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 { else {
startIndex = -1; // Proceed with unprocessed value.
startIndex = buf.indexOf(PLACEHOLDER_PREFIX, endIndex
+ PLACEHOLDER_SUFFIX.length());
} }
visitedPlaceholders.remove(originalPlaceholder);
} }
else {
return buf.toString(); startIndex = -1;
}
private String resolvePlaceholder(String text, String placeholderName) {
try {
String propVal = System.getProperty(placeholderName);
if (propVal == null) {
// Fall back to searching the system environment.
propVal = System.getenv(placeholderName);
}
return propVal;
}
catch (Throwable ex) {
System.err.println("Could not resolve placeholder '" + placeholderName
+ "' in [" + text + "] as system property: " + ex);
return null;
} }
} }
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) { return buf.toString();
int index = startIndex + PLACEHOLDER_PREFIX.length(); }
int withinNestedPlaceholder = 0;
while (index < buf.length()) { private static String resolvePlaceholder(String text, String placeholderName) {
if (substringMatch(buf, index, PLACEHOLDER_SUFFIX)) { try {
if (withinNestedPlaceholder > 0) { String propVal = System.getProperty(placeholderName);
withinNestedPlaceholder--; if (propVal == null) {
index = index + PLACEHOLDER_SUFFIX.length(); // Fall back to searching the system environment.
} propVal = System.getenv(placeholderName);
else { }
return index; return propVal;
} }
} catch (Throwable ex) {
else if (substringMatch(buf, index, System.err.println("Could not resolve placeholder '" + placeholderName
PropertyPlaceholderHelper.simplePrefix)) { + "' in [" + text + "] as system property: " + ex);
withinNestedPlaceholder++; return null;
index = index + PropertyPlaceholderHelper.simplePrefix.length(); }
}
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 { else {
index++; return index;
} }
} }
return -1; else if (substringMatch(buf, index, SIMPLE_PREFIX)) {
} withinNestedPlaceholder++;
index = index + SIMPLE_PREFIX.length();
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; else {
} index++;
private static class Assert {
public static void notNull(Object target, String message) {
if (target == null) {
throw new IllegalStateException(message);
}
} }
} }
return -1;
}
private static boolean substringMatch(CharSequence str, int index,
CharSequence substring) {
for (int j = 0; j < substring.length(); j++) {
int i = index + j;
if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {
return false;
}
}
return true;
} }
} }

View File

@ -16,6 +16,8 @@
package org.springframework.boot.loader; package org.springframework.boot.loader;
import java.io.File;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
@ -40,7 +42,8 @@ public class PropertiesLauncherTests {
@Test @Test
public void testDefaultHome() { public void testDefaultHome() {
assertEquals(System.getProperty("user.dir"), this.launcher.getHomeDirectory()); assertEquals(new File(System.getProperty("user.dir")),
this.launcher.getHomeDirectory());
} }
@Test @Test