org.apache.shindig.config.JsonContainerConfig.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.shindig.config.JsonContainerConfig.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.shindig.config;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;

import org.apache.commons.lang.StringUtils;
import org.apache.shindig.common.JsonSerializer;
import org.apache.shindig.common.util.ResourceLoader;
import org.apache.shindig.expressions.Expressions;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

// Temporary replacement of javax.annotation.Nullable
import org.apache.shindig.common.Nullable;

import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ValueExpression;

/**
 * Represents a container configuration using JSON notation.
 *
 * See config/container.js for an example configuration.
 *
 * We use a cascading model, so you only have to specify attributes in
 * your config that you actually want to change.
 *
 * String values may use expressions. The variable context defaults to the 'current' container,
 * but parent values may be accessed through the special "parent" property.
 */
@Singleton
public class JsonContainerConfig extends AbstractContainerConfig {
    private static final Logger LOG = Logger.getLogger(JsonContainerConfig.class.getName());
    public static final char FILE_SEPARATOR = ',';
    public static final String PARENT_KEY = "parent";
    // TODO: Rename this to simply "container", gadgets.container is unnecessary.
    public static final String CONTAINER_KEY = "gadgets.container";
    public static final String SERVER_PORT = "SERVER_PORT";
    public static final String SERVER_HOST = "SERVER_HOST";

    private final Map<String, Map<String, Object>> config;
    private final Expressions expressions;

    private static final Pattern CRLF_PATTERN = Pattern.compile("[\r\n]+");

    private final String host;
    private final String port;

    // Used by tests
    public JsonContainerConfig(String containers, Expressions expressions) throws ContainerConfigException {
        this(containers, "localhost", "8080", expressions);
    }

    /**
     * Creates a new configuration from files.
     * @throws ContainerConfigException
     */
    @Inject
    public JsonContainerConfig(@Named("shindig.containers.default") String containers,
            @Nullable @Named("shindig.host") String host, @Nullable @Named("shindig.port") String port,
            Expressions expressions) throws ContainerConfigException {
        this.expressions = expressions;
        this.host = host;
        this.port = port;

        JSONObject configJson = loadContainers(containers);
        config = createContainers(configJson);
        init();
    }

    /**
     * Creates a new configuration from a JSON Object, for use in testing.
     */
    public JsonContainerConfig(JSONObject json, Expressions expressions) {
        this.expressions = expressions;
        host = null;
        port = null;
        config = createContainers(json);
        init();
    }

    /**
     * Initializes the configuration.  Called during construction.
     */
    protected void init() {
        for (Map.Entry<String, Map<String, Object>> configEntry : config.entrySet()) {
            @SuppressWarnings("unchecked")
            Map<String, Object> value = (Map<String, Object>) evaluateAll(configEntry.getValue());
            configEntry.setValue(value);
        }
    }

    @Override
    public Collection<String> getContainers() {
        return Collections.unmodifiableSet(config.keySet());
    }

    @Override
    public Map<String, Object> getProperties(String container) {
        return config.get(container);
    }

    @Override
    public Object getProperty(String container, String property) {
        if (property.startsWith("${")) {
            // An expression!
            try {
                ValueExpression expression = expressions.parse(property, Object.class);
                return expression.getValue(createExpressionContext(container));
            } catch (ELException e) {
                return null;
            }
        }

        Map<String, Object> containerData = config.get(container);
        if (containerData == null) {
            return null;
        }
        return containerData.get(property);
    }

    /**
     * Initialize each container's configuration.
     */
    private Map<String, Map<String, Object>> createContainers(JSONObject json) {
        Map<String, Map<String, Object>> map = Maps.newHashMap();
        for (String container : JSONObject.getNames(json)) {
            ELContext context = createExpressionContext(container);
            map.put(container, jsonToMap(json.optJSONObject(container), expressions, context));
        }

        return map;
    }

    /**
     * Make Expressions available to subclasses so they can create ELContexts
     */
    protected Expressions getExpressions() {
        return expressions;
    }

    /**
     * Protected to allow overriding.
     */
    protected ELContext createExpressionContext(String container) {
        return getExpressions().newELContext(new ContainerConfigELResolver(this, container));
    }

