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 trace(e.getMessage() + ". Allowing Missing File, this can be turned off by setting" + 184 " ConfigParseOptions.allowMissing = false"); 185 return SimpleConfigObject.emptyMissing(origin); 186 } else { 187 trace("exception loading " + origin.description() + ": " + e.getClass().getName() 188 + ": " + e.getMessage()); 189 throw new ConfigException.IO(origin, 190 e.getClass().getName() + ": " + e.getMessage(), e); 191 } 192 } 193 } 194 195 final ConfigDocument parseDocument(ConfigParseOptions baseOptions) { 196 // note that we are NOT using our "initialOptions", 197 // but using the ones from the passed-in options. The idea is that 198 // callers can get our original options and then parse with different 199 // ones if they want. 200 ConfigParseOptions options = fixupOptions(baseOptions); 201 202 // passed-in options can override origin 203 ConfigOrigin origin; 204 if (options.getOriginDescription() != null) 205 origin = SimpleConfigOrigin.newSimple(options.getOriginDescription()); 206 else 207 origin = initialOrigin; 208 return parseDocument(origin, options); 209 } 210 211 final private ConfigDocument parseDocument(ConfigOrigin origin, 212 ConfigParseOptions finalOptions) { 213 try { 214 return rawParseDocument(origin, finalOptions); 215 } catch (IOException e) { 216 if (finalOptions.getAllowMissing()) { 217 ArrayList<AbstractConfigNode> children = new ArrayList<AbstractConfigNode>(); 218 children.add(new ConfigNodeObject(new ArrayList<AbstractConfigNode>())); 219 return new SimpleConfigDocument(new ConfigNodeRoot(children, origin), finalOptions); 220 } else { 221 trace("exception loading " + origin.description() + ": " + e.getClass().getName() 222 + ": " + e.getMessage()); 223 throw new ConfigException.IO(origin, 224 e.getClass().getName() + ": " + e.getMessage(), e); 225 } 226 } 227 } 228 229 // this is parseValue without post-processing the IOException or handling 230 // options.getAllowMissing() 231 protected AbstractConfigValue rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) 232 throws IOException { 233 Reader reader = reader(finalOptions); 234 235 // after reader() we will have loaded the Content-Type. 236 ConfigSyntax contentType = contentType(); 237 238 ConfigParseOptions optionsWithContentType; 239 if (contentType != null) { 240 if (ConfigImpl.traceLoadsEnabled() && finalOptions.getSyntax() != null) 241 trace("Overriding syntax " + finalOptions.getSyntax() 242 + " with Content-Type which specified " + contentType); 243 244 optionsWithContentType = finalOptions.setSyntax(contentType); 245 } else { 246 optionsWithContentType = finalOptions; 247 } 248 249 try { 250 return rawParseValue(reader, origin, optionsWithContentType); 251 } finally { 252 reader.close(); 253 } 254 } 255 256 private AbstractConfigValue rawParseValue(Reader reader, ConfigOrigin origin, 257 ConfigParseOptions finalOptions) throws IOException { 258 if (finalOptions.getSyntax() == ConfigSyntax.PROPERTIES) { 259 return PropertiesParser.parse(reader, origin); 260 } else { 261 Iterator<Token> tokens = Tokenizer.tokenize(origin, reader, finalOptions.getSyntax()); 262 ConfigNodeRoot document = ConfigDocumentParser.parse(tokens, origin, finalOptions); 263 return ConfigParser.parse(document, origin, finalOptions, includeContext()); 264 } 265 } 266 267 // this is parseDocument without post-processing the IOException or handling 268 // options.getAllowMissing() 269 protected ConfigDocument rawParseDocument(ConfigOrigin origin, ConfigParseOptions finalOptions) 270 throws IOException { 271 Reader reader = reader(finalOptions); 272 273 // after reader() we will have loaded the Content-Type. 274 ConfigSyntax contentType = contentType(); 275 276 ConfigParseOptions optionsWithContentType; 277 if (contentType != null) { 278 if (ConfigImpl.traceLoadsEnabled() && finalOptions.getSyntax() != null) 279 trace("Overriding syntax " + finalOptions.getSyntax() 280 + " with Content-Type which specified " + contentType); 281 282 optionsWithContentType = finalOptions.setSyntax(contentType); 283 } else { 284 optionsWithContentType = finalOptions; 285 } 286 287 try { 288 return rawParseDocument(reader, origin, optionsWithContentType); 289 } finally { 290 reader.close(); 291 } 292 } 293 294 private ConfigDocument rawParseDocument(Reader reader, ConfigOrigin origin, 295 ConfigParseOptions finalOptions) throws IOException { 296 Iterator<Token> tokens = Tokenizer.tokenize(origin, reader, finalOptions.getSyntax()); 297 return new SimpleConfigDocument(ConfigDocumentParser.parse(tokens, origin, finalOptions), finalOptions); 298 } 299 300 public ConfigObject parse() { 301 return forceParsedToObject(parseValue(options())); 302 } 303 304 public ConfigDocument parseConfigDocument() { 305 return parseDocument(options()); 306 } 307 308 AbstractConfigValue parseValue() { 309 return parseValue(options()); 310 } 311 312 @Override 313 public final ConfigOrigin origin() { 314 return initialOrigin; 315 } 316 317 protected abstract ConfigOrigin createOrigin(); 318 319 @Override 320 public ConfigParseOptions options() { 321 return initialOptions; 322 } 323 324 @Override 325 public String toString() { 326 return getClass().getSimpleName(); 327 } 328 329 private static Reader readerFromStream(InputStream input) { 330 return readerFromStream(input, "UTF-8"); 331 } 332 333 private static Reader readerFromStream(InputStream input, String encoding) { 334 try { 335 // well, this is messed up. If we aren't going to close 336 // the passed-in InputStream then we have no way to 337 // close these readers. So maybe we should not have an 338 // InputStream version, only a Reader version. 339 Reader reader = new InputStreamReader(input, encoding); 340 return new BufferedReader(reader); 341 } catch (UnsupportedEncodingException e) { 342 throw new ConfigException.BugOrBroken("Java runtime does not support UTF-8", e); 343 } 344 } 345 346 private static Reader doNotClose(Reader input) { 347 return new FilterReader(input) { 348 @Override 349 public void close() { 350 // NOTHING. 351 } 352 }; 353 } 354 355 static URL relativeTo(URL url, String filename) { 356 // I'm guessing this completely fails on Windows, help wanted 357 if (new File(filename).isAbsolute()) 358 return null; 359 360 try { 361 URI siblingURI = url.toURI(); 362 URI relative = new URI(filename); 363 364 // this seems wrong, but it's documented that the last 365 // element of the path in siblingURI gets stripped out, 366 // so to get something in the same directory as 367 // siblingURI we just call resolve(). 368 URL resolved = siblingURI.resolve(relative).toURL(); 369 370 return resolved; 371 } catch (MalformedURLException e) { 372 return null; 373 } catch (URISyntaxException e) { 374 return null; 375 } catch (IllegalArgumentException e) { 376 return null; 377 } 378 } 379 380 static File relativeTo(File file, String filename) { 381 File child = new File(filename); 382 383 if (child.isAbsolute()) 384 return null; 385 386 File parent = file.getParentFile(); 387 388 if (parent == null) 389 return null; 390 else 391 return new File(parent, filename); 392 } 393 394 // this is a parseable that doesn't exist and just throws when you try to 395 // parse it 396 private final static class ParseableNotFound extends Parseable { 397 final private String what; 398 final private String message; 399 400 ParseableNotFound(String what, String message, ConfigParseOptions options) { 401 this.what = what; 402 this.message = message; 403 postConstruct(options); 404 } 405 406 @Override 407 protected Reader reader() throws IOException { 408 throw new FileNotFoundException(message); 409 } 410 411 @Override 412 protected ConfigOrigin createOrigin() { 413 return SimpleConfigOrigin.newSimple(what); 414 } 415 } 416 417 public static Parseable newNotFound(String whatNotFound, String message, 418 ConfigParseOptions options) { 419 return new ParseableNotFound(whatNotFound, message, options); 420 } 421 422 private final static class ParseableReader extends Parseable { 423 final private Reader reader; 424 425 ParseableReader(Reader reader, ConfigParseOptions options) { 426 this.reader = reader; 427 postConstruct(options); 428 } 429 430 @Override 431 protected Reader reader() { 432 if (ConfigImpl.traceLoadsEnabled()) 433 trace("Loading config from reader " + reader); 434 return reader; 435 } 436 437 @Override 438 protected ConfigOrigin createOrigin() { 439 return SimpleConfigOrigin.newSimple("Reader"); 440 } 441 } 442 443 // note that we will never close this reader; you have to do it when parsing 444 // is complete. 445 public static Parseable newReader(Reader reader, ConfigParseOptions options) { 446 447 return new ParseableReader(doNotClose(reader), options); 448 } 449 450 private final static class ParseableString extends Parseable { 451 final private String input; 452 453 ParseableString(String input, ConfigParseOptions options) { 454 this.input = input; 455 postConstruct(options); 456 } 457 458 @Override 459 protected Reader reader() { 460 if (ConfigImpl.traceLoadsEnabled()) 461 trace("Loading config from a String " + input); 462 return new StringReader(input); 463 } 464 465 @Override 466 protected ConfigOrigin createOrigin() { 467 return SimpleConfigOrigin.newSimple("String"); 468 } 469 470 @Override 471 public String toString() { 472 return getClass().getSimpleName() + "(" + input + ")"; 473 } 474 } 475 476 public static Parseable newString(String input, ConfigParseOptions options) { 477 return new ParseableString(input, options); 478 } 479 480 private static final String jsonContentType = "application/json"; 481 private static final String propertiesContentType = "text/x-java-properties"; 482 private static final String hoconContentType = "application/hocon"; 483 484 private static class ParseableURL extends Parseable { 485 final protected URL input; 486 private String contentType = null; 487 488 protected ParseableURL(URL input) { 489 this.input = input; 490 // does not postConstruct (subclass does it) 491 } 492 493 ParseableURL(URL input, ConfigParseOptions options) { 494 this(input); 495 postConstruct(options); 496 } 497 498 @Override 499 protected Reader reader() throws IOException { 500 throw new ConfigException.BugOrBroken("reader() without options should not be called on ParseableURL"); 501 } 502 503 private static String acceptContentType(ConfigParseOptions options) { 504 if (options.getSyntax() == null) 505 return null; 506 507 switch (options.getSyntax()) { 508 case JSON: 509 return jsonContentType; 510 case CONF: 511 return hoconContentType; 512 case PROPERTIES: 513 return propertiesContentType; 514 } 515 516 // not sure this is reachable but javac thinks it is 517 return null; 518 } 519 520 @Override 521 protected Reader reader(ConfigParseOptions options) throws IOException { 522 try { 523 if (ConfigImpl.traceLoadsEnabled()) 524 trace("Loading config from a URL: " + input.toExternalForm()); 525 URLConnection connection = input.openConnection(); 526 527 // allow server to serve multiple types from one URL 528 String acceptContent = acceptContentType(options); 529 if (acceptContent != null) { 530 connection.setRequestProperty("Accept", acceptContent); 531 } 532 533 connection.connect(); 534 535 // save content type for later 536 contentType = connection.getContentType(); 537 if (contentType != null) { 538 if (ConfigImpl.traceLoadsEnabled()) 539 trace("URL sets Content-Type: '" + contentType + "'"); 540 contentType = contentType.trim(); 541 int semi = contentType.indexOf(';'); 542 if (semi >= 0) 543 contentType = contentType.substring(0, semi); 544 } 545 546 InputStream stream = connection.getInputStream(); 547 548 return readerFromStream(stream); 549 } catch (FileNotFoundException fnf) { 550 // If the resource is not found (HTTP response 551 // code 404 or something alike), then it's fine to 552 // treat it according to the allowMissing setting 553 // and "include" spec. But if we have something 554 // like HTTP 503 it seems to be better to fail 555 // early, because this may be a sign of broken 556 // environment. Java throws FileNotFoundException 557 // if it sees 404 or 410. 558 throw fnf; 559 } catch (IOException e) { 560 throw new ConfigException.BugOrBroken("Cannot load config from URL: " + input.toExternalForm(), e); 561 } 562 } 563 564 @Override 565 ConfigSyntax guessSyntax() { 566 return ConfigImplUtil.syntaxFromExtension(input.getPath()); 567 } 568 569 @Override 570 ConfigSyntax contentType() { 571 if (contentType != null) { 572 if (contentType.equals(jsonContentType)) 573 return ConfigSyntax.JSON; 574 else if (contentType.equals(propertiesContentType)) 575 return ConfigSyntax.PROPERTIES; 576 else if (contentType.equals(hoconContentType)) 577 return ConfigSyntax.CONF; 578 else { 579 if (ConfigImpl.traceLoadsEnabled()) 580 trace("'" + contentType + "' isn't a known content type"); 581 return null; 582 } 583 } else { 584 return null; 585 } 586 } 587 588 @Override 589 ConfigParseable relativeTo(String filename) { 590 URL url = relativeTo(input, filename); 591 if (url == null) 592 return null; 593 return newURL(url, options().setOriginDescription(null)); 594 } 595 596 @Override 597 protected ConfigOrigin createOrigin() { 598 return SimpleConfigOrigin.newURL(input); 599 } 600 601 @Override 602 public String toString() { 603 return getClass().getSimpleName() + "(" + input.toExternalForm() + ")"; 604 } 605 } 606 607 public static Parseable newURL(URL input, ConfigParseOptions options) { 608 // we want file: URLs and files to always behave the same, so switch 609 // to a file if it's a file: URL 610 if (input.getProtocol().equals("file")) { 611 return newFile(ConfigImplUtil.urlToFile(input), options); 612 } else { 613 return new ParseableURL(input, options); 614 } 615 } 616 617 private final static class ParseableFile extends Parseable { 618 final private File input; 619 620 ParseableFile(File input, ConfigParseOptions options) { 621 this.input = input; 622 postConstruct(options); 623 } 624 625 @Override 626 protected Reader reader() throws IOException { 627 if (ConfigImpl.traceLoadsEnabled()) 628 trace("Loading config from a file: " + input); 629 InputStream stream = new FileInputStream(input); 630 return readerFromStream(stream); 631 } 632 633 @Override 634 ConfigSyntax guessSyntax() { 635 return ConfigImplUtil.syntaxFromExtension(input.getName()); 636 } 637 638 @Override 639 ConfigParseable relativeTo(String filename) { 640 File sibling; 641 if ((new File(filename)).isAbsolute()) { 642 sibling = new File(filename); 643 } else { 644 // this may return null 645 sibling = relativeTo(input, filename); 646 } 647 if (sibling == null) 648 return null; 649 if (sibling.exists()) { 650 trace(sibling + " exists, so loading it as a file"); 651 return newFile(sibling, options().setOriginDescription(null)); 652 } else { 653 trace(sibling + " does not exist, so trying it as a classpath resource"); 654 return super.relativeTo(filename); 655 } 656 } 657 658 @Override 659 protected ConfigOrigin createOrigin() { 660 return SimpleConfigOrigin.newFile(input.getPath()); 661 } 662 663 @Override 664 public String toString() { 665 return getClass().getSimpleName() + "(" + input.getPath() + ")"; 666 } 667 } 668 669 public static Parseable newFile(File input, ConfigParseOptions options) { 670 return new ParseableFile(input, options); 671 } 672 673 674 private final static class ParseableResourceURL extends ParseableURL { 675 676 private final Relativizer relativizer; 677 private final String resource; 678 679 ParseableResourceURL(URL input, ConfigParseOptions options, String resource, Relativizer relativizer) { 680 super(input); 681 this.relativizer = relativizer; 682 this.resource = resource; 683 postConstruct(options); 684 } 685 686 @Override 687 protected ConfigOrigin createOrigin() { 688 return SimpleConfigOrigin.newResource(resource, input); 689 } 690 691 @Override 692 ConfigParseable relativeTo(String filename) { 693 return relativizer.relativeTo(filename); 694 } 695 } 696 697 private static Parseable newResourceURL(URL input, ConfigParseOptions options, String resource, Relativizer relativizer) { 698 return new ParseableResourceURL(input, options, resource, relativizer); 699 } 700 701 private final static class ParseableResources extends Parseable implements Relativizer { 702 final private String resource; 703 704 ParseableResources(String resource, ConfigParseOptions options) { 705 this.resource = resource; 706 postConstruct(options); 707 } 708 709 @Override 710 protected Reader reader() throws IOException { 711 throw new ConfigException.BugOrBroken("reader() should not be called on resources"); 712 } 713 714 @Override 715 protected AbstractConfigObject rawParseValue(ConfigOrigin origin, 716 ConfigParseOptions finalOptions) throws IOException { 717 ClassLoader loader = finalOptions.getClassLoader(); 718 if (loader == null) 719 throw new ConfigException.BugOrBroken( 720 "null class loader; pass in a class loader or use Thread.currentThread().setContextClassLoader()"); 721 Enumeration<URL> e = loader.getResources(resource); 722 if (!e.hasMoreElements()) { 723 if (ConfigImpl.traceLoadsEnabled()) 724 trace("Loading config from class loader " + loader 725 + " but there were no resources called " + resource); 726 throw new IOException("resource not found on classpath: " + resource); 727 } 728 AbstractConfigObject merged = SimpleConfigObject.empty(origin); 729 while (e.hasMoreElements()) { 730 URL url = e.nextElement(); 731 732 if (ConfigImpl.traceLoadsEnabled()) 733 trace("Loading config from resource '" + resource + "' URL " + url.toExternalForm() + " from class loader " 734 + loader); 735 736 Parseable element = newResourceURL(url, finalOptions, resource, this); 737 738 AbstractConfigValue v = element.parseValue(); 739 740 merged = merged.withFallback(v); 741 } 742 743 return merged; 744 } 745 746 @Override 747 ConfigSyntax guessSyntax() { 748 return ConfigImplUtil.syntaxFromExtension(resource); 749 } 750 751 static String parent(String resource) { 752 // the "resource" is not supposed to begin with a "/" 753 // because it's supposed to be the raw resource 754 // (ClassLoader#getResource), not the 755 // resource "syntax" (Class#getResource) 756 int i = resource.lastIndexOf('/'); 757 if (i < 0) { 758 return null; 759 } else { 760 return resource.substring(0, i); 761 } 762 } 763 764 @Override 765 public ConfigParseable relativeTo(String sibling) { 766 if (sibling.startsWith("/")) { 767 // if it starts with "/" then don't make it relative to 768 // the including resource 769 return newResources(sibling.substring(1), options().setOriginDescription(null)); 770 } else { 771 // here we want to build a new resource name and let 772 // the class loader have it, rather than getting the 773 // url with getResource() and relativizing to that url. 774 // This is needed in case the class loader is going to 775 // search a classpath. 776 String parent = parent(resource); 777 if (parent == null) 778 return newResources(sibling, options().setOriginDescription(null)); 779 else 780 return newResources(parent + "/" + sibling, options() 781 .setOriginDescription(null)); 782 } 783 } 784 785 @Override 786 protected ConfigOrigin createOrigin() { 787 return SimpleConfigOrigin.newResource(resource); 788 } 789 790 @Override 791 public String toString() { 792 return getClass().getSimpleName() + "(" + resource + ")"; 793 } 794 } 795 796 public static Parseable newResources(Class<?> klass, String resource, ConfigParseOptions options) { 797 return newResources(convertResourceName(klass, resource), 798 options.setClassLoader(klass.getClassLoader())); 799 } 800 801 // this function is supposed to emulate the difference 802 // between Class.getResource and ClassLoader.getResource 803 // (unfortunately there doesn't seem to be public API for it). 804 // We're using it because the Class API is more limited, 805 // for example it lacks getResources(). So we want to be able to 806 // use ClassLoader directly. 807 private static String convertResourceName(Class<?> klass, String resource) { 808 if (resource.startsWith("/")) { 809 // "absolute" resource, chop the slash 810 return resource.substring(1); 811 } else { 812 String className = klass.getName(); 813 int i = className.lastIndexOf('.'); 814 if (i < 0) { 815 // no package 816 return resource; 817 } else { 818 // need to be relative to the package 819 String packageName = className.substring(0, i); 820 String packagePath = packageName.replace('.', '/'); 821 return packagePath + "/" + resource; 822 } 823 } 824 } 825 826 public static Parseable newResources(String resource, ConfigParseOptions options) { 827 if (options.getClassLoader() == null) 828 throw new ConfigException.BugOrBroken( 829 "null class loader; pass in a class loader or use Thread.currentThread().setContextClassLoader()"); 830 return new ParseableResources(resource, options); 831 } 832 833 private final static class ParseableProperties extends Parseable { 834 final private Properties props; 835 836 ParseableProperties(Properties props, ConfigParseOptions options) { 837 this.props = props; 838 postConstruct(options); 839 } 840 841 @Override 842 protected Reader reader() throws IOException { 843 throw new ConfigException.BugOrBroken("reader() should not be called on props"); 844 } 845 846 @Override 847 protected AbstractConfigObject rawParseValue(ConfigOrigin origin, 848 ConfigParseOptions finalOptions) { 849 if (ConfigImpl.traceLoadsEnabled()) 850 trace("Loading config from properties " + props); 851 return PropertiesParser.fromProperties(origin, props); 852 } 853 854 @Override 855 ConfigSyntax guessSyntax() { 856 return ConfigSyntax.PROPERTIES; 857 } 858 859 @Override 860 protected ConfigOrigin createOrigin() { 861 return SimpleConfigOrigin.newSimple("properties"); 862 } 863 864 @Override 865 public String toString() { 866 return getClass().getSimpleName() + "(" + props.size() + " props)"; 867 } 868 } 869 870 public static Parseable newProperties(Properties properties, ConfigParseOptions options) { 871 return new ParseableProperties(properties, options); 872 } 873}