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

org.plasma.sdo.jdbc.service.GraphAssembler Maven / Gradle / Ivy

There is a newer version: 2.2.1
Show newest version
/**
 *         PlasmaSDO™ License
 * 
 * This is a community release of PlasmaSDO™, a dual-license 
 * Service Data Object (SDO) 2.1 implementation. 
 * This particular copy of the software is released under the 
 * version 2 of the GNU General Public License. PlasmaSDO™ was developed by 
 * TerraMeta Software, Inc.
 * 
 * Copyright (c) 2013, TerraMeta Software, Inc. All rights reserved.
 * 
 * General License information can be found below.
 * 
 * This distribution may include materials developed by third
 * parties. For license and attribution notices for these
 * materials, please refer to the documentation that accompanies
 * this distribution (see the "Licenses for Third-Party Components"
 * appendix) or view the online documentation at 
 * .
 *  
 */
package org.plasma.sdo.jdbc.service;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.plasma.query.collector.PropertySelectionCollector;
import org.plasma.query.model.Where;
import org.plasma.sdo.PlasmaDataGraph;
import org.plasma.sdo.PlasmaDataObject;
import org.plasma.sdo.PlasmaProperty;
import org.plasma.sdo.PlasmaType;
import org.plasma.sdo.access.DataAccessException;
import org.plasma.sdo.access.DataGraphAssembler;
import org.plasma.sdo.access.provider.common.PropertyPair;
import org.plasma.sdo.access.provider.jdbc.AliasMap;
import org.plasma.sdo.core.CoreConstants;
import org.plasma.sdo.core.CoreNode;
import org.plasma.sdo.core.TraversalDirection;
import org.plasma.sdo.helper.PlasmaDataFactory;
import org.plasma.sdo.jdbc.filter.FilterAssembler;
import org.plasma.sdo.profile.KeyType;

import commonj.sdo.DataGraph;
import commonj.sdo.DataObject;
import commonj.sdo.Property;
import commonj.sdo.Type;

public class GraphAssembler extends JDBCSupport implements DataGraphAssembler {

  private static Log log = LogFactory.getLog(GraphAssembler.class);
  private PlasmaType rootType;
  private PlasmaDataObject root;
  private Map> propertyMap;
  private Map predicateMap;
  private Timestamp snapshotDate;
  private Connection con;
  private RDBDataConverter converter;
  private Map dataObjectMap = new HashMap();
  private Comparator nameComparator;

  @SuppressWarnings("unused")
  private GraphAssembler() {
  }

  public GraphAssembler(PlasmaType rootType, PropertySelectionCollector collector,
      Timestamp snapshotDate, Connection con) {
    this.rootType = rootType;
    this.propertyMap = collector.getResult();
    this.predicateMap = collector.getPredicateMap();
    this.snapshotDate = snapshotDate;
    this.con = con;
    this.converter = RDBDataConverter.INSTANCE;

    this.nameComparator = new Comparator() {
      public int compare(PropertyPair o1, PropertyPair o2) {
        return o1.getProp().getName().compareTo(o2.getProp().getName());
      }
    };
  }

  /**
   * Initiates the assembly of a data graph based on the given results list.
   * 
   * @param results
   *          the results list
   * 
   * @see DataGraphAssembler.getDataGraph()
   */
  public void assemble(List results) {

    DataGraph dataGraph = PlasmaDataFactory.INSTANCE.createDataGraph();
    this.root = (PlasmaDataObject) dataGraph.createRootObject(this.rootType);
    if (log.isDebugEnabled())
      log.debug("assembling root: " + this.root.getType().getName());

    CoreNode rootNode = (CoreNode) this.root;
    // add concurrency fields
    if (snapshotDate != null)
      rootNode.setValue(CoreConstants.PROPERTY_NAME_SNAPSHOT_TIMESTAMP, snapshotDate);
    // set data properties
    for (PropertyPair pair : results) {
      if (pair.getProp().getType().isDataType()) {
        rootNode.setValue(pair.getProp().getName(), pair.getValue());
      }
    }

    // singular reference props
    for (PropertyPair pair : results) {
      if (pair.getProp().isMany() || pair.getProp().getType().isDataType())
        continue;
      List childKeyProps = new ArrayList();
      List childPkProps = ((PlasmaType) pair.getProp().getType())
          .findProperties(KeyType.primary);
      if (childPkProps.size() == 1) {
        childKeyProps.add(new PropertyPair((PlasmaProperty) childPkProps.get(0), pair.getValue()));
      } else
        throwPriKeyError(childPkProps, pair.getProp().getType(), pair.getProp());
      assemble((PlasmaType) pair.getProp().getType(), (PlasmaDataObject) this.root, pair.getProp(),
          childKeyProps);

    }

    // multi reference props (not found in results)
    List names = this.propertyMap.get(this.rootType);
    for (String name : names) {
      PlasmaProperty prop = (PlasmaProperty) rootType.getProperty(name);
      if (prop.isMany() && !prop.getType().isDataType()) {
        PlasmaProperty opposite = (PlasmaProperty) prop.getOpposite();
        if (opposite == null)
          throw new DataAccessException("no opposite property found"
              + " - cannot map from many property, " + prop.getType() + "." + prop.getName());
        List childKeyProps = new ArrayList();
        List rootPkProps = ((PlasmaType) root.getType()).findProperties(KeyType.primary);
        if (rootPkProps.size() == 1) {
          childKeyProps.add(new PropertyPair(opposite, root.get(rootPkProps.get(0))));
        } else
          throwPriKeyError(rootPkProps, root.getType(), prop);
        assemble((PlasmaType) prop.getType(), (PlasmaDataObject) this.root, prop, childKeyProps);
      }
    }
  }

