/** * 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.io; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.hbase.client.Delete; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.Row; import org.apache.hadoop.hbase.client.Table; import org.apache.hadoop.hbase.util.Bytes; import org.cloudgraph.config.TableConfig; import org.cloudgraph.hbase.key.StatefullColumnKeyFactory; import org.cloudgraph.state.GraphRow; import org.cloudgraph.state.GraphState; import org.cloudgraph.store.key.GraphStatefullColumnKeyFactory; import org.cloudgraph.store.service.DuplicateRowException; import org.cloudgraph.store.service.GraphServiceException; import org.cloudgraph.store.service.MissingRowException; import org.cloudgraph.store.service.ToumbstoneRowException; import org.plasma.sdo.PlasmaDataObject; import org.plasma.sdo.PlasmaProperty; import org.plasma.sdo.PlasmaType; import commonj.sdo.ChangeSummary; import commonj.sdo.DataObject; /** * The operational, configuration and other state information * required for write operations on a single graph row. * <p> * Acts as a single component within a {@link TableWriter} container * and encapsulates the HBase client <a target="#" href="http://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Put.html">Put</a> and * <a target="#" href="http://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Put.html">Put</a> * operations for use in write operations across multiple logical * entities within a graph row. * </p> * * @see org.cloudgraph.hbase.io.TableWriter * @author Scott Cinnamond * @since 0.5.1 */ public class GraphRowWriter extends GraphRow implements RowWriter { private static Log log = LogFactory.getLog(GraphRowWriter.class); private TableWriter tableWriter; private Put row; private Delete rowDelete; private List<Row> operations = new ArrayList<Row>(); public GraphRowWriter(byte[] rowKey, DataObject rootDataObject, TableWriter tableWriter) { super(rowKey, rootDataObject); this.tableWriter = tableWriter; this.row = new Put(rowKey); this.operations.add(this.row); } @Override public GraphState getGraphState() throws IOException { if (this.graphState == null) { this.graphState = createGraphState(this.rowKey, this.rootDataObject, this.rootDataObject.getDataGraph().getChangeSummary(), this.tableWriter.getTableConfig(), this.tableWriter.getTable()); } return this.graphState; } @Override public GraphStatefullColumnKeyFactory getColumnKeyFactory() throws IOException { if (this.columnKeyFactory == null) { this.columnKeyFactory = new StatefullColumnKeyFactory( (PlasmaType)this.rootDataObject.getType(), this.getGraphState()); } return this.columnKeyFactory; } @Override public Put getRow() { return this.row; } @Override public void deleteRow() { getRowDelete(); } /** * Returns the existing (or creates a new) row delete mutation. * @return the existing (or creates a new) row delete mutation. */ @Override public Delete getRowDelete() { if (this.rowDelete == null) { this.rowDelete = new Delete(this.getRowKey()); this.operations.add(this.rowDelete); } return this.rowDelete; } /** * Returns whether there is an existing row delete mutation. * @return whether there is an existing row delete mutation. */ @Override public boolean hasRowDelete() { return this.rowDelete != null; } /** * Returns a single column value for this row given a context * data object and property. Uses a statefull column key factory * to generate a column key based on the given context data object * and property. * @param dataObject the context data object * @param property the context property * @return the column value bytes * @throws IOException * * @see StatefullColumnKeyFactory */ @Override public byte[] fetchColumnValue(PlasmaDataObject dataObject, PlasmaProperty property) throws IOException { byte[] qualifier = this.getColumnKeyFactory().createColumnKey( dataObject, property); Get existing = new Get(this.rowKey); byte[] family = tableWriter.getTableConfig().getDataColumnFamilyNameBytes(); existing.addColumn(family, qualifier); Result result = this.getTableWriter().getTable().get(existing); return result.getValue(family, qualifier); } @Override public TableWriter getTableWriter() { return this.tableWriter; } /** * Returns whether the root data object for this writer * is created. * @return whether the root data object for this writer * is created. */ @Override public boolean isRootCreated() { return this.rootDataObject.getDataGraph().getChangeSummary().isCreated( this.rootDataObject); } /** * Returns whether the root data object for this writer * is deleted. * @return whether the root data object for this writer * is deleted. */ @Override public boolean isRootDeleted() { return this.rootDataObject.getDataGraph().getChangeSummary().isDeleted( this.rootDataObject); } @Override public List<Row> getWriteOperations() { return this.operations; } /** * Initializes a graph state by querying for a row * based on the given row key and either creating a new (empty) * graph state for an entirely new graph, or otherwise initializing * a graph state based on state or state and management columns in * the existing returned row. * * @param rowKey the row key * @param dataGraph the data graph * @param changeSummary the change summary * @return the graph state * @throws IOException * @throws DuplicateRowException for a new graph if a row already exists * for the given row key * @throws GraphServiceException where except for a new graph, if no row * exists for the given row key */ protected GraphState createGraphState(byte[] rowKey, DataObject dataObject, ChangeSummary changeSummary, TableConfig tableConfig, Table con) throws IOException { GraphState graphState; // --ensure row exists unless a new row/graph // --use empty get with only necessary "state" management columns // if entirely new graph for the given // distributed or sub-graph root if (changeSummary.isCreated(dataObject)) { if (tableConfig.uniqueChecks()) { Result result = getMinimalRow(rowKey, tableConfig, con); if (!result.isEmpty()) { if (!result.containsColumn( tableConfig.getDataColumnFamilyNameBytes(), Bytes.toBytes(GraphState.TOUMBSTONE_COLUMN_NAME))) { throw new DuplicateRowException("no row for id '" + Bytes.toString(rowKey) + "' expected when creating new row for table '" + tableConfig.getTable().getName() + "'"); } else { if (!tableConfig.tombstoneRowsOverwriteable()) throw new ToumbstoneRowException("no toumbstone row for id '" + Bytes.toString(rowKey) + "' expected when creating new row for table '" + tableConfig.getTable().getName() + "' - cannot overwrite toumbstone row"); } } } PlasmaDataObject root = (PlasmaDataObject)dataObject; graphState = new GraphState(root.getUUID(), this.tableWriter.getFederatedOperation().getMarshallingContext()); } else { Result result = getStateRow(rowKey, tableConfig, con); if (result.isEmpty()) { throw new MissingRowException(tableConfig.getTable().getName(), Bytes.toString(rowKey)); } if (result.containsColumn( tableConfig.getDataColumnFamilyNameBytes(), Bytes.toBytes(GraphState.TOUMBSTONE_COLUMN_NAME))) { throw new ToumbstoneRowException("no row for id '" + Bytes.toString(rowKey) + "' expected when modifying row for table '" + tableConfig.getTable().getName() + "' - cannot overwrite toumbstone row"); } byte[] state = result.getValue(Bytes.toBytes(tableConfig.getDataColumnFamilyName()), Bytes.toBytes(GraphState.STATE_COLUMN_NAME)); if (state != null) { if (log.isDebugEnabled()) { byte[] root = result.getValue(Bytes.toBytes(tableConfig.getDataColumnFamilyName()), Bytes.toBytes(GraphState.ROOT_UUID_COLUMN_NAME)); log.debug("root: " + Bytes.toString(root) + " state: " + Bytes.toString(state)); } } else throw new OperationException("expected column '" + GraphState.STATE_COLUMN_NAME + " for row " + Bytes.toString(rowKey) + "'"); graphState = new GraphState(Bytes.toString(state), this.tableWriter.getFederatedOperation().getMarshallingContext()); // Even though we found a row, the user could have committed a data object // which was copied and has a different UUID than the original data object // which generated the graph state. Subsequent update or delete operations using // this mismatched UUID can cause havoc, as the UUID is a key used to look up // sequence values from the state and create column keys and modify or delete // associated values. PlasmaDataObject root = (PlasmaDataObject)dataObject; if (!graphState.getRootUUID().equals(root.getUUID())) throw new UUIDMismatchException("Graph state root UUID '"+graphState.getRootUUID()+"' " + "does not match writer sub-root, "+root + " - can be caused by data object copy operations, " + "where only properties are copied not the UUID, then the copied object is modified and comitted"); } return graphState; } private Result getMinimalRow(byte[] rowKey, TableConfig tableConfig, Table table) throws IOException { Get existing = new Get(rowKey); existing.addColumn(tableConfig.getDataColumnFamilyNameBytes(), Bytes.toBytes(GraphState.ROOT_UUID_COLUMN_NAME)); existing.addColumn(tableConfig.getDataColumnFamilyNameBytes(), Bytes.toBytes(GraphState.TOUMBSTONE_COLUMN_NAME)); return table.get(existing); } private Result getStateRow(byte[] rowKey, TableConfig tableConfig, Table table) throws IOException { Get existing = new Get(rowKey); existing.addColumn(tableConfig.getDataColumnFamilyNameBytes(), Bytes.toBytes(GraphState.ROOT_UUID_COLUMN_NAME)); existing.addColumn(tableConfig.getDataColumnFamilyNameBytes(), Bytes.toBytes(GraphState.STATE_COLUMN_NAME)); existing.addColumn(tableConfig.getDataColumnFamilyNameBytes(), Bytes.toBytes(GraphState.TOUMBSTONE_COLUMN_NAME)); return table.get(existing); } }