it.okkam.exhibit.JSONSerializer.java Source code

Java tutorial

Introduction

Here is the source code for it.okkam.exhibit.JSONSerializer.java

Source

/**
 * $Id: JSONSerializer.java,v 1.2 2007/07/03 15:16:38 alimanfoo Exp $
 */
package it.okkam.exhibit;

/*
Copyright (c) 2007 Alistair Miles <http://purl.org/net/aliman>
    
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
    
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
    
The Software shall be used for Good, not Evil.
    
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.hp.hpl.jena.rdf.model.Alt;
import com.hp.hpl.jena.rdf.model.Bag;
import com.hp.hpl.jena.rdf.model.Container;
import com.hp.hpl.jena.rdf.model.Literal;
import com.hp.hpl.jena.rdf.model.NodeIterator;
import com.hp.hpl.jena.rdf.model.RDFList;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Seq;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.util.iterator.ExtendedIterator;
import com.hp.hpl.jena.vocabulary.DC;
import com.hp.hpl.jena.vocabulary.DCTerms;
import com.hp.hpl.jena.vocabulary.OWL;
import com.hp.hpl.jena.vocabulary.RDF;
import com.hp.hpl.jena.vocabulary.RDFS;
import com.hp.hpl.jena.vocabulary.XSD;

/**
 * <p>This class maps an RDF graph to JSON objects, given a node in the graph as the root of the mapping.</p>
 * 
 * <p>This implementation is very much inspired by the <a href="http://jdil.org">JSON Data Integration Language</a>,
 * although this is not a complete implementation of that specification - see the methods of the
 * {@link net.sf.conception.rdfoo.TestJSONSerializer} test case for details of this implementation.</p>
 * 
 * <p>This implementation is built using the <a href="http://jena.sourceforge.net/">Jena Semantic Web Framework</a> and the JSON for Java classes from <a href="http://json.org/java">http://json.org/java</a>.</p>
 *
 * <p>To create a new JSONSerializer, call the default constructor:</p>
 * 
 * <pre>JSONSerializer serializer = new JSONSerializer();</pre>
 * 
 * <p>By default, a JSONSerializer is <strong>namespace aware</strong>. This means it will use the full URIs
 * of the properties and resources in the graph when constructing properties and values in the JSON object graph.</p>
 * 
 * <p>To construct a JSONSerializer which is <strong>not</strong> namespace aware, call the constructor
 * with a single boolean argument:</p>
 * 
 * <pre>
JSONSerializer serializer = new JSONSerializer(false);
 * </pre>
 * 
 * <p>To register a namespace-to-prefix mapping with a namespace aware serializer, for example:</p>
 * 
 * <pre>
serializer.putNamespace("http://www.w3.org/2004/02/skos/core#","skos");
 * </pre>
 * 
 * <p>A namespace aware serializer comes with prefixes already registered for: 
 * "rdf" (RDF namespace), 
 * "rdfs" (RDFS namespace),
 * "owl" (OWL namespace),
 * "xsd" (XML Schema Datatypes namespace), 
 * "dc" (Dublin Core Elements namespace),
 * "dcterms" (Dublin Core Terms namespace). </p>
 * 
 * <p>A JSONSerializer has a number of public methods of the form <code>JSONSerializer.objectify...</code> 
 * for constructing a JSONObject from an RDF graph,
 * given a node in the graph as the root of the transformation. </p>
 * 
 * <p>For example, the code below builds an RDF graph then constructs a JSONObject from one of the nodes in the graph:</p>
 * 
 * <pre>
// set up a model
Model model = ModelFactory.createDefaultModel();
Resource resource = model.createResource();
Property p = model.createProperty("http://www.example.com/foo#p");
resource.addProperty(p, "bar");
    
// create a serializer (namespace aware)
JSONSerializer serializer = new JSONSerializer();
serializer.putNamespace("http://www.example.com/foo#", "foo");
    
try 
{
   // objectify
   JSONObject jo = (JSONObject) serializer.objectify(resource);
}
catch (JSONException e) 
{
   e.printStackTrace();
}
 * </pre>
 * 
 * <p>A call to <code>jo.toString()</code> would then return the following string:</p>
 * 
 * <pre>
{
   "@id":"7620170f:1138b3911ac:-7fff",
   "@namespaces":
   {
  "foo":"http://www.example.com/foo#",
  "dcterms":"http://purl.org/dc/terms/",
  "xsd":"http://www.w3.org/2001/XMLSchema#",
  "dc":"http://purl.org/dc/elements/1.1/",
  "rdfs":"http://www.w3.org/2000/01/rdf-schema#",
  "rdf":"http://www.w3.org/1999/02/22-rdf-syntax-ns#",
  "owl":"http://www.w3.org/2002/07/owl#"
   },
   "foo:p":"bar"
}
 * </pre>
 * 
 * <p>If the serializer is <strong>not</strong> namespace aware, a JSONObject will be constructed which serializes as:</p>
 * 
 * <pre>
{
   "@id":"7620170f:1138b3911ac:-7fff",
   "p":"bar"
}
 * </pre>
 * 
 * <p>Note that a serializer which is <strong>not</strong> namespace aware will conflate all properties that
 * have the same local name.</p>
 * 
 * <p>The method <code>JSONSerializer.serialize</code> is a convenience method which simply calls <code>JSONSerializer.objectify</code> 
 * then returns the value of <code>object.toString</code> called on the constructed object.</p> 
 * 
 * <p>By default, a JSONSerializer will only include those properties which have a literal value.
 * To include properties which have a resource value, a JSONSerializer must be explicitly told to <strong>follow</strong> specific properties.</p>
 * 
 * <p>The <code>JSONSerializer.follow</code> method can be called with a single string, which is either the full URI
 * of the property to follow resource values, or the prefix abbreviated URI (if a prefix has been registered).</p>
 * 
 * <p>For example, a serializer set up as follows:</p>
 * 
 * <pre>
JSONSerializer serializer = new JSONSerializer();
serializer.putNamespace("http://www.example.com/foo#", "foo");
serializer.follow("foo:p");
 * </pre>
 * 
 * <p>will follow the <code>foo:p</code> property <strong>wherever it is found in the transformation</strong>.</p>
 * 
 * <p>Alternatively, a JSONSerializer can be told to follow specific properties but up to a maximum number of
 * steps (arcs) from the root of the transformation.</p>
    
 * <p>For example, a serializer set up as follows:</p>
 * 
 * <pre>
JSONSerializer serializer = new JSONSerializer();
serializer.putNamespace("http://www.example.com/foo#", "foo");
serializer.follow("foo:p", 1);
 * </pre>
 * 
 * <p>will follow the <code>foo:p</code> property only from the root of the transformation (one arc deep), and not thereafter.</p>
 * 
 * <p>See the documentation on the methods of the
 * {@link net.sf.conception.rdfoo.TestJSONSerializer} test case for more examples.</p>
 * 
 * <p>Note that all public methods of this class are currently declared as throwing a JSONException.
 * This is an alpha release, and there may be bugs. If you encounter a JSONException, you've probably 
 * found a bug in this code. Please <a href="mailto:a.j.miles@rl.ac.uk">tell me</a> about any bugs you find.</p> 
 * </p>
 * 
 * @author $Author: alimanfoo $
 * @version $Revision: 1.2 $ on $Date: 2007/07/03 15:16:38 $
 */
