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}