blob: f8032be5b992878623523ad1dc46f09d1b3abb13 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.openjpa.jdbc.meta.strats;
import java.sql.SQLException;
import java.util.BitSet;
import java.util.Collection;
import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.FieldMapping;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.Table;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.Row;
import org.apache.openjpa.jdbc.sql.RowImpl;
import org.apache.openjpa.jdbc.sql.RowManager;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.StoreManager;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.util.ArrayStateImage;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.MetaDataException;
/**
* Uses a state image to determine whether concurrency violations take place.
*
* @author Abe White
*/
public class StateComparisonVersionStrategy
extends AbstractVersionStrategy {
private static final long serialVersionUID = 1L;
public static final String ALIAS = "state-comparison";
private static final Localizer _loc = Localizer.forPackage
(StateComparisonVersionStrategy.class);
@Override
public String getAlias() {
return ALIAS;
}
@Override
public void map(boolean adapt) {
ClassMapping cls = vers.getClassMapping();
if (cls.getJoinablePCSuperclassMapping() != null
|| cls.getEmbeddingMetaData() != null)
throw new MetaDataException(_loc.get("not-base-vers", cls));
vers.getMappingInfo().assertNoSchemaComponents(vers, true);
}
@Override
public void insert(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
throws SQLException {
FieldMapping[] fields = (FieldMapping[]) sm.getMetaData().getFields();
Object[] state = ArrayStateImage.newImage(fields.length);
BitSet loaded = ArrayStateImage.getLoaded(state);
// take a snapshot of all versionable field values
for (int i = 0; i < fields.length; i++) {
if (!fields[i].isPrimaryKey() && fields[i].isVersionable()) {
loaded.set(i);
state[i] = sm.fetch(fields[i].getIndex());
}
}
sm.setNextVersion(state);
}
/**
* This method is for class mappings that take over the insert
* process, but still want to use this indicator for optimistic locking.
*/
@Override
public void customInsert(OpenJPAStateManager sm, JDBCStore store)
throws SQLException {
insert(sm, store, null);
}
@Override
public void update(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
throws SQLException {
// if there is no recorded state (for example, modification made to
// hollow instance and no fields every loaded), can't do anything
Object[] state = (Object[]) sm.getVersion();
if (state == null)
return;
BitSet loaded = ArrayStateImage.getLoaded(state);
Object[] nextState = ArrayStateImage.clone(state);
// loop through fields and update changing values for the next state
// image, plus add WHERE conditions on updates to make sure that
// db values match our previous image
FieldMapping[] fields = (FieldMapping[]) sm.getMetaData().getFields();
Row row;
if (sm.isVersionCheckRequired()) {
for (int i = 0, max = loaded.length(); i < max; i++) {
if (!loaded.get(i))
continue;
// update our next state image with the new field value
if (sm.getDirty().get(i) && !sm.getFlushed().get(i))
nextState[i] = sm.fetch(fields[i].getIndex());
// fetch the row for this field; if no row exists, then we can't
// add one because we have no updates to perform; that means we
// won't detect OL exceptions when another transaction changes
// fields that aren't in any of the same tables as fields that
// this transaction changed
row = rm.getRow(fields[i].getTable(), Row.ACTION_UPDATE,
sm, false);
if (row == null)
continue;
// set WHERE criteria matching the previous state image so the
// update will fail for any changes made by another transaction
fields[i].where(sm, store, rm, state[i]);
row.setFailedObject(sm.getManagedInstance());
}
}
sm.setNextVersion(nextState);
}
/**
* This method is for class mappings that take over the update
* process, but still want to use this indicator for optimistic locking.
*
* @param sm the instance to test
* @param store store manager context
* @param table only state image values in this table will be tested;
* if the custom mapping uses different updates for
* different tables, this method can be called multiple
* times for the multiple tables
* @param record set this parameter to true the last time you call
* this method, so the indicator can setup the next
* version of the given state manager
* @return a {@link CustomUpdate} whose getSQL method yields a
* boolean SQL expression that tests whether the current
* record is equal to our recorded state image, and whose
* setParameters method parameterizes the given prepared
* statement with the values used in the above boolean expression
*/
public CustomUpdate customUpdate(OpenJPAStateManager sm, JDBCStore store,
Table table, boolean record)
throws SQLException {
CustomUpdate custom = new CustomUpdate(table);
Object[] state = (Object[]) sm.getVersion();
if (state == null)
return custom;
BitSet loaded = ArrayStateImage.getLoaded(state);
Object[] nextState = null;
if (record)
nextState = ArrayStateImage.clone(state);
FieldMapping[] fields = (FieldMapping[]) sm.getMetaData().getFields();
for (int i = 0, max = loaded.length(); i < max; i++) {
if (!loaded.get(i))
continue;
if (record && sm.getDirty().get(i) && !sm.getFlushed().get(i))
nextState[i] = sm.fetch(fields[i].getIndex());
if (fields[i].getTable() == table)
fields[i].where(sm, store, custom, state[i]);
}
if (record)
sm.setNextVersion(nextState);
return custom;
}
@Override
public void afterLoad(OpenJPAStateManager sm, JDBCStore store) {
FieldMapping[] fields = (FieldMapping[]) sm.getMetaData().getFields();
Object[] state = (Object[]) sm.getVersion();
if (state == null)
state = ArrayStateImage.newImage(fields.length);
BitSet loaded = ArrayStateImage.getLoaded(state);
// take a snapshot of all versionable field values that were loaded
for (int i = 0; i < fields.length; i++) {
if (!fields[i].isPrimaryKey()
&& fields[i].isVersionable()
&& sm.getLoaded().get(fields[i].getIndex())
&& !loaded.get(i)
&& !sm.getDirty().get(fields[i].getIndex())) {
loaded.set(i);
state[i] = sm.fetch(fields[i].getIndex());
}
}
sm.setVersion(state);
}
@Override
public boolean checkVersion(OpenJPAStateManager sm, JDBCStore store,
boolean updateVersion)
throws SQLException {
if (updateVersion)
sm.setVersion(null);
return !updateVersion;
}
@Override
public int compareVersion(Object v1, Object v2) {
return (ArrayStateImage.sameVersion((Object[]) v1, (Object[]) v2))
? StoreManager.VERSION_SAME : StoreManager.VERSION_DIFFERENT;
}
/**
* Row implementation we use to pass to versionable mappings so they
* can set up the where conditions we need to add to update statements.
*
* @author Abe White
*/
public static class CustomUpdate
extends RowImpl
implements RowManager {
private CustomUpdate(Table table) {
this(table.getColumns());
}
private CustomUpdate(Column[] cols) {
super(cols, Row.ACTION_UPDATE);
}
/**
* Return a boolean SQL expression that should be added to the
* WHERE clause of an UPDATE to test whether the current database
* record matches our stored version.
*/
@Override
public String getSQL(DBDictionary dict) {
Column[] cols = getTable().getColumns();
StringBuilder buf = new StringBuilder();
boolean hasWhere = false;
Object val;
for (Column col : cols) {
val = getWhere(col);
if (val == null)
continue;
if (hasWhere)
buf.append(" AND ");
if (val == NULL)
buf.append(dict.getColumnDBName(col) + " IS NULL");
else
buf.append(dict.getColumnDBName(col) + " = ?");
hasWhere = true;
}
return buf.toString();
}
@Override
protected RowImpl newInstance(Column[] cols, int action) {
return new CustomUpdate(cols);
}
/////////////////////////////
// RowManager implementation
/////////////////////////////
public boolean hasAutoAssignConstraints() {
return false;
}
public Collection getInserts() {
throw new InternalException();
}
public Collection getUpdates() {
throw new InternalException();
}
public Collection getDeletes() {
throw new InternalException();
}
public Collection getSecondaryUpdates() {
throw new InternalException();
}
public Collection getSecondaryDeletes() {
throw new InternalException();
}
public Collection getAllRowUpdates() {
throw new InternalException();
}
public Collection getAllRowDeletes() {
throw new InternalException();
}
@Override
public Row getRow(Table table, int action, OpenJPAStateManager sm,
boolean create) {
// verionable mappings will never want to create rows, so we
// can always safely return null
if (table != getTable())
return null;
return this;
}
@Override
public Row getSecondaryRow(Table table, int action) {
throw new InternalException();
}
@Override
public void flushSecondaryRow(Row row) {
}
@Override
public Row getAllRows(Table table, int action) {
throw new InternalException();
}
@Override
public void flushAllRows(Row row) {
}
@Override
public void setObject(Column col, Object val)
throws SQLException {
throw new InternalException();
}
}
}