/* * #! * 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.jtm; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Collection; import java.util.LinkedList; import java.util.Map; import net.ontopia.infoset.core.LocatorIF; import net.ontopia.topicmaps.core.AssociationIF; import net.ontopia.topicmaps.core.AssociationRoleIF; import net.ontopia.topicmaps.core.ReifiableIF; import net.ontopia.topicmaps.core.ScopedIF; import net.ontopia.topicmaps.core.TMObjectIF; import net.ontopia.topicmaps.core.TopicNameIF; import net.ontopia.topicmaps.core.OccurrenceIF; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.core.TopicMapWriterIF; import net.ontopia.topicmaps.core.TypedIF; import net.ontopia.topicmaps.core.VariantNameIF; import net.ontopia.topicmaps.core.index.ClassInstanceIndexIF; import net.ontopia.topicmaps.utils.PSI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * PUBLIC: Exports topic maps to the JTM 1.0 interchange format. See the <a * href="http://www.cerny-online.com/jtm/1.0/">JTM homepage</a> for a * specification of the JTM 1.0 exchange format for topic map fragments. * * @since 5.1.0 */ public class JTMTopicMapWriter implements TopicMapWriterIF { static Logger log = LoggerFactory .getLogger(JTMTopicMapWriter.class.getName()); private final static String VERSION = "1.0"; private JSONWriter writer; private LocatorIF baseLoc; private enum LOCATOR_TYPE { IID, SID, SL } /** * PUBLIC: Create an JTMTopicMapWriter that writes to a given OutputStream in * UTF-8. <b>Warning:</b> Use of this method is discouraged, as it is very * easy to get character encoding errors with this method. * * @param stream Where the output should be written. */ public JTMTopicMapWriter(OutputStream stream) throws IOException { this(stream, "utf-8"); } /** * PUBLIC: Create an JTMTopicMapWriter that writes to a given OutputStream in * the given encoding. * * @param stream Where the output should be written. * @param encoding The desired character encoding. */ public JTMTopicMapWriter(OutputStream stream, String encoding) throws IOException { this(new OutputStreamWriter(stream, encoding)); } /** * PUBLIC: Create an JTMTopicMapWriter that writes to a given Writer. * * @param out Where the output should be written. */ public JTMTopicMapWriter(Writer out) { writer = new JSONWriter(out); } /** * PUBLIC: Writes out the given topic map. * * @param tm The topic map to be serialized as JTM. */ public void write(TopicMapIF tm) throws IOException { write((TMObjectIF) tm); } /** * PUBLIC: Write the given topic map construct as a JTM fragment. * * @param object The topic map construct to be serialized as JTM fragment. */ public void write(TMObjectIF object) throws IOException { // store the base address for this map baseLoc = object.getTopicMap().getStore().getBaseAddress(); writer.object().pair("version", VERSION); String key = "item_type"; if (object instanceof TopicMapIF) { writer.pair(key, "topicmap"); serializeTopicMap((TopicMapIF) object); } else if (object instanceof TopicIF) { writer.pair(key, "topic"); serializeTopic((TopicIF) object, true); } else if (object instanceof TopicNameIF) { writer.pair(key, "name"); serializeName((TopicNameIF) object, true); } else if (object instanceof VariantNameIF) { writer.pair(key, "variant"); serializeVariant((VariantNameIF) object, true); } else if (object instanceof OccurrenceIF) { writer.pair(key, "occurrence"); serializeOccurrence((OccurrenceIF) object, true); } else if (object instanceof AssociationIF) { writer.pair(key, "association"); serializeAssociation((AssociationIF) object, true); } else if (object instanceof AssociationRoleIF) { writer.pair(key, "role"); serializeRole((AssociationRoleIF) object, true); } writer.finish(); } /** * EXPERIMENTAL: Write out a collection of topics and associations * as a JTM fragment, represented as a complete topic map. The * identities, names, variants, occurrences, and types of the topics * are output, as are the complete associations. Note that the * associations of topics in the topics collection are not output, * unless they are contained in the assocs collection. */ public void write(Collection<TopicIF> topics, Collection<AssociationIF> assocs) throws IOException { baseLoc = null; writer.object().pair("version", VERSION); writer.pair("item_type", "topicmap"); writer.key("topics").array(); for (TopicIF topic : topics) { if (baseLoc == null) baseLoc = topic.getTopicMap().getStore().getBaseAddress(); serializeTopic(topic, false); } writer.endArray(); writer.key("associations").array(); for (AssociationIF assoc : assocs) serializeAssociation(assoc, false); for (TopicIF instance : topics) for (TopicIF type : (Collection<TopicIF>) instance.getTypes()) serializeTypeInstanceAssociation(type, instance); writer.endArray(); writer.endObject(); writer.finish(); } /** * INTERNAL: Serializes a complete topic map to the JTM output stream. * * @param tm the topic map to be serialized as JTM. */ @SuppressWarnings("unchecked") private void serializeTopicMap(TopicMapIF tm) throws IOException { // ----------------- Topics -------------------- Collection<TopicIF> topics = tm.getTopics(); if (!topics.isEmpty()) { writer.key("topics").array(); for (TopicIF topic : topics) { serializeTopic(topic, false); } writer.endArray(); } // ----------------- Associations -------------- ClassInstanceIndexIF classIndex = (ClassInstanceIndexIF) tm .getIndex("net.ontopia.topicmaps.core.index.ClassInstanceIndexIF"); Collection<AssociationIF> assocs = tm.getAssociations(); // type-instance associations have to be retrieved from the index, // as they are not returned by tm.getAssociations() Collection<TopicIF> topicTypes = classIndex.getTopicTypes(); // only write the "associations" key if there are any associations defined. if (!assocs.isEmpty() || !topicTypes.isEmpty()) { writer.key("associations").array(); // first, write all type-instance associations for (TopicIF type : topicTypes) { Collection<TopicIF> instances = classIndex.getTopics(type); for (TopicIF instance : instances) { serializeTypeInstanceAssociation(type, instance); } } // and now write the remaining associations for (AssociationIF assoc : assocs) { serializeAssociation(assoc, false); } writer.endArray(); } serializeItemIdentifiers(tm); serializeReifier(tm); writer.endObject(); } /** * INTERNAL: Serialize a topic to the underlying JTM stream. * * @param topic the topic to be serialized. * @param topLevel if the element is serialized as top-level element. */ @SuppressWarnings("unchecked") private void serializeTopic(TopicIF topic, boolean topLevel) throws IOException { if (!topLevel) { writer.object(); } serializeItemIdentifiers(topic); serializeSubjectIdentifiers(topic); serializeSubjectLocators(topic); // ------------------- Names ----------------- Collection<TopicNameIF> names = topic.getTopicNames(); if (!names.isEmpty()) { writer.key("names").array(); for (TopicNameIF name : names) { serializeName(name, false); } writer.endArray(); } // ----------------- Occurrences -------------- Collection<OccurrenceIF> occurrences = topic.getOccurrences(); if (!occurrences.isEmpty()) { writer.key("occurrences").array(); for (OccurrenceIF oc : occurrences) { serializeOccurrence(oc, false); } writer.endArray(); } writer.endObject(); } /** * INTERNAL: Serialize an association to the underlying JTM stream. * * @param association the association to be serialized. * @param topLevel if the element is serialized as top-level element. */ @SuppressWarnings("unchecked") private void serializeAssociation(AssociationIF association, boolean topLevel) throws IOException { if (!topLevel) { writer.object(); } serializeType(association); // ----------------- Roles -------------- Collection<AssociationRoleIF> roles = association.getRoles(); if (!roles.isEmpty()) { writer.key("roles").array(); for (AssociationRoleIF role : roles) { serializeRole(role, false); } writer.endArray(); } serializeScope(association); serializeItemIdentifiers(association); serializeReifier(association); writer.endObject(); } /** * INTERNAL: Serialize a role to the underlying JTM stream. * * @param role the role to be serialized. * @param topLevel if the element is serialized as top-level element. */ private void serializeRole(AssociationRoleIF role, boolean topLevel) throws IOException { if (!topLevel) { writer.object(); } writer. pair("player", getTopicRef(role.getPlayer())). pair("type", getTopicRef(role.getType())); serializeItemIdentifiers(role); serializeReifier(role); writer.endObject(); } /** * INTERNAL: Serialize a type-instance association. * * @param type the given type topic. * @param instance the given instance topic. */ private void serializeTypeInstanceAssociation(TopicIF type, TopicIF instance) throws IOException { writer.object().pair("type", "si:" + PSI.getSAMTypeInstance().getExternalForm()); writer.key("roles").array(); // Type Role writer.object(); writer.pair("player", getTopicRef(type)); writer.pair("type", "si:" + PSI.getSAMType().getExternalForm()); writer.endObject(); // Instance Role writer.object(); writer.pair("player", getTopicRef(instance)); writer.pair("type", "si:" + PSI.getSAMInstance().getExternalForm()); writer.endObject(); writer.endArray(); writer.endObject(); } /** * INTERNAL: Serialize a topic name to the underlying JTM stream. * * @param name the name to be serialized. * @param topLevel if the element is serialized as top-level element. */ @SuppressWarnings("unchecked") private void serializeName(TopicNameIF name, boolean topLevel) throws IOException { if (!topLevel) { writer.object(); } else { serializeParent(name.getTopic()); } writer.pair("value", name.getValue()); serializeType(name); // ----------------- Variants -------------- Collection<VariantNameIF> variants = name.getVariants(); if (!variants.isEmpty()) { writer.key("variants").array(); for (VariantNameIF var : variants) { serializeVariant(var, false); } writer.endArray(); } serializeScope(name); serializeItemIdentifiers(name); serializeReifier(name); writer.endObject(); } /** * INTERNAL: Serialize a variant to the underlying JTM stream. * * @param variant the variant to be serialized. * @param topLevel if the element is serialized as top-level element. */ private void serializeVariant(VariantNameIF variant, boolean topLevel) throws IOException { if (!topLevel) { writer.object(); } serializeValue(variant.getLocator(), variant.getValue()); serializeDataType(variant.getDataType()); serializeScope(variant); serializeItemIdentifiers(variant); serializeReifier(variant); writer.endObject(); } /** * INTERNAL: Serialize an occurrence to the underlying JTM stream. * * @param occurrence the occurrence to be serialized. * @param topLevel if the element is serialized as top-level element. */ private void serializeOccurrence(OccurrenceIF occurrence, boolean topLevel) throws IOException { if (!topLevel) { writer.object(); } else { serializeParent(occurrence.getTopic()); } serializeValue(occurrence.getLocator(), occurrence.getValue()); serializeType(occurrence); serializeDataType(occurrence.getDataType()); serializeScope(occurrence); serializeItemIdentifiers(occurrence); serializeReifier(occurrence); writer.endObject(); } /** * INTERNAL: Serialize the parent topic of a topic map construct. The parent * is serialized by merging all his item/subject identifiers and subject * locators together. If the parent is null, nothing will be serialized. * * @param parent the parent topic to be serialized. */ private void serializeParent(TopicIF parent) throws IOException { if (parent != null) { Collection<String> ids = new LinkedList<String>(); for (Object loc : parent.getItemIdentifiers()) { ids.add(getJTMTopicRef(LOCATOR_TYPE.IID, (LocatorIF) loc)); } for (Object loc : parent.getSubjectIdentifiers()) { ids.add(getJTMTopicRef(LOCATOR_TYPE.SID, (LocatorIF) loc)); } for (Object loc : parent.getSubjectLocators()) { ids.add(getJTMTopicRef(LOCATOR_TYPE.SL, (LocatorIF) loc)); } if (!ids.isEmpty()) { writer.key("parent").array(); for (String id : ids) { writer.value(id); } writer.endArray(); } } } /** * INTERNAL: Serialize the item identifiers of a {@link TMObjectIF}. If the * object does not have an item identifier, nothing will be serialized. * * @param obj the {@link TMObjectIF} to be serialized. */ @SuppressWarnings("unchecked") private void serializeItemIdentifiers(TMObjectIF obj) throws IOException { Collection<LocatorIF> ids = obj.getItemIdentifiers(); serializeIdentifiers("item_identifiers", ids); } /** * INTERNAL: Serialize the subject identifiers of a {@link TopicIF}. If the * object does not have a subject identifier, nothing will be serialized. * * @param topic the {@link TopicIF} to be serialized. */ @SuppressWarnings("unchecked") private void serializeSubjectIdentifiers(TopicIF topic) throws IOException { Collection<LocatorIF> sids = topic.getSubjectIdentifiers(); serializeIdentifiers("subject_identifiers", sids); } /** * INTERNAL: Serialize the subject locators of a {@link TopicIF}. If the * object does not have a subject locator, nothing will be serialized. * * @param topic the {@link TopicIF} to be serialized. */ @SuppressWarnings("unchecked") private void serializeSubjectLocators(TopicIF topic) throws IOException { Collection<LocatorIF> slocs = topic.getSubjectLocators(); serializeIdentifiers("subject_locators", slocs); } /** * INTERNAL: Serialize a collection of {@link LocatorIF} objects. * * @param key the key to be used for serialization. * @param ids the collection of ids to be serialized. */ private void serializeIdentifiers(String key, Collection<LocatorIF> ids) throws IOException { if (!ids.isEmpty()) { writer.key(key).array(); for (LocatorIF id : ids) { writer.value(normaliseLocatorReference(id)); } writer.endArray(); } } /** * INTERNAL: Serialize the scopes for a given topic map construct. If the * construct is not scoped, nothing will be serialized. * * @param obj the scoped object to be used. */ @SuppressWarnings("unchecked") private void serializeScope(ScopedIF obj) throws IOException { Collection<TopicIF> scopes = obj.getScope(); if (!scopes.isEmpty()) { writer.key("scope").array(); for (TopicIF ref : scopes) { writer.value(getTopicRef(ref)); } writer.endArray(); } } /** * INTERNAL: Serialize the type for a given typed topic map construct. If the * construct is not typed, nothing will be serialized. * * @param obj the typed topic map construct to be used. */ private void serializeType(TypedIF obj) throws IOException { TopicIF type = obj.getType(); if (type != null) { writer.pair("type", getTopicRef(type)); } } /** * INTERNAL: Serialize the given datatype for a variant or occurrence * construct. If the datatype is equal to the default datatype ( * {@link PSI#XSD_STRING}), nothing will be serialized. * * @param type a locator defining the datatype. * @see PSI#XSD_STRING */ private void serializeDataType(LocatorIF type) throws IOException { if (type != null && !PSI.getXSDString().equals(type)) { writer.pair("datatype", type.getExternalForm()); } } /** * INTERNAL: Serialize the value for a given typed topic map construct. If the * locator representation is not null or empty, write a normalised version of * it (relative to the base locator of the topic map), otherwise serialize the * string version of the value. * * @param loc the value as locator. * @param value the value in string representation. */ private void serializeValue(LocatorIF loc, String value) throws IOException { String v = value; if (loc != null) { v = normaliseLocatorReference(loc); } writer.pair("value", v); } /** * INTERNAL: Serialize the reference to the reifier of a topic map construct, * if there is one present. * * @param obj a reifiable topic map construct. */ private void serializeReifier(ReifiableIF obj) throws IOException { if (obj.getReifier() != null) { writer.pair("reifier", getTopicRef(obj.getReifier())); } } /** * INTERNAL: Normalise a given locator reference according to the CXTM spec, i.e. make * it relative to the base locator of the topic map. * * @param reference the reference locator address. */ private String normaliseLocatorReference(LocatorIF loc) { String reference = loc.getAddress(); String retVal = reference.substring(longestCommonPath(reference, baseLoc.getAddress()).length()); if (retVal.startsWith("/")) retVal = retVal.substring(1); return retVal; } /** * INTERNAL: Returns the longest common path of two Strings. * The longest common path is the longest common prefix that ends with a '/'. * If one string is a prefix of the other, the the longest common path is * the shortest (i.e. the one that is a prefix of the other). */ private String longestCommonPath(String source1, String source2) { String retVal = ""; if (source1.startsWith(source2)) retVal = source2; else if (source2.startsWith(source1)) retVal = source1; else { int i = 0; int lastSlashIndex = 0; while (i < source1.length() && i < source2.length() && source1.charAt(i) == source2.charAt(i)) { if (source1.charAt(i) == '/') lastSlashIndex = i; i++; } if (lastSlashIndex == -1) retVal = ""; else retVal = source1.substring(0, lastSlashIndex); } return retVal; } /** * INTERNAL: Returns the reference to a topic in JTM notation. * * The order of preference for constructing a topic reference is as follows: * * <ul> * <li>subject identifier * <li>subject locator * <li>item identifier * </ul> * * @param ref the topic to be referenced. * @return a reference to this topic in JTM notation. * @see #getJTMTopicRef(LOCATOR_TYPE, LocatorIF) */ private String getTopicRef(TopicIF ref) { // prefer subject identifiers if present if (!ref.getSubjectIdentifiers().isEmpty()) { return getJTMTopicRef(LOCATOR_TYPE.SID, (LocatorIF) ref .getSubjectIdentifiers().iterator().next()); } else if (!ref.getSubjectLocators().isEmpty()) { return getJTMTopicRef(LOCATOR_TYPE.SL, (LocatorIF) ref .getSubjectLocators().iterator().next()); } else if (!ref.getItemIdentifiers().isEmpty()) { return getJTMTopicRef(LOCATOR_TYPE.IID, (LocatorIF) ref .getItemIdentifiers().iterator().next()); } else { // should not happen, as every topic needs to have one of them log.warn("Topic with objectID:" + ref.getObjectId() + " has not a single item/subject identifier or locator."); return new String(""); } } /** * INTERNAL: Returns a locator in JTM notation. When a construct is * referred to by means of a locator L, the string is to be constructed as * follows: * * <ul> * <li>L is a subject identifier: si:L * <li>L is a subject locator: sl:L * <li>L is an item identifier: ii:L * </ul> * * @param type the type of the locator. * @param loc the internal locator to be converted. * @return a string representation of the internal locator in JTM notation. */ private String getJTMTopicRef(LOCATOR_TYPE type, LocatorIF loc) { StringBuilder sb = new StringBuilder(); switch (type) { case IID: sb.append("ii:"); String id = normaliseLocatorReference(loc); if (!id.startsWith("http://") && !id.startsWith("#")) { sb.append("#"); } sb.append(id); break; case SID: sb.append("si:"); sb.append(loc.getAddress()); break; case SL: sb.append("si:"); sb.append(loc.getAddress()); break; } return sb.toString(); } /** * JTMTopicMapWriter has no additional properties. * @param properties */ public void setAdditionalProperties(Map<String, Object> properties) { // no-op } }