blob: 32de14176ccd9ee5e6a88beeec0ccdb95d4f6b42 [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.lang.reflect.Array;
import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.Comparator;
import org.apache.openjpa.jdbc.identifier.DBIdentifier;
import org.apache.openjpa.jdbc.kernel.JDBCFetchConfiguration;
import org.apache.openjpa.jdbc.kernel.JDBCStore;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.VersionMappingInfo;
import org.apache.openjpa.jdbc.schema.Column;
import org.apache.openjpa.jdbc.schema.ColumnIO;
import org.apache.openjpa.jdbc.schema.ForeignKey;
import org.apache.openjpa.jdbc.schema.Index;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.Joins;
import org.apache.openjpa.jdbc.sql.Result;
import org.apache.openjpa.jdbc.sql.Row;
import org.apache.openjpa.jdbc.sql.RowManager;
import org.apache.openjpa.jdbc.sql.Select;
import org.apache.openjpa.kernel.LockLevels;
import org.apache.openjpa.kernel.MixedLockLevels;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.StoreManager;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.MetaDataException;
/**
* Uses a one or more column(s) and corresponding version object.
*
* @author Marc Prud'hommeaux
* @author Pinaki Poddar
*/
public abstract class ColumnVersionStrategy
extends AbstractVersionStrategy {
private static final long serialVersionUID = 1L;
private static final Localizer _loc = Localizer.forPackage
(ColumnVersionStrategy.class);
/**
* Return the code from {@link JavaTypes} for the version values this
* strategy uses. This method is only used during mapping installation.
*/
protected abstract int getJavaType();
/**
* Return the code from {@link JavaTypes} for the version value this given
* column index uses. Only used if the version strategy employs more than
* one column.
*/
protected int getJavaType(int i) {
throw new AbstractMethodError(_loc.get(
"multi-column-version-unsupported",getAlias()).toString());
}
/**
* Return the next version given the current one, which may be null.
*/
protected abstract Object nextVersion(Object version);
/**
* Compare the two versions. Defaults to assuming the version objects
* implement {@link Comparable}.
*
* @see Comparator#compare
*/
protected int compare(Object v1, Object v2) {
if (v1 == v2)
return 0;
if (v1 == null)
return -1;
if (v2 == null)
return 1;
if (v1.getClass().isArray()) {
if (!v2.getClass().isArray())
throw new InternalException();
return compare((Object[])v1, (Object[])v2);
}
if (v1.getClass() != v2.getClass()) {
if (v1 instanceof Number && !(v1 instanceof BigDecimal))
v1 = new BigDecimal(((Number) v1).doubleValue());
if (v2 instanceof Number && !(v2 instanceof BigDecimal))
v2 = new BigDecimal(((Number) v2).doubleValue());
}
return ((Comparable) v1).compareTo(v2);
}
/**
* Compare each element of the given arrays that must be of equal size.
* The given array values represent version values and the result designate
* whether first version is earlier, same or later than the second one.
*
* @return If any element of a1 is later than corresponding element of
* a2 then returns 1 i.e. the first version is later than the second
* version. If each element of a1 is equal to corresponding element of a2
* then return 0 i.e. the first version is same as the second version.
* else return a negative number i.e. the first version is earlier than
* the second version.
*/
protected int compare(Object[] a1, Object[] a2) {
if (a1.length != a2.length)
throw new InternalException();
int total = 0;
for (int i = 0; i < a1.length; i++) {
int c = compare(a1[i], a2[i]);
if (c > 0)
return 1;
total += c;
}
return total;
}
@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));
VersionMappingInfo info = vers.getMappingInfo();
info.assertNoJoin(vers, true);
info.assertNoForeignKey(vers, !adapt);
info.assertNoUnique(vers, false);
if (info.getColumns().size() > 1) {
Column[] templates = new Column[info.getColumns().size()];
for (int i = 0; i < info.getColumns().size(); i++) {
templates[i] = new Column();
Column infoColumn = (Column)info.getColumns().get(i);
templates[i].setTableIdentifier(infoColumn.getTableIdentifier());
templates[i].setType(infoColumn.getType());
templates[i].setSize(infoColumn.getSize());
templates[i].setDecimalDigits(infoColumn.getDecimalDigits());
templates[i].setJavaType(getJavaType(i));
templates[i].setIdentifier(infoColumn.getIdentifier());
}
Column[] cols = info.getColumns(vers, templates, adapt);
for (Column col : cols) {
col.setVersionStrategy(this);
}
vers.setColumns(cols);
vers.setColumnIO(info.getColumnIO());
} else {
Column tmplate = new Column();
tmplate.setJavaType(getJavaType());
DBDictionary dict = vers.getMappingRepository().getDBDictionary();
DBIdentifier versName = DBIdentifier.newColumn("versn", dict != null ? dict.delimitAll() : false);
tmplate.setIdentifier(versName);
Column[] cols = info.getColumns(vers, new Column[]{ tmplate },
adapt);
cols[0].setVersionStrategy(this);
vers.setColumns(cols);
vers.setColumnIO(info.getColumnIO());
Index idx = info.getIndex(vers, cols, adapt);
vers.setIndex(idx);
}
}
@Override
public void insert(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
throws SQLException {
Column[] cols = vers.getColumns();
ColumnIO io = vers.getColumnIO();
Object initial = nextVersion(null);
for (int i = 0; i < cols.length; i++) {
Row row = rm.getRow(cols[i].getTable(), Row.ACTION_INSERT, sm,
true);
if (io.isInsertable(i, initial == null))
row.setObject(cols[i], getColumnValue(initial, i));
}
// set initial version into state manager
Object nextVersion;
nextVersion = initial;
sm.setNextVersion(nextVersion);
}
@Override
public void update(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
throws SQLException {
Column[] cols = vers.getColumns();
if (cols == null || cols.length == 0 ||
!sm.isDirty() && !sm.isVersionUpdateRequired())
return;
Object curVersion = sm.getVersion();
Object nextVersion = nextVersion(curVersion);
// set where and update conditions on row
for (int i = 0; i < cols.length; i++) {
Row row = rm.getRow(cols[i].getTable(), Row.ACTION_UPDATE, sm,
true);
row.setFailedObject(sm.getManagedInstance());
if (curVersion != null && sm.isVersionCheckRequired()) {
row.whereObject(cols[i], getColumnValue(curVersion, i));
if (isSecondaryColumn(cols[i], sm)) {
ForeignKey[] fks = cols[i].getTable().getForeignKeys();
for (ForeignKey fk : fks) {
row.whereForeignKey(fk, sm);
}
}
}
if (vers.getColumnIO().isUpdatable(i, nextVersion == null))
row.setObject(cols[i], getColumnValue(nextVersion, i));
}
if (nextVersion != null)
sm.setNextVersion(nextVersion);
}
@Override
public void delete(OpenJPAStateManager sm, JDBCStore store, RowManager rm)
throws SQLException {
Column[] cols = vers.getColumns();
Object curVersion = sm.getVersion();
Object cur;
for (int i = 0; i < cols.length; i++) {
Row row = rm.getRow(cols[i].getTable(),
Row.ACTION_DELETE, sm, true);
row.setFailedObject(sm.getManagedInstance());
cur = getColumnValue(curVersion, i);
// set where and update conditions on row
if (cur != null) {
row.whereObject(cols[i], cur);
if (isSecondaryColumn(cols[i], sm)) {
ForeignKey[] fks = cols[i].getTable().getForeignKeys();
for (ForeignKey fk : fks) {
row.whereForeignKey(fk, sm);
}
}
}
}
}
@Override
public boolean select(Select sel, ClassMapping mapping) {
sel.select(vers.getColumns());
return true;
}
@Override
public Object load(OpenJPAStateManager sm, JDBCStore store, Result res)
throws SQLException {
return this.load(sm, store, res, null);
}
@Override
public Object load(OpenJPAStateManager sm, JDBCStore store, Result res, Joins joins)
throws SQLException {
// typically if one version column is in the result, they all are, so
// optimize by checking for the first one before doing any real work
Column[] cols = vers.getColumns();
if (!res.contains(cols[0], joins)) {
return null;
}
Object version = populateFromResult(res, joins);
// OPENJPA-662 Allow a null StateManager because this method may just be
// invoked to get the result of projection query
if (sm != null) {
sm.setVersion(version);
}
return version;
}
@Override
public boolean checkVersion(OpenJPAStateManager sm, JDBCStore store,
boolean updateVersion)
throws SQLException {
Column[] cols = vers.getColumns();
Select sel = store.getSQLFactory().newSelect();
sel.select(cols);
sel.wherePrimaryKey(sm.getObjectId(), vers.getClassMapping(), store);
// No need to lock version field (i.e. optimistic), except when version update is required (e.g. refresh)
JDBCFetchConfiguration fetch = store.getFetchConfiguration();
if (!updateVersion && fetch.getReadLockLevel() >= MixedLockLevels.LOCK_PESSIMISTIC_READ) {
fetch = (JDBCFetchConfiguration) fetch.clone();
fetch.setReadLockLevel(LockLevels.LOCK_NONE);
}
Result res = sel.execute(store, fetch);
try {
if (!res.next())
return false;
Object memVersion = sm.getVersion();
Object dbVersion = populateFromResult(res, null);
boolean refresh = compare(memVersion, dbVersion) < 0;
if (updateVersion)
sm.setVersion(dbVersion);
return !refresh;
} finally {
res.close();
}
}
@Override
public int compareVersion(Object v1, Object v2) {
if (v1 == v2)
return StoreManager.VERSION_SAME;
if (v1 == null || v2 == null)
return StoreManager.VERSION_DIFFERENT;
int cmp = compare(v1, v2);
if (cmp < 0)
return StoreManager.VERSION_EARLIER;
if (cmp > 0)
return StoreManager.VERSION_LATER;
return StoreManager.VERSION_SAME;
}
/**
* Populate values of a version object from the given result.
*
* @return a single Object or an array depending on whether using a single
* or multiple columns being used for representation.
*/
Object populateFromResult(Result res, Joins joins) throws SQLException {
if (res == null)
return null;
Column[] cols = vers.getColumns();
Object[] values = new Object[cols.length];
for (int i = 0; i < cols.length; i++) {
values[i] = res.getObject(cols[i], null, joins);
}
return (cols.length == 1) ? values[0] : values;
}
Object getColumnValue(Object o, int idx) {
if (o == null)
return null;
if (o.getClass().isArray())
return Array.get(o, idx);
return o;
}
boolean isSecondaryColumn(Column col, OpenJPAStateManager sm) {
ClassMapping mapping = (ClassMapping)sm.getMetaData();
while (mapping != null) {
if (mapping.getTable() == col.getTable())
return false;
else
mapping = mapping.getPCSuperclassMapping();
}
return true;
}
}