All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.ontopia.topicmaps.db2tm.Processor Maven / Gradle / Ivy

The newest version!
/*
 * #!
 * Ontopia DB2TM
 * #-
 * 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.db2tm;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import net.ontopia.infoset.core.LocatorIF;
import net.ontopia.infoset.impl.basic.URILocator;
import net.ontopia.topicmaps.core.AssociationIF;
import net.ontopia.topicmaps.core.AssociationRoleIF;
import net.ontopia.topicmaps.core.DataTypes;
import net.ontopia.topicmaps.core.OccurrenceIF;
import net.ontopia.topicmaps.core.ScopedIF;
import net.ontopia.topicmaps.core.TMObjectIF;
import net.ontopia.topicmaps.core.TopicIF;
import net.ontopia.topicmaps.core.TopicMapIF;
import net.ontopia.topicmaps.core.TopicMapStoreIF;
import net.ontopia.topicmaps.core.TopicNameIF;
import net.ontopia.topicmaps.entry.TopicMapReferenceIF;
import net.ontopia.topicmaps.impl.basic.InMemoryTopicMapStore;
import net.ontopia.topicmaps.utils.PSI;
import net.ontopia.utils.CompactHashSet;
import net.ontopia.utils.OntopiaRuntimeException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * INTERNAL: Class that performs the actual db2tm processing.
 */
public class Processor {
  
  // --- define a logging category.
  private static Logger log = LoggerFactory.getLogger(Processor.class);
  
  public static int NEVER_COMMIT_MODE = 0;
  public static int RELATIONAL_COMMIT_MODE = 1;
  public static int TUPLE_COMMIT_MODE = 2;
  public static int COUNT_COMMIT_MODE = 3;
  public static int DEFAULT_COMMIT_MODE = NEVER_COMMIT_MODE;

  private static final LocatorIF LOC_SYNCHRONIZATION_STATE =
    URILocator.create("http://psi.ontopia.net/db2tm/synchronization-state");
    
  private Processor() {
  }
  
  private static TopicMapIF doCommit(TopicMapIF topicmap) throws IOException {
    TopicMapStoreIF store = topicmap.getStore();
    TopicMapReferenceIF reference = store.getReference();
    store.commit();
    if (!(store instanceof InMemoryTopicMapStore)) {
      store.close();
      store = reference.createStore(false);
    } //never forget!
    topicmap = store.getTopicMap();

    return topicmap;
  }

  private static String commitModeToString(int usedCommitMode, int usedCommitCount) {
    if (usedCommitMode == RELATIONAL_COMMIT_MODE) {
      return "relational";
    }
    if (usedCommitMode == TUPLE_COMMIT_MODE) {
      return "tuple";
    }
    if (usedCommitMode == COUNT_COMMIT_MODE) {
      return "count (" + usedCommitCount + ")";
    }
    return "unknown";
  }

  /**
   * INTERNAL: Runs a DB2TM process by adding tuples to the topic map.
   */
  public static void addRelations(RelationMapping rmapping, Collection relnames, TopicMapIF topicmap, LocatorIF baseloc) {
    int ttuples = 0;
    long tstime = System.currentTimeMillis();
    Context ctx = new Context();
    if (log.isInfoEnabled()) {
      log.info("Adding relations: {}", new Date());
    }

    try {
      // verify relation mapping
      Map> ds_relations = Utils.verifyRelationsForMapping(rmapping);
      
      // set up context object
      ctx.setMapping(rmapping);
      ctx.setTopicMap(topicmap);

      int topLevelCommitMode = NEVER_COMMIT_MODE; // default
      int topLevelCommitCount = -1;

      // figure out the commit mode
      String cm = rmapping.getCommitMode();
      if (cm != null) {
        if ("relation".equals(cm)) {
          topLevelCommitMode = RELATIONAL_COMMIT_MODE;
        }
        if ("tuple".equals(cm)) {
          topLevelCommitMode = TUPLE_COMMIT_MODE;
        }
        if (cm.startsWith("count:")) {
          topLevelCommitMode = COUNT_COMMIT_MODE;
          topLevelCommitCount = Integer.parseInt(cm.substring(6));
        }
      }

      if (baseloc != null) {
        ctx.setBaseLocator(baseloc);
      } else {
        log.info("No base locator specified, so using base of topic maps store.");
        ctx.setBaseLocator(topicmap.getStore().getBaseAddress());
      }
      
      // loop over datasources
      for (DataSourceIF datasource : ds_relations.keySet()) {
        log.debug("Adding tuples from data source {}", datasource);
      
        // loop over relations
        for (Relation relation : ds_relations.get(datasource)) {
          String relationName = relation.getName();

          // do not process non-listed relations
          if (relnames != null && !relnames.contains(relationName)) {
            log.debug("  ignoring relation: {}", relationName);
            continue;
          } else {
            log.info("  adding relation: {}", relationName);
          }
      
          int rtuples = 0;
          long rstime1 = System.currentTimeMillis();
          long rstime2 = 0;
      
          // set current relation
          ctx.setRelation(relation);
      
          // figure out commit mode for this relation
          int usedCommitMode = NEVER_COMMIT_MODE; // default
          int usedCommitCount = -1;
          cm = relation.getCommitMode();
          if (cm == null) {
            usedCommitMode = topLevelCommitMode;
            usedCommitCount = topLevelCommitCount;
          } else {
            if ("relation".equals(cm)) {
              usedCommitMode = RELATIONAL_COMMIT_MODE;
            }
            if ("tuple".equals(cm)) {
              usedCommitMode = TUPLE_COMMIT_MODE;
            }
            if (cm.startsWith("count:")) {
              usedCommitMode = COUNT_COMMIT_MODE;
              usedCommitCount = Integer.parseInt(cm.substring(6));
            }
          }

          // changelog synchronization; set start order values
            for (Changelog sync : relation.getSyncs()) {
              String maxOrderValue = datasource.getMaxOrderValue(sync);
              log.debug("New order value: {}={}", sync.getTable(), maxOrderValue);
              setStartOrder(sync, ctx, maxOrderValue);
            }

          if (usedCommitMode > NEVER_COMMIT_MODE) {
            log.info("  using commit mode: {}", commitModeToString(usedCommitMode, usedCommitCount));
          }
          
          // loop over tuples        
          TupleReaderIF reader = datasource.getReader(relationName);
          String [] tuple = null;
          while ((tuple = reader.readNext()) != null) {
            // process individual tuple
            long time = System.currentTimeMillis();
            // FIXME: we could change to updateTuple here with no ill effects,
            // except possibly a performance hit. still debating whether to do
            // that.
            addTuple(relation, tuple, ctx);
            rstime2 += (System.currentTimeMillis()-time);
            rtuples++;
            if (usedCommitMode == TUPLE_COMMIT_MODE) {
              topicmap = doCommit(topicmap);
              ctx.setTopicMap(topicmap);
            }

            if ((usedCommitMode == COUNT_COMMIT_MODE) && (rtuples % usedCommitCount == 0)) {
              topicmap = doCommit(topicmap);
              ctx.setTopicMap(topicmap);
              log.info("    committed after {} tuples ", rtuples);
            }
          }

          // commit if needed
          if ((usedCommitMode == RELATIONAL_COMMIT_MODE) || (usedCommitMode == COUNT_COMMIT_MODE)) {
            topicmap = doCommit(topicmap);
            ctx.setTopicMap(topicmap);
          }

          log.info("    Added {} tuples from {}, {}/{} ms",
                   new Object[] {rtuples, relationName, (System.currentTimeMillis()-rstime1), rstime2});
          ttuples += rtuples;
          reader.close();
        }
      }
    } catch (Exception e) {
      // unwrap so we can find the real exception
      String msg = e.getMessage();
      if (e instanceof OntopiaRuntimeException &&
          e.getCause() instanceof Exception) {
        e = (Exception) e.getCause();
      }
      
      if (e instanceof DB2TMException) {
        // don't wrap if it's already a DB2TMException, because this causes
        // the cmd-line tool to hide the real error
        throw (DB2TMException) e;
      } else {
        throw new DB2TMException(msg == null ? "Error occurred in addRelations call." : msg, e);
      }
    } finally {
      ctx.close();
    }
    if (log.isInfoEnabled()) {
      log.info("done adding relations: {} tuples, {} ms. {}", new Object[] {ttuples, (System.currentTimeMillis()-tstime), new Date()});
    }
  }

