org.apache.shindig.social.opensocial.util.ApiValidator.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.shindig.social.opensocial.util.ApiValidator.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.social.opensocial.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * A class that loads a feature set from features into a Javascript Parser to
 * make the model available to validate JSON messages against.
 */
public class ApiValidator {

    private static final Log log = LogFactory.getLog(ApiValidator.class);
    private Context ctx;
    private ScriptableObject scope;

    /**
     * @param feature
     *                The name of the feature are eg "opensocial-reference", this
     *                is a classpath stub (not starting with /) where the location
     *                contains a feature.xml file.
     * @throws SAXException
     *                 if feature.xml is not parsable
     * @throws ParserConfigurationException
     *                 if the parsers are invalid
     * @throws IOException
     *                 if feature.xml or the javascript that represents the
     *                 feature is missing
     *
     */
    private ApiValidator(String feature) throws IOException, ParserConfigurationException, SAXException {
        ctx = Context.enter();
        scope = ctx.initStandardObjects();
        load(feature);
    }

    /**
     * Load the ApiValidator with no features, this avoids having features in the classpath
     * @throws IOException
     */
    public ApiValidator() throws IOException {
        ctx = Context.enter();
        scope = ctx.initStandardObjects();
    }

    /**
     * @param json
     *                The json to validate expected in a form { xyz: yyy } form
     * @param object
     *                The json Fields object specifying the structure of the json
     *                object, each field in this object contains the name of the
     *                json field in the json structure.
     * @param optionalFields
     *                If any of the fields that appear in the json structure are
     *                optional, then they should be defined in this parameter.
     * @param nullfields
     * @throws ApiValidatorExpcetion
     *                 if there is a problem validating the json
     * @return a map so string object pairs containing the fields at the top level
     *         of the json tree. Where these are native java objects, they will
     *         appear as native object. Complex json objects will appear as Rhino
     *         specific objects
     */
    public Map<String, Object> validate(String json, String object, String[] optionalFields, String[] nullfields)
            throws ApiValidatorExpcetion {

        /*
         * Object[] ids = ScriptableObject.getPropertyIds(scope); for (Object id :
         * ids) { Object o = ScriptableObject.getProperty(scope,
         * String.valueOf(id)); log.debug("ID is " + id + " class " + id.getClass() + "
         * is " + o); if (o instanceof ScriptableObject) {
         * listScriptable(String.valueOf(id), (ScriptableObject) o); } }
         */

        log.debug("Loading " + json);
        json = json.trim();
        if (!json.endsWith("}")) {
            json = json + '}';
        }
        if (!json.startsWith("{")) {
            json = '{' + json;
        }
        json = "( testingObject = " + json + " )";

        Object so = null;
        try {
            so = ctx.evaluateString(scope, json, "test json", 0, null);
        } catch (EvaluatorException ex) {
            log.error("Non parseable JSON " + json);
        }
        log.debug("Loaded " + so);

        ScriptableObject specification = getScriptableObject(object);
        log.debug("Looking for  " + object + " found " + specification);
        listScriptable(object, specification);
        Object[] fields = specification.getIds();
        String[] fieldNames = new String[fields.length];
        for (int i = 0; i < fields.length; i++) {
            Object fieldName = specification.get(String.valueOf(fields[i]), specification);
            fieldNames[i] = String.valueOf(fieldName);
        }

        return validateObject(so, fieldNames, optionalFields, nullfields);

    }

