/*
* Copyright 2006-2012 Amazon Technologies, Inc. or its affiliates.
* Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
* of Amazon Technologies, Inc. or its affiliates. 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.
*/
package com.amazon.carbonado.repo.jdbc;
import java.lang.reflect.Method;
import java.lang.reflect.UndeclaredThrowableException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.EnumSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.cojen.classfile.ClassFile;
import org.cojen.classfile.CodeBuilder;
import org.cojen.classfile.Label;
import org.cojen.classfile.LocalVariable;
import org.cojen.classfile.MethodInfo;
import org.cojen.classfile.Modifiers;
import org.cojen.classfile.Opcode;
import org.cojen.classfile.TypeDesc;
import org.cojen.util.ClassInjector;
import org.cojen.util.KeyFactory;
import org.joda.time.ReadableInstant;
import com.amazon.carbonado.FetchException;
import com.amazon.carbonado.OptimisticLockException;
import com.amazon.carbonado.PersistException;
import com.amazon.carbonado.Storable;
import com.amazon.carbonado.SupportException;
import com.amazon.carbonado.lob.Lob;
import com.amazon.carbonado.info.StorablePropertyAdapter;
import com.amazon.carbonado.gen.CodeBuilderUtil;
import com.amazon.carbonado.gen.MasterFeature;
import com.amazon.carbonado.gen.MasterStorableGenerator;
import com.amazon.carbonado.gen.MasterSupport;
import com.amazon.carbonado.gen.StorableGenerator;
import com.amazon.carbonado.gen.TriggerSupport;
import static com.amazon.carbonado.gen.CommonMethodNames.*;
import com.amazon.carbonado.util.SoftValuedCache;
/**
* Generates concrete implementations of {@link Storable} types for
* {@link JDBCRepository}.
*
* @author Brian S O'Neill
*/
class JDBCStorableGenerator<S extends Storable> {
// These method names end in "$" to prevent name collisions with any
// inherited methods.
private static final String EXTRACT_ALL_METHOD_NAME = "extractAll$";
private static final String EXTRACT_DATA_METHOD_NAME = "extractData$";
private static final String LOB_LOADER_FIELD_PREFIX = "lobLoader$";
// Initial StringBuilder capactity for update statement.
private static final int INITIAL_UPDATE_BUFFER_SIZE = 100;
// Modes for automatic versioning when setting PreparedStatement values.
private static final int NORMAL = 0;
private static final int NOT_NULL = 1;
private static final int INITIAL_VERSION = 2;
private static final int INCREMENT_VERSION = 3;
private static final SoftValuedCache<Object, Class<? extends Storable>> cCache;
static {
cCache = SoftValuedCache.newCache(11);
}
static <S extends Storable> Class<? extends S> getGeneratedClass(JDBCStorableInfo<S> info,
boolean isMaster,
boolean autoVersioning,
boolean suppressReload)
throws SupportException
{
Object key = KeyFactory.createKey(new Object[] {
info, isMaster, autoVersioning, suppressReload});
synchronized (cCache) {
Class<? extends S> generatedClass = (Class<? extends S>) cCache.get(key);
if (generatedClass != null) {
return generatedClass;
}
generatedClass = new JDBCStorableGenerator<S>
(info, isMaster, autoVersioning, suppressReload)
.generateAndInjectClass();
cCache.put(key, generatedClass);
return generatedClass;
}
}
private final Class<S> mStorableType;
private final JDBCStorableInfo<S> mInfo;
private static enum Versioning {
NONE, EXTERNAL, AUTO
}
private final Versioning mVersioning;
private static final int RELOAD_ALWAYS = 1, RELOAD_SOMETIMES = 2, RELOAD_NEVER = 3;
private final int mReload;
private final Map<String, ? extends JDBCStorableProperty<S>> mAllProperties;
private final ClassLoader mParentClassLoader;
private final ClassInjector mClassInjector;
private final ClassFile mClassFile;
private JDBCStorableGenerator(final JDBCStorableInfo<S> info,
final boolean isMaster,
final boolean autoVersioning,
final boolean suppressReload)
throws SupportException
{
mStorableType = info.getStorableType();
mInfo = info;
mAllProperties = mInfo.getAllProperties();
EnumSet<MasterFeature> features = EnumSet
.of(MasterFeature.INSERT_TXN, // Required because of reload after insert.
MasterFeature.UPDATE_TXN); // Required because of reload after update.
if (!isMaster) {
mVersioning = Versioning.NONE;
} else {
features.add(MasterFeature.INSERT_SEQUENCES);
// Must use @Automatic to override.
features.add(MasterFeature.INSERT_CHECK_REQUIRED);
if (info.getVersionProperty() != null && info.getVersionProperty().isDerived()) {
features.add(MasterFeature.VERSIONING);
// Say none because master storable takes care of it.
mVersioning = Versioning.NONE;
} else {
mVersioning = autoVersioning ? Versioning.AUTO : Versioning.EXTERNAL;
}
}
int reload = RELOAD_ALWAYS;
if (suppressReload) {
reload = RELOAD_NEVER;
// No need to be in a transaction if reload never happens.
honorSuppression: {
Map<String, JDBCStorableProperty<S>> identityProperties =
info.getIdentityProperties();
for (JDBCStorableProperty<S> prop : mAllProperties.values()) {
if (!prop.isSelectable()) {
continue;
}
if (prop.isAutomatic() && !identityProperties.containsKey(prop.getName())) {
// Might still need to reload. This could be determined
// dynamically, but this is an optimization that can be
// implemented later.
// TODO: perform dynamic check with RELOAD_SOMETIMES
reload = RELOAD_ALWAYS;
break honorSuppression;
}
if (prop.isVersion() && mVersioning == Versioning.EXTERNAL) {
// Always need to reload for version.
reload = RELOAD_ALWAYS;
break honorSuppression;
}
if (isLobType(prop)) {
reload = RELOAD_SOMETIMES;
}
}
if (reload == RELOAD_NEVER) {
features.remove(MasterFeature.INSERT_TXN);
features.remove(MasterFeature.UPDATE_TXN);
}
}
}
mReload = reload;
final Class<? extends S> abstractClass =
MasterStorableGenerator.getAbstractClass(mStorableType, features);
mParentClassLoader = abstractClass.getClassLoader();
mClassInjector = ClassInjector.create(mStorableType.getName(), mParentClassLoader);
mClassFile = new ClassFile(mClassInjector.getClassName(), abstractClass);
mClassFile.markSynthetic();
mClassFile.setSourceFile(JDBCStorableGenerator.class.getName());
mClassFile.setTarget("1.5");
}
private Class<? extends S> generateAndInjectClass() throws SupportException {
// We'll need these "inner classes" which serve as Lob loading
// callbacks. Lob's need to be reloaded if the original transaction has
// been committed.
final Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap = generateLobLoaders();
// Declare some types.
final TypeDesc jdbcSupportType = TypeDesc.forClass(JDBCSupport.class);
final TypeDesc resultSetType = TypeDesc.forClass(ResultSet.class);
final TypeDesc connectionType = TypeDesc.forClass(Connection.class);
final TypeDesc preparedStatementType = TypeDesc.forClass(PreparedStatement.class);
final TypeDesc lobArrayType = TypeDesc.forClass(Lob.class).toArrayType();
final TypeDesc masterSupportType = TypeDesc.forClass(MasterSupport.class);
final TypeDesc classType = TypeDesc.forClass(Class.class);
if (lobLoaderMap.size() > 0) {
// Add static initializer to save references to Lob
// loaders. Otherwise, classes might get unloaded before they are
// used for the first time.
MethodInfo mi = mClassFile.addInitializer();
CodeBuilder b = new CodeBuilder(mi);
int i = 0;
for (Class<?> loaderClass : lobLoaderMap.values()) {
String fieldName = LOB_LOADER_FIELD_PREFIX + i;
mClassFile.addField
(Modifiers.PRIVATE.toStatic(true).toFinal(true), fieldName, classType);
b.loadConstant(TypeDesc.forClass(loaderClass));
b.storeStaticField(fieldName, classType);
i++;
}
b.returnVoid();
}
// Add constructor that accepts a JDBCSupport.
{
TypeDesc[] params = {jdbcSupportType};
MethodInfo mi = mClassFile.addConstructor(Modifiers.PUBLIC, params);
CodeBuilder b = new CodeBuilder(mi);
b.loadThis();
b.loadLocal(b.getParameter(0));
b.checkCast(masterSupportType);
b.invokeSuperConstructor(new TypeDesc[] {masterSupportType});
b.returnVoid();
}
// Add constructor that accepts a JDBCSupport and a ResultSet row.
{
TypeDesc[] params = {jdbcSupportType, resultSetType, TypeDesc.INT};
MethodInfo mi = mClassFile.addConstructor(Modifiers.PUBLIC, params);
CodeBuilder b = new CodeBuilder(mi);
b.loadThis();
b.loadLocal(b.getParameter(0));
b.checkCast(masterSupportType);
b.invokeSuperConstructor(new TypeDesc[] {masterSupportType});
// Call extractAll method to fill in properties.
b.loadThis();
b.loadLocal(b.getParameter(1));
b.loadLocal(b.getParameter(2));
b.invokePrivate(EXTRACT_ALL_METHOD_NAME, null,
new TypeDesc[] {resultSetType, TypeDesc.INT});
// Indicate load completed in order to mark properties as valid and
// invoke load triggers.
b.loadThis();
b.invokeVirtual(StorableGenerator.LOAD_COMPLETED_METHOD_NAME, null, null);
b.returnVoid();
}
CodeBuilderUtil.definePrepareMethod(mClassFile, mStorableType, jdbcSupportType);
// Add private method to extract all properties from a ResultSet row.
defineExtractAllMethod(lobLoaderMap);
// Add private method to extract non-pk properties from a ResultSet row.
defineExtractDataMethod(lobLoaderMap);
// For all unsupported properties, override get/set method to throw
// UnsupportedOperationException. Special treatment given if property
// is also Nullable. Get returns null and set only allows null.
{
for (JDBCStorableProperty<S> property : mAllProperties.values()) {
if (property.isDerived() || property.isJoin() || property.isSupported()) {
continue;
}
CodeBuilder b;
Class exClass = UnsupportedOperationException.class;
String message = "Independent property \"" + property.getName() +
"\" is not supported by the SQL schema: ";
if (property.isNullable()) {
b = new CodeBuilder(mClassFile.addMethod(property.getReadMethod()));
b.loadNull();
b.returnValue(TypeDesc.OBJECT);
b = new CodeBuilder(mClassFile.addMethod(property.getWriteMethod()));
b.loadLocal(b.getParameter(0));
Label notNull = b.createLabel();
b.ifNullBranch(notNull, false);
b.returnVoid();
notNull.setLocation();
CodeBuilderUtil.throwException(b, exClass, message);
} else {
message += mInfo.getTableName();
b = new CodeBuilder(mClassFile.addMethod(property.getReadMethod()));
CodeBuilderUtil.throwException(b, exClass, message);
b = new CodeBuilder(mClassFile.addMethod(property.getWriteMethod()));
CodeBuilderUtil.throwException(b, exClass, message);
}
}
}
// Add required protected doTryLoad method.
{
MethodInfo mi = mClassFile.addMethod
(Modifiers.PROTECTED,
MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null);
mi.addException(TypeDesc.forClass(FetchException.class));
CodeBuilder b = new CodeBuilder(mi);
LocalVariable supportVar = getJDBCSupport(b);
Label tryBeforeCon = b.createLabel().setLocation();
LocalVariable conVar = getConnection(b, supportVar);
Label tryAfterCon = b.createLabel().setLocation();
b.loadThis();
b.loadLocal(supportVar);
b.loadLocal(conVar);
b.loadNull(); // No Lobs to update
b.invokeVirtual(MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME,
TypeDesc.BOOLEAN,
new TypeDesc[] {jdbcSupportType, connectionType, lobArrayType});
LocalVariable resultVar = b.createLocalVariable("result", TypeDesc.BOOLEAN);
b.storeLocal(resultVar);
yieldConAndHandleException(b, supportVar, tryBeforeCon, conVar, tryAfterCon, false);
b.loadLocal(resultVar);
b.returnValue(TypeDesc.BOOLEAN);
}
// Now define doTryLoad(JDBCSupport, Connection, Lob[]). The Lob array argument
// is optional, and it indicates which (large) Lobs should be updated upon load.
{
MethodInfo mi = mClassFile.addMethod
(Modifiers.PROTECTED,
MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME, TypeDesc.BOOLEAN,
new TypeDesc[] {jdbcSupportType, connectionType, lobArrayType});
mi.addException(TypeDesc.forClass(Exception.class));
CodeBuilder b = new CodeBuilder(mi);
StringBuilder selectBuilder = null;
for (JDBCStorableProperty<S> property : mAllProperties.values()) {
// Along with unsupported properties and joins, primary keys are not loaded.
// This is because they are included in the where clause.
if (!property.isSelectable() || property.isPrimaryKeyMember()) {
continue;
}
if (selectBuilder == null) {
selectBuilder = new StringBuilder();
selectBuilder.append("SELECT ");
} else {
selectBuilder.append(',');
}
selectBuilder.append(property.getColumnName());
}
if (selectBuilder == null) {
// All properties are pks. A select still needs to be
// performed, but just discard the results. The select needs to
// be performed in order to verify that a record exists, since
// we need to return true or false.
selectBuilder = new StringBuilder();
selectBuilder.append("SELECT ");
selectBuilder.append
(mInfo.getPrimaryKeyProperties().values().iterator().next().getColumnName());
}
selectBuilder.append(" FROM ");
selectBuilder.append(mInfo.getQualifiedTableName());
LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
Label tryAfterPs = buildWhereClauseAndPreparedStatement
(b, selectBuilder, b.getParameter(1), psVar, b.getParameter(0), null);
b.loadLocal(psVar);
b.invokeInterface(preparedStatementType, "executeQuery", resultSetType, null);
LocalVariable rsVar = b.createLocalVariable("rs", resultSetType);
b.storeLocal(rsVar);
Label tryAfterRs = b.createLabel().setLocation();
// If no results, then return false. Otherwise, there must be
// exactly one result.
LocalVariable resultVar = b.createLocalVariable("result", TypeDesc.BOOLEAN);
b.loadLocal(rsVar);
b.invokeInterface(resultSetType, "next", TypeDesc.BOOLEAN, null);
b.storeLocal(resultVar);
b.loadLocal(resultVar);
Label noResults = b.createLabel();
b.ifZeroComparisonBranch(noResults, "==");
b.loadThis();
b.loadLocal(rsVar);
b.loadConstant(1);
b.loadLocal(b.getParameter(2)); // Pass Lobs to update
b.invokePrivate(EXTRACT_DATA_METHOD_NAME, null,
new TypeDesc[] {resultSetType, TypeDesc.INT, lobArrayType});
noResults.setLocation();
closeResultSet(b, rsVar, tryAfterRs);
closeStatement(b, psVar, tryAfterPs);
b.loadLocal(resultVar);
b.returnValue(TypeDesc.BOOLEAN);
}
// Unlike the other methods, doTryInsert is allowed to throw an
// SQLException. Override insert and tryInsert to catch SQLException.
// The tryInsert method must also decide if it is a unique constraint
// exception and returns false instead. This design allows the original
// SQLException to be passed with the UniqueConstraintException,
// providing more context.
// Override insert method.
{
MethodInfo mi = mClassFile.addMethod
(Modifiers.PUBLIC, INSERT_METHOD_NAME, null, null);
mi.addException(TypeDesc.forClass(PersistException.class));
CodeBuilder b = new CodeBuilder(mi);
Label tryStart = b.createLabel().setLocation();
b.loadThis();
b.invokeSuper(mClassFile.getSuperClassName(), INSERT_METHOD_NAME, null, null);
Label tryEnd = b.createLabel().setLocation();
b.returnVoid();
b.exceptionHandler(tryStart, tryEnd, RuntimeException.class.getName());
b.throwObject();
b.exceptionHandler(tryStart, tryEnd, Exception.class.getName());
pushJDBCSupport(b);
// Swap exception object and JDBCSupport instance.
b.swap();
TypeDesc[] params = {TypeDesc.forClass(Throwable.class)};
b.invokeInterface(jdbcSupportType, "toPersistException",
TypeDesc.forClass(PersistException.class), params);
b.throwObject();
}
// Override tryInsert method.
{
MethodInfo mi = mClassFile.addMethod
(Modifiers.PUBLIC, TRY_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, null);
mi.addException(TypeDesc.forClass(PersistException.class));
CodeBuilder b = new CodeBuilder(mi);
Label tryStart = b.createLabel().setLocation();
b.loadThis();
b.invokeSuper(mClassFile.getSuperClassName(),
TRY_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, null);
Label innerTryEnd = b.createLabel().setLocation();
b.returnValue(TypeDesc.BOOLEAN);
b.exceptionHandler(tryStart, innerTryEnd, SQLException.class.getName());
b.dup(); // dup the SQLException
pushJDBCSupport(b);
b.swap(); // swap the dup'ed SQLException to pass to method
b.invokeInterface(jdbcSupportType, "isUniqueConstraintError",
TypeDesc.BOOLEAN,
new TypeDesc[] {TypeDesc.forClass(SQLException.class)});
Label notConstraint = b.createLabel();
b.ifZeroComparisonBranch(notConstraint, "==");
// Return false to indicate unique constraint violation.
b.loadConstant(false);
b.returnValue(TypeDesc.BOOLEAN);
notConstraint.setLocation();
// Re-throw SQLException, since it is not a unique constraint violation.
b.throwObject();
Label outerTryEnd = b.createLabel().setLocation();
b.exceptionHandler(tryStart, outerTryEnd, RuntimeException.class.getName());
b.throwObject();
b.exceptionHandler(tryStart, outerTryEnd, Exception.class.getName());
pushJDBCSupport(b);
// Swap exception object and JDBCSupport instance.
b.swap();
TypeDesc[] params = {TypeDesc.forClass(Throwable.class)};
b.invokeInterface(jdbcSupportType, "toPersistException",
TypeDesc.forClass(PersistException.class), params);
b.throwObject();
}
// Add required protected doTryInsert method.
{
MethodInfo mi = mClassFile.addMethod
(Modifiers.PROTECTED,
MasterStorableGenerator.DO_TRY_INSERT_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null);
mi.addException(TypeDesc.forClass(PersistException.class));
CodeBuilder b = new CodeBuilder(mi);
LocalVariable supportVar = getJDBCSupport(b);
LocalVariable conVar = getConnection(b, supportVar);
Label tryAfterCon = b.createLabel().setLocation();
// Push connection in preparation for preparing a statement.
b.loadLocal(conVar);
String staticInsertStatement;
{
// Build the full static insert statement, even though it might
// not be used. If not used, then the length of the full static
// statement is used to determine the initial buffer size of
// the dynamically generated statement.
StringBuilder sb = new StringBuilder();
sb.append("INSERT INTO ");
sb.append(mInfo.getQualifiedTableName());
sb.append(" ( ");
int ordinal = 0;
for (JDBCStorableProperty<?> property : mAllProperties.values()) {
if (!property.isSelectable()) {
continue;
}
if (ordinal > 0) {
sb.append(',');
}
sb.append(property.getColumnName());
ordinal++;
}
sb.append(" ) VALUES (");
for (int i=0; i<ordinal; i++) {
if (i > 0) {
sb.append(',');
}
sb.append('?');
}
sb.append(')');
staticInsertStatement = sb.toString();
}
boolean useStaticInsertStatement = true;
for (JDBCStorableProperty<?> property : mAllProperties.values()) {
if (!property.isDerived()) {
if (property.isVersion() || property.isAutomatic()) {
useStaticInsertStatement = false;
break;
}
}
}
// Count of inserted properties when using dynamically generated statement.
LocalVariable insertCountVar = null;
if (useStaticInsertStatement) {
// Load static insert statement to stack.
b.loadConstant(staticInsertStatement);
} else {
// Dynamically build insert statement, ignoring automatic and
// version properties which are not DIRTY.
insertCountVar = b.createLocalVariable(null, TypeDesc.INT);
int initialCount = 0;
for (JDBCStorableProperty<?> property : mAllProperties.values()) {
if (!property.isSelectable()) {
continue;
}
if (isAlwaysInserted(property)) {
// Don't bother dynamically counting properties which
// will always be inserted.
initialCount++;
}
}
b.loadConstant(initialCount);
b.storeLocal(insertCountVar);
TypeDesc stringBuilderType = TypeDesc.forClass(StringBuilder.class);
b.newObject(stringBuilderType);
b.dup();
b.loadConstant(staticInsertStatement.length());
b.invokeConstructor(stringBuilderType, new TypeDesc[] {TypeDesc.INT});
// Note extra space after left paren. This is required for case
// where no properties are explicitly inserted. The logic below
// to blindly delete the last character with a (thinking it is
// a comma) causes no harm when there are no properties.
b.loadConstant("INSERT INTO " + mInfo.getQualifiedTableName() + " ( ");
CodeBuilderUtil.callStringBuilderAppendString(b);
int propNumber = -1;
for (JDBCStorableProperty<?> property : mAllProperties.values()) {
propNumber++;
if (!property.isSelectable()) {
continue;
}
Label nextProperty = b.createLabel();
if (!isAlwaysInserted(property)) {
// Property is set only if value manually supplied.
branchIfDirty(b, propNumber, nextProperty, false);
b.integerIncrement(insertCountVar, 1);
}
// Append property name (with trailing comma) to StringBuilder.
b.loadConstant(property.getColumnName() + ',');
CodeBuilderUtil.callStringBuilderAppendString(b);
nextProperty.setLocation();
}
// Blindly delete last character, assuming it is a trailing comma.
LocalVariable sbVar = b.createLocalVariable(null, stringBuilderType);
b.storeLocal(sbVar);
b.loadLocal(sbVar);
b.loadLocal(sbVar);
CodeBuilderUtil.callStringBuilderLength(b);
b.loadConstant(1);
b.math(Opcode.ISUB);
CodeBuilderUtil.callStringBuilderSetLength(b);
b.loadLocal(sbVar); // Load StringBuilder to stack as before.
b.loadConstant(" ) VALUES (");
CodeBuilderUtil.callStringBuilderAppendString(b);
// Append all the necessary question marks.
b.loadLocal(insertCountVar);
Label finishStatement = b.createLabel();
b.ifZeroComparisonBranch(finishStatement, "<=");
b.loadConstant('?');
CodeBuilderUtil.callStringBuilderAppendChar(b);
Label loopStart = b.createLabel().setLocation();
b.integerIncrement(insertCountVar, -1);
b.loadLocal(insertCountVar);
b.ifZeroComparisonBranch(finishStatement, "<=");
b.loadConstant(",?");
CodeBuilderUtil.callStringBuilderAppendString(b);
b.branch(loopStart);
finishStatement.setLocation();
b.loadConstant(')');
CodeBuilderUtil.callStringBuilderAppendChar(b);
CodeBuilderUtil.callStringBuilderToString(b);
}
// At this point, the stack contains a connection and a complete
// SQL insert statement String.
// Determine if generated keys need to be retrieved.
Collection<JDBCStorableProperty<S>> identityProperties =
mInfo.getIdentityProperties().values();
LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
if (identityProperties.isEmpty()) {
b.invokeInterface(connectionType, "prepareStatement", preparedStatementType,
new TypeDesc[] {TypeDesc.STRING});
} else {
b.loadConstant(Statement.RETURN_GENERATED_KEYS);
b.invokeInterface(connectionType, "prepareStatement", preparedStatementType,
new TypeDesc[] {TypeDesc.STRING, TypeDesc.INT});
}
b.storeLocal(psVar);
Label tryAfterPs = b.createLabel().setLocation();
// Now fill in parameters with property values.
// Gather all Lob properties to track if a post-insert update is required.
Map<JDBCStorableProperty<S>, Integer> lobIndexMap = findLobs();
LocalVariable lobArrayVar = null;
if (lobIndexMap.size() != 0) {
// Create array to track which Lobs are too large and need extra work.
lobArrayVar = b.createLocalVariable(null, lobArrayType);
b.loadConstant(lobIndexMap.size());
b.newObject(lobArrayType);
b.storeLocal(lobArrayVar);
}
int ordinal = 0;
LocalVariable ordinalVar = null;
if (!useStaticInsertStatement) {
// Increment parameter ordinal at runtime.
ordinalVar = b.createLocalVariable(null, TypeDesc.INT);
b.loadConstant(0);
b.storeLocal(ordinalVar);
}
int propNumber = -1;
for (JDBCStorableProperty<S> property : mAllProperties.values()) {
propNumber++;
if (!property.isSelectable()) {
continue;
}
Label nextProperty = b.createLabel();
if (!isAlwaysInserted(property)) {
// Property is set only if value manually supplied.
branchIfDirty(b, propNumber, nextProperty, false);
}
b.loadLocal(psVar);
if (ordinalVar == null) {
b.loadConstant(++ordinal);
} else {
b.integerIncrement(ordinalVar, 1);
b.loadLocal(ordinalVar);
}
Label setNormally = b.createLabel();
if (property.isVersion() && mVersioning == Versioning.AUTO) {
// Automatically supply initial value unless manually supplied.
branchIfDirty(b, propNumber, setNormally, true);
setPreparedStatementValue
(b, property, INITIAL_VERSION,
null, lobArrayVar, lobIndexMap.get(property));
b.branch(nextProperty);
}
setNormally.setLocation();
setPreparedStatementValue
(b, property, NORMAL, null, lobArrayVar, lobIndexMap.get(property));
nextProperty.setLocation();
}
// Execute the statement.
b.loadLocal(psVar);
b.invokeInterface(preparedStatementType, "executeUpdate", TypeDesc.INT, null);
b.pop();
if (identityProperties.size() > 0) {
// Get the generated keys and set the properties.
b.loadLocal(psVar);
b.invokeInterface(preparedStatementType, "getGeneratedKeys", resultSetType, null);
LocalVariable rsVar = b.createLocalVariable("rs", resultSetType);
b.storeLocal(rsVar);
Label tryAfterRs = b.createLabel().setLocation();
b.loadLocal(rsVar);
b.invokeInterface(resultSetType, "next", TypeDesc.BOOLEAN, null);
Label noResults = b.createLabel();
b.ifZeroComparisonBranch(noResults, "==");
// Set property value.
LocalVariable initialOffsetVar = b.createLocalVariable(null, TypeDesc.INT);
b.loadConstant(1);
b.storeLocal(initialOffsetVar);
defineExtract(b, rsVar, initialOffsetVar, null, // no lobArrayVar
mInfo.getIdentityProperties().values(),
lobLoaderMap);
noResults.setLocation();
closeResultSet(b, rsVar, tryAfterRs);
}
closeStatement(b, psVar, tryAfterPs);
if (mReload != RELOAD_NEVER) {
Label reloaded = b.createLabel();
if (lobArrayVar != null && mReload == RELOAD_SOMETIMES) {
// Dynamically determine if reload should be performed, because a Lob is
// too large and needs to be updated.
Label doReload = b.createLabel();
for (int i=0; i<lobIndexMap.size(); i++) {
b.loadLocal(lobArrayVar);
b.loadConstant(i);
b.loadFromArray(TypeDesc.OBJECT);
b.ifNullBranch(doReload, false);
}
b.branch(reloaded);
doReload.setLocation();
}
// Immediately reload object, to ensure that any database supplied
// default values are properly retrieved. Since INSERT_TXN is
// enabled, superclass ensures that transaction is still in
// progress at this point.
b.loadThis();
b.loadLocal(supportVar);
b.loadLocal(conVar);
if (lobArrayVar == null) {
b.loadNull();
} else {
b.loadLocal(lobArrayVar);
}
b.invokeVirtual(MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME,
TypeDesc.BOOLEAN,
new TypeDesc[] {jdbcSupportType, connectionType, lobArrayType});
b.ifZeroComparisonBranch(reloaded, "!=");
String message = "Reload after insert failed, " +
"possibly because database changed the primary key: ";
for (JDBCStorableProperty<S> prop : mInfo.getPrimaryKeyProperties().values()) {
Class type = prop.getType();
if (Date.class.isAssignableFrom(type) ||
ReadableInstant.class.isAssignableFrom(type))
{
message += "Property type of date may have been truncated: " +
prop.getName() + ": ";
}
}
TypeDesc persistExType = TypeDesc.forClass(PersistException.class);
b.newObject(persistExType);
b.dup();
b.loadConstant(message);
b.loadThis();
b.invokeVirtual(TO_STRING_KEY_ONLY_METHOD_NAME, TypeDesc.STRING, null);
b.invokeVirtual(TypeDesc.STRING, "concat",
TypeDesc.STRING, new TypeDesc[] {TypeDesc.STRING});
b.invokeConstructor(persistExType, new TypeDesc[] {TypeDesc.STRING});
b.throwObject();
reloaded.setLocation();
}
// Note: yieldConAndHandleException is not called, allowing any
// SQLException to be thrown. The insert or tryInsert methods must handle it.
yieldCon(b, supportVar, conVar, tryAfterCon);
b.loadConstant(true);
b.returnValue(TypeDesc.BOOLEAN);
}
// Add required protected doTryUpdate method.
{
MethodInfo mi = mClassFile.addMethod
(Modifiers.PROTECTED,
MasterStorableGenerator.DO_TRY_UPDATE_MASTER_METHOD_NAME,
TypeDesc.BOOLEAN, null);
mi.addException(TypeDesc.forClass(PersistException.class));
CodeBuilder b = new CodeBuilder(mi);
// Only update properties with state DIRTY. Therefore, update
// statement is always dynamic.
LocalVariable supportVar = getJDBCSupport(b);
Label tryBeforeCon = b.createLabel().setLocation();
LocalVariable conVar = getConnection(b, supportVar);
Label tryAfterCon = b.createLabel().setLocation();
// Load connection in preparation for creating statement.
b.loadLocal(conVar);
TypeDesc stringBuilderType = TypeDesc.forClass(StringBuilder.class);
b.newObject(stringBuilderType);
b.dup();
b.loadConstant(INITIAL_UPDATE_BUFFER_SIZE);
b.invokeConstructor(stringBuilderType, new TypeDesc[] {TypeDesc.INT});
{
StringBuilder sqlBuilder = new StringBuilder();
sqlBuilder.append("UPDATE ");
sqlBuilder.append(mInfo.getQualifiedTableName());
sqlBuilder.append(" SET ");
b.loadConstant(sqlBuilder.toString());
// Method leaves StringBuilder on stack.
CodeBuilderUtil.callStringBuilderAppendString(b);
}
// Iterate over the properties, appending a set parameter for each
// that is dirty.
LocalVariable countVar = b.createLocalVariable("count", TypeDesc.INT);
b.loadConstant(0);
b.storeLocal(countVar);
int propNumber = -1;
for (JDBCStorableProperty property : mAllProperties.values()) {
propNumber++;
if (property.isSelectable() && !property.isPrimaryKeyMember()) {
if (property.isVersion() && mVersioning == Versioning.EXTERNAL) {
// Assume database trigger manages version.
continue;
}
Label isNotDirty = null;
if (!property.isVersion() || mVersioning != Versioning.AUTO) {
// Auto version must always be updated, but all other
// properties are updated only if dirty.
isNotDirty = b.createLabel();
branchIfDirty(b, propNumber, isNotDirty, false);
}
b.loadLocal(countVar);
Label isZero = b.createLabel();
b.ifZeroComparisonBranch(isZero, "==");
b.loadConstant(',');
CodeBuilderUtil.callStringBuilderAppendChar(b);
isZero.setLocation();
b.loadConstant(property.getColumnName());
CodeBuilderUtil.callStringBuilderAppendString(b);
b.loadConstant("=?");
CodeBuilderUtil.callStringBuilderAppendString(b);
b.integerIncrement(countVar, 1);
if (isNotDirty != null) {
isNotDirty.setLocation();
}
}
}
Collection<JDBCStorableProperty<S>> whereProperties =
mInfo.getPrimaryKeyProperties().values();
JDBCStorableProperty<S> versionProperty = mInfo.getVersionProperty();
if (versionProperty != null) {
if (!versionProperty.isSelectable() || mVersioning == Versioning.NONE) {
versionProperty = null;
} else {
// Include version property in WHERE clause to support optimistic locking.
List<JDBCStorableProperty<S>> list =
new ArrayList<JDBCStorableProperty<S>>(whereProperties);
list.add(versionProperty);
whereProperties = list;
}
}
// If no dirty properties, a valid update statement must still be
// created. Just update the first "where" property to itself.
{
b.loadLocal(countVar);
Label notZero = b.createLabel();
b.ifZeroComparisonBranch(notZero, "!=");
b.loadConstant(whereProperties.iterator().next().getColumnName());
CodeBuilderUtil.callStringBuilderAppendString(b);
b.loadConstant("=?");
CodeBuilderUtil.callStringBuilderAppendString(b);
notZero.setLocation();
}
b.loadConstant(" WHERE ");
CodeBuilderUtil.callStringBuilderAppendString(b);
// FIXME: Code duplication. "appendDynamicWhereClauseProperties"
int ordinal = 0;
for (JDBCStorableProperty<S> property : whereProperties) {
if (ordinal > 0) {
b.loadConstant(" AND ");
CodeBuilderUtil.callStringBuilderAppendString(b);
}
b.loadConstant(property.getColumnName());
CodeBuilderUtil.callStringBuilderAppendString(b);
Label next = b.createLabel();
if (property.isNullable()) {
// Determine if property value is null.
b.loadThis();
// FIXME: Does not consider property adapter
b.loadField(property.getName(), TypeDesc.forClass(property.getType()));
Label notNull = b.createLabel();
b.ifNullBranch(notNull, false);
b.loadConstant(" IS NULL");
CodeBuilderUtil.callStringBuilderAppendString(b);
b.branch(next);
notNull.setLocation();
}
b.loadConstant("=?");
CodeBuilderUtil.callStringBuilderAppendString(b);
next.setLocation();
ordinal++;
}
// Convert StringBuilder value to a String.
CodeBuilderUtil.callStringBuilderToString(b);
// At this point, the stack contains a connection and a SQL
// statement String.
LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
b.invokeInterface(connectionType, "prepareStatement", preparedStatementType,
new TypeDesc[] {TypeDesc.STRING});
b.storeLocal(psVar);
Label tryAfterPs = b.createLabel().setLocation();
// Walk through dirty properties again, setting values on statement.
LocalVariable indexVar = b.createLocalVariable("index", TypeDesc.INT);
// First prepared statement index is always one, so says JDBC.
b.loadConstant(1);
b.storeLocal(indexVar);
// Gather all Lob properties to track if a post-update update is required.
Map<JDBCStorableProperty<S>, Integer> lobIndexMap = findLobs();
LocalVariable lobArrayVar = null;
if (lobIndexMap.size() != 0) {
// Create array to track which Lobs are too large and need extra work.
lobArrayVar = b.createLocalVariable(null, lobArrayType);
b.loadConstant(lobIndexMap.size());
b.newObject(lobArrayType);
b.storeLocal(lobArrayVar);
}
// If no dirty properties, fill in extra property from before.
{
b.loadLocal(countVar);
Label notZero = b.createLabel();
b.ifZeroComparisonBranch(notZero, "!=");
JDBCStorableProperty property = whereProperties.iterator().next();
b.loadLocal(psVar);
b.loadLocal(indexVar);
setPreparedStatementValue
(b, property, NORMAL, null, lobArrayVar, lobIndexMap.get(property));
b.integerIncrement(indexVar, 1);
notZero.setLocation();
}
propNumber = -1;
for (JDBCStorableProperty property : mAllProperties.values()) {
propNumber++;
if (property.isSelectable() && !property.isPrimaryKeyMember()) {
if (property.isVersion() && mVersioning == Versioning.EXTERNAL) {
// Assume database trigger manages version.
continue;
}
Label isNotDirty = null;
if (!property.isVersion() || mVersioning != Versioning.AUTO) {
// Auto version must always be updated, but all other
// properties are updated only if dirty.
isNotDirty = b.createLabel();
branchIfDirty(b, propNumber, isNotDirty, false);
}
b.loadLocal(psVar);
b.loadLocal(indexVar);
int mode = (property.isVersion() && mVersioning == Versioning.AUTO) ?
INCREMENT_VERSION : NORMAL;
setPreparedStatementValue
(b, property, mode,
null, lobArrayVar, lobIndexMap.get(property));
b.integerIncrement(indexVar, 1);
if (isNotDirty != null) {
isNotDirty.setLocation();
}
}
}
// Walk through where clause properties again, setting values on
// statement.
for (JDBCStorableProperty<S> property : whereProperties) {
Label nextProperty = b.createLabel();
if (property.isNullable()) {
// If runtime value of property is null, then where clause
// was built with "IS NULL".
b.loadThis();
// FIXME: Does not consider property adapter
b.loadField(property.getName(), TypeDesc.forClass(property.getType()));
b.ifNullBranch(nextProperty, true);
}
b.loadLocal(psVar);
b.loadLocal(indexVar);
setPreparedStatementValue(b, property, NOT_NULL, null, null, null);
b.integerIncrement(indexVar, 1);
nextProperty.setLocation();
}
// Execute the update statement.
b.loadLocal(psVar);
LocalVariable updateCount = b.createLocalVariable("updateCount", TypeDesc.INT);
b.invokeInterface(preparedStatementType, "executeUpdate", TypeDesc.INT, null);
b.storeLocal(updateCount);
closeStatement(b, psVar, tryAfterPs);
Label doReload = b.createLabel();
Label skipReload = b.createLabel();
if (versionProperty == null) {
b.loadLocal(updateCount);
b.ifZeroComparisonBranch(skipReload, "==");
} else {
// If update count is zero, either the record was deleted or
// the version doesn't match. To distinguish these two cases,
// select record version. If not found, return
// false. Otherwise, throw OptimisticLockException.
b.loadLocal(updateCount);
b.ifZeroComparisonBranch(doReload, "!=");
StringBuilder selectBuilder = new StringBuilder();
selectBuilder.append("SELECT ");
selectBuilder.append(versionProperty.getColumnName());
selectBuilder.append(" FROM ");
selectBuilder.append(mInfo.getQualifiedTableName());
LocalVariable countPsVar = b.createLocalVariable("ps", preparedStatementType);
Label tryAfterCountPs = buildWhereClauseAndPreparedStatement
(b, selectBuilder, conVar, countPsVar, null, null);
b.loadLocal(countPsVar);
b.invokeInterface(preparedStatementType, "executeQuery", resultSetType, null);
LocalVariable rsVar = b.createLocalVariable("rs", resultSetType);
b.storeLocal(rsVar);
Label tryAfterRs = b.createLabel().setLocation();
b.loadLocal(rsVar);
b.invokeInterface(resultSetType, "next", TypeDesc.BOOLEAN, null);
// Record missing, return false.
b.ifZeroComparisonBranch(skipReload, "==");
b.loadLocal(rsVar);
b.loadConstant(1); // column 1
b.invokeInterface(resultSetType, "getLong",
TypeDesc.LONG, new TypeDesc[] {TypeDesc.INT});
LocalVariable actualVersion = b.createLocalVariable(null, TypeDesc.LONG);
b.storeLocal(actualVersion);
closeResultSet(b, rsVar, tryAfterRs);
closeStatement(b, countPsVar, tryAfterCountPs);
// Throw exception.
{
TypeDesc desc = TypeDesc.forClass(OptimisticLockException.class);
b.newObject(desc);
b.dup();
b.loadThis();
// Pass expected version number for exception message.
TypeDesc propertyType = TypeDesc.forClass(versionProperty.getType());
b.loadField(versionProperty.getName(), propertyType);
b.convert(propertyType, TypeDesc.LONG.toObjectType());
b.loadLocal(actualVersion);
b.convert(TypeDesc.LONG, TypeDesc.LONG.toObjectType());
b.invokeConstructor(desc, new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT});
b.throwObject();
}
}
doReload.setLocation();
if (mReload != RELOAD_NEVER) {
if (lobArrayVar != null && mReload == RELOAD_SOMETIMES) {
// Dynamically determine if reload should be performed, because a Lob is
// too large and needs to be updated.
Label reallyDoReload = b.createLabel();
for (int i=0; i<lobIndexMap.size(); i++) {
b.loadLocal(lobArrayVar);
b.loadConstant(i);
b.loadFromArray(TypeDesc.OBJECT);
b.ifNullBranch(reallyDoReload, false);
}
b.branch(skipReload);
reallyDoReload.setLocation();
}
// Immediately reload object, to ensure that any database supplied
// default values are properly retrieved. Since UPDATE_TXN is
// enabled, superclass ensures that transaction is still in
// progress at this point.
b.loadThis();
b.loadLocal(supportVar);
b.loadLocal(conVar);
if (lobArrayVar == null) {
b.loadNull();
} else {
b.loadLocal(lobArrayVar);
}
b.invokeVirtual(MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME,
TypeDesc.BOOLEAN,
new TypeDesc[] {jdbcSupportType, connectionType, lobArrayType});
// Even though a boolean is returned, the actual value for true and
// false is an int, 1 or 0.
b.storeLocal(updateCount);
}
skipReload.setLocation();
yieldConAndHandleException(b, supportVar, tryBeforeCon, conVar, tryAfterCon, true);
b.loadLocal(updateCount);
b.convert(TypeDesc.INT, TypeDesc.BOOLEAN);
b.returnValue(TypeDesc.BOOLEAN);
}
// Add required protected doTryDelete method.
{
MethodInfo mi = mClassFile.addMethod
(Modifiers.PROTECTED,
MasterStorableGenerator.DO_TRY_DELETE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null);
mi.addException(TypeDesc.forClass(PersistException.class));
CodeBuilder b = new CodeBuilder(mi);
StringBuilder deleteBuilder = new StringBuilder();
deleteBuilder.append("DELETE FROM ");
deleteBuilder.append(mInfo.getQualifiedTableName());
LocalVariable supportVar = getJDBCSupport(b);
Label tryBeforeCon = b.createLabel().setLocation();
LocalVariable conVar = getConnection(b, supportVar);
Label tryAfterCon = b.createLabel().setLocation();
LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
Label tryAfterPs = buildWhereClauseAndPreparedStatement
(b, deleteBuilder, conVar, psVar, null, null);
b.loadLocal(psVar);
b.invokeInterface(preparedStatementType, "executeUpdate", TypeDesc.INT, null);
// Return false if count is zero, true otherwise. Just return the
// int as if it were boolean.
LocalVariable resultVar = b.createLocalVariable("result", TypeDesc.INT);
b.storeLocal(resultVar);
closeStatement(b, psVar, tryAfterPs);
yieldConAndHandleException(b, supportVar, tryBeforeCon, conVar, tryAfterCon, true);
b.loadLocal(resultVar);
b.convert(TypeDesc.INT, TypeDesc.BOOLEAN);
b.returnValue(TypeDesc.BOOLEAN);
}
Class<? extends S> generatedClass = mClassInjector.defineClass(mClassFile);
// Touch lobLoaderMap to ensure reference to these classes are kept
// until after storable class is generated. Otherwise, these classes
// might get unloaded.
lobLoaderMap.size();
return generatedClass;
}
/**
* Returns true if property value is always part of insert statement.
*/
private boolean isAlwaysInserted(JDBCStorableProperty<?> property) {
return property.isVersion() ? (mVersioning == Versioning.AUTO) : !property.isAutomatic();
}
/**
* Finds all Lob properties and maps them to a zero-based index. This
* information is used to update large Lobs after an insert or update.
*/
private Map<JDBCStorableProperty<S>, Integer> findLobs() {
Map<JDBCStorableProperty<S>, Integer> lobIndexMap =
new IdentityHashMap<JDBCStorableProperty<S>, Integer>();
int lobIndex = 0;
for (JDBCStorableProperty<S> property : mAllProperties.values()) {
if (isLobType(property)) {
lobIndexMap.put(property, lobIndex++);
}
}
return lobIndexMap;
}
private static boolean isLobType(JDBCStorableProperty property) {
if (!property.isSelectable() || property.isVersion()) {
return false;
}
if (Lob.class.isAssignableFrom(property.getType())) {
return true;
}
Class psClass = property.getPreparedStatementSetMethod().getParameterTypes()[1];
return java.sql.Blob.class.isAssignableFrom(psClass)
|| java.sql.Clob.class.isAssignableFrom(psClass);
}
/**
* Generates code to get the JDBCSupport instance and store it in a local
* variable.
*/
private LocalVariable getJDBCSupport(CodeBuilder b) {
pushJDBCSupport(b);
LocalVariable supportVar =
b.createLocalVariable("support", TypeDesc.forClass(JDBCSupport.class));
b.storeLocal(supportVar);
return supportVar;
}
/**
* Generates code to push the JDBCSupport instance on the stack.
*/
private void pushJDBCSupport(CodeBuilder b) {
b.loadThis();
b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, TypeDesc.forClass(TriggerSupport.class));
b.checkCast(TypeDesc.forClass(JDBCSupport.class));
}
/**
* Generates code to get connection from JDBCConnectionCapability and store
* it in a local variable.
*
* @param capVar reference to JDBCConnectionCapability
*/
private LocalVariable getConnection(CodeBuilder b, LocalVariable capVar) {
b.loadLocal(capVar);
b.invokeInterface(TypeDesc.forClass(JDBCConnectionCapability.class),
"getConnection", TypeDesc.forClass(Connection.class), null);
LocalVariable conVar = b.createLocalVariable("con", TypeDesc.forClass(Connection.class));
b.storeLocal(conVar);
return conVar;
}
/**
* Generates code which emulates this:
*
* // May throw FetchException
* JDBCConnectionCapability.yieldConnection(con);
*
* @param capVar required reference to JDBCConnectionCapability
* @param conVar optional connection to yield
*/
private void yieldConnection(CodeBuilder b, LocalVariable capVar, LocalVariable conVar) {
if (conVar != null) {
b.loadLocal(capVar);
b.loadLocal(conVar);
b.invokeInterface(TypeDesc.forClass(JDBCConnectionCapability.class),
"yieldConnection", null,
new TypeDesc[] {TypeDesc.forClass(Connection.class)});
}
}
/**
* Generates code that finishes the given SQL statement by appending a
* WHERE clause. Prepared statement is then created and all parameters are
* filled in.
*
* @param sqlBuilder contains SQL statement right before the WHERE clause
* @param conVar local variable referencing connection
* @param psVar declared local variable which will receive PreparedStatement
* @param capVar when non-null, check transaction if SELECT should be FOR UPDATE
* @param instanceVar when null, assume properties are contained in
* "this". Otherwise, invoke property access methods on storable referenced
* in var.
* @return label right after prepared statement was created, which is to be
* used as the start of a try block that ensures the prepared statement is
* closed.
*/
private Label buildWhereClauseAndPreparedStatement
(CodeBuilder b,
StringBuilder sqlBuilder,
LocalVariable conVar,
LocalVariable psVar,
LocalVariable capVar,
LocalVariable instanceVar)
throws SupportException
{
final TypeDesc superType = TypeDesc.forClass(mClassFile.getSuperClassName());
final Iterable<? extends JDBCStorableProperty<?>> properties =
mInfo.getPrimaryKeyProperties().values();
sqlBuilder.append(" WHERE ");
List<JDBCStorableProperty> nullableProperties = new ArrayList<JDBCStorableProperty>();
int ordinal = 0;
for (JDBCStorableProperty property : properties) {
if (!property.isSelectable()) {
continue;
}
if (property.isNullable()) {
// Nullable properties need to alter the SQL where clause
// syntax at runtime, taking the forms "=?" or "IS NULL".
nullableProperties.add(property);
continue;
}
if (ordinal > 0) {
sqlBuilder.append(" AND ");
}
sqlBuilder.append(property.getColumnName());
sqlBuilder.append("=?");
ordinal++;
}
// Push connection in preparation for preparing a statement.
b.loadLocal(conVar);
if (nullableProperties.isEmpty()) {
b.loadConstant(sqlBuilder.toString());
// Determine at runtime if SELECT should be " FOR UPDATE".
if (capVar != null) {
b.loadLocal(capVar);
b.invokeInterface(TypeDesc.forClass(JDBCConnectionCapability.class),
"isTransactionForUpdate", TypeDesc.BOOLEAN, null);
Label notForUpdate = b.createLabel();
b.ifZeroComparisonBranch(notForUpdate, "==");
b.loadConstant(" FOR UPDATE");
b.invokeVirtual(TypeDesc.STRING, "concat",
TypeDesc.STRING, new TypeDesc[] {TypeDesc.STRING});
notForUpdate.setLocation();
}
} else {
// Finish select statement at runtime, since we don't know if the
// properties are null or not.
if (ordinal > 0) {
sqlBuilder.append(" AND ");
}
// Make runtime buffer capacity large enough to hold all "IS NULL" phrases.
int capacity = sqlBuilder.length() + 7 * nullableProperties.size();
if (nullableProperties.size() > 1) {
// Account for all the appended " AND " phrases.
capacity += 5 * (nullableProperties.size() - 1);
}
for (JDBCStorableProperty property : nullableProperties) {
// Account for property names.
capacity += property.getColumnName().length();
}
TypeDesc stringBuilderType = TypeDesc.forClass(StringBuilder.class);
b.newObject(stringBuilderType);
b.dup();
b.loadConstant(capacity);
b.invokeConstructor(stringBuilderType, new TypeDesc[] {TypeDesc.INT});
b.loadConstant(sqlBuilder.toString());
// Method leaves StringBuilder on stack.
CodeBuilderUtil.callStringBuilderAppendString(b);
// FIXME: Code duplication. "appendDynamicWhereClauseProperties"
ordinal = 0;
for (JDBCStorableProperty property : nullableProperties) {
if (ordinal > 0) {
b.loadConstant(" AND ");
CodeBuilderUtil.callStringBuilderAppendString(b);
}
b.loadConstant(property.getColumnName());
CodeBuilderUtil.callStringBuilderAppendString(b);
b.loadThis();
final TypeDesc propertyType = TypeDesc.forClass(property.getType());
// FIXME: Does not consider property adapter
b.loadField(superType, property.getName(), propertyType);
Label notNull = b.createLabel();
b.ifNullBranch(notNull, false);
b.loadConstant(" IS NULL");
CodeBuilderUtil.callStringBuilderAppendString(b);
Label next = b.createLabel();
b.branch(next);
notNull.setLocation();
b.loadConstant("=?");
CodeBuilderUtil.callStringBuilderAppendString(b);
next.setLocation();
ordinal++;
}
// Determine at runtime if SELECT should be " FOR UPDATE".
if (capVar != null) {
b.loadLocal(capVar);
b.invokeInterface(TypeDesc.forClass(JDBCConnectionCapability.class),
"isTransactionForUpdate", TypeDesc.BOOLEAN, null);
Label notForUpdate = b.createLabel();
b.ifZeroComparisonBranch(notForUpdate, "==");
b.loadConstant(" FOR UPDATE");
CodeBuilderUtil.callStringBuilderAppendString(b);
notForUpdate.setLocation();
}
// Convert StringBuilder to String.
CodeBuilderUtil.callStringBuilderToString(b);
}
// At this point, the stack contains a connection and a SQL statement String.
final TypeDesc connectionType = TypeDesc.forClass(Connection.class);
final TypeDesc preparedStatementType = TypeDesc.forClass(PreparedStatement.class);
b.invokeInterface(connectionType, "prepareStatement", preparedStatementType,
new TypeDesc[] {TypeDesc.STRING});
b.storeLocal(psVar);
Label tryAfterPs = b.createLabel().setLocation();
// Now set where clause parameters. First pass, set non-nullable properties.
ordinal = 0;
for (JDBCStorableProperty property : properties) {
if (!property.isSelectable() || property.isNullable()) {
continue;
}
ordinal++;
b.loadLocal(psVar);
b.loadConstant(ordinal);
setPreparedStatementValue(b, property, NOT_NULL, instanceVar, null, null);
}
// Now set nullable properties.
if (!nullableProperties.isEmpty()) {
ordinal++;
LocalVariable indexVar;
if (nullableProperties.size() == 1) {
indexVar = null;
} else {
indexVar = b.createLocalVariable(null, TypeDesc.INT);
b.loadConstant(ordinal);
b.storeLocal(indexVar);
}
Iterator<JDBCStorableProperty> it = nullableProperties.iterator();
while (true) {
JDBCStorableProperty property = it.next();
// Nullable properties are dynamically added to where clause,
// and are at the end of the prepared statement. If value is
// null, then skip to the next property, since the statement
// was appended earlier with "IS NULL".
TypeDesc propertyType = TypeDesc.forClass(property.getType());
b.loadThis();
// FIXME: Does not consider property adapter
b.loadField(superType, property.getName(), propertyType);
Label nextProperty = b.createLabel();
b.ifNullBranch(nextProperty, true);
b.loadLocal(psVar);
if (indexVar == null) {
b.loadConstant(ordinal);
} else {
b.loadLocal(indexVar);
}
setPreparedStatementValue(b, property, NOT_NULL, instanceVar, null, null);
if (it.hasNext()) {
if (indexVar != null) {
b.integerIncrement(indexVar, 1);
}
nextProperty.setLocation();
} else {
nextProperty.setLocation();
break;
}
}
}
return tryAfterPs;
}
/**
* Generates code to call a PreparedStatement.setXxx(int, Xxx) method, with
* the value of the given property. Assumes that PreparedStatement and int
* index are on the stack.
*
* If the property is a Lob, then pass in the optional lobTooLargeVar to
* track if it was too large to insert/update. The type of lobTooLargeVar
* must be the carbonado lob type. At runtime, if the variable's value is
* not null, then lob was too large to insert. The value of the variable is
* the original lob. An update statement needs to be issued after the load
* to insert/update the large value.
*
* @param mode one of NORMAL, NOT_NULL, INITIAL_VERSION or INCREMENT_VERSION
* @param instanceVar when null, assume properties are contained in
* "this". Otherwise, invoke property access methods on storable referenced
* in var.
* @param lobArrayVar optional, used for lob properties
* @param lobIndex optional, used for lob properties
*/
private void setPreparedStatementValue
(CodeBuilder b,
JDBCStorableProperty<?> property,
int mode,
LocalVariable instanceVar,
LocalVariable lobArrayVar,
Integer lobIndex)
throws SupportException
{
Class psClass = property.getPreparedStatementSetMethod().getParameterTypes()[1];
TypeDesc psType = TypeDesc.forClass(psClass);
TypeDesc propertyType = TypeDesc.forClass(property.getType());
StorablePropertyAdapter adapter = property.getAppliedAdapter();
if (mode != INITIAL_VERSION) {
// Load storable to extract property value from.
if (instanceVar == null) {
b.loadThis();
} else {
b.loadLocal(instanceVar);
}
}
TypeDesc fromType;
if (adapter == null) {
if (mode != INITIAL_VERSION) {
// Get protected field directly, since no adapter.
if (instanceVar == null) {
b.loadField(property.getName(), propertyType);
} else {
b.loadField(instanceVar.getType(), property.getName(), propertyType);
}
}
fromType = propertyType;
} else {
Class toClass = psClass;
if (java.sql.Blob.class.isAssignableFrom(toClass)) {
toClass = com.amazon.carbonado.lob.Blob.class;
} else if (java.sql.Clob.class.isAssignableFrom(toClass)) {
toClass = com.amazon.carbonado.lob.Clob.class;
}
Method adaptMethod = adapter.findAdaptMethod(property.getType(), toClass);
if (adaptMethod == null) {
if (toClass == String.class) {
// Check if special case for converting character to String.
adaptMethod = adapter.findAdaptMethod(property.getType(), char.class);
if (adaptMethod == null) {
adaptMethod = adapter.findAdaptMethod
(property.getType(), Character.class);
}
}
if (adaptMethod == null) {
throw new SupportException
("Unable to adapt " + property.getType() + " to " + toClass.getName());
}
}
TypeDesc adaptType = TypeDesc.forClass(adaptMethod.getReturnType());
if (mode != INITIAL_VERSION) {
// Invoke special inherited protected method that gets the field
// and invokes the adapter. Method was generated by
// StorableGenerator.
String methodName = property.getReadMethodName() + '$';
if (instanceVar == null) {
b.invokeVirtual(methodName, adaptType, null);
} else {
b.invokeVirtual (instanceVar.getType(), methodName, adaptType, null);
}
}
fromType = adaptType;
}
Label done = b.createLabel();
if (mode == INITIAL_VERSION) {
CodeBuilderUtil.initialVersion(b, fromType, 1);
} else if (mode == INCREMENT_VERSION) {
CodeBuilderUtil.incrementVersion(b, fromType);
} else if (!fromType.isPrimitive() && mode != NOT_NULL) {
// Handle case where property value is null.
b.dup();
Label notNull = b.createLabel();
b.ifNullBranch(notNull, false);
// Invoke setNull method instead.
b.pop(); // discard duplicate null.
b.loadConstant(property.getDataType());
b.invokeInterface(TypeDesc.forClass(PreparedStatement.class), "setNull",
null, new TypeDesc[] {TypeDesc.INT, TypeDesc.INT});
b.branch(done);
notNull.setLocation();
}
if (Lob.class.isAssignableFrom(fromType.toClass())) {
// Run special conversion.
LocalVariable lobVar = b.createLocalVariable(null, fromType);
b.storeLocal(lobVar);
LocalVariable columnVar = b.createLocalVariable(null, TypeDesc.INT);
b.storeLocal(columnVar);
LocalVariable psVar = b.createLocalVariable
("ps", TypeDesc.forClass(PreparedStatement.class));
b.storeLocal(psVar);
if (lobArrayVar != null && lobIndex != null) {
// Prepare for update result. If too large, then array entry is not null.
b.loadLocal(lobArrayVar);
b.loadConstant(lobIndex);
}
pushJDBCSupport(b);
b.loadLocal(psVar);
b.loadLocal(columnVar);
b.loadLocal(lobVar);
// Stack looks like this: JDBCSupport, PreparedStatement, int (column), Lob
Method setValueMethod;
try {
String name = fromType.toClass().getName();
name = "set" + name.substring(name.lastIndexOf('.') + 1) + "Value";
setValueMethod = JDBCSupport.class.getMethod
(name, PreparedStatement.class, int.class, fromType.toClass());
} catch (NoSuchMethodException e) {
throw new UndeclaredThrowableException(e);
}
b.invoke(setValueMethod);
if (lobArrayVar == null || lobIndex == null) {
b.pop();
} else {
b.storeToArray(TypeDesc.OBJECT);
}
} else {
if (psType == TypeDesc.STRING && fromType.toPrimitiveType() == TypeDesc.CHAR) {
// Special case for converting character to String.
b.convert(fromType, fromType.toPrimitiveType());
b.invokeStatic(String.class.getName(), "valueOf",
TypeDesc.STRING, new TypeDesc[] {TypeDesc.CHAR});
} else {
b.convert(fromType, psType);
}
b.invoke(property.getPreparedStatementSetMethod());
}
done.setLocation();
}
/**
* Generates code which emulates this:
*
* ...
* } finally {
* JDBCConnectionCapability.yieldConnection(con);
* }
*
* @param capVar required reference to JDBCConnectionCapability
* @param conVar optional connection variable
* @param tryAfterCon label right after connection acquisition
*/
private void yieldCon
(CodeBuilder b,
LocalVariable capVar,
LocalVariable conVar,
Label tryAfterCon)
{
Label endFinallyLabel = b.createLabel().setLocation();
Label contLabel = b.createLabel();
yieldConnection(b, capVar, conVar);
b.branch(contLabel);
b.exceptionHandler(tryAfterCon, endFinallyLabel, null);
yieldConnection(b, capVar, conVar);
b.throwObject();
contLabel.setLocation();
}
/**
* Generates code which emulates this:
*
* ...
* } finally {
* JDBCConnectionCapability.yieldConnection(con);
* }
* } catch (RuntimeException e) {
* throw e;
* } catch (Exception e) {
* throw JDBCConnectionCapability.toFetchException(e);
* }
*
* @param capVar required reference to JDBCConnectionCapability
* @param txnVar optional transaction variable to commit/exit
* @param tryBeforeCon label right before connection acquisition
* @param conVar optional connection variable
* @param tryAfterCon label right after connection acquisition
*/
private void yieldConAndHandleException
(CodeBuilder b,
LocalVariable capVar,
Label tryBeforeCon, LocalVariable conVar, Label tryAfterCon,
boolean forPersist)
{
Label endFinallyLabel = b.createLabel().setLocation();
Label contLabel = b.createLabel();
yieldConnection(b, capVar, conVar);
b.branch(contLabel);
b.exceptionHandler(tryAfterCon, endFinallyLabel, null);
yieldConnection(b, capVar, conVar);
b.throwObject();
b.exceptionHandler
(tryBeforeCon, b.createLabel().setLocation(), RuntimeException.class.getName());
b.throwObject();
b.exceptionHandler
(tryBeforeCon, b.createLabel().setLocation(), Exception.class.getName());
b.loadLocal(capVar);
// Swap exception object and JDBCConnectionCapability instance.
b.swap();
TypeDesc[] params = {TypeDesc.forClass(Throwable.class)};
if (forPersist) {
b.invokeInterface(TypeDesc.forClass(JDBCConnectionCapability.class),
"toPersistException",
TypeDesc.forClass(PersistException.class), params);
} else {
b.invokeInterface(TypeDesc.forClass(JDBCConnectionCapability.class),
"toFetchException",
TypeDesc.forClass(FetchException.class), params);
}
b.throwObject();
contLabel.setLocation();
}
/**
* Generates code which emulates this:
*
* ...
* } finally {
* statement.close();
* }
*
* @param statementVar Statement variable
* @param tryAfterStatement label right after Statement acquisition
*/
private void closeStatement
(CodeBuilder b, LocalVariable statementVar, Label tryAfterStatement)
{
Label contLabel = b.createLabel();
Label endFinallyLabel = b.createLabel().setLocation();
b.loadLocal(statementVar);
b.invokeInterface(TypeDesc.forClass(Statement.class), "close", null, null);
b.branch(contLabel);
b.exceptionHandler(tryAfterStatement, endFinallyLabel, null);
b.loadLocal(statementVar);
b.invokeInterface(TypeDesc.forClass(Statement.class), "close", null, null);
b.throwObject();
contLabel.setLocation();
}
/**
* Generates code which emulates this:
*
* ...
* } finally {
* rs.close();
* }
*
* @param rsVar ResultSet variable
* @param tryAfterRs label right after ResultSet acquisition
*/
private void closeResultSet
(CodeBuilder b, LocalVariable rsVar, Label tryAfterRs)
{
Label contLabel = b.createLabel();
Label endFinallyLabel = b.createLabel().setLocation();
b.loadLocal(rsVar);
b.invokeInterface(TypeDesc.forClass(ResultSet.class), "close", null, null);
b.branch(contLabel);
b.exceptionHandler(tryAfterRs, endFinallyLabel, null);
b.loadLocal(rsVar);
b.invokeInterface(TypeDesc.forClass(ResultSet.class), "close", null, null);
b.throwObject();
contLabel.setLocation();
}
/**
* Generates code to branch if a property is dirty.
*
* @param propNumber property number from all properties map
* @param target branch target
* @param when true, branch if dirty; when false, branch when not dirty
*/
private void branchIfDirty(CodeBuilder b, int propNumber,
Label target, boolean branchIfDirty)
{
String stateFieldName = StorableGenerator.PROPERTY_STATE_FIELD_NAME + (propNumber >> 4);
b.loadThis();
b.loadField(stateFieldName, TypeDesc.INT);
int shift = (propNumber & 0xf) * 2;
b.loadConstant(StorableGenerator.PROPERTY_STATE_MASK << shift);
b.math(Opcode.IAND);
b.loadConstant(StorableGenerator.PROPERTY_STATE_DIRTY << shift);
b.ifComparisonBranch(target, branchIfDirty ? "==" : "!=");
}
private void defineExtractAllMethod(Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap)
throws SupportException
{
MethodInfo mi = mClassFile.addMethod
(Modifiers.PRIVATE, EXTRACT_ALL_METHOD_NAME, null,
new TypeDesc[] {TypeDesc.forClass(ResultSet.class), TypeDesc.INT});
CodeBuilder b = new CodeBuilder(mi);
defineExtract(b, b.getParameter(0), b.getParameter(1), null,
mInfo.getPrimaryKeyProperties().values(), lobLoaderMap);
// Invoke extract data method to do the rest.
b.loadThis();
// Load the ResultSet var.
b.loadLocal(b.getParameter(0));
// The offset variable has already been incremented by code generated
// by defineExtract, except for the last property.
b.loadLocal(b.getParameter(1));
b.loadConstant(1);
b.math(Opcode.IADD);
b.loadNull(); // No Lobs to update
b.invokePrivate(EXTRACT_DATA_METHOD_NAME, null,
new TypeDesc[] {TypeDesc.forClass(ResultSet.class), TypeDesc.INT,
TypeDesc.forClass(Lob.class).toArrayType()});
b.returnVoid();
}
private void defineExtractDataMethod(Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap)
throws SupportException
{
MethodInfo mi = mClassFile.addMethod
(Modifiers.PRIVATE, EXTRACT_DATA_METHOD_NAME, null,
new TypeDesc[] {TypeDesc.forClass(ResultSet.class), TypeDesc.INT,
TypeDesc.forClass(Lob.class).toArrayType()});
CodeBuilder b = new CodeBuilder(mi);
defineExtract(b, b.getParameter(0), b.getParameter(1), b.getParameter(2),
mInfo.getDataProperties().values(), lobLoaderMap);
b.returnVoid();
}
private void defineExtract
(CodeBuilder b,
LocalVariable rsVar, LocalVariable initialOffsetVar, LocalVariable lobArrayVar,
Iterable<JDBCStorableProperty<S>> properties,
Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap)
throws SupportException
{
LocalVariable offsetVar = null;
int lobIndex = 0;
for (JDBCStorableProperty<S> property : properties) {
if (!property.isSelectable()) {
continue;
}
// Push this in preparation for calling setXxx method.
b.loadThis();
b.loadLocal(rsVar);
if (offsetVar == null) {
offsetVar = initialOffsetVar;
} else {
b.integerIncrement(offsetVar, 1);
}
b.loadLocal(offsetVar);
Method resultSetGetMethod = property.getResultSetGetMethod();
b.invoke(resultSetGetMethod);
TypeDesc resultSetType = TypeDesc.forClass(resultSetGetMethod.getReturnType());
Label wasNull = b.createLabel();
if (resultSetType.isPrimitive() && property.isColumnNullable()) {
b.loadLocal(rsVar);
b.invokeInterface
(TypeDesc.forClass(ResultSet.class), "wasNull", TypeDesc.BOOLEAN, null);
Label wasNotNull = b.createLabel();
// boolean value is false (==0) when was not null.
b.ifZeroComparisonBranch(wasNotNull, "==");
// Discard result and replace with null.
if (resultSetType.isDoubleWord()) {
b.pop2();
} else {
b.pop();
}
b.loadNull();
b.branch(wasNull);
wasNotNull.setLocation();
}
if (Lob.class.isAssignableFrom(property.getType()) ||
java.sql.Blob.class.isAssignableFrom(resultSetType.toClass()) ||
java.sql.Clob.class.isAssignableFrom(resultSetType.toClass())) {
// Run special conversion and then lie about the result set type.
boolean isClob =
com.amazon.carbonado.lob.Clob.class.isAssignableFrom(property.getType()) ||
java.sql.Clob.class.isAssignableFrom(resultSetType.toClass());
String lobTypeName = isClob ? "Clob" : "Blob";
Method convertMethod;
try {
String loaderName =
"com.amazon.carbonado.repo.jdbc.JDBC" + lobTypeName + "Loader";
convertMethod = JDBCSupport.class.getMethod
("convert".concat(lobTypeName),
resultSetType.toClass(), Class.forName(loaderName));
} catch (ClassNotFoundException e) {
throw new UndeclaredThrowableException(e);
} catch (NoSuchMethodException e) {
throw new UndeclaredThrowableException(e);
}
pushJDBCSupport(b);
b.swap();
// Instantiate loader, which may be used later to reload the
// lob. Loader is passed to convert method, where it is saved
// inside the converted lob for future use.
TypeDesc lobLoaderType = TypeDesc.forClass(lobLoaderMap.get(property));
b.newObject(lobLoaderType);
b.dup();
b.loadThis();
b.invokeConstructor(lobLoaderType, new TypeDesc[] {mClassFile.getType()});
b.invoke(convertMethod);
resultSetType = TypeDesc.forClass(convertMethod.getReturnType());
if (lobArrayVar != null) {
// Add code to check if Lob needs to be updated.
b.loadLocal(lobArrayVar);
Label noUpdateLob = b.createLabel();
b.ifNullBranch(noUpdateLob, true);
b.loadLocal(lobArrayVar);
b.loadConstant(lobIndex);
b.loadFromArray(TypeDesc.OBJECT);
b.ifNullBranch(noUpdateLob, true);
// The Lob in the array represents the new value. What is
// currently on the stack (as converted above) is the old
// value currently in the database. Call the JDBCSupport
// updateXlob method, which stuffs the new blob contents
// into the old blob, thus updating it.
TypeDesc lobType = TypeDesc.forClass(convertMethod.getReturnType());
LocalVariable lob = b.createLocalVariable(null, lobType);
b.storeLocal(lob);
pushJDBCSupport(b);
b.loadLocal(lob);
b.loadLocal(lobArrayVar);
b.loadConstant(lobIndex);
b.loadFromArray(TypeDesc.OBJECT);
b.checkCast(lobType);
TypeDesc[] params = {lobType, lobType};
b.invokeInterface(TypeDesc.forClass(JDBCSupport.class),
"update".concat(lobTypeName), null, params);
// Lob content now updated.
b.loadLocal(lob);
noUpdateLob.setLocation();
lobIndex++;
}
}
TypeDesc superType = TypeDesc.forClass(mClassFile.getSuperClassName());
StorablePropertyAdapter adapter = property.getAppliedAdapter();
if (adapter == null) {
TypeDesc propertyType = TypeDesc.forClass(property.getType());
convertFromResultSet(b, property, resultSetType, propertyType);
wasNull.setLocation();
// Set protected field directly, since no adapter.
b.storeField(superType, property.getName(), propertyType);
} else {
Method adaptMethod = adapter.findAdaptMethod
(resultSetType.toClass(), property.getType());
if (adaptMethod == null) {
if (resultSetType == TypeDesc.STRING) {
// Check if special case for converting String to character.
adaptMethod = adapter.findAdaptMethod(char.class, property.getType());
if (adaptMethod == null) {
adaptMethod = adapter.findAdaptMethod
(Character.class, property.getType());
}
}
if (adaptMethod == null) {
throw new SupportException
("Unable to adapt " +
resultSetType.toClass().getName() + " to " + property.getType());
}
}
TypeDesc adaptType = TypeDesc.forClass(adaptMethod.getParameterTypes()[0]);
convertFromResultSet(b, property, resultSetType, adaptType);
wasNull.setLocation();
// Invoke special inherited protected method that invokes the
// adapter and sets the field. Method was generated by StorableGenerator.
b.invokeVirtual(superType,
property.getWriteMethodName() + '$',
null, new TypeDesc[] {adaptType});
}
}
}
private void convertFromResultSet(CodeBuilder b,
JDBCStorableProperty<S> property,
TypeDesc resultSetType,
TypeDesc toType)
{
if (resultSetType == TypeDesc.STRING && toType.toPrimitiveType() == TypeDesc.CHAR) {
// Special case for converting String to character.
Label charWasNull = null;
if (property.isNullable()) {
charWasNull = b.createLabel();
LocalVariable temp = b.createLocalVariable(null, resultSetType);
b.storeLocal(temp);
b.loadLocal(temp);
b.ifNullBranch(charWasNull, true);
b.loadLocal(temp);
}
b.loadConstant(0);
b.invokeVirtual(String.class.getName(), "charAt",
TypeDesc.CHAR, new TypeDesc[] {TypeDesc.INT});
b.convert(TypeDesc.CHAR, toType);
if (charWasNull != null) {
Label skipNull = b.createLabel();
b.branch(skipNull);
charWasNull.setLocation();
b.loadNull();
skipNull.setLocation();
}
} else {
b.convert(resultSetType, toType);
}
}
private Map<JDBCStorableProperty<S>, Class<?>> generateLobLoaders()
throws SupportException
{
Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap =
new IdentityHashMap<JDBCStorableProperty<S>, Class<?>>();
for (JDBCStorableProperty<S> property : mAllProperties.values()) {
if (!property.isSelectable() || property.isVersion()) {
continue;
}
Class psClass = property.getPreparedStatementSetMethod().getParameterTypes()[1];
Class<?> lobLoader;
if (com.amazon.carbonado.lob.Blob.class.isAssignableFrom(property.getType()) ||
java.sql.Blob.class.isAssignableFrom(psClass)) {
lobLoader = generateLobLoader(property, JDBCBlobLoader.class);
} else if (com.amazon.carbonado.lob.Clob.class.isAssignableFrom(property.getType()) ||
java.sql.Clob.class.isAssignableFrom(psClass)) {
lobLoader = generateLobLoader(property, JDBCClobLoader.class);
} else {
continue;
}
lobLoaderMap.put(property, lobLoader);
}
return lobLoaderMap;
}
/**
* Generates an inner class conforming to JDBCBlobLoader or JDBCClobLoader.
*
* @param loaderType either JDBCBlobLoader or JDBCClobLoader
*/
private Class<?> generateLobLoader(JDBCStorableProperty<S> property, Class<?> loaderType)
throws SupportException
{
ClassInjector ci = ClassInjector.create
(property.getEnclosingType().getName(), mParentClassLoader);
ClassFile cf = new ClassFile(ci.getClassName());
cf.markSynthetic();
cf.setSourceFile(JDBCStorableGenerator.class.getName());
cf.setTarget("1.5");
cf.addInterface(loaderType);
boolean isClob = loaderType == JDBCClobLoader.class;
final TypeDesc capType = TypeDesc.forClass(JDBCConnectionCapability.class);
final TypeDesc resultSetType = TypeDesc.forClass(ResultSet.class);
final TypeDesc preparedStatementType = TypeDesc.forClass(PreparedStatement.class);
final TypeDesc sqlLobType = TypeDesc.forClass
(isClob ? java.sql.Clob.class : java.sql.Blob.class);
final String enclosingFieldName = "enclosing";
final TypeDesc enclosingType = mClassFile.getType();
cf.addField(Modifiers.PRIVATE, enclosingFieldName, enclosingType);
// Add constructor that accepts reference to enclosing storable.
{
MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, new TypeDesc[] {enclosingType});
CodeBuilder b = new CodeBuilder(mi);
b.loadThis();
b.invokeSuperConstructor(null);
b.loadThis();
b.loadLocal(b.getParameter(0));
b.storeField(enclosingFieldName, enclosingType);
b.returnVoid();
}
MethodInfo mi = cf.addMethod
(Modifiers.PUBLIC, "load", sqlLobType, new TypeDesc[] {capType});
mi.addException(TypeDesc.forClass(FetchException.class));
CodeBuilder b = new CodeBuilder(mi);
LocalVariable capVar = b.getParameter(0);
Label tryBeforeCon = b.createLabel().setLocation();
LocalVariable conVar = getConnection(b, capVar);
Label tryAfterCon = b.createLabel().setLocation();
StringBuilder selectBuilder = new StringBuilder();
selectBuilder.append("SELECT ");
selectBuilder.append(property.getColumnName());
selectBuilder.append(" FROM ");
selectBuilder.append(mInfo.getQualifiedTableName());
LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
LocalVariable instanceVar = b.createLocalVariable(null, enclosingType);
b.loadThis();
b.loadField(enclosingFieldName, enclosingType);
b.storeLocal(instanceVar);
Label tryAfterPs = buildWhereClauseAndPreparedStatement
(b, selectBuilder, conVar, psVar, capVar, instanceVar);
b.loadLocal(psVar);
b.invokeInterface(preparedStatementType, "executeQuery", resultSetType, null);
LocalVariable rsVar = b.createLocalVariable("rs", resultSetType);
b.storeLocal(rsVar);
Label tryAfterRs = b.createLabel().setLocation();
// If no results, then return null. Otherwise, there must be exactly
// one result.
LocalVariable resultVar = b.createLocalVariable(null, sqlLobType);
b.loadNull();
b.storeLocal(resultVar);
b.loadLocal(rsVar);
b.invokeInterface(resultSetType, "next", TypeDesc.BOOLEAN, null);
Label noResults = b.createLabel();
b.ifZeroComparisonBranch(noResults, "==");
b.loadLocal(rsVar);
b.loadConstant(1);
b.invokeInterface(resultSetType, isClob ? "getClob" : "getBlob",
sqlLobType, new TypeDesc[] {TypeDesc.INT});
b.storeLocal(resultVar);
noResults.setLocation();
closeResultSet(b, rsVar, tryAfterRs);
closeStatement(b, psVar, tryAfterPs);
yieldConAndHandleException(b, capVar, tryBeforeCon, conVar, tryAfterCon, false);
b.loadLocal(resultVar);
b.returnValue(sqlLobType);
return ci.defineClass(cf);
}
}