  /**
   * INTERNAL: Runs a DB2TM process by removing tuples from the topic map.
   */
  public static void removeRelations(RelationMapping rmapping, Collection relnames, TopicMapIF topicmap, LocatorIF baseloc) {
    int ttuples = 0;
    long tstime = System.currentTimeMillis();
    Context ctx = new Context();
    if (log.isInfoEnabled()) {
      log.info("Removing relations: {}", new Date());
    }

    try {
      // verify relation mapping
      Map> ds_relations = Utils.verifyRelationsForMapping(rmapping);
      
      // set up context object
      ctx.setMapping(rmapping);
      ctx.setTopicMap(topicmap);
      if (baseloc != null) {
        ctx.setBaseLocator(baseloc);
      } else {
        log.info("No base locator specified, so using base of topic maps store.");
        ctx.setBaseLocator(topicmap.getStore().getBaseAddress());
      }
      
      // loop over datasources
      for (DataSourceIF datasource : ds_relations.keySet()) {
        log.debug("Removing tuples from data source: {}", datasource);
      
        // loop over relations
        for (Relation relation : ds_relations.get(datasource)) {
          String relationName = relation.getName();
      
          // do not process non-listed relations
          if (relnames != null && !relnames.contains(relationName)) {
            log.debug("  ignoring relation: {}", relationName);
            continue;
          } else {
            log.debug("  removing relation: {}", relationName);
          }
      
          int rtuples = 0;
          long rstime1 = System.currentTimeMillis();
          long rstime2 = 0;
      
          // set current relation
          ctx.setRelation(relation);
          
          // loop over tuples        
          TupleReaderIF reader = datasource.getReader(relationName);
          
          String [] tuple = null;
          while ((tuple = reader.readNext()) != null) {
            // process individual tuple
            long time = System.currentTimeMillis();
            removeTuple(relation, tuple, ctx);
            rstime2 += (System.currentTimeMillis()-time);
            rtuples++;
          }
          log.info("    Removed {} tuples from {}, {}/{} ms",
                   new Object[] {rtuples, relationName, (System.currentTimeMillis()-rstime1), rstime2});
          ttuples += rtuples;
        }
      }
    } catch (Exception e) {
      throw new DB2TMException("Error occurred in removeRelations call.", e);
    } finally {
      ctx.close();
    }
    if (log.isInfoEnabled()) {
      log.info("done removing relations: {} tuples, {} ms. {}", new Object[] {ttuples, (System.currentTimeMillis()-tstime), new Date()});
    }
  }
  
  public static void addTuple(Relation relation, String[] tuple, Context ctx) {
    if (log.isDebugEnabled()) {
      log.debug("    a({}),{}", StringUtils.join(tuple, "|"), tuple.length);
    }
    
    List entities = relation.getEntities();
    for (int i=0; i < entities.size(); i++) {
      Entity entity = entities.get(i);
      try {
        Object o = addEntity(relation, entity, tuple, ctx);
        ctx.setEntityObject(i, o);
      } catch (Exception e) {
        throw new DB2TMException("Error occurred while adding tuple " + Arrays.asList(tuple) + " from relation " + relation.getName() + " to entity " + entity, e);
      }
    }
  }

  protected static Object addEntity(Relation relation, Entity entity, String[] tuple, Context ctx) {
    // check condition before proceeding
    if (!checkCondition(relation, entity, tuple, ctx)) {
      return null;
    }
    
    TopicIF topic = null;
    
    if (entity.requiresTopic()) {
      // look up or create topic given identities
      topic = addIdentities(topic, relation, entity, tuple, ctx);

      // NOTE: if the topic is null at this point none of the identity
      // locators can be created
      
      // do nothing more not if entity is not primary
      if (topic == null) {
        if (!entity.isPrimary()) {
          return null;
        } else if (entity.getEntityType() == Entity.TYPE_TOPIC) {
          throw new DB2TMInputException("Not able to find topic for primary entity. None of the identity fields could be used.", entity, tuple);
        } else if (entity.getEntityType() == Entity.TYPE_ASSOCIATION) {
          // create new topic if not found
          topic = ctx.getBuilder().makeTopic();
          ctx.registerNewObject(topic);
        }
      }
      
      // add topic types
      if (entity.getEntityType() == Entity.TYPE_TOPIC) {
        // NOTE: association reifiers cannot have types
        addTypes(topic, entity.getTypes(), entity, tuple, ctx);
      }
        
      // add characteristics
      List cfields = entity.getCharacteristicFields();
      for (int i=0; i < cfields.size(); i++) {
       Field field = cfields.get(i);
          
       switch (field.getFieldType()) {
       case Field.TYPE_TOPIC_NAME:
         addTopicName(topic, relation, entity, field, i, tuple, ctx);
         break;
       case Field.TYPE_OCCURRENCE:
         addOccurrence(topic, relation, entity, field, i, tuple, ctx);
         break;
       case Field.TYPE_PLAYER:
         addPlayer(topic, relation, entity, field, i, tuple, ctx);
         break;
       default:
         throw new DB2TMConfigException("Illegal characteristic field type: " + field);
       }
      }
    }

    if (entity.getEntityType() == Entity.TYPE_ASSOCIATION) {
      // create association
      return addAssociation(topic, relation, entity, tuple, ctx);

    } else {
      return topic;
    }
  }

  protected static boolean checkCondition(Relation relation, Entity entity, String[] tuple, Context ctx) {
    ValueIF condition = entity.getConditionValue();    
    return (condition == null || condition.getValue(tuple) != null);
  }
  
  protected static AssociationIF addAssociation(TopicIF reifier, Relation relation, Entity entity, 
                                                String[] tuple, Context ctx) {
    
    // roles in association
    List rfields = entity.getRoleFields();
    int rlen = rfields.size();
    
    // only create association when all mandatory players actually exist
    TopicIF[] rtypes = new TopicIF[rlen];
    TopicIF[] players = new TopicIF[rlen];
    for (int i=0; i < rlen; i++) {
      Field role = rfields.get(i);
      players[i] = Utils.getTopic(role.getPlayer(), ctx);
      // if player is null then we'll do nothing
      if (players[i] == null) {
        switch (role.getOptional()) {
        case Field.OPTIONAL_FALSE:
          return null;
        case Field.OPTIONAL_TRUE:
          continue;
        case Field.OPTIONAL_DEFAULT:
          if (rlen > 2) {
            continue;
        } else {
            return null;
        }
        }
      }
      // get role type
      rtypes[i] = Utils.getTopic(role.getRoleType(), ctx);
      if (rtypes[i] == null) {
        throw new DB2TMInputException("Role type not found", entity, tuple, role.getRoleType());
      }
    }
    
    // find association      
    AssociationIF assoc = findAssociationByIdentities(relation, entity, tuple, ctx);

    // get association type
    TopicIF atype = Utils.getTopic(entity.getAssociationType(), ctx);
    if (atype == null) {
      throw new DB2TMInputException("Association type not found", entity, tuple, entity.getAssociationType());
    }
    
    if (assoc == null) {    
      // create association
      assoc = ctx.getBuilder().makeAssociation(atype);
      log.trace("      +A {} {}", assoc, atype);
    
      // add roles
      int arity = 0;
      for (int i=0; i < rlen; i++) {
        if (players[i] != null) {
          arity++;
          log.trace("      +R {} :{}", players[i], rtypes[i]);
          ctx.getBuilder().makeAssociationRole(assoc, rtypes[i], players[i]);
          if (arity == 1) {
            ctx.characteristicsChanged(players[i]);
          }
        }
      }
      
      // add scope
      addScope(assoc, entity.getScope(), entity, tuple, ctx);
    
      
    } else {
      // reuse association      
      log.trace("      =A {}", assoc);
      assoc.setType(atype);

      List oroles = new ArrayList(assoc.getRoles());
      for (int i=0; i < rlen; i++) {
        AssociationRoleIF or = extractRoleOfType(oroles, rtypes[i]);
        if (or != null) {
          if (!Objects.equals(or.getPlayer(), players[i])) {
            or.setPlayer(players[i]);
          }
          log.trace("      =R {} :{}", players[i], rtypes[i]);
        } else {
          log.trace("      +R {} :{}", players[i], rtypes[i]);
          ctx.getBuilder().makeAssociationRole(assoc, rtypes[i], players[i]);
        }
        if (i == 1) {
          ctx.characteristicsChanged(players[i]);
        }
      }
      if (!oroles.isEmpty()) {
        for (int i=0; i < oroles.size(); i++) {
          AssociationRoleIF or = oroles.get(i);
          TopicIF player = or.getPlayer();
          log.trace("      -R {} :{}", player, or.getType());
          or.remove();
          if (player != null) {
            ctx.characteristicsChanged(player);
          }
        }
      }
    
      // replace scope
      updateScope(assoc, entity.getScope(), entity, tuple, ctx);      
    }
    
    // add missing identities
    addIdentities(assoc, relation, entity, tuple, ctx);

    // if reifier, handle reification
    if (reifier != null) {
      assoc.setReifier(reifier);
    }
    return assoc;
  }

