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

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

/**
 *         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();
	}
	
}