  /**
   * 
   * @param targetType
   * @param source
   * @param sourceProperty
   * @param childKeyPairs
   */
  private void assemble(PlasmaType targetType, PlasmaDataObject source,
      PlasmaProperty sourceProperty, List childKeyPairs) {
    List names = this.propertyMap.get(targetType);
    if (log.isDebugEnabled())
      log.debug("assemble: " + source.getType().getName() + "." + sourceProperty.getName() + ": "
          + names);

    List> result = null;
    Where where = this.predicateMap.get(sourceProperty);
    if (where == null) {
      try {
        StringBuilder query = createSelect(targetType, names, childKeyPairs);
        result = fetch(targetType, query, this.con);
      } catch (SQLException e) {
        throw new DataAccessException(e);
      }
    } else {
      AliasMap aliasMap = new AliasMap(targetType);
      FilterAssembler filterAssembler = new FilterAssembler(where, targetType, aliasMap);
      try {
        List params = new ArrayList();
        StringBuilder query = createSelect(targetType, names, childKeyPairs, filterAssembler,
            params, aliasMap);
        Object[] paramArray = new Object[params.size()];
        params.toArray(paramArray);

        result = fetch(targetType, query, paramArray, this.con);
      } catch (SQLException e) {
        throw new DataAccessException(e);
      }
    }

    if (log.isDebugEnabled())
      log.debug("results: " + result.size());

    // first create (or link existing) data objects
    // "filling out" the containment hierarchy at this traversal level
    // BEFORE recursing, as we may "cancel" out an object
    // at the current level if it is first encountered
    // within the recursion.
    Map> resultMap = new HashMap>();
    for (List row : result) {

      PlasmaDataObject target = findDataObject(targetType, row);
      // if no existing data-object in graph
      if (target == null) {
        target = createDataObject(row, source, sourceProperty);
        resultMap.put(target, row);
      } else {
        link(target, source, sourceProperty);
        continue;
        // Assume we traverse no farther given no traversal
        // direction or containment info. We only know that we
        // encountered an existing node. Need more path specific
        // info including containment and traversal direction to construct
        // a directed graph here.
        // Since the current selection collector maps any and all
        // properties selected to a type, for each type/data-object
        // we will, at this point, have gotten all the properties we expect
        // anyway.
        // So we create a link from the source to the existing DO, but
        // traverse no further.
      }
    }

    // now traverse
    Iterator iter = resultMap.keySet().iterator();
    while (iter.hasNext()) {
      PlasmaDataObject target = iter.next();
      List row = resultMap.get(target);
      // traverse singular results props
      for (PropertyPair pair : row) {
        if (pair.getProp().isMany() || pair.getProp().getType().isDataType())
          continue; // only singular reference props
        List nextKeyPairs = new ArrayList();
        List nextKeyProps = ((PlasmaType) pair.getProp().getType())
            .findProperties(KeyType.primary);

        // FIXME: need UML profile link to target PK props
        // where there are multiple PKs !!
        if (nextKeyProps.size() == 1) {
          nextKeyPairs.add(new PropertyPair((PlasmaProperty) nextKeyProps.get(0), pair.getValue()));
        } else
          throwPriKeyError(nextKeyProps, pair.getProp().getType(), pair.getProp());

        if (log.isDebugEnabled())
          log.debug("traverse: (" + pair.getProp().isMany() + ") "
              + pair.getProp().getType().getName() + "." + pair.getProp().getName());
        assemble((PlasmaType) pair.getProp().getType(), target, pair.getProp(), nextKeyPairs);
      }

      // traverse multi props based, not on the results
      // row, but on keys within this data object
      for (String name : names) {
        PlasmaProperty prop = (PlasmaProperty) targetType.getProperty(name);
        if (!prop.isMany() || prop.getType().isDataType())
          continue; // only many reference props

        PlasmaProperty opposite = (PlasmaProperty) prop.getOpposite();
        if (opposite == null)
          throw new DataAccessException("no opposite property found"
              + " - cannot map from many property, " + prop.getType() + "." + prop.getName());
        List childKeyProps = new ArrayList();
        List nextKeyProps = ((PlasmaType) targetType).findProperties(KeyType.primary);
        if (nextKeyProps.size() == 1) {
          childKeyProps.add(new PropertyPair(opposite, target.get(nextKeyProps.get(0))));
        } else
          throwPriKeyError(nextKeyProps, targetType, prop);
        if (log.isDebugEnabled())
          log.debug("traverse: (" + prop.isMany() + ") " + target.getType().getName() + "."
              + prop.getName());
        assemble((PlasmaType) prop.getType(), target, prop, childKeyProps);
      }
    }
  }