  public static void removeTuple(Relation relation, String[] tuple, Context ctx) {
    if (log.isDebugEnabled()) {
      log.trace("    r({}),{}", StringUtils.join(tuple, "|"), tuple.length);
    }

    List entities = relation.getEntities();
    
    // first find entity objects with ids (used to look up other
    // characteristics)
    for (int i=0; i < entities.size(); i++) {
      Entity entity = entities.get(i);
      Object o = findTopicByIdentities(relation, entity, tuple, ctx);
      ctx.setEntityObject(i, o);
    }
    // then try to remove each of them (note: reverse order)
    for (int i=entities.size()-1; i >=0; i--) {
      Entity entity = entities.get(i);
      try {
        removeEntity(relation, entity, tuple, ctx);
      } catch (Exception e) {
        throw new DB2TMException("Error occurred while removing tuple " + Arrays.asList(tuple) + " from relation " + relation.getName() + " to entity " + entity, e);
      }
    }
  }

  protected static void removeEntity(Relation relation, Entity entity, String[] tuple, Context ctx) {
    // find candidate topic
    TopicIF topic = null;
    if (entity.requiresTopic()) {
      topic = findTopicByIdentities(relation, entity, tuple, ctx);
    }
    
    if (entity.getEntityType() == Entity.TYPE_TOPIC) {
      // remove topic
      if (topic != null) {
        removeTopic(topic, relation, entity, tuple, ctx);
      }
      
    } else if (entity.getEntityType() == Entity.TYPE_ASSOCIATION) {
      if (topic != null) {
        // if reifier topic found, then use that topic to find association instance
        TMObjectIF reified = topic.getReified();
        if (reified instanceof AssociationIF) {
          // remove association
          AssociationIF assoc = (AssociationIF)reified;
          log.trace("      -A-reified {} -> {} {}", new Object[] {topic, assoc, assoc.getType()});
          assoc.remove();
        }
        // remove reifier topic
        removeTopic(topic, relation, entity, tuple, ctx);
      } else {
        // find association 
        removeAssociation(relation, entity, tuple, ctx);
      }
    }
  }

  protected static void removeTopic(TopicIF topic, Relation relation, Entity entity, String[] tuple, Context ctx) {
    // if entity is primary; delete topic
    if (entity.isPrimary()) {
      // delete topic (and identities)
      deleteTopic(topic);
    } else {
      log.debug("      >T {}", topic);

      // TODO: reject if non-primary entity and relation.cardinality > 1 and field is dynamic
      // CONSTRAINT: primary entity cannot occur in multiple rows if changelog
    
      // remove characteristics, but not identities
      for (Field field : entity.getCharacteristicFields()) {
        switch (field.getFieldType()) {
        case Field.TYPE_TOPIC_NAME: {
          List names = getTopicNames(topic, relation, entity, field, tuple, ctx);
          for (int i=0; i < names.size(); i++) {
            TopicNameIF _bn = names.get(i);
            log.trace("      -N {} {}", topic, _bn);
            _bn.remove();
          }
          //! removeTopicName(topic, relation, entity, field, tuple, ctx);
          break;
        } case Field.TYPE_OCCURRENCE: {
          List occs = getOccurrences(topic, relation, entity, field, tuple, ctx);
          for (int i=0; i < occs.size(); i++) {
            OccurrenceIF _occ = occs.get(i);
            log.trace("      -O {} {}", topic, _occ);
            _occ.remove();
          }
          //! removeOccurrence(topic, relation, entity, field, tuple, ctx);
          break;
        } case Field.TYPE_PLAYER: {
          List roles = getPlayers(topic, relation, entity, field, tuple, ctx);
          for (int i=0; i < roles.size(); i++) {
            AssociationRoleIF role = roles.get(i);
            AssociationIF assoc = role.getAssociation();
            log.trace("      -P {} {}", assoc, assoc.getType());
            assoc.remove();
          }
          //! removePlayer(topic, relation, entity, field, tuple, ctx);
          break;
        } default:
          throw new DB2TMConfigException("Illegal characteristic field type: " + field);
        }
      }
      // remove types
      removeTypes(topic, entity.getTypes(), ctx);          
    }
  }

  protected static void deleteTopic(TopicIF topic) {
    // first remove all topics that reifies any of the topic's associations
    for (AssociationRoleIF role : topic.getRoles()) {
      AssociationIF assoc = role.getAssociation();
      // if reifier topic found, then remove it
      TopicIF reifier = assoc.getReifier();
      if (reifier != null) {
        // remove reifier topic
        log.trace("      -A-reifier {} {} -> {}", new Object[] {topic, reifier, assoc});
        reifier.remove();
      }
    }
    // remove topic (and identities)
    log.debug("      -T {}", topic);
    topic.remove();
  }
  
  protected static TopicIF findTopicByIdentities(Relation relation, Entity entity, String[] tuple, Context ctx) {
    for (Field field : entity.getIdentityFields()) {
      TopicIF topic = findTopicByIdentity(relation, entity, field, tuple, ctx);
      if (topic != null) {
        return topic;
      }
    }
    return null;
  }
  
  private static TopicIF findTopicByIdentity(Relation relation, Entity entity, Field field, String[] tuple, Context ctx) {
    switch (field.getFieldType()) {
    case Field.TYPE_SUBJECT_LOCATOR: {
      LocatorIF loc = Utils.getLocator(relation, entity, field, tuple, ctx);
      if (loc == null) {
        return null;
      }
      return ctx.getTopicMap().getTopicBySubjectLocator(loc);
    }
    case Field.TYPE_SUBJECT_IDENTIFIER: {
      LocatorIF loc = Utils.getLocator(relation, entity, field, tuple, ctx);
      if (loc == null) {
        return null;
      }
      return ctx.getTopicMap().getTopicBySubjectIdentifier(loc);
    }
    case Field.TYPE_ITEM_IDENTIFIER: {
      // note: do not look up topics by item identifier if entity type is association
      if (entity.getEntityType() == Entity.TYPE_ASSOCIATION) {
        return null;
      }
      LocatorIF loc = Utils.getLocator(relation, entity, field, tuple, ctx);
      if (loc == null) {
        return null;
      }
      TMObjectIF tmobject = ctx.getTopicMap().getObjectByItemIdentifier(loc);
      if (tmobject instanceof TopicIF) {
        return (TopicIF)tmobject;
      } else {
        if (tmobject != null) {
          log.warn("Item identifier lookup returned non-topic: {} -> {}", loc, tmobject);
        }
        return null;
      }
    } default:
      throw new DB2TMConfigException("Illegal identity field type: " + field);
    }
  }
  