public class JSONSerializer {

    private boolean namespaceAware = true;
    private Hashtable prefixes = new Hashtable();
    private Hashtable namespaces = new Hashtable();
    private Hashtable arcs = new Hashtable();

    public static final String VALUE = "@value";
    public static final String VALUES = "@values";
    public static final String LANG = "@lang";
    public static final String TYPE = "@type";
    public static final String ID = "@id";
    public static final String BAG = "@bag";
    public static final String SEQ = "@seq";
    public static final String ALT = "@alt";
    public static final String LIST = "@list";
    public static final String NAMESPACES = "@namespaces";

    public JSONSerializer() {
        this.initns();
    }

    public JSONSerializer(boolean namespaceAware) {
        this.namespaceAware = namespaceAware;
        this.initns();
    }

    private void initns() {
        // initialise the namespace mappings
        putNamespace(RDF.getURI(), "rdf");
        putNamespace(RDFS.getURI(), "rdfs");
        putNamespace(XSD.getURI(), "xsd");
        putNamespace(OWL.getURI(), "owl");
        putNamespace(DC.getURI(), "dc");
        putNamespace(DCTerms.getURI(), "dcterms");
    }

    /**
     * Put a namespace-to-prefix mapping for this serializer.
     * @param namespace the namespace URI to be abbreviated
     * @param prefix the prefix to use as an abbreviation
     */
    public void putNamespace(String namespace, String prefix) {
        prefixes.put(namespace, prefix);
        namespaces.put(prefix, namespace);
    }

    /**
     * Instruct this serializer to follow all arcs of type predicate.
     * @param predicate the URI of the predicate to follow (can use abbreviated form if namespace is registered with this serializer)
     */
    public JSONSerializer follow(String predicate) {
        this.arcs.put(this.unabbrev(predicate), new Integer(-1));
        return this;
    }

    /**
     * Instruct this serializer to follow arcs of type predicate to a maximum depth.
     * 
     * @param predicate the URI of the predicate to follow (can use abbreviated form if namespace is registered with this serializer)
     * @param depth the maximum number of arcs to traverse (negative value means unlimited)
     */
    public JSONSerializer follow(String predicate, int depth) {
        this.arcs.put(this.unabbrev(predicate), new Integer(depth));
        return this;
    }

    /**
     * If namespace aware, use any namespace mappings to abbreviate the URI, otherwise get the local name.
     * @param uri the URI to abbreviate
     * @return an abbreviated URI
     */
    protected String abbrev(String uri) {
        if (namespaceAware) // try to make a namespace mapping
        {
            // iterate over namespaces
            Enumeration e = prefixes.keys();
            while (e.hasMoreElements()) {
                String ns = (String) e.nextElement();

                // check for a match
                if (uri.startsWith(ns)) {
                    String prefix = (String) prefixes.get(ns);
                    return uri.replaceFirst(ns, prefix + ":");
                }
            }
        } else // try to return the local name
        {
            int index = Math.max(uri.lastIndexOf("#"), uri.lastIndexOf("/"));
            if (index > 0) {
                return uri.substring(index + 1);
            }
        }

        // if no mappings or cannot obtain local name, return the input
        return uri;
    }

    protected String unabbrev(String name) {
        for (Enumeration e = namespaces.keys(); e.hasMoreElements();) {
            String prefix = (String) e.nextElement();
            if (name.startsWith(prefix + ":")) {
                return name.replaceFirst(prefix + ":", ((String) namespaces.get(prefix)));
            }
        }
        return name;
    }

    /**
     * Create a JSON representation of an RDF literal.
     * <p>
     * If the literal has no language tag or datatype, return a String, otherwise return a JSONObject.
     * @param literal the literal to objectify
     * @return a String or a JSON object
     * @throws JSONException
     */
    public Object objectifyLiteral(Literal literal) throws JSONException {
        return this.objectifyLiteral(literal, true);
    }

    /**
     * Objectify an RDF literal, either root or nested.
     * @param literal
     * @param root
     * @return
     * @throws JSONException
     */
    protected Object objectifyLiteral(Literal literal, boolean root) throws JSONException {
        // access the lexical value, datatype URI and language tag
        String datatypeURI = literal.getDatatypeURI();
        String language = literal.getLanguage();
        String value = literal.getLexicalForm();

        if (datatypeURI == null && language.equals("")) // handle a simple literal
        {
            return value;
        } else // handle a plain or typed literal
        {
            JSONObject object = root ? this.createRootObject() : new JSONObject();

            // put the lexical value
            object.put(VALUE, value);

            if (datatypeURI != null) // handle a typed literal
            {
                // put the type
                object.put(TYPE, this.abbrev(datatypeURI));
            } else // handle a plain literal
            {
                // put the language tag
                object.put(LANG, language);
            }

            return object;
        }
    }

    /**
     * Create a root object, including namespace mappings if namespace aware.
     * @return a JSON object with namespace mappings
     */
    protected JSONObject createRootObject() throws JSONException {
        JSONObject object = new JSONObject();

        if (namespaceAware) // create a namespace object
        {
            JSONObject nsObject = new JSONObject();
            for (Enumeration e = namespaces.keys(); e.hasMoreElements();) {
                String prefix = (String) e.nextElement();
                String ns = (String) namespaces.get(prefix);
                nsObject.put(prefix, ns);
            }
            object.put(NAMESPACES, nsObject);
        }

        return object;
    }

    /**
     * Return a JSON serialization of an RDF node.
     * @param node
     * @return
     */
    public String serialize(RDFNode node) throws JSONException {
        return this.objectify(node).toString();
    }

    public Object objectify(RDFNode node) throws JSONException {
        if (node.isLiteral()) {
            return this.objectifyLiteral((Literal) node);
        } else {
            return this.objectifyResource((Resource) node);
        }
    }

    /**
     * Create a JSON object to represent an RDF resource.
     * @param resource the resource to objectify
     * @return
     * @throws JSONException
     */
    public JSONObject objectifyResource(Resource resource) throws JSONException {
        return this.objectifyResource(resource, true, 0, new ArrayList());
    }

    protected JSONObject objectifyResource(Resource resource, boolean root, int depth, Collection visited)
            throws JSONException {
        // check if List, Container or Resource proper
        if (resource.canAs(RDFList.class)) {
            return this.objectifyRDFList((RDFList) resource.as(RDFList.class), root, depth, visited);
        } else if (resource.hasProperty(RDF.type, RDF.Seq)) {
            return this.objectifyContainer((Seq) resource.as(Seq.class), root, depth, visited);
        } else if (resource.hasProperty(RDF.type, RDF.Bag)) {
            return this.objectifyContainer((Bag) resource.as(Bag.class), root, depth, visited);
        } else if (resource.hasProperty(RDF.type, RDF.Alt)) {
            return this.objectifyContainer((Alt) resource.as(Alt.class), root, depth, visited);
        } else {
            return this.objectifyResourceProper(resource, root, depth, visited);
        }
    }

    protected Object objectifyRDFNode(RDFNode node, boolean root, int depth, Collection visited)
            throws JSONException {
        if (node.isLiteral()) {
            return this.objectifyLiteral((Literal) node, root);
        } else // handle resources
        {
            return this.objectifyResource((Resource) node, root, depth, visited);
        }
    }

    protected JSONObject objectifyRDFList(RDFList list, boolean root, int depth, Collection visited)
            throws JSONException {
        // initialise JSON object
        JSONObject jo = root ? this.createRootObject() : new JSONObject();

        // put the id
        if (list.isURIResource()) {
            jo.put(ID, list.getURI());
        } else if (list.isAnon()) {
            jo.put(ID, list.getId().toString());
        }

        if (!visited.contains(list)) {
            visited.add(list);

            // create a JSON array to hold list values
            JSONArray vals = new JSONArray();

            // populate the array
            ExtendedIterator it = list.iterator();
            while (it.hasNext()) {
                RDFNode node = (RDFNode) it.next();
                Object val = this.objectifyRDFNode(node, false, depth, visited);
                vals.put(val);
            }

            // put the array
            jo.put(LIST, vals);
        }

        return jo;
    }

    /**
     * Create a JSON object representing and RDF list.
     * @param list
     * @return a JSON object representing an RDF list
     * @throws JSONException
     */
    public JSONObject objectifyRDFList(RDFList list) throws JSONException {
        return this.objectifyRDFList(list, true, 0, new ArrayList());
    }

    public JSONObject objectifyContainer(Container con) throws JSONException {
        return this.objectifyContainer(con, true, 0, new ArrayList());
    }

    protected JSONObject objectifyResourceProper(Resource resource, boolean root, int depth, Collection visited)
            throws JSONException {
        // create the JSON object
        JSONObject jo = root ? this.createRootObject() : new JSONObject();

        // put the id
        if (resource.isURIResource()) {
            jo.put(ID, resource.getURI());
        } else if (resource.isAnon()) {
            jo.put(ID, resource.getId().toString());
        }

        if (!visited.contains(resource)) {
            visited.add(resource);

            // build a properties table for the resource
            Hashtable propsTable = this.buildPropsTable(resource, depth);

            // handle the properties
            Enumeration propNames = propsTable.keys();
            while (propNames.hasMoreElements()) {
                String propName = (String) propNames.nextElement();
                ArrayList vals = (ArrayList) propsTable.get(propName);
                if (vals.size() == 1) // handle single value props
                {
                    RDFNode node = (RDFNode) vals.get(0);
                    if (node.isLiteral()) // handle literal value
                    {
                        jo.put(this.abbrev(propName), this.objectifyLiteral((Literal) node, false));
                    } else // handle resource value
                    {
                        JSONObject nested = this.objectifyResource((Resource) node, false, depth + 1, visited);
                        jo.put(this.abbrev(propName), nested);
                    }
                } else if (vals.size() > 1) // handle multi value props
                {
                    JSONArray array = new JSONArray();
                    for (int i = 0; i < vals.size(); i++) {
                        RDFNode node = (RDFNode) vals.get(i);
                        if (node.isLiteral()) // handle literal value
                        {
                            array.put(this.objectifyLiteral((Literal) node, false));
                        } else if (node.isResource()) // handle resource values
                        {
                            JSONObject nested = this.objectifyResource((Resource) node, false, depth + 1, visited);
                            array.put(nested);
                        }
                    }
                    JSONObject valsobj = (new JSONObject()).put(VALUES, array);
                    jo.put(this.abbrev(propName), valsobj);
                }
            }
        }

        return jo;
    }

    protected JSONObject objectifyContainer(Container container, boolean root, int depth, Collection visited)
            throws JSONException {
        // initialise JSON object
        JSONObject jo = root ? this.createRootObject() : new JSONObject();

        // put the id
        if (container.isURIResource()) {
            jo.put(ID, container.getURI());
        } else if (container.isAnon()) {
            jo.put(ID, container.getId().toString());
        }

        if (!visited.contains(container)) {
            visited.add(container);

            // create a JSON array to hold container values
            JSONArray vals = new JSONArray();

            // populate the array
            NodeIterator it = container.iterator();
            while (it.hasNext()) {
                RDFNode node = it.nextNode();
                Object val = this.objectifyRDFNode(node, false, depth, visited);
                vals.put(val);
            }

            // put the array
            String propName = null;
            if (container instanceof Seq) {
                propName = SEQ;
            } else if (container instanceof Bag) {
                propName = BAG;
            } else if (container instanceof Alt) {
                propName = ALT;
            }
            jo.put(propName, vals);
        }

        return jo;
    }

    protected Hashtable buildPropsTable(Resource resource, int depth) {
        StmtIterator it = resource.listProperties();

        // set up a hashtable of property to value mappings, so it's easy to differentiate single and 
        // multi- value props
        Hashtable propsTable = new Hashtable();
        while (it.hasNext()) {
            Statement s = it.nextStatement();
            // get the property name
            String propName = s.getPredicate().getURI();
            ArrayList vals;
            if (propsTable.containsKey(propName)) {
                vals = (ArrayList) propsTable.get(propName);
            } else {
                vals = new ArrayList();
                propsTable.put(propName, vals);
            }
            RDFNode node = s.getObject();
            if (node.isLiteral()) // add to the values
            {
                vals.add(node);
            } else // check if we should follow
            {
                if (arcs.containsKey(propName)) {
                    int max = ((Integer) arcs.get(propName)).intValue();
                    if (max < 0 || depth < max) {
                        vals.add(node);
                    }
                }
            }
        }

        return propsTable;
    }

}