  /**
   * Creates a new data object contained by the given source data object and
   * source property.
   * 
   * @param row
   *          the results row
   * @param source
   *          the source data object
   * @param sourceProperty
   *          the source containment property
   * @return the new data object
   */
  private PlasmaDataObject createDataObject(List row, PlasmaDataObject source,
      PlasmaProperty sourceProperty) {
    PlasmaDataObject target = (PlasmaDataObject) source.createDataObject(sourceProperty);
    if (log.isDebugEnabled())
      log.debug("create: " + source.getType().getName() + "." + sourceProperty.getName() + "->"
          + target.getType().getName());
    CoreNode node = (CoreNode) target;

    // add concurrency fields
    if (snapshotDate != null)
      node.setValue(CoreConstants.PROPERTY_NAME_SNAPSHOT_TIMESTAMP, snapshotDate);

    // set data properties bypassing SDO "setter" API
    // to avoid triggering read-only property error
    for (PropertyPair pair : row) {
      if (pair.getProp().getType().isDataType()) {
        if (log.isDebugEnabled())
          log.debug("set: (" + pair.getValue() + ") "
              + pair.getProp().getContainingType().getName() + "." + pair.getProp().getName());
        node.setValue(pair.getProp().getName(), pair.getValue());
      }
    }

    // map it
    int key = createHashKey((PlasmaType) target.getType(), row);
    if (log.isDebugEnabled())
      log.debug("mapping " + key + "->" + target);
    this.dataObjectMap.put(key, target);

    return target;
  }

  /**
   * Finds and returns an existing data object based on hte given results row
   * which is part if this assembly unit, or returns null if not exists
   * 
   * @param type
   *          the target type
   * @param row
   *          the results row
   * @return the data object
   */
  private PlasmaDataObject findDataObject(PlasmaType type, List row) {
    int key = createHashKey(type, row);
    PlasmaDataObject result = this.dataObjectMap.get(key);
    if (log.isDebugEnabled()) {
      if (result != null)
        log.debug("found existing mapping " + key + "->" + result);
      else
        log.debug("found no existing mapping for key: " + key);
    }
    return result;
  }

  /**
   * Creates a unique mappable key using the qualified type name and all key
   * property values from the given row.
   * 
   * @param type
   *          the type
   * @param row
   *          the data values
   * @return the key
   */
  private int createHashKey(PlasmaType type, List row) {
    PropertyPair[] pairs = new PropertyPair[row.size()];
    row.toArray(pairs);
    Arrays.sort(pairs, this.nameComparator);
    int result = type.getQualifiedName().hashCode();

    int pks = 0;
    for (int i = 0; i < pairs.length; i++) {
      if (pairs[i].getProp().isKey(KeyType.primary)) {
        Object value = pairs[i].getValue();
        result = result ^ value.hashCode();
        pks++;
      }
    }
    if (pks == 0)
      throw new IllegalStateException("cannot create hash key - no primary keys found for type, "
          + type.toString());
    return result;
  }

