/* * #! * 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; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import net.ontopia.utils.DeciderIF; import net.ontopia.utils.DeciderUtils; import net.ontopia.utils.CollectionUtils; import net.ontopia.utils.ObjectUtils; import net.ontopia.utils.CompactHashSet; 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.OccurrenceIF; import net.ontopia.topicmaps.core.ReifiableIF; import net.ontopia.topicmaps.core.ScopedIF; import net.ontopia.topicmaps.core.TMObjectIF; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.topicmaps.core.TopicMapBuilderIF; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.core.TopicMapStoreIF; import net.ontopia.topicmaps.core.TopicNameIF; import net.ontopia.topicmaps.core.TypedIF; import net.ontopia.topicmaps.core.UniquenessViolationException; import net.ontopia.topicmaps.core.VariantNameIF; import net.ontopia.topicmaps.core.index.ClassInstanceIndexIF; import net.ontopia.topicmaps.core.index.ScopeIndexIF; import net.ontopia.topicmaps.impl.rdbms.RDBMSTopicMapStore; /** * PUBLIC: Utilities for merging topics and topic maps. This class * provides static methods for testing whether topics should be * merged, merging topics and for merging topic maps. */ public class MergeUtils { /** * PUBLIC: Tests whether two topics should be merged or not, * according to XTM rules. * * @param t1 topicIF; #1 topic to merge * @param t2 topicIF; #2 topic to merge * @return boolean; true if the topics should be merged, false otherwise. */ public static boolean shouldMerge(TopicIF t1, TopicIF t2) { // check subject locators if (CollectionUtils.overlaps(t1.getSubjectLocators(), t2.getSubjectLocators())) return true; // check subject indicators and source locators if (CollectionUtils.overlaps(t1.getSubjectIdentifiers(), t2.getSubjectIdentifiers()) || CollectionUtils.overlaps(t1.getItemIdentifiers(), t2.getSubjectIdentifiers())) return true; if (CollectionUtils.overlaps(t1.getItemIdentifiers(), t2.getItemIdentifiers()) || CollectionUtils.overlaps(t1.getSubjectIdentifiers(), t2.getItemIdentifiers())) return true; // should merge if they reify the same object ReifiableIF r1 = t1.getReified(); ReifiableIF r2 = t2.getReified(); if (r1 != null && ObjectUtils.equals(r1, r2)) return true; return false; } /** * PUBLIC: Merges the characteristics of one topic into another * topic. The source topic stripped of characteristics, all of * which are moved to the target topic. Duplicate characteristics * are suppressed. The topics must be in the same topic map, and the * source topic is removed from the topic map. * * @param source topicIF; the source topic. This is empty after the * operation and is removed from the topic map. * @param target topicIF; the target topic. This gets new characteristics. * @exception throws ConstraintViolationException if the two topics * have different values for the 'subject' property, since if they * do they cannot represent the same subject. If this exception is * thrown both topics remain untouched. */ public static void mergeInto(TopicIF target, TopicIF source) throws ConstraintViolationException { if (target.getTopicMap() == null) throw new IllegalArgumentException("Target topic has no topic map"); if (source.getTopicMap() == null) throw new IllegalArgumentException("Source topic has no topic map"); if (!target.getTopicMap().equals(source.getTopicMap())) throw new IllegalArgumentException("Topics not in same topic map"); if (target == source) throw new IllegalArgumentException("Cannot merge topic with itself!"); // move reified moveReified(target, source); // replace source by target throughout replaceTopics(target, source); // remove subject locators from source List<LocatorIF> subjectLocators = new ArrayList<LocatorIF>(source.getSubjectLocators()); for (LocatorIF subjectLocator : subjectLocators) { source.removeSubjectLocator(subjectLocator); } // remove subject indicators from source List<LocatorIF> subjectIdentifiers = new ArrayList<LocatorIF>(source.getSubjectIdentifiers()); for (LocatorIF subjectIdentifier: subjectIdentifiers) { source.removeSubjectIdentifier(subjectIdentifier); } // remove item identifiers from source List<LocatorIF> itemIdentifiers = new ArrayList<LocatorIF>(source.getItemIdentifiers()); for (LocatorIF itemIdentifier : itemIdentifiers) { source.removeItemIdentifier(itemIdentifier); } // add subject locators to target for (LocatorIF subjectLocator : subjectLocators) { target.addSubjectLocator(subjectLocator); } // add subject indicators to target for (LocatorIF subjectIdentifier: subjectIdentifiers) { target.addSubjectIdentifier(subjectIdentifier); } // add item identifiers to target for (LocatorIF itemIdentifier : itemIdentifiers) { target.addItemIdentifier(itemIdentifier); } // copying types for (TopicIF type : source.getTypes()) { target.addType(type); } // copying base names Map<String, TopicNameIF> topicnameMap = buildKeyMap(target.getTopicNames()); for (TopicNameIF sourcebn : new ArrayList<TopicNameIF>(source.getTopicNames())) { String key = KeyGenerator.makeTopicNameKey(sourcebn); TopicNameIF targetbn = topicnameMap.get(key); if (targetbn == null) { targetbn = CopyUtils.copyTopicName(target, sourcebn); moveReifier(targetbn, sourcebn); sourcebn.remove(); } else mergeInto(targetbn, sourcebn); } // copying occurrences Map<String, OccurrenceIF> occurrenceMap = buildKeyMap(target.getOccurrences()); for (OccurrenceIF sourceoc : new ArrayList<OccurrenceIF>(source.getOccurrences())) { OccurrenceIF targetoc = occurrenceMap.get(KeyGenerator.makeOccurrenceKey(sourceoc)); if (targetoc == null) { targetoc = CopyUtils.copyOccurrence(target, sourceoc); moveReifier(targetoc, sourceoc); sourceoc.remove(); } else mergeInto(targetoc, sourceoc); } // copying roles Set<String> keys = new CompactHashSet<String>(); for (AssociationRoleIF role : target.getRoles()) { keys.add(KeyGenerator.makeAssociationKey(role.getAssociation())); } for (AssociationRoleIF ar : new ArrayList<AssociationRoleIF>(source.getRoles())) { ar.setPlayer(target); String key = KeyGenerator.makeAssociationKey(ar.getAssociation()); if (keys.contains(key)) { ar.getAssociation().remove(); // ISSUE: should we move any reifier over to the duplicate? } } // removing source source.remove(); // notify transactions of performed merge notifyTransaction(source, target); } private static <R extends ReifiableIF> Map<String, R> buildKeyMap(Collection<R> objects) { Map<String, R> map = new HashMap<String, R>(); Iterator<R> it = objects.iterator(); while (it.hasNext()) { R object = it.next(); String key = KeyGenerator.makeKey(object); map.put(key, object); } return map; } /** * INTERNAL: Replaces source by target throughout the topic map. * source and target must belong to the same topic map. */ private static void replaceTopics(TopicIF target, TopicIF source) { TopicMapIF topicmap = target.getTopicMap(); // types ClassInstanceIndexIF typeIndex = (ClassInstanceIndexIF) topicmap.getIndex("net.ontopia.topicmaps.core.index.ClassInstanceIndexIF"); replaceTopicType(typeIndex.getAssociationRoles(source), target); replaceTopicType(typeIndex.getAssociations(source), target); replaceTopicType(typeIndex.getTopicNames(source), target); replaceTopicType(typeIndex.getOccurrences(source), target); replaceTopicTypes(typeIndex.getTopics(source), target, source); // scopes ScopeIndexIF scopeIndex = (ScopeIndexIF) topicmap.getIndex("net.ontopia.topicmaps.core.index.ScopeIndexIF"); replaceTopicInScope(scopeIndex.getAssociations(source), target, source); replaceTopicInScope(scopeIndex.getTopicNames(source), target, source); replaceTopicInScope(scopeIndex.getOccurrences(source), target, source); replaceTopicInScope(scopeIndex.getVariants(source), target, source); } private static <T extends TypedIF> void replaceTopicType(Collection<T> objects, TopicIF t1) { Iterator<T> it = objects.iterator(); while (it.hasNext()) { T object = it.next(); object.setType(t1); } } /** * INTERNAL: Replace source by target as the type of objects. */ private static void replaceTopicTypes(Collection<TopicIF> objects, TopicIF target, TopicIF source) { Iterator<TopicIF> it = objects.iterator(); while (it.hasNext()) { TopicIF object = it.next(); object.removeType(source); object.addType(target); } } private static <S extends ScopedIF> void replaceTopicInScope(Collection<S> objects, TopicIF t1, TopicIF t2) { Iterator<S> it = objects.iterator(); while (it.hasNext()) { S object = it.next(); object.removeTheme(t2); object.addTheme(t1); } } /** * PUBLIC: Merges the source name into the target name. The two * names must be in the same topic map, but need not have the same * parent topic. It is assumed (but not verified) that the two * names are actually equal. * @since 5.1.0 */ public static void mergeInto(TopicNameIF target, TopicNameIF source) { Iterator<VariantNameIF> it = new ArrayList<VariantNameIF>(source.getVariants()).iterator(); while (it.hasNext()) { VariantNameIF sourcevn = it.next(); VariantNameIF targetvn = CopyUtils.copyVariant(target, sourcevn); moveReifier(targetvn, sourcevn); sourcevn.remove(); } moveReifier(target, source); moveItemIdentifiers(target, source); source.remove(); } private static void moveItemIdentifiers(TMObjectIF target, TMObjectIF source) { Iterator<LocatorIF> it = new ArrayList<LocatorIF>(source.getItemIdentifiers()).iterator(); while (it.hasNext()) { LocatorIF itemid = it.next(); source.removeItemIdentifier(itemid); target.addItemIdentifier(itemid); } } /** * PUBLIC: Merges the source occurrence into the target * occurrence. The two occurrences must be in the same topic map, but * need not have the same parent topic. It is assumed (but not * verified) that the two occurrences are actually equal. * @since 5.1.0 */ public static void mergeInto(OccurrenceIF target, OccurrenceIF source) { moveReifier(target, source); moveItemIdentifiers(target, source); source.remove(); } /** * PUBLIC: Merges the source association into the target * association. The two associations must be in the same topic * map. If the two associations are not actually equal a * ConstraintViolationException is thrown. * @since 5.1.0 */ public static void mergeInto(AssociationIF target, AssociationIF source) { moveReifier(target, source); moveItemIdentifiers(target, source); // set up key map Map<String, AssociationRoleIF> keys = new HashMap<String, AssociationRoleIF>(); Iterator<AssociationRoleIF> it = target.getRoles().iterator(); while (it.hasNext()) { AssociationRoleIF role = it.next(); keys.put(KeyGenerator.makeAssociationRoleKey(role), role); } // merge the roles it = source.getRoles().iterator(); while (it.hasNext()) { AssociationRoleIF srole = it.next(); AssociationRoleIF trole = keys.get(KeyGenerator.makeAssociationRoleKey(srole)); if (trole == null) throw new ConstraintViolationException("Cannot merge unequal associations"); mergeIntoChecked(trole, srole); } source.remove(); } /** * PUBLIC: Merges the source role into the target role. The two * roles must be in the same topic map, but need not have the same * parent association. If the associations are not the same, they * are merged, provided that they are equal; if they are not equal, * a ConstraintViolationException is thrown. It is assumed (but not * verified) that the two roles are actually equal. * @since 5.1.0 */ public static void mergeInto(AssociationRoleIF target, AssociationRoleIF source) { if (target.getAssociation() != source.getAssociation()) { String key1 = KeyGenerator.makeAssociationKey(target.getAssociation()); String key2 = KeyGenerator.makeAssociationKey(source.getAssociation()); if (!key1.equals(key2)) throw new ConstraintViolationException("Cannot merge roles in different " + " associations"); mergeInto(target.getAssociation(), source.getAssociation()); } else { mergeIntoChecked(target, source); source.remove(); } } private static void mergeIntoChecked(AssociationRoleIF target, AssociationRoleIF source) { moveReifier(target, source); moveItemIdentifiers(target, source); } /** * PUBLIC: Merges the source variant into the target variant. The * two variants must be in the same topic map, but need not have the * same parent name. It is assumed (but not verified) that the two * variants are actually equal. * @since 5.1.0 */ public static void mergeInto(VariantNameIF target, VariantNameIF source) { moveReifier(target, source); moveItemIdentifiers(target, source); source.remove(); } /** * PUBLIC: Merges the source object into the target object. The two * objects must be in the same topic map, but need not have the same * parent. It is assumed (but not verified) that the two objects are * actually equal. * @since 5.1.0 */ public static void mergeInto(ReifiableIF target, ReifiableIF source) { if (target instanceof TopicNameIF) mergeInto((TopicNameIF) target, (TopicNameIF) source); else if (target instanceof OccurrenceIF) mergeInto((OccurrenceIF) target, (OccurrenceIF) source); else if (target instanceof AssociationIF) mergeInto((AssociationIF) target, (AssociationIF) source); else if (target instanceof AssociationRoleIF) mergeInto((AssociationRoleIF) target, (AssociationRoleIF) source); else if (target instanceof VariantNameIF) mergeInto((VariantNameIF) target, (VariantNameIF) source); else throw new UnsupportedOperationException("Cannot merge objects of this type: " + target); } /** * PUBLIC: Merges the source object into a target topic in another * topic map. Makes no attempt to verify that the source topic * represents the same subject as the target topic. * @since 5.1.3 */ public static ReifiableIF mergeInto(TopicIF target, ReifiableIF source) { if (source instanceof TopicNameIF) return mergeInto(target, (TopicNameIF) source); else if (source instanceof OccurrenceIF) return mergeInto(target, (OccurrenceIF) source); else if (source instanceof AssociationIF) return mergeInto(target.getTopicMap(), (AssociationIF) source); else throw new UnsupportedOperationException("Cannot merge objects of this type: " + source); } /** * PUBLIC: Merges the source topic name into the target topic in * another topic map. Makes no attempt to verify that the source * topic represents the same subject as the target topic. * @return The new topic name in the target topic map. * @since 5.1.3 */ public static TopicNameIF mergeInto(TopicIF target, TopicNameIF source) { TopicMapIF tm = target.getTopicMap(); TopicMapBuilderIF builder = tm.getBuilder(); TopicIF type = findTopic(tm, source.getType()); TopicNameIF newtn = builder.makeTopicName(target, type, source.getValue()); for (TopicIF theme : source.getScope()) newtn.addTheme(findTopic(tm, theme)); return newtn; } /** * PUBLIC: Merges the source occurrence into the target topic in * another topic map. Makes no attempt to verify that the source * topic represents the same subject as the target topic. * @return The new occurrence in the target topic map. * @since 5.1.3 */ public static OccurrenceIF mergeInto(TopicIF target, OccurrenceIF source) { TopicMapIF tm = target.getTopicMap(); TopicMapBuilderIF builder = tm.getBuilder(); TopicIF type = findTopic(tm, source.getType()); OccurrenceIF newocc = builder.makeOccurrence(target, type, source.getValue(), source.getDataType()); for (TopicIF theme : source.getScope()) newocc.addTheme(findTopic(tm, theme)); return newocc; } /** * PUBLIC: Merges the source association into the target topic * map. Makes no attempt to verify that the source association is * not already present. * @return The new association in the target topic map. * @since 5.1.3 */ public static AssociationIF mergeInto(TopicMapIF topicmap, AssociationIF source) { TopicMapBuilderIF builder = topicmap.getBuilder(); TopicIF type = findTopic(topicmap, source.getType()); AssociationIF newa = builder.makeAssociation(type); for (TopicIF theme : source.getScope()) newa.addTheme(findTopic(topicmap, theme)); for (AssociationRoleIF role : source.getRoles()) { type = findTopic(topicmap, role.getType()); TopicIF player = findTopic(topicmap, role.getPlayer()); builder.makeAssociationRole(newa, type, player); } return newa; } /** * PUBLIC: Merges the source topic from into the target topic map, * when the source topic is not already in the target topic map. * All characteristics of the source topic are copied over, but * topics referenced from the source topic are only included as * identity stubs. The source topic is untouched. * @since 2.0 */ public static TopicIF mergeInto(TopicMapIF targettm, TopicIF source) { return mergeInto(targettm, source, DeciderUtils.<TMObjectIF>getTrueDecider()); } /** * PUBLIC: Merges the source topic from into the target topic map, * when the source topic is not already in the target topic map. * All characteristics of the source topic that are approved by the * decider are copied over, but topics referenced from the source * topic are only included as identity stubs. The source topic is * untouched. * @param decider Used to decide which topic characteristics to copy. * Is asked for each base name, occurrence, and association role. * @since 2.0 */ public static TopicIF mergeInto(TopicMapIF targettm, TopicIF source, DeciderIF<TMObjectIF> decider) { if (source.getTopicMap() == targettm) return source; TopicMapBuilderIF builder = targettm.getBuilder(); TopicIF target = copyTopic(targettm, source); // copying types Iterator<TopicIF> typeIterator = source.getTypes().iterator(); while (typeIterator.hasNext()) target.addType(copyTopic(targettm, typeIterator.next())); // copying base names Iterator<TopicNameIF> topicnameIterator = source.getTopicNames().iterator(); while (topicnameIterator.hasNext()) { TopicNameIF bnsource = topicnameIterator.next(); if (!decider.ok(bnsource)) continue; TopicNameIF bntarget = builder.makeTopicName(target, resolveTopic(builder.getTopicMap(), bnsource.getType()), bnsource.getValue()); copyScope(bntarget, bnsource); Iterator<VariantNameIF> it2 = bnsource.getVariants().iterator(); while (it2.hasNext()) { VariantNameIF vnsource = it2.next(); if (!decider.ok(vnsource)) continue; VariantNameIF vntarget = builder.makeVariantName(bntarget, vnsource.getValue(), vnsource.getDataType()); copyScope(vntarget, vnsource); vntarget = resolveIdentities(vntarget, vnsource); copyReifier(vntarget, vnsource); } bntarget = resolveIdentities(bntarget, bnsource); copyReifier(bntarget, bnsource); } // copying occurrences Iterator<OccurrenceIF> occurrenceIterator = source.getOccurrences().iterator(); while (occurrenceIterator.hasNext()) { OccurrenceIF osource = occurrenceIterator.next(); if (!decider.ok(osource)) continue; OccurrenceIF otarget = builder.makeOccurrence(target, resolveTopic(builder.getTopicMap(), osource.getType()), ""); CopyUtils.copyOccurrenceData(otarget, osource); copyScope(otarget, osource); otarget = resolveIdentities(otarget, osource); copyReifier(otarget, osource); } // copying associations Iterator<AssociationRoleIF> roleIterator = source.getRoles().iterator(); while (roleIterator.hasNext()) { AssociationRoleIF rstart = roleIterator.next(); if (!decider.ok(rstart)) continue; AssociationIF asource = rstart.getAssociation(); AssociationIF atarget = builder.makeAssociation(resolveTopic(builder.getTopicMap(), asource.getType())); copyScope(atarget, asource); Iterator<AssociationRoleIF> it2 = asource.getRoles().iterator(); while (it2.hasNext()) { AssociationRoleIF rsource = it2.next(); AssociationRoleIF rtarget = builder.makeAssociationRole(atarget, resolveTopic(builder.getTopicMap(), rsource.getType()), (rsource == rstart ? target : copyTopic(targettm, rsource.getPlayer()))); rtarget = resolveIdentities(rtarget, rsource); copyReifier(rtarget, rsource); } atarget = resolveIdentities(atarget, asource); copyReifier(atarget, asource); } return target; } private static void copyScope(ScopedIF target, ScopedIF source) { Iterator<TopicIF> it = source.getScope().iterator(); while (it.hasNext()) target.addTheme(copyTopic(target.getTopicMap(), it.next())); } private static TopicIF resolveTopic(TopicMapIF targetTopicMap, TopicIF sourceTopic) { if (sourceTopic == null) return null; else return copyTopic(targetTopicMap, sourceTopic); } // returns false if object is a duplicate, true if it is not // expects caller to remove duplicate @SuppressWarnings("unchecked") private static <O extends TMObjectIF> O resolveIdentities(O target, O source) { TopicMapIF targettm = target.getTopicMap(); Iterator<LocatorIF> it = source.getItemIdentifiers().iterator(); while (it.hasNext()) { LocatorIF loc = it.next(); TMObjectIF object = targettm.getObjectByItemIdentifier(loc); if (object != null) { if (!equals(target, object)) { throw new ConstraintViolationException("Different topic map objects have " + "the same source locator (" + loc + "): " + target + " and " + object); } else { target.remove(); return (O) object; // this is a duplicate } } else target.addItemIdentifier(loc); } return target; } private static void copyReifier(ReifiableIF target, ReifiableIF source) { TopicIF reifier = source.getReifier(); if (reifier != null) { TopicIF treifier = mergeInto(target.getTopicMap(), reifier); target.setReifier(treifier); } } private static void copyReifier(ReifiableIF target, ReifiableIF source, Map<TopicIF, TopicIF> mergemap) { TopicIF _sourceReifier = source.getReifier(); if (_sourceReifier != null) { TopicIF targetReifier = target.getReifier(); TopicIF sourceReifier = resolveTopic(target.getTopicMap(), _sourceReifier, mergemap); if (targetReifier == null) { if (sourceReifier != null) target.setReifier(sourceReifier); } else if (sourceReifier != null) { if (targetReifier != sourceReifier) mergeInto(targetReifier, sourceReifier); } } } private static TopicIF copyTopic(TopicMapIF targettm, TopicIF source) { if (source == null) return null; TopicMapBuilderIF builder = targettm.getBuilder(); TopicIF target = builder.makeTopic(); return copyIdentifiers(target, source); } /** * INTERNAL: Copies all the identifiers from the source to the * target topic. Note that this method might cause topics in the * target topic map to merge. The source and target are assumed to * come from different topic maps. */ public static TopicIF copyIdentifiers(TopicIF target, TopicIF source) { TopicMapIF targettm = target.getTopicMap(); // merging on subject locators Iterator<LocatorIF> it = source.getSubjectLocators().iterator(); while (it.hasNext()) { LocatorIF loc = it.next(); TopicIF found = targettm.getTopicBySubjectLocator(loc); if (found != null) { if (found != target) { mergeInto(found, target); target = found; } } else target.addSubjectLocator(loc); } // merging on subject identifiers it = source.getSubjectIdentifiers().iterator(); while (it.hasNext()) { LocatorIF loc = it.next(); TopicIF found = targettm.getTopicBySubjectIdentifier(loc); if (found == null) { TMObjectIF f = targettm.getObjectByItemIdentifier(loc); if (f instanceof TopicIF) found = (TopicIF) f; } if (found != null) { if (found != target) { mergeInto(found, target); target = found; } } // have to copy subject identifier across, in case we merged via item // identifier target.addSubjectIdentifier(loc); } // merging on item identifiers it = source.getItemIdentifiers().iterator(); while (it.hasNext()) { LocatorIF loc = it.next(); TMObjectIF f = targettm.getObjectByItemIdentifier(loc); if (f != null && !(f instanceof TopicIF)) throw new ConstraintViolationException("Item identifier " + loc + " of source topic clashed with"+ " " + f); TopicIF found = (TopicIF) f; if (found == null) found = targettm.getTopicBySubjectIdentifier(loc); if (found != null) { if (found != target) { mergeInto(found, target); target = found; } } // have to copy item identifier across, in case we merged via subject // identifier target.addItemIdentifier(loc); } return target; } public static TopicIF copyIdentifiers(TopicIF target, TopicIF source, Map<TopicIF, TopicIF> mergemap) { TopicMapIF targettm = target.getTopicMap(); // merging subject locators Iterator<LocatorIF> it = source.getSubjectLocators().iterator(); while (it.hasNext()) { LocatorIF loc = it.next(); TopicIF found = targettm.getTopicBySubjectLocator(loc); if (found != null) { if (found != target) { mergemap.put(source, found); mergeInto(found, target); target = found; } } else { target.addSubjectLocator(loc); } } // merging subject indicators it = source.getSubjectIdentifiers().iterator(); while (it.hasNext()) { LocatorIF loc = it.next(); TopicIF found = targettm.getTopicBySubjectIdentifier(loc); if (found != null) { if (found != target) { mergemap.put(source, found); mergeInto(found, target); target = found; } } else { target.addSubjectIdentifier(loc); } } // merging source locators it = source.getItemIdentifiers().iterator(); while (it.hasNext()) { LocatorIF loc = it.next(); TMObjectIF f = targettm.getObjectByItemIdentifier(loc); if (f != null && !(f instanceof TopicIF)) throw new ConstraintViolationException("Source locator " + loc + " of source topic clashed with"+ " " + f); TopicIF found = (TopicIF) f; if (found != null) { if (found != target) { mergemap.put(source, found); mergeInto(found, target); target = found; } } else { target.addItemIdentifier(loc); } } return target; } /** * PUBLIC: Merges one topic map into another topic map. The source topic * map is left untouched, while its contents are copied into the * target topic map. The target topic map is updated accordingly, * and no duplicate characteristics should be present after the merge. * * <p>Merges are done on the basis of subject locators, subject * identifiers, item identifiers, and topic names (with scope). * * @param source topicIF; the source topic map. This is untouched after the * operation. * @param target topicIF; the target topic map. This gets new topics * and topic characteristics. * @exception throws ConstraintViolationException if two topics * that are to be merged under XTM 1.0 rules have different values * for the 'subject' property, since if they do they cannot * represent the same subject. */ public static void mergeInto(TopicMapIF target, TopicMapIF source) throws ConstraintViolationException { // Initialization Map<TopicIF, TopicIF> mergemap = new HashMap<TopicIF, TopicIF>(); // see INV comment below for enlightenment // STEP 1: URI-based merges // may find that topics in target should be merged, due to extra // information provided by source; in these cases, merge those // topics and update mergemap accordingly Map<TopicIF, Set<TopicIF>> mergemapRev = new HashMap<TopicIF, Set<TopicIF>>(); Iterator<TopicIF> it = source.getTopics().iterator(); while (it.hasNext()) { TopicIF sourceT = it.next(); TopicIF targetT; // subject locators Iterator<LocatorIF> it2 = new ArrayList<LocatorIF>(sourceT.getSubjectLocators()).iterator(); while (it2.hasNext()) { LocatorIF loc = it2.next(); targetT = target.getTopicBySubjectLocator(loc); if (targetT != null) registerMerge(targetT, sourceT, mergemap, mergemapRev); } // subject identifiers it2 = new ArrayList<LocatorIF>(sourceT.getSubjectIdentifiers()).iterator(); while (it2.hasNext()) { LocatorIF ind = it2.next(); targetT = target.getTopicBySubjectIdentifier(ind); if (targetT == null) { TMObjectIF object = target.getObjectByItemIdentifier(ind); if (object != null && object instanceof TopicIF) { targetT = (TopicIF) object; } } if (targetT != null) registerMerge(targetT, sourceT, mergemap, mergemapRev); } // item identifiers it2 = new ArrayList<LocatorIF>(sourceT.getItemIdentifiers()).iterator(); while (it2.hasNext()) { LocatorIF loc = it2.next(); TMObjectIF object = target.getObjectByItemIdentifier(loc); if (object != null && object instanceof TopicIF) targetT = (TopicIF) object; else targetT = target.getTopicBySubjectIdentifier(loc); if (targetT != null) registerMerge(targetT, sourceT, mergemap, mergemapRev); } } // INV: mergeMap contains sourceT -> targetT mapping for all // topics in source that are to be merged with a topic in target, // based on URIs. no topics in target need to be merged with // each other. mergemapRev = null; // no longer needed; conserve memory // STEP 3: copy to target Map<TopicIF, TopicIF> merged = new HashMap<TopicIF, TopicIF>(mergemap); // a) copy unmerged topics it = source.getTopics().iterator(); while (it.hasNext()) { TopicIF t2 = it.next(); if (!mergemap.containsKey(t2)) copyTopic(target, t2, mergemap); } // b) copy characteristics of merged topics (except roles) it = merged.keySet().iterator(); while (it.hasNext()) { TopicIF t2 = it.next(); TopicIF t1 = (TopicIF) merged.get(t2); copyCharacteristics(t1, t2, mergemap); } // c) copy associations Set<String> assocs = getAssociationKeySet(target.getAssociations()); Iterator<AssociationIF> associationIterator = source.getAssociations().iterator(); while (associationIterator.hasNext()) copyAssociation(target, associationIterator.next(), mergemap, assocs); // d) reifier // NOTE: the reifier is *not* to be copied, because if a topic is // reifying the source topic map that's a different subject from // the target topic map, and so if we copied we'd be changing the // subject of the topic. } private static void registerMerge(TopicIF target, TopicIF source, Map<TopicIF, TopicIF> mergemap, Map<TopicIF, Set<TopicIF>> mergemapRev) { if (target.getTopicMap() == null) throw new IllegalArgumentException("Target " + target + " has no topic map"); // do the merge Set<TopicIF> sources = mergemapRev.get(target); if (sources == null) { sources = new CompactHashSet<TopicIF>(); mergemapRev.put(target, sources); } sources.add(source); TopicIF origTarget = mergemap.get(source); mergemap.put(source, target); if (origTarget != null && !origTarget.equals(target)) { Iterator<TopicIF> it = mergemapRev.get(origTarget).iterator(); while (it.hasNext()) { TopicIF otherSource = it.next(); sources.add(otherSource); mergemap.put(otherSource, target); } mergemapRev.remove(origTarget); mergeInto(target, origTarget); } } private static void copyAssociation(TopicMapIF targettm, AssociationIF source, Map<TopicIF, TopicIF> mergemap, Set<String> assocs) { TopicMapBuilderIF builder = targettm.getBuilder(); AssociationIF target = builder.makeAssociation(resolveTopic(builder.getTopicMap(), source.getType(), mergemap)); copyScope(target, source, mergemap); Iterator<AssociationRoleIF> it = source.getRoles().iterator(); while (it.hasNext()) { AssociationRoleIF sourceRole = it.next(); builder.makeAssociationRole(target, resolveTopic(builder.getTopicMap(), sourceRole.getType(), mergemap), resolveTopic(builder.getTopicMap(), sourceRole.getPlayer(), mergemap)); } if (assocs.contains(KeyGenerator.makeAssociationKey(target))) target.remove(); else { copyReifier(target, source, mergemap); copySourceLocators(target, source); } } private static Set<String> getAssociationKeySet(Collection<AssociationIF> associations) { Set<String> assocs = new CompactHashSet<String>(); Iterator<AssociationIF> it = associations.iterator(); while (it.hasNext()) { AssociationIF assoc = it.next(); assocs.add(KeyGenerator.makeAssociationKey(assoc)); } return assocs; } // FIXME: note: updates mergemap private static TopicIF copyTopic(TopicMapIF targettm, TopicIF source, Map<TopicIF, TopicIF> mergemap) { TopicMapBuilderIF builder = targettm.getBuilder(); TopicIF target = builder.makeTopic(); mergemap.put(source, target); copyCharacteristics(target, source, mergemap); if (target.getTopicMap() == null) // got merged away (bug #2168) return (TopicIF) mergemap.get(source); else return target; } // assumes the objects are in different topic maps private static void copySourceLocators(TMObjectIF target, TMObjectIF source) { Iterator<LocatorIF> it = source.getItemIdentifiers().iterator(); while (it.hasNext()) { LocatorIF srcloc = it.next(); try { target.addItemIdentifier(srcloc); } catch (UniquenessViolationException e) { TopicMapIF tm = target.getTopicMap(); TMObjectIF other = tm.getObjectByItemIdentifier(srcloc); if (!equals(target, other)) throw e; // so, they were equal. that means they should merge. so what // do we do now? is it enough to transfer the source locators? // and what happens if we lose 'target'? surely it's needed // elsewhere? } } } private static void copyCharacteristics(TopicIF target, TopicIF source, Map<TopicIF, TopicIF> mergemap) { TopicMapBuilderIF builder = target.getTopicMap().getBuilder(); // copy identifiers target = copyIdentifiers(target, source, mergemap); // copying types Iterator<TopicIF> typeIterator = source.getTypes().iterator(); while (typeIterator.hasNext()) { TopicIF sourceType = typeIterator.next(); target.addType(resolveTopic(target.getTopicMap(), sourceType, mergemap)); } // copying base names HashMap<String, TopicNameIF> map = new HashMap<String, TopicNameIF>(); Iterator<TopicNameIF> topicnameIterator = target.getTopicNames().iterator(); while (topicnameIterator.hasNext()) { TopicNameIF bn = topicnameIterator.next(); String key = KeyGenerator.makeTopicNameKey(bn); map.put(key, bn); } topicnameIterator = source.getTopicNames().iterator(); while (topicnameIterator.hasNext()) { TopicNameIF bn2 = topicnameIterator.next(); // first copy the type, fixes #409 TopicIF nametype = mergemap.get(bn2.getType()); if (nametype == null) { nametype = copyTopic(builder.getTopicMap(), bn2.getType()); mergemap.put(bn2.getType(), nametype); copyCharacteristics(nametype, bn2.getType(), mergemap); } TopicNameIF bn1 = builder.makeTopicName(target, nametype, bn2.getValue()); copyScope(bn1, bn2, mergemap); String key = KeyGenerator.makeTopicNameKey(bn1); TopicNameIF dupl = map.get(key); if (dupl == null) { copyVariants(bn1, bn2, mergemap); } else { bn1.remove(); bn1 = dupl; copyVariants(bn1, bn2, mergemap); } copyReifier(bn1, bn2, mergemap); copySourceLocators(bn1, bn2); } // copying occurrences Set<String> keys = new CompactHashSet<String>(); Iterator<OccurrenceIF> occurrenceIterator = target.getOccurrences().iterator(); while (occurrenceIterator.hasNext()) keys.add(KeyGenerator.makeOccurrenceKey(occurrenceIterator.next())); occurrenceIterator = source.getOccurrences().iterator(); while (occurrenceIterator.hasNext()) { OccurrenceIF occ2 = occurrenceIterator.next(); // first copy the type, fixes #409 TopicIF occtype = mergemap.get(occ2.getType()); if (occtype == null) { occtype = copyTopic(builder.getTopicMap(), occ2.getType()); mergemap.put(occ2.getType(), occtype); copyCharacteristics(occtype, occ2.getType(), mergemap); } OccurrenceIF occ1 = builder.makeOccurrence(target, occtype, ""); CopyUtils.copyOccurrenceData(occ1, occ2); copyScope(occ1, occ2, mergemap); copyReifier(occ1, occ2, mergemap); if (keys.contains(KeyGenerator.makeOccurrenceKey(occ1))) occ1.remove(); else copySourceLocators(occ1, occ2); } // note: roles are not copied; they are left for the // association copying, which will take care of them more // cleanly than we could do here } private static void copyScope(ScopedIF target, ScopedIF source, Map<TopicIF, TopicIF> mergemap) { Iterator<TopicIF> it = source.getScope().iterator(); while (it.hasNext()) { TopicIF replacement = it.next(); target.addTheme(resolveTopic(target.getTopicMap(), replacement, mergemap)); } } private static TopicIF resolveTopic(TopicMapIF targetTopicMap, TopicIF sourceTopic, Map<TopicIF, TopicIF> mergemap) { if (sourceTopic == null) return null; if (mergemap.containsKey(sourceTopic)) return mergemap.get(sourceTopic); else return copyTopic(targetTopicMap, sourceTopic, mergemap); } private static void copyVariants(TopicNameIF target, TopicNameIF source, Map<TopicIF, TopicIF> mergemap) { TopicMapBuilderIF builder = target.getTopicMap().getBuilder(); Iterator<VariantNameIF> it = source.getVariants().iterator(); while (it.hasNext()) { VariantNameIF sv = it.next(); VariantNameIF tv = builder.makeVariantName(target, sv.getValue(), sv.getDataType()); copyScope(tv, sv, mergemap); copyReifier(tv, sv, mergemap); copySourceLocators(tv, sv); } } /** * PUBLIC: Find a topic in the other topic map which would merge * with the given topic if that were to be added to the same topic * map. Even if there are more topics which would merge only one is * returned. * @param othertm The topic map to find the corresponding topic in. * @param topic A topic in a topic map other than othertm to look up * in othertm. * @return The corresponding topic. * @since 5.1.3 */ public static TopicIF findTopic(TopicMapIF othertm, TopicIF topic) { TopicIF other; for (LocatorIF si : topic.getSubjectIdentifiers()) { other = othertm.getTopicBySubjectIdentifier(si); if (other != null) return other; } for (LocatorIF sl : topic.getSubjectLocators()) { other = othertm.getTopicBySubjectLocator(sl); if (other != null) return other; } for (LocatorIF ii : topic.getItemIdentifiers()) { other = (TopicIF) othertm.getObjectByItemIdentifier(ii); if (other != null) return other; } return null; } // --- equals methods // assumes obj1 and obj2 belong to same TM private static boolean equals(TMObjectIF obj1, TMObjectIF obj2) { // can't be topics, or we wouldn't be here if (obj1 instanceof AssociationIF && obj2 instanceof AssociationIF) { AssociationIF a1 = (AssociationIF) obj1; AssociationIF a2 = (AssociationIF) obj2; if (a1.getType() == a2.getType() && a1.getRoles().size() == a2.getRoles().size() && a1.getScope().equals(a2.getScope())) { ArrayList<AssociationRoleIF> roles2 = new ArrayList<AssociationRoleIF>(a2.getRoles()); Iterator<AssociationRoleIF> it1 = a1.getRoles().iterator(); while (it1.hasNext()) { AssociationRoleIF role1 = it1.next(); Iterator<AssociationRoleIF> it2 = roles2.iterator(); boolean found = false; while (it2.hasNext()) { AssociationRoleIF role2 = it2.next(); if (role2.getPlayer() == role1.getPlayer() && role1.getType() == role2.getType()) { roles2.remove(role2); found = true; break; } } if (!found) break; } return roles2.isEmpty(); } } else if (obj1 instanceof TopicNameIF && obj2 instanceof TopicNameIF) { TopicNameIF bn1 = (TopicNameIF) obj1; TopicNameIF bn2 = (TopicNameIF) obj2; return (bn1.getTopic().equals(bn2.getTopic()) && sameAs(bn1.getValue(), bn2.getValue()) && sameAs(bn1.getType(), bn2.getType()) && sameAs(bn1.getScope(), bn2.getScope())); } else if (obj1 instanceof OccurrenceIF && obj2 instanceof OccurrenceIF) { OccurrenceIF occ1 = (OccurrenceIF) obj1; OccurrenceIF occ2 = (OccurrenceIF) obj2; return (occ1.getTopic().equals(occ2.getTopic()) && sameAs(occ1.getValue(), occ2.getValue()) && sameAs(occ1.getDataType(), occ2.getDataType()) && sameAs(occ1.getType(), occ2.getType()) && sameAs(occ1.getScope(), occ2.getScope())); } return false; } private static boolean sameAs(Object o1, Object o2) { return ((o1 == null && o2 == null) || (o1 != null && o1.equals(o2))); } private static void moveReified(TopicIF target, TopicIF source) { ReifiableIF sreified = source.getReified(); if (sreified != null) { ReifiableIF treified = target.getReified(); if (treified != null) { if (!KeyGenerator.makeKey(sreified).equals(KeyGenerator.makeKey(treified))) throw new ConstraintViolationException("Cannot merge topics which " + "reify different objects"); // FIXME: must verify that parents are equal mergeInto(treified, sreified); } else { sreified.setReifier(null); sreified.setReifier(target); } } } private static void moveReifier(ReifiableIF target, ReifiableIF source) { TopicIF sreifier = source.getReifier(); if (sreifier != null) { TopicIF treifier = target.getReifier(); if (treifier != null) { source.setReifier(null); mergeInto(treifier, sreifier); } else { source.setReifier(null); target.setReifier(sreifier); } } } private static void notifyTransaction(TMObjectIF source, TMObjectIF target) { TopicMapStoreIF store = target.getTopicMap().getStore(); if (store instanceof RDBMSTopicMapStore) { ((RDBMSTopicMapStore) store).merged(source, target); } } }