/** * CloudGraph Community Edition (CE) License * * This is a community release of CloudGraph, a dual-license suite of * Service Data Object (SDO) 2.1 services designed for relational and * big-table style "cloud" databases, such as HBase and others. * This particular copy of the software is released under the * version 2 of the GNU General Public License. CloudGraph 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 * <http://cloudgraph.org/licenses/>. */ package org.cloudgraph.hbase.mutation; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.UUID; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.util.Bytes; import org.cloudgraph.config.CloudGraphConfig; import org.cloudgraph.config.DataGraphConfig; import org.cloudgraph.config.TableConfig; import org.cloudgraph.config.UserDefinedRowKeyFieldConfig; import org.cloudgraph.hbase.io.DistributedWriter; import org.cloudgraph.hbase.io.RowWriter; import org.cloudgraph.hbase.io.TableWriter; import org.cloudgraph.hbase.service.HBaseDataConverter; import org.cloudgraph.hbase.service.ServiceContext; import org.cloudgraph.state.GraphState.Edge; import org.cloudgraph.store.service.GraphServiceException; import org.plasma.sdo.DataFlavor; import org.plasma.sdo.PlasmaDataObject; import org.plasma.sdo.PlasmaEdge; import org.plasma.sdo.PlasmaNode; import org.plasma.sdo.PlasmaProperty; import org.plasma.sdo.PlasmaType; import org.plasma.sdo.access.DataAccessException; import org.plasma.sdo.access.RequiredPropertyException; import org.plasma.sdo.access.provider.common.PropertyPair; import org.plasma.sdo.core.CoreConstants; import org.plasma.sdo.core.CoreDataObject; import org.plasma.sdo.core.NullValue; import org.plasma.sdo.core.SnapshotMap; import org.plasma.sdo.helper.DataConverter; import org.plasma.sdo.profile.ConcurrencyType; import org.plasma.sdo.profile.ConcurrentDataFlavor; import org.plasma.sdo.profile.KeyType; import commonj.sdo.ChangeSummary; import commonj.sdo.DataGraph; import commonj.sdo.DataObject; import commonj.sdo.Property; abstract class DefaultMutation { private static Log log = LogFactory.getLog(DefaultMutation.class); private static HashMap<String, DataObject> EMPTY_EDGE_MAP = new HashMap<String, DataObject>(); protected SnapshotMap snapshotMap; protected String username; protected ServiceContext context; public DefaultMutation(ServiceContext context, SnapshotMap snapshotMap, String username) { super(); this.snapshotMap = snapshotMap; this.username = username; this.context = context; } protected HashMap<String, DataObject> getOldEdgeMap(Object oldValue, Property property) { HashMap<String, DataObject> result = EMPTY_EDGE_MAP; if (!(oldValue instanceof NullValue)) { if (oldValue instanceof List) { @SuppressWarnings("unchecked") List<DataObject> oldValueList = (List<DataObject>)oldValue; result = new HashMap<String, DataObject>(oldValueList.size()); for (DataObject dataObject : oldValueList) result.put(((PlasmaDataObject)dataObject).getUUIDAsString(), dataObject); } else { PlasmaDataObject oldOpposite = (PlasmaDataObject)oldValue; result = new HashMap<String, DataObject>(1); result.put(oldOpposite.getUUIDAsString(), oldOpposite); } } return result; } protected byte[] createEdgeValueBytes(PlasmaNode dataNode, List<PlasmaEdge> edges, RowWriter rowWriter) throws IOException { String valueStr = rowWriter.getGraphState().marshalEdges(dataNode, edges); return Bytes.toBytes(valueStr); } protected void addRowKeys( PlasmaDataObject dataObject, PlasmaNode dataNode, Property property, List <PlasmaEdge> edges, DistributedWriter graphWriter, TableWriter tableWriter, RowWriter rowWriter) throws IOException { PlasmaType dataObjectType = (PlasmaType)dataObject.getType(); boolean thisTypeBound = CloudGraphConfig.getInstance().findTable( dataObjectType.getQualifiedName()) != null; for (PlasmaEdge edge : edges) { PlasmaDataObject opposite = edge.getOpposite(dataNode).getDataObject(); //edge.getDirection() PlasmaType oppositeType = (PlasmaType)opposite.getType(); boolean oppositeTypeBound = CloudGraphConfig.getInstance().findTable( oppositeType.getQualifiedName()) != null; RowWriter oppositeRowWriter = graphWriter.findRowWriter(opposite); if (oppositeRowWriter == null) { oppositeRowWriter = graphWriter.createRowWriter(opposite); } TableWriter oppositeTableWriter = oppositeRowWriter.getTableWriter(); // maps opposite UUID to its row key // in the state for this row if (oppositeTypeBound) { rowWriter.getGraphState().addRowKey(opposite, oppositeTableWriter.getTableConfig(), oppositeRowWriter.getRowKey()); if (log.isDebugEnabled()) log.debug("adding row key for opposite data object ("+opposite+") to this data-object state, " + dataObject); } else { if (log.isDebugEnabled()) log.debug("ignoring row key for unbound opposite data object, " + opposite.toString()); } // Maps this DO uuid to current row key in opposite row // If this data object is not "bound" to a // table, disregard as it will have no opposite row // but will be contained within this row Property oppositeProperty = property.getOpposite(); if (oppositeProperty != null && thisTypeBound) { ChangeSummary changeSummary = dataObject.getChangeSummary(); if (changeSummary.isCreated(opposite) || changeSummary.isModified(opposite)) { oppositeRowWriter.getGraphState().addRowKey(dataObject, tableWriter.getTableConfig(), rowWriter.getRowKey()); if (log.isDebugEnabled()) log.debug("adding row key for this data object ("+dataObject+") to opposite data-object state, " + opposite + " for property, " + property); } // Otherwise we are just adding to the state for a DO who's ref property // will never get updated // And is this call even necessary as if the opposite's property IS modified // it will get updated above. } else { if (log.isDebugEnabled()) { if (oppositeProperty == null) log.debug("ignoring row key for opposite data-object state for property, " + property.toString() + " as no opposite property exists"); else log.debug("ignoring row key for opposite data-object state for property, " + property.toString() + " as this type is not bound"); } } } } /** * Screens out edges that are not owned by the given data node, but * still may be present. */ protected List <PlasmaEdge> findUpdateEdges( PlasmaNode dataNode, Property property, List <PlasmaEdge> edges, DistributedWriter graphWriter, RowWriter rowWriter) throws IOException { List <PlasmaEdge> result = new ArrayList<PlasmaEdge>(); for (PlasmaEdge edge : edges) { PlasmaDataObject opposite = edge.getOpposite(dataNode).getDataObject(); PlasmaType oppositeType = (PlasmaType)opposite.getType(); boolean oppositeTypeBound = CloudGraphConfig.getInstance().findTable( oppositeType.getQualifiedName()) != null; RowWriter oppositeRowWriter = graphWriter.findRowWriter(opposite); if (oppositeRowWriter == null) { oppositeRowWriter = graphWriter.createRowWriter(opposite); } // If the opposite not bound to a table and // it is already linked within another row, // don't write the edge. This graph does not // own it. if (oppositeTypeBound || oppositeRowWriter.equals(rowWriter)) { result.add(edge); } else { if (log.isDebugEnabled()) log.debug("ignoring non-owned edge for property, " + property.toString()); } } return result; } protected void updateCell(RowWriter rowContext, PlasmaDataObject dataObject, Property property, byte[] value) throws IOException { PlasmaProperty prop = (PlasmaProperty) property; byte[] qualifier = rowContext.getColumnKeyFactory().createColumnKey(dataObject, prop); TableConfig table = rowContext.getTableWriter().getTableConfig(); if (log.isDebugEnabled()) { if (prop.getType().isDataType()) { Object objectValue = HBaseDataConverter.INSTANCE.fromBytes(prop, value); log.debug("setting " + property + " / " + table.getName() + "." + new String(qualifier, table.getCharset()) + " = '" + String.valueOf(objectValue) + "'"); } else { log.debug("setting " + property + " / " + table.getName() + "." + new String(qualifier, table.getCharset()) + " = '" + new String(value, table.getCharset()) + "'"); } } rowContext.getRow().add(table.getDataColumnFamilyNameBytes(), qualifier, value); } /** * Removes all versions of the associated cell. * @param rowContext row writer * @param dataObject the data object * @param property * @throws IOException */ protected void deleteCell(RowWriter rowContext, PlasmaDataObject dataObject, Property property) throws IOException { PlasmaProperty prop = (PlasmaProperty) property; byte[] qualifier = rowContext.getColumnKeyFactory().createColumnKey(dataObject, prop); TableConfig table = rowContext.getTableWriter().getTableConfig(); if (log.isDebugEnabled()) log.debug("deleting " + property + " / " + table.getName() + "." + new String(qualifier, table.getCharset())); rowContext.getRowDelete().addColumns(table.getDataColumnFamilyNameBytes(), qualifier); } protected void setOrigination(PlasmaDataObject dataObject, PlasmaType type) { // FIXME - could be a reference to a user Property originationUserProperty = type.findProperty(ConcurrencyType.origination, ConcurrentDataFlavor.user); if (originationUserProperty != null) { if (!originationUserProperty.isReadOnly()) dataObject.set(originationUserProperty, username); else ((CoreDataObject) dataObject).setValue(originationUserProperty.getName(), username); // FIXME: // bypassing // readonly // modification // detection } else if (log.isDebugEnabled()) log.debug( "could not find origination (username) property for type, " + type.getURI() + "#" + type.getName()); Property originationTimestampProperty = type.findProperty(ConcurrencyType.origination, ConcurrentDataFlavor.time); if (originationTimestampProperty != null) { Date dateSnapshot = new Date(this.snapshotMap.getSnapshotDate().getTime()); Object snapshot = DataConverter.INSTANCE.convert(originationTimestampProperty.getType(), dateSnapshot); if (!originationTimestampProperty.isReadOnly()) dataObject.set(originationTimestampProperty, snapshot); else ((CoreDataObject) dataObject).setValue(originationTimestampProperty.getName(), snapshot); // FIXME: // bypassing // readonly // modification // detection } else if (log.isDebugEnabled()) log.debug("could not find origination date property for type, " + type.getURI() + "#" + type.getName()); } protected void updateKeys(PlasmaDataObject dataObject, PlasmaType type, RowWriter rowWriter) throws IOException { UUID uuid = ((CoreDataObject) dataObject).getUUID(); if (uuid == null) throw new GraphServiceException("expected UUID for created entity '" + type.getName() + "'"); List<Property> pkList = type.findProperties(KeyType.primary); if (pkList == null || pkList.size() == 0) return; // don't care for NOSQL services for (Property pkp : pkList) { if (!pkp.getType().isDataType()) continue; // noop for non-data pks PlasmaProperty targetPriKeyProperty = (PlasmaProperty) pkp; Object pk = dataObject.get(targetPriKeyProperty); if (pk == null) { DataFlavor dataFlavor = targetPriKeyProperty.getDataFlavor(); switch (dataFlavor) { case integral: if (log.isDebugEnabled()) { log.debug("getting seq-num for " + type.getName()); } pk = rowWriter.getGraphState().findSequence(dataObject); if (pk == null) pk = rowWriter.getGraphState().addSequence(dataObject); pk = DataConverter.INSTANCE.convert(targetPriKeyProperty.getType(), pk); byte[] pkBytes = HBaseDataConverter.INSTANCE.toBytes(targetPriKeyProperty, pk); this.updateCell(rowWriter, dataObject, targetPriKeyProperty, pkBytes); ((CoreDataObject) dataObject).setValue(targetPriKeyProperty.getName(), pk); // FIXME: // bypassing // modification // detection // on // pri-key break; default: throw new DataAccessException("found null primary key property '" + targetPriKeyProperty.getName() + "' for type, " + type.getURI() + "#" + type.getName()); } } else { byte[] pkBytes = HBaseDataConverter.INSTANCE.toBytes(targetPriKeyProperty, pk); this.updateCell(rowWriter, dataObject, targetPriKeyProperty, pkBytes); } if (log.isDebugEnabled()) { log.debug("mapping UUID '" + uuid + "' to pk (" + String.valueOf(pk) + ")"); } snapshotMap.put(uuid, new PropertyPair(targetPriKeyProperty, pk)); } } protected void updateOrigination(PlasmaDataObject dataObject, PlasmaType type, RowWriter rowContext) throws IOException { // FIXME - could be a reference to a user Property originationUserProperty = type.findProperty(ConcurrencyType.origination, ConcurrentDataFlavor.user); if (originationUserProperty != null) { if (!originationUserProperty.isReadOnly()) dataObject.set(originationUserProperty, username); else ((CoreDataObject) dataObject).setValue(originationUserProperty.getName(), username); // FIXME: // bypassing // readonly // modification // detection } else if (log.isDebugEnabled()) log.debug( "could not find origination (username) property for type, " + type.getURI() + "#" + type.getName()); Property originationTimestampProperty = type.findProperty(ConcurrencyType.origination, ConcurrentDataFlavor.time); if (originationTimestampProperty != null) { Date dateSnapshot = new Date(this.snapshotMap.getSnapshotDate().getTime()); Object snapshot = DataConverter.INSTANCE.convert(originationTimestampProperty.getType(), dateSnapshot); byte[] bytes = HBaseDataConverter.INSTANCE.toBytes(originationTimestampProperty, snapshot); this.updateCell(rowContext, dataObject, originationTimestampProperty, bytes); } else if (log.isDebugEnabled()) log.debug("could not find origination date property for type, " + type + "#" + type.getName()); } protected void setOptimistic(PlasmaDataObject dataObject, PlasmaType type, Timestamp snapshotDate) { PlasmaProperty concurrencyUserProperty = (PlasmaProperty) type.findProperty(ConcurrencyType.optimistic, ConcurrentDataFlavor.user); if (concurrencyUserProperty == null) { if (log.isDebugEnabled()) log.debug("could not find optimistic concurrency (username) property for type, " + type.getURI() + "#" + type.getName()); } else { if (!concurrencyUserProperty.isReadOnly()) dataObject.set(concurrencyUserProperty, username); else ((CoreDataObject) dataObject).setValue(concurrencyUserProperty.getName(), username); } PlasmaProperty concurrencyTimestampProperty = (PlasmaProperty) type.findProperty(ConcurrencyType.optimistic, ConcurrentDataFlavor.time); if (concurrencyTimestampProperty == null) { if (log.isDebugEnabled()) log.debug("could not find optimistic concurrency timestamp property for type, " + type.getURI() + "#" + type.getName()); } else { Date dateSnapshot = new Date(this.snapshotMap.getSnapshotDate().getTime()); Object snapshot = DataConverter.INSTANCE.convert(concurrencyTimestampProperty.getType(), dateSnapshot); if (!concurrencyTimestampProperty.isReadOnly()) dataObject.set(concurrencyTimestampProperty, snapshot); else ((CoreDataObject) dataObject).setValue(concurrencyTimestampProperty.getName(), snapshot); } } // FIXME: need CG configuration related to enabling and handling concurrency // checks protected void updateOptimistic(PlasmaDataObject dataObject, PlasmaType type, RowWriter rowContext) throws IOException { PlasmaProperty concurrencyUserProperty = (PlasmaProperty) type.findProperty(ConcurrencyType.optimistic, ConcurrentDataFlavor.user); if (concurrencyUserProperty == null) { if (log.isDebugEnabled()) log.debug("could not find optimistic concurrency (username) property for type, " + type.getURI() + "#" + type.getName()); } else { byte[] bytes = HBaseDataConverter.INSTANCE.toBytes(concurrencyUserProperty, username); this.updateCell(rowContext, dataObject, concurrencyUserProperty, bytes); } PlasmaProperty concurrencyTimestampProperty = (PlasmaProperty) type.findProperty(ConcurrencyType.optimistic, ConcurrentDataFlavor.time); if (concurrencyTimestampProperty == null) { if (log.isDebugEnabled()) log.debug("could not find optimistic concurrency timestamp property for type, " + type.getURI() + "#" + type.getName()); } else { Date dateSnapshot = new Date(this.snapshotMap.getSnapshotDate().getTime()); Object snapshot = DataConverter.INSTANCE.convert(concurrencyTimestampProperty.getType(), dateSnapshot); byte[] bytes = HBaseDataConverter.INSTANCE.toBytes(concurrencyTimestampProperty, snapshot); this.updateCell(rowContext, dataObject, concurrencyTimestampProperty, bytes); } } protected void validateModifications(DataGraph dataGraph, PlasmaDataObject dataObject, RowWriter rowWriter) throws IllegalAccessException { PlasmaType type = (PlasmaType)dataObject.getType(); if (log.isDebugEnabled()) log.debug("updating " + type.getName() + " '" + ((PlasmaDataObject)dataObject).getUUIDAsString()+ "'"); PlasmaNode dataNode = (PlasmaNode)dataObject; PlasmaType rootType = (PlasmaType)rowWriter.getRootDataObject().getType(); DataGraphConfig dataGraphConfig = CloudGraphConfig.getInstance().getDataGraph(rootType.getQualifiedName()); List<Property> properties = type.getProperties(); for (Property p : properties) { PlasmaProperty property = (PlasmaProperty)p; Object oldValue = dataGraph.getChangeSummary().getOldValue(dataObject, property); if (oldValue == null) continue; // it's not been modified if (property.isReadOnly()) throw new IllegalAccessException("attempt to modify read-only property, " + type.getURI() + "#" + type.getName() + "." + property.getName()); UserDefinedRowKeyFieldConfig userDefinedField = dataGraphConfig.findUserDefinedRowKeyField(property); if (userDefinedField != null) { throw new IllegalAccessException("attempt to modify row-key property, " + type.getURI() + "#" + type.getName() + "." + property.getName() + " - this property is configured as a row-key field for table '" + dataGraphConfig.getTable().getName() + "'"); } //FIXME: what if an entire entity is deleted which is part // of the row key. Detect this. Or added for that matter. } } protected void checkConcurrency(DataGraph dataGraph, PlasmaDataObject dataObject) { PlasmaType type = (PlasmaType)dataObject.getType(); if (dataGraph.getChangeSummary().isCreated(dataObject)) { this.setOrigination(dataObject, type); } else if (dataGraph.getChangeSummary().isModified(dataObject)) { Timestamp snapshotDate = (Timestamp)((CoreDataObject)dataObject).getValue(CoreConstants.PROPERTY_NAME_SNAPSHOT_TIMESTAMP); if (snapshotDate == null) throw new RequiredPropertyException("instance property '" + CoreConstants.PROPERTY_NAME_SNAPSHOT_TIMESTAMP + "' is required to update entity " + dataObject); //FIXME: check optimistic/pessimistic concurrency this.setOptimistic(dataObject, type, snapshotDate); } else if (dataGraph.getChangeSummary().isDeleted(dataObject)) { //FIXME: check optimistic/pessimistic concurrency } } // FIXME: need CG configuration related to enabling and handling concurrency // checks protected void checkOptimistic(PlasmaDataObject dataObject, PlasmaType type, Timestamp snapshotDate) { PlasmaProperty concurrencyUserProperty = (PlasmaProperty) type.findProperty(ConcurrencyType.optimistic, ConcurrentDataFlavor.user); if (concurrencyUserProperty == null) { if (log.isDebugEnabled()) log.debug("could not find optimistic concurrency (username) property for type, " + type.getURI() + "#" + type.getName()); } PlasmaProperty concurrencyTimestampProperty = (PlasmaProperty) type.findProperty(ConcurrencyType.optimistic, ConcurrentDataFlavor.time); if (concurrencyTimestampProperty == null) { if (log.isDebugEnabled()) log.debug("could not find optimistic concurrency timestamp property for type, " + type.getURI() + "#" + type.getName()); } } // FIXME: need CG configuration related to enabling and handling concurrency // checks protected void checkLock(PlasmaDataObject dataObject, PlasmaType type, Timestamp snapshotDate) { PlasmaProperty lockingUserProperty = (PlasmaProperty) type.findProperty(ConcurrencyType.pessimistic, ConcurrentDataFlavor.user); if (lockingUserProperty == null) if (log.isDebugEnabled()) log.debug("could not find locking user property for type, " + type.getURI() + "#" + type.getName()); PlasmaProperty lockingTimestampProperty = (PlasmaProperty) type.findProperty(ConcurrencyType.pessimistic, ConcurrentDataFlavor.time); if (lockingTimestampProperty == null) if (log.isDebugEnabled()) log.debug( "could not find locking timestamp property for type, " + type.getURI() + "#" + type.getName()); } protected String toString(Edge[] edges) { StringBuilder buf = new StringBuilder(); if (edges != null) for (int i = 0; i < edges.length; i++) { if (i > 0) buf.append(", "); buf.append(edges[i].toString()); } return buf.toString(); } protected String toString(List<Edge> edges) { StringBuilder buf = new StringBuilder(); for (int i = 0; i < edges.size(); i++) { if (i > 0) buf.append(", "); buf.append(edges.get(i).toString()); } return buf.toString(); } protected String toString(Map<String, DataObject> map) { StringBuilder buf = new StringBuilder(); if (map != null) { Iterator<String> iter = map.keySet().iterator(); int i = 0; while (iter.hasNext()) { if (i > 0) buf.append(", "); buf.append(iter.next()); } } return buf.toString(); } }