  protected static AssociationIF findAssociationByIdentities(Relation relation, Entity entity, String[] tuple, Context ctx) {
    // look up association object by item identifier
    for (Field field : entity.getIdentityFields()) {
      // associations can only have item identifiers
      if (field.getFieldType() == Field.TYPE_ITEM_IDENTIFIER) {
        LocatorIF loc = Utils.getLocator(relation, entity, field, tuple, ctx);
        TMObjectIF tmobject = ctx.getTopicMap().getObjectByItemIdentifier(loc);
        if (tmobject instanceof AssociationIF) {
          return (AssociationIF)tmobject;
        } else if (tmobject != null) {
          log.warn("Item identifier lookup returned non-association: {} -> {}", loc, tmobject);
        }
      }
    }
    return null;
  }

  protected static TopicIF addIdentities(TopicIF topic, Relation relation,
                                         Entity entity,
                                         String[] tuple, Context ctx) {
    // look up topic and/or add identities
    TopicMapIF tm = ctx.getTopicMap();

    // Note: topic will be created only if entity is primary
    for (Field field : entity.getIdentityFields()) {
      TopicIF found = null;
      LocatorIF loc = null;

      switch (field.getFieldType()) {
      case Field.TYPE_SUBJECT_LOCATOR:
        loc = Utils.getLocator(relation, entity, field, tuple, ctx);
        if (loc == null) {
          continue;
      }
        found = tm.getTopicBySubjectLocator(loc);

        if (found != null) {
          if (topic != null) {
            if (!found.equals(topic)) {
              ctx.mergeTopics(topic, found);
            }
          } else {
            topic = found;
          }
        } else {
          if (topic == null) {
            topic = ctx.getBuilder().makeTopic();
            ctx.registerNewObject(topic);
          }
          topic.addSubjectLocator(loc);
        }
        break;
      case Field.TYPE_SUBJECT_IDENTIFIER:
        loc = Utils.getLocator(relation, entity, field, tuple, ctx);
        if (loc == null) {
          continue;
      }
        found = tm.getTopicBySubjectIdentifier(loc);

        if (found != null) {
          if (topic != null) {
            if (!found.equals(topic)) {
              ctx.mergeTopics(topic, found);
            }
          } else {
            topic = found;
          }
        } else {
          if (topic == null) {
            topic = ctx.getBuilder().makeTopic();
            ctx.registerNewObject(topic);
          }
          topic.addSubjectIdentifier(loc);
        }
        break;
      case Field.TYPE_ITEM_IDENTIFIER:
        // note: add item identifier iff entity type is topic
        if (entity.getEntityType() == Entity.TYPE_TOPIC) {
          loc = Utils.getLocator(relation, entity, field, tuple, ctx);
          if (loc == null) {
            continue;
          }
          found = (TopicIF) tm.getObjectByItemIdentifier(loc);
          
          if (found != null) {
            if (topic != null) {
              if (!found.equals(topic)) {
                ctx.mergeTopics(topic, found);
              }
            } else {
              topic = found;
            }
          } else {
            if (topic == null) {
              topic = ctx.getBuilder().makeTopic();
              ctx.registerNewObject(topic);
            }
            topic.addItemIdentifier(loc);
          }
        }
        break;
      default:
        throw new DB2TMConfigException("Illegal identity field type: " + field);
      }
    }
    return topic;
  }

  protected static TopicIF updateIdentities(TopicIF topic, Relation relation, Entity entity,
                                            String[] tuple, Context ctx) {
    // FIXME: do we want to just blindly update identities like this?
    return addIdentities(topic, relation, entity, tuple, ctx);
  }
  
  protected static void addIdentities(AssociationIF assoc, Relation relation, Entity entity,
                                      String[] tuple, Context ctx) {
    Objects.requireNonNull(assoc, "Cannot add identities to null association.");
    for (Field field : entity.getIdentityFields()) {
      if (field.getFieldType() == Field.TYPE_ITEM_IDENTIFIER) {
        LocatorIF loc = Utils.getLocator(relation, entity, field, tuple, ctx);
        if (loc == null) {
          continue;
        }        
        // note: at this point we should know that there are no other objects with the same identity
        assoc.addItemIdentifier(loc);
      }      
    }
  }
  
  protected static void addTypes(TopicIF topic, String[] types, Entity entity, String[] tuple, Context ctx) {
    for (int i = 0; i < types.length; i++) {
      TopicIF type = Utils.getTopic(types[i], ctx);
      if (type != null) {
        topic.addType(type);
      } else {
        throw new DB2TMInputException("Topic type not found", entity, tuple, types[i]);
      }
    }
  }

  protected static void updateTypes(TopicIF topic, String[] types, Entity entity, String[] tuple, Context ctx) {
    // this is a bit convoluted because we don't want to change anything
    // unless the set of types has actually changed

    // tracking old types with this
    Set oldtypes = new CompactHashSet(topic.getTypes());

    // loop over new list of types
    for (int i = 0; i < types.length; i++) {
      TopicIF type = Utils.getTopic(types[i], ctx);
      if (type == null) {
        throw new DB2TMInputException("Topic type not found", entity, tuple,
                types[i]);
      }

      if (oldtypes.contains(type)) {
        oldtypes.remove(type);
      } else {
        topic.addType(type);
      }
    }

    // any old types that still remain need to be removed
    for (TopicIF oldtype : oldtypes) {
      topic.removeType(oldtype);
    }
  }
  
  protected static void removeTypes(TopicIF topic, String[] types, Context ctx) {
    for (int i = 0; i < types.length; i++) {
      TopicIF type = Utils.getTopic(types[i], ctx);
      if (type != null) {
        topic.removeType(type);
      }
    }
  }

  protected static void addScope(ScopedIF scoped, String[] scope, Entity entity, String[] tuple, Context ctx) {
    // TODO: should really remove any existing scope
    for (int i = 0; i < scope.length; i++) {
      TopicIF theme = Utils.getTopic(scope[i], ctx);
      if (theme != null) {
        scoped.addTheme(theme);
      } else {
        throw new DB2TMInputException("Scoping topic not found", entity, tuple, scope[i]);
      }
    }
  }

  protected static void updateScope(ScopedIF scoped, String[] scope, Entity entity, String[] tuple, Context ctx) {
    // clear existing scope
    Collection _scope = scoped.getScope();
    if (!_scope.isEmpty()) {
      TopicIF[] themes = _scope.toArray(new TopicIF[0]);
      for (int i=0; i < themes.length; i++) {
        scoped.removeTheme(themes[i]);
      }
    }
    // add new scoping topics
    addScope(scoped, scope, entity, tuple, ctx);
  }
  
  protected static boolean compareScope(String[] scope1, Collection scope2, Entity entity, String[] tuple, Context ctx) {
    if (scope1.length != scope2.size()) {
      return false; // ISSUE: what if scope attribute contains duplicates?
    }
    for (int i=0; i < scope1.length; i++) {
      TopicIF theme = Utils.getTopic(scope1[i], ctx);
      if (theme == null) {
        throw new DB2TMInputException("Scoping topic not found", entity, tuple, scope1[i]);
      }
      if (!scope2.contains(theme)) {
        return false;
      }
    }
    return true;
  }

