    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}