/*
* #!
* Ontopia Engine
* #-
* Copyright (C) 2001 - 2013 The Ontopia Project
* #-
* Licensed 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 net.ontopia.topicmaps.utils.rdf;
import com.hp.hpl.jena.rdfxml.xmlinput.AResource;
import com.hp.hpl.jena.rdfxml.xmlinput.ALiteral;
import com.hp.hpl.jena.rdfxml.xmlinput.StatementHandler;
import com.hp.hpl.jena.rdf.model.AnonId;
import com.hp.hpl.jena.rdf.model.Literal;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.Resource;
import java.io.Writer;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Collection;
import java.net.MalformedURLException;
import net.ontopia.utils.DeciderIF;
import net.ontopia.utils.ObjectUtils;
import net.ontopia.utils.OntopiaRuntimeException;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.infoset.impl.basic.URILocator;
import net.ontopia.topicmaps.utils.PSI;
import net.ontopia.topicmaps.utils.deciders.TMExporterDecider;
import net.ontopia.topicmaps.query.utils.QueryUtils;
import net.ontopia.topicmaps.core.AssociationIF;
import net.ontopia.topicmaps.core.AssociationRoleIF;
import net.ontopia.topicmaps.core.DataTypes;
import net.ontopia.topicmaps.core.OccurrenceIF;
import net.ontopia.topicmaps.core.ReifiableIF;
import net.ontopia.topicmaps.core.ScopedIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.core.TopicMapWriterIF;
import net.ontopia.topicmaps.core.TopicNameIF;
import net.ontopia.topicmaps.query.core.InvalidQueryException;
import net.ontopia.topicmaps.query.core.QueryProcessorIF;
import net.ontopia.topicmaps.query.core.QueryResultIF;
/**
* PUBLIC: A topic map writer that can convert topic maps to RDF. The
* conversion may result in an RDF event stream, an RDF model, or RDF
* serialized into the RDF/XML format.
*
* @since 2.0
*/
public class RDFTopicMapWriter implements TopicMapWriterIF {
public static final String PROPERTY_PRESERVE_REIFICATION = "preserveReification";
public static final String PROPERTY_PRESERVE_SCOPE = "preserveScope";
public static final String PROPERTY_FILTER = "filter";
protected StatementHandler handler;
protected Model model;
protected Writer writer;
protected Map namepreds;
protected Map preferred_roles;
protected boolean preserve_scope = true;
protected boolean preserve_reification = true;
protected DeciderIF filter;
private static final String NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
private static final String NS_RDFS = "http://www.w3.org/2000/01/rdf-schema#";
private static final String NS_OWL = "http://www.w3.org/2002/07/owl#";
private static final String NS_TM = "http://psi.ontopia.net/tminrdf/#";
private static final String NS_TM2RDF = "http://psi.ontopia.net/tm2rdf/#";
/// constructors
/**
* PUBLIC: Creates a writer that writes the RDF representation to the
* given StatementHandler.
*/
public RDFTopicMapWriter(StatementHandler handler) {
this.handler = handler;
}
/**
* PUBLIC: Creates a writer that writes the RDF representation to
* the given OutputStream serialized to RDF/XML and using the UTF-8
* character encoding.
*/
public RDFTopicMapWriter(OutputStream stream) throws IOException {
this(stream, "utf-8");
}
/**
* PUBLIC: Creates a writer that writes the RDF representation to
* the given OutputStream serialized to RDF/XML and using the given
* character encoding.
* @since 5.1.3
*/
public RDFTopicMapWriter(OutputStream stream, String encoding)
throws IOException {
this(new OutputStreamWriter(stream, "utf-8"));
}
/**
* PUBLIC: Creates a writer that writes the RDF representation to
* the given OutputStream serialized to RDF/XML.
*/
public RDFTopicMapWriter(Writer writer) {
this.writer = writer;
model = ModelFactory.createDefaultModel();
handler = new ModelBuildingHandler(model);
}
/**
* PUBLIC: Creates a writer that builds an RDF representation of the
* topic map in the given Jena RDF model.
*/
public RDFTopicMapWriter(Model model) {
this.model = model;
this.handler = new ModelBuildingHandler(model);
}
/// options
/**
* PUBLIC: Controls whether the writer will use RDF reification to
* preserve the scopes in the topic map.
*/
public void setPreserveScope(boolean preserve_scope) {
this.preserve_scope = preserve_scope;
}
/**
* PUBLIC: Returns true if the writer will use RDF reification to
* preserve the scopes in the topic map.
*/
public boolean getPreserveScope() {
return preserve_scope;
}
/**
* PUBLIC: Controls whether the writer will use RDF reification to
* preserve reification in the topic map.
*/
public void setPreserveReification(boolean preserve_reification) {
this.preserve_reification = preserve_reification;
}
/**
* PUBLIC: Returns true if the writer will use RDF reification to
* preserve reification in the topic map.
*/
public boolean getPreserveReification() {
return preserve_reification;
}
/**
* PUBLIC: Sets the filter that decides which topic map constructs
* are accepted and exported. Uses 'filter' to identify individual
* topic constructs as allowed or disallowed. TM constructs that
* depend on the disallowed topics are also disallowed.
*
* @param filter Places constraints on individual topicmap constructs.
*/
public void setFilter(DeciderIF filter) {
this.filter = new TMExporterDecider(filter);
}
/**
* Filter a single object..
* @param unfiltered The object to filter.
* @return True if the object is accepted by the filter or the filter is null.
* False otherwise.
*/
private boolean filterOk(Object unfiltered) {
if (filter == null)
return true;
return filter.ok(unfiltered);
}
/**
* Filter a whole collection of objects.
* @param unfiltered The objects to filter.
* @return A new collection containing all objects accepted by the filter, or
* if this.filter is null, returns the original collection.
*/
private Collection filterCollection(Collection unfiltered) {
if (filter == null)
return unfiltered;
Collection retVal = new ArrayList();
Iterator unfilteredIt = unfiltered.iterator();
while (unfilteredIt.hasNext()) {
Object current = unfilteredIt.next();
if (filter.ok(current))
retVal.add(current);
}
return retVal;
}
/// the actual writer
public void write(TopicMapIF topicmap) {
// http://www.ilrt.bris.ac.uk/discovery/chatlogs/rdfig/2003-12-17#T12-14-33
setup(topicmap);
// topics
Collection topics = topicmap.getTopics();
topics = filterCollection(topics);
Iterator it = topics.iterator();
while (it.hasNext()) {
TopicIF topic = (TopicIF) it.next();
write(topic);
}
// associations
Collection associations = topicmap.getAssociations();
associations = filterCollection(associations);
it = associations.iterator();
while (it.hasNext()) {
AssociationIF assoc = (AssociationIF) it.next();
write(assoc);
}
// finishing up
if (model != null && writer != null)
model.write(writer);
}
protected void write(TopicIF topic) {
AResource namedef = new AResourceWrapper(NS_RDFS + "label");
AResource type = new AResourceWrapper(NS_RDF + "type");
AResource sameas = new AResourceWrapper(NS_OWL + "sameAs");
AResource subject = getResource(topic);
// subject indicators
Iterator it2 = topic.getSubjectIdentifiers().iterator();
while (it2.hasNext()) {
AResource other = getResource((LocatorIF) it2.next());
if (!other.equals(subject))
handler.statement(subject, sameas, other);
}
// types
Collection types = topic.getTypes();
types = filterCollection(types);
it2 = types.iterator();
AResource topictype = null;
while (it2.hasNext()) {
topictype = getResource((TopicIF) it2.next());
handler.statement(subject, type, topictype);
}
// base names
Collection baseNames = topic.getTopicNames();
baseNames = filterCollection(baseNames);
it2 = baseNames.iterator();
while (it2.hasNext()) {
TopicNameIF bn = (TopicNameIF) it2.next();
AResource namepred;
if (bn.getType().getSubjectIdentifiers().contains(PSI.getSAMNameType())) {
namepred = (AResource) namepreds.get(topictype);
if (namepred == null)
namepred = namedef;
} else
namepred = getResource(bn.getType());
statement(subject, namepred, getLiteral(bn.getValue()), bn);
}
// occurrences
Collection occurrences = topic.getOccurrences();
occurrences = filterCollection(occurrences);
it2 = occurrences.iterator();
while (it2.hasNext()) {
OccurrenceIF occ = (OccurrenceIF) it2.next();
if (ObjectUtils.equals(occ.getDataType(), DataTypes.TYPE_URI))
statement(subject, getResource(occ.getType()),
getResource(occ.getLocator()), occ);
else
statement(subject, getResource(occ.getType()),
getLiteral(occ.getValue()), occ);
// else don't make a statement, since there is no value (bug #1797)
}
}
protected void write(AssociationIF assoc) {
if (assoc.getRoles().size() == 1) {
// unary
AResource trueres = new AResourceWrapper(NS_TM + "true");
AResource pred = getResource(assoc.getType());
AssociationRoleIF role = (AssociationRoleIF)
assoc.getRoles().iterator().next();
statement(getResource(role.getPlayer()), pred, trueres, assoc);
} else if (assoc.getRoles().size() == 2) {
// binary
TopicMapIF topicmap = assoc.getTopicMap();
TopicIF preferredRoleType = getTopic(topicmap, "preferred-role");
TopicIF namePropertyType = getTopic(topicmap, "name-property");
TopicIF assoctype = assoc.getType();
if (assoctype != null &&
(assoctype.equals(namePropertyType) ||
assoctype.equals(preferredRoleType)))
return; // don't export mapping
TopicIF preferred = (TopicIF) preferred_roles.get(assoctype);
TopicIF subject = null;
TopicIF object = null;
Iterator it2 = assoc.getRoles().iterator();
AssociationRoleIF role = (AssociationRoleIF) it2.next();
if (preferred == null || preferred.equals(role.getType())) {
subject = role.getPlayer();
if (preferred == null)
preferred_roles.put(assoctype, role.getType());
} else
object = role.getPlayer();
role = (AssociationRoleIF) it2.next();
if (preferred != null && preferred.equals(role.getType()) &&
subject == null) {
subject = role.getPlayer();
if (preferred == null)
preferred_roles.put(assoctype, role.getType());
} else
object = role.getPlayer();
if (subject != null && assoctype != null && object != null)
statement(getResource(subject),
getResource(assoctype),
getResource(object),
assoc);
} else {
// lotsary
AResource type = new AResourceWrapper(NS_RDF + "type");
AResource subject;
TopicIF reifier = assoc.getReifier();
if (reifier == null || !filterOk(reifier))
subject = getResource();
else
subject = getResource(reifier);
handler.statement(subject, type, getResource(assoc.getType()));
assertScope(subject, assoc.getScope());
Iterator it2 = assoc.getRoles().iterator();
while (it2.hasNext()) {
AssociationRoleIF role = (AssociationRoleIF) it2.next();
handler.statement(subject,
getResource(role.getType()),
getResource(role.getPlayer()));
}
}
}
// --- Actual RDF generation
private void statement(AResource subj, AResource pred, Object obj,
ScopedIF tmconstruct) {
boolean assert_unreified = true;
AResource statement = null;
if (preserve_reification) {
if (tmconstruct instanceof ReifiableIF) {
TopicIF reifier = ((ReifiableIF)tmconstruct).getReifier();
if (reifier != null && filterOk(reifier)) {
statement = getResource(reifier);
assertReified(statement, subj, pred, obj);
}
}
}
if (preserve_scope) {
Collection scope = tmconstruct.getScope();
if (!scope.isEmpty()) {
if (statement == null) {
statement = getResource();
assertReified(statement, subj, pred, obj);
}
assertScope(statement, scope);
assert_unreified = false; // it has scope, so don't assert it
}
}
if (assert_unreified) {
if (obj instanceof AResource)
handler.statement(subj, pred, (AResource) obj);
else
handler.statement(subj, pred, (ALiteral) obj);
}
}
private void assertReified(AResource statement,
AResource subject,
AResource predicate,
Object object) {
handler.statement(statement, getResource(NS_RDF + "subject"), subject);
handler.statement(statement, getResource(NS_RDF + "predicate"), predicate);
handler.statement(statement,
getResource(NS_RDF + "type"),
getResource(NS_RDF + "Statement"));
if (object instanceof AResource)
handler.statement(statement, getResource(NS_RDF + "object"), (AResource)object);
else
handler.statement(statement, getResource(NS_RDF + "object"), (ALiteral)object);
}
private void assertScope(AResource statement, Collection scope) {
Iterator it = scope.iterator();
AResource inscope = getResource(NS_TM + "inscope");
while (it.hasNext())
handler.statement(statement, inscope, getResource((TopicIF) it.next()));
}
// --- Internal methods
protected void setup(TopicMapIF topicmap) {
QueryProcessorIF proc = QueryUtils.getQueryProcessor(topicmap);
try {
namepreds = new HashMap();
QueryResultIF result = proc.execute(
"using tm for i\"http://psi.ontopia.net/tm2rdf/#\" " +
"tm:name-property($TYPE : tm:type, $PROP : tm:property)?");
while (result.next())
namepreds.put(getResource((TopicIF) result.getValue("TYPE")),
getResource((TopicIF) result.getValue("PROP")));
result.close();
} catch (InvalidQueryException e) {
}
try {
preferred_roles = new HashMap();
QueryResultIF result = proc.execute(
"using tm for i\"http://psi.ontopia.net/tm2rdf/#\" " +
"tm:preferred-role($ATYPE : tm:association-type, $RTYPE : tm:role-type)?");
while (result.next())
preferred_roles.put(result.getValue("ATYPE"), result.getValue("RTYPE"));
result.close();
} catch (InvalidQueryException e) {
}
}
private AResource getResource(TopicIF topic) {
LocatorIF locator = null;
if (locator == null && !topic.getSubjectLocators().isEmpty())
locator = (LocatorIF) topic.getSubjectLocators().iterator().next();
if (locator == null && !topic.getSubjectIdentifiers().isEmpty())
locator = (LocatorIF) topic.getSubjectIdentifiers().iterator().next();
if (locator != null)
return new AResourceWrapper(locator.getExternalForm());
return makeAnonymousNode(topic);
}
private AResource getResource(LocatorIF locator) {
return new AResourceWrapper(locator.getExternalForm());
}
private AResource getResource() {
return new AnonymousResource(Integer.toString(System.identityHashCode(new Object())));
}
private AResource getResource(String uri) {
return new AResourceWrapper(uri);
}
private ALiteral getLiteral(String value) {
return new ALiteralWrapper(value);
}
private AResource makeAnonymousNode(TopicIF topic) {
return new AnonymousResource(Integer.toString(System.identityHashCode(topic)));
}
private TopicIF getTopic(TopicMapIF topicmap, String fragment) {
try {
LocatorIF loc = new URILocator(NS_TM2RDF + fragment);
return topicmap.getTopicBySubjectIdentifier(loc);
} catch (MalformedURLException e) {
throw new OntopiaRuntimeException(e);
}
}
// --- Resource wrapper
static class AResourceWrapper implements AResource {
public String uri;
public AResourceWrapper(String uri) {
this.uri = uri;
}
public boolean isAnonymous() {
return false;
}
public String getAnonymousID() {
return null;
}
public String getURI() {
return uri;
}
public Object getUserData() {
return null;
}
public void setUserData(Object d) {
}
public int hashCode() {
return uri.hashCode();
}
public boolean equals(Object obj) {
if (obj instanceof AResource)
return uri.equals(((AResource) obj).getURI());
else
return false;
}
public String toString() {
return "<" + uri + ">";
}
public boolean hasNodeID() {
return false;
}
}
static class AnonymousResource implements AResource {
public String anonid;
public AnonymousResource(String anonid) {
this.anonid = anonid;
}
public boolean isAnonymous() {
return true;
}
public String getAnonymousID() {
return anonid;
}
public String getURI() {
return null;
}
public Object getUserData() {
return null;
}
public void setUserData(Object d) {
}
public int hashCode() {
return anonid.hashCode();
}
public boolean equals(Object obj) {
if (obj instanceof AResource)
return anonid.equals(((AResource) obj).getAnonymousID());
else
return false;
}
public String toString() {
return "<<<" + anonid + ">>>";
}
public boolean hasNodeID() {
return true;
}
}
// --- Literal wrapper
class ALiteralWrapper implements ALiteral {
private String value;
public ALiteralWrapper(String value) {
this.value = value;
}
public boolean isWellFormedXML() {
return false;
}
public String getParseType() {
return null;
}
public String toString() {
return value;
}
public String getLang() {
return null;
}
public String getDatatypeURI() {
return null;
}
private boolean tainted;
public void taint() {
tainted = true;
}
public boolean isTainted() {
return tainted;
}
}
// --- Jena model-building StatementHandler
static class ModelBuildingHandler implements StatementHandler {
private Model model;
public ModelBuildingHandler(Model model) {
this.model = model;
}
public void statement(AResource subj, AResource pred, AResource obj) {
model.add(convert(subj), convertPred(pred), convert(obj));
}
public void statement(AResource subj, AResource pred, ALiteral lit) {
model.add(convert(subj), convertPred(pred), convert(lit));
}
private Literal convert(ALiteral lit) {
String dt = lit.getDatatypeURI();
if (dt == null)
return model.createLiteral(lit.toString(), lit.getLang());
else
return model.createTypedLiteral(lit.toString(), dt);
}
private Resource convert(AResource r) {
if (r.isAnonymous())
return model.createResource(new StringAnonId(r.getAnonymousID()));
else
return model.createResource(r.getURI());
}
private Property convertPred(AResource r) {
if (r.isAnonymous())
return model.createProperty("http://anonymous.ontopia.net/#id" +
r.getAnonymousID());
else
return model.createProperty(r.getURI());
}
}
// --- AnonId implementation
static class StringAnonId extends AnonId {
private String id;
public StringAnonId(String id) {
this.id = id;
}
public int hashCode() {
return id.hashCode();
}
public boolean equals(Object object) {
return (object instanceof StringAnonId &&
object.toString().equals(id));
}
public String toString() {
return id;
}
}
/**
* Sets additional properties for the RDFTopicMapWriter. Accepted properties:
* <ul><li>'preserveReification' (Boolean), corresponds to
* {@link #setPreserveReification(boolean)}</li>
* <li>'preserveScope' (Boolean), corresponds to {@link #setPreserveScope(boolean)}</li>
* <li>'filter' (DeciderIF), corresponds to {@link #setFilter(net.ontopia.utils.DeciderIF)}</li>
* </ul>
* @param properties
*/
public void setAdditionalProperties(Map<String, Object> properties) {
Object value = properties.get(PROPERTY_PRESERVE_REIFICATION);
if ((value != null) && (value instanceof Boolean)) {
setPreserveReification((Boolean) value);
}
value = properties.get(PROPERTY_PRESERVE_SCOPE);
if ((value != null) && (value instanceof Boolean)) {
setPreserveScope((Boolean) value);
}
value = properties.get(PROPERTY_FILTER);
if ((value != null) && (value instanceof DeciderIF)) {
setFilter((DeciderIF) value);
}
}
}