  protected static void addTopicName(TopicIF topic, Relation relation,
                                     Entity entity, Field field, int fieldIndex,
                                     String[] tuple, Context ctx) {
    String value = Utils.getValue(relation, entity, field, tuple, ctx);
    if (!Utils.isValueEmpty(value)) {
      TopicIF type = Utils.getTopic(field.getType(), ctx);
      if (type == null && field.getType() != null) {
        throw new DB2TMInputException("Name type not found", entity, tuple, field.getType());
      }
      
      TopicNameIF bn = (TopicNameIF)ctx.reuseOldFieldValue(topic, fieldIndex);
      if (bn == null) {
        bn = ctx.getBuilder().makeTopicName(topic, type, value);
        addScope(bn, field.getScope(), entity, tuple, ctx);
        log.trace("      +N {} {}", topic, bn);
      } else {
        if (!bn.getValue().equals(value)) {
          bn.setValue(value);
          log.trace("      =N {} {}", topic, bn);
        }
      }
      // notify context
      ctx.characteristicsChanged(topic);
    }
  }

  protected static List getTopicNames(TopicIF topic, Relation relation,
                                      Entity entity, Field field,
                                      String[] tuple, Context ctx) {
    TopicIF type = Utils.getTopic(field.getType(), ctx);
    if (type == null) {
      if (field.getType() != null) {
        throw new DB2TMInputException("Name type not found", entity, tuple,
                field.getType());
      }
      // this means the type is the default name type. sync of this will fail
      // because null != defaultnametype.
      type = getDefaultNameType(ctx);
    }
    
    // loop over names and update
    List result = new ArrayList();
    Collection bns = topic.getTopicNames();
    if (!bns.isEmpty()) {
      TopicNameIF[] ba = bns.toArray(new TopicNameIF[0]);
      for (int i=0; i < ba.length; i++) {
        TopicNameIF _bn = ba[i];
        // check type
        TopicIF _type = _bn.getType();
        if (!Objects.equals(_type, type)) {
          continue;
        }
        // check scope
        if (!compareScope(field.getScope(), _bn.getScope(), entity, tuple, ctx)) {
          continue;
        }
        result.add(_bn);          
      }
    }
    return result;
  }

  private static TopicIF getDefaultNameType(Context ctx) {
    // if the default topic name type doesn't exist this will return nothing.
    // that's OK, because in that case there will be no existing names with
    // this type, either.
    return ctx.getTopicMap().getTopicBySubjectIdentifier(PSI.getSAMNameType());
  }

  protected static void removeTopicName(TopicIF topic, Relation relation, Entity entity, Field field,
                                        String[] tuple, Context ctx) {
    String value = Utils.getValue(relation, entity, field, tuple, ctx);
    TopicIF type = Utils.getTopic(field.getType(), ctx);
    if (type == null) {
      if (field.getType() != null) {
        throw new DB2TMInputException("Name type not found", entity, tuple,
                                      field.getType());
      }
      // this means the type is the default name type. remove will fail
      // because null != defaultnametype.
      type = getDefaultNameType(ctx);
    }

    // loop over names and remove first matching
    Iterator iter = topic.getTopicNames().iterator();
    while (iter.hasNext()) {
      TopicNameIF _bn = iter.next();
      // check value
      if (!Objects.equals(_bn.getValue(), value)) {
        continue;
      }
      // check type
      if (!Objects.equals(_bn.getType(), type)) {
        continue;
      }
      // check scope
      if (!compareScope(field.getScope(), _bn.getScope(), entity, tuple, ctx)) {
        continue;
      }

      log.trace("      -N {} {}", topic, _bn);
      // remove matching name
      _bn.remove();
      // notify context
      ctx.characteristicsChanged(topic);
      break;
    }
  }

  protected static void addOccurrence(TopicIF topic, Relation relation,
                                      Entity entity, Field field, int fieldIndex,
                                      String[] tuple, Context ctx) {
    String value = Utils.getValue(relation, entity, field, tuple, ctx);
    if (!Utils.isValueEmpty(value)) {

      TopicIF type = Utils.getTopic(field.getType(), ctx);
      if (type == null) {
        throw new DB2TMInputException("Occurrence type not found", entity, tuple, field.getType());
      }

      String occvalue = value;
      LocatorIF occDatatype = DataTypes.TYPE_STRING;
      if (field.getDatatype() != null) {
        String datatype = Utils.expandPrefixedValue(field.getDatatype(), ctx);
        if (datatype.equals(DataTypes.TYPE_URI)) {
          occvalue = ctx.getBaseLocator().resolveAbsolute(value).getAddress();
          occDatatype = DataTypes.TYPE_URI;
        } else {
          occDatatype = URILocator.create(datatype);
        }
      }
      
      OccurrenceIF oc = (OccurrenceIF)ctx.reuseOldFieldValue(topic, fieldIndex);
      if (oc == null) {
        // FIXME: rewrite so that we can set occurrence value directly
        oc = ctx.getBuilder().makeOccurrence(topic, type, occvalue, occDatatype); 
        addScope(oc, field.getScope(), entity, tuple, ctx);
        log.trace("      +O {} {}", topic, oc);
      } else {
        if (!oc.getValue().equals(occvalue) ||
            !oc.getDataType().equals(occDatatype)) {
          oc.setValue(occvalue, occDatatype);
          log.trace("      =O {} {}", topic, oc);
        }
      }
      // notify context
      ctx.characteristicsChanged(topic);
    }
  }

  protected static List getOccurrences(TopicIF topic, Relation relation, Entity entity, Field field,
                                       String[] tuple, Context ctx) {
    TopicIF type = Utils.getTopic(field.getType(), ctx);
    if (type == null) {
      throw new DB2TMInputException("Occurrence type not found", entity, tuple, field.getType());
    }
    
    //! String datatype = (field.getDatatype() == null ? null : Utils.expandPrefixedValue(field.getDatatype(), ctx));    

    // loop over occurrences and clear
    List result = new ArrayList();
    Collection occs = topic.getOccurrences();
    if (!occs.isEmpty()) {
      OccurrenceIF[] oa = occs.toArray(new OccurrenceIF[0]);
      for (int i=0; i < oa.length; i++) {
        OccurrenceIF _occ = oa[i];
        // check type
        if (!Objects.equals(_occ.getType(), type)) {
          continue;
        }

				// FIXME: compare datatype?

        // check scope
        if (!compareScope(field.getScope(), _occ.getScope(), entity, tuple, ctx)) {
          continue;
        }
        result.add(_occ);                  
      }
    }
    return result;
  }
    
  protected static void removeOccurrence(TopicIF topic, Relation relation, Entity entity, Field field,
                                         String[] tuple, Context ctx) {
    
    String value = Utils.getValue(relation, entity, field, tuple, ctx);
    TopicIF type = Utils.getTopic(field.getType(), ctx);
    if (type == null) {
      throw new DB2TMInputException("Occurrence type not found", entity, tuple, field.getType());
    }

    //! String datatype = (field.getDatatype() == null ? null : Utils.expandPrefixedValue(field.getDatatype(), ctx));

    // loop over occurrences and remove first matching
    Iterator iter = topic.getOccurrences().iterator();
    while (iter.hasNext()) {
      OccurrenceIF _occ = iter.next();
      // check value or locator
			if (!Objects.equals(_occ.getValue(), value)) {
        continue;
      }

			// FIXME: compare datatype?

      // check type
      if (!Objects.equals(_occ.getType(), type)) {
        continue;
      }
      // check scope
      if (!compareScope(field.getScope(), _occ.getScope(), entity, tuple, ctx)) {
        continue;
      }

      log.trace("      -O {} {}", topic, _occ);
      // remove matching occurrence
      _occ.remove();
      // notify context
      ctx.characteristicsChanged(topic);
      break;
    }
  }

