    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}