net.ontopia.topicmaps.utils.TopicMapSynchronizer.java Source code

Java tutorial

Introduction

Here is the source code for net.ontopia.topicmaps.utils.TopicMapSynchronizer.java

Source

/*
 * #!
 * 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.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.topicmaps.core.AssociationIF;
import net.ontopia.topicmaps.core.AssociationRoleIF;
import net.ontopia.topicmaps.core.OccurrenceIF;
import net.ontopia.topicmaps.core.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.TopicNameIF;
import net.ontopia.topicmaps.core.VariantNameIF;
import net.ontopia.topicmaps.query.core.InvalidQueryException;
import net.ontopia.topicmaps.query.core.QueryProcessorIF;
import net.ontopia.topicmaps.query.core.QueryResultIF;
import net.ontopia.topicmaps.query.utils.QueryUtils;
import net.ontopia.utils.CompactHashSet;
import net.ontopia.utils.DeciderIF;
import net.ontopia.utils.DeciderUtils;
import org.apache.commons.lang3.StringUtils;
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.
    private static final 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);
        }
    }
}