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