  /**
   * Creates a directed (link) between the given source and target data objects.
   * The reference is created as a containment reference only if the given
   * target has no container.
   * 
   * @param target
   *          the data object which is the target
   * @param source
   *          the source data object
   * @param sourceProperty
   *          the source property
   * 
   * @see TraversalDirection
   */
  private void link(PlasmaDataObject target, PlasmaDataObject source, PlasmaProperty sourceProperty) {
    if (log.isDebugEnabled())
      log.debug("linking source (" + source.getUUIDAsString() + ") " + source.getType().getURI()
          + "#" + source.getType().getName() + "." + sourceProperty.getName() + "->("
          + target.getUUIDAsString() + ") " + target.getType().getURI() + "#"
          + target.getType().getName());

    if (sourceProperty.isMany()) {

      PlasmaProperty opposite = (PlasmaProperty) sourceProperty.getOpposite();
      if (opposite != null && !opposite.isMany() && target.isSet(opposite)) {
        PlasmaDataObject existingOpposite = (PlasmaDataObject) target.get(opposite);
        if (existingOpposite != null) {
          if (log.isDebugEnabled())
            log.debug("encountered existing opposite (" + existingOpposite.getType().getName()
                + ") value found while creating link (" + source.getUUIDAsString() + ") "
                + source.getType().getURI() + "#" + source.getType().getName() + "."
                + sourceProperty.getName() + "->(" + target.getUUIDAsString() + ") "
                + target.getType().getURI() + "#" + target.getType().getName()
                + " - no link created");
          return;
        }
      }
      @SuppressWarnings("unchecked")
      List list = source.getList(sourceProperty);
      if (list == null)
        list = new ArrayList();
      if (log.isDebugEnabled()) {
        for (DataObject existingObject : list) {
          log.debug("existing: (" + ((org.plasma.sdo.PlasmaNode) existingObject).getUUIDAsString()
              + ") " + existingObject.getType().getURI() + "#" + existingObject.getType().getName());
        }
      }
      if (!list.contains(target)) {
        if (log.isDebugEnabled())
          log.debug("adding target  (" + source.getUUIDAsString() + ") "
              + source.getType().getURI() + "#" + source.getType().getName() + "."
              + sourceProperty.getName() + "->(" + target.getUUIDAsString() + ") "
              + target.getType().getURI() + "#" + target.getType().getName());
        if (target.getContainer() == null) {
          target.setContainer(source);
          target.setContainmentProperty(sourceProperty);
        }
        list.add(target);
        source.setList(sourceProperty, list);
        // FIXME: replaces existing list according to SDO spec (memory churn)
        // store some temp instance-property list on DO and only set using SDO
        // API on completion of graph.
      }
    } else {
      // Selection map keys are paths from the root entity and
      // elements in the path are often repeated. Expect repeated
      // events for repeated path elements, which
      // may be useful for some implementations, but not this one. So
      // we screen these out here.
      PlasmaDataObject existing = (PlasmaDataObject) source.get(sourceProperty);
      if (existing == null) {
        source.set(sourceProperty, target);
        // While the SDO spec seems to indicate (see 3.1.6 Containment) that
        // a Type may have only 1 reference property which a containment
        // property, this seems too inflexible given the almost infinite
        // number of ways a graph could be constructed. We therefore allow any
        // reference
        // property to be a containment property, and let the graph assembly
        // order determine which properties are containment properties for a
        // particular
        // graph result. The SDO spec is crystal clear that every Data Object
        // other than the root, must have one-and-only-one container. We set the
        // container
        // here as well as the specific reference property that currently is
        // the containment property, based on graph traversal order. Note it
        // would be
        // possible to specify exactly which property is containment in a
        // query specification. We set no indication of containment on the
        // (source) container object because all reference properties are
        // potentially containment properties.
        if (target.getContainer() == null) {
          target.setContainer(source);
          target.setContainmentProperty(sourceProperty);
        }
      } else if (!existing.getUUIDAsString().equals(target.getUUIDAsString()))
        if (log.isDebugEnabled())
          log.debug("encountered existing (" + existing.getType().getName()
              + ") value found while creating link (" + source.getUUIDAsString() + ") "
              + source.getType().getURI() + "#" + source.getType().getName() + "."
              + sourceProperty.getName() + "->(" + target.getUUIDAsString() + ") "
              + target.getType().getURI() + "#" + target.getType().getName());
    }
  }

  private void throwPriKeyError(List rootPkProps, Type type, Property prop) {
    if (prop.isMany())
      if (rootPkProps.size() == 0)
        throw new DataAccessException("no pri-keys found for " + type.getURI() + "#"
            + type.getName() + " - cannot map from many property, " + prop.getType() + "."
            + prop.getName());
      else
        throw new DataAccessException("multiple pri-keys found for " + type.getURI() + "#"
            + type.getName() + " - cannot map from many property, " + prop.getType() + "."
            + prop.getName());
    else if (rootPkProps.size() == 0)
      throw new DataAccessException("no pri-keys found for " + type.getURI() + "#" + type.getName()
          + " - cannot map from singular property, " + prop.getType() + "." + prop.getName());
    else
      throw new DataAccessException("multiple pri-keys found for " + type.getURI() + "#"
          + type.getName() + " - cannot map from singular property, " + prop.getType() + "."
          + prop.getName());
  }

  public PlasmaDataGraph getDataGraph() {
    return (PlasmaDataGraph) this.root.getDataGraph();
  }

  public void clear() {
    this.root = null;
    this.dataObjectMap.clear();
  }

}