/** * Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com> */ package org.deephacks.confit.internal.core.property.typesafe.impl; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FilterReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedList; import java.util.Properties; import org.deephacks.confit.internal.core.property.typesafe.ConfigException; import org.deephacks.confit.internal.core.property.typesafe.ConfigIncludeContext; import org.deephacks.confit.internal.core.property.typesafe.ConfigObject; import org.deephacks.confit.internal.core.property.typesafe.ConfigOrigin; import org.deephacks.confit.internal.core.property.typesafe.ConfigParseOptions; import org.deephacks.confit.internal.core.property.typesafe.ConfigParseable; import org.deephacks.confit.internal.core.property.typesafe.ConfigSyntax; import org.deephacks.confit.internal.core.property.typesafe.ConfigValue; /** * This is public but it's only for use by the typesafe package; DO NOT TOUCH. The * point of this class is to avoid "propagating" each overload on * "thing which can be parsed" through multiple interfaces. Most interfaces can * have just one overload that takes a Parseable. Also it's used as an abstract * "resource handle" in the ConfigIncluder interface. */ public abstract class Parseable implements ConfigParseable { private ConfigIncludeContext includeContext; private ConfigParseOptions initialOptions; private ConfigOrigin initialOrigin; private static final ThreadLocal<LinkedList<Parseable>> parseStack = new ThreadLocal<LinkedList<Parseable>>() { @Override protected LinkedList<Parseable> initialValue() { return new LinkedList<Parseable>(); } }; private static final int MAX_INCLUDE_DEPTH = 50; protected Parseable() { } private ConfigParseOptions fixupOptions(ConfigParseOptions baseOptions) { ConfigSyntax syntax = baseOptions.getSyntax(); if (syntax == null) { syntax = guessSyntax(); } if (syntax == null) { syntax = ConfigSyntax.CONF; } ConfigParseOptions modified = baseOptions.setSyntax(syntax); // make sure the app-provided includer falls back to default modified = modified.appendIncluder(ConfigImpl.defaultIncluder()); // make sure the app-provided includer is complete modified = modified.setIncluder(SimpleIncluder.makeFull(modified.getIncluder())); return modified; } protected void postConstruct(ConfigParseOptions baseOptions) { this.initialOptions = fixupOptions(baseOptions); this.includeContext = new SimpleIncludeContext(this); if (initialOptions.getOriginDescription() != null) initialOrigin = SimpleConfigOrigin.newSimple(initialOptions.getOriginDescription()); else initialOrigin = createOrigin(); } // the general idea is that any work should be in here, not in the // constructor, so that exceptions are thrown from the public parse() // function and not from the creation of the Parseable. // Essentially this is a lazy field. The parser should close the // reader when it's done with it. // ALSO, IMPORTANT: if the file or URL is not found, this must throw. // to support the "allow missing" feature. protected abstract Reader reader() throws IOException; protected static void trace(String message) { if (ConfigImpl.traceLoadsEnabled()) { ConfigImpl.trace(message); } } ConfigSyntax guessSyntax() { return null; } ConfigParseable relativeTo(String filename) { // fall back to classpath; we treat the "filename" as absolute // (don't add a package name in front), // if it starts with "/" then remove the "/", for consistency // with ParseableResources.relativeTo String resource = filename; if (filename.startsWith("/")) resource = filename.substring(1); return newResources(resource, options().setOriginDescription(null)); } ConfigIncludeContext includeContext() { return includeContext; } static AbstractConfigObject forceParsedToObject(ConfigValue value) { if (value instanceof AbstractConfigObject) { return (AbstractConfigObject) value; } else { throw new ConfigException.WrongType(value.origin(), "", "object at file root", value .valueType().name()); } } @Override public ConfigObject parse(ConfigParseOptions baseOptions) { LinkedList<Parseable> stack = parseStack.get(); if (stack.size() >= MAX_INCLUDE_DEPTH) { throw new ConfigException.Parse(initialOrigin, "include statements nested more than " + MAX_INCLUDE_DEPTH + " times, you probably have a cycle in your includes. Trace: " + stack); } stack.addFirst(this); try { return forceParsedToObject(parseValue(baseOptions)); } finally { stack.removeFirst(); if (stack.isEmpty()) { parseStack.remove(); } } } final AbstractConfigValue parseValue(ConfigParseOptions baseOptions) { // note that we are NOT using our "initialOptions", // but using the ones from the passed-in options. The idea is that // callers can lookup our original options and then parse with different // ones if they want. ConfigParseOptions options = fixupOptions(baseOptions); // passed-in options can override origin ConfigOrigin origin; if (options.getOriginDescription() != null) origin = SimpleConfigOrigin.newSimple(options.getOriginDescription()); else origin = initialOrigin; return parseValue(origin, options); } final private AbstractConfigValue parseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) { try { return rawParseValue(origin, finalOptions); } catch (IOException e) { if (finalOptions.getAllowMissing()) { return SimpleConfigObject.emptyMissing(origin); } else { trace("exception loading " + origin.description() + ": " + e.getClass().getName() + ": " + e.getMessage()); throw new ConfigException.IO(origin, e.getClass().getName() + ": " + e.getMessage(), e); } } } // this is parseValue without post-processing the IOException or handling // options.getAllowMissing() protected AbstractConfigValue rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) throws IOException { Reader reader = reader(); try { return rawParseValue(reader, origin, finalOptions); } finally { reader.close(); } } protected AbstractConfigValue rawParseValue(Reader reader, ConfigOrigin origin, ConfigParseOptions finalOptions) throws IOException { if (finalOptions.getSyntax() == ConfigSyntax.PROPERTIES) { return PropertiesParser.parse(reader, origin); } else { Iterator<Token> tokens = Tokenizer.tokenize(origin, reader, finalOptions.getSyntax()); return Parser.parse(tokens, origin, finalOptions, includeContext()); } } public ConfigObject parse() { return forceParsedToObject(parseValue(options())); } AbstractConfigValue parseValue() { return parseValue(options()); } @Override public final ConfigOrigin origin() { return initialOrigin; } protected abstract ConfigOrigin createOrigin(); @Override public ConfigParseOptions options() { return initialOptions; } @Override public String toString() { return getClass().getSimpleName(); } private static ConfigSyntax syntaxFromExtension(String name) { if (name.endsWith(".json")) return ConfigSyntax.JSON; else if (name.endsWith(".conf")) return ConfigSyntax.CONF; else if (name.endsWith(".properties")) return ConfigSyntax.PROPERTIES; else return null; } private static Reader readerFromStream(InputStream input) { try { // well, this is messed up. If we aren't going to close // the passed-in InputStream then we have no way to // close these readers. So maybe we should not have an // InputStream version, only a Reader version. Reader reader = new InputStreamReader(input, "UTF-8"); return new BufferedReader(reader); } catch (UnsupportedEncodingException e) { throw new ConfigException.BugOrBroken("Java config does not support UTF-8", e); } } private static Reader doNotClose(Reader input) { return new FilterReader(input) { @Override public void close() { // NOTHING. } }; } static URL relativeTo(URL url, String filename) { // I'm guessing this completely fails on Windows, help wanted if (new File(filename).isAbsolute()) return null; try { URI siblingURI = url.toURI(); URI relative = new URI(filename); // this seems wrong, but it's documented that the last // element of the path in siblingURI gets stripped out, // so to lookup something in the same directory as // siblingURI we just call resolve(). URL resolved = siblingURI.resolve(relative).toURL(); return resolved; } catch (MalformedURLException e) { return null; } catch (URISyntaxException e) { return null; } catch (IllegalArgumentException e) { return null; } } static File relativeTo(File file, String filename) { File child = new File(filename); if (child.isAbsolute()) return null; File parent = file.getParentFile(); if (parent == null) return null; else return new File(parent, filename); } // this is a parseable that doesn't exist and just throws when you try to // parse it private final static class ParseableNotFound extends Parseable { final private String what; final private String message; ParseableNotFound(String what, String message, ConfigParseOptions options) { this.what = what; this.message = message; postConstruct(options); } @Override protected Reader reader() throws IOException { throw new FileNotFoundException(message); } @Override protected ConfigOrigin createOrigin() { return SimpleConfigOrigin.newSimple(what); } } public static Parseable newNotFound(String whatNotFound, String message, ConfigParseOptions options) { return new ParseableNotFound(whatNotFound, message, options); } private final static class ParseableReader extends Parseable { final private Reader reader; ParseableReader(Reader reader, ConfigParseOptions options) { this.reader = reader; postConstruct(options); } @Override protected Reader reader() { if (ConfigImpl.traceLoadsEnabled()) trace("Loading typesafe from reader " + reader); return reader; } @Override protected ConfigOrigin createOrigin() { return SimpleConfigOrigin.newSimple("Reader"); } } /** * note that we will never close this reader; you have to do it when parsing * is complete. */ public static Parseable newReader(Reader reader, ConfigParseOptions options) { return new ParseableReader(doNotClose(reader), options); } private final static class ParseableString extends Parseable { final private String input; ParseableString(String input, ConfigParseOptions options) { this.input = input; postConstruct(options); } @Override protected Reader reader() { if (ConfigImpl.traceLoadsEnabled()) trace("Loading typesafe from a String " + input); return new StringReader(input); } @Override protected ConfigOrigin createOrigin() { return SimpleConfigOrigin.newSimple("String"); } @Override public String toString() { return getClass().getSimpleName() + "(" + input + ")"; } } public static Parseable newString(String input, ConfigParseOptions options) { return new ParseableString(input, options); } private final static class ParseableURL extends Parseable { final private URL input; ParseableURL(URL input, ConfigParseOptions options) { this.input = input; postConstruct(options); } @Override protected Reader reader() throws IOException { if (ConfigImpl.traceLoadsEnabled()) trace("Loading typesafe from a URL: " + input.toExternalForm()); InputStream stream = input.openStream(); return readerFromStream(stream); } @Override ConfigSyntax guessSyntax() { return syntaxFromExtension(input.getPath()); } @Override ConfigParseable relativeTo(String filename) { URL url = relativeTo(input, filename); if (url == null) return null; return newURL(url, options().setOriginDescription(null)); } @Override protected ConfigOrigin createOrigin() { return SimpleConfigOrigin.newURL(input); } @Override public String toString() { return getClass().getSimpleName() + "(" + input.toExternalForm() + ")"; } } public static Parseable newURL(URL input, ConfigParseOptions options) { // we want file: URLs and files to always behave the same, so switch // to a file if it's a file: URL if (input.getProtocol().equals("file")) { return newFile(ConfigImplUtil.urlToFile(input), options); } else { return new ParseableURL(input, options); } } private final static class ParseableFile extends Parseable { final private File input; ParseableFile(File input, ConfigParseOptions options) { this.input = input; postConstruct(options); } @Override protected Reader reader() throws IOException { if (ConfigImpl.traceLoadsEnabled()) trace("Loading typesafe from a file: " + input); InputStream stream = new FileInputStream(input); return readerFromStream(stream); } @Override ConfigSyntax guessSyntax() { return syntaxFromExtension(input.getName()); } @Override ConfigParseable relativeTo(String filename) { File sibling; if ((new File(filename)).isAbsolute()) { sibling = new File(filename); } else { // this may return null sibling = relativeTo(input, filename); } if (sibling == null) return null; if (sibling.exists()) { return newFile(sibling, options().setOriginDescription(null)); } else { return super.relativeTo(filename); } } @Override protected ConfigOrigin createOrigin() { return SimpleConfigOrigin.newFile(input.getPath()); } @Override public String toString() { return getClass().getSimpleName() + "(" + input.getPath() + ")"; } } public static Parseable newFile(File input, ConfigParseOptions options) { return new ParseableFile(input, options); } private final static class ParseableResources extends Parseable { final private String resource; ParseableResources(String resource, ConfigParseOptions options) { this.resource = resource; postConstruct(options); } @Override protected Reader reader() throws IOException { throw new ConfigException.BugOrBroken("reader() should not be called on resources"); } @Override protected AbstractConfigObject rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) throws IOException { ClassLoader loader = finalOptions.getClassLoader(); Enumeration<URL> e = loader.getResources(resource); if (!e.hasMoreElements()) { if (ConfigImpl.traceLoadsEnabled()) trace("Loading typesafe from class loader " + loader + " but there were no resources called " + resource); throw new IOException("resource not found on classpath: " + resource); } AbstractConfigObject merged = SimpleConfigObject.empty(origin); while (e.hasMoreElements()) { URL url = e.nextElement(); if (ConfigImpl.traceLoadsEnabled()) trace("Loading typesafe from URL " + url.toExternalForm() + " from class loader " + loader); ConfigOrigin elementOrigin = ((SimpleConfigOrigin) origin).addURL(url); AbstractConfigValue v; // it's tempting to use ParseableURL here but it would be wrong // because the wrong relativeTo() would be used for includes. InputStream stream = url.openStream(); try { Reader reader = readerFromStream(stream); stream = null; // reader now owns it try { // parse in "raw" mode which will throw any IOException // from here. v = rawParseValue(reader, elementOrigin, finalOptions); } finally { reader.close(); } } finally { // stream is null if the reader owns it if (stream != null) stream.close(); } merged = merged.withFallback(v); } return merged; } @Override ConfigSyntax guessSyntax() { return syntaxFromExtension(resource); } static String parent(String resource) { // the "resource" is not supposed to begin with a "/" // because it's supposed to be the raw resource // (ClassLoader#getResource), not the // resource "syntax" (Class#getResource) int i = resource.lastIndexOf('/'); if (i < 0) { return null; } else { return resource.substring(0, i); } } @Override ConfigParseable relativeTo(String sibling) { if (sibling.startsWith("/")) { // if it starts with "/" then don't make it relative to // the including resource return newResources(sibling.substring(1), options().setOriginDescription(null)); } else { // here we want to build a new resource name and let // the class loader have it, rather than getting the // url with getResource() and relativizing to that url. // This is needed in case the class loader is going to // search a classpath. String parent = parent(resource); if (parent == null) return newResources(sibling, options().setOriginDescription(null)); else return newResources(parent + "/" + sibling, options() .setOriginDescription(null)); } } @Override protected ConfigOrigin createOrigin() { return SimpleConfigOrigin.newResource(resource); } @Override public String toString() { return getClass().getSimpleName() + "(" + resource + ")"; } } public static Parseable newResources(Class<?> klass, String resource, ConfigParseOptions options) { return newResources(convertResourceName(klass, resource), options.setClassLoader(klass.getClassLoader())); } // this function is supposed to emulate the difference // between Class.getResource and ClassLoader.getResource // (unfortunately there doesn't seem to be public API for it). // We're using it because the Class API is more limited, // for example it lacks getResources(). So we want to be able to // use ClassLoader directly. private static String convertResourceName(Class<?> klass, String resource) { if (resource.startsWith("/")) { // "absolute" resource, chop the slash return resource.substring(1); } else { String className = klass.getName(); int i = className.lastIndexOf('.'); if (i < 0) { // no package return resource; } else { // need to be relative to the package String packageName = className.substring(0, i); String packagePath = packageName.replace('.', '/'); return packagePath + "/" + resource; } } } public static Parseable newResources(String resource, ConfigParseOptions options) { return new ParseableResources(resource, options); } private final static class ParseableProperties extends Parseable { final private Properties props; ParseableProperties(Properties props, ConfigParseOptions options) { this.props = props; postConstruct(options); } @Override protected Reader reader() throws IOException { throw new ConfigException.BugOrBroken("reader() should not be called on props"); } @Override protected AbstractConfigObject rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions) { if (ConfigImpl.traceLoadsEnabled()) trace("Loading typesafe from properties " + props); return PropertiesParser.fromProperties(origin, props); } @Override ConfigSyntax guessSyntax() { return ConfigSyntax.PROPERTIES; } @Override protected ConfigOrigin createOrigin() { return SimpleConfigOrigin.newSimple("properties"); } @Override public String toString() { return getClass().getSimpleName() + "(" + props.size() + " props)"; } } public static Parseable newProperties(Properties properties, ConfigParseOptions options) { return new ParseableProperties(properties, options); } }