001/** 002 * Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com> 003 */ 004package com.typesafe.config.impl; 005 006import java.io.BufferedReader; 007import java.io.File; 008import java.io.FileInputStream; 009import java.io.FileNotFoundException; 010import java.io.FilterReader; 011import java.io.IOException; 012import java.io.InputStream; 013import java.io.InputStreamReader; 014import java.io.Reader; 015import java.io.StringReader; 016import java.io.UnsupportedEncodingException; 017import java.net.MalformedURLException; 018import java.net.URI; 019import java.net.URISyntaxException; 020import java.net.URL; 021import java.net.URLConnection; 022import java.util.*; 023 024import com.typesafe.config.*; 025import com.typesafe.config.parser.*; 026 027/** 028 * Internal implementation detail, not ABI stable, do not touch. 029 * For use only by the {@link com.typesafe.config} package. 030 * The point of this class is to avoid "propagating" each 031 * overload on "thing which can be parsed" through multiple 032 * interfaces. Most interfaces can have just one overload that 033 * takes a Parseable. Also it's used as an abstract "resource 034 * handle" in the ConfigIncluder interface. 035 */ 036public abstract class Parseable implements ConfigParseable { 037 private ConfigIncludeContext includeContext; 038 private ConfigParseOptions initialOptions; 039 private ConfigOrigin initialOrigin; 040 041 /** 042 * Internal implementation detail, not ABI stable, do not touch. 043 */ 044 protected interface Relativizer { 045 ConfigParseable relativeTo(String filename); 046 } 047 048 private static final ThreadLocal<LinkedList<Parseable>> parseStack = new ThreadLocal<LinkedList<Parseable>>() { 049 @Override 050 protected LinkedList<Parseable> initialValue() { 051 return new LinkedList<Parseable>(); 052 } 053 }; 054 055 private static final int MAX_INCLUDE_DEPTH = 50; 056 057 protected Parseable() { 058 } 059 060 private ConfigParseOptions fixupOptions(ConfigParseOptions baseOptions) { 061 ConfigSyntax syntax = baseOptions.getSyntax(); 062 if (syntax == null) { 063 syntax = guessSyntax(); 064 } 065 if (syntax == null) { 066 syntax = ConfigSyntax.CONF; 067 } 068 ConfigParseOptions modified = baseOptions.setSyntax(syntax); 069 070 // make sure the app-provided includer falls back to default 071 modified = modified.appendIncluder(ConfigImpl.defaultIncluder()); 072 // make sure the app-provided includer is complete 073 modified = modified.setIncluder(SimpleIncluder.makeFull(modified.getIncluder())); 074 075 return modified; 076 } 077 078 protected void postConstruct(ConfigParseOptions baseOptions) { 079 this.initialOptions = fixupOptions(baseOptions); 080 081 this.includeContext = new SimpleIncludeContext(this); 082 083 if (initialOptions.getOriginDescription() != null) 084 initialOrigin = SimpleConfigOrigin.newSimple(initialOptions.getOriginDescription()); 085 else 086 initialOrigin = createOrigin(); 087 } 088 089 // the general idea is that any work should be in here, not in the 090 // constructor, so that exceptions are thrown from the public parse() 091 // function and not from the creation of the Parseable. 092 // Essentially this is a lazy field. The parser should close the 093 // reader when it's done with it. 094 // ALSO, IMPORTANT: if the file or URL is not found, this must throw. 095 // to support the "allow missing" feature. 096 protected abstract Reader reader() throws IOException; 097 098 protected Reader reader(ConfigParseOptions options) throws IOException { 099 return reader(); 100 } 101 102 protected static void trace(String message) { 103 if (ConfigImpl.traceLoadsEnabled()) { 104 ConfigImpl.trace(message); 105 } 106 } 107 108 ConfigSyntax guessSyntax() { 109 return null; 110 } 111 112 ConfigSyntax contentType() { 113 return null; 114 } 115 116 ConfigParseable relativeTo(String filename) { 117 // fall back to classpath; we treat the "filename" as absolute 118 // (don't add a package name in front), 119 // if it starts with "/" then remove the "/", for consistency 120 // with ParseableResources.relativeTo 121 String resource = filename; 122 if (filename.startsWith("/")) 123 resource = filename.substring(1); 124 return newResources(resource, options().setOriginDescription(null)); 125 } 126 127 ConfigIncludeContext includeContext() { 128 return includeContext; 129 } 130 131 static AbstractConfigObject forceParsedToObject(ConfigValue value) { 132 if (value instanceof AbstractConfigObject) { 133 return (AbstractConfigObject) value; 134 } else { 135 throw new ConfigException.WrongType(value.origin(), "", "object at file root", value 136 .valueType().name()); 137 } 138 } 139 140 @Override 141 public ConfigObject parse(ConfigParseOptions baseOptions) { 142 143 LinkedList<Parseable> stack = parseStack.get(); 144 if (stack.size() >= MAX_INCLUDE_DEPTH) { 145 throw new ConfigException.Parse(initialOrigin, "include statements nested more than " 146 + MAX_INCLUDE_DEPTH 147 + " times, you probably have a cycle in your includes. Trace: " + stack); 148 } 149 150 stack.addFirst(this); 151 try { 152 return forceParsedToObject(parseValue(baseOptions)); 153 } finally { 154 stack.removeFirst(); 155 if (stack.isEmpty()) { 156 parseStack.remove(); 157 } 158 } 159 } 160 161 final AbstractConfigValue parseValue(ConfigParseOptions baseOptions) { 162 // note that we are NOT using our "initialOptions", 163 // but using the ones from the passed-in options. The idea is that 164 // callers can get our original options and then parse with different 165 // ones if they want. 166 ConfigParseOptions options = fixupOptions(baseOptions); 167 168 // passed-in options can override origin 169 ConfigOrigin origin; 170 if (options.getOriginDescription() != null) 171 origin = SimpleConfigOrigin.newSimple(options.getOriginDescription()); 172 else 173 origin = initialOrigin; 174 return parseValue(origin, options); 175 } 176 177 final private AbstractConfigValue parseValue(ConfigOrigin origin, 178 ConfigParseOptions finalOptions) { 179 try { 180 return rawParseValue(origin, finalOptions); 181 } catch (IOException e) { 182 if (finalOptions.getAllowMissing()) { 183 return SimpleConfigObject.emptyMissing(origin); 184 } else { 185 trace("exception loading " + origin.description() + ": " + e.getClass().getName() 186 + ": " + e.getMessage()); 187 throw new ConfigException.IO(origin, 188 e.getClass().getName() + ": " + e.getMessage(), e); 189 } 190 } 191 } 192 193 final ConfigDocument parseDocument(ConfigParseOptions baseOptions) { 194 // note that we are NOT using our "initialOptions", 195 // but using the ones from the passed-in options. The idea is that 196 // callers can get our original options and then parse with different 197 // ones if they want. 198 ConfigParseOptions options = fixupOptions(baseOptions); 199 200 // passed-in options can override origin 201 ConfigOrigin origin; 202 if (options.getOriginDescription() != null) 203 origin = SimpleConfigOrigin.newSimple(options.getOriginDescription()); 204 else 205 origin = initialOrigin; 206 return parseDocument(origin, options); 207 } 208 209 final private ConfigDocument parseDocument(ConfigOrigin origin, 210 ConfigParseOptions finalOptions) { 211 try { 212 return rawParseDocument(origin, finalOptions); 213 } catch (IOException e) { 214 if (finalOptions.getAllowMissing()) { 215 ArrayList<AbstractConfigNode> children = new ArrayList<AbstractConfigNode>(); 216 children.add(new ConfigNodeObject(new ArrayList<AbstractConfigNode>())); 217 return new SimpleConfigDocument(new ConfigNodeRoot(children, origin), finalOptions); 218 } else { 219 trace("exception loading " + origin.description() + ": " + e.getClass().getName() 220 + ": " + e.getMessage()); 221 throw new ConfigException.IO(origin, 222 e.getClass().getName() + ": " + e.getMessage(), e); 223 } 224 } 225 } 226 227 // this is parseValue without post-processing the IOException or handling 228 // options.getAllowMissing() 229 protected AbstractConfigValue rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) 230 throws IOException { 231 Reader reader = reader(finalOptions); 232 233 // after reader() we will have loaded the Content-Type. 234 ConfigSyntax contentType = contentType(); 235 236 ConfigParseOptions optionsWithContentType; 237 if (contentType != null) { 238 if (ConfigImpl.traceLoadsEnabled() && finalOptions.getSyntax() != null) 239 trace("Overriding syntax " + finalOptions.getSyntax() 240 + " with Content-Type which specified " + contentType); 241 242 optionsWithContentType = finalOptions.setSyntax(contentType); 243 } else { 244 optionsWithContentType = finalOptions; 245 } 246 247 try { 248 return rawParseValue(reader, origin, optionsWithContentType); 249 } finally { 250 reader.close(); 251 } 252 } 253 254 private AbstractConfigValue rawParseValue(Reader reader, ConfigOrigin origin, 255 ConfigParseOptions finalOptions) throws IOException { 256 if (finalOptions.getSyntax() == ConfigSyntax.PROPERTIES) { 257 return PropertiesParser.parse(reader, origin); 258 } else { 259 Iterator<Token> tokens = Tokenizer.tokenize(origin, reader, finalOptions.getSyntax()); 260 ConfigNodeRoot document = ConfigDocumentParser.parse(tokens, origin, finalOptions); 261 return ConfigParser.parse(document, origin, finalOptions, includeContext()); 262 } 263 } 264 265 // this is parseDocument without post-processing the IOException or handling 266 // options.getAllowMissing() 267 protected ConfigDocument rawParseDocument(ConfigOrigin origin, ConfigParseOptions finalOptions) 268 throws IOException { 269 Reader reader = reader(finalOptions); 270 271 // after reader() we will have loaded the Content-Type. 272 ConfigSyntax contentType = contentType(); 273 274 ConfigParseOptions optionsWithContentType; 275 if (contentType != null) { 276 if (ConfigImpl.traceLoadsEnabled() && finalOptions.getSyntax() != null) 277 trace("Overriding syntax " + finalOptions.getSyntax() 278 + " with Content-Type which specified " + contentType); 279 280 optionsWithContentType = finalOptions.setSyntax(contentType); 281 } else { 282 optionsWithContentType = finalOptions; 283 } 284 285 try { 286 return rawParseDocument(reader, origin, optionsWithContentType); 287 } finally { 288 reader.close(); 289 } 290 } 291 292 private ConfigDocument rawParseDocument(Reader reader, ConfigOrigin origin, 293 ConfigParseOptions finalOptions) throws IOException { 294 Iterator<Token> tokens = Tokenizer.tokenize(origin, reader, finalOptions.getSyntax()); 295 return new SimpleConfigDocument(ConfigDocumentParser.parse(tokens, origin, finalOptions), finalOptions); 296 } 297 298 public ConfigObject parse() { 299 return forceParsedToObject(parseValue(options())); 300 } 301 302 public ConfigDocument parseConfigDocument() { 303 return parseDocument(options()); 304 } 305 306 AbstractConfigValue parseValue() { 307 return parseValue(options()); 308 } 309 310 @Override 311 public final ConfigOrigin origin() { 312 return initialOrigin; 313 } 314 315 protected abstract ConfigOrigin createOrigin(); 316 317 @Override 318 public ConfigParseOptions options() { 319 return initialOptions; 320 } 321 322 @Override 323 public String toString() { 324 return getClass().getSimpleName(); 325 } 326 327 private static ConfigSyntax syntaxFromExtension(String name) { 328 if (name.endsWith(".json")) 329 return ConfigSyntax.JSON; 330 else if (name.endsWith(".conf")) 331 return ConfigSyntax.CONF; 332 else if (name.endsWith(".properties")) 333 return ConfigSyntax.PROPERTIES; 334 else 335 return null; 336 } 337 338 private static Reader readerFromStream(InputStream input) { 339 return readerFromStream(input, "UTF-8"); 340 } 341 342 private static Reader readerFromStream(InputStream input, String encoding) { 343 try { 344 // well, this is messed up. If we aren't going to close 345 // the passed-in InputStream then we have no way to 346 // close these readers. So maybe we should not have an 347 // InputStream version, only a Reader version. 348 Reader reader = new InputStreamReader(input, encoding); 349 return new BufferedReader(reader); 350 } catch (UnsupportedEncodingException e) { 351 throw new ConfigException.BugOrBroken("Java runtime does not support UTF-8", e); 352 } 353 } 354 355 private static Reader doNotClose(Reader input) { 356 return new FilterReader(input) { 357 @Override 358 public void close() { 359 // NOTHING. 360 } 361 }; 362 } 363 364 static URL relativeTo(URL url, String filename) { 365 // I'm guessing this completely fails on Windows, help wanted 366 if (new File(filename).isAbsolute()) 367 return null; 368 369 try { 370 URI siblingURI = url.toURI(); 371 URI relative = new URI(filename); 372 373 // this seems wrong, but it's documented that the last 374 // element of the path in siblingURI gets stripped out, 375 // so to get something in the same directory as 376 // siblingURI we just call resolve(). 377 URL resolved = siblingURI.resolve(relative).toURL(); 378 379 return resolved; 380 } catch (MalformedURLException e) { 381 return null; 382 } catch (URISyntaxException e) { 383 return null; 384 } catch (IllegalArgumentException e) { 385 return null; 386 } 387 } 388 389 static File relativeTo(File file, String filename) { 390 File child = new File(filename); 391 392 if (child.isAbsolute()) 393 return null; 394 395 File parent = file.getParentFile(); 396 397 if (parent == null) 398 return null; 399 else 400 return new File(parent, filename); 401 } 402 403 // this is a parseable that doesn't exist and just throws when you try to 404 // parse it 405 private final static class ParseableNotFound extends Parseable { 406 final private String what; 407 final private String message; 408 409 ParseableNotFound(String what, String message, ConfigParseOptions options) { 410 this.what = what; 411 this.message = message; 412 postConstruct(options); 413 } 414 415 @Override 416 protected Reader reader() throws IOException { 417 throw new FileNotFoundException(message); 418 } 419 420 @Override 421 protected ConfigOrigin createOrigin() { 422 return SimpleConfigOrigin.newSimple(what); 423 } 424 } 425 426 public static Parseable newNotFound(String whatNotFound, String message, 427 ConfigParseOptions options) { 428 return new ParseableNotFound(whatNotFound, message, options); 429 } 430 431 private final static class ParseableReader extends Parseable { 432 final private Reader reader; 433 434 ParseableReader(Reader reader, ConfigParseOptions options) { 435 this.reader = reader; 436 postConstruct(options); 437 } 438 439 @Override 440 protected Reader reader() { 441 if (ConfigImpl.traceLoadsEnabled()) 442 trace("Loading config from reader " + reader); 443 return reader; 444 } 445 446 @Override 447 protected ConfigOrigin createOrigin() { 448 return SimpleConfigOrigin.newSimple("Reader"); 449 } 450 } 451 452 // note that we will never close this reader; you have to do it when parsing 453 // is complete. 454 public static Parseable newReader(Reader reader, ConfigParseOptions options) { 455 456 return new ParseableReader(doNotClose(reader), options); 457 } 458 459 private final static class ParseableString extends Parseable { 460 final private String input; 461 462 ParseableString(String input, ConfigParseOptions options) { 463 this.input = input; 464 postConstruct(options); 465 } 466 467 @Override 468 protected Reader reader() { 469 if (ConfigImpl.traceLoadsEnabled()) 470 trace("Loading config from a String " + input); 471 return new StringReader(input); 472 } 473 474 @Override 475 protected ConfigOrigin createOrigin() { 476 return SimpleConfigOrigin.newSimple("String"); 477 } 478 479 @Override 480 public String toString() { 481 return getClass().getSimpleName() + "(" + input + ")"; 482 } 483 } 484 485 public static Parseable newString(String input, ConfigParseOptions options) { 486 return new ParseableString(input, options); 487 } 488 489 private static final String jsonContentType = "application/json"; 490 private static final String propertiesContentType = "text/x-java-properties"; 491 private static final String hoconContentType = "application/hocon"; 492 493 private static class ParseableURL extends Parseable { 494 final protected URL input; 495 private String contentType = null; 496 497 protected ParseableURL(URL input) { 498 this.input = input; 499 // does not postConstruct (subclass does it) 500 } 501 502 ParseableURL(URL input, ConfigParseOptions options) { 503 this(input); 504 postConstruct(options); 505 } 506 507 @Override 508 protected Reader reader() throws IOException { 509 throw new ConfigException.BugOrBroken("reader() without options should not be called on ParseableURL"); 510 } 511 512 private static String acceptContentType(ConfigParseOptions options) { 513 if (options.getSyntax() == null) 514 return null; 515 516 switch (options.getSyntax()) { 517 case JSON: 518 return jsonContentType; 519 case CONF: 520 return hoconContentType; 521 case PROPERTIES: 522 return propertiesContentType; 523 } 524 525 // not sure this is reachable but javac thinks it is 526 return null; 527 } 528 529 @Override 530 protected Reader reader(ConfigParseOptions options) throws IOException { 531 try { 532 if (ConfigImpl.traceLoadsEnabled()) 533 trace("Loading config from a URL: " + input.toExternalForm()); 534 URLConnection connection = input.openConnection(); 535 536 // allow server to serve multiple types from one URL 537 String acceptContent = acceptContentType(options); 538 if (acceptContent != null) { 539 connection.setRequestProperty("Accept", acceptContent); 540 } 541 542 connection.connect(); 543 544 // save content type for later 545 contentType = connection.getContentType(); 546 if (contentType != null) { 547 if (ConfigImpl.traceLoadsEnabled()) 548 trace("URL sets Content-Type: '" + contentType + "'"); 549 contentType = contentType.trim(); 550 int semi = contentType.indexOf(';'); 551 if (semi >= 0) 552 contentType = contentType.substring(0, semi); 553 } 554 555 InputStream stream = connection.getInputStream(); 556 557 return readerFromStream(stream); 558 } catch (FileNotFoundException fnf) { 559 // If the resource is not found (HTTP response 560 // code 404 or something alike), then it's fine to 561 // treat it according to the allowMissing setting 562 // and "include" spec. But if we have something 563 // like HTTP 503 it seems to be better to fail 564 // early, because this may be a sign of broken 565 // environment. Java throws FileNotFoundException 566 // if it sees 404 or 410. 567 throw fnf; 568 } catch (IOException e) { 569 throw new ConfigException.BugOrBroken("Cannot load config from URL: " + input.toExternalForm(), e); 570 } 571 } 572 573 @Override 574 ConfigSyntax guessSyntax() { 575 return syntaxFromExtension(input.getPath()); 576 } 577 578 @Override 579 ConfigSyntax contentType() { 580 if (contentType != null) { 581 if (contentType.equals(jsonContentType)) 582 return ConfigSyntax.JSON; 583 else if (contentType.equals(propertiesContentType)) 584 return ConfigSyntax.PROPERTIES; 585 else if (contentType.equals(hoconContentType)) 586 return ConfigSyntax.CONF; 587 else { 588 if (ConfigImpl.traceLoadsEnabled()) 589 trace("'" + contentType + "' isn't a known content type"); 590 return null; 591 } 592 } else { 593 return null; 594 } 595 } 596 597 @Override 598 ConfigParseable relativeTo(String filename) { 599 URL url = relativeTo(input, filename); 600 if (url == null) 601 return null; 602 return newURL(url, options().setOriginDescription(null)); 603 } 604 605 @Override 606 protected ConfigOrigin createOrigin() { 607 return SimpleConfigOrigin.newURL(input); 608 } 609 610 @Override 611 public String toString() { 612 return getClass().getSimpleName() + "(" + input.toExternalForm() + ")"; 613 } 614 } 615 616 public static Parseable newURL(URL input, ConfigParseOptions options) { 617 // we want file: URLs and files to always behave the same, so switch 618 // to a file if it's a file: URL 619 if (input.getProtocol().equals("file")) { 620 return newFile(ConfigImplUtil.urlToFile(input), options); 621 } else { 622 return new ParseableURL(input, options); 623 } 624 } 625 626 private final static class ParseableFile extends Parseable { 627 final private File input; 628 629 ParseableFile(File input, ConfigParseOptions options) { 630 this.input = input; 631 postConstruct(options); 632 } 633 634 @Override 635 protected Reader reader() throws IOException { 636 if (ConfigImpl.traceLoadsEnabled()) 637 trace("Loading config from a file: " + input); 638 InputStream stream = new FileInputStream(input); 639 return readerFromStream(stream); 640 } 641 642 @Override 643 ConfigSyntax guessSyntax() { 644 return syntaxFromExtension(input.getName()); 645 } 646 647 @Override 648 ConfigParseable relativeTo(String filename) { 649 File sibling; 650 if ((new File(filename)).isAbsolute()) { 651 sibling = new File(filename); 652 } else { 653 // this may return null 654 sibling = relativeTo(input, filename); 655 } 656 if (sibling == null) 657 return null; 658 if (sibling.exists()) { 659 trace(sibling + " exists, so loading it as a file"); 660 return newFile(sibling, options().setOriginDescription(null)); 661 } else { 662 trace(sibling + " does not exist, so trying it as a classpath resource"); 663 return super.relativeTo(filename); 664 } 665 } 666 667 @Override 668 protected ConfigOrigin createOrigin() { 669 return SimpleConfigOrigin.newFile(input.getPath()); 670 } 671 672 @Override 673 public String toString() { 674 return getClass().getSimpleName() + "(" + input.getPath() + ")"; 675 } 676 } 677 678 public static Parseable newFile(File input, ConfigParseOptions options) { 679 return new ParseableFile(input, options); 680 } 681 682 683 private final static class ParseableResourceURL extends ParseableURL { 684 685 private final Relativizer relativizer; 686 private final String resource; 687 688 ParseableResourceURL(URL input, ConfigParseOptions options, String resource, Relativizer relativizer) { 689 super(input); 690 this.relativizer = relativizer; 691 this.resource = resource; 692 postConstruct(options); 693 } 694 695 @Override 696 protected ConfigOrigin createOrigin() { 697 return SimpleConfigOrigin.newResource(resource, input); 698 } 699 700 @Override 701 ConfigParseable relativeTo(String filename) { 702 return relativizer.relativeTo(filename); 703 } 704 } 705 706 private static Parseable newResourceURL(URL input, ConfigParseOptions options, String resource, Relativizer relativizer) { 707 return new ParseableResourceURL(input, options, resource, relativizer); 708 } 709 710 private final static class ParseableResources extends Parseable implements Relativizer { 711 final private String resource; 712 713 ParseableResources(String resource, ConfigParseOptions options) { 714 this.resource = resource; 715 postConstruct(options); 716 } 717 718 @Override 719 protected Reader reader() throws IOException { 720 throw new ConfigException.BugOrBroken("reader() should not be called on resources"); 721 } 722 723 @Override 724 protected AbstractConfigObject rawParseValue(ConfigOrigin origin, 725 ConfigParseOptions finalOptions) throws IOException { 726 ClassLoader loader = finalOptions.getClassLoader(); 727 if (loader == null) 728 throw new ConfigException.BugOrBroken( 729 "null class loader; pass in a class loader or use Thread.currentThread().setContextClassLoader()"); 730 Enumeration<URL> e = loader.getResources(resource); 731 if (!e.hasMoreElements()) { 732 if (ConfigImpl.traceLoadsEnabled()) 733 trace("Loading config from class loader " + loader 734 + " but there were no resources called " + resource); 735 throw new IOException("resource not found on classpath: " + resource); 736 } 737 AbstractConfigObject merged = SimpleConfigObject.empty(origin); 738 while (e.hasMoreElements()) { 739 URL url = e.nextElement(); 740 741 if (ConfigImpl.traceLoadsEnabled()) 742 trace("Loading config from resource '" + resource + "' URL " + url.toExternalForm() + " from class loader " 743 + loader); 744 745 Parseable element = newResourceURL(url, finalOptions, resource, this); 746 747 AbstractConfigValue v = element.parseValue(); 748 749 merged = merged.withFallback(v); 750 } 751 752 return merged; 753 } 754 755 @Override 756 ConfigSyntax guessSyntax() { 757 return syntaxFromExtension(resource); 758 } 759 760 static String parent(String resource) { 761 // the "resource" is not supposed to begin with a "/" 762 // because it's supposed to be the raw resource 763 // (ClassLoader#getResource), not the 764 // resource "syntax" (Class#getResource) 765 int i = resource.lastIndexOf('/'); 766 if (i < 0) { 767 return null; 768 } else { 769 return resource.substring(0, i); 770 } 771 } 772 773 @Override 774 public ConfigParseable relativeTo(String sibling) { 775 if (sibling.startsWith("/")) { 776 // if it starts with "/" then don't make it relative to 777 // the including resource 778 return newResources(sibling.substring(1), options().setOriginDescription(null)); 779 } else { 780 // here we want to build a new resource name and let 781 // the class loader have it, rather than getting the 782 // url with getResource() and relativizing to that url. 783 // This is needed in case the class loader is going to 784 // search a classpath. 785 String parent = parent(resource); 786 if (parent == null) 787 return newResources(sibling, options().setOriginDescription(null)); 788 else 789 return newResources(parent + "/" + sibling, options() 790 .setOriginDescription(null)); 791 } 792 } 793 794 @Override 795 protected ConfigOrigin createOrigin() { 796 return SimpleConfigOrigin.newResource(resource); 797 } 798 799 @Override 800 public String toString() { 801 return getClass().getSimpleName() + "(" + resource + ")"; 802 } 803 } 804 805 public static Parseable newResources(Class<?> klass, String resource, ConfigParseOptions options) { 806 return newResources(convertResourceName(klass, resource), 807 options.setClassLoader(klass.getClassLoader())); 808 } 809 810 // this function is supposed to emulate the difference 811 // between Class.getResource and ClassLoader.getResource 812 // (unfortunately there doesn't seem to be public API for it). 813 // We're using it because the Class API is more limited, 814 // for example it lacks getResources(). So we want to be able to 815 // use ClassLoader directly. 816 private static String convertResourceName(Class<?> klass, String resource) { 817 if (resource.startsWith("/")) { 818 // "absolute" resource, chop the slash 819 return resource.substring(1); 820 } else { 821 String className = klass.getName(); 822 int i = className.lastIndexOf('.'); 823 if (i < 0) { 824 // no package 825 return resource; 826 } else { 827 // need to be relative to the package 828 String packageName = className.substring(0, i); 829 String packagePath = packageName.replace('.', '/'); 830 return packagePath + "/" + resource; 831 } 832 } 833 } 834 835 public static Parseable newResources(String resource, ConfigParseOptions options) { 836 if (options.getClassLoader() == null) 837 throw new ConfigException.BugOrBroken( 838 "null class loader; pass in a class loader or use Thread.currentThread().setContextClassLoader()"); 839 return new ParseableResources(resource, options); 840 } 841 842 private final static class ParseableProperties extends Parseable { 843 final private Properties props; 844 845 ParseableProperties(Properties props, ConfigParseOptions options) { 846 this.props = props; 847 postConstruct(options); 848 } 849 850 @Override 851 protected Reader reader() throws IOException { 852 throw new ConfigException.BugOrBroken("reader() should not be called on props"); 853 } 854 855 @Override 856 protected AbstractConfigObject rawParseValue(ConfigOrigin origin, 857 ConfigParseOptions finalOptions) { 858 if (ConfigImpl.traceLoadsEnabled()) 859 trace("Loading config from properties " + props); 860 return PropertiesParser.fromProperties(origin, props); 861 } 862 863 @Override 864 ConfigSyntax guessSyntax() { 865 return ConfigSyntax.PROPERTIES; 866 } 867 868 @Override 869 protected ConfigOrigin createOrigin() { 870 return SimpleConfigOrigin.newSimple("properties"); 871 } 872 873 @Override 874 public String toString() { 875 return getClass().getSimpleName() + "(" + props.size() + " props)"; 876 } 877 } 878 879 public static Parseable newProperties(Properties properties, ConfigParseOptions options) { 880 return new ParseableProperties(properties, options); 881 } 882}