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