001/* 002 * Copyright 2012-2017 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.loader; 018 019import java.io.File; 020import java.io.FileInputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.net.HttpURLConnection; 024import java.net.URL; 025import java.net.URLConnection; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.LinkedHashSet; 029import java.util.List; 030import java.util.Properties; 031import java.util.Set; 032import java.util.jar.Manifest; 033import java.util.regex.Matcher; 034import java.util.regex.Pattern; 035 036import org.springframework.boot.loader.archive.Archive; 037import org.springframework.boot.loader.archive.Archive.Entry; 038import org.springframework.boot.loader.archive.Archive.EntryFilter; 039import org.springframework.boot.loader.archive.ExplodedArchive; 040import org.springframework.boot.loader.archive.JarFileArchive; 041import org.springframework.boot.loader.util.SystemPropertyUtils; 042 043/** 044 * {@link Launcher} for archives with user-configured classpath and main class via a 045 * properties file. This model is often more flexible and more amenable to creating 046 * well-behaved OS-level services than a model based on executable jars. 047 * <p> 048 * Looks in various places for a properties file to extract loader settings, defaulting to 049 * {@code application.properties} either on the current classpath or in the current 050 * working directory. The name of the properties file can be changed by setting a System 051 * property {@code loader.config.name} (e.g. {@code -Dloader.config.name=foo} will look 052 * for {@code foo.properties}. If that file doesn't exist then tries 053 * {@code loader.config.location} (with allowed prefixes {@code classpath:} and 054 * {@code file:} or any valid URL). Once that file is located turns it into Properties and 055 * extracts optional values (which can also be provided overridden as System properties in 056 * case the file doesn't exist): 057 * <ul> 058 * <li>{@code loader.path}: a comma-separated list of directories (containing file 059 * resources and/or nested archives in *.jar or *.zip or archives) or archives to append 060 * to the classpath. {@code BOOT-INF/classes,BOOT-INF/lib} in the application archive are 061 * always used</li> 062 * <li>{@code loader.main}: the main method to delegate execution to once the class loader 063 * is set up. No default, but will fall back to looking for a {@code Start-Class} in a 064 * {@code MANIFEST.MF}, if there is one in <code>${loader.home}/META-INF</code>.</li> 065 * </ul> 066 * 067 * @author Dave Syer 068 * @author Janne Valkealahti 069 * @author Andy Wilkinson 070 */ 071public class PropertiesLauncher extends Launcher { 072 073 private static final String DEBUG = "loader.debug"; 074 075 /** 076 * Properties key for main class. As a manifest entry can also be specified as 077 * {@code Start-Class}. 078 */ 079 public static final String MAIN = "loader.main"; 080 081 /** 082 * Properties key for classpath entries (directories possibly containing jars or 083 * jars). Multiple entries can be specified using a comma-separated list. {@code 084 * BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used. 085 */ 086 public static final String PATH = "loader.path"; 087 088 /** 089 * Properties key for home directory. This is the location of external configuration 090 * if not on classpath, and also the base path for any relative paths in the 091 * {@link #PATH loader path}. Defaults to current working directory ( 092 * <code>${user.dir}</code>). 093 */ 094 public static final String HOME = "loader.home"; 095 096 /** 097 * Properties key for default command line arguments. These arguments (if present) are 098 * prepended to the main method arguments before launching. 099 */ 100 public static final String ARGS = "loader.args"; 101 102 /** 103 * Properties key for name of external configuration file (excluding suffix). Defaults 104 * to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is 105 * provided instead. 106 */ 107 public static final String CONFIG_NAME = "loader.config.name"; 108 109 /** 110 * Properties key for config file location (including optional classpath:, file: or 111 * URL prefix). 112 */ 113 public static final String CONFIG_LOCATION = "loader.config.location"; 114 115 /** 116 * Properties key for boolean flag (default false) which if set will cause the 117 * external configuration properties to be copied to System properties (assuming that 118 * is allowed by Java security). 119 */ 120 public static final String SET_SYSTEM_PROPERTIES = "loader.system"; 121 122 private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+"); 123 124 private final File home; 125 126 private List<String> paths = new ArrayList<String>(); 127 128 private final Properties properties = new Properties(); 129 130 private Archive parent; 131 132 public PropertiesLauncher() { 133 try { 134 this.home = getHomeDirectory(); 135 initializeProperties(); 136 initializePaths(); 137 this.parent = createArchive(); 138 } 139 catch (Exception ex) { 140 throw new IllegalStateException(ex); 141 } 142 } 143 144 protected File getHomeDirectory() { 145 try { 146 return new File(getPropertyWithDefault(HOME, "${user.dir}")); 147 } 148 catch (Exception ex) { 149 throw new IllegalStateException(ex); 150 } 151 } 152 153 private void initializeProperties() throws Exception, IOException { 154 List<String> configs = new ArrayList<String>(); 155 if (getProperty(CONFIG_LOCATION) != null) { 156 configs.add(getProperty(CONFIG_LOCATION)); 157 } 158 else { 159 String[] names = getPropertyWithDefault(CONFIG_NAME, "loader,application") 160 .split(","); 161 for (String name : names) { 162 configs.add("file:" + getHomeDirectory() + "/" + name + ".properties"); 163 configs.add("classpath:" + name + ".properties"); 164 configs.add("classpath:BOOT-INF/classes/" + name + ".properties"); 165 } 166 } 167 for (String config : configs) { 168 InputStream resource = getResource(config); 169 if (resource != null) { 170 debug("Found: " + config); 171 try { 172 this.properties.load(resource); 173 } 174 finally { 175 resource.close(); 176 } 177 for (Object key : Collections.list(this.properties.propertyNames())) { 178 if (config.endsWith("application.properties") 179 && ((String) key).startsWith("loader.")) { 180 warn("Use of application.properties for PropertiesLauncher is deprecated"); 181 } 182 String text = this.properties.getProperty((String) key); 183 String value = SystemPropertyUtils 184 .resolvePlaceholders(this.properties, text); 185 if (value != null) { 186 this.properties.put(key, value); 187 } 188 } 189 if ("true".equals(getProperty(SET_SYSTEM_PROPERTIES))) { 190 debug("Adding resolved properties to System properties"); 191 for (Object key : Collections.list(this.properties.propertyNames())) { 192 String value = this.properties.getProperty((String) key); 193 System.setProperty((String) key, value); 194 } 195 } 196 // Load the first one we find 197 return; 198 } 199 else { 200 debug("Not found: " + config); 201 } 202 } 203 } 204 205 private InputStream getResource(String config) throws Exception { 206 if (config.startsWith("classpath:")) { 207 return getClasspathResource(config.substring("classpath:".length())); 208 } 209 config = stripFileUrlPrefix(config); 210 if (isUrl(config)) { 211 return getURLResource(config); 212 } 213 return getFileResource(config); 214 } 215 216 private String stripFileUrlPrefix(String config) { 217 if (config.startsWith("file:")) { 218 config = config.substring("file:".length()); 219 if (config.startsWith("//")) { 220 config = config.substring(2); 221 } 222 } 223 return config; 224 } 225 226 private boolean isUrl(String config) { 227 return config.contains("://"); 228 } 229 230 private InputStream getClasspathResource(String config) { 231 while (config.startsWith("/")) { 232 config = config.substring(1); 233 } 234 config = "/" + config; 235 debug("Trying classpath: " + config); 236 return getClass().getResourceAsStream(config); 237 } 238 239 private InputStream getFileResource(String config) throws Exception { 240 File file = new File(config); 241 debug("Trying file: " + config); 242 if (file.canRead()) { 243 return new FileInputStream(file); 244 } 245 return null; 246 } 247 248 private InputStream getURLResource(String config) throws Exception { 249 URL url = new URL(config); 250 if (exists(url)) { 251 URLConnection con = url.openConnection(); 252 try { 253 return con.getInputStream(); 254 } 255 catch (IOException ex) { 256 // Close the HTTP connection (if applicable). 257 if (con instanceof HttpURLConnection) { 258 ((HttpURLConnection) con).disconnect(); 259 } 260 throw ex; 261 } 262 } 263 return null; 264 } 265 266 private boolean exists(URL url) throws IOException { 267 // Try a URL connection content-length header... 268 URLConnection connection = url.openConnection(); 269 try { 270 connection.setUseCaches( 271 connection.getClass().getSimpleName().startsWith("JNLP")); 272 if (connection instanceof HttpURLConnection) { 273 HttpURLConnection httpConnection = (HttpURLConnection) connection; 274 httpConnection.setRequestMethod("HEAD"); 275 int responseCode = httpConnection.getResponseCode(); 276 if (responseCode == HttpURLConnection.HTTP_OK) { 277 return true; 278 } 279 else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { 280 return false; 281 } 282 } 283 return (connection.getContentLength() >= 0); 284 } 285 finally { 286 if (connection instanceof HttpURLConnection) { 287 ((HttpURLConnection) connection).disconnect(); 288 } 289 } 290 } 291 292 private void initializePaths() throws Exception { 293 String path = getProperty(PATH); 294 if (path != null) { 295 this.paths = parsePathsProperty(path); 296 } 297 debug("Nested archive paths: " + this.paths); 298 } 299 300 private List<String> parsePathsProperty(String commaSeparatedPaths) { 301 List<String> paths = new ArrayList<String>(); 302 for (String path : commaSeparatedPaths.split(",")) { 303 path = cleanupPath(path); 304 // "" means the user wants root of archive but not current directory 305 path = ("".equals(path) ? "/" : path); 306 paths.add(path); 307 } 308 if (paths.isEmpty()) { 309 paths.add("lib"); 310 } 311 return paths; 312 } 313 314 protected String[] getArgs(String... args) throws Exception { 315 String loaderArgs = getProperty(ARGS); 316 if (loaderArgs != null) { 317 String[] defaultArgs = loaderArgs.split("\\s+"); 318 String[] additionalArgs = args; 319 args = new String[defaultArgs.length + additionalArgs.length]; 320 System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length); 321 System.arraycopy(additionalArgs, 0, args, defaultArgs.length, 322 additionalArgs.length); 323 } 324 return args; 325 } 326 327 @Override 328 protected String getMainClass() throws Exception { 329 String mainClass = getProperty(MAIN, "Start-Class"); 330 if (mainClass == null) { 331 throw new IllegalStateException( 332 "No '" + MAIN + "' or 'Start-Class' specified"); 333 } 334 return mainClass; 335 } 336 337 @Override 338 protected ClassLoader createClassLoader(List<Archive> archives) throws Exception { 339 Set<URL> urls = new LinkedHashSet<URL>(archives.size()); 340 for (Archive archive : archives) { 341 urls.add(archive.getUrl()); 342 } 343 ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(new URL[0]), 344 getClass().getClassLoader()); 345 debug("Classpath: " + urls); 346 String customLoaderClassName = getProperty("loader.classLoader"); 347 if (customLoaderClassName != null) { 348 loader = wrapWithCustomClassLoader(loader, customLoaderClassName); 349 debug("Using custom class loader: " + customLoaderClassName); 350 } 351 return loader; 352 } 353 354 @SuppressWarnings("unchecked") 355 private ClassLoader wrapWithCustomClassLoader(ClassLoader parent, 356 String loaderClassName) throws Exception { 357 Class<ClassLoader> loaderClass = (Class<ClassLoader>) Class 358 .forName(loaderClassName, true, parent); 359 360 try { 361 return loaderClass.getConstructor(ClassLoader.class).newInstance(parent); 362 } 363 catch (NoSuchMethodException ex) { 364 // Ignore and try with URLs 365 } 366 try { 367 return loaderClass.getConstructor(URL[].class, ClassLoader.class) 368 .newInstance(new URL[0], parent); 369 } 370 catch (NoSuchMethodException ex) { 371 // Ignore and try without any arguments 372 } 373 return loaderClass.newInstance(); 374 } 375 376 private String getProperty(String propertyKey) throws Exception { 377 return getProperty(propertyKey, null, null); 378 } 379 380 private String getProperty(String propertyKey, String manifestKey) throws Exception { 381 return getProperty(propertyKey, manifestKey, null); 382 } 383 384 private String getPropertyWithDefault(String propertyKey, String defaultValue) 385 throws Exception { 386 return getProperty(propertyKey, null, defaultValue); 387 } 388 389 private String getProperty(String propertyKey, String manifestKey, 390 String defaultValue) throws Exception { 391 if (manifestKey == null) { 392 manifestKey = propertyKey.replace('.', '-'); 393 manifestKey = toCamelCase(manifestKey); 394 } 395 String property = SystemPropertyUtils.getProperty(propertyKey); 396 if (property != null) { 397 String value = SystemPropertyUtils.resolvePlaceholders(this.properties, 398 property); 399 debug("Property '" + propertyKey + "' from environment: " + value); 400 return value; 401 } 402 if (this.properties.containsKey(propertyKey)) { 403 String value = SystemPropertyUtils.resolvePlaceholders(this.properties, 404 this.properties.getProperty(propertyKey)); 405 debug("Property '" + propertyKey + "' from properties: " + value); 406 return value; 407 } 408 try { 409 if (this.home != null) { 410 // Prefer home dir for MANIFEST if there is one 411 Manifest manifest = new ExplodedArchive(this.home, false).getManifest(); 412 if (manifest != null) { 413 String value = manifest.getMainAttributes().getValue(manifestKey); 414 if (value != null) { 415 debug("Property '" + manifestKey 416 + "' from home directory manifest: " + value); 417 return SystemPropertyUtils.resolvePlaceholders(this.properties, 418 value); 419 } 420 } 421 } 422 } 423 catch (IllegalStateException ex) { 424 // Ignore 425 } 426 // Otherwise try the parent archive 427 Manifest manifest = createArchive().getManifest(); 428 if (manifest != null) { 429 String value = manifest.getMainAttributes().getValue(manifestKey); 430 if (value != null) { 431 debug("Property '" + manifestKey + "' from archive manifest: " + value); 432 return SystemPropertyUtils.resolvePlaceholders(this.properties, value); 433 } 434 } 435 return defaultValue == null ? defaultValue 436 : SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue); 437 } 438 439 @Override 440 protected List<Archive> getClassPathArchives() throws Exception { 441 List<Archive> lib = new ArrayList<Archive>(); 442 for (String path : this.paths) { 443 for (Archive archive : getClassPathArchives(path)) { 444 if (archive instanceof ExplodedArchive) { 445 List<Archive> nested = new ArrayList<Archive>( 446 archive.getNestedArchives(new ArchiveEntryFilter())); 447 nested.add(0, archive); 448 lib.addAll(nested); 449 } 450 else { 451 lib.add(archive); 452 } 453 } 454 } 455 addNestedEntries(lib); 456 return lib; 457 } 458 459 private List<Archive> getClassPathArchives(String path) throws Exception { 460 String root = cleanupPath(stripFileUrlPrefix(path)); 461 List<Archive> lib = new ArrayList<Archive>(); 462 File file = new File(root); 463 if (!"/".equals(root)) { 464 if (!isAbsolutePath(root)) { 465 file = new File(this.home, root); 466 } 467 if (file.isDirectory()) { 468 debug("Adding classpath entries from " + file); 469 Archive archive = new ExplodedArchive(file, false); 470 lib.add(archive); 471 } 472 } 473 Archive archive = getArchive(file); 474 if (archive != null) { 475 debug("Adding classpath entries from archive " + archive.getUrl() + root); 476 lib.add(archive); 477 } 478 List<Archive> nestedArchives = getNestedArchives(root); 479 if (nestedArchives != null) { 480 debug("Adding classpath entries from nested " + root); 481 lib.addAll(nestedArchives); 482 } 483 return lib; 484 } 485 486 private boolean isAbsolutePath(String root) { 487 // Windows contains ":" others start with "/" 488 return root.contains(":") || root.startsWith("/"); 489 } 490 491 private Archive getArchive(File file) throws IOException { 492 String name = file.getName().toLowerCase(); 493 if (name.endsWith(".jar") || name.endsWith(".zip")) { 494 return new JarFileArchive(file); 495 } 496 return null; 497 } 498 499 private List<Archive> getNestedArchives(String path) throws Exception { 500 Archive parent = this.parent; 501 String root = path; 502 if (!root.equals("/") && root.startsWith("/") 503 || parent.getUrl().equals(this.home.toURI().toURL())) { 504 // If home dir is same as parent archive, no need to add it twice. 505 return null; 506 } 507 if (root.contains("!")) { 508 int index = root.indexOf("!"); 509 File file = new File(this.home, root.substring(0, index)); 510 if (root.startsWith("jar:file:")) { 511 file = new File(root.substring("jar:file:".length(), index)); 512 } 513 parent = new JarFileArchive(file); 514 root = root.substring(index + 1, root.length()); 515 while (root.startsWith("/")) { 516 root = root.substring(1); 517 } 518 } 519 if (root.endsWith(".jar")) { 520 File file = new File(this.home, root); 521 if (file.exists()) { 522 parent = new JarFileArchive(file); 523 root = ""; 524 } 525 } 526 if (root.equals("/") || root.equals("./") || root.equals(".")) { 527 // The prefix for nested jars is actually empty if it's at the root 528 root = ""; 529 } 530 EntryFilter filter = new PrefixMatchingArchiveFilter(root); 531 List<Archive> archives = new ArrayList<Archive>(parent.getNestedArchives(filter)); 532 if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar") 533 && parent != this.parent) { 534 // You can't find the root with an entry filter so it has to be added 535 // explicitly. But don't add the root of the parent archive. 536 archives.add(parent); 537 } 538 return archives; 539 } 540 541 private void addNestedEntries(List<Archive> lib) { 542 // The parent archive might have "BOOT-INF/lib/" and "BOOT-INF/classes/" 543 // directories, meaning we are running from an executable JAR. We add nested 544 // entries from there with low priority (i.e. at end). 545 try { 546 lib.addAll(this.parent.getNestedArchives(new EntryFilter() { 547 548 @Override 549 public boolean matches(Entry entry) { 550 if (entry.isDirectory()) { 551 return entry.getName().equals(JarLauncher.BOOT_INF_CLASSES); 552 } 553 return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB); 554 } 555 556 })); 557 } 558 catch (IOException ex) { 559 // Ignore 560 } 561 } 562 563 private String cleanupPath(String path) { 564 path = path.trim(); 565 // No need for current dir path 566 if (path.startsWith("./")) { 567 path = path.substring(2); 568 } 569 if (path.toLowerCase().endsWith(".jar") || path.toLowerCase().endsWith(".zip")) { 570 return path; 571 } 572 if (path.endsWith("/*")) { 573 path = path.substring(0, path.length() - 1); 574 } 575 else { 576 // It's a directory 577 if (!path.endsWith("/") && !path.equals(".")) { 578 path = path + "/"; 579 } 580 } 581 return path; 582 } 583 584 public static void main(String[] args) throws Exception { 585 PropertiesLauncher launcher = new PropertiesLauncher(); 586 args = launcher.getArgs(args); 587 launcher.launch(args); 588 } 589 590 public static String toCamelCase(CharSequence string) { 591 if (string == null) { 592 return null; 593 } 594 StringBuilder builder = new StringBuilder(); 595 Matcher matcher = WORD_SEPARATOR.matcher(string); 596 int pos = 0; 597 while (matcher.find()) { 598 builder.append(capitalize(string.subSequence(pos, matcher.end()).toString())); 599 pos = matcher.end(); 600 } 601 builder.append(capitalize(string.subSequence(pos, string.length()).toString())); 602 return builder.toString(); 603 } 604 605 private static String capitalize(String str) { 606 return Character.toUpperCase(str.charAt(0)) + str.substring(1); 607 } 608 609 private void debug(String message) { 610 if (Boolean.getBoolean(DEBUG)) { 611 log(message); 612 } 613 } 614 615 private void warn(String message) { 616 log("WARNING: " + message); 617 } 618 619 private void log(String message) { 620 // We shouldn't use java.util.logging because of classpath issues 621 System.out.println(message); 622 } 623 624 /** 625 * Convenience class for finding nested archives that have a prefix in their file path 626 * (e.g. "lib/"). 627 */ 628 private static final class PrefixMatchingArchiveFilter implements EntryFilter { 629 630 private final String prefix; 631 632 private final ArchiveEntryFilter filter = new ArchiveEntryFilter(); 633 634 private PrefixMatchingArchiveFilter(String prefix) { 635 this.prefix = prefix; 636 } 637 638 @Override 639 public boolean matches(Entry entry) { 640 if (entry.isDirectory()) { 641 return entry.getName().equals(this.prefix); 642 } 643 return entry.getName().startsWith(this.prefix) && this.filter.matches(entry); 644 } 645 646 } 647 648 /** 649 * Convenience class for finding nested archives (archive entries that can be 650 * classpath entries). 651 */ 652 private static final class ArchiveEntryFilter implements EntryFilter { 653 654 private static final String DOT_JAR = ".jar"; 655 656 private static final String DOT_ZIP = ".zip"; 657 658 @Override 659 public boolean matches(Entry entry) { 660 return entry.getName().endsWith(DOT_JAR) || entry.getName().endsWith(DOT_ZIP); 661 } 662 663 } 664 665}