001/**
002 *   Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com>
003 */
004package com.typesafe.config.impl;
005
006import java.io.File;
007import java.lang.ref.WeakReference;
008import java.net.URL;
009import java.time.Duration;
010import java.util.ArrayList;
011import java.util.Collections;
012import java.util.HashMap;
013import java.util.Iterator;
014import java.util.List;
015import java.util.Map;
016import java.util.Properties;
017import java.util.concurrent.Callable;
018
019import com.typesafe.config.Config;
020import com.typesafe.config.ConfigException;
021import com.typesafe.config.ConfigIncluder;
022import com.typesafe.config.ConfigMemorySize;
023import com.typesafe.config.ConfigObject;
024import com.typesafe.config.ConfigOrigin;
025import com.typesafe.config.ConfigParseOptions;
026import com.typesafe.config.ConfigParseable;
027import com.typesafe.config.ConfigValue;
028import com.typesafe.config.impl.SimpleIncluder.NameSource;
029
030/**
031 * Internal implementation detail, not ABI stable, do not touch.
032 * For use only by the {@link com.typesafe.config} package.
033 */
034public class ConfigImpl {
035
036    private static class LoaderCache {
037        private Config currentSystemProperties;
038        private WeakReference<ClassLoader> currentLoader;
039        private Map<String, Config> cache;
040
041        LoaderCache() {
042            this.currentSystemProperties = null;
043            this.currentLoader = new WeakReference<ClassLoader>(null);
044            this.cache = new HashMap<String, Config>();
045        }
046
047        // for now, caching as long as the loader remains the same,
048        // drop entire cache if it changes.
049        synchronized Config getOrElseUpdate(ClassLoader loader, String key, Callable<Config> updater) {
050            if (loader != currentLoader.get()) {
051                // reset the cache if we start using a different loader
052                cache.clear();
053                currentLoader = new WeakReference<ClassLoader>(loader);
054            }
055
056            Config systemProperties = systemPropertiesAsConfig();
057            if (systemProperties != currentSystemProperties) {
058                cache.clear();
059                currentSystemProperties = systemProperties;
060            }
061
062            Config config = cache.get(key);
063            if (config == null) {
064                try {
065                    config = updater.call();
066                } catch (RuntimeException e) {
067                    throw e; // this will include ConfigException
068                } catch (Exception e) {
069                    throw new ConfigException.Generic(e.getMessage(), e);
070                }
071                if (config == null)
072                    throw new ConfigException.BugOrBroken("null config from cache updater");
073                cache.put(key, config);
074            }
075
076            return config;
077        }
078    }
079
080    private static class LoaderCacheHolder {
081        static final LoaderCache cache = new LoaderCache();
082    }
083
084    public static Config computeCachedConfig(ClassLoader loader, String key,
085            Callable<Config> updater) {
086        LoaderCache cache;
087        try {
088            cache = LoaderCacheHolder.cache;
089        } catch (ExceptionInInitializerError e) {
090            throw ConfigImplUtil.extractInitializerError(e);
091        }
092        return cache.getOrElseUpdate(loader, key, updater);
093    }
094
095
096    static class FileNameSource implements SimpleIncluder.NameSource {
097        @Override
098        public ConfigParseable nameToParseable(String name, ConfigParseOptions parseOptions) {
099            return Parseable.newFile(new File(name), parseOptions);
100        }
101    };
102
103    static class ClasspathNameSource implements SimpleIncluder.NameSource {
104        @Override
105        public ConfigParseable nameToParseable(String name, ConfigParseOptions parseOptions) {
106            return Parseable.newResources(name, parseOptions);
107        }
108    };
109
110    static class ClasspathNameSourceWithClass implements SimpleIncluder.NameSource {
111        final private Class<?> klass;
112
113        public ClasspathNameSourceWithClass(Class<?> klass) {
114            this.klass = klass;
115        }
116
117        @Override
118        public ConfigParseable nameToParseable(String name, ConfigParseOptions parseOptions) {
119            return Parseable.newResources(klass, name, parseOptions);
120        }
121    };
122
123    public static ConfigObject parseResourcesAnySyntax(Class<?> klass, String resourceBasename,
124            ConfigParseOptions baseOptions) {
125        NameSource source = new ClasspathNameSourceWithClass(klass);
126        return SimpleIncluder.fromBasename(source, resourceBasename, baseOptions);
127    }
128
129    public static ConfigObject parseResourcesAnySyntax(String resourceBasename,
130            ConfigParseOptions baseOptions) {
131        NameSource source = new ClasspathNameSource();
132        return SimpleIncluder.fromBasename(source, resourceBasename, baseOptions);
133    }
134
135    public static ConfigObject parseFileAnySyntax(File basename, ConfigParseOptions baseOptions) {
136        NameSource source = new FileNameSource();
137        return SimpleIncluder.fromBasename(source, basename.getPath(), baseOptions);
138    }
139
140    static AbstractConfigObject emptyObject(String originDescription) {
141        ConfigOrigin origin = originDescription != null ? SimpleConfigOrigin
142                .newSimple(originDescription) : null;
143        return emptyObject(origin);
144    }
145
146    public static Config emptyConfig(String originDescription) {
147        return emptyObject(originDescription).toConfig();
148    }
149
150    static AbstractConfigObject empty(ConfigOrigin origin) {
151        return emptyObject(origin);
152    }
153
154    // default origin for values created with fromAnyRef and no origin specified
155    final private static ConfigOrigin defaultValueOrigin = SimpleConfigOrigin
156            .newSimple("hardcoded value");
157    final private static ConfigBoolean defaultTrueValue = new ConfigBoolean(
158            defaultValueOrigin, true);
159    final private static ConfigBoolean defaultFalseValue = new ConfigBoolean(
160            defaultValueOrigin, false);
161    final private static ConfigNull defaultNullValue = new ConfigNull(
162            defaultValueOrigin);
163    final private static SimpleConfigList defaultEmptyList = new SimpleConfigList(
164            defaultValueOrigin, Collections.<AbstractConfigValue> emptyList());
165    final private static SimpleConfigObject defaultEmptyObject = SimpleConfigObject
166            .empty(defaultValueOrigin);
167
168    private static SimpleConfigList emptyList(ConfigOrigin origin) {
169        if (origin == null || origin == defaultValueOrigin)
170            return defaultEmptyList;
171        else
172            return new SimpleConfigList(origin,
173                    Collections.<AbstractConfigValue> emptyList());
174    }
175
176    private static AbstractConfigObject emptyObject(ConfigOrigin origin) {
177        // we want null origin to go to SimpleConfigObject.empty() to get the
178        // origin "empty config" rather than "hardcoded value"
179        if (origin == defaultValueOrigin)
180            return defaultEmptyObject;
181        else
182            return SimpleConfigObject.empty(origin);
183    }
184
185    private static ConfigOrigin valueOrigin(String originDescription) {
186        if (originDescription == null)
187            return defaultValueOrigin;
188        else
189            return SimpleConfigOrigin.newSimple(originDescription);
190    }
191
192    public static ConfigValue fromAnyRef(Object object, String originDescription) {
193        ConfigOrigin origin = valueOrigin(originDescription);
194        return fromAnyRef(object, origin, FromMapMode.KEYS_ARE_KEYS);
195    }
196
197    public static ConfigObject fromPathMap(
198            Map<String, ? extends Object> pathMap, String originDescription) {
199        ConfigOrigin origin = valueOrigin(originDescription);
200        return (ConfigObject) fromAnyRef(pathMap, origin,
201                FromMapMode.KEYS_ARE_PATHS);
202    }
203
204    static AbstractConfigValue fromAnyRef(Object object, ConfigOrigin origin,
205            FromMapMode mapMode) {
206        if (origin == null)
207            throw new ConfigException.BugOrBroken(
208                    "origin not supposed to be null");
209
210        if (object == null) {
211            if (origin != defaultValueOrigin)
212                return new ConfigNull(origin);
213            else
214                return defaultNullValue;
215        } else if(object instanceof AbstractConfigValue) {
216            return (AbstractConfigValue) object;
217        } else if (object instanceof Boolean) {
218            if (origin != defaultValueOrigin) {
219                return new ConfigBoolean(origin, (Boolean) object);
220            } else if ((Boolean) object) {
221                return defaultTrueValue;
222            } else {
223                return defaultFalseValue;
224            }
225        } else if (object instanceof String) {
226            return new ConfigString.Quoted(origin, (String) object);
227        } else if (object instanceof Number) {
228            // here we always keep the same type that was passed to us,
229            // rather than figuring out if a Long would fit in an Int
230            // or a Double has no fractional part. i.e. deliberately
231            // not using ConfigNumber.newNumber() when we have a
232            // Double, Integer, or Long.
233            if (object instanceof Double) {
234                return new ConfigDouble(origin, (Double) object, null);
235            } else if (object instanceof Integer) {
236                return new ConfigInt(origin, (Integer) object, null);
237            } else if (object instanceof Long) {
238                return new ConfigLong(origin, (Long) object, null);
239            } else {
240                return ConfigNumber.newNumber(origin,
241                        ((Number) object).doubleValue(), null);
242            }
243        } else if (object instanceof Duration) {
244            return new ConfigLong(origin, ((Duration) object).toMillis(), null);
245        } else if (object instanceof Map) {
246            if (((Map<?, ?>) object).isEmpty())
247                return emptyObject(origin);
248
249            if (mapMode == FromMapMode.KEYS_ARE_KEYS) {
250                Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
251                for (Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
252                    Object key = entry.getKey();
253                    if (!(key instanceof String))
254                        throw new ConfigException.BugOrBroken(
255                                "bug in method caller: not valid to create ConfigObject from map with non-String key: "
256                                        + key);
257                    AbstractConfigValue value = fromAnyRef(entry.getValue(),
258                            origin, mapMode);
259                    values.put((String) key, value);
260                }
261
262                return new SimpleConfigObject(origin, values);
263            } else {
264                return PropertiesParser.fromPathMap(origin, (Map<?, ?>) object);
265            }
266        } else if (object instanceof Iterable) {
267            Iterator<?> i = ((Iterable<?>) object).iterator();
268            if (!i.hasNext())
269                return emptyList(origin);
270
271            List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>();
272            while (i.hasNext()) {
273                AbstractConfigValue v = fromAnyRef(i.next(), origin, mapMode);
274                values.add(v);
275            }
276
277            return new SimpleConfigList(origin, values);
278        } else if (object instanceof ConfigMemorySize) {
279            return new ConfigLong(origin, ((ConfigMemorySize) object).toBytes(), null);
280        } else {
281            throw new ConfigException.BugOrBroken(
282                    "bug in method caller: not valid to create ConfigValue from: "
283                            + object);
284        }
285    }
286
287    private static class DefaultIncluderHolder {
288        static final ConfigIncluder defaultIncluder = new SimpleIncluder(null);
289    }
290
291    static ConfigIncluder defaultIncluder() {
292        try {
293            return DefaultIncluderHolder.defaultIncluder;
294        } catch (ExceptionInInitializerError e) {
295            throw ConfigImplUtil.extractInitializerError(e);
296        }
297    }
298
299    private static Properties getSystemProperties() {
300        // Avoid ConcurrentModificationException due to parallel setting of system properties by copying properties
301        final Properties systemProperties = System.getProperties();
302        final Properties systemPropertiesCopy = new Properties();
303        synchronized (systemProperties) {
304            systemPropertiesCopy.putAll(systemProperties);
305        }
306        return systemPropertiesCopy;
307    }
308
309    private static AbstractConfigObject loadSystemProperties() {
310        return (AbstractConfigObject) Parseable.newProperties(getSystemProperties(),
311                ConfigParseOptions.defaults().setOriginDescription("system properties")).parse();
312    }
313
314    private static class SystemPropertiesHolder {
315        // this isn't final due to the reloadSystemPropertiesConfig() hack below
316        static volatile AbstractConfigObject systemProperties = loadSystemProperties();
317    }
318
319    static AbstractConfigObject systemPropertiesAsConfigObject() {
320        try {
321            return SystemPropertiesHolder.systemProperties;
322        } catch (ExceptionInInitializerError e) {
323            throw ConfigImplUtil.extractInitializerError(e);
324        }
325    }
326
327    public static Config systemPropertiesAsConfig() {
328        return systemPropertiesAsConfigObject().toConfig();
329    }
330
331    public static void reloadSystemPropertiesConfig() {
332        // ConfigFactory.invalidateCaches() relies on this having the side
333        // effect that it drops all caches
334        SystemPropertiesHolder.systemProperties = loadSystemProperties();
335    }
336
337    private static AbstractConfigObject loadEnvVariables() {
338        Map<String, String> env = System.getenv();
339        Map<String, AbstractConfigValue> m = new HashMap<String, AbstractConfigValue>();
340        for (Map.Entry<String, String> entry : env.entrySet()) {
341            String key = entry.getKey();
342            m.put(key,
343                    new ConfigString.Quoted(SimpleConfigOrigin.newSimple("env var " + key), entry
344                            .getValue()));
345        }
346        return new SimpleConfigObject(SimpleConfigOrigin.newSimple("env variables"),
347                m, ResolveStatus.RESOLVED, false /* ignoresFallbacks */);
348    }
349
350    private static class EnvVariablesHolder {
351        static final AbstractConfigObject envVariables = loadEnvVariables();
352    }
353
354    static AbstractConfigObject envVariablesAsConfigObject() {
355        try {
356            return EnvVariablesHolder.envVariables;
357        } catch (ExceptionInInitializerError e) {
358            throw ConfigImplUtil.extractInitializerError(e);
359        }
360    }
361
362    public static Config envVariablesAsConfig() {
363        return envVariablesAsConfigObject().toConfig();
364    }
365
366    public static Config defaultReference(final ClassLoader loader) {
367        return computeCachedConfig(loader, "defaultReference", new Callable<Config>() {
368            @Override
369            public Config call() {
370                Config unresolvedResources = Parseable
371                        .newResources("reference.conf",
372                                ConfigParseOptions.defaults().setClassLoader(loader))
373                        .parse().toConfig();
374                return systemPropertiesAsConfig().withFallback(unresolvedResources).resolve();
375            }
376        });
377    }
378
379    private static class DebugHolder {
380        private static String LOADS = "loads";
381        private static String SUBSTITUTIONS = "substitutions";
382
383        private static Map<String, Boolean> loadDiagnostics() {
384            Map<String, Boolean> result = new HashMap<String, Boolean>();
385            result.put(LOADS, false);
386            result.put(SUBSTITUTIONS, false);
387
388            // People do -Dconfig.trace=foo,bar to enable tracing of different things
389            String s = System.getProperty("config.trace");
390            if (s == null) {
391                return result;
392            } else {
393                String[] keys = s.split(",");
394                for (String k : keys) {
395                    if (k.equals(LOADS)) {
396                        result.put(LOADS, true);
397                    } else if (k.equals(SUBSTITUTIONS)) {
398                        result.put(SUBSTITUTIONS, true);
399                    } else {
400                        System.err.println("config.trace property contains unknown trace topic '"
401                                + k + "'");
402                    }
403                }
404                return result;
405            }
406        }
407
408        private static final Map<String, Boolean> diagnostics = loadDiagnostics();
409
410        private static final boolean traceLoadsEnabled = diagnostics.get(LOADS);
411        private static final boolean traceSubstitutionsEnabled = diagnostics.get(SUBSTITUTIONS);
412
413        static boolean traceLoadsEnabled() {
414            return traceLoadsEnabled;
415        }
416
417        static boolean traceSubstitutionsEnabled() {
418            return traceSubstitutionsEnabled;
419        }
420    }
421
422    public static boolean traceLoadsEnabled() {
423        try {
424            return DebugHolder.traceLoadsEnabled();
425        } catch (ExceptionInInitializerError e) {
426            throw ConfigImplUtil.extractInitializerError(e);
427        }
428    }
429
430    public static boolean traceSubstitutionsEnabled() {
431        try {
432            return DebugHolder.traceSubstitutionsEnabled();
433        } catch (ExceptionInInitializerError e) {
434            throw ConfigImplUtil.extractInitializerError(e);
435        }
436    }
437
438    public static void trace(String message) {
439        System.err.println(message);
440    }
441
442    public static void trace(int indentLevel, String message) {
443        while (indentLevel > 0) {
444            System.err.print("  ");
445            indentLevel -= 1;
446        }
447        System.err.println(message);
448    }
449
450    // the basic idea here is to add the "what" and have a canonical
451    // toplevel error message. the "original" exception may however have extra
452    // detail about what happened. call this if you have a better "what" than
453    // further down on the stack.
454    static ConfigException.NotResolved improveNotResolved(Path what,
455            ConfigException.NotResolved original) {
456        String newMessage = what.render()
457                + " has not been resolved, you need to call Config#resolve(),"
458                + " see API docs for Config#resolve()";
459        if (newMessage.equals(original.getMessage()))
460            return original;
461        else
462            return new ConfigException.NotResolved(newMessage, original);
463    }
464
465    public static ConfigOrigin newSimpleOrigin(String description) {
466        if (description == null) {
467            return defaultValueOrigin;
468        } else {
469            return SimpleConfigOrigin.newSimple(description);
470        }
471    }
472
473    public static ConfigOrigin newFileOrigin(String filename) {
474        return SimpleConfigOrigin.newFile(filename);
475    }
476
477    public static ConfigOrigin newURLOrigin(URL url) {
478        return SimpleConfigOrigin.newURL(url);
479    }
480}