/* * #! * 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.Reader; import java.io.Writer; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.xml.sax.AttributeList; import org.xml.sax.helpers.AttributeListImpl; import net.ontopia.xml.CanonicalPrinter; import net.ontopia.infoset.core.LocatorIF; import net.ontopia.topicmaps.core.AssociationIF; import net.ontopia.topicmaps.core.AssociationRoleIF; import net.ontopia.topicmaps.core.ConstraintViolationException; 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.core.TypedIF; import net.ontopia.topicmaps.core.VariantNameIF; import net.ontopia.topicmaps.core.index.ClassInstanceIndexIF; import net.ontopia.topicmaps.utils.DuplicateSuppressionUtils; import net.ontopia.topicmaps.utils.PSI; import net.ontopia.utils.IteratorComparator; import net.ontopia.utils.ObjectUtils; import net.ontopia.utils.OntopiaRuntimeException; import net.ontopia.utils.CompactHashSet; /** * PUBLIC: A topic map writer that writes topic maps out to the format * defined in ISO 13250-4: Topic Maps -- Canonicalization. The format * is also known as Canonical XTM, but should not be confused with * that defined by Ontopia. The current implementation conforms to the * final standard (ISO 13250-4:2009). * * @since 2.0.3 */ public class CanonicalXTMWriter implements TopicMapWriterIF { private CanonicalPrinter out; private AttributeListImpl EMPTY; private Map tmIndex; // Maps TMObjectIFs to corresponding index within parent private Map extraRoles; // TopicIF -> List<AssocRoleIFs for type-instance> private String base; private String strippedBase; private TopicIF typeInstance; // possibly fake private TopicIF instance; // possibly fake private TopicIF type; // possibly fake private static TopicMapIF tmForFake; // should only be used by fake nested classes. private final AssociationComparator associationComparator = new AssociationComparator(); private final AssociationRoleComparator associationRoleComparator = new AssociationRoleComparator(); private final NameComparator nameComparator = new NameComparator(); private final OccurrenceComparator occurrenceComparator = new OccurrenceComparator(); private final LocatorComparator locatorComparator = new LocatorComparator(); private final TopicComparator topicComparator = new TopicComparator(); private final VariantComparator variantComparator = new VariantComparator(); private Comparator indexComparator; private Set startNewlineElem; private static final char[] LINEBREAK = { (char) 0x0A }; public CanonicalXTMWriter(OutputStream out) throws UnsupportedEncodingException { this.out = new CanonicalPrinter(out); init(); } /** * PUBLIC: Creates a canonicalizer that writes to the given Writer * in whatever encoding that Writer uses. <b>Warning:</b> Canonical * XTM requires the output encoding to be UTF-8, so for correct * results the given Writer <i>must</i> produce UTF-8. Using this * method is <b>not</b> recommended. */ public CanonicalXTMWriter(Writer out) { this.out = new CanonicalPrinter(out); init(); } private void init() { this.EMPTY = new AttributeListImpl(); this.startNewlineElem = new CompactHashSet(12); this.extraRoles = new HashMap(); startNewlineElem.add("topicMap"); startNewlineElem.add("topic"); startNewlineElem.add("name"); startNewlineElem.add("variant"); startNewlineElem.add("occurrence"); startNewlineElem.add("association"); startNewlineElem.add("role"); startNewlineElem.add("scope"); startNewlineElem.add("itemIdentifiers"); startNewlineElem.add("subjectIdentifiers"); startNewlineElem.add("subjectLocators"); } public void write(TopicMapIF topicmap) { DuplicateSuppressionUtils.removeDuplicates(topicmap); tmForFake = topicmap; base = topicmap.getStore().getBaseAddress().getAddress(); strippedBase = stripLocator(base); Object[] topics = getTopics(topicmap); Object[] associations = getAssociations(topicmap); recordIndexes(topics, associations); out.startDocument(); startElement("topicMap", reifier(topicmap)); writeLocators(topicmap.getItemIdentifiers(), "itemIdentifiers"); for (int ix = 0; ix < topics.length; ix++) write((TopicIF) topics[ix]); for (int ix = 0; ix < associations.length; ix++) write((AssociationIF) associations[ix], ix + 1); endElement("topicMap"); out.endDocument(); } /** * Maps topics, topic names, variant names, occurrences, associations and * association roles to an index value (given as a string). * Index value is the canonically ordered position in the parent object. * @param topics The topic (with names and occurrences) make indexes for. * @param associations (with roles) to make indexes for. * Post: the paramaters 'topics' and 'associations are in canonical order. */ private void recordIndexes(Object[] topics, Object[] associations) { // Create necessary objects tmIndex = new HashMap(); indexComparator = new IndexComparator(tmIndex); // Sort the topics in canonical order. Arrays.sort(topics, topicComparator); // Map each topic to its canonical position within the topic map. for (int i = 0; i < topics.length; i++) tmIndex.put(topics[i], new Integer(i + 1)); // Sort associations in canonical order Arrays.sort(associations, associationComparator); // For each association (in canonical order) of the topic map for (int i = 0; i < associations.length; i++) { AssociationIF assoc = (AssociationIF) associations[i]; // Map the association to it's position within the topic map. tmIndex.put(assoc, new Integer(i + 1)); Object roles[] = assoc.getRoles().toArray(); Arrays.sort(roles, associationRoleComparator); // For each association role (in canonical order) of the association for (int j = 0; j < roles.length; j++) // Map the role to it's position within the association. tmIndex.put(roles[j], new Integer(j + 1)); } } private void write(TopicIF topic) { AttributeListImpl attributes = new AttributeListImpl(); attributes.addAttribute("number", null, "" + tmIndex.get(topic)); startElement("topic", attributes); attributes.clear(); writeLocators(topic.getSubjectIdentifiers(), "subjectIdentifiers"); writeLocators(topic.getSubjectLocators(), "subjectLocators"); writeLocators(topic.getItemIdentifiers(), "itemIdentifiers"); Object[] names = topic.getTopicNames().toArray(); Arrays.sort(names, nameComparator); for (int ix = 0; ix < names.length; ix++) write((TopicNameIF) names[ix], ix + 1); Object[] occurrences = makeFakes(topic.getOccurrences().toArray()); Arrays.sort(occurrences, occurrenceComparator); for (int ix = 0; ix < occurrences.length; ix++) write((OccurrenceIF) occurrences[ix], ix + 1); Collection r = new ArrayList(topic.getRoles()); Collection extras = (Collection) extraRoles.get(topic); if (extras != null) r.addAll(extras); Object[] roles = r.toArray(); Arrays.sort(roles, associationRoleComparator); for (int ix = 0; ix < roles.length; ix++) { AssociationRoleIF currentRole = (AssociationRoleIF)roles[ix]; AssociationIF currentAssociation = currentRole.getAssociation(); AttributeListImpl roleAttributes = new AttributeListImpl(); String refValue = "association." + tmIndex.get(currentAssociation) + ".role." + tmIndex.get(currentRole); roleAttributes.addAttribute("ref", null, refValue); startElement("rolePlayed", roleAttributes); endElement("rolePlayed"); } endElement("topic"); } private void write(TopicNameIF basename, int number) { AttributeListImpl attributes = reifier(basename); attributes.addAttribute("number", null, "" + number); startElement("name", attributes); attributes.clear(); write(basename.getValue()); writeType(basename); write(basename.getScope()); Object[] variants = basename.getVariants().toArray(); Arrays.sort(variants, variantComparator); for (int ix = 0; ix < variants.length; ix++) write((VariantNameIF) variants[ix], ix + 1); writeLocators(basename.getItemIdentifiers(), "itemIdentifiers"); endElement("name"); } private void write(VariantNameIF variant, int number) { AttributeListImpl attributes = reifier(variant); attributes.addAttribute("number", null, "" + number); startElement("variant", attributes); attributes.clear(); if (ObjectUtils.equals(variant.getDataType(), DataTypes.TYPE_URI)) { LocatorIF locator = variant.getLocator(); if (locator != null) write(normaliseLocatorReference(locator.getAddress())); } else { String value = variant.getValue(); if (value != null) write(value); } write(variant.getDataType(), "datatype"); write(variant.getScope()); writeLocators(variant.getItemIdentifiers(), "itemIdentifiers"); endElement("variant"); } private Object[] makeFakes(Object[] occs) { for (int ix = 0; ix < occs.length; ix++) { OccurrenceIF original = (OccurrenceIF) occs[ix]; occs[ix] = new FakeOccurrence(original); } return occs; } private void write(OccurrenceIF occurrence, int number) { AttributeListImpl attributes = reifier(occurrence); attributes.addAttribute("number", null, "" + number); startElement("occurrence", attributes); attributes.clear(); write(occurrence.getValue()); // normalized in FakeOccurrence below write(occurrence.getDataType(), "datatype"); writeType(occurrence); write(occurrence.getScope()); writeLocators(occurrence.getItemIdentifiers(), "itemIdentifiers"); endElement("occurrence"); } private void write(AssociationIF association, int number) { AttributeListImpl attributes = reifier(association); attributes.addAttribute("number", null, "" + number); startElement("association", attributes); attributes.clear(); writeType(association); Object[] roles = association.getRoles().toArray(); Arrays.sort(roles, associationRoleComparator); for (int ix = 0; ix < roles.length; ix++) write((AssociationRoleIF) roles[ix], ix + 1); write(association.getScope()); writeLocators(association.getItemIdentifiers(), "itemIdentifiers"); endElement("association"); } private void write(AssociationRoleIF role, int number) { AttributeListImpl attributes = reifier(role); attributes.addAttribute("number", null, "" + number); startElement("role", attributes); attributes.clear(); startElement("player", topicRef(role.getPlayer())); endElement("player"); writeType(role); writeLocators(role.getItemIdentifiers(), "itemIdentifiers"); endElement("role"); } private void write(Collection scope) { if (scope.isEmpty()) return; startElement("scope", EMPTY); Object[] topics = scope.toArray(); Arrays.sort(topics, indexComparator); for (int ix = 0; ix < topics.length; ix++) { startElement("scopingTopic", topicRef((TopicIF) topics[ix])); endElement("scopingTopic"); } endElement("scope"); } private void writeType(TypedIF object) { TopicIF topic = object.getType(); if (topic == null) { throw new OntopiaRuntimeException("TypedIF had null type: " + object); } startElement("type", topicRef(topic)); endElement("type"); } private void write(String value) { if (value == null) throw new OntopiaRuntimeException("Object had null value"); startElement("value", EMPTY); out.characters(value.toCharArray(), 0, value.length()); endElement("value"); } private void write(LocatorIF uri, String element) { startElement(element, EMPTY); String value = uri.getAddress(); out.characters(value.toCharArray(), 0, value.length()); endElement(element); } private void write(LocatorIF locator) { String address = normaliseLocatorReference(locator.getAddress()); startElement("locator", EMPTY); out.characters(address.toCharArray(), 0, address.length()); endElement("locator"); } private void writeLocators(Collection locators, String elementName) { Object locs[] = locators.toArray(); Arrays.sort(locs, locatorComparator); if (locs.length > 0) { startElement(elementName, EMPTY); for (int i = 0; i < locs.length; i++) { LocatorIF loc = (LocatorIF) locs[i]; write(loc); } endElement(elementName); } } // --- XML handling private void startElement(String element, AttributeList atts) { out.startElement(element, atts); if (startNewlineElem.contains(element)) writeln(); } private void endElement(String element) { out.endElement(element); writeln(); } private void writeln() { out.characters(LINEBREAK, 0, 1); } // --- Helpers private AttributeListImpl reifier(ReifiableIF reified) { TopicIF reifier = reified.getReifier(); if (reifier == null) return EMPTY; AttributeListImpl atts = new AttributeListImpl(); atts.addAttribute("reifier", null, String.valueOf(tmIndex.get(reifier))); return atts; } /** * @return an attribute list with a reference to a given topic. */ private AttributeList topicRef(TopicIF topic) { AttributeListImpl atts = new AttributeListImpl(); atts.addAttribute("topicref", null, "" + tmIndex.get(topic)); return atts; } /** * @return an array with all the topics of a given topic map. */ private Object[] getTopics(TopicMapIF topicmap) { Collection topics = new ArrayList(topicmap.getTopics().size() + 4); topics.addAll(topicmap.getTopics()); // add the type-instance PSI topics, if necessary ClassInstanceIndexIF index = (ClassInstanceIndexIF) topicmap .getIndex("net.ontopia.topicmaps.core.index.ClassInstanceIndexIF"); if (!index.getTopicTypes().isEmpty()) { typeInstance = getTopic(topicmap, PSI.getSAMTypeInstance(), topics); instance = getTopic(topicmap, PSI.getSAMInstance(), topics); type = getTopic(topicmap, PSI.getSAMType(), topics); } return topics.toArray(); } /** * @return an array with all the associations a given topic map. */ private Object[] getAssociations(TopicMapIF topicmap) { ClassInstanceIndexIF index = (ClassInstanceIndexIF) topicmap .getIndex("net.ontopia.topicmaps.core.index.ClassInstanceIndexIF"); if (index.getTopicTypes().isEmpty()) return topicmap.getAssociations().toArray(); Collection assocs = new ArrayList(topicmap.getAssociations()); Iterator it = index.getTopicTypes().iterator(); while (it.hasNext()) { TopicIF thetype = (TopicIF) it.next(); Iterator it2 = index.getTopics(thetype).iterator(); while (it2.hasNext()) { TopicIF theinstance = (TopicIF) it2.next(); AssociationIF assoc = new FakeAssociation(thetype, theinstance); recordRole(thetype, assoc.getRolesByType(type)); recordRole(theinstance, assoc.getRolesByType(instance)); assocs.add(assoc); } } return assocs.toArray(); } private void recordRole(TopicIF topic, Collection roles) { Collection extra = (Collection) extraRoles.get(topic); if (extra == null) { extra = new ArrayList(); extraRoles.put(topic, extra); } extra.addAll(roles); } /** * Return the topic with a given PSI. Create a fake topic for it if * does not exist, and add it to the topics collection. */ private TopicIF getTopic(TopicMapIF tm, LocatorIF indicator, Collection topics) { TopicIF topic = tm.getTopicBySubjectIdentifier(indicator); if (topic == null) { topic = new FakeTopic(indicator, tmForFake); topics.add(topic); } return topic; } // --- Datatype normalisation private String normalizeNumber(String number) { if (number.indexOf('.') > -1) return normalizeDecimal(number); else return normalizeInteger(number); } // NOTE: The following two methods are copied from tinyTiM, donated // by Lars Heuer private static String normalizeInteger(final String value) { final String val = value.trim(); int len = val.length(); if (len == 0) throw new IllegalArgumentException("Illegal integer value: " + value); int idx = 0; boolean negative = false; switch (val.charAt(idx)) { case '-': idx++; negative = true; break; case '+': idx++; break; } // Skip leading zeros if any while (idx < len && val.charAt(idx) == '0') { idx++; } if (idx == len) { return "0"; } final String normalized = val.substring(idx); len = normalized.length(); // Check if everything is a digit for (int i = 0; i < len; i++) { if (!Character.isDigit(normalized.charAt(i))) { throw new IllegalArgumentException("Illegal integer value: " + value); } } return negative && normalized.charAt(0) != 0 ? '-' + normalized : normalized; } private static String normalizeDecimal(final String value) { final String val = value.trim(); int len = val.length(); if (len == 0) throw new IllegalArgumentException("Illegal decimal value: " + value); int idx = 0; boolean negative = false; switch (val.charAt(idx)) { case '-': idx++; negative = true; break; case '+': idx++; break; } // Skip leading zeros if any while (idx < len && val.charAt(idx) == '0') { idx++; } if (idx == len) { return "0.0"; } StringBuilder normalized = new StringBuilder(len); if (val.charAt(idx) == '.') { normalized.append('0'); } else { while (idx < len && val.charAt(idx) != '.') { char c = val.charAt(idx); if (!Character.isDigit(c)) { throw new IllegalArgumentException("Illegal decimal value: " + value); } normalized.append(c); idx++; } } normalized.append('.'); len--; while (len >= idx && val.charAt(len) == '0') { len--; } if (len <= idx) { normalized.append('0'); if (normalized.charAt(0) == '0') { return "0.0"; } } else { // idx points to the '.', increment it idx++; while (idx <= len) { char c = val.charAt(idx); if (!Character.isDigit(c)) { throw new IllegalArgumentException("Illegal decimal value: " + value); } normalized.append(c); idx++; } } return negative ? '-' + normalized.toString() : normalized.toString(); } /** * Normalise a given locator reference according to CXTM spec. */ private String normaliseLocatorReference(String reference) { String retVal = reference.substring(longestCommonPath(reference, strippedBase).length()); if (retVal.startsWith("/")) retVal = retVal.substring(1); return retVal; } /** * 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; } /** * Remove the fragment- and query-parts of a given locatorString. * @param locatorString The string from which to remove parts. * @return The string after the necessary removing parts. */ private String stripLocator(String locatorString) { String retVal = locatorString; int queryIndex = retVal.indexOf('?'); if (queryIndex > 0) retVal = retVal.substring(0, queryIndex); int hashIndex = retVal.indexOf('#'); if (hashIndex > 0) retVal = retVal.substring(0, hashIndex); return retVal; } /** * CanonicalXTMWriter has no additional properties. * @param properties */ public void setAdditionalProperties(Map<String, Object> properties) { // no-op } // --- Comparators abstract class AbstractComparator implements Comparator { protected int compareLocatorSet(Collection c1, Collection c2) { if (c1.size() < c2.size()) return -1; if (c1.size() > c2.size()) return 1; // INV: locator sets must now be of equal size. Object locators1[] = c1.toArray(); Object locators2[] = c2.toArray(); Arrays.sort(locators1, locatorComparator); Arrays.sort(locators2, locatorComparator); for (int i = 0; i < locators1.length; i++) { int currentCmp = compareLocator((LocatorIF) locators1[i], (LocatorIF) locators2[i]); if (currentCmp != 0) return currentCmp; } return 0; } protected int compareTopicSet(Collection c1, Collection c2) { int cmp = c1.size() - c2.size(); Iterator it1 = c1.iterator(); Iterator it2 = c2.iterator(); while (cmp == 0 && it1.hasNext()) { TopicIF t1 = (TopicIF) it1.next(); TopicIF t2 = (TopicIF) it2.next(); cmp = compareTopic(t1, t2); } return cmp; } protected int compareSet(Collection c1, Collection c2, Comparator comp) { int cmp = c1.size() - c2.size(); Iterator it1 = c1.iterator(); Iterator it2 = c2.iterator(); while (cmp == 0 && it1.hasNext()) { cmp = comp.compare(it1.next(), it2.next()); } return cmp; } protected int compareString(String s1, String s2) { if ((s1 == null) && (s2 == null)) return 0; if (s1 == null) return -1; if (s2 == null) return 1; return s1.compareTo(s2); } protected int compareLocator(LocatorIF l1, LocatorIF l2) { if (l1 == l2) return 0; if (l1 == null) return -1; if (l2 == null) return 1; int cmp = normaliseLocatorReference(l1.getAddress()) .compareTo(normaliseLocatorReference(l2.getAddress())); if (cmp == 0) cmp = l1.getNotation().compareTo(l2.getNotation()); return cmp; } protected int compareTopic(TopicIF t1, TopicIF t2) { if (t1 == t2) return 0; if (t1 == null) return -1; if (t2 == null) return 1; int pos1 = ((Integer)tmIndex.get(t1)).intValue(); int pos2 = ((Integer)tmIndex.get(t2)).intValue(); return pos1 - pos2; } protected int compareAssociation(AssociationIF a1, AssociationIF a2) { if (a1 == a2) return 0; if (a1 == null) return -1; if (a2 == null) return 1; int pos1 = ((Integer)tmIndex.get(a1)).intValue(); int pos2 = ((Integer)tmIndex.get(a2)).intValue(); return pos1 - pos2; } } public static class IndexComparator implements Comparator { private Map indexMap; public IndexComparator(Map indexMap) { this.indexMap = indexMap; } public int compare(Object o1, Object o2) { Integer index1 = (Integer)indexMap.get(o1); Integer index2 = (Integer)indexMap.get(o2); if (index1 == null) { if (index2 == null) return 0; return -1; } if (index2 == null) return 1; return index1.intValue() - index2.intValue(); } } class LocatorComparator extends AbstractComparator { public int compare(Object o1, Object o2) { return compareLocator((LocatorIF)o1, (LocatorIF)o2); } } class TopicComparator extends AbstractComparator { public int compare(Object o1, Object o2) { TopicIF t1 = (TopicIF) o1; TopicIF t2 = (TopicIF) o2; int cmp = compareLocatorSet(t1.getSubjectIdentifiers(), t2.getSubjectIdentifiers()); if (cmp == 0) cmp = compareLocatorSet(t1.getSubjectLocators(), t2.getSubjectLocators()); if (cmp == 0) cmp = compareLocatorSet(t1.getItemIdentifiers(), t2.getItemIdentifiers()); return cmp; } } class NameComparator extends AbstractComparator { public int compare(Object o1, Object o2) { TopicNameIF bn1 = (TopicNameIF) o1; TopicNameIF bn2 = (TopicNameIF) o2; int cmp = compareString(bn1.getValue(), bn2.getValue()); // FIXME: Compare by type here when we can! if (cmp == 0) cmp = compareTopicSet(bn1.getScope(), bn2.getScope()); return cmp; } } class SetComparator extends AbstractComparator { private Comparator elementComparator; public SetComparator(Comparator elementComparator) { this.elementComparator = elementComparator; } public int compare(Object o1, Object o2) { Collection c1 = (Collection) o1; Collection c2 = (Collection) o2; int cmp = c1.size() - c2.size(); Iterator it1 = c1.iterator(); Iterator it2 = c2.iterator(); while (cmp == 0 && it1.hasNext()) { cmp = elementComparator.compare(it1.next(), it2.next()); } return cmp; } } class VariantComparator extends AbstractComparator { public int compare(Object o1, Object o2) { VariantNameIF vn1 = (VariantNameIF) o1; VariantNameIF vn2 = (VariantNameIF) o2; int cmp = compareString(vn1.getValue(), vn2.getValue()); if (cmp == 0) cmp = compareLocator(vn1.getLocator(), vn2.getLocator()); if (cmp == 0) cmp = compareTopicSet(vn1.getScope(), vn2.getScope()); return cmp; } } class OccurrenceComparator extends AbstractComparator { public int compare(Object o1, Object o2) { OccurrenceIF occ1 = (OccurrenceIF) o1; OccurrenceIF occ2 = (OccurrenceIF) o2; int cmp = compareString(occ1.getValue(), occ2.getValue()); if (cmp == 0) cmp = compareLocator(occ1.getDataType(), occ2.getDataType()); if (cmp == 0) cmp = compareTopic(occ1.getType(), occ2.getType()); if (cmp == 0) cmp = compareTopicSet(occ1.getScope(), occ2.getScope()); return cmp; } } class AssociationComparator extends AbstractComparator { private Comparator collectionComparator; public AssociationComparator() { collectionComparator = new CollectionSizeFirstComparator( new RoleInAssociationComparator()); } public int compare(Object o1, Object o2) { AssociationIF assoc1 = (AssociationIF) o1; AssociationIF assoc2 = (AssociationIF) o2; int cmp = compareTopic(assoc1.getType(), assoc2.getType()); if (cmp == 0) cmp = collectionComparator.compare(assoc1.getRoles(), assoc2.getRoles()); if (cmp == 0) cmp = compareTopicSet(assoc1.getScope(), assoc2.getScope()); return cmp; } } class RoleInAssociationComparator extends AbstractComparator { public int compare(Object o1, Object o2) { AssociationRoleIF role1 = (AssociationRoleIF) o1; AssociationRoleIF role2 = (AssociationRoleIF) o2; int cmp = compareTopic(role1.getPlayer(), role2.getPlayer()); if (cmp == 0) cmp = compareTopic(role1.getType(), role2.getType()); // No need to compare the parent assocaitions since this comparator only // compares roles within one assocaition. return cmp; } } class AssociationRoleComparator extends AbstractComparator { public int compare(Object o1, Object o2) { AssociationRoleIF role1 = (AssociationRoleIF) o1; AssociationRoleIF role2 = (AssociationRoleIF) o2; int cmp = compareTopic(role1.getPlayer(), role2.getPlayer()); if (cmp == 0) cmp = compareTopic(role1.getType(), role2.getType()); if (cmp == 0) cmp = compareAssociation(role1.getAssociation(), role2.getAssociation()); return cmp; } } /** * Comparator for Collections. * Collections of fewer elements are ordered before Collections with more. * Collecitons of equal size are sorted, and then compared element-wise. */ class CollectionSizeFirstComparator extends CollectionComparator { public CollectionSizeFirstComparator (Comparator elementComparator) { super(elementComparator); } public CollectionSizeFirstComparator (Comparator betweenComparator, Comparator withinComparator) { super(betweenComparator, withinComparator); } public int compare(Object o1, Object o2) { if (o1 == o2) return 0; Collection c1 = (Collection)o1; Collection c2 = (Collection)o2; // Order Collection in increasing order by size. if (c1.size() > c2.size()) return 1; if (c1.size() < c2.size()) return -1; return super.compare(c1, c2); } } /** * Comparator for Collections that first compares the elements, and then * the size of the collection. * The Collecitons are sorted, and then compared element-wise. * If the Collections are of equal size, the one with fewer elements is * ordered first. */ private class CollectionComparator implements Comparator { // Compares elements within collection. private Comparator betweenComp; // Compares elements between two collections. private Comparator withinComp; private IteratorComparator iteratorComparator; // Compares elements. /** * Constructs a CollectionComparator that uses elementComparator for * comparison. * @param elementComparator Compares individual elements, both within a * colleciton and for elements in two different collections. */ public CollectionComparator (Comparator elementComparator) { this(elementComparator, elementComparator); } /** * Constructs a CollectionComparator that uses withinComparator and * betweenComparator for comparison. * @param withinComparator Compares individual elements within a * collection. * @param betweenComparator Compares individual elements between two * collections. */ public CollectionComparator (Comparator betweenComparator, Comparator withinComparator) { this.betweenComp = betweenComparator; this.withinComp = withinComparator; iteratorComparator = new IteratorComparator(betweenComp); } public int compare(Object o1, Object o2) { if (o1 == o2) return 0; Collection c1 = (Collection)o1; Collection c2 = (Collection)o2; return iteratorComparator.compare(sort(c1, withinComp).iterator(), sort(c2, withinComp).iterator()); } } /** * Sort the given collection with the given comparator. */ private SortedSet sort(Collection collection, Comparator comparator) { SortedSet sorted = new TreeSet(comparator); Iterator it = collection.iterator(); while (it.hasNext()) { sorted.add(it.next()); } return sorted; } // --- Fake wrappers abstract class FakeScoped implements ScopedIF { public Collection getScope() { return Collections.EMPTY_SET; } public void addTheme(TopicIF theme) {} public void removeTheme(TopicIF theme) {} public String getObjectId() { return null; } public boolean isReadOnly() { return true; } public TopicMapIF getTopicMap() { return null; } public Collection getItemIdentifiers() { return Collections.EMPTY_SET; } public void addItemIdentifier(LocatorIF source_locator) {} public void removeItemIdentifier(LocatorIF source_locator) {} public Collection getTypes() { return Collections.EMPTY_SET; } public void addType(TopicIF type) {} public void removeType(TopicIF type) {} public void remove() {} } class FakeTopic extends FakeScoped implements TopicIF { private Collection indicator; private TopicMapIF tmForFake; public FakeTopic(LocatorIF indicator, TopicMapIF tmForFake) { this.tmForFake = tmForFake; this.indicator = Collections.singleton(indicator); } public Collection getSubjectIdentifiers() { return indicator; } public Collection getSubjectLocators() { return Collections.EMPTY_SET; } public Collection getTopicNames() { return Collections.EMPTY_SET; } public Collection<TopicNameIF> getTopicNamesByType(TopicIF type) { return Collections.EMPTY_SET; } public Collection getOccurrences() { return Collections.EMPTY_SET; } public Collection<OccurrenceIF> getOccurrencesByType(TopicIF type) { return Collections.EMPTY_SET; } public Collection getRoles() { return Collections.EMPTY_SET; } public Collection getRolesByType(TopicIF roletype) { return Collections.EMPTY_SET; } public Collection getRolesByType(TopicIF roletype, TopicIF assoc_type) { return Collections.EMPTY_SET; } public Collection<AssociationIF> getAssociations() { return Collections.EMPTY_SET; } public Collection<AssociationIF> getAssociationsByType(TopicIF type) { return Collections.EMPTY_SET; } public TopicMapIF getTopicMap() { return tmForFake; } public void addSubjectLocator(LocatorIF subject_locator) throws ConstraintViolationException {} public void removeSubjectLocator(LocatorIF subject_locator) {} public void addSubjectIdentifier(LocatorIF subject_indicator) {} public void removeSubjectIdentifier(LocatorIF subject_indicator) {} public void merge(TopicIF topic) {} public ReifiableIF getReified() { return null; } } class FakeAssociation extends FakeScoped implements AssociationIF { private Collection roles; public FakeAssociation(TopicIF t, TopicIF i) { roles = new ArrayList(2); roles.add(new FakeRole(this, type, t)); roles.add(new FakeRole(this, instance, i)); } public Collection getRoles() { return roles; } public Collection getRoleTypes() { return null; } public Collection getRolesByType(TopicIF roletype) { Collection rolesoftype = new ArrayList(); Iterator it = roles.iterator(); while (it.hasNext()) { AssociationRoleIF role = (AssociationRoleIF) it.next(); if (roletype.equals(role.getType())) rolesoftype.add(role); } return rolesoftype; } public TopicIF getType() { return typeInstance; } public void setType(TopicIF type) {} public TopicIF getReifier() { return null; } public void setReifier(TopicIF reifier) {} } class FakeRole extends FakeScoped implements AssociationRoleIF { private AssociationIF association; private TopicIF type; private TopicIF player; public FakeRole(AssociationIF association, TopicIF type, TopicIF player) { this.association = association; this.type = type; this.player = player; } public TopicIF getType() { return type; } public AssociationIF getAssociation() { return association; } public TopicIF getPlayer() { return player; } public void setType(TopicIF type) {} public void setPlayer(TopicIF player) {} public TopicIF getReifier() { return null; } public void setReifier(TopicIF reifier) {} } // we need this class because occurrences are output ordered by normalized // value, and not by the literal value class FakeOccurrence implements OccurrenceIF { private OccurrenceIF occ; private String value; public FakeOccurrence(OccurrenceIF occ) { this.occ = occ; LocatorIF datatype = occ.getDataType(); if (datatype.equals(DataTypes.TYPE_URI)) { LocatorIF locator = occ.getLocator(); this.value = normaliseLocatorReference(locator.getAddress()); } else if (datatype.equals(DataTypes.TYPE_INTEGER) || datatype.equals(DataTypes.TYPE_DECIMAL)) this.value = normalizeNumber(occ.getValue()); else this.value = occ.getValue(); } public TopicIF getTopic() { return occ.getTopic(); } public LocatorIF getDataType() { return occ.getDataType(); } public String getValue() { return value; } public Reader getReader() { throw new UnsupportedOperationException(); } public void setValue(String value) { throw new UnsupportedOperationException(); } public LocatorIF getLocator() { throw new UnsupportedOperationException(); } public void setLocator(LocatorIF locator) { throw new UnsupportedOperationException(); } public void setValue(String value, LocatorIF datatype) { throw new UnsupportedOperationException(); } public void setReader(Reader value, long length, LocatorIF datatype) { throw new UnsupportedOperationException(); } public long getLength() { throw new UnsupportedOperationException(); } public TopicIF getType() { return occ.getType(); } public void setType(TopicIF type) { throw new UnsupportedOperationException(); } public TopicIF getReifier() { return occ.getReifier(); } public void setReifier(TopicIF reifier) { throw new UnsupportedOperationException(); } public Collection getScope() { return occ.getScope(); } public void addTheme(TopicIF theme) { throw new UnsupportedOperationException(); } public void removeTheme(TopicIF theme) { throw new UnsupportedOperationException(); } public String getObjectId() { return occ.getObjectId(); } public boolean isReadOnly() { return true; } public TopicMapIF getTopicMap() { return occ.getTopicMap(); } public Collection getItemIdentifiers() { return occ.getItemIdentifiers(); } public void addItemIdentifier(LocatorIF source_locator) { throw new UnsupportedOperationException(); } public void removeItemIdentifier(LocatorIF source_locator) { throw new UnsupportedOperationException(); } public void remove() { throw new UnsupportedOperationException(); } } }