/* * #! * 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.Set; import java.util.Map; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.ArrayList; import java.util.Collection; import net.ontopia.utils.DeciderIF; import net.ontopia.utils.StringUtils; import net.ontopia.utils.DeciderUtils; import net.ontopia.utils.CompactHashSet; import net.ontopia.infoset.core.LocatorIF; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.topicmaps.core.ScopedIF; import net.ontopia.topicmaps.core.TMObjectIF; import net.ontopia.topicmaps.core.TopicMapIF; import net.ontopia.topicmaps.core.ReifiableIF; import net.ontopia.topicmaps.core.TopicNameIF; import net.ontopia.topicmaps.core.OccurrenceIF; import net.ontopia.topicmaps.core.VariantNameIF; import net.ontopia.topicmaps.core.AssociationIF; import net.ontopia.topicmaps.core.AssociationRoleIF; import net.ontopia.topicmaps.core.TopicMapBuilderIF; import net.ontopia.topicmaps.query.core.QueryResultIF; import net.ontopia.topicmaps.query.core.QueryProcessorIF; import net.ontopia.topicmaps.query.core.InvalidQueryException; import net.ontopia.topicmaps.query.utils.QueryUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * PUBLIC: Implementation of the TMSync algorithm. * @since 3.1.1 */ public class TopicMapSynchronizer { // --- define a logging category. static Logger log = LoggerFactory.getLogger(TopicMapSynchronizer.class.getName()); /** * PUBLIC: Updates the target topic map against the source topic, * including all characteristics from the source topic. */ public static void update(TopicMapIF target, TopicIF source) { update(target, source, DeciderUtils.<TMObjectIF>getTrueDecider()); } /** * PUBLIC: Updates the target topic map against the source topic, * synchronizing only the characteristics from the target that are * accepted by the filter. */ public static void update(TopicMapIF target, TopicIF source, DeciderIF<TMObjectIF> tfilter) { update(target, source, tfilter, DeciderUtils.<TMObjectIF>getTrueDecider()); } /** * PUBLIC: Updates the target topic map against the source topic, * synchronizing only the characteristics from the target and source * that are accepted by the filters. * @param target the topic map to update * @param source the topic to get updates from * @param tfilter filter for the target characteristics to update * @param sfilter filter for the source characteristics to include * @since 3.2.0 */ public static void update(TopicMapIF target, TopicIF source, DeciderIF<TMObjectIF> tfilter, DeciderIF<TMObjectIF> sfilter) { AssociationTracker tracker = new AssociationTracker(); update(target, source, tfilter, sfilter, tracker); // delete unsupported associations Iterator<AssociationIF> it = tracker.getUnsupported().iterator(); while (it.hasNext()) { AssociationIF tassoc = it.next(); log.debug(" target associations removed {}", tassoc); tassoc.remove(); } } /** * INTERNAL: Updates the target topic in the usual way, but does not * delete associations. Instead, it registers its findings using the * AssociationTracker. It is then up to the caller to delete * unwanted associations. The general principle is that associations * are wanted as long as there is one source that wants them; the * method will therefore feel free to copy new associations from the * source. In addition, associations to topics outside the set of * topics being synchronized must be kept because they cannot be * synchronized (they belong to the topics not being synchronized). */ private static void update(TopicMapIF target, TopicIF source, DeciderIF<TMObjectIF> tfilter, DeciderIF<TMObjectIF> sfilter, AssociationTracker tracker) { TopicMapBuilderIF builder = target.getBuilder(); // find target TopicIF targett = getTopic(target, source); if (targett == null) { targett = builder.makeTopic(); log.debug("Updating new target {} with source {}", targett, source); } else { log.debug("Updating existing target {} with source {}", targett, source); } targett = copyIdentifiers(targett, source); // synchronize types Set<TopicIF> origtypes = new CompactHashSet<TopicIF>(targett.getTypes()); Iterator<TopicIF> topicIterator = source.getTypes().iterator(); while (topicIterator.hasNext()) { TopicIF stype = topicIterator.next(); TopicIF ttype = getOrCreate(target, stype); if (origtypes.contains(ttype)) origtypes.remove(ttype); else targett.addType(ttype); } topicIterator = origtypes.iterator(); while (topicIterator.hasNext()) targett.removeType(topicIterator.next()); // synchronize names Map<String, TopicNameIF> originalTopicNames = new HashMap<String, TopicNameIF>(); Iterator<TopicNameIF> topicnameIterator = targett.getTopicNames().iterator(); while (topicnameIterator.hasNext()) { TopicNameIF bn = topicnameIterator.next(); if (tfilter.ok(bn)) { log.debug(" target name included {}", bn); originalTopicNames.put(KeyGenerator.makeTopicNameKey(bn), bn); } else { log.debug(" target name excluded {}", bn); } } topicnameIterator = source.getTopicNames().iterator(); while (topicnameIterator.hasNext()) { TopicNameIF sbn = topicnameIterator.next(); if (!sfilter.ok(sbn)) { log.debug(" source name excluded {}", sbn); continue; } log.debug(" source name included {}", sbn); TopicIF ttype = getOrCreate(target, sbn.getType()); Collection<TopicIF> tscope = translateScope(target, sbn.getScope()); String key = KeyGenerator.makeScopeKey(tscope) + "$" + KeyGenerator.makeTopicKey(ttype) + "$$" + sbn.getValue(); if (originalTopicNames.containsKey(key)) { TopicNameIF tbn = originalTopicNames.get(key); update(tbn, sbn, tfilter); originalTopicNames.remove(key); } else { TopicNameIF tbn = builder.makeTopicName(targett, ttype, sbn.getValue()); addScope(tbn, tscope); addReifier(tbn, sbn.getReifier(), tfilter, sfilter, tracker); update(tbn, sbn, tfilter); log.debug(" target name added {}", tbn); } } topicnameIterator = originalTopicNames.values().iterator(); while (topicnameIterator.hasNext()) { TopicNameIF tbn = topicnameIterator.next(); log.debug(" target name removed {}", tbn); tbn.remove(); } // synchronize occurrences Map<String, OccurrenceIF> originalOccurrences = new HashMap<String, OccurrenceIF>(); Iterator<OccurrenceIF> occurrenceIterator = targett.getOccurrences().iterator(); while (occurrenceIterator.hasNext()) { OccurrenceIF occ = occurrenceIterator.next(); if (tfilter.ok(occ)) { log.debug(" target occurrence included: {}", occ); originalOccurrences.put(KeyGenerator.makeOccurrenceKey(occ), occ); } else { log.debug(" target occurrence excluded {}", occ); } } occurrenceIterator = source.getOccurrences().iterator(); while (occurrenceIterator.hasNext()) { OccurrenceIF socc = occurrenceIterator.next(); if (!sfilter.ok(socc)) { log.debug(" source occurrence excluded {}", socc); continue; } log.debug(" source occurrence included: {}", socc); TopicIF ttype = getOrCreate(target, socc.getType()); Collection<TopicIF> tscope = translateScope(target, socc.getScope()); String key = KeyGenerator.makeScopeKey(tscope) + "$" + KeyGenerator.makeTopicKey(ttype) + KeyGenerator.makeDataKey(socc); if (originalOccurrences.containsKey(key)) originalOccurrences.remove(key); else { OccurrenceIF tocc = builder.makeOccurrence(targett, ttype, ""); CopyUtils.copyOccurrenceData(tocc, socc); addScope(tocc, tscope); addReifier(tocc, socc.getReifier(), tfilter, sfilter, tracker); log.debug(" target occurrence added {}", tocc); } } occurrenceIterator = originalOccurrences.values().iterator(); while (occurrenceIterator.hasNext()) { OccurrenceIF tocc = occurrenceIterator.next(); log.debug(" target occurrence removed {}", tocc); tocc.remove(); } // synchronize associations // originals tracked by AssociationTracker, not the 'origs' set Iterator<AssociationRoleIF> roleIterator = targett.getRoles().iterator(); while (roleIterator.hasNext()) { AssociationRoleIF role = roleIterator.next(); AssociationIF assoc = role.getAssociation(); if (tfilter.ok(assoc) && tracker.isWithinSyncSet(assoc)) { log.debug(" target association included: {}", assoc); tracker.unwanted(assoc); // means: unwanted if not found in source } else { log.debug(" target association excluded {}", assoc); } } roleIterator = source.getRoles().iterator(); while (roleIterator.hasNext()) { AssociationRoleIF role = roleIterator.next(); AssociationIF sassoc = role.getAssociation(); if (!sfilter.ok(sassoc)) { log.debug(" source association excluded {}", sassoc); continue; } log.debug(" source association included: {}", sassoc); TopicIF ttype = getOrCreate(target, sassoc.getType()); Collection<TopicIF> tscope = translateScope(target, sassoc.getScope()); String key = KeyGenerator.makeTopicKey(ttype) + "$" + KeyGenerator.makeScopeKey(tscope) + "$" + makeRoleKeys(target, sassoc.getRoles()); if (!tracker.isKnown(key)) { // if the key is not known it means this association does not // exist in the target, and so we must create it AssociationIF tassoc = builder.makeAssociation(ttype); addScope(tassoc, tscope); addReifier(tassoc, sassoc.getReifier(), tfilter, sfilter, tracker); Iterator<AssociationRoleIF> it2 = sassoc.getRoles().iterator(); while (it2.hasNext()) { role = it2.next(); builder.makeAssociationRole(tassoc, getOrCreate(target, role.getType()), getOrCreate(target, role.getPlayer())); } log.debug(" target association added {}", tassoc); } tracker.wanted(key); } // run duplicate suppression DuplicateSuppressionUtils.removeDuplicates(targett); DuplicateSuppressionUtils.removeDuplicateAssociations(targett); } /** * PUBLIC: Updates the target topic map from the source topic map, * synchronizing the selected topics in the target (ttopicq) with * the selected topics in the source (stopicq) using the deciders to * filter topic characteristics to synchronize. * @param target the topic map to update * @param ttopicq tolog query selecting the target topics to update * @param tchard filter for the target characteristics to update * @param source the source topic map * @param stopicq tolog query selecting the source topics to use * @param schard filter for the source characteristics to update */ public static void update(TopicMapIF target, String ttopicq, DeciderIF<TMObjectIF> tchard, TopicMapIF source, String stopicq, DeciderIF<TMObjectIF> schard) throws InvalidQueryException { // build sets of topics Set<TopicIF> targetts = queryForSet(target, ttopicq); Set<TopicIF> sourcets = queryForSet(source, stopicq); // loop over source topics (we change targetts later, so we have to pass // a copy to the tracker) AssociationTracker tracker = new AssociationTracker(new CompactHashSet<TopicIF>(targetts), sourcets); Iterator<TopicIF> topicIterator = sourcets.iterator(); while (topicIterator.hasNext()) { TopicIF stopic = topicIterator.next(); TopicIF ttopic = getOrCreate(target, stopic); targetts.remove(ttopic); update(target, stopic, tchard, schard, tracker); } // remove extraneous associations Iterator<AssociationIF> associationIterator = tracker.getUnsupported().iterator(); while (associationIterator.hasNext()) { AssociationIF assoc = associationIterator.next(); log.debug("Tracker removing {}", assoc); assoc.remove(); } // remove extraneous topics topicIterator = targetts.iterator(); while (topicIterator.hasNext()) topicIterator.next().remove(); } // ----------------------------------------------------------------- // INTERNAL // ----------------------------------------------------------------- private static Set<TopicIF> queryForSet(TopicMapIF tm, String query) throws InvalidQueryException { Set<TopicIF> set = new CompactHashSet<TopicIF>(); QueryProcessorIF proc = QueryUtils.getQueryProcessor(tm); QueryResultIF result = proc.execute(query); while (result.next()) set.add((TopicIF) result.getValue(0)); result.close(); return set; } private static void update(TopicNameIF tbn, TopicNameIF sbn, DeciderIF<TMObjectIF> tfilter) { TopicMapIF target = tbn.getTopicMap(); TopicMapBuilderIF builder = target.getBuilder(); // build map of existing variants Map<String, VariantNameIF> origs = new HashMap<String, VariantNameIF>(); Iterator<VariantNameIF> it = tbn.getVariants().iterator(); while (it.hasNext()) { VariantNameIF vn = it.next(); if (tfilter.ok(vn)) origs.put(KeyGenerator.makeVariantKey(vn), vn); } // walk through new variants it = sbn.getVariants().iterator(); while (it.hasNext()) { VariantNameIF svn = it.next(); Collection<TopicIF> tscope = translateScope(target, svn.getScope()); String key = KeyGenerator.makeScopeKey(tscope) + KeyGenerator.makeDataKey(svn); if (origs.containsKey(key)) origs.remove(key); // we've got it already; remember not to delete it else { // this is a new variant; add it VariantNameIF tvn = builder.makeVariantName(tbn, svn.getValue(), svn.getDataType()); addScope(tvn, tscope); } } // delete old variants not in source it = origs.values().iterator(); while (it.hasNext()) it.next().remove(); } private static String makeRoleKeys(TopicMapIF tm, Collection<AssociationRoleIF> roles) { String[] rolekeys = new String[roles.size()]; int i = 0; for (Iterator<AssociationRoleIF> it = roles.iterator(); it.hasNext(); ) { AssociationRoleIF role = it.next(); TopicIF ttype = getOrCreate(tm, role.getType()); TopicIF tplayer = getOrCreate(tm, role.getPlayer()); rolekeys[i++] = KeyGenerator.makeTopicKey(ttype) + ":" + KeyGenerator.makeTopicKey(tplayer); } Arrays.sort(rolekeys); return StringUtils.join(rolekeys, "$"); } private static TopicIF getOrCreate(TopicMapIF tm, TopicIF source) { if (source == null) return null; TopicIF target = getTopic(tm, source); if (target == null) { target = tm.getBuilder().makeTopic(); target = copyIdentifiers(target, source); } return target; } private static TopicIF getTopic(TopicMapIF tm, TopicIF find) { // ISSUE: what if find maps to multiple topics in target? // ISSUE: what if find has no identity? TopicIF found = null; Iterator<LocatorIF> it = find.getSubjectLocators().iterator(); while (it.hasNext() && found == null) { LocatorIF psi = it.next(); found = tm.getTopicBySubjectLocator(psi); } it = find.getSubjectIdentifiers().iterator(); while (it.hasNext() && found == null) { LocatorIF psi = it.next(); found = tm.getTopicBySubjectIdentifier(psi); } it = find.getItemIdentifiers().iterator(); while (it.hasNext() && found == null) { LocatorIF srcloc = it.next(); TMObjectIF obj = tm.getObjectByItemIdentifier(srcloc); // ISSUE: what if this is not a topic? if (obj instanceof TopicIF) found = (TopicIF) obj; } return found; } private static TopicIF copyIdentifiers(TopicIF target, TopicIF source) { return MergeUtils.copyIdentifiers(target, source); } private static Collection<TopicIF> translateScope(TopicMapIF tm, Collection<TopicIF> sscope) { Collection<TopicIF> tscope = new ArrayList<TopicIF>(); Iterator<TopicIF> it = sscope.iterator(); while (it.hasNext()) { TopicIF topic = it.next(); tscope.add(getOrCreate(tm, topic)); } return tscope; } private static void addScope(ScopedIF scoped, Collection<TopicIF> scope) { Iterator<TopicIF> it = scope.iterator(); while (it.hasNext()) scoped.addTheme(it.next()); } // reifiers is topic in source, not target! private static void addReifier(ReifiableIF reified, TopicIF reifiers, DeciderIF<TMObjectIF> tfilter, DeciderIF<TMObjectIF> sfilter, AssociationTracker tracker) { if (reifiers == null) return; if (!tracker.isSourceTopicsSet()) { // this means we're synchronizing a single topic. different mode // of operation if (!sfilter.ok(reifiers)) return; // client doesn't want the reifier, so we skip it // FIXME: if there is cycle of reification here we could fall into // a recursion well // sync the reifier across update(reified.getTopicMap(), reifiers, tfilter, sfilter, tracker); } else if (!tracker.inSourceTopics(reifiers)) // this means we're synchronizing a set of topics, but the reifier // is not one of them, so we skip it return; // just set the reifier. statements about the reifier will either be // synchronized by the main code, or have been synchronized above. TopicIF reifiert = getOrCreate(reified.getTopicMap(), reifiers); reified.setReifier(reifiert); } // --- AssociationTracker /** * Used to track which associations are wanted by at least one * topic, and which are not wanted by any topic. In addition, it * keeps track of which topics are being synchronized (in both * source and target) in order to be able to control which * associations should be synchronized. */ static class AssociationTracker { private Set<TopicIF> targettopics; // target topics being synchronized private Set<TopicIF> sourcetopics; // source topics being synchronized private Set<String> wanted; // there is a source which wants these associations private Map<String, AssociationIF> unwanted; // no source wants these associations public AssociationTracker(Set<TopicIF> targettopics, Set<TopicIF> sourcetopics) { this.targettopics = targettopics; this.sourcetopics = sourcetopics; this.wanted = new CompactHashSet<String>(); this.unwanted = new HashMap<String, AssociationIF>(); } public AssociationTracker() { this(null, null); } /** * Returns true iff all players are within set of topics being * synchronized. */ public boolean isWithinSyncSet(AssociationIF assoc) { if (targettopics == null) return true; Iterator<AssociationRoleIF> it = assoc.getRoles().iterator(); while (it.hasNext()) { AssociationRoleIF role = it.next(); if (!targettopics.contains(role.getPlayer())) return false; } return true; } public boolean isKnown(String key) { return wanted.contains(key) || unwanted.containsKey(key); } public void wanted(String key) { // we do not pass the AssociationIF object as this may not exist in // the target if (unwanted.containsKey(key)) unwanted.remove(key); wanted.add(key); } public void unwanted(AssociationIF assoc) { String key = KeyGenerator.makeAssociationKey(assoc); if (!wanted.contains(key)) unwanted.put(key, assoc); } public Collection<AssociationIF> getUnsupported() { return unwanted.values(); } public boolean isSourceTopicsSet() { return (sourcetopics != null); } public boolean inSourceTopics(TopicIF topic) { if (sourcetopics == null) return false; return sourcetopics.contains(topic); } } }