  protected static void addPlayer(TopicIF topic, Relation relation,
                                  Entity entity, Field field, int fieldIndex,
                                  String[] tuple, Context ctx) {
    
    // other roles in association
    List rfields = field.getOtherRoleFields();
    int rlen = rfields.size();

    // only create association when all mandatory players actually exist
    TopicIF[] rtypes = new TopicIF[rlen];
    TopicIF[] players = new TopicIF[rlen];
    for (int i=0; i < rlen; i++) {
      Field role = rfields.get(i);
      players[i] = Utils.getTopic(role.getPlayer(), ctx);

      // if player is null then we'll do nothing
      if (players[i] == null) {
        switch (role.getOptional()) {
        case Field.OPTIONAL_FALSE:
          return;
        case Field.OPTIONAL_TRUE:
          continue;
        case Field.OPTIONAL_DEFAULT:
          if (rlen > 2) {
            continue;
        } else {
            return;
        }
        }
      }
      // get role type
      rtypes[i] = Utils.getTopic(role.getRoleType(), ctx);
      if (rtypes[i] == null) {
        throw new DB2TMInputException("Role type not found", entity, tuple, role.getRoleType());
      }
    }

    AssociationRoleIF ar = (AssociationRoleIF)ctx.reuseOldFieldValue(topic, fieldIndex);
    if (ar == null) {
      // get association type
      TopicIF atype = Utils.getTopic(field.getAssociationType(), ctx);
      if (atype == null) {
        throw new DB2TMInputException("Association type not found", entity, tuple, entity.getAssociationType());
      }
      // get current role type
      TopicIF rtype = Utils.getTopic(field.getRoleType(), ctx);
      if (rtype == null) {
        throw new DB2TMInputException("Role type not found", entity, tuple, field.getRoleType());
      }
      
      // create association
      AssociationIF assoc = ctx.getBuilder().makeAssociation(atype);
      log.trace("      +P {} {}", assoc, atype);

      // add scope
      addScope(assoc, field.getScope(), entity, tuple, ctx);
      
      // add current role
      log.trace("      +R {} :{}", topic, rtype);
      ctx.getBuilder().makeAssociationRole(assoc, rtype, topic);
      
      // add other roles
      for (int i=0; i < rlen; i++) {
        // do not create role if player is null
        if (players[i] != null) {
          log.trace("      +R {} :{}", players[i], rtypes[i]);
          ctx.getBuilder().makeAssociationRole(assoc, rtypes[i], players[i]);
        } else {
          log.trace("      ?R {} :{}", players[i], rtypes[i]);          
        }
      }

    } else {
      // reuse association
      AssociationIF assoc = ar.getAssociation();
      log.trace("      =P {} {}", topic, assoc);

      List oroles = new ArrayList(assoc.getRoles());
      oroles.remove(ar);
      for (int i=0; i < rlen; i++) {
        AssociationRoleIF or = extractRoleOfType(oroles, rtypes[i]);
        if (or != null) {
          if (!Objects.equals(or.getPlayer(), players[i])) {
            or.setPlayer(players[i]);
          }
          log.trace("      =R {} :{}", players[i], rtypes[i]);
        } else {
          log.trace("      +R {} :{}", players[i], rtypes[i]);
          ctx.getBuilder().makeAssociationRole(assoc, rtypes[i], players[i]);
        }
      }
      if (!oroles.isEmpty()) {
        for (int i=0; i < oroles.size(); i++) {
          AssociationRoleIF or = oroles.get(i);
          TopicIF player = or.getPlayer();
          log.trace("      -R {} :{}", player, or.getType());
          or.remove();
          if (player != null) {
            ctx.characteristicsChanged(player);
          }
        }
      }      
    }
    // notify context
    ctx.characteristicsChanged(topic);
  }

  private static AssociationRoleIF extractRoleOfType(List roles, TopicIF rtype) {
    int length = roles.size();
    for (int i=0; i < length; i++) {
      AssociationRoleIF r = roles.get(i);
      if (Objects.equals(rtype, r.getType())) {
        roles.remove(i);
        return r;
      }
    }
    return null;
  }
  
  protected static List getPlayers(TopicIF topic, Relation relation, Entity entity, Field field,
                                   String[] tuple, Context ctx) {    
    TopicIF atype = Utils.getTopic(field.getAssociationType(), ctx);
    if (atype == null) {
      throw new DB2TMInputException("Association type not found", entity, tuple, field.getAssociationType());
    }
    TopicIF rtype_p = Utils.getTopic(field.getRoleType(), ctx);
    if (rtype_p == null) {
      throw new DB2TMInputException("Role type not found", entity, tuple, field.getRoleType());
    }

    // loop over roles and update
    List result = new ArrayList();
    Collection troles = topic.getRoles();
    if (!troles.isEmpty()) {
      AssociationRoleIF[] ra = troles.toArray(new AssociationRoleIF[0]);
      Collection rfields = field.getOtherRoleFields();
      
      outer:
      for (int i=0; i < ra.length; i++) {
        AssociationRoleIF role = ra[i];
        // check role type
        if (!Objects.equals(role.getType(), rtype_p)) {
          continue;
        }
        // check association type
        AssociationIF assoc = role.getAssociation();
        if (!Objects.equals(assoc.getType(), atype)) {
          continue;
        }
        // check scope
        if (!compareScope(field.getScope(), assoc.getScope(), entity, tuple, ctx)) {
          continue;
        }
        // check association cardinality
        Collection roles = assoc.getRoles();
        if (roles.size() != (rfields.size() + 1)) {
          continue;
        }
        for (AssociationRoleIF arole : roles) {
          if (arole.equals(role)) {
            continue;
          }
          TopicIF rtype = arole.getType();
          // check role
          Field matching_rfield = null;
          for (Field rfield : rfields) {
            TopicIF rtype_o = Utils.getTopic(rfield.getRoleType(), ctx);
            if (rtype_o == null) {
              throw new DB2TMInputException("Role type not found", entity, tuple, rfield.getRoleType());
            }
            // check role type
            if (!Objects.equals(rtype, rtype_o)) {
              continue;
            }
            // role field matched
            matching_rfield = rfield;
            break;
          }
          if (matching_rfield == null) {
            continue outer;
          }
        }
        result.add(role);
      }
    }
    return result;
  }
  
  protected static void removePlayer(TopicIF topic, Relation relation, Entity entity, Field field,
                                     String[] tuple, Context ctx) {
    
    TopicIF atype = Utils.getTopic(field.getAssociationType(), ctx);
    if (atype == null) {
      throw new DB2TMInputException("Association type not found", entity, tuple, field.getAssociationType());
    }
    TopicIF rtype_p = Utils.getTopic(field.getRoleType(), ctx);
    if (rtype_p == null) {
      throw new DB2TMInputException("Role type not found", entity, tuple, field.getRoleType());
    }

    Collection rfields = field.getOtherRoleFields();

    outer:
    for (AssociationRoleIF role : topic.getRoles()) {
      // check role type
      if (!Objects.equals(role.getType(), rtype_p)) {
        continue;
      }
      // check association type
      AssociationIF assoc = role.getAssociation();
      if (!Objects.equals(assoc.getType(), atype)) {
        continue;
      }
      // check scope
      if (!compareScope(field.getScope(), assoc.getScope(), entity, tuple, ctx)) {
        continue;
      }
      // check association cardinality
      Collection roles = assoc.getRoles();
      if (roles.size() != (rfields.size() + 1)) {
        continue;
      }
      for (AssociationRoleIF arole : roles) {
        if (arole.equals(role)) {
          continue;
        }
        TopicIF rtype = arole.getType();
        TopicIF player = arole.getPlayer();
        // check role
        Field matching_rfield = null;
        for (Field rfield : rfields) {
          TopicIF rtype_o = Utils.getTopic(rfield.getRoleType(), ctx);
          if (rtype_o == null) {
            throw new DB2TMInputException("Role type not found", entity, tuple, rfield.getRoleType());
          }
          // check role type and player
          if (!Objects.equals(rtype, rtype_o) ||
              !Objects.equals(player, Utils.getTopic(rfield.getPlayer(), ctx))) {
            continue;
          }
          // role field matched
          matching_rfield = rfield;
          break;
        }
        if (matching_rfield == null) {
          continue outer;
        }
      }
      //! // if reifier topic found, then remove it (or its characteristics)
      //! TopicIF reifier = assoc.getReifier();
      //! if (reifier != null)
      //!   // remove reifier topic
      //!   reifier.remove();
      log.trace("      -P {} {}", assoc, atype);
      // remove association
      assoc.remove();
      // notify context
      ctx.characteristicsChanged(topic);
      break;
    }    
  }

