/* * #! * 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.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Stack; import net.ontopia.topicmaps.core.AssociationIF; import net.ontopia.topicmaps.core.AssociationRoleIF; import net.ontopia.topicmaps.core.TMObjectIF; import net.ontopia.topicmaps.core.TopicIF; import net.ontopia.utils.DeciderIF; import net.ontopia.utils.DeciderIterator; import net.ontopia.utils.EqualsDecider; import net.ontopia.utils.GrabberDecider; /** * PUBLIC: Computes the transitive closure of a relation characterized by * two specific roles within a specific association type.</p> * * The relation is characterized by an association type A and a pair * of roles R1 & R2 such that two topics T1 and T2 are related if T1 * plays role R1 and T2 plays role R2 in an association A. * * A <i>transitive</i> relation is where "x is related to * y" and "y is related to z" always implies that "x is related to * z" Here, an association type, together with two roles * within that type of association, is taken as the relation * which is transitive.</p> * * In topic map terms, if: T1 plays role R1 in association A1 and T2 * plays role R2 in association A1 and: T2 plays role R1 in * association A2 and T3 plays role R2 in association A2 then: T1 and * T3 are also (transitively) related.</p> */ public class AssociationWalker { /** * PROTECTED: The decider used to filter associations to only those * which are being walked */ protected DeciderIF<AssociationIF> assocDecider; /** * PROTECTED: The decider used to filter the left-hand role of the * transitive association */ protected DeciderIF<AssociationRoleIF> leftRoleDecider; /** * PROTECTED: The decider used to filter the right-hand role of the * transitive association. */ protected DeciderIF<AssociationRoleIF> rightRoleDecider; /** * PROTECTED: The listeners to be informed as the walker processes * the topic map. */ protected List<AssociationWalkerListenerIF> listeners; /** * PUBLIC: Creates a walker which determines that a topic A is * related to topic B if A plays a role specified by * <code>leftRoleSpec</code> in an association of type * <code>associationType</code> and topic B plays a role specified * by <code>rightRoleSpec</code> in the same association. * * @param associationType The given association type; an object * implementing TopicIF. * @param leftRoleSpec The first given association rolespec; an * object implementing TopicIF. * @param rightRoleSpec The second given association rolespec; an * object implementing TopicIF. */ public AssociationWalker(TopicIF associationType, TopicIF leftRoleSpec, TopicIF rightRoleSpec) { assocDecider = new GrabberDecider<AssociationIF, TopicIF>(new TypedIFGrabber<AssociationIF>(), new EqualsDecider<TopicIF>(associationType)); leftRoleDecider = new GrabberDecider<AssociationRoleIF, TopicIF>(new TypedIFGrabber<AssociationRoleIF>(), new EqualsDecider<TopicIF>(leftRoleSpec)); rightRoleDecider = new GrabberDecider<AssociationRoleIF, TopicIF>(new TypedIFGrabber<AssociationRoleIF>(), new EqualsDecider<TopicIF>(rightRoleSpec)); listeners = new ArrayList<AssociationWalkerListenerIF>(); } /** * PUBLIC: Creates a walker which uses deciders to traverse the associations. * * @param assocDecider ; an object implementing DeciderIF. * @param fromRoleDecider ; an object implementing DeciderIF. * @param toRoleDecider ; an object implementing DeciderIF. */ public AssociationWalker(DeciderIF<AssociationIF> assocDecider, DeciderIF<AssociationRoleIF> fromRoleDecider, DeciderIF<AssociationRoleIF> toRoleDecider) { this.assocDecider = assocDecider; leftRoleDecider = fromRoleDecider; rightRoleDecider = toRoleDecider; listeners = new ArrayList<AssociationWalkerListenerIF>(); } /** * PUBLIC: Computes the transitive closure under the association * type and rolespec definitions provided in the constructor, and * returns the result as a set of topics. * * @param start The topic to start the computation from; an object * implementing TopicIF. * @return An unmodifiable Set of TopicIF objects; the topics * present in the closure. */ public Set<TopicIF> walkTopics(TopicIF start) { WalkerState state = walk(start, false); return Collections.unmodifiableSet(state.closure); } /** * PUBLIC: Computes the transitive closure under the association * type and rolespec definitions provided in the constructor, and * returns a set containing the paths taken through the topic map in * computing the closure. Each path is a list consisting of * alternating TopicIF and AssociationIF entries. The element at * the start of the list is the starting TopicIF. The following * AssociationIF is an association in which the TopicIF plays the * specified left-hand role. The next node is a TopicIF which plays * the specified right-hand role in the association and so on. The * walker algorithm avoids cycles by cutting off paths as soon as a * duplicate topic is encountered. * * @param start The topic to start the computation from; an object * implementing TopicIF. * @return An unmodifiable Collection of List objects. */ public Collection<List<TMObjectIF>> walkPaths(TopicIF start) { WalkerState state = walk(start, true); return Collections.unmodifiableCollection(state.paths); } /** * PROTECTED: Computes the transitive closure under the association * type and rolespec definitions provided in the constructor; this * method is used by both walkTopics and walkPaths. If the * <code>storePaths</code> parameter is <code>false</code> then the * walker will collect only the set of topics which form the * transitive closure and will not store the individual paths * discovered. * * @param start The topic to start the computation from; an object * implementing TopicIF. * @param storePaths Boolean: if true, store paths walked; if * false, store only topics found. * @return A WalkerState object; the state of the walk at completion. */ protected WalkerState walk(TopicIF start, boolean storePaths) { WalkerState state = new WalkerState(start, storePaths); doWalk(start, state); return state; } /** * PRIVATE: Iterates through the roles played by fromTopic which are * of the type defined as the leftRoleSpec in the constructor, then * for each role, grabs the association and iterates through the * roles which are of the type defined as the rightRoleSpec in the * constructor. The heart of the walker function. * * <p> Whenever a (left-role-player, association, right-role-player) * triple are found, the walkAssociation() member function is * invoked; the association and right-role-player are appended to * the current tree path; and the function is called recursively to * walk from the right-role-player. * * <p> Whenever no right-role-players are found, the current tree * path is added to the set of processed tree paths and the * recursion unwinds. Cycles are avoided by never recursively * processing a right-role-player if it is already part of the * closure. * * @param fromTopic The topic from which to start the walk; an * object implementing TopicIF. * @param state A WalkerState object which contains the final * state at the end of the walk. */ private void doWalk(TopicIF fromTopic, WalkerState state) { // ignore if from topic is null if (fromTopic == null) return; Collection<AssociationRoleIF> fromRoles = fromTopic.getRoles(); if (fromRoles.isEmpty()) { foundLeaf(state); } else { DeciderIterator<AssociationRoleIF> leftRolesIt = new DeciderIterator<AssociationRoleIF>(leftRoleDecider, fromRoles.iterator()); if (!leftRolesIt.hasNext()) { foundLeaf(state); } while (!state.foundTopic && leftRolesIt.hasNext()) { AssociationRoleIF leftRole = leftRolesIt.next(); AssociationIF assoc = leftRole.getAssociation(); if (assocDecider.ok(assoc)) { Collection<AssociationRoleIF> assocRoles = assoc.getRoles(); DeciderIterator<AssociationRoleIF> rightRolesIt = new DeciderIterator<AssociationRoleIF>(rightRoleDecider, assocRoles.iterator()); if (!rightRolesIt.hasNext()) { // We have traversed to a leaf. Add the current path to the tree set foundLeaf(state); } else { // This association is another node in the tree so we can add it to // the current path and then traverse it. state.pushPath(assoc); while (!state.foundTopic && rightRolesIt.hasNext()) { AssociationRoleIF rightRole = rightRolesIt.next(); TopicIF rightPlayer = rightRole.getPlayer(); state.pushPath(rightPlayer); if (state.closure.contains(rightPlayer)) { // Reached a topic which we have already traversed. foundLeaf(state); } else { state.closure.add(rightPlayer); notifyListeners(fromTopic, assoc, rightPlayer); if ((state.toTopic != null) && (state.toTopic.equals(rightPlayer))) { state.foundTopic = true; return; } else { doWalk(rightPlayer, state); } } state.popPath(); } state.popPath(); } } } } } /** * PROTECTED: Invoked when the walker encounters the end of a * transitive association path. This function is used to store the * association path for later retrieval. If the current association * path is a singleton, it is not stored. * * @param state A WalkerState object; the current state of the walk. */ protected void foundLeaf(WalkerState state) { if (state.storePaths && (state.currPath.size() > 1)) { state.addCurrPath(); } } /** * PUBLIC: Returns true if the two topics are directly or indirectly * associated under the association type and rolespec definitions * provided in the constructor for this walker. The calculation is * performed using a depth-first traversal of the tree formed by the * associations concerned, which aborts as soon as the associated * topic is found. * * @param start The topic to begin computation from; an object implementing TopicIF. * @param associated The topic to be found in the association; an object implementing TopicIF. * * @return Boolean: true iff the given topics are directly or indirectly associated */ public boolean isAssociated(TopicIF start, TopicIF associated) { WalkerState state = new WalkerState(start, false); state.toTopic = associated; doWalk(start, state); return state.foundTopic; } /** * PUBLIC: Registers a listener with the walker. The listener will * be notified each time the walker encounters a topic, association, * associated-topic triple. * * @param listener The listener to be registered; an object * implementing AssociationWalkerListenerIF. * @see AssociationWalkerListenerIF */ public void addListener(AssociationWalkerListenerIF listener) { listeners.add(listener); } /** * PUBLIC: Unregisters a listener with the walker. * * @param listener The listener to be unregistered; an object * implementing AssociationWalkerListenerIF. * @see AssociationWalkerListenerIF */ public void removeListener(AssociationWalkerListenerIF listener) { listeners.remove(listener); } /** * PRIVATE: This function is invoked by the walk() function, for each * topic, association, associated-topic triple found during * the computation of the transitive closure. * It notifies each registered listener of the triple encountered. * * @param leftRolePlayer The first topic in the triple; an object implementing TopicIF. * @param assoc The association in the triple; an object implementing AssociationIF. * @param rightRolePlayer The second topic in the triple; an object implementing TopicIF. */ private void notifyListeners(TopicIF leftRolePlayer, AssociationIF assoc, TopicIF rightRolePlayer) { Iterator<AssociationWalkerListenerIF> it = listeners.iterator(); while (it.hasNext()) { AssociationWalkerListenerIF listener = it.next(); listener.walkAssociation(leftRolePlayer, assoc, rightRolePlayer); } } } /** * PRIVATE: A simple data structure which maintains the state of a walk. */ class WalkerState { /** * PROTECTED: The topics which form the transitive closure. * This variable is null if no walk has been * performed. */ protected Set<TopicIF> closure; /** * PROTECTED: The paths followed by the last walk. The storage of * the paths in the transitive closure is optional. This variable is * null if no walk has been performed or if the last walk was * invoked without storing paths. */ protected Collection<List<TMObjectIF>> paths; /** * PROTECTED: The tree path currently being walked. */ Stack<TMObjectIF> currPath; /** * PROTECTED: The topic to start walking from */ protected TopicIF startTopic; /** * PROTECTED: The topic to be located by the walk. */ protected TopicIF toTopic; /** * PROTECTED: Flag indicating if the walk has found the topic it was * looking for. */ protected boolean foundTopic; /** * PROTECTED: Flag indicating whether to store the paths found * through the associated topics set. */ protected boolean storePaths; protected WalkerState(TopicIF start, boolean storePaths) { startTopic = start; this.storePaths = storePaths; if (storePaths) paths = new ArrayList<List<TMObjectIF>>(); currPath = new Stack<TMObjectIF>(); currPath.push(start); foundTopic = false; toTopic = null; closure = new HashSet<TopicIF>(); } protected void addCurrPath() { paths.add(new ArrayList<TMObjectIF>(currPath)); } protected void pushPath(TMObjectIF lastElement) { if (storePaths) currPath.push(lastElement); } protected void popPath() { if (storePaths) currPath.pop(); } }