RhinoProperties.java

package org.mozilla.javascript.config;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.ServiceLoader;

/**
 * Utility class to read current rhino configuration from various properties.
 *
 * <p>Rhino properties typically begins with "rhino." (properties) or "RHINO_" (env)
 *
 * <p>You can override this behaviour and implement a {@link RhinoPropertiesLoader} and register it
 * as service. If no loader was found, the configuration is read from these locations by default:
 *
 * <ol>
 *   <li>rhino.config file from current class' classpath
 *   <li>rhino.config file from current threas's classpath
 *   <li>rhino.config file from current directory
 *   <li>rhino-test.config file from current class' classpath
 *   <li>rhino-test.config file from current threas's classpath
 *   <li>rhino-test.config file from current directory
 *   <li>env variables starting with "RHINO_" (underscores are replaced by '.' and string is
 *   <li>System-properties starting with "rhino."
 * </ol>
 *
 * <p>(the later config can override previous ones)
 *
 * <p>The config files are in UTF-8 format and all keys in this configuration are case-insensitive
 * and dot/underscore-insensitive.
 *
 * <p>This means, "rhino.use_java_policy_security=true" is equvalent to
 * "RHINO_USE_JAVA_POLICY_SECURITY=TRUE"
 *
 * @author Roland Praml, Foconis Analytics GmbH
 */
public class RhinoProperties {

    private static String[] CONFIG_FILES = {"rhino.config", "rhino-test.config"};
    private List<Map<?, ?>> configs = new ArrayList<>();
    // this allows debugging via system property "rhino.debugConfig"
    private final boolean debug = Boolean.getBoolean("rhino.debugConfig");

    /**
     * Initializes the default, used in RhinoConfig. If there is RhinoPropertiesLoader present, then
     * this loader has full control. Ohterwise, properties are loaded from default locations.
     */
    static RhinoProperties init() {
        RhinoProperties props = new RhinoProperties();
        Iterator<RhinoPropertiesLoader> loader =
                ServiceLoader.load(RhinoPropertiesLoader.class).iterator();
        if (loader.hasNext()) {
            while (loader.hasNext()) {
                RhinoPropertiesLoader next = loader.next();
                props.logDebug("Using loader %s", next.getClass().getName());
                next.load(props);
            }
        } else {
            props.logDebug("No loader found. Loading defaults");
            props.loadDefaults();
        }
        return props;
    }

    /** Load properties from the default locations. */
    public void loadDefaults() {
        ClassLoader classLoader = RhinoProperties.class.getClassLoader();
        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

        for (String configFile : CONFIG_FILES) {
            loadFromClasspath(classLoader, configFile);
            loadFromClasspath(contextClassLoader, configFile);
            loadFromFile(new File(configFile));
        }
        logDebug("loading configuration from System.getEnv()");
        addConfig(System.getenv());

        logDebug("Rhino: loading configuration from System.getProperties()");
        addConfig(System.getProperties());
    }

    /** Loads the configuration from the given file. */
    public void loadFromFile(File config) {
        if (!config.exists()) {
            return;
        }

        logDebug("loading configuration from %s", config.getAbsoluteFile());
        try (InputStream in = new FileInputStream(config)) {
            Properties props = new Properties();
            props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
            addConfig(props);
        } catch (IOException e) {
            logError(
                    "Error loading configuration from %s: %s",
                    config.getAbsoluteFile(), e.getMessage());
        }
    }

    /** Loads the configuration from classpath. */
    public void loadFromClasspath(ClassLoader cl, String location) {
        if (cl != null) {
            loadFromResource(cl.getResource(location));
        }
    }

    /** Loads the configuration from the given resource. */
    public void loadFromResource(URL resource) {
        if (resource == null) {
            return;
        }

        logDebug("Rhino: loading configuration from %s", resource);
        try (InputStream in = resource.openStream()) {
            Properties props = new Properties();
            props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
            addConfig(props);
        } catch (IOException e) {
            logError("Error loading configuration from %s: %s", resource, e.getMessage());
        }
    }

    /** Adds a config map. Later added configs are overriding previous ones */
    public void addConfig(Map<?, ?> config) {
        logDebug("added %d values", config.size());
        configs.add(0, config);
    }

    /**
     * Tries to find the property in the maps. It tries the property first, then it tries the camel
     * upper version.
     */
    public Object get(String property) {
        Objects.requireNonNull(property, "property must not be null");
        for (Map<?, ?> map : configs) {
            String key = property;
            for (int i = 0; i < 2; i++) {
                Object ret = map.get(key);
                if (ret != null) {
                    logDebug("get(%s)=%s", key, ret);
                    return ret;
                }
                key = toCamelUpper(property);
            }
        }
        return null;
    }

    /** converts camelCaseStrings like "rhino.printICode" to "RHINO_PRINT_ICODE". */
    private String toCamelUpper(String property) {
        String s = property.replace('.', '_');
        StringBuilder sb = new StringBuilder(s.length() + 5);
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            if (i > 0 && Character.isUpperCase(c) && Character.isLowerCase(s.charAt(i - 1))) {
                sb.append('_');
            }
            sb.append(Character.toUpperCase(c));
        }
        return sb.toString();
    }

    private void logDebug(String msg, Object... args) {
        if (!debug) {
            return;
        }
        System.out.println("[Rhino] " + String.format(msg, args));
    }

    private void logError(String msg, Object... args) {
        System.err.println("[Rhino] " + String.format(msg, args));
    }
}