  protected static void removeAssociation(Relation relation, Entity entity, String[] tuple, Context ctx) {

    // TODO: needs improvement. take optional roles into account
    
    // use first role fields as starting point
    List rfields = entity.getRoleFields();
    Field pfield = rfields.get(0);
    
    TopicIF atype = Utils.getTopic(entity.getAssociationType(), ctx);
    if (atype == null) {
      throw new DB2TMInputException("Association type not found", entity, tuple, entity.getAssociationType());
    }
    TopicIF rtype_p = Utils.getTopic(pfield.getRoleType(), ctx);
    if (rtype_p == null) {
      throw new DB2TMInputException("Role type not found", entity, tuple, pfield.getRoleType());
    }

    TopicIF topic = Utils.getTopic(pfield.getPlayer(), ctx);
    
    // if player topic is gone, then there won't be any matching associations either
    if (topic == null) {
      return;
    }
    
    outer:
    for (AssociationRoleIF role : topic.getRoles()) {
      // check role type
      if (!Objects.equals(role.getType(), rtype_p)) {
        continue;
      }
      // check association type
      AssociationIF assoc = role.getAssociation();
      if (!Objects.equals(assoc.getType(), atype)) {
        continue;
      }
      // check association cardinality
      Collection roles = assoc.getRoles();
      if (roles.size() != rfields.size()) {
        continue;
      }
      for (AssociationRoleIF arole : roles) {
        if (arole.equals(role)) {
          continue;
        }
        TopicIF rtype = arole.getType();
        TopicIF player = arole.getPlayer();
        // check role
        Field matching_rfield = null;
        for (int i=0; i < rfields.size(); i++) {
          Field rfield = rfields.get(i);
          if (rfield.equals(pfield)) {
            continue;
          }
          TopicIF rtype_o = Utils.getTopic(rfield.getRoleType(), ctx);
          if (rtype_o == null) {
            throw new DB2TMInputException("Role type not found", entity, tuple, rfield.getRoleType());
          }
          // check role type and player
          if (!Objects.equals(rtype, rtype_o) ||
              !Objects.equals(player, Utils.getTopic(rfield.getPlayer(), ctx))) {
            continue;
          }
          // role field matched
          matching_rfield = rfield;
          break;
        }
        if (matching_rfield == null) {
          continue outer;
        }
      }
      // check scope
      if (!compareScope(entity.getScope(), assoc.getScope(), entity, tuple, ctx)) {
        continue;
      }

      // if reifier topic found, then remove it (or its characteristics)
      TopicIF reifier = assoc.getReifier();
      if (reifier != null) {
        removeTopic(reifier, relation, entity, tuple, ctx);
      }
      // remove association
      if (entity.isPrimary()) {
        log.trace("      -A {} {}", assoc, atype);
        assoc.remove();
      } else {
        log.trace("      >A {}", assoc);
      }
      break;
    }    
  }

  /**
   * INTERNAL: Runs a DB2TM process by synchronizing the relations.
   */
  public static void synchronizeRelations(RelationMapping rmapping,
                                          Collection relnames,
                                          TopicMapIF topicmap,
                                          LocatorIF baseloc) {
    synchronizeRelations(rmapping, relnames, topicmap, baseloc, false);
  }
  
  public static void synchronizeRelations(RelationMapping rmapping,
                                          Collection relnames,
                                          TopicMapIF topicmap,
                                          LocatorIF baseloc,
                                          boolean forceRescan) {
    int ttuples = 0;
    long tstime = System.currentTimeMillis();
    Context ctx = new Context();
    if (log.isInfoEnabled()) {
      log.info("Synchronizing relations: {}", new Date());
    }

    try {
      // verify relation mapping
      Map> ds_relations = Utils.verifyRelationsForMapping(rmapping);
      
      // set up context object
      ctx.setMapping(rmapping);
      ctx.setTopicMap(topicmap);
      if (baseloc != null) {
        ctx.setBaseLocator(baseloc);
      } else {
        log.info("No base locator specified, so using base of topic maps store.");
        ctx.setBaseLocator(topicmap.getStore().getBaseAddress());
      }
      
      // loop over datasources
      for (DataSourceIF datasource : ds_relations.keySet()) {
        log.debug("Synchronizing relations in data source: {}", datasource);
      
        // loop over relations
        for (Relation relation : ds_relations.get(datasource)) {
          String relationName = relation.getName();
      
          // do not process non-listed relations
          if (relnames != null && !relnames.contains(relationName)) {
            log.debug("  ignoring relation: {}", relationName);
            continue;
          }
      
          // figure out what the synchronization type is
          int synctype = relation.getSynchronizationType();
      
          if (forceRescan) {
            synctype = Relation.SYNCHRONIZATION_RESCAN;
          }
      
          if (synctype == Relation.SYNCHRONIZATION_UNKNOWN) {
            if (!relation.getSyncs().isEmpty()) {
              synctype = Relation.SYNCHRONIZATION_CHANGELOG;
              log.debug("  defaulting synchronization type for relation {} to {}", relationName, synctype);
            } else {
              synctype = Relation.SYNCHRONIZATION_RESCAN;
              log.debug("  defaulting synchronization type for relation {} to {}", relationName, synctype);
            }
          }
          log.debug("  synchronizing relation: {} type: {} {} force: {}", 
                    new Object[] {relation.getName(), synctype, Relation.getSynchronizationTypeName(synctype), forceRescan});
          
          int rtuples = 0;
          long rstime1 = System.currentTimeMillis();
          long rstime2 = 0;
      
          // set current relation
          ctx.setRelation(relation);
            
          // synchronize relation if configured to do so
          if (synctype == Relation.SYNCHRONIZATION_CHANGELOG) {
            // changelog synchronization
              for (Changelog sync : relation.getSyncs()) {
                log.debug("  changelog, table {}", sync.getTable());
                
                // get start order from topic map
                String startOrder = getStartOrder(sync, ctx);
                String highestOrder = startOrder;
                log.debug("Old order value: {}={}", sync.getTable(), startOrder);
                ChangelogReaderIF reader = datasource.getChangelogReader(sync, startOrder);
                reader = new ChangelogReaderWrapper(reader, relation);
                
                try {
                  String[] tuple;
                  while ((tuple = reader.readNext()) != null) {
                    // process individual tuple
                    long time = System.currentTimeMillis();
                    
                    // track order value
                    String orderValue = reader.getOrderValue();                
                    if (highestOrder == null ||
                        highestOrder.compareTo(orderValue) < 0) {
                      highestOrder = orderValue;
                    }

                    if (reader.getChangeType() == ChangeType.UPDATE) {
                      updateTuple(relation, tuple, ctx);
                    } else {
                      removeTuple(relation, tuple, ctx);
                    }
                    
                    rstime2 += (System.currentTimeMillis()-time);
                    rtuples++;
                  }
                  
                  // update start order
                  log.debug("New order value: {}={}", sync.getTable(), highestOrder);
                  setStartOrder(sync, ctx, highestOrder);
                  
                } finally {
                  reader.close();
                }
              }            
          }
          else if (synctype == Relation.SYNCHRONIZATION_RESCAN) {
            
            // EXPERIMENTAL: load extents
            ctx.loadExtents();
      
            // update start order values if there are changelogs declared
              for (Changelog sync : relation.getSyncs()) {
                String maxOrderValue = datasource.getMaxOrderValue(sync);
                log.debug("New order value: {}={}", sync.getTable(), maxOrderValue);
                setStartOrder(sync, ctx, maxOrderValue);
              }
            
            // full relation rescan
            TupleReaderIF reader = datasource.getReader(relationName);
            
            try {
              log.debug("  full rescan, table {}", relationName);
                
              String [] tuple = null;
              while ((tuple = reader.readNext()) != null) {
                // process individual tuple
                long time = System.currentTimeMillis();
                  
                updateTuple(relation, tuple, ctx);
                  
                rstime2 += (System.currentTimeMillis()-time);
                rtuples++;
              }
            } finally {
              reader.close();
            }
              
            // EXPERIMENTAL: remove untouched extent objects from the topic map
            ctx.removeExtentObjects();
          }
      
          // EXPERIMENTAL: remove expired field values (characteristics)
          ctx.removeOldValues();
          
          log.info("    Synchronized {} tuples for {}, {}/{} ms",
                   new Object[] {rtuples, relationName, (System.currentTimeMillis()-rstime1), rstime2});
          ttuples += rtuples;
        }
      }
    } finally {
      ctx.close();
    }
    if (log.isInfoEnabled()) {
      log.info("done synchronizing relations: {} tuples, {} ms. {}", new Object[] {ttuples, (System.currentTimeMillis()-tstime), new Date()});
    }
  }
  
