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}