    /**
     * @param json
     *                The json to validate expected in a form { xyz: yyy } form
     * @param fieldNames
     *                An Array of field names that the oject should be tested against
     * @param optionalFields
     *                If any of the fields that appear in the json structure are
     *                optional, then they should be defined in this parameter.
     * @param nullfields
     * @throws ApiValidatorExpcetion
     *                 if there is a problem validating the json
     * @return a map so string object pairs containing the fields at the top level
     *         of the json tree. Where these are native java objects, they will
     *         appear as native object. Complex json objects will appear as Rhino
     *         specific objects
     */
    public Map<String, Object> validate(String json, String[] fieldNames, String[] optionalFields,
            String[] nullfields) throws ApiValidatorExpcetion {

        log.debug("Loading " + json);
        json = json.trim();
        if (!json.endsWith("}")) {
            json = json + '}';
        }
        if (!json.startsWith("{")) {
            json = '{' + json;
        }
        json = "( testingObject = " + json + " )";

        Object so = null;
        try {
            so = ctx.evaluateString(scope, json, "test json", 0, null);
        } catch (EvaluatorException ex) {
            log.error("Non parseable JSON " + json);
        }
        log.debug("Loaded " + so);

        return validateObject(so, fieldNames, optionalFields, nullfields);

    }

    /**
     * Validate an JSON Object extracted
     *
     * @param object
     * @param string
     * @param optional
     * @return
     * @throws ApiValidatorExpcetion
     */
    public Map<String, Object> validateObject(Object jsonObject, String[] fieldNames, String[] optionalFields,
            String[] nullFields) throws ApiValidatorExpcetion {
        Map<String, String> optional = Maps.newHashMap();
        for (String opt : optionalFields) {
            optional.put(opt, opt);
        }
        Map<String, String> nullf = Maps.newHashMap();
        for (String nf : nullFields) {
            nullf.put(nf, nf);
        }

        Map<String, Object> resultFields = Maps.newHashMap();

        if (jsonObject instanceof ScriptableObject) {
            ScriptableObject parsedJSONObject = (ScriptableObject) jsonObject;
            listScriptable("testingObject", parsedJSONObject);
            for (String fieldName : fieldNames) {
                Object o = parsedJSONObject.get(fieldName, parsedJSONObject);
                if (o == Scriptable.NOT_FOUND) {
                    if (optional.containsKey(fieldName)) {
                        log.warn("Missing Optional Field " + fieldName);
                    } else if (!nullf.containsKey(fieldName)) {
                        log.error("Missing Field " + fieldName);
                        throw new ApiValidatorExpcetion("Missing Field " + fieldName);
                    }
                } else {
                    if (nullf.containsKey(fieldName)) {
                        log.error("Field should have been null and was not");
                    }
                    if (o == null) {
                        if (nullf.containsKey(fieldName)) {
                            log.error("Null Fields has been serialized " + fieldName);
                        }
                        log.debug("Got a Null object for Field " + fieldName + " on json [[" + jsonObject + "]]");

                    } else {

                        log.debug("Got JSON Field  Field," + fieldName + " as " + o + ' ' + o.getClass());
                    }
                    resultFields.put(String.valueOf(fieldName), o);
                }
            }

        } else {
            throw new ApiValidatorExpcetion("Parsing JSON resulted in invalid Javascript object, which was "
                    + jsonObject + " JSON was [[" + jsonObject + "]]");
        }
        return resultFields;
    }

    /**
     * get an object from the json context and scope.
     *
     * @param object
     *                the name of the object specified as a path from the base
     *                object
     * @return the json object
     */
    private ScriptableObject getScriptableObject(String object) {
        String[] path = object.split("\\.");
        log.debug("Looking up " + object + " elements " + path.length);

        ScriptableObject s = scope;
        for (String pe : path) {
            log.debug("Looking up " + pe + " in " + s);
            s = (ScriptableObject) s.get(pe, s);
            log.debug("Looking for " + pe + " in found " + s);
        }
        return s;
    }

