/*
* #!
* 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.xml;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collection;
import java.util.Iterator;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.topicmaps.core.AssociationIF;
import net.ontopia.topicmaps.core.AssociationRoleIF;
import net.ontopia.topicmaps.core.TopicNameIF;
import net.ontopia.topicmaps.core.DataTypes;
import net.ontopia.topicmaps.core.OccurrenceIF;
import net.ontopia.topicmaps.core.ReifiableIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.core.TypedIF;
import net.ontopia.topicmaps.core.VariantNameIF;
import net.ontopia.utils.ObjectUtils;
import net.ontopia.xml.PrettyPrinter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
/**
* INTERNAL: Exports topic maps to the XTM 1.0 interchange format.
*/
public class XTMTopicMapExporter extends AbstractTopicMapExporter {
static Logger log = LoggerFactory.getLogger(XTMTopicMapExporter.class.getName());
protected AttributesImpl atts;
protected static final AttributesImpl EMPTY_ATTR_LIST = new AttributesImpl();
protected static final String EMPTY_NAMESPACE = "";
protected static final String EMPTY_LOCALNAME = "";
protected boolean export_srclocs = false;
/**
* Used to initialize the XTM Exporter
*/
public XTMTopicMapExporter() {
atts = new AttributesImpl();
}
/**
* INTERNAL: Returns true if source locators should be exported.
*/
public boolean getExportSourceLocators() {
return export_srclocs;
}
/**
* INTERNAL: Set the flag that says whether source locators should be exported
* or not.
*/
public void setExportSourceLocators(boolean export_srclocs) {
this.export_srclocs = export_srclocs;
}
/**
* INTERNAL: Returns true if configured to add IDs to all elements.
*
* @since 2.0
*/
public boolean getAddIds() {
return add_ids;
}
/**
* INTERNAL: Tells the exporter whether or not to add IDs to all elements.
* (Default: true.)
*
* @since 2.0
*/
public void setAddIds(boolean add_ids) {
this.add_ids = add_ids;
}
/**
* INTERNAL: Default export method. An XML pretty printer is used and the
* output is written to stdout.
*/
public void export(TopicMapIF tm) throws SAXException, IOException {
PrintStream stream = new PrintStream(new BufferedOutputStream(System.out));
export(tm, new PrettyPrinter(stream));
}
/**
* INTERNAL: Traverses a Topic Map and emits SAX document handler events
* according to the Topic Map interchange format to the given document
* handler.
*/
public void export(TopicMapIF tm, ContentHandler dh) throws SAXException {
dh.startDocument();
// TOPICMAP
// Calculate attributes
atts.clear();
atts.addAttribute(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "xmlns", "CDATA", "http://www.topicmaps.org/xtm/1.0/");
atts.addAttribute(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "xmlns:xlink", "CDATA", "http://www.w3.org/1999/xlink");
// Element id
addId(atts, tm);
// Output element
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "topicMap", atts);
// Do all the topics
Collection topics = tm.getTopics();
topics = filterCollection(topics);
Iterator iter = topics.iterator();
while (iter.hasNext())
writeTopic((TopicIF) iter.next(), dh);
// Finished with the topics, so allow them to be GC-ed
topics = null;
iter = null;
// Do all the associations
Collection associations = filterCollection(tm.getAssociations());
iter = associations.iterator();
while (iter.hasNext())
writeAssociation((AssociationIF) iter.next(), dh);
// Close element
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "topicMap");
dh.endDocument();
}
// --------------------------------------------------------------------
// Methods used on Topics
// --------------------------------------------------------------------
protected void writeTopic(TopicIF topic, ContentHandler dh)
throws SAXException {
// Calculate attributes
atts.clear();
addId(atts, topic);
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "topic", atts);
writeInstanceOf(topic, dh);
writeSubjectIdentity(topic, dh);
Collection basenames = topic.getTopicNames();
basenames = filterCollection(basenames);
if (!basenames.isEmpty())
writeTopicNames(basenames, dh);
Collection occurs = topic.getOccurrences();
if (!occurs.isEmpty())
writeOccurrences(filterCollection(topic.getOccurrences()), dh);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "topic");
}
protected void writeInstanceOf(TopicIF topic, ContentHandler dh)
throws SAXException {
Collection types = topic.getTypes();
types = filterCollection(types);
if (!types.isEmpty()) {
Iterator iter = types.iterator();
while (iter.hasNext()) {
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "instanceOf", EMPTY_ATTR_LIST);
writeTopicRef((TopicIF) iter.next(), dh);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "instanceOf");
}
}
}
protected void writeInstanceOf(TypedIF typed, ContentHandler dh)
throws SAXException {
TopicIF type = typed.getType();
if (type != null && filterOk(type)) {
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "instanceOf", EMPTY_ATTR_LIST);
writeTopicRef(type, dh);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "instanceOf");
}
}
protected void writeTopicNames(Collection names, ContentHandler dh)
throws SAXException {
// Get names, and sort the out.
Iterator iter = names.iterator();
while (iter.hasNext()) {
TopicNameIF basename = (TopicNameIF) iter.next();
// Calculate attributes
atts.clear();
addId(atts, basename);
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "baseName", atts);
// Write instanceOf
writeInstanceOf(basename, dh);
// Write scope
writeScope(basename.getScope(), dh);
// Write baseNameString
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "baseNameString", EMPTY_ATTR_LIST);
String value = basename.getValue();
if (value != null && !value.equals("")) {
char[] chars = value.toCharArray();
dh.characters(chars, 0, chars.length);
}
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "baseNameString");
// Write variant
Collection variants = basename.getVariants();
if (!variants.isEmpty())
writeVariants(filterCollection(variants), dh);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "baseName");
}
}
protected void writeTopicRef(TopicIF topic, ContentHandler dh)
throws SAXException {
atts.clear();
atts.addAttribute(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "xlink:href", "CDATA", "#" + getElementId(topic));
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "topicRef", atts);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "topicRef");
}
/**
* INTERNAL: This method is used to get the string form of the
* subject indicator of a topic. If the subject indicator is the
* source locator of some object in the topic map, a special
* procedure is applied. If it is not, the address of the locator is
* just returned.
*
* <p>If the subject indicator is of the form "base#fragment",
* where base is the base address of the topic map store, the
* returned address is "#fragment". If not, but the indicator is
* used for reification, the returned address is "id" + the object
* ID of the reified object.
*
* <p>This procedure is employed to ensure that this method produces
* the same results as AbstractTopicMapExporter.getElementId, which
* is necessary to avoid breaking reification of local objects on
* export.
*/
protected String getSubjectIndicatorRef(TopicIF topic, LocatorIF indicator) {
TopicMapIF topicmap = topic.getTopicMap();
LocatorIF baseloc = topicmap.getStore().getBaseAddress();
String address = indicator.getExternalForm();
String id = null;
if (baseloc != null) {
String base = baseloc.getExternalForm();
if (base != null && address.startsWith(base)
&& address.indexOf('#') != -1) {
id = address.substring(address.indexOf('#'));
}
}
if (id != null) {
String idfrag = id.substring(1);
if (!(mayCollide(idfrag) || !isValidXMLId(idfrag)))
return id;
}
//! TMObjectIF reified = topic.getReified();
//! if (reified != null && reified != topic)
//! return "#id" + reified.getObjectId();
return address;
}
protected void writeSubjectIdentity(TopicIF topic, ContentHandler dh)
throws SAXException {
Collection subjects = topic.getSubjectLocators();
Collection subinds = topic.getSubjectIdentifiers();
Collection srclocs = topic.getItemIdentifiers();
boolean outputIdentities = !(subjects.isEmpty() && subinds.isEmpty()
&& (!export_srclocs || srclocs.isEmpty()));
ReifiableIF reified = topic.getReified();
if (outputIdentities || reified != null)
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "subjectIdentity", EMPTY_ATTR_LIST);
if (outputIdentities) {
// Subject address(es)
if (!subjects.isEmpty()) {
Iterator it = subjects.iterator();
if (it.hasNext()) { // NOTE: we only pick out the first one
LocatorIF subject = (LocatorIF) it.next();
String notation = subject.getNotation();
if (notation != null && notation.equals("URI")) {
atts.clear();
atts.addAttribute(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "xlink:href", "CDATA", subject.getExternalForm());
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceRef", atts);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceRef");
} else
reportInvalidLocator(subject);
}
}
// Subject indicators
if (!subinds.isEmpty()) {
Iterator it = subinds.iterator();
while (it.hasNext()) {
LocatorIF indicator = (LocatorIF) it.next();
String notation = indicator.getNotation();
if (notation != null && notation.equals("URI")) {
atts.clear();
atts.addAttribute(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "xlink:href", "CDATA", getSubjectIndicatorRef(
topic, indicator));
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "subjectIndicatorRef", atts);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "subjectIndicatorRef");
} else
reportInvalidLocator(indicator);
}
}
// Source locators (only if configured)
if (export_srclocs && !srclocs.isEmpty()) {
Iterator it = srclocs.iterator();
while (it.hasNext()) {
LocatorIF srcloc = (LocatorIF) it.next();
String notation = srcloc.getNotation();
if (notation != null && notation.equals("URI")) {
atts.clear();
atts.addAttribute(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "xlink:href", "CDATA", srcloc.getExternalForm());
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "subjectIndicatorRef", atts);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "subjectIndicatorRef");
} else
reportInvalidLocator(srcloc);
}
}
}
// Old-style reification
if (reified != null) {
atts.clear();
atts.addAttribute(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "xlink:href", "CDATA", "#" + getElementId(reified));
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "subjectIndicatorRef", atts);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "subjectIndicatorRef");
}
if (outputIdentities || reified != null)
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "subjectIdentity");
}
protected void writeVariants(Collection variants, ContentHandler dh)
throws SAXException {
Iterator iter = variants.iterator();
while (iter.hasNext()) {
VariantNameIF var = (VariantNameIF) iter.next();
// Calculate attributes
atts.clear();
addId(atts, var);
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "variant", atts);
// write parameters
writeParameters(var, dh);
// write variantName
writeVariantName(var, dh);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "variant");
}
}
protected void writeVariantName(VariantNameIF variant, ContentHandler dh)
throws SAXException {
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "variantName", EMPTY_ATTR_LIST);
if (ObjectUtils.equals(variant.getDataType(), DataTypes.TYPE_URI)) {
LocatorIF varloc = variant.getLocator();
if (varloc != null) {
String notation = varloc.getNotation();
if (notation != null && notation.equals("URI")) {
// Write resourceRef
atts.clear();
atts.addAttribute(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "xlink:href", "CDATA", varloc.getExternalForm());
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceRef", atts);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceRef");
} else
reportInvalidLocator(varloc);
}
} else {
// FIXME: what to do about data type?
atts.clear();
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceData", atts);
String value = variant.getValue();
if (value != null && !value.equals("")) {
char[] chars = value.toCharArray();
dh.characters(chars, 0, chars.length);
}
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceData");
}
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "variantName");
}
protected void writeParameters(VariantNameIF variant, ContentHandler dh)
throws SAXException {
Collection params = variant.getScope();
if (!params.isEmpty()) {
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "parameters", EMPTY_ATTR_LIST);
Iterator it = params.iterator();
while (it.hasNext())
writeTopicRef((TopicIF) it.next(), dh);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "parameters");
}
}
protected void writeScope(Collection scope, ContentHandler dh)
throws SAXException {
if (!scope.isEmpty()) {
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "scope", EMPTY_ATTR_LIST);
Iterator iter = scope.iterator();
while (iter.hasNext()) {
TopicIF topic = (TopicIF) iter.next();
writeTopicRef(topic, dh);
}
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "scope");
}
}
protected void writeOccurrences(Collection occurrences, ContentHandler dh)
throws SAXException {
Iterator iter = occurrences.iterator();
while (iter.hasNext()) {
OccurrenceIF occr = (OccurrenceIF) iter.next();
// Calculate attributes
atts.clear();
addId(atts, occr);
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "occurrence", atts);
// Write instanceOf
writeInstanceOf(occr, dh);
// Write scope
writeScope(occr.getScope(), dh);
// Write resourceRef
if (ObjectUtils.equals(occr.getDataType(), DataTypes.TYPE_URI)) {
LocatorIF occloc = occr.getLocator();
if (occloc != null) {
//! String notation = occloc.getNotation();
//! if (notation != null && notation.equals("URI")) {
atts.clear();
atts.addAttribute(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "xlink:href", "CDATA", occloc.getExternalForm());
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceRef", atts);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceRef");
//! } else
//! reportInvalidLocator(occloc);
}
}
// Write resourceData
else {
// FIXME: what to do about data type?
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceData", EMPTY_ATTR_LIST);
String value = occr.getValue();
if (value != null && !value.equals("")) {
char[] chars = value.toCharArray();
dh.characters(chars, 0, chars.length);
}
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "resourceData");
}
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "occurrence");
}
}
// --------------------------------------------------------------------
// Methods used on associations
// --------------------------------------------------------------------
protected void writeAssociation(AssociationIF assoc, ContentHandler dh)
throws SAXException {
Collection roles = filterCollection(assoc.getRoles());
if (roles.isEmpty()) {
log.info("Not exporting association " + assoc + " with no roles");
return; // otherwise we're producing invalid XTM (bug #1024)
}
// Calculate attributes
atts.clear();
addId(atts, assoc);
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "association", atts);
writeInstanceOf(assoc, dh);
writeScope(assoc.getScope(), dh);
writeMembers(roles, dh);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "association");
}
protected void writeMembers(Collection roles, ContentHandler dh)
throws SAXException {
if (!roles.isEmpty()) {
Iterator iter = roles.iterator();
while (iter.hasNext()) {
AssociationRoleIF role = (AssociationRoleIF) iter.next();
// Calculate attributes
atts.clear();
addId(atts, role);
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "member", atts);
// Write roleSpec
TopicIF type = role.getType();
if (type != null)
writeRoleSpec(type, dh);
// Write topicRef
TopicIF player = role.getPlayer();
if (player != null)
writeTopicRef(player, dh);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "member");
}
}
}
protected void writeRoleSpec(TopicIF topic, ContentHandler dh)
throws SAXException {
dh.startElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "roleSpec", EMPTY_ATTR_LIST);
writeTopicRef(topic, dh);
dh.endElement(EMPTY_NAMESPACE, EMPTY_LOCALNAME, "roleSpec");
}
protected void reportInvalidLocator(LocatorIF locator) {
// log.warn("Cannot export non-URI locator '" + locator + "'.");
throw new InvalidTopicMapException("Cannot export non-URI locator '"
+ locator + "'.");
}
}