Commit e4c807b8 authored by Dave Syer's avatar Dave Syer Committed by Andy Wilkinson

Tighten up PropertiesLauncher's contract

The main changes are:

- Switch to `loader.properties` instead of `application.properties`
- Search for `loader.properties` in `loader.home` as well as in
  the classpath
- Placeholder replacements in MANIFEST.MF (using `loader.properties`
  or system/env vars)

See gh-7221
Closes gh-8346
parent 5278baca
...@@ -147,7 +147,7 @@ files in directories (as opposed to explicitly on the classpath). In the case of ...@@ -147,7 +147,7 @@ files in directories (as opposed to explicitly on the classpath). In the case of
you just add extra jars in those locations if you want more. The `PropertiesLauncher` you just add extra jars in those locations if you want more. The `PropertiesLauncher`
looks in `BOOT-INF/lib/` in your application archive by default, but you can add looks in `BOOT-INF/lib/` in your application archive by default, but you can add
additional locations by setting an environment variable `LOADER_PATH` or `loader.path` additional locations by setting an environment variable `LOADER_PATH` or `loader.path`
in `application.properties` (comma-separated list of directories or archives). in `loader.properties` (comma-separated list of directories or archives).
...@@ -198,7 +198,13 @@ the appropriate launcher: ...@@ -198,7 +198,13 @@ the appropriate launcher:
`PropertiesLauncher` has a few special features that can be enabled with external `PropertiesLauncher` has a few special features that can be enabled with external
properties (System properties, environment variables, manifest entries or properties (System properties, environment variables, manifest entries or
`application.properties`). `loader.properties`).
NOTE: `PropertiesLauncher` supports loading properties from
`loader.properties` and also (for historic reasons)
`application.properties`. We recommend using
`loader.properties` exclusively, as support for
`application.properties` is deprecated and may be removed in the future.
|=== |===
|Key |Purpose |Key |Purpose
...@@ -208,8 +214,7 @@ properties (System properties, environment variables, manifest entries or ...@@ -208,8 +214,7 @@ properties (System properties, environment variables, manifest entries or
just like a regular `-classpath` on the `javac` command line. just like a regular `-classpath` on the `javac` command line.
|`loader.home` |`loader.home`
|Location of additional properties file, e.g. `file:///opt/app` |Used to resolve relative paths in `loader.path`. E.g. `loader.path=lib` then `${loader.home}/lib` is a classpath location (along with all jar files in that directory). Also used to locate a `loader.properties file`. Example `file:///opt/app` (defaults to `${user.dir}`).
(defaults to `${user.dir}`)
|`loader.args` |`loader.args`
|Default arguments for the main method (space separated) |Default arguments for the main method (space separated)
...@@ -218,11 +223,11 @@ properties (System properties, environment variables, manifest entries or ...@@ -218,11 +223,11 @@ properties (System properties, environment variables, manifest entries or
|Name of main class to launch, e.g. `com.app.Application`. |Name of main class to launch, e.g. `com.app.Application`.
|`loader.config.name` |`loader.config.name`
|Name of properties file, e.g. `loader` (defaults to `application`). |Name of properties file, e.g. `launcher` (defaults to `loader`).
|`loader.config.location` |`loader.config.location`
|Path to properties file, e.g. `classpath:loader.properties` (defaults to |Path to properties file, e.g. `classpath:loader.properties` (defaults to
`application.properties`). `loader.properties`).
|`loader.system` |`loader.system`
|Boolean flag to indicate that all properties should be added to System properties |Boolean flag to indicate that all properties should be added to System properties
...@@ -241,7 +246,7 @@ be used: ...@@ -241,7 +246,7 @@ be used:
|`LOADER_PATH` |`LOADER_PATH`
|`loader.home` |`loader.home`
| |`Loader-Home`
|`LOADER_HOME` |`LOADER_HOME`
|`loader.args` |`loader.args`
...@@ -253,11 +258,11 @@ be used: ...@@ -253,11 +258,11 @@ be used:
|`LOADER_MAIN` |`LOADER_MAIN`
|`loader.config.location` |`loader.config.location`
| |`Loader-Config-Location`
|`LOADER_CONFIG_LOCATION` |`LOADER_CONFIG_LOCATION`
|`loader.system` |`loader.system`
| |`Loader-System`
|`LOADER_SYSTEM` |`LOADER_SYSTEM`
|=== |===
...@@ -266,15 +271,21 @@ TIP: Build plugins automatically move the `Main-Class` attribute to `Start-Class ...@@ -266,15 +271,21 @@ TIP: Build plugins automatically move the `Main-Class` attribute to `Start-Class
the fat jar is built. If you are using that, specify the name of the class to launch using the fat jar is built. If you are using that, specify the name of the class to launch using
the `Main-Class` attribute and leave out `Start-Class`. the `Main-Class` attribute and leave out `Start-Class`.
* `loader.home` is the directory location of an additional properties file (overriding * `loader.properties` are searched for in `loader.home` then in the root of the classpath,
then in `classpath:/BOOT-INF/classes`. The first location that exists is used.
* `loader.home` is only the directory location of an additional properties file (overriding
the default) as long as `loader.config.location` is not specified. the default) as long as `loader.config.location` is not specified.
* `loader.path` can contain directories (scanned recursively for jar and zip files), * `loader.path` can contain directories (scanned recursively for jar and zip files),
archive paths, or wildcard patterns (for the default JVM behavior). archive paths, or wildcard patterns (for the default JVM behavior).
* `loader.path` (if empty) defaults to `BOOT-INF/lib` (meaning a local directory or a * `loader.path` (if empty) defaults to `BOOT-INF/lib` (meaning a local directory or a
nested one if running from an archive). Because of this `PropertiesLauncher` behaves the nested one if running from an archive). Because of this `PropertiesLauncher` behaves the
same as `JarLauncher` when no additional configuration is provided. same as `JarLauncher` when no additional configuration is provided.
* `loader.path` can not be used to configure the location of `loader.properties` (the classpath
used to search for the latter is the JVM classpath when `PropertiesLauncher` is launched).
* Placeholder replacement is done from System and environment variables plus the * Placeholder replacement is done from System and environment variables plus the
properties file itself on all values before use. properties file itself on all values before use.
* The search order for properties (where it makes sense to look in more than one place)
is env vars, system properties, `loader.properties`, exploded archive manifest, archive manifest.
......
...@@ -142,48 +142,64 @@ public class PropertiesLauncher extends Launcher { ...@@ -142,48 +142,64 @@ public class PropertiesLauncher extends Launcher {
} }
protected File getHomeDirectory() { protected File getHomeDirectory() {
return new File(SystemPropertyUtils try {
.resolvePlaceholders(System.getProperty(HOME, "${user.dir}"))); return new File(getPropertyWithDefault(HOME, "${user.dir}"));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
} }
private void initializeProperties() throws Exception, IOException { private void initializeProperties() throws Exception, IOException {
String config = "classpath:BOOT-INF/classes/" List<String> configs = new ArrayList<String>();
+ SystemPropertyUtils.resolvePlaceholders( if (getProperty(CONFIG_LOCATION) != null) {
SystemPropertyUtils.getProperty(CONFIG_NAME, "application")) configs.add(getProperty(CONFIG_LOCATION));
+ ".properties"; }
config = SystemPropertyUtils.resolvePlaceholders( else {
SystemPropertyUtils.getProperty(CONFIG_LOCATION, config)); String[] names = getPropertyWithDefault(CONFIG_NAME, "loader,application")
InputStream resource = getResource(config); .split(",");
if (resource != null) { for (String name : names) {
log("Found: " + config); configs.add("file:" + getHomeDirectory() + "/" + name + ".properties");
try { configs.add("classpath:" + name + ".properties");
this.properties.load(resource); configs.add("classpath:BOOT-INF/classes/" + name + ".properties");
}
finally {
resource.close();
} }
for (Object key : Collections.list(this.properties.propertyNames())) { }
String text = this.properties.getProperty((String) key); for (String config : configs) {
String value = SystemPropertyUtils.resolvePlaceholders(this.properties, InputStream resource = getResource(config);
text); if (resource != null) {
if (value != null) { log("Found: " + config);
this.properties.put(key, value); try {
this.properties.load(resource);
}
finally {
resource.close();
} }
}
if (SystemPropertyUtils
.resolvePlaceholders("${" + SET_SYSTEM_PROPERTIES + ":false}")
.equals("true")) {
log("Adding resolved properties to System properties");
for (Object key : Collections.list(this.properties.propertyNames())) { for (Object key : Collections.list(this.properties.propertyNames())) {
String value = this.properties.getProperty((String) key); if (config.endsWith("application.properties")
System.setProperty((String) key, value); && ((String) key).startsWith("loader.")) {
warn("WARNING: use of application.properties for PropertiesLauncher is deprecated");
}
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))) {
log("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);
}
} }
// Load the first one we find
return;
}
else {
log("Not found: " + config);
} }
} }
else {
log("Not found: " + config);
}
} }
private InputStream getResource(String config) throws Exception { private InputStream getResource(String config) throws Exception {
...@@ -354,34 +370,50 @@ public class PropertiesLauncher extends Launcher { ...@@ -354,34 +370,50 @@ public class PropertiesLauncher extends Launcher {
} }
private String getProperty(String propertyKey) throws Exception { private String getProperty(String propertyKey) throws Exception {
return getProperty(propertyKey, null); return getProperty(propertyKey, null, null);
} }
private String getProperty(String propertyKey, String manifestKey) throws Exception { 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) { if (manifestKey == null) {
manifestKey = propertyKey.replace('.', '-'); manifestKey = propertyKey.replace('.', '-');
manifestKey = toCamelCase(manifestKey); manifestKey = toCamelCase(manifestKey);
} }
String property = SystemPropertyUtils.getProperty(propertyKey); String property = SystemPropertyUtils.getProperty(propertyKey);
if (property != null) { if (property != null) {
String value = SystemPropertyUtils.resolvePlaceholders(property); String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
property);
log("Property '" + propertyKey + "' from environment: " + value); log("Property '" + propertyKey + "' from environment: " + value);
return value; return value;
} }
if (this.properties.containsKey(propertyKey)) { if (this.properties.containsKey(propertyKey)) {
String value = SystemPropertyUtils String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
.resolvePlaceholders(this.properties.getProperty(propertyKey)); this.properties.getProperty(propertyKey));
log("Property '" + propertyKey + "' from properties: " + value); log("Property '" + propertyKey + "' from properties: " + value);
return value; return value;
} }
try { try {
// Prefer home dir for MANIFEST if there is one if (this.home != null) {
Manifest manifest = new ExplodedArchive(this.home, false).getManifest(); // Prefer home dir for MANIFEST if there is one
if (manifest != null) { Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
String value = manifest.getMainAttributes().getValue(manifestKey); if (manifest != null) {
log("Property '" + manifestKey + "' from home directory manifest: " String value = manifest.getMainAttributes().getValue(manifestKey);
+ value); if (value != null) {
return value; log("Property '" + manifestKey
+ "' from home directory manifest: " + value);
return SystemPropertyUtils.resolvePlaceholders(this.properties,
value);
}
}
} }
} }
catch (IllegalStateException ex) { catch (IllegalStateException ex) {
...@@ -393,10 +425,11 @@ public class PropertiesLauncher extends Launcher { ...@@ -393,10 +425,11 @@ public class PropertiesLauncher extends Launcher {
String value = manifest.getMainAttributes().getValue(manifestKey); String value = manifest.getMainAttributes().getValue(manifestKey);
if (value != null) { if (value != null) {
log("Property '" + manifestKey + "' from archive manifest: " + value); log("Property '" + manifestKey + "' from archive manifest: " + value);
return value; return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
} }
} }
return null; return defaultValue == null ? defaultValue
: SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue);
} }
@Override @Override
...@@ -436,10 +469,10 @@ public class PropertiesLauncher extends Launcher { ...@@ -436,10 +469,10 @@ public class PropertiesLauncher extends Launcher {
log("Adding classpath entries from archive " + archive.getUrl() + root); log("Adding classpath entries from archive " + archive.getUrl() + root);
lib.add(archive); lib.add(archive);
} }
Archive nested = getNestedArchive(root); List<Archive> nested = getNestedArchive(root);
if (nested != null) { if (nested != null) {
log("Adding classpath entries from nested " + nested.getUrl() + root); log("Adding classpath entries from nested " + root);
lib.add(nested); lib.addAll(nested);
} }
return lib; return lib;
} }
...@@ -457,19 +490,21 @@ public class PropertiesLauncher extends Launcher { ...@@ -457,19 +490,21 @@ public class PropertiesLauncher extends Launcher {
return null; return null;
} }
private Archive getNestedArchive(String root) throws Exception { private List<Archive> getNestedArchive(String root) throws Exception {
List<Archive> list = new ArrayList<Archive>();
if (root.startsWith("/") if (root.startsWith("/")
|| this.parent.getUrl().equals(this.home.toURI().toURL())) { || this.parent.getUrl().equals(this.home.toURI().toURL())) {
// If home dir is same as parent archive, no need to add it twice. // If home dir is same as parent archive, no need to add it twice.
return null; return list;
} }
EntryFilter filter = new PrefixMatchingArchiveFilter(root); EntryFilter filter = new PrefixMatchingArchiveFilter(root);
if (this.parent.getNestedArchives(filter).isEmpty()) { if (this.parent.getNestedArchives(filter).isEmpty()) {
return null; return list;
} }
// If there are more archives nested in this subdirectory (root) then create a new // If there are more archives nested in this subdirectory (root) then create a new
// virtual archive for them, and have it added to the classpath // virtual archive for them, and have it added to the classpath
return new FilteredArchive(this.parent, filter); list.add(new FilteredArchive(this.parent, filter));
return list;
} }
private void addNestedEntries(List<Archive> lib) { private void addNestedEntries(List<Archive> lib) {
...@@ -548,6 +583,11 @@ public class PropertiesLauncher extends Launcher { ...@@ -548,6 +583,11 @@ public class PropertiesLauncher extends Launcher {
} }
} }
private void warn(String message) {
// We shouldn't use java.util.logging because of classpath issues
System.out.println(message);
}
/** /**
* Convenience class for finding nested archives that have a prefix in their file path * Convenience class for finding nested archives that have a prefix in their file path
* (e.g. "lib/"). * (e.g. "lib/").
......
...@@ -31,6 +31,7 @@ import org.junit.After; ...@@ -31,6 +31,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
...@@ -50,6 +51,9 @@ public class PropertiesLauncherTests { ...@@ -50,6 +51,9 @@ public class PropertiesLauncherTests {
@Rule @Rule
public InternalOutputCapture output = new InternalOutputCapture(); public InternalOutputCapture output = new InternalOutputCapture();
@Rule
public ExpectedException expected = ExpectedException.none();
@Rule @Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder(); public TemporaryFolder temporaryFolder = new TemporaryFolder();
...@@ -72,9 +76,28 @@ public class PropertiesLauncherTests { ...@@ -72,9 +76,28 @@ public class PropertiesLauncherTests {
@Test @Test
public void testDefaultHome() { public void testDefaultHome() {
System.clearProperty("loader.home");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getHomeDirectory())
.isEqualTo(new File(System.getProperty("user.dir")));
}
@Test
public void testAlternateHome() throws Exception {
System.setProperty("loader.home", "src/test/resources/home");
PropertiesLauncher launcher = new PropertiesLauncher(); PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getHomeDirectory()) assertThat(launcher.getHomeDirectory())
.isEqualTo(new File(System.getProperty("loader.home"))); .isEqualTo(new File(System.getProperty("loader.home")));
assertThat(launcher.getMainClass()).isEqualTo("demo.HomeApplication");
}
@Test
public void testNonExistentHome() throws Exception {
System.setProperty("loader.home", "src/test/resources/nonexistent");
this.expected.expectMessage("Invalid source folder");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getHomeDirectory())
.isNotEqualTo(new File(System.getProperty("loader.home")));
} }
@Test @Test
...@@ -93,6 +116,13 @@ public class PropertiesLauncherTests { ...@@ -93,6 +116,13 @@ public class PropertiesLauncherTests {
.isEqualTo("[etc/]"); .isEqualTo("[etc/]");
} }
@Test
public void testRootOfClasspathFirst() throws Exception {
System.setProperty("loader.config.name", "bar");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getMainClass()).isEqualTo("my.BarApplication");
}
@Test @Test
public void testUserSpecifiedDotPath() throws Exception { public void testUserSpecifiedDotPath() throws Exception {
System.setProperty("loader.path", "."); System.setProperty("loader.path", ".");
...@@ -232,6 +262,13 @@ public class PropertiesLauncherTests { ...@@ -232,6 +262,13 @@ public class PropertiesLauncherTests {
.containsExactly("/foo.jar", "/bar/"); .containsExactly("/foo.jar", "/bar/");
} }
@Test
public void testManifestWithPlaceholders() throws Exception {
System.setProperty("loader.home", "src/test/resources/placeholders");
PropertiesLauncher launcher = new PropertiesLauncher();
assertThat(launcher.getMainClass()).isEqualTo("demo.FooApplication");
}
private void waitFor(String value) throws Exception { private void waitFor(String value) throws Exception {
int count = 0; int count = 0;
boolean timeout = false; boolean timeout = false;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment