/* * #! * 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.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; 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.OccurrenceIF; 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.core.VariantNameIF; import net.ontopia.utils.GrabberIF; import net.ontopia.utils.OntopiaRuntimeException; import net.ontopia.utils.StringifierComparator; import net.ontopia.utils.StringifierIF; import net.ontopia.xml.CanonicalPrinter; import org.xml.sax.AttributeList; import org.xml.sax.DocumentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributeListImpl; /** * PUBLIC: A topic map writer that writes topic maps out to Ontopia's * Canonical XTM topic map format. This format is generally used for * testing and not for other purposes. * * <p><b>Note:</b> this is the format defined in <a * href="http://www.ontopia.net/topicmaps/materials/cxtm.html">an * Ontopia technical report</a>, not the upcoming standard format. * For new code, please use the standard format. */ public class CanonicalTopicMapWriter implements TopicMapWriterIF { protected DocumentHandler out; // If stream is instantiated here we'll close it when we're done. protected OutputStream stream; protected LocatorIF baseloc; // constants private AttributeListImpl empty = new AttributeListImpl(); /** * Creates a topic map writer bound to the file given in the arguments. * @param filename The name of the file to which the topic map is to * be written. */ public CanonicalTopicMapWriter(String filename) throws IOException { this(new File(filename)); } /** * Creates a topic map writer bound to the file given in the arguments. * @param file The file object to which the topic map is to be written. */ public CanonicalTopicMapWriter(File file) throws IOException { this.stream = new FileOutputStream(file); try { this.out = new CanonicalXTMPrinter(stream); } catch (UnsupportedEncodingException e) { throw new OntopiaRuntimeException(e); } } /** * Creates a topic map writer bound to the output stream given in * the arguments. * @param stream The output stream to which the topic map is to be * written. */ public CanonicalTopicMapWriter(OutputStream stream) { try { this.out = new CanonicalXTMPrinter(stream); } catch (UnsupportedEncodingException e) { throw new OntopiaRuntimeException(e); } } public void write(TopicMapIF topicmap) throws IOException { try { export(topicmap, out); if (stream != null) stream.close(); } catch (SAXException e) { if (e.getException() instanceof IOException) throw (IOException) e.getException(); throw new IOException("XML writing problem: " + e.toString()); } } /** * INTERNAL: Gets the base locator used to resolve relative locators. */ public LocatorIF getBaseLocator() { return baseloc; } /** * INTERNAL: Sets the base locator used to resolve relative locators. */ public void setBaseLocator(LocatorIF baseloc) { this.baseloc = baseloc; } // ===== THE EXPORT CODE ================================================= /** * PUBLIC: Exports the topic map to the given DocumentHandler. */ public void export(TopicMapIF topicmap, DocumentHandler dh) throws IOException, SAXException { dh.startDocument(); AttributeListImpl atts = new AttributeListImpl(); atts.addAttribute("xmlns", "CDATA", "http://www.topicmaps.org/cxtm/1.0/"); dh.startElement("topicMap", atts); atts.clear(); // topics ContextHolder context = createContext(topicmap); Iterator<TopicIF> it = context.topicsInOrder(topicmap.getTopics()); while (it.hasNext()) writeTopic(it.next(), dh, context); // associations Iterator<AssociationIF> ait = context.assocsInOrder(topicmap.getAssociations()); while (ait.hasNext()) writeAssociation(ait.next(), dh, context); dh.endElement("topicMap"); dh.endDocument(); } private ContextHolder createContext(TopicMapIF topicmap) { HashMap<TopicIF, String> topicIds = new HashMap<TopicIF, String>(); ContextHolder context = new ContextHolder(topicIds); Iterator<TopicIF> it = context.topicsInOrder(topicmap.getTopics()); int counter = 1; while (it.hasNext()) topicIds.put(it.next(), "id" + Integer.toString(counter++)); return context; } private void writeTopic(TopicIF topic, DocumentHandler dh, ContextHolder context) throws SAXException { AttributeListImpl atts = new AttributeListImpl(); atts.addAttribute("id", "ID", context.getTopicId(topic)); dh.startElement("topic", atts); atts.clear(); // instanceOf if (topic.getTypes().size() != 0) { Iterator<TopicIF> it = context.topicRefsInOrder(topic.getTypes()); while (it.hasNext()) writeInstanceOf(it.next(), dh, context); } // subjectIdentity if (topic.getSubjectLocators().size() > 0 || topic.getSubjectIdentifiers().size() > 0) { dh.startElement("subjectIdentity", empty); Iterator<LocatorIF> it = orderedIterator(topic.getSubjectLocators(), new StringifierComparator<LocatorIF>(new LocatorStringifier())); while (it.hasNext()) writeResourceRef(it.next(), dh); it = orderedIterator(topic.getSubjectIdentifiers(), new StringifierComparator<LocatorIF>(new LocatorStringifier())); while (it.hasNext()) { LocatorIF loc = it.next(); atts.addAttribute("href", "CDATA", resolveRelative(loc)); dh.startElement("subjectIndicatorRef", atts); atts.clear(); dh.endElement("subjectIndicatorRef"); } dh.endElement("subjectIdentity"); } // baseName if (topic.getTopicNames().size() > 0) { Iterator<TopicNameIF> it = context.baseNamesInOrder(topic.getTopicNames()); while (it.hasNext()) writeTopicName(it.next(), dh, context); } // occurrences Iterator<OccurrenceIF> it = orderedIterator(topic.getOccurrences(), new StringifierComparator<OccurrenceIF>(new OccurrenceStringifier())); while (it.hasNext()) { OccurrenceIF occ = it.next(); dh.startElement("occurrence", empty); if (occ.getType() != null) writeInstanceOf(occ.getType(), dh, context); writeScope(occ, dh, context); if (occ.getLocator() != null) writeResourceRef(occ.getLocator(), dh); else writeResourceData(occ.getValue(), dh); dh.endElement("occurrence"); } dh.endElement("topic"); } private void writeInstanceOf(TopicIF topic, DocumentHandler dh, ContextHolder context) throws SAXException { AttributeListImpl atts = new AttributeListImpl(); atts.addAttribute("href", "CDATA", "#" + context.getTopicId(topic)); dh.startElement("instanceOf", atts); dh.endElement("instanceOf"); } private void writeScope(ScopedIF scoped, DocumentHandler dh, ContextHolder context) throws SAXException { if (scoped.getScope().size() > 0) { dh.startElement("scope", empty); Iterator<TopicIF> it = context.topicRefsInOrder(scoped.getScope()); while (it.hasNext()) writeTopicRef(it.next(), dh, context); dh.endElement("scope"); } } private void writeTopicRef(TopicIF topic, DocumentHandler dh, ContextHolder context) throws SAXException { AttributeListImpl atts = new AttributeListImpl(); atts.addAttribute("href", "CDATA", "#" + context.getTopicId(topic)); dh.startElement("topicRef", atts); dh.endElement("topicRef"); } private void writeResourceRef(LocatorIF loc, DocumentHandler dh) throws SAXException { AttributeListImpl atts = new AttributeListImpl(); atts.addAttribute("href", "CDATA", resolveRelative(loc)); dh.startElement("resourceRef", atts); dh.endElement("resourceRef"); } private void writeResourceData(String data, DocumentHandler dh) throws SAXException { dh.startElement("resourceData", empty); if (data != null) { char[] chars = data.toCharArray(); dh.characters(chars, 0, chars.length); } dh.endElement("resourceData"); } private void writeTopicName(TopicNameIF basename, DocumentHandler dh, ContextHolder context) throws SAXException { dh.startElement("baseName", empty); if (basename.getType() != null) writeInstanceOf(basename.getType(), dh, context); writeScope(basename, dh, context); dh.startElement("baseNameString", empty); if (basename.getValue() != null) { char[] chars = basename.getValue().toCharArray(); dh.characters(chars, 0, chars.length); } dh.endElement("baseNameString"); if (basename.getVariants().size() > 0) { Iterator<VariantNameIF> it = context.variantsInOrder(basename.getVariants()); while (it.hasNext()) writeVariant(it.next(), dh, context); } dh.endElement("baseName"); } private void writeVariant(VariantNameIF variant, DocumentHandler dh, ContextHolder context) throws SAXException { dh.startElement("variant", empty); writeScope(variant, dh, context); dh.startElement("variantName", empty); if (variant.getLocator() == null) writeResourceData(variant.getValue(), dh); else writeResourceRef(variant.getLocator(), dh); dh.endElement("variantName"); dh.endElement("variant"); } private void writeAssociation(AssociationIF assoc, DocumentHandler dh, ContextHolder context) throws SAXException { dh.startElement("association", empty); if (assoc.getType() != null) writeInstanceOf(assoc.getType(), dh, context); writeScope(assoc, dh, context); Iterator<AssociationRoleIF> it = context.rolesInOrder(assoc.getRoles()); while (it.hasNext()) { AssociationRoleIF role = it.next(); dh.startElement("member", empty); if (role.getType() != null) writeInstanceOf(role.getType(), dh, context); if (role.getPlayer() != null) writeTopicRef(role.getPlayer(), dh, context); dh.endElement("member"); } dh.endElement("association"); } // --- Utility methods private <T> Iterator<T> orderedIterator(Collection<T> coll, Comparator<? super T> comparator) { List<T> list = new ArrayList<T>(coll); Collections.sort(list, comparator); return list.iterator(); } private String resolveRelative(LocatorIF locator) { // resolve locator relatively to base locator if (baseloc == null) return locator.getExternalForm(); else { // HACK: replace this code with baseloc.resolveRelative(locator); String base = baseloc.getExternalForm(); String address = locator.getExternalForm(); String pbase = null; int lix = base.lastIndexOf("/"); if (lix > 0) pbase = base.substring(0, lix+1); // TODO: walk up the entire path this way if (address.startsWith(base)) return address.substring(base.length()); else if (pbase != null && address.startsWith(pbase)) return address.substring(pbase.length()); else return address; } } /** * CanonicalTopicMapWriter has no additional properties. * @param properties */ public void setAdditionalProperties(Map<String, Object> properties) { // no-op } // --- Comparators static abstract class AbstractComparator<T> implements Comparator<T> { protected <O> int compareObjects(Comparable<O> obj1, O obj2) { // Compares two objects; null values means lower ordering if (obj1 == null) { if (obj2 != null) return -1; return 0; } else { if (obj2 == null) return 1; return obj1.compareTo(obj2); } } protected <O> int compareObjects(O obj1, O obj2, Comparator<? super O> comparator) { // Compares two objects; null values means lower ordering if (obj1 == null) { if (obj2 != null) return -1; return 0; } else { if (obj2 == null) return 1; return comparator.compare(obj1, obj2); } } @SuppressWarnings("unchecked") protected <O> int compareCollections(Collection<O> coll1, Collection<O> coll2, Comparator<? super O> comparator) { // Convert collections to arrays Object[] array1 = coll1.toArray(); Object[] array2 = coll2.toArray(); // Sort the arrays Arrays.sort((O[])array1, comparator); Arrays.sort((O[])array2, comparator); // Compare individual items int length = (array1.length < array2.length ? array1.length : array2.length); for (int i=0; i < length; i++) { int cval = comparator.compare((O)array1[i], (O)array2[i]); if (cval != 0) return cval; } // Compare array sizes if (array1.length > array2.length) return 1; else if (array1.length < array2.length) return -1; else return 0; } } static class TopicComparator extends AbstractComparator<TopicIF> { protected static TopicComparator instance; public static TopicComparator getInstance() { if (instance == null) instance = new TopicComparator(); return instance; } public int compare(TopicIF topic1, TopicIF topic2) { if (topic1 == topic2) return 0; // Compare the subject int cval0 = compareCollections(topic1.getSubjectLocators(), topic2.getSubjectLocators(), LocatorComparator.getInstance()); if (cval0 != 0) return cval0; // Compare subject indicators int cval1 = compareCollections(topic1.getSubjectIdentifiers(), topic2.getSubjectIdentifiers(), LocatorComparator.getInstance()); if (cval1 != 0) return cval1; // Compare basenames int cval2 = compareCollections(topic1.getTopicNames(), topic2.getTopicNames(), TopicNameComparator.getInstance()); if (cval2 != 0) return cval2; // Compare occurrences int cval3 = compareCollections(topic1.getOccurrences(), topic2.getOccurrences(), OccurrenceComparator.getInstance()); if (cval3 != 0) return cval3; // Compare types int cval4 = compareCollections(topic1.getTypes(), topic2.getTypes(), TopicComparator.getInstance()); if (cval4 != 0) return cval4; // Compare source locators int cval5 = compareCollections(topic1.getItemIdentifiers(), topic2.getItemIdentifiers(), LocatorComparator.getInstance()); if (cval5 != 0) return cval5; // Compare object ids return topic1.getObjectId().compareTo(topic2.getObjectId()); } } static class LocatorComparator extends AbstractComparator<LocatorIF> { protected static LocatorComparator instance; public static LocatorComparator getInstance() { if (instance == null) instance = new LocatorComparator(); return instance; } public int compare(LocatorIF loc1, LocatorIF loc2) { if (loc1 == loc2) return 0; // Compare address int c_address = loc1.getExternalForm().compareTo(loc2.getExternalForm()); if (c_address != 0) return c_address; // Compare notation return loc1.getNotation().compareTo(loc2.getNotation()); } } static class TopicNameComparator extends AbstractComparator<TopicNameIF> { protected static TopicNameComparator instance; public static TopicNameComparator getInstance() { if (instance == null) instance = new TopicNameComparator(); return instance; } public int compare(TopicNameIF bn1, TopicNameIF bn2) { if (bn1 == bn2) return 0; // Compare basename values int cval1 = compareObjects(bn1.getValue(), bn2.getValue()); if (cval1 != 0) return cval1; // Compare scope int cval2 = compareCollections(bn1.getScope(), bn2.getScope(), TopicComparator.getInstance()); if (cval2 != 0) return cval2; // Compare variant names return compareCollections(bn1.getVariants(), bn2.getVariants(), VariantNameComparator.getInstance()); } } static class VariantNameComparator extends AbstractComparator<VariantNameIF> { protected static VariantNameComparator instance; public static VariantNameComparator getInstance() { if (instance == null) instance = new VariantNameComparator(); return instance; } public int compare(VariantNameIF vn1, VariantNameIF vn2) { if (vn1 == vn2) return 0; // Compare variant name values int cval1 = compareObjects(vn1.getValue(), vn2.getValue()); if (cval1 != 0) return cval1; // Compare variant name locators int cval2 = compareObjects(vn1.getLocator(), vn2.getLocator(), LocatorComparator.getInstance()); if (cval2 != 0) return cval2; // Compare scope return compareCollections(vn1.getScope(), vn2.getScope(), TopicComparator.getInstance()); } } static class OccurrenceComparator extends AbstractComparator<OccurrenceIF> { protected static OccurrenceComparator instance; public static OccurrenceComparator getInstance() { if (instance == null) instance = new OccurrenceComparator(); return instance; } public int compare(OccurrenceIF occ1, OccurrenceIF occ2) { if (occ1 == occ2) return 0; // Compare occurrence values int cval1 = compareObjects(occ1.getValue(), occ2.getValue()); if (cval1 != 0) return cval1; // Compare occurrence locators int cval2 = compareObjects(occ1.getLocator(), occ2.getLocator(), LocatorComparator.getInstance()); if (cval2 != 0) return cval2; // Compare type int cval3 = compareObjects(occ1.getType(), occ2.getType(), TopicComparator.getInstance()); if (cval3 != 0) return cval3; // Compare scope return compareCollections(occ1.getScope(), occ2.getScope(), TopicComparator.getInstance()); } } static class AssociationComparator extends AbstractComparator<AssociationIF> { protected static AssociationComparator instance; public static AssociationComparator getInstance() { if (instance == null) instance = new AssociationComparator(); return instance; } public int compare(AssociationIF assoc1, AssociationIF assoc2) { if (assoc1 == assoc2) return 0; // Compare type int cval1 = compareObjects(assoc1.getType(), assoc2.getType(), TopicComparator.getInstance()); if (cval1 != 0) return cval1; // Compare scope int cval2 = compareCollections(assoc1.getScope(), assoc2.getScope(), TopicComparator.getInstance()); if (cval2 != 0) return cval2; // Compare roles return compareCollections(assoc1.getRoles(), assoc2.getRoles(), AssociationRoleComparator.getInstance()); } } static class AssociationRoleComparator extends AbstractComparator<AssociationRoleIF> { protected static AssociationRoleComparator instance; public static AssociationRoleComparator getInstance() { if (instance == null) instance = new AssociationRoleComparator(); return instance; } public int compare(AssociationRoleIF role1, AssociationRoleIF role2) { if (role1 == role2) return 0; // Compare types int cval2 = compareObjects(role1.getType(), role2.getType(), TopicComparator.getInstance()); if (cval2 != 0) return cval2; // Compare players return compareObjects(role1.getPlayer(), role2.getPlayer(), TopicComparator.getInstance()); } } // --- Sort key stringifiers class TopicRefStringifier implements StringifierIF<TopicIF> { private Map<TopicIF, String> topicIds; public TopicRefStringifier(Map<TopicIF, String> topicIds) { this.topicIds = topicIds; } public String toString(TopicIF topic) { return topicIds.get(topic); } } // --- Other utilities class ContextHolder { private Map<TopicIF, String> topicIds; private Comparator<TopicIF> topicComparator; private Comparator<TopicIF> topicRefComparator; private Comparator<TopicNameIF> baseNameComparator; private Comparator<VariantNameIF> variantNameComparator; private Comparator<AssociationIF> assocComparator; private Comparator<AssociationRoleIF> roleComparator; public ContextHolder(Map<TopicIF, String> topicIds) { this.topicIds = topicIds; topicRefComparator = new StringifierComparator<TopicIF>(new TopicRefStringifier(topicIds)); topicComparator = TopicComparator.getInstance(); baseNameComparator = TopicNameComparator.getInstance(); variantNameComparator = VariantNameComparator.getInstance(); assocComparator = AssociationComparator.getInstance(); roleComparator = AssociationRoleComparator.getInstance(); } public String getTopicId(TopicIF topic) { return topicIds.get(topic); } public Iterator<TopicIF> topicsInOrder(Collection<TopicIF> topics) { return orderedIterator(topics, topicComparator); } public Iterator<TopicIF> topicRefsInOrder(Collection<TopicIF> topics) { return orderedIterator(topics, topicRefComparator); } public Iterator<VariantNameIF> variantsInOrder(Collection<VariantNameIF> variants) { return orderedIterator(variants, variantNameComparator); } public Iterator<TopicNameIF> baseNamesInOrder(Collection<TopicNameIF> basenames) { return orderedIterator(basenames, baseNameComparator); } public Iterator<AssociationIF> assocsInOrder(Collection<AssociationIF> assocs) { return orderedIterator(assocs, assocComparator); } public Iterator<AssociationRoleIF> rolesInOrder(Collection<AssociationRoleIF> roles) { return orderedIterator(roles, roleComparator); } } class TopicRefComparator implements Comparator<TopicIF> { private Map<TopicIF, String> topicIds; public TopicRefComparator(Map<TopicIF, String> topicIds) { this.topicIds = topicIds; } public int compare(TopicIF o1, TopicIF o2) { String s1 = topicIds.get(o1); String s2 = topicIds.get(o2); return s1.compareTo(s2); } } class FirstGrabber<T> implements GrabberIF<Collection<T>, T> { private Comparator<? super T> comparator; public FirstGrabber(Comparator<? super T> comparator) { this.comparator = comparator; } public T grab(Collection<T> coll) { return orderedIterator(coll, comparator).next(); } } class StringComparator implements Comparator<String> { public int compare(String s1, String s2) { return s1.compareTo(s2); } } class LocatorStringifier implements StringifierIF<LocatorIF> { public String toString(LocatorIF loc) { return loc.getExternalForm(); } } class OccurrenceStringifier implements StringifierIF<OccurrenceIF> { public String toString(OccurrenceIF occ) { if (occ.getLocator() != null) return occ.getLocator().getExternalForm(); else return "$" + occ.getValue(); } } // --- XML writer public class CanonicalXTMPrinter extends CanonicalPrinter { public CanonicalXTMPrinter(OutputStream stream) throws UnsupportedEncodingException { super(stream); } public void startElement(String name, AttributeList atts) { super.startElement(name, atts); if (!"baseNameString".equals(name) && !"resourceData".equals(name) && !"topicRef".equals(name) && !"instanceOf".equals(name) && !"resourceRef".equals(name) && !"subjectIndicatorRef".equals(name)) writer.print("\n"); } public void endElement(String name) { super.endElement(name); writer.print("\n"); } } }