    /**
     * List a scriptable object at log debug level, constructors will not be
     * expanded as this loads to recursion.
     *
     * @param id
     *                The name of the object
     * @param scriptableObject
     *                the scriptable Object
     */
    private void listScriptable(String id, ScriptableObject scriptableObject) {
        log.debug("ID is Scriptable " + id);
        if (!id.endsWith("constructor")) {
            Object[] allIDs = scriptableObject.getAllIds();
            for (Object oid : allIDs) {
                log.debug(id + '.' + oid);
                Object o = scriptableObject.get(String.valueOf(oid), scriptableObject);
                if (o instanceof ScriptableObject) {
                    listScriptable(id + '.' + String.valueOf(oid), (ScriptableObject) o);
                }
            }
        }
    }

    /**
     * Load a feature based on the spec
     *
     * @param spec
     *                The name of the location of the spec in the classpath, must
     *                not start with a '/' and must should contain a feature.xml
     *                file in the location
     * @throws IOException
     *                 If any of the resources cant be found
     * @throws ParserConfigurationException
     *                 If the parser has a problem being constructed
     * @throws SAXException
     *                 on a parse error on the features.xml
     */
    private void load(String spec) throws IOException, SAXException, ParserConfigurationException {

        List<String> scripts = getScripts(spec);

        List<Script> compiled = Lists.newArrayList();
        for (String script : scripts) {
            String scriptPath = spec + '/' + script;
            InputStream in = this.getClass().getClassLoader().getResourceAsStream(scriptPath);
            if (in == null) {
                in = this.getClass().getClassLoader().getResourceAsStream("features/" + scriptPath);
                if (in == null) {
                    throw new IOException("Cant load spec " + spec + " or features/" + spec + " from classpath");
                }
            }
            InputStreamReader reader = new InputStreamReader(in);
            Script compiledScript = ctx.compileReader(reader, spec, 0, null);
            compiled.add(compiledScript);
        }

        for (Script compiledScript : compiled) {
            compiledScript.exec(ctx, scope);
        }

    }

    /**
     * Add some javascript to the context, and execute it. If extra custom
     * javascript is wanted in the context or scope then this method will load it.
     *
     * @param javascript
     */
    public void addScript(String javascript) {
        Script compileScript = ctx.compileString(javascript, "AdditionalJS", 0, null);
        compileScript.exec(ctx, scope);
    }

    /**
     * Get an ordered list of javascript resources from a feature sets.
     *
     * @param spec
     *                The spec location
     * @return An ordered list of javascript resources, these are relative to
     *         specification file.
     * @throws IOException
     *                 If any of the resources can't be loaded.
     * @throws SAXException
     *                 Where the feature.xml file is not parsable
     * @throws ParserConfigurationException
     *                 where the parser can't be constructed.
     * @return An ordered list of script that need to be loaded and executed to
     *         make the feature available in the context.
     */
    private List<String> getScripts(String spec) throws SAXException, IOException, ParserConfigurationException {
        String features = spec + "/feature.xml";
        InputStream in = this.getClass().getClassLoader().getResourceAsStream(features);
        if (in == null) {
            in = this.getClass().getClassLoader().getResourceAsStream("features/" + features);
            if (in == null) {
                throw new IOException("Cant find " + features + " or features/" + features + " in classpath ");
            }
        }
        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
        Document doc = documentBuilder.parse(in);
        NodeList nl = doc.getElementsByTagName("script");
        List<String> scripts = Lists.newArrayList();
        for (int i = 0; i < nl.getLength(); i++) {
            Node scriptNode = nl.item(i);
            NamedNodeMap attributes = scriptNode.getAttributes();
            Node scriptAttr = attributes.getNamedItem("src");
            String script = scriptAttr.getNodeValue();
            scripts.add(script);
        }
        return scripts;
    }

    /**
     * @param nameJSON
     */
    public static void dump(Map<?, ?> nameJSON) {
        if (log.isDebugEnabled()) {
            for (Entry<?, ?> entry : nameJSON.entrySet()) {
                Object k = entry.getKey();
                Object o = entry.getValue();
                log.info("Key [" + k + "] value:[" + (o == null ? "null" : o + ":" + o.getClass()) + ']');
            }
        }
    }

}