    /**
     * Convert a JSON value to a configuration value.
     */
    private static Object jsonToConfig(Object json, Expressions expressions, ELContext context) {
        if (JSONObject.NULL.equals(json)) {
            return null;
        } else if (json instanceof CharSequence) {
            return new DynamicConfigProperty(json.toString(), expressions, context);
        } else if (json instanceof JSONArray) {
            JSONArray jsonArray = (JSONArray) json;
            List<Object> values = new ArrayList<Object>(jsonArray.length());
            for (int i = 0, j = jsonArray.length(); i < j; ++i) {
                values.add(jsonToConfig(jsonArray.opt(i), expressions, context));
            }
            return Collections.unmodifiableList(values);
        } else if (json instanceof JSONObject) {
            return jsonToMap((JSONObject) json, expressions, context);
        }

        // A (boxed) primitive.
        return json;
    }

    private static Map<String, Object> jsonToMap(JSONObject json, Expressions expressions, ELContext context) {
        Map<String, Object> values = new HashMap<String, Object>(json.length(), 1);
        for (String key : JSONObject.getNames(json)) {
            Object val = jsonToConfig(json.opt(key), expressions, context);
            if (val != null) {
                values.put(key, val);
            }
        }
        return Collections.unmodifiableMap(values);
    }

    /**
     * Loads containers from directories recursively.
     *
     * Only files with a .js or .json extension will be loaded.
     *
     * @param files The files to examine.
     * @throws ContainerConfigException
     */
    private void loadFiles(File[] files, JSONObject all) throws ContainerConfigException {
        for (File file : files) {
            try {
                if (file == null)
                    continue;
                LOG.info("Reading container config: " + file.getName());
                if (file.isDirectory()) {
                    loadFiles(file.listFiles(), all);
                } else if (file.getName().toLowerCase(Locale.ENGLISH).endsWith(".js")
                        || file.getName().toLowerCase(Locale.ENGLISH).endsWith(".json")) {
                    if (!file.exists()) {
                        throw new ContainerConfigException(
                                "The file '" + file.getAbsolutePath() + "' doesn't exist.");
                    }
                    loadFromString(ResourceLoader.getContent(file), all);
                } else {
                    if (LOG.isLoggable(Level.FINEST))
                        LOG.finest(file.getAbsolutePath() + " doesn't seem to be a JS or JSON file.");
                }
            } catch (IOException e) {
                throw new ContainerConfigException("The file '" + file.getAbsolutePath() + "' has errors", e);
            }
        }
    }

    /**
     * Loads resources recursively.
     * @param files The base paths to look for container.xml
     * @throws ContainerConfigException
     */
    private void loadResources(String[] files, JSONObject all) throws ContainerConfigException {
        try {
            for (String entry : files) {
                LOG.info("Reading container config: " + entry);
                String content = ResourceLoader.getContent(entry);
                if (content == null || content.length() == 0)
                    throw new IOException("The file " + entry + "is empty");
                loadFromString(content, all);
            }
        } catch (IOException e) {
            throw new ContainerConfigException(e);
        }
    }

    /**
     * Merges two JSON objects together (recursively), with values from "merge"
     * replacing values in "base" to produce a new object.
     *
     * @param base The base object that values will be replaced into.
     * @param merge The object to merge values from.
     *
     * @throws org.json.JSONException if the two objects can't be merged for some reason.
     */
    private JSONObject mergeObjects(JSONObject base, JSONObject merge) throws JSONException {
        // Clone the initial object (JSONObject doesn't support "clone").

        JSONObject clone = new JSONObject(base, JSONObject.getNames(base));
        // Walk parameter list for the merged object and merge recursively.
        String[] fields = JSONObject.getNames(merge);
        for (String field : fields) {
            Object existing = clone.opt(field);
            Object update = merge.get(field);
            if (JSONObject.NULL.equals(existing) || JSONObject.NULL.equals(update)) {
                // It's new custom config, not referenced in the prototype, or
                // it's removing a pre-configured value.
                clone.put(field, update);
            } else {
                // Merge if object type is JSONObject.
                if (update instanceof JSONObject && existing instanceof JSONObject) {
                    clone.put(field, mergeObjects((JSONObject) existing, (JSONObject) update));
                } else {
                    // Otherwise we just overwrite it.
                    clone.put(field, update);
                }
            }
        }
        return clone;
    }

    /**
     * Recursively merge values from parent objects in the prototype chain.
     *
     * @return The object merged with all parents.
     *
     * @throws ContainerConfigException If there is an invalid parent parameter
     *    in the prototype chain.
     */
    private JSONObject mergeParents(String container, JSONObject all)
            throws ContainerConfigException, JSONException {
        JSONObject base = all.getJSONObject(container);
        if (DEFAULT_CONTAINER.equals(container)) {
            return base;
        }

        String parent = base.optString(PARENT_KEY, DEFAULT_CONTAINER);
        if (!all.has(parent)) {
            throw new ContainerConfigException(
                    "Unable to locate parent '" + parent + "' required by " + base.getString(CONTAINER_KEY));
        }
        return mergeObjects(mergeParents(parent, all), base);
    }

    /**
     * Processes a container file.
     *
     * @param json
     * @throws ContainerConfigException
     */
    protected void loadFromString(String json, JSONObject all) throws ContainerConfigException {
        try {
            JSONObject contents = new JSONObject(json);
            JSONArray containers = contents.getJSONArray(CONTAINER_KEY);

            for (int i = 0, j = containers.length(); i < j; ++i) {
                // Copy the default object and produce a new one.
                String container = containers.getString(i);
                all.put(container, contents);
            }
        } catch (JSONException e) {
            LOG.warning("Trouble parsing " + json);
            throw new ContainerConfigException("Trouble parsing " + json, e);
        }
    }

    /**
     * Loads containers from the specified resource. Follows the same rules
     * as {@code JsFeatureLoader.loadFeatures} for locating resources.
     *
     * @param path
     * @throws ContainerConfigException
     */
    private JSONObject loadContainers(String path) throws ContainerConfigException {
        JSONObject all = new JSONObject();
        try {
            for (String location : StringUtils.split(path, FILE_SEPARATOR)) {
                if (location.startsWith("res://")) {
                    location = location.substring(6);
                    LOG.info("Loading resources from: " + location);
                    if (path.endsWith(".txt")) {
                        loadResources(CRLF_PATTERN.split(ResourceLoader.getContent(location)), all);
                    } else {
                        loadResources(new String[] { location }, all);
                    }
                } else {
                    LOG.info("Loading files from: " + location);
                    File file = new File(location);
                    loadFiles(new File[] { file }, all);
                }
            }

            // Add {@code SERVER_HOST} and {@code SERVER_PORT} environment to default
            // container.
            try {
                all.getJSONObject(DEFAULT_CONTAINER).put(SERVER_PORT, port);
                all.getJSONObject(DEFAULT_CONTAINER).put(SERVER_HOST, host);
            } catch (JSONException e) {
                // ignore
            }

            // Now that all containers are loaded, we go back through them and merge
            // recursively. This is done at startup to simplify lookups.
            for (String container : JSONObject.getNames(all)) {
                all.put(container, mergeParents(container, all));
            }

            return all;
        } catch (IOException e) {
            throw new ContainerConfigException(e);
        } catch (JSONException e) {
            throw new ContainerConfigException(e);
        }
    }

    @Override
    public String toString() {
        return JsonSerializer.serialize(config);
    }

    private Object evaluateAll(Object value) {
        if (value instanceof CharSequence) {
            return value.toString();
        } else if (value instanceof Map<?, ?>) {
            Map<?, ?> mapValue = (Map<?, ?>) value;
            ImmutableMap.Builder<Object, Object> newMap = ImmutableMap.builder();
            for (Map.Entry<?, ?> entry : mapValue.entrySet()) {
                newMap.put(entry.getKey(), evaluateAll(entry.getValue()));
            }

            return newMap.build();
        } else if (value instanceof List<?>) {
            ImmutableList.Builder<Object> newList = ImmutableList.builder();
            for (Object entry : (List<?>) value) {
                newList.add(evaluateAll(entry));
            }

            return newList.build();
        } else {
            return value;
        }
    }
}