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