  /**
   * INTERNAL: Gets the current start order value for the given changelog.
   */
  private static String getStartOrder(Changelog sync, Context ctx) {
    // if there is no reifier then no sync has been made yet
    TopicMapIF topicmap = ctx.getTopicMap();
    TopicIF reifier = topicmap.getReifier();
      
    if (reifier != null) {
      // look up occurrence type
      TopicIF otype = topicmap.getTopicBySubjectIdentifier(LOC_SYNCHRONIZATION_STATE);
      if (otype != null) {
        // create prefix value
        String procname = ctx.getMapping().getName();
        String relname = sync.getRelation().getName();
        String syncname = sync.getTable();
        String prefix = procname + ":" + relname + ":" + syncname + ":";      
        // loop over occurrences to find appropriate value
        for (OccurrenceIF occ : reifier.getOccurrences()) {
          TopicIF otype_ = occ.getType();
          if (otype_ != null && otype_.equals(otype)) {
            String value = occ.getValue();
            if (value != null && value.startsWith(prefix)) {
              return value.substring(prefix.length());
            }
          }
        }
      }
    }
    return null;
  }

  /**
   * INTERNAL: Sets the start order value for the given changelog.
   */
  private static void setStartOrder(Changelog sync, Context ctx, String startOrder) {
    if (startOrder != null) {
      // if there is no reifier then no sync has been made yet
      TopicMapIF topicmap = ctx.getTopicMap();
      TopicIF reifier = topicmap.getReifier();
      // look up occurrence type
      TopicIF otype = topicmap.getTopicBySubjectIdentifier(LOC_SYNCHRONIZATION_STATE);
      
      // create prefix value
      String procname = ctx.getMapping().getName();
      String relname = sync.getRelation().getName();
      String syncname = sync.getTable();
      String prefix = procname + ":" + relname + ":" + syncname + ":";      
      OccurrenceIF match = null;
      
      if (reifier != null && otype != null) {
        // loop over occurrences to find appropriate value
        for (OccurrenceIF occ : reifier.getOccurrences()) {
          TopicIF otype_ = occ.getType();
          if (otype_ != null && otype_.equals(otype)) {
            String value = occ.getValue();
            if (value != null && value.startsWith(prefix)) {
              match = occ;
              break;
            }
          }
        }
      }
      
      String matchValue = prefix + startOrder;
      if (match == null) {
        if (reifier == null) {
          // create reifier
          reifier = ctx.getBuilder().makeTopic();
          topicmap.setReifier(reifier);
        }
        if (otype == null) {
          // create occurrence type
          otype = ctx.getBuilder().makeTopic();
          otype.addSubjectIdentifier(LOC_SYNCHRONIZATION_STATE);
          ctx.getBuilder().makeTopicName(otype, "DB2TM synchronization state");
        }
        // create new occurrence
        match = ctx.getBuilder().makeOccurrence(reifier, otype, matchValue); 
      } else {
        // update value
        match.setValue(matchValue);
      }
    }
  }
  
  private static void updateTuple(Relation relation, String[] tuple, Context ctx) {

    if (log.isDebugEnabled()) {
      log.debug("    u({}),{}", StringUtils.join(tuple, "|"), tuple.length);
    }
    
    List entities = relation.getEntities();
    for (int i=0; i < entities.size(); i++) {
      Entity entity = entities.get(i);
      try {
        Object o = updateEntity(relation, entity, tuple, ctx);
        ctx.setEntityObject(i, o);
      } catch (Exception e) {
        throw new DB2TMException("Error occurred while updating tuple " + Arrays.asList(tuple) + " from relation " + relation.getName() + " to entity " + entity, e);
      }
    }

  }

  private static Object updateEntity(Relation relation, Entity entity, String[] tuple, Context ctx) {
    // 1. create entity if it does not exist
    // 2. synchronize characteristics

    TopicIF topic = null;
    if (entity.requiresTopic()) {
      // find candidate topic
      topic = addIdentities(topic, relation, entity, tuple, ctx);

      // FIXME: if we track updated objects can we then avoid loading
      // full extents?
      
      if (topic != null) {
        // this is an existing object, so we need to track it
        boolean firstTimeSeen = ctx.registerOldObject(topic);

        // if this is the first time we see this object then track the
        // existing values of relevant fields. once we've done that we
        // can add new characteristics. it will also allow us to reuse
        // any of those values. note that this tracking will only
        // happen once per object per relation.
        List cfields = entity.getCharacteristicFields();
        if (firstTimeSeen) {
          List[] existingValues = new List[cfields.size()];
          for (int i=0; i < cfields.size(); i++) {
            Field field = cfields.get(i);
            switch (field.getFieldType()) {
            case Field.TYPE_TOPIC_NAME:
              existingValues[i] = getTopicNames(topic, relation, entity, field, tuple, ctx);
              break;
            case Field.TYPE_OCCURRENCE:
              existingValues[i] = getOccurrences(topic, relation, entity, field, tuple, ctx);
              break;
            case Field.TYPE_PLAYER:
              existingValues[i] = getPlayers(topic, relation, entity, field, tuple, ctx);
              break;
            default:
              throw new DB2TMConfigException("Illegal characteristic field type: " + field);
            }
          }
          ctx.registerOldFieldValues(topic, existingValues);
        }        

        // ISSUE: should we clear the types and identities here as well?
        
        // update identities
        topic = updateIdentities(topic, relation, entity, tuple, ctx);
        
        // update topic types if primary
        if (entity.getEntityType() == Entity.TYPE_TOPIC) {
          // NOTE: association reifiers cannot have types
          if (entity.isPrimary()) {
            updateTypes(topic, entity.getTypes(), entity, tuple, ctx);
          }
        }

        // update characteristics
        for (int i=0; i < cfields.size(); i++) {
          Field field = cfields.get(i);
          
          switch (field.getFieldType()) {
          case Field.TYPE_TOPIC_NAME:
            addTopicName(topic, relation, entity, field, i, tuple, ctx);
            break;
          case Field.TYPE_OCCURRENCE:
            addOccurrence(topic, relation, entity, field, i, tuple, ctx);
            break;
          case Field.TYPE_PLAYER:
            addPlayer(topic, relation, entity, field, i, tuple, ctx);
            break;
          default:
            throw new DB2TMConfigException("Illegal characteristic field type: " + field);
          }
        }
        
      }
      // create topic entity if it does not exist
      else if (entity.getEntityType() == Entity.TYPE_TOPIC) {
        return addEntity(relation, entity, tuple, ctx);
      }
      
      // if association entity, we'll have to wait a little      
    }
    
    if (entity.getEntityType() == Entity.TYPE_ASSOCIATION) {
      // create association
      return addAssociation(topic, relation, entity, tuple, ctx);
    } else {
      return topic;
    }
  }
  
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy