com.github.jsonldjava.core.RDFDataset Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jsonld-java Show documentation
Show all versions of jsonld-java Show documentation
Json-LD core implementation
/*
* Copyright (c) 2012, Deutsche Forschungszentrum für Künstliche Intelligenz GmbH
* Copyright (c) 2012-2017, JSONLD-Java contributors
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.github.jsonldjava.core;
import static com.github.jsonldjava.core.JsonLdConsts.RDF_FIRST;
import static com.github.jsonldjava.core.JsonLdConsts.RDF_LANGSTRING;
import static com.github.jsonldjava.core.JsonLdConsts.RDF_NIL;
import static com.github.jsonldjava.core.JsonLdConsts.RDF_REST;
import static com.github.jsonldjava.core.JsonLdConsts.RDF_TYPE;
import static com.github.jsonldjava.core.JsonLdConsts.XSD_BOOLEAN;
import static com.github.jsonldjava.core.JsonLdConsts.XSD_DOUBLE;
import static com.github.jsonldjava.core.JsonLdConsts.XSD_INTEGER;
import static com.github.jsonldjava.core.JsonLdConsts.XSD_STRING;
import static com.github.jsonldjava.core.JsonLdUtils.isKeyword;
import static com.github.jsonldjava.core.JsonLdUtils.isList;
import static com.github.jsonldjava.core.JsonLdUtils.isObject;
import static com.github.jsonldjava.core.JsonLdUtils.isString;
import static com.github.jsonldjava.core.JsonLdUtils.isValue;
import static com.github.jsonldjava.utils.Obj.newMap;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Starting to migrate away from using plain java Maps as the internal RDF
* dataset store. Currently each item just wraps a Map based on the old format
* so everything doesn't break. Will phase this out once everything is using the
* new format.
*
* @author Tristan
*/
public class RDFDataset extends LinkedHashMap {
private static final long serialVersionUID = 2796344994239879165L;
private static final Pattern PATTERN_INTEGER = Pattern.compile("^[\\-+]?[0-9]+$");
private static final Pattern PATTERN_DOUBLE = Pattern
.compile("^([+-])?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)([Ee]([+-])?[0-9]+)?$");
public static class Quad extends LinkedHashMap implements Comparable {
private static final long serialVersionUID = -7021918051975883082L;
public Quad(final String subject, final String predicate, final String object,
final String graph) {
this(subject, predicate,
object.startsWith("_:") ? new BlankNode(object) : new IRI(object), graph);
}
public Quad(final String subject, final String predicate, final String value,
final String datatype, final String language, final String graph) {
this(subject, predicate, new Literal(value, datatype, language), graph);
}
private Quad(final String subject, final String predicate, final Node object,
final String graph) {
this(subject.startsWith("_:") ? new BlankNode(subject) : new IRI(subject),
new IRI(predicate), object, graph);
}
public Quad(final Node subject, final Node predicate, final Node object,
final String graph) {
super();
put("subject", subject);
put("predicate", predicate);
put("object", object);
if (graph != null && !"@default".equals(graph)) {
// TODO: i'm not yet sure if this should be added or if the
// graph should only be represented by the keys in the dataset
put("name", graph.startsWith("_:") ? new BlankNode(graph) : new IRI(graph));
}
}
public Node getSubject() {
return (Node) get("subject");
}
public Node getPredicate() {
return (Node) get("predicate");
}
public Node getObject() {
return (Node) get("object");
}
public Node getGraph() {
return (Node) get("name");
}
@Override
public int compareTo(Quad o) {
if (o == null) {
return 1;
}
int rval = getGraph().compareTo(o.getGraph());
if (rval != 0) {
return rval;
}
rval = getSubject().compareTo(o.getSubject());
if (rval != 0) {
return rval;
}
rval = getPredicate().compareTo(o.getPredicate());
if (rval != 0) {
return rval;
}
return getObject().compareTo(o.getObject());
}
}
public abstract static class Node extends LinkedHashMap
implements Comparable {
private static final long serialVersionUID = 1460990331795672793L;
public abstract boolean isLiteral();
public abstract boolean isIRI();
public abstract boolean isBlankNode();
public String getValue() {
return (String) get("value");
}
public String getDatatype() {
return (String) get("datatype");
}
public String getLanguage() {
return (String) get("language");
}
@Override
public int compareTo(Node o) {
if (o == null) {
// valid nodes are > null nodes
return 1;
}
if (this.isIRI()) {
if (!o.isIRI()) {
// IRIs > everything
return 1;
}
} else if (this.isBlankNode()) {
if (o.isIRI()) {
// IRI > blank node
return -1;
} else if (o.isLiteral()) {
// blank node > literal
return 1;
}
} else if (this.isLiteral()) {
if (o.isIRI() || o.isBlankNode()) {
return -1; // literals < blanknode < IRI
}
}
// NOTE: Literal will also need to compare
// language and datatype
return this.getValue().compareTo(o.getValue());
}
/**
* Converts an RDF triple object to a JSON-LD object.
*
* @param useNativeTypes true to output native types, false not to.
* @return the JSON-LD object.
* @throws JsonLdError JsonLdError
*/
Map toObject(Boolean useNativeTypes) throws JsonLdError {
// If value is an an IRI or a blank node identifier, return a new
// JSON object consisting
// of a single member @id whose value is set to value.
if (isIRI() || isBlankNode()) {
return newMap("@id", getValue());
}
// convert literal object to JSON-LD
final Map rval = newMap("@value", getValue());
// add language
if (getLanguage() != null) {
rval.put("@language", getLanguage());
}
// add datatype
else {
final String type = getDatatype();
final String value = getValue();
if (useNativeTypes) {
// use native datatypes for certain xsd types
if (XSD_STRING.equals(type)) {
// don't add xsd:string
} else if (XSD_BOOLEAN.equals(type)) {
if ("true".equals(value)) {
rval.put("@value", Boolean.TRUE);
} else if ("false".equals(value)) {
rval.put("@value", Boolean.FALSE);
} else {
// Else do not replace the value, and add the
// boolean type in
rval.put("@type", type);
}
} else if (
// http://www.w3.org/TR/xmlschema11-2/#integer
(XSD_INTEGER.equals(type) && PATTERN_INTEGER.matcher(value).matches())
// http://www.w3.org/TR/xmlschema11-2/#nt-doubleRep
|| (XSD_DOUBLE.equals(type)
&& PATTERN_DOUBLE.matcher(value).matches())) {
try {
final Double d = Double.parseDouble(value);
if (!Double.isNaN(d) && !Double.isInfinite(d)) {
if (XSD_INTEGER.equals(type)) {
final Integer i = d.intValue();
if (i.toString().equals(value)) {
rval.put("@value", i);
}
} else if (XSD_DOUBLE.equals(type)) {
rval.put("@value", d);
} else {
throw new RuntimeException(
"This should never happen as we checked the type was "
+ "either integer or "
+ "double");
}
}
} catch (final NumberFormatException e) {
// TODO: This should never happen since we match the
// value with regex!
throw new RuntimeException(e);
}
}
// do not add xsd:string type
else {
rval.put("@type", type);
}
} else if (!XSD_STRING.equals(type)) {
rval.put("@type", type);
}
}
return rval;
}
}
public static class Literal extends Node {
private static final long serialVersionUID = 8124736271571220251L;
public Literal(String value, String datatype, String language) {
super();
put("type", "literal");
put("value", value);
put("datatype", datatype != null ? datatype : XSD_STRING);
if (language != null) {
put("language", language);
}
}
@Override
public boolean isLiteral() {
return true;
}
@Override
public boolean isIRI() {
return false;
}
@Override
public boolean isBlankNode() {
return false;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static int nullSafeCompare(Comparable a, Comparable b) {
if (a == null && b == null) {
return 0;
}
if (a == null) {
return 1;
}
if (b == null) {
return -1;
}
return a.compareTo(b);
}
@Override
public int compareTo(Node o) {
// NOTE: this will also compare getValue() early!
int nodeCompare = super.compareTo(o);
if (nodeCompare != 0) {
// null, different type or different value
return nodeCompare;
}
if (this.getLanguage() != null || o.getLanguage() != null) {
// We'll ignore type-checking if either has language tag
// as language tagged literals should always have the type
// rdf:langString in RDF 1.1
return nullSafeCompare(this.getLanguage(), o.getLanguage());
} else {
return nullSafeCompare(this.getDatatype(), o.getDatatype());
}
// NOTE: getValue() already compared by super.compareTo()
}
}
public static class IRI extends Node {
private static final long serialVersionUID = 1540232072155490782L;
public IRI(String iri) {
super();
put("type", "IRI");
put("value", iri);
}
@Override
public boolean isLiteral() {
return false;
}
@Override
public boolean isIRI() {
return true;
}
@Override
public boolean isBlankNode() {
return false;
}
}
public static class BlankNode extends Node {
private static final long serialVersionUID = -2842402820440697318L;
public BlankNode(String attribute) {
super();
put("type", "blank node");
put("value", attribute);
}
@Override
public boolean isLiteral() {
return false;
}
@Override
public boolean isIRI() {
return false;
}
@Override
public boolean isBlankNode() {
return true;
}
}
private static final Node first = new IRI(RDF_FIRST);
private static final Node rest = new IRI(RDF_REST);
private static final Node nil = new IRI(RDF_NIL);
private final Map context;
// private UniqueNamer namer;
private JsonLdApi api;
public RDFDataset() {
super();
put("@default", new ArrayList());
context = new LinkedHashMap<>();
// put("@context", context);
}
/*
* public RDFDataset(String blankNodePrefix) { this(new
* UniqueNamer(blankNodePrefix)); }
*
* public RDFDataset(UniqueNamer namer) { this(); this.namer = namer; }
*/
public RDFDataset(JsonLdApi jsonLdApi) {
this();
this.api = jsonLdApi;
}
public void setNamespace(String ns, String prefix) {
context.put(ns, prefix);
}
public String getNamespace(String ns) {
return context.get(ns);
}
/**
* clears all the namespaces in this dataset.
*/
public void clearNamespaces() {
context.clear();
}
public Map getNamespaces() {
return context;
}
/**
* Returns a valid context containing any namespaces set.
*
* @return The context map
*/
public Map getContext() {
final Map rval = newMap();
rval.putAll(context);
// replace "" with "@vocab"
if (rval.containsKey("")) {
rval.put("@vocab", rval.remove(""));
}
return rval;
}
/**
* parses a context object and sets any namespaces found within it.
*
* @param contextLike The context to parse
* @throws JsonLdError If the context can't be parsed
*/
public void parseContext(Object contextLike) throws JsonLdError {
Context context;
if (api != null) {
context = new Context(api.opts);
} else {
context = new Context();
}
// Context will do our recursive parsing and initial IRI resolution
context = context.parse(contextLike);
// And then leak to us the potential 'prefixes'
final Map prefixes = context.getPrefixes();
for (final String key : prefixes.keySet()) {
final String val = prefixes.get(key);
if ("@vocab".equals(key)) {
if (val == null || isString(val)) {
setNamespace("", val);
}
} else if (!isKeyword(key)) {
setNamespace(key, val);
// TODO: should we make sure val is a valid URI prefix (i.e. it
// ends with /# or ?)
// or is it ok that full URIs for terms are used?
}
}
}
/**
* Adds a triple to the @default graph of this dataset.
*
* @param subject the subject for the triple
* @param predicate the predicate for the triple
* @param value the value of the literal object for the triple
* @param datatype the datatype of the literal object for the triple (null values
* will default to xsd:string)
* @param language the language of the literal object for the triple (or null)
*/
public void addTriple(final String subject, final String predicate, final String value,
final String datatype, final String language) {
addQuad(subject, predicate, value, datatype, language, "@default");
}
/**
* Adds a triple to the default graph of this dataset.
*
* @param subject the subject for the triple
* @param predicate the predicate for the triple
* @param object the object for the triple
*/
public void addTriple(final String subject, final String predicate, final String object) {
addQuad(subject, predicate, object, "@default");
}
/**
* Adds a triple to the specified graph of this dataset.
*
* @param subject the subject for the triple
* @param predicate the predicate for the triple
* @param object the object for the triple
* @param graph the graph to add this triple to
*/
@SuppressWarnings("unchecked")
public void addQuad(final String subject, final String predicate, final String object,
String graph) {
if (graph == null) {
graph = "@default";
}
if (!containsKey(graph)) {
put(graph, new ArrayList());
}
((ArrayList) get(graph)).add(new Quad(subject, predicate, object, graph));
}
/**
* Adds a triple to the specified graph of this dataset.
*
* @param s the subject for the triple
* @param p the predicate for the triple
* @param value the value of the literal object for the triple
* @param datatype the datatype of the literal object for the triple (null values
* will default to xsd:string)
* @param graph the graph to add this triple to
* @param language the language of the literal object for the triple (or null)
*/
@SuppressWarnings("unchecked")
public void addQuad(final String s, final String p, final String value, final String datatype,
final String language, String graph) {
if (graph == null) {
graph = "@default";
}
if (!containsKey(graph)) {
put(graph, new ArrayList());
}
((ArrayList) get(graph)).add(new Quad(s, p, value, datatype, language, graph));
}
/**
* Creates an array of RDF triples for the given graph.
*
* @param graphName The graph URI
* @param graph the graph to create RDF triples for.
*/
@SuppressWarnings("unchecked")
public void graphToRDF(String graphName, Map graph) {
// 4.2)
final List triples = new ArrayList<>();
// 4.3)
final List subjects = new ArrayList<>(graph.keySet());
// Collections.sort(subjects);
for (final String id : subjects) {
if (JsonLdUtils.isRelativeIri(id)) {
continue;
}
final Map node = (Map) graph.get(id);
final List properties = new ArrayList<>(node.keySet());
Collections.sort(properties);
for (String property : properties) {
final List