/**********************************************************************
Copyright (c) 2002 Mike Martin (TJDO) and others. All rights reserved.
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.
Contributors:
2003 Erik Bengtson - optimistic transaction support
2004 Andy Jefferson - coding standards
2004 Andy Jefferson - conversion to use Logger
2005 Andy Jefferson - added handling for updating FK in related object
2006 Andy Jefferson - changed to extend VersionCheckRequest
...
**********************************************************************/
package org.jpox.store.rdbms.request;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jpox.ClassLoaderResolver;
import org.jpox.ManagedConnection;
import org.jpox.ObjectManager;
import org.jpox.StateManager;
import org.jpox.exceptions.JPOXDataStoreException;
import org.jpox.exceptions.JPOXException;
import org.jpox.exceptions.JPOXOptimisticException;
import org.jpox.metadata.AbstractClassMetaData;
import org.jpox.metadata.AbstractMemberMetaData;
import org.jpox.metadata.ColumnMetaData;
import org.jpox.metadata.IdentityType;
import org.jpox.store.exceptions.NotYetFlushedException;
import org.jpox.store.mapped.DatastoreClass;
import org.jpox.store.mapped.DatastoreField;
import org.jpox.store.mapped.StatementExpressionIndex;
import org.jpox.store.mapped.mapping.JavaTypeMapping;
import org.jpox.store.mapped.mapping.MappingCallbacks;
import org.jpox.store.mapped.mapping.MappingConsumer;
import org.jpox.store.mapped.mapping.VersionMapping;
import org.jpox.store.rdbms.RDBMSManager;
import org.jpox.store.rdbms.SQLController;
import org.jpox.store.rdbms.fieldmanager.ParameterSetter;
import org.jpox.store.rdbms.mapping.RDBMSMapping;
import org.jpox.util.JPOXLogger;
import org.jpox.util.StringUtils;
/**
* Class to provide a means of update of records in a data store.
* Extends basic request class implementing the execute method to do a JDBC
* update operation.
*
* @version $Revision: 1.93 $
*/
public class UpdateRequest extends VersionCheckRequest
{
/** SQL statement for the update. */
private final String updateStmt;
/** SQL statement for the update when using optimistic txns. */
private final String updateStmtOptimistic;
/** callback mappings will have their postUpdate method called after the update */
private final MappingCallbacks[] callbacks;
/** the index for the expression in the update sql statement. */
private MappingStatementIndex mappingStatementIndex;
/** Numbers of all fields to be updated (except PK). */
private final int[] updateFieldNumbers;
/** Numbers of PK fields. */
private final int[] pkFieldNumbers;
/**
* Constructor, taking the table. Uses the structure of the datastore
* table to build a basic query. The query will be of the form
* <PRE>
* UPDATE table-name SET param1=?,param2=? WHERE id1=? AND id2=?
*</PRE>
*or (when also performing version checks)
* <PRE>
* UPDATE table-name SET param1=?,param2=?,version={newvers} WHERE id1=? AND id2=? AND version={oldvers}
*</PRE>
*
* @param table The Class Table representing the datastore table to update
* @param reqFieldMetaData MetaData of the fields to update
* @param cls Class of objects being updated
* @param clr ClassLoader resolver
*/
public UpdateRequest(DatastoreClass table, AbstractMemberMetaData[] reqFieldMetaData, Class cls, ClassLoaderResolver clr)
{
super(table, cls, clr);
// Set up the basic mapping information
mappingStatementIndex = new MappingStatementIndex(); // Populated using the subsequent lines
UpdateMappingConsumer consumer = new UpdateMappingConsumer(cmd);
// Fields to update
if (versionMetaData != null)
{
if (versionMetaData.getFieldName() != null)
{
// Version field
// TODO If the passed fields arent included in the statement (e.g SCO collection) update version?
AbstractMemberMetaData[] updateFmds = new AbstractMemberMetaData[reqFieldMetaData.length + 1];
for (int i=0;i<reqFieldMetaData.length;i++)
{
updateFmds[i] = reqFieldMetaData[i];
}
updateFmds[updateFmds.length-1] = cmd.getMetaDataForMember(versionMetaData.getFieldName());
table.provideMappingsForFields(consumer, updateFmds, false);
}
else
{
// Surrogate version column
table.provideMappingsForFields(consumer, reqFieldMetaData, false);
table.provideVersionMappings(consumer);
}
}
else
{
// No version field
table.provideMappingsForFields(consumer, reqFieldMetaData, false);
}
// WHERE clause - add identity
consumer.setWhereClauseConsumption(true);
table.providePrimaryKeyMappings(consumer);
table.provideDatastoreIdMappings(consumer);
updateStmt = consumer.getStatement();
// Add on the optimistic discriminator (if appropriate) to get the update statement for optimistic txns
if (versionMetaData != null)
{
if (versionMetaData.getFieldName() != null)
{
// Version field
// TODO If the passed fields arent included in the statement (e.g SCO collection) update version?
AbstractMemberMetaData[] updateFmds = new AbstractMemberMetaData[1];
updateFmds[0] = cmd.getMetaDataForMember(versionMetaData.getFieldName());
table.provideMappingsForFields(consumer, updateFmds, false);
}
else
{
// Surrogate version column
table.provideVersionMappings(consumer);
}
}
updateStmtOptimistic = consumer.getStatement();
callbacks = (MappingCallbacks[])consumer.getMappingCallbacks().toArray(new MappingCallbacks[consumer.getMappingCallbacks().size()]);
pkFieldNumbers = consumer.getPrimaryKeyFieldNumbers();
updateFieldNumbers = consumer.getUpdateFieldNumbers();
}
/**
* Method performing the update of the record in the datastore.
* Takes the constructed update query and populates with the specific record information.
* @param sm The state manager for the record to be updated
*/
public void execute(StateManager sm)
{
// Choose the statement based on whether optimistic or not
String stmt = null;
ObjectManager om = sm.getObjectManager();
boolean optimisticChecks = (versionMetaData != null && om.getTransaction().getOptimistic() && versionChecks);
if (optimisticChecks)
{
stmt = updateStmtOptimistic;
}
else
{
stmt = updateStmt;
}
if (stmt != null)
{
if (JPOXLogger.PERSISTENCE.isDebugEnabled())
{
// Debug info about fields being updated
StringBuffer fieldStr = new StringBuffer();
for (int i=0;i<updateFieldNumbers.length;i++)
{
if (fieldStr.length() > 0)
{
fieldStr.append(",");
}
fieldStr.append(cmd.getMetaDataForManagedMemberAtAbsolutePosition(updateFieldNumbers[i]).getName());
}
if (versionMetaData != null && versionMetaData.getFieldName() == null)
{
if (fieldStr.length() > 0)
{
fieldStr.append(",");
}
fieldStr.append("[VERSION]");
}
// Debug information about what we are updating
JPOXLogger.PERSISTENCE.debug(LOCALISER.msg("052214",
StringUtils.toJVMIDString(sm.getObject()), fieldStr.toString(), table));
}
RDBMSManager storeMgr = (RDBMSManager)om.getStoreManager();
boolean batch = false;
// TODO Set the batch flag based on whether we have no other SQL being invoked in here just our UPDATE
try
{
ManagedConnection mconn = storeMgr.getConnection(om);
SQLController sqlControl = storeMgr.getSQLController();
try
{
// Perform the update
PreparedStatement ps = sqlControl.getStatementForUpdate(mconn, stmt, batch);
try
{
Object currentVersion = sm.getTransactionalVersion(sm.getObject());
Object nextVersion = null;
if (versionMetaData != null)
{
// Set the next version in the object
if (versionMetaData.getFieldName() != null)
{
// Version field
AbstractMemberMetaData verfmd = cmd.getMetaDataForMember(table.getVersionMetaData().getFieldName());
if (currentVersion instanceof Integer)
{
// Cater for Integer-based versions TODO Generalise this
currentVersion = new Long(((Integer)currentVersion).longValue());
}
nextVersion =
VersionMapping.getNextVersion(versionMetaData, currentVersion);
if (verfmd.getType() == Integer.class || verfmd.getType() == int.class)
{
// Cater for Integer-based versions TODO Generalise this
nextVersion = new Integer(((Long)nextVersion).intValue());
}
sm.replaceField(verfmd.getAbsoluteFieldNumber(), nextVersion, false);
}
else
{
// Surrogate version column
nextVersion = VersionMapping.getNextVersion(versionMetaData, currentVersion);
}
sm.setTransactionalVersion(nextVersion);
}
// SELECT clause - set the required fields to be updated
if (updateFieldNumbers != null)
{
sm.provideFields(updateFieldNumbers, new ParameterSetter(sm, ps, mappingStatementIndex.getFields(), true));
}
if (versionMetaData != null && versionMetaData.getFieldName() == null)
{
// SELECT clause - set the surrogate version column to the new version
table.getVersionMapping(false).setObject(om, ps,
mappingStatementIndex.getVersion().getParameterIndex(), nextVersion);
}
// WHERE clause - primary key fields
if (table.getIdentityType() == IdentityType.DATASTORE)
{
// a). datastore identity
table.getDataStoreObjectIdMapping().setObject(om, ps, mappingStatementIndex.getDatastoreId().getParameterIndex(),
sm.getInternalObjectId());
}
else if (table.getIdentityType() == IdentityType.APPLICATION)
{
// b). application identity
sm.provideFields(pkFieldNumbers, new ParameterSetter(sm, ps, mappingStatementIndex.getPrimaryKeys(),true));
}
if (optimisticChecks)
{
if (currentVersion == null)
{
// Somehow the version is not set on this object (not read in ?) so report the bug
String msg = LOCALISER.msg("052201",
sm.getInternalObjectId(), table);
JPOXLogger.PERSISTENCE.error(msg);
throw new JPOXException(msg);
}
// WHERE clause - current version discriminator
JavaTypeMapping verMapping = mappingStatementIndex.getVersion2().getMapping();
verMapping.setObject(om, ps,
mappingStatementIndex.getVersion2().getParameterIndex(), currentVersion);
}
int[] rcs = sqlControl.executeStatementUpdate(mconn, stmt, ps, !batch);
if (rcs[0] == 0 && optimisticChecks)
{
// No object updated so either object disappeared or failed optimistic version checks
// TODO Batching : when we use batching here we need to process these somehow
String msg = LOCALISER.msg("052203",
StringUtils.toJVMIDString(sm.getObject()), sm.getInternalObjectId(),
"" + sm.getTransactionalVersion(sm.getObject()));
JPOXLogger.PERSISTENCE.error(msg);
throw new JPOXOptimisticException(msg, sm.getObject());
}
}
finally
{
sqlControl.closeStatement(mconn, ps);
}
}
finally
{
mconn.release();
}
}
catch (SQLException e)
{
String msg = LOCALISER.msg("052215",
StringUtils.toJVMIDString(sm.getObject()), stmt, StringUtils.getStringFromStackTrace(e));
JPOXLogger.DATASTORE_PERSIST.error(msg);
List exceptions = new ArrayList();
exceptions.add(e);
while((e = e.getNextException())!=null)
{
exceptions.add(e);
}
throw new JPOXDataStoreException(msg, (Throwable[])exceptions.toArray(new Throwable[exceptions.size()]));
}
}
// Execute any mapping actions now that we have done the update
for (int i=0; i<callbacks.length; ++i)
{
try
{
if (JPOXLogger.PERSISTENCE.isDebugEnabled())
{
JPOXLogger.PERSISTENCE.debug(LOCALISER.msg("052216",
StringUtils.toJVMIDString(sm.getObject()),
((JavaTypeMapping)callbacks[i]).getFieldMetaData().getFullFieldName()));
}
callbacks[i].postUpdate(sm);
}
catch (NotYetFlushedException e)
{
sm.updateFieldAfterInsert(e.getPersistable(),
((JavaTypeMapping)callbacks[i]).getFieldMetaData().getAbsoluteFieldNumber());
}
}
}
/**
* Mapping Consumer used for generating the UPDATE statement for an object in a table.
* This statement will be of the form
* <PRE>
* UPDATE table-name SET param1=?,param2=? WHERE id1=? AND id2=?
* </PRE>
* or (when also performing version checks)
* <PRE>
* UPDATE table-name SET param1=?,param2=?,version={newvers} WHERE id1=? AND id2=? AND version={oldvers}
* </PRE>
*
* @version $Revision: 1.93 $
*/
private class UpdateMappingConsumer implements MappingConsumer
{
/** Flag for initialisation state of the consumer. */
boolean initialized = false;
/** Current parameter index. */
int paramIndex = 1;
/** maximum field number possible to be present in this statement. */
int highestFieldNumber = 0;
/** Numbers of all fields to be updated. */
List updateFields = new ArrayList();
/** Numbers of all PK fields. */
List pkFields = new ArrayList();
List mc = new ArrayList();
/** for UPDATE statement **/
StringBuffer columnAssignments = new StringBuffer();
Map assignedColumns = new HashMap();
/** Where clause for the statement. Built during the consumption process. */
StringBuffer where = new StringBuffer();
/** MetaData for the class of the object */
private final AbstractClassMetaData cmd;
private boolean whereClauseConsumption = false;
/**
* Constructor
* @param clr the ClassLoaderResolver
*/
public UpdateMappingConsumer(AbstractClassMetaData cmd)
{
super();
this.cmd = cmd;
}
public void setWhereClauseConsumption(boolean whereClause)
{
this.whereClauseConsumption = whereClause;
}
/**
* Accessor for the highest field number consumed.
* @return The highest (absolute) field number
*/
public int getHighestFieldNumber()
{
return highestFieldNumber;
}
public void preConsumeMapping(int highest)
{
if (!initialized)
{
highestFieldNumber = highest;
mappingStatementIndex.setPrimaryKeys(new StatementExpressionIndex[highest]);
mappingStatementIndex.setFields(new StatementExpressionIndex[highest]);
initialized = true;
}
}
/**
* Consumes a mapping for a field.
* @param m The mapping.
* @param fmd MetaData for the field
*/
public void consumeMapping(JavaTypeMapping m, AbstractMemberMetaData fmd)
{
if (!fmd.getAbstractClassMetaData().isSameOrAncestorOf(cmd))
{
return;
}
if (m.includeInUpdateStatement())
{
// Check if the field is "updatable" (either using JPA column, or JDO extension)
if (fmd.hasExtension("updateable") && fmd.getValueForExtension("updateable").equalsIgnoreCase("false"))
{
return;
}
ColumnMetaData[] colmds = fmd.getColumnMetaData();
if (colmds != null && colmds.length > 0)
{
for (int i=0;i<colmds.length;i++)
{
if (!colmds[i].getUpdateable())
{
// Not to be updated
return;
}
}
}
Integer abs_field_num = new Integer(fmd.getAbsoluteFieldNumber());
int parametersIndex[] = new int[m.getNumberOfDatastoreFields()];
StatementExpressionIndex sei = new StatementExpressionIndex();
sei.setMapping(m);
sei.setParameterIndex(parametersIndex);
if (fmd.isPrimaryKey())
{
//primary keys goes to the WHERE COLUMN = ?, ....
mappingStatementIndex.getPrimaryKeys()[fmd.getAbsoluteFieldNumber()] = sei;
for (int j=0; j<parametersIndex.length; j++)
{
if (where.length() > 0)
{
where.append(" AND ");
}
String condition = m.getDataStoreMapping(j).getDatastoreField().getIdentifier() +
"=" + ((RDBMSMapping)m.getDataStoreMapping(j)).getUpdateInputParameter();
where.append(condition);
if (!pkFields.contains(abs_field_num))
{
pkFields.add(abs_field_num);
}
parametersIndex[j] = paramIndex++;
}
}
else
{
if (whereClauseConsumption)
{
// Version field to be put in WHERE clause (only non-PK field that will come here)
mappingStatementIndex.setVersion2(sei);
parametersIndex[0] = paramIndex++;
String inputParam = ((RDBMSMapping)m.getDataStoreMapping(0)).getUpdateInputParameter();
String condition = " AND " + m.getDataStoreMapping(0).getDatastoreField().getIdentifier() + "=" + inputParam;
where.append(condition);
}
else
{
//non primary keys goes to the SELECT TBL SET COLUMN = ?, ....
mappingStatementIndex.getFields()[fmd.getAbsoluteFieldNumber()] = sei;
for (int j = 0; j < parametersIndex.length; j++)
{
// check if the column was not already assigned
if (!assignedColumns.containsKey(m.getDataStoreMapping(j).getDatastoreField().getIdentifier().toString()))
{
if (columnAssignments.length() > 0)
{
columnAssignments.append(", ");
}
String param = ((RDBMSMapping)m.getDataStoreMapping(j)).getUpdateInputParameter();
columnAssignments.append(m.getDataStoreMapping(j).getDatastoreField().getIdentifier()).append("=").append(param);
if (param.indexOf("?") > -1) // only add fields to be replaced by the real values only if the param value has '?'
{
if (!updateFields.contains(abs_field_num))
{
updateFields.add(abs_field_num);
}
parametersIndex[j] = paramIndex++;
}
assignedColumns.put(m.getDataStoreMapping(j).getDatastoreField().getIdentifier().toString(),
new Integer(fmd.getAbsoluteFieldNumber()));
}
else
{
parametersIndex[j] = ((Integer)assignedColumns.get(
m.getDataStoreMapping(j).getDatastoreField().getIdentifier().toString())).intValue();
}
}
}
}
}
if (m instanceof MappingCallbacks)
{
mc.add(m);
}
}
/**
* Consumes a mapping associated to "special" columns.
* @param m The mapping.
* @param mappingType the Mapping type
*/
public void consumeMapping(JavaTypeMapping m, int mappingType)
{
if (mappingType == MappingConsumer.MAPPING_TYPE_VERSION)
{
// Surrogate version column
String inputParam = ((RDBMSMapping)m.getDataStoreMapping(0)).getUpdateInputParameter();
int[] param = { paramIndex++ };
StatementExpressionIndex versStmtIdx = null;
if (whereClauseConsumption)
{
versStmtIdx = mappingStatementIndex.getVersion2();
String condition = " AND " + m.getDataStoreMapping(0).getDatastoreField().getIdentifier() + "=" + inputParam;
where.append(condition);
}
else
{
versStmtIdx = mappingStatementIndex.getVersion();
String condition = m.getDataStoreMapping(0).getDatastoreField().getIdentifier() + "=" + inputParam;
if (columnAssignments.length() > 0)
{
columnAssignments.append(", ");
}
columnAssignments.append(condition);
}
versStmtIdx.setMapping(m);
versStmtIdx.setParameterIndex(param);
}
else if (mappingType == MappingConsumer.MAPPING_TYPE_DATASTORE_ID)
{
// Surrogate datastore id column
String condition = ((DatastoreField)key.getColumns().get(0)).getIdentifier() + "=?";
where.append(condition);
int[] param = { paramIndex++ };
mappingStatementIndex.getDatastoreId().setParameterIndex(param);
}
}
/**
* Consumer a datastore field without mapping.
* @param fld The datastore field
*/
public void consumeUnmappedDatastoreField(DatastoreField fld)
{
// Do nothing since we dont handle unmapped columns
}
/**
* @return Returns the mappingCallbacks.
*/
public List getMappingCallbacks()
{
return mc;
}
/**
* Accessor for the numbers of fields to be updated (excluding PK fields).
* @return array of absolute field numbers
*/
public int[] getUpdateFieldNumbers()
{
int[] fieldNumbers = new int[updateFields.size()];
for (int i = 0; i < updateFields.size(); ++i)
{
fieldNumbers[i] = ((Integer) updateFields.get(i)).intValue();
}
return fieldNumbers;
}
/**
* Accessor for the numbers of PK fields.
* @return array of absolute primary key field numbers
*/
public int[] getPrimaryKeyFieldNumbers()
{
int[] fieldNumbers = new int[pkFields.size()];
for (int i = 0; i < pkFields.size(); i++)
{
fieldNumbers[i] = ((Integer) pkFields.get(i)).intValue();
}
return fieldNumbers;
}
/**
* Accessor for the basic update SQL statement.
* <PRE>
* UPDATE TABLE SET COL1 = ?, COL2 = ? WHERE COL3 = ? AND COL4 = ?
* </PRE>
* @return The update SQL statement
*/
public String getStatement()
{
if (columnAssignments.length() < 1)
{
return null;
}
return "UPDATE " + table.toString() + " SET " + columnAssignments + " WHERE " + where;
}
}
}