From 641a7812d439daca3045e7471b604beb807c1891 Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Sun, 1 Apr 2007 00:00:07 +0000 Subject: Move Storable code generation support to separate package. --- .../com/amazon/carbonado/gen/CodeBuilderUtil.java | 536 +++ .../amazon/carbonado/gen/CommonMethodNames.java | 87 + .../com/amazon/carbonado/gen/MasterFeature.java | 59 + .../carbonado/gen/MasterStorableGenerator.java | 835 +++++ .../com/amazon/carbonado/gen/MasterSupport.java | 39 + .../amazon/carbonado/gen/StorableGenerator.java | 3671 ++++++++++++++++++++ .../amazon/carbonado/gen/StorableSerializer.java | 337 ++ .../com/amazon/carbonado/gen/StorableSupport.java | 41 + .../com/amazon/carbonado/gen/TriggerSupport.java | 50 + .../com/amazon/carbonado/gen/WrappedSupport.java | 75 + .../com/amazon/carbonado/gen/package-info.java | 22 + 11 files changed, 5752 insertions(+) create mode 100644 src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java create mode 100644 src/main/java/com/amazon/carbonado/gen/CommonMethodNames.java create mode 100644 src/main/java/com/amazon/carbonado/gen/MasterFeature.java create mode 100644 src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java create mode 100644 src/main/java/com/amazon/carbonado/gen/MasterSupport.java create mode 100644 src/main/java/com/amazon/carbonado/gen/StorableGenerator.java create mode 100644 src/main/java/com/amazon/carbonado/gen/StorableSerializer.java create mode 100644 src/main/java/com/amazon/carbonado/gen/StorableSupport.java create mode 100644 src/main/java/com/amazon/carbonado/gen/TriggerSupport.java create mode 100644 src/main/java/com/amazon/carbonado/gen/WrappedSupport.java create mode 100644 src/main/java/com/amazon/carbonado/gen/package-info.java (limited to 'src/main/java/com/amazon/carbonado/gen') diff --git a/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java b/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java new file mode 100644 index 0000000..c1f26a0 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java @@ -0,0 +1,536 @@ +/* + * Copyright 2006 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.gen; + +import java.util.HashSet; +import java.util.Set; +import java.util.Map; +import java.util.HashMap; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Label; +import org.cojen.classfile.TypeDesc; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodDesc; +import org.cojen.classfile.Opcode; +import org.cojen.util.ClassInjector; + +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; + +import static com.amazon.carbonado.gen.CommonMethodNames.*; + +/** + * Collection of useful utilities for generating Carbonado code. + * + * @author Don Schneider + * @author Brian S O'Neill + */ +public class CodeBuilderUtil { + + /** + * Generate code to throw an exception if a parameter is null + * @param b CodeBuilder into which to append the code + * @param paramIndex index of the parameter to check + */ + public static void assertParameterNotNull(CodeBuilder b, int paramIndex) { + b.loadLocal(b.getParameter(paramIndex)); + Label notNull = b.createLabel(); + b.ifNullBranch(notNull, false); + throwException(b, IllegalArgumentException.class, null); + notNull.setLocation(); + } + + /** + * Generate code to create a local variable containing the specified parameter coerced + * to the specified type. This is useful for re-interpreting erased generics into + * the more specific genericized type. + * + * @param b CodeBuilder into which to append the code + * @param paramType the more specific type which was erased during compilation + * @param paramIndex index of the parameter to unerase + * @return a local variable referencing the type-cast parameter + */ + public static LocalVariable uneraseGenericParameter( + CodeBuilder b, TypeDesc paramType, final int paramIndex) + { + b.loadLocal(b.getParameter(paramIndex)); + b.checkCast(paramType); + LocalVariable result = b.createLocalVariable(null, paramType); + b.storeLocal(result); + return result; + } + + /** + * Generate code to throw an exception with an optional message. + * @param b {@link CodeBuilder} to which to add code + * @param type type of the object to throw + * @param message optional message to provide to the constructor + */ + public static void throwException(CodeBuilder b, Class type, String message) { + TypeDesc desc = TypeDesc.forClass(type); + b.newObject(desc); + b.dup(); + if (message == null) { + b.invokeConstructor(desc, null); + } else { + b.loadConstant(message); + b.invokeConstructor(desc, new TypeDesc[] {TypeDesc.STRING}); + } + b.throwObject(); + } + + /** + * Collect a set of all the interfaces and recursively all superclasses for the leaf + * (genericised class) and root (genericised base class). Eg, for Object, all + * classes and implemented interfaces for every superclass between foo (the leaf) and + * Object (the base). + *

A copy must be coercible into any of these types, and copy bridge methods must be + * provided to do so. + * + *

Note that the official documentation for this is in draft form, and you have to be + * psychic to have figured out the necessity in the first place. + * + * @param set set into which the class types will be collected + * @param leaf leaf class + * @return same set as was passed in + */ + public static Set gatherAllBridgeTypes(Set set, Class leaf) { + set.add(leaf); + for (Class c : leaf.getInterfaces()) { + gatherAllBridgeTypes(set, c); + } + if ((leaf = leaf.getSuperclass()) != null) { + gatherAllBridgeTypes(set, leaf); + } + return set; + } + + /** + * Add copy bridge methods for all classes/interfaces between the leaf (genericised class) + * and the root (genericised baseclass). + * + * @param cf file to which to add the copy bridge + * @param leaf leaf class + */ + public static void defineCopyBridges(ClassFile cf, Class leaf) { + for (Class c : gatherAllBridgeTypes(new HashSet(), leaf)) { + if (c != Object.class) { + defineCopyBridge(cf, leaf, c); + } + } + } + + /** + * Add a copy bridge method to the classfile for the given type. This is needed to allow + * the genericised class make a copy itself -- which will be erased to the base type -- and + * return it as the correct type. + * + * @param cf file to which to add the copy bridge + * @param leaf leaf class + * @param returnClass type returned from generated bridge method + */ + private static void defineCopyBridge(ClassFile cf, Class leaf, Class returnClass) { + TypeDesc returnType = TypeDesc.forClass(returnClass); + + if (isPublicMethodFinal(leaf, COPY_METHOD_NAME, returnType, null)) { + // Cannot override. + return; + } + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC.toBridge(true), + COPY_METHOD_NAME, returnType, null); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.invokeVirtual(COPY_METHOD_NAME, cf.getType(), null); + b.returnValue(returnType); + } + + /** + * Returns true if a public final method exists which matches the given + * specification. + */ + public static boolean isPublicMethodFinal(Class clazz, String name, + TypeDesc retType, TypeDesc[] params) + { + if (!clazz.isInterface()) { + Class[] paramClasses; + if (params == null || params.length == 0) { + paramClasses = null; + } else { + paramClasses = new Class[params.length]; + for (int i=0; i + *

  • implements Storable
  • + *
  • implements Cloneable + *
  • abstract if appropriate + *
  • marked synthetic + *
  • targetted for java version 1.5 + * + * @param ci ClassInjector for the storable + * @param type specific Storable implementation to generate + * @param isAbstract true if the class should be abstract + * @param aSourcefileName identifier for the classfile, typically the factory class name + * @return ClassFile object ready to have methods added. + */ + public static ClassFile createStorableClassFile( + ClassInjector ci, Class type, boolean isAbstract, String aSourcefileName) + { + ClassFile cf; + if (type.isInterface()) { + cf = new ClassFile(ci.getClassName()); + cf.addInterface(type); + } else { + cf = new ClassFile(ci.getClassName(), type); + } + + if (isAbstract) { + Modifiers modifiers = cf.getModifiers().toAbstract(true); + cf.setModifiers(modifiers); + } + cf.addInterface(Storable.class); + cf.addInterface(Cloneable.class); + cf.markSynthetic(); + cf.setSourceFile(aSourcefileName); + cf.setTarget("1.5"); + return cf; + } + + /** + * Generates code to compare a field in this object against the same one in a + * different instance. Branch to the provided Label if they are not equal. + * + * @param b {@link CodeBuilder} to which to add the code + * @param fieldName the name of the field + * @param fieldType the type of the field + * @param testForNull if true and the values are references, they will be considered + * unequal unless neither or both are null. If false, assume neither is null. + * @param fail the label to branch to + * @param other the other instance to test + */ + public static void addEqualsCall(CodeBuilder b, + String fieldName, + TypeDesc fieldType, + boolean testForNull, + Label fail, + LocalVariable other) + { + b.loadThis(); + b.loadField(fieldName, fieldType); + + b.loadLocal(other); + b.loadField(fieldName, fieldType); + + addValuesEqualCall(b, fieldType, testForNull, fail, false); + } + + /** + * Generates code to compare two values on the stack, and branch to the + * provided Label if they are not equal. Both values must be of the same type. + * + *

    The generated instruction consumes both values on the stack. + * + * @param b {@link CodeBuilder} to which to add the code + * @param valueType the type of the values + * @param testForNull if true and the values are references, they will be considered + * unequal unless neither or both are null. If false, assume neither is null. + * @param label the label to branch to + * @param choice when true, branch to label if values are equal, else + * branch to label if values are unequal. + */ + public static void addValuesEqualCall(final CodeBuilder b, + final TypeDesc valueType, + final boolean testForNull, + final Label label, + final boolean choice) + { + if (valueType.getTypeCode() != TypeDesc.OBJECT_CODE) { + b.ifComparisonBranch(label, choice ? "==" : "!=", valueType); + return; + } + + // Equals method returns zero for false, so if choice is true, branch + // if not zero. Note that operator selection is opposite when invoking + // a direct ifComparisonBranch method. + String equalsBranchOp = choice ? "!=" : "=="; + + if (!testForNull) { + addEqualsCallTo(b, valueType); + b.ifZeroComparisonBranch(label, equalsBranchOp); + return; + } + + Label isNotNull = b.createLabel(); + LocalVariable value = b.createLocalVariable(null, valueType); + b.storeLocal(value); + b.loadLocal(value); + b.ifNullBranch(isNotNull, false); + + // First value popped off stack is null. Just test remaining one for null. + b.ifNullBranch(label, choice); + Label cont = b.createLabel(); + b.branch(cont); + + // First value popped off stack is not null, but second one might + // be. Call equals method, but swap values so that the second value is + // an argument into the equals method. + isNotNull.setLocation(); + b.loadLocal(value); + b.swap(); + addEqualsCallTo(b, valueType); + b.ifZeroComparisonBranch(label, equalsBranchOp); + + cont.setLocation(); + } + + public static void addEqualsCallTo(CodeBuilder b, TypeDesc fieldType) { + if (fieldType.isArray()) { + if (!fieldType.getComponentType().isPrimitive()) { + TypeDesc type = TypeDesc.forClass(Object[].class); + b.invokeStatic("java.util.Arrays", "deepEquals", + TypeDesc.BOOLEAN, new TypeDesc[] {type, type}); + } else { + b.invokeStatic("java.util.Arrays", "equals", + TypeDesc.BOOLEAN, new TypeDesc[] {fieldType, fieldType}); + } + } else { + TypeDesc[] params = {TypeDesc.OBJECT}; + if (fieldType.toClass() != null) { + if (fieldType.toClass().isInterface()) { + b.invokeInterface(fieldType, "equals", TypeDesc.BOOLEAN, params); + } else { + b.invokeVirtual(fieldType, "equals", TypeDesc.BOOLEAN, params); + } + } else { + b.invokeVirtual(TypeDesc.OBJECT, "equals", TypeDesc.BOOLEAN, params); + } + } + } + + /** + * Converts a value on the stack. If "to" type is a String, then conversion + * may call the String.valueOf(from). + */ + public static void convertValue(CodeBuilder b, Class from, Class to) { + if (from == to) { + return; + } + + TypeDesc fromType = TypeDesc.forClass(from); + TypeDesc toType = TypeDesc.forClass(to); + + // Let CodeBuilder have a crack at the conversion first. + try { + b.convert(fromType, toType); + return; + } catch (IllegalArgumentException e) { + if (to != String.class && to != Object.class && to != CharSequence.class) { + throw e; + } + } + + // Fallback case is to convert to a String. + + if (fromType.isPrimitive()) { + b.invokeStatic(TypeDesc.STRING, "valueOf", TypeDesc.STRING, new TypeDesc[]{fromType}); + } else { + // If object on stack is null, then just leave it alone. + b.dup(); + Label isNull = b.createLabel(); + b.ifNullBranch(isNull, true); + b.invokeStatic(TypeDesc.STRING, "valueOf", TypeDesc.STRING, + new TypeDesc[]{TypeDesc.OBJECT}); + isNull.setLocation(); + } + } + + /** + * Generates code to push an initial version property value on the stack. + * + * @throws SupportException if version type is not supported + */ + public static void initialVersion(CodeBuilder b, TypeDesc type, int value) + throws SupportException + { + adjustVersion(b, type, value, false); + } + + /** + * Generates code to increment a version property value, already on the stack. + * + * @throws SupportException if version type is not supported + */ + public static void incrementVersion(CodeBuilder b, TypeDesc type) + throws SupportException + { + adjustVersion(b, type, 0, true); + } + + private static void adjustVersion(CodeBuilder b, TypeDesc type, int value, boolean increment) + throws SupportException + { + TypeDesc primitiveType = type.toPrimitiveType(); + supportCheck: { + if (primitiveType != null) { + switch (primitiveType.getTypeCode()) { + case TypeDesc.INT_CODE: + case TypeDesc.LONG_CODE: + break supportCheck; + } + } + throw new SupportException("Unsupported version type: " + type.getFullName()); + } + + if (!increment) { + if (primitiveType == TypeDesc.LONG) { + b.loadConstant((long) value); + } else { + b.loadConstant(value); + } + } else { + Label setVersion = b.createLabel(); + if (!type.isPrimitive()) { + b.dup(); + Label versionNotNull = b.createLabel(); + b.ifNullBranch(versionNotNull, false); + b.pop(); + if (primitiveType == TypeDesc.LONG) { + b.loadConstant(1L); + } else { + b.loadConstant(1); + } + b.branch(setVersion); + versionNotNull.setLocation(); + b.convert(type, primitiveType); + } + if (primitiveType == TypeDesc.LONG) { + b.loadConstant(1L); + b.math(Opcode.LADD); + } else { + b.loadConstant(1); + b.math(Opcode.IADD); + } + setVersion.setLocation(); + } + + b.convert(primitiveType, type); + } + + /** + * Determines which overloaded "with" method on Query should be bound to. + */ + public static TypeDesc bindQueryParam(Class clazz) { + // This method is a bit vestigial. Once upon a time the Query class did + // not support all primitive types. + if (clazz.isPrimitive()) { + TypeDesc type = TypeDesc.forClass(clazz); + switch (type.getTypeCode()) { + case TypeDesc.INT_CODE: + case TypeDesc.LONG_CODE: + case TypeDesc.FLOAT_CODE: + case TypeDesc.DOUBLE_CODE: + case TypeDesc.BOOLEAN_CODE: + case TypeDesc.CHAR_CODE: + case TypeDesc.BYTE_CODE: + case TypeDesc.SHORT_CODE: + return type; + } + } + return TypeDesc.OBJECT; + } + + /** + * Appends a String to a StringBuilder. A StringBuilder and String must be + * on the stack, and a StringBuilder is left on the stack after the call. + */ + public static void callStringBuilderAppendString(CodeBuilder b) { + // Because of JDK1.5 bug which exposes AbstractStringBuilder class, + // cannot use reflection to get method signature. + TypeDesc stringBuilder = TypeDesc.forClass(StringBuilder.class); + b.invokeVirtual(stringBuilder, "append", stringBuilder, new TypeDesc[] {TypeDesc.STRING}); + } + + /** + * Appends a char to a StringBuilder. A StringBuilder and char must be on + * the stack, and a StringBuilder is left on the stack after the call. + */ + public static void callStringBuilderAppendChar(CodeBuilder b) { + // Because of JDK1.5 bug which exposes AbstractStringBuilder class, + // cannot use reflection to get method signature. + TypeDesc stringBuilder = TypeDesc.forClass(StringBuilder.class); + b.invokeVirtual(stringBuilder, "append", stringBuilder, new TypeDesc[] {TypeDesc.CHAR}); + } + + /** + * Calls length on a StringBuilder on the stack, leaving an int on the stack. + */ + public static void callStringBuilderLength(CodeBuilder b) { + // Because of JDK1.5 bug which exposes AbstractStringBuilder class, + // cannot use reflection to get method signature. + TypeDesc stringBuilder = TypeDesc.forClass(StringBuilder.class); + b.invokeVirtual(stringBuilder, "length", TypeDesc.INT, null); + } + + /** + * Calls setLength on a StringBuilder. A StringBuilder and int must be on + * the stack, and both are consumed after the call. + */ + public static void callStringBuilderSetLength(CodeBuilder b) { + // Because of JDK1.5 bug which exposes AbstractStringBuilder class, + // cannot use reflection to get method signature. + TypeDesc stringBuilder = TypeDesc.forClass(StringBuilder.class); + b.invokeVirtual(stringBuilder, "setLength", null, new TypeDesc[] {TypeDesc.INT}); + } + + /** + * Calls toString on a StringBuilder. A StringBuilder must be on the stack, + * and a String is left on the stack after the call. + */ + public static void callStringBuilderToString(CodeBuilder b) { + // Because of JDK1.5 bug which exposes AbstractStringBuilder class, + // cannot use reflection to get method signature. + TypeDesc stringBuilder = TypeDesc.forClass(StringBuilder.class); + b.invokeVirtual(stringBuilder, "toString", TypeDesc.STRING, null); + } +} diff --git a/src/main/java/com/amazon/carbonado/gen/CommonMethodNames.java b/src/main/java/com/amazon/carbonado/gen/CommonMethodNames.java new file mode 100644 index 0000000..4e56e0a --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/CommonMethodNames.java @@ -0,0 +1,87 @@ +/* + * Copyright 2006 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.gen; + +/** + * Collection of constant method names for the public API. + * + * @author Brian S O'Neill + */ +public class CommonMethodNames { + /** Storable API method name */ + public static final String + LOAD_METHOD_NAME = "load", + INSERT_METHOD_NAME = "insert", + UPDATE_METHOD_NAME = "update", + DELETE_METHOD_NAME = "delete", + TRY_LOAD_METHOD_NAME = "tryLoad", + TRY_INSERT_METHOD_NAME = "tryInsert", + TRY_UPDATE_METHOD_NAME = "tryUpdate", + TRY_DELETE_METHOD_NAME = "tryDelete", + STORABLE_TYPE_METHOD_NAME = "storableType", + COPY_METHOD_NAME = "copy", + CLONE_METHOD_NAME = "clone", + COPY_ALL_PROPERTIES = "copyAllProperties", + COPY_PRIMARY_KEY_PROPERTIES = "copyPrimaryKeyProperties", + COPY_VERSION_PROPERTY = "copyVersionProperty", + COPY_UNEQUAL_PROPERTIES = "copyUnequalProperties", + COPY_DIRTY_PROPERTIES = "copyDirtyProperties", + HAS_DIRTY_PROPERTIES = "hasDirtyProperties", + MARK_PROPERTIES_CLEAN = "markPropertiesClean", + MARK_ALL_PROPERTIES_CLEAN = "markAllPropertiesClean", + MARK_PROPERTIES_DIRTY = "markPropertiesDirty", + MARK_ALL_PROPERTIES_DIRTY = "markAllPropertiesDirty", + IS_PROPERTY_UNINITIALIZED = "isPropertyUninitialized", + IS_PROPERTY_DIRTY = "isPropertyDirty", + IS_PROPERTY_CLEAN = "isPropertyClean", + IS_PROPERTY_SUPPORTED = "isPropertySupported", + TO_STRING_KEY_ONLY_METHOD_NAME = "toStringKeyOnly", + TO_STRING_METHOD_NAME = "toString", + HASHCODE_METHOD_NAME = "hashCode", + EQUALS_METHOD_NAME = "equals", + EQUAL_PRIMARY_KEYS_METHOD_NAME = "equalPrimaryKeys", + EQUAL_PROPERTIES_METHOD_NAME = "equalProperties"; + + /** Storage API method name */ + public static final String + QUERY_METHOD_NAME = "query", + PREPARE_METHOD_NAME = "prepare"; + + /** Query API method name */ + public static final String + LOAD_ONE_METHOD_NAME = "loadOne", + TRY_LOAD_ONE_METHOD_NAME = "tryLoadOne", + WITH_METHOD_NAME = "with", + FETCH_METHOD_NAME = "fetch"; + + /** Repository API method name */ + public static final String + STORAGE_FOR_METHOD_NAME = "storageFor", + ENTER_TRANSACTION_METHOD_NAME = "enterTransaction", + GET_TRANSACTION_ISOLATION_LEVEL_METHOD_NAME = "getTransactionIsolationLevel"; + + /** Transaction API method name */ + public static final String + SET_FOR_UPDATE_METHOD_NAME = "setForUpdate", + COMMIT_METHOD_NAME = "commit", + EXIT_METHOD_NAME = "exit"; + + /** WrappedStorage.Support API method name */ + public static final String CREATE_WRAPPED_SUPPORT_METHOD_NAME = "createSupport"; +} diff --git a/src/main/java/com/amazon/carbonado/gen/MasterFeature.java b/src/main/java/com/amazon/carbonado/gen/MasterFeature.java new file mode 100644 index 0000000..7875d49 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/MasterFeature.java @@ -0,0 +1,59 @@ +/* + * Copyright 2006 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.gen; + +/** + * Master feature to enable when using {@link MasterStorableGenerator}. + * + * @author Brian S O'Neill + */ +public enum MasterFeature { + /** Insert and update operations implement record versioning, if version property exists */ + VERSIONING, + + /** Update operations load clean copy first, to prevent destructive update */ + UPDATE_FULL, + + /** Ensure update operation always is in a transaction */ + UPDATE_TXN, + + /** Ensure update operation always is in a transaction, "for update" */ + UPDATE_TXN_FOR_UPDATE, + + /** Insert operation applies any sequences to unset properties */ + INSERT_SEQUENCES, + + /** + * Insert operation checks that all required data properties have been set, + * excluding automatic properties and version property. + */ + INSERT_CHECK_REQUIRED, + + /** Ensure insert operation always is in a transaction */ + INSERT_TXN, + + /** Ensure insert operation always is in a transaction, "for update" */ + INSERT_TXN_FOR_UPDATE, + + /** Ensure delete operation always is in a transaction */ + DELETE_TXN, + + /** Ensure delete operation always is in a transaction, "for update" */ + DELETE_TXN_FOR_UPDATE, +} diff --git a/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java b/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java new file mode 100644 index 0000000..17efb70 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java @@ -0,0 +1,835 @@ +/* + * Copyright 2006 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.gen; + +import java.lang.reflect.Method; +import java.util.EnumSet; +import java.util.HashSet; +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.cojen.util.SoftValuedHashMap; + +import com.amazon.carbonado.ConstraintException; +import com.amazon.carbonado.OptimisticLockException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.Transaction; + +import com.amazon.carbonado.info.StorableInfo; +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableProperty; + +import com.amazon.carbonado.sequence.SequenceValueProducer; + +import static com.amazon.carbonado.gen.CommonMethodNames.*; + +/** + * Generates and caches abstract implementations of {@link Storable} types + * suitable for use by master repositories. The generated classes extend those + * generated by {@link StorableGenerator}. Subclasses need not worry about + * transactions since this class takes care of that. + * + * @author Brian S O'Neill + */ +public final class MasterStorableGenerator { + // Note: All generated fields/methods have a "$" character in them to + // prevent name collisions with any inherited fields/methods. User storable + // properties are defined as fields which exactly match the property + // name. We don't want collisions with those either. Legal bean properties + // cannot have "$" in them, so there's nothing to worry about. + + /** Name of protected abstract method in generated storable */ + public static final String + DO_TRY_LOAD_MASTER_METHOD_NAME = StorableGenerator.DO_TRY_LOAD_METHOD_NAME, + DO_TRY_INSERT_MASTER_METHOD_NAME = "doTryInsert$master", + DO_TRY_UPDATE_MASTER_METHOD_NAME = "doTryUpdate$master", + DO_TRY_DELETE_MASTER_METHOD_NAME = "doTryDelete$master"; + + private static final String APPEND_UNINIT_PROPERTY = "appendUninitializedPropertyName$"; + + private static final String INSERT_OP = "Insert"; + private static final String UPDATE_OP = "Update"; + private static final String DELETE_OP = "Delete"; + + // Cache of generated abstract classes. + private static Map> cCache = new SoftValuedHashMap(); + + /** + * Returns an abstract implementation of the given Storable type, which + * is fully thread-safe. The Storable type itself may be an interface or + * a class. If it is a class, then it must not be final, and it must have a + * public, no-arg constructor. The constructor for the returned abstract + * class looks like this: + * + *

    +     * public <init>(MasterSupport);
    +     * 
    + * + * Subclasses must implement the following abstract protected methods, + * whose exact names are defined by constants in this class: + * + *
    +     * // Load the object by examining the primary key.
    +     * protected abstract boolean doTryLoad() throws FetchException;
    +     *
    +     * // Insert the object into the storage layer.
    +     * protected abstract boolean doTryInsert_master() throws PersistException;
    +     *
    +     * // Update the object in the storage.
    +     * protected abstract boolean doTryUpdate_master() throws PersistException;
    +     *
    +     * // Delete the object from the storage layer by the primary key.
    +     * protected abstract boolean doTryDelete_master() throws PersistException;
    +     * 
    + * + * Subclasses can access the MasterSupport instance via the protected field + * named by {@link StorableGenerator#SUPPORT_FIELD_NAME SUPPORT_FIELD_NAME}. + * + * @throws com.amazon.carbonado.MalformedTypeException if Storable type is not well-formed + * @throws IllegalArgumentException if type is null + * @see MasterSupport + */ + public static Class + getAbstractClass(Class type, EnumSet features) + throws SupportException, IllegalArgumentException + { + StorableInfo info = StorableIntrospector.examine(type); + + anySequences: + if (features.contains(MasterFeature.INSERT_SEQUENCES)) { + for (StorableProperty property : info.getAllProperties().values()) { + if (property.getSequenceName() != null) { + break anySequences; + } + } + features.remove(MasterFeature.INSERT_SEQUENCES); + } + + if (info.getVersionProperty() == null) { + features.remove(MasterFeature.VERSIONING); + } + + if (features.contains(MasterFeature.VERSIONING)) { + // Implied feature. + features.add(MasterFeature.UPDATE_FULL); + } + + if (alwaysHasTxn(INSERT_OP, features)) { + // Implied feature. + features.add(MasterFeature.INSERT_TXN); + } + if (alwaysHasTxn(UPDATE_OP, features)) { + // Implied feature. + features.add(MasterFeature.UPDATE_TXN); + } + if (alwaysHasTxn(DELETE_OP, features)) { + // Implied feature. + features.add(MasterFeature.DELETE_TXN); + } + + if (requiresTxnForUpdate(INSERT_OP, features)) { + // Implied feature. + features.add(MasterFeature.INSERT_TXN_FOR_UPDATE); + } + if (requiresTxnForUpdate(UPDATE_OP, features)) { + // Implied feature. + features.add(MasterFeature.UPDATE_TXN_FOR_UPDATE); + } + if (requiresTxnForUpdate(DELETE_OP, features)) { + // Implied feature. + features.add(MasterFeature.DELETE_TXN_FOR_UPDATE); + } + + Object key = KeyFactory.createKey(new Object[] {type, features}); + + synchronized (cCache) { + Class abstractClass = (Class) cCache.get(key); + if (abstractClass != null) { + return abstractClass; + } + abstractClass = + new MasterStorableGenerator(type, features).generateAndInjectClass(); + cCache.put(key, abstractClass); + return abstractClass; + } + } + + private final EnumSet mFeatures; + private final StorableInfo mInfo; + private final Map> mAllProperties; + + private final ClassInjector mClassInjector; + private final ClassFile mClassFile; + + private MasterStorableGenerator(Class storableType, EnumSet features) { + mFeatures = features; + mInfo = StorableIntrospector.examine(storableType); + mAllProperties = mInfo.getAllProperties(); + + final Class abstractClass = StorableGenerator.getAbstractClass(storableType); + + mClassInjector = ClassInjector.create + (storableType.getName(), abstractClass.getClassLoader()); + + mClassFile = new ClassFile(mClassInjector.getClassName(), abstractClass); + mClassFile.setModifiers(mClassFile.getModifiers().toAbstract(true)); + mClassFile.markSynthetic(); + mClassFile.setSourceFile(MasterStorableGenerator.class.getName()); + mClassFile.setTarget("1.5"); + } + + private Class generateAndInjectClass() throws SupportException { + generateClass(); + Class abstractClass = mClassInjector.defineClass(mClassFile); + return (Class) abstractClass; + } + + private void generateClass() throws SupportException { + // Declare some types. + final TypeDesc storableType = TypeDesc.forClass(Storable.class); + final TypeDesc triggerSupportType = TypeDesc.forClass(TriggerSupport.class); + final TypeDesc masterSupportType = TypeDesc.forClass(MasterSupport.class); + final TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + final TypeDesc optimisticLockType = TypeDesc.forClass(OptimisticLockException.class); + final TypeDesc persistExceptionType = TypeDesc.forClass(PersistException.class); + + // Add constructor that accepts a MasterSupport. + { + TypeDesc[] params = {masterSupportType}; + MethodInfo mi = mClassFile.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeSuperConstructor(new TypeDesc[] {triggerSupportType}); + + b.returnVoid(); + } + + // Declare protected abstract methods. + { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_INSERT_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + + mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + + mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_DELETE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + } + + // Add required protected doTryInsert method. + { + // If sequence support requested, implement special insert hook to + // call sequences for properties which are UNINITIALIZED. User may + // provide explicit values for properties with sequences. + + if (mFeatures.contains(MasterFeature.INSERT_SEQUENCES)) { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED, + StorableGenerator.CHECK_PK_FOR_INSERT_METHOD_NAME, + null, null); + CodeBuilder b = new CodeBuilder(mi); + + int ordinal = 0; + for (StorableProperty property : mAllProperties.values()) { + if (property.getSequenceName() != null) { + // Check the state of this property, to see if it is + // uninitialized. Uninitialized state has value zero. + + String stateFieldName = + StorableGenerator.PROPERTY_STATE_FIELD_NAME + (ordinal >> 4); + + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + int shift = (ordinal & 0xf) * 2; + b.loadConstant(StorableGenerator.PROPERTY_STATE_MASK << shift); + b.math(Opcode.IAND); + + Label isInitialized = b.createLabel(); + b.ifZeroComparisonBranch(isInitialized, "!="); + + // Load this in preparation for storing value to property. + b.loadThis(); + + // Call MasterSupport.getSequenceValueProducer(String). + TypeDesc seqValueProdType = TypeDesc.forClass(SequenceValueProducer.class); + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(masterSupportType); + b.loadConstant(property.getSequenceName()); + b.invokeInterface + (masterSupportType, "getSequenceValueProducer", + seqValueProdType, new TypeDesc[] {TypeDesc.STRING}); + + // Find appropriate method to call for getting next sequence value. + TypeDesc propertyType = TypeDesc.forClass(property.getType()); + TypeDesc propertyObjType = propertyType.toObjectType(); + Method method; + + try { + if (propertyObjType == TypeDesc.LONG.toObjectType()) { + method = SequenceValueProducer.class + .getMethod("nextLongValue", (Class[]) null); + } else if (propertyObjType == TypeDesc.INT.toObjectType()) { + method = SequenceValueProducer.class + .getMethod("nextIntValue", (Class[]) null); + } else if (propertyObjType == TypeDesc.STRING) { + method = SequenceValueProducer.class + .getMethod("nextDecimalValue", (Class[]) null); + } else { + throw new SupportException + ("Unable to support sequence of type \"" + + property.getType().getName() + "\" for property: " + + property.getName()); + } + } catch (NoSuchMethodException e) { + Error err = new NoSuchMethodError(); + err.initCause(e); + throw err; + } + + b.invoke(method); + b.convert(TypeDesc.forClass(method.getReturnType()), propertyType); + + // Store property + b.storeField(property.getName(), propertyType); + + // Set state to dirty. + b.loadThis(); + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.loadConstant(StorableGenerator.PROPERTY_STATE_DIRTY << shift); + b.math(Opcode.IOR); + b.storeField(stateFieldName, TypeDesc.INT); + + isInitialized.setLocation(); + } + + ordinal++; + } + + // We've tried our best to fill in missing values, now run the + // original check method. + b.loadThis(); + b.invokeSuper(mClassFile.getSuperClassName(), + StorableGenerator.CHECK_PK_FOR_INSERT_METHOD_NAME, + null, null); + b.returnVoid(); + } + + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED.toFinal(true), + StorableGenerator.DO_TRY_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + CodeBuilder b = new CodeBuilder(mi); + + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + + Label tryStart = addEnterTransaction(b, INSERT_OP, txnVar); + + if (mFeatures.contains(MasterFeature.VERSIONING)) { + // Only set if uninitialized. + b.loadThis(); + b.invokeVirtual(StorableGenerator.IS_VERSION_INITIALIZED_METHOD_NAME, + TypeDesc.BOOLEAN, null); + Label isInitialized = b.createLabel(); + b.ifZeroComparisonBranch(isInitialized, "!="); + addAdjustVersionProperty(b, null, 1); + isInitialized.setLocation(); + } + + if (mFeatures.contains(MasterFeature.INSERT_CHECK_REQUIRED)) { + // Ensure that required properties have been set. + b.loadThis(); + b.invokeVirtual(StorableGenerator.IS_REQUIRED_DATA_INITIALIZED_METHOD_NAME, + TypeDesc.BOOLEAN, null); + Label isInitialized = b.createLabel(); + b.ifZeroComparisonBranch(isInitialized, "!="); + + // Throw a ConstraintException. + TypeDesc exType = TypeDesc.forClass(ConstraintException.class); + b.newObject(exType); + b.dup(); + + // Append all the uninitialized property names to the exception message. + + LocalVariable countVar = b.createLocalVariable(null, TypeDesc.INT); + b.loadConstant(0); + b.storeLocal(countVar); + + TypeDesc sbType = TypeDesc.forClass(StringBuilder.class); + b.newObject(sbType); + b.dup(); + b.loadConstant("Not all required properties have been set: "); + TypeDesc[] stringParam = {TypeDesc.STRING}; + b.invokeConstructor(sbType, stringParam); + LocalVariable sbVar = b.createLocalVariable(null, sbType); + b.storeLocal(sbVar); + + int ordinal = -1; + + HashSet stateAppendMethods = new HashSet(); + + // Parameters are: StringBuilder, count, mask, property name + TypeDesc[] appendParams = {sbType, TypeDesc.INT, TypeDesc.INT, TypeDesc.STRING}; + + for (StorableProperty property : mAllProperties.values()) { + ordinal++; + + if (property.isJoin() || property.isPrimaryKeyMember() + || property.isNullable() + || property.isAutomatic() || property.isVersion()) + { + continue; + } + + int stateField = ordinal >> 4; + + String stateAppendMethodName = APPEND_UNINIT_PROPERTY + stateField; + + if (!stateAppendMethods.contains(stateField)) { + stateAppendMethods.add(stateField); + + MethodInfo mi2 = mClassFile.addMethod + (Modifiers.PRIVATE, stateAppendMethodName, TypeDesc.INT, appendParams); + + CodeBuilder b2 = new CodeBuilder(mi2); + + // Load the StringBuilder parameter. + b2.loadLocal(b2.getParameter(0)); + + String stateFieldName = + StorableGenerator.PROPERTY_STATE_FIELD_NAME + (ordinal >> 4); + + b2.loadThis(); + b2.loadField(stateFieldName, TypeDesc.INT); + // Load the mask parameter. + b2.loadLocal(b2.getParameter(2)); + b2.math(Opcode.IAND); + + Label propIsInitialized = b2.createLabel(); + b2.ifZeroComparisonBranch(propIsInitialized, "!="); + + // Load the count parameter. + b2.loadLocal(b2.getParameter(1)); + Label noComma = b2.createLabel(); + b2.ifZeroComparisonBranch(noComma, "=="); + b2.loadConstant(", "); + b2.invokeVirtual(sbType, "append", sbType, stringParam); + noComma.setLocation(); + + // Load the property name parameter. + b2.loadLocal(b2.getParameter(3)); + b2.invokeVirtual(sbType, "append", sbType, stringParam); + + // Increment the count parameter. + b2.integerIncrement(b2.getParameter(1), 1); + + propIsInitialized.setLocation(); + + // Return the possibly updated count. + b2.loadLocal(b2.getParameter(1)); + b2.returnValue(TypeDesc.INT); + } + + b.loadThis(); + // Parameters are: StringBuilder, count, mask, property name + b.loadLocal(sbVar); + b.loadLocal(countVar); + b.loadConstant(StorableGenerator.PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2)); + b.loadConstant(property.getName()); + b.invokePrivate(stateAppendMethodName, TypeDesc.INT, appendParams); + b.storeLocal(countVar); + } + + b.loadLocal(sbVar); + b.invokeVirtual(sbType, "toString", TypeDesc.STRING, null); + b.invokeConstructor(exType, new TypeDesc[] {TypeDesc.STRING}); + b.throwObject(); + + isInitialized.setLocation(); + } + + b.loadThis(); + b.invokeVirtual(DO_TRY_INSERT_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + + if (tryStart == null) { + b.returnValue(TypeDesc.BOOLEAN); + } else { + Label failed = b.createLabel(); + b.ifZeroComparisonBranch(failed, "=="); + + addCommitAndExitTransaction(b, INSERT_OP, txnVar); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + failed.setLocation(); + addExitTransaction(b, INSERT_OP, txnVar); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + addExitTransaction(b, INSERT_OP, txnVar, tryStart); + } + } + + // Add required protected doTryUpdate method. + addDoTryUpdate: { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED.toFinal(true), + StorableGenerator.DO_TRY_UPDATE_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + CodeBuilder b = new CodeBuilder(mi); + + if ((!mFeatures.contains(MasterFeature.VERSIONING)) && + (!mFeatures.contains(MasterFeature.UPDATE_FULL)) && + (!mFeatures.contains(MasterFeature.UPDATE_TXN))) + { + // Nothing special needs to be done, so just delegate and return. + b.loadThis(); + b.invokeVirtual(DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + b.returnValue(TypeDesc.BOOLEAN); + break addDoTryUpdate; + } + + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + LocalVariable savedVar = null; + + Label tryStart = addEnterTransaction(b, UPDATE_OP, txnVar); + + Label failed = b.createLabel(); + + if (mFeatures.contains(MasterFeature.UPDATE_FULL)) { + // Storable saved = copy(); + b.loadThis(); + b.invokeVirtual(COPY_METHOD_NAME, storableType, null); + b.checkCast(mClassFile.getType()); + savedVar = b.createLocalVariable(null, mClassFile.getType()); + b.storeLocal(savedVar); + + // if (!saved.tryLoad()) { + // goto failed; + // } + b.loadLocal(savedVar); + b.invokeInterface(storableType, TRY_LOAD_METHOD_NAME, TypeDesc.BOOLEAN, null); + b.ifZeroComparisonBranch(failed, "=="); + + // if (version support enabled) { + // if (this.getVersionNumber() != saved.getVersionNumber()) { + // throw new OptimisticLockException + // (this.getVersionNumber(), saved.getVersionNumber(), this); + // } + // } + if (mFeatures.contains(MasterFeature.VERSIONING)) { + TypeDesc versionType = TypeDesc.forClass(mInfo.getVersionProperty().getType()); + b.loadThis(); + b.invoke(mInfo.getVersionProperty().getReadMethod()); + b.loadLocal(savedVar); + b.invoke(mInfo.getVersionProperty().getReadMethod()); + Label sameVersion = b.createLabel(); + CodeBuilderUtil.addValuesEqualCall(b, versionType, true, sameVersion, true); + b.newObject(optimisticLockType); + b.dup(); + b.loadThis(); + b.invoke(mInfo.getVersionProperty().getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.loadLocal(savedVar); + b.invoke(mInfo.getVersionProperty().getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.loadThis(); + b.invokeConstructor + (optimisticLockType, + new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT, storableType}); + b.throwObject(); + sameVersion.setLocation(); + } + + // this.copyDirtyProperties(saved); + // if (version support enabled) { + // saved.setVersionNumber(saved.getVersionNumber() + 1); + // } + b.loadThis(); + b.loadLocal(savedVar); + b.invokeVirtual(COPY_DIRTY_PROPERTIES, null, new TypeDesc[] {storableType}); + if (mFeatures.contains(MasterFeature.VERSIONING)) { + addAdjustVersionProperty(b, savedVar, -1); + } + + // if (!saved.doTryUpdateMaster()) { + // goto failed; + // } + b.loadLocal(savedVar); + b.invokeVirtual(DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + b.ifZeroComparisonBranch(failed, "=="); + + // saved.copyUnequalProperties(this); + b.loadLocal(savedVar); + b.loadThis(); + b.invokeInterface + (storableType, COPY_UNEQUAL_PROPERTIES, null, new TypeDesc[] {storableType}); + } else { + // if (!this.doTryUpdateMaster()) { + // goto failed; + // } + b.loadThis(); + b.invokeVirtual(DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + b.ifZeroComparisonBranch(failed, "=="); + } + + // txn.commit(); + // txn.exit(); + // return true; + addCommitAndExitTransaction(b, UPDATE_OP, txnVar); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + // failed: + // txn.exit(); + failed.setLocation(); + addExitTransaction(b, UPDATE_OP, txnVar); + // return false; + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + addExitTransaction(b, UPDATE_OP, txnVar, tryStart); + } + + // Add required protected doTryDelete method. + { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED.toFinal(true), + StorableGenerator.DO_TRY_DELETE_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + CodeBuilder b = new CodeBuilder(mi); + + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + + Label tryStart = addEnterTransaction(b, DELETE_OP, txnVar); + + b.loadThis(); + b.invokeVirtual(DO_TRY_DELETE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + + if (tryStart == null) { + b.returnValue(TypeDesc.BOOLEAN); + } else { + Label failed = b.createLabel(); + b.ifZeroComparisonBranch(failed, "=="); + addCommitAndExitTransaction(b, DELETE_OP, txnVar); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + failed.setLocation(); + addExitTransaction(b, DELETE_OP, txnVar); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + addExitTransaction(b, DELETE_OP, txnVar, tryStart); + } + } + } + + /** + * Generates code to enter a transaction, if required. + * + * @param opType type of operation, Insert, Update, or Delete + * @param txnVar required variable of type Transaction for storing transaction + * @return optional try start label for transaction + */ + private Label addEnterTransaction(CodeBuilder b, String opType, LocalVariable txnVar) { + if (!alwaysHasTxn(opType)) { + return null; + } + + // txn = masterSupport.getRootRepository().enterTransaction(); + + TypeDesc repositoryType = TypeDesc.forClass(Repository.class); + TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + TypeDesc triggerSupportType = TypeDesc.forClass(TriggerSupport.class); + TypeDesc masterSupportType = TypeDesc.forClass(MasterSupport.class); + + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.invokeInterface(masterSupportType, "getRootRepository", + repositoryType, null); + b.invokeInterface(repositoryType, ENTER_TRANSACTION_METHOD_NAME, + transactionType, null); + b.storeLocal(txnVar); + if (requiresTxnForUpdate(opType)) { + // txn.setForUpdate(true); + b.loadLocal(txnVar); + b.loadConstant(true); + b.invokeInterface(transactionType, SET_FOR_UPDATE_METHOD_NAME, null, + new TypeDesc[] {TypeDesc.BOOLEAN}); + } + + return b.createLabel().setLocation(); + } + + private boolean alwaysHasTxn(String opType) { + return alwaysHasTxn(opType, mFeatures); + } + + private static boolean alwaysHasTxn(String opType, EnumSet features) { + if (opType == UPDATE_OP) { + return + features.contains(MasterFeature.UPDATE_TXN) || + features.contains(MasterFeature.UPDATE_TXN_FOR_UPDATE) || + features.contains(MasterFeature.VERSIONING) || + features.contains(MasterFeature.UPDATE_FULL); + } else if (opType == INSERT_OP) { + return + features.contains(MasterFeature.INSERT_TXN) || + features.contains(MasterFeature.INSERT_TXN_FOR_UPDATE); + } else if (opType == DELETE_OP) { + return + features.contains(MasterFeature.DELETE_TXN) || + features.contains(MasterFeature.DELETE_TXN_FOR_UPDATE); + } + return false; + } + + private boolean requiresTxnForUpdate(String opType) { + return requiresTxnForUpdate(opType, mFeatures); + } + + private static boolean requiresTxnForUpdate(String opType, EnumSet features) { + if (opType == UPDATE_OP) { + return + features.contains(MasterFeature.UPDATE_TXN_FOR_UPDATE) || + features.contains(MasterFeature.VERSIONING) || + features.contains(MasterFeature.UPDATE_FULL); + } else if (opType == INSERT_OP) { + return features.contains(MasterFeature.INSERT_TXN_FOR_UPDATE); + } else if (opType == DELETE_OP) { + return features.contains(MasterFeature.DELETE_TXN_FOR_UPDATE); + } + return false; + } + + private void addCommitAndExitTransaction(CodeBuilder b, String opType, LocalVariable txnVar) { + if (!alwaysHasTxn(opType)) { + return; + } + + TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + + // txn.commit(); + // txn.exit(); + b.loadLocal(txnVar); + b.invokeInterface(transactionType, COMMIT_METHOD_NAME, null, null); + b.loadLocal(txnVar); + b.invokeInterface(transactionType, EXIT_METHOD_NAME, null, null); + } + + /** + * + * @param opType type of operation, Insert, Update, or Delete + */ + private void addExitTransaction(CodeBuilder b, String opType, LocalVariable txnVar) { + if (!alwaysHasTxn(opType)) { + return; + } + + TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + + // txn.exit(); + b.loadLocal(txnVar); + b.invokeInterface(transactionType, EXIT_METHOD_NAME, null, null); + } + + /** + * + * @param opType type of operation, Insert, Update, or Delete + */ + private void addExitTransaction(CodeBuilder b, String opType, LocalVariable txnVar, + Label tryStart) + { + if (tryStart == null) { + addExitTransaction(b, opType, txnVar); + return; + } + + // } catch (... e) { + // txn.exit(); + // throw e; + // } + + Label tryEnd = b.createLabel().setLocation(); + b.exceptionHandler(tryStart, tryEnd, null); + addExitTransaction(b, opType, txnVar); + b.throwObject(); + } + + /* + * Generates code to adjust the version property. If value parameter is negative, then + * version is incremented as follows: + * + * storable.setVersionNumber(storable.getVersionNumber() + 1); + * + * Otherwise, the version is set: + * + * storable.setVersionNumber(value); + * + * @param storableVar references storable instance, or null if this + * @param value if negative, increment version, else, set version to this value + */ + private void addAdjustVersionProperty(CodeBuilder b, + LocalVariable storableVar, + int value) + throws SupportException + { + // Push storable to stack in preparation for calling set method below. + if (storableVar == null) { + b.loadThis(); + } else { + b.loadLocal(storableVar); + } + + StorableProperty versionProperty = mInfo.getVersionProperty(); + TypeDesc versionType = TypeDesc.forClass(versionProperty.getType()); + + if (value >= 0) { + CodeBuilderUtil.initialVersion(b, versionType, value); + } else { + // Load current property value. + b.dup(); + b.invoke(versionProperty.getReadMethod()); + CodeBuilderUtil.incrementVersion(b, versionType); + } + + b.invoke(versionProperty.getWriteMethod()); + } +} diff --git a/src/main/java/com/amazon/carbonado/gen/MasterSupport.java b/src/main/java/com/amazon/carbonado/gen/MasterSupport.java new file mode 100644 index 0000000..cd1a183 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/MasterSupport.java @@ -0,0 +1,39 @@ +/* + * Copyright 2006 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.gen; + +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.sequence.SequenceValueProducer; + +/** + * Provides runtime support for Storable classes generated by {@link MasterStorableGenerator}. + * + * @author Brian S O'Neill + */ +public interface MasterSupport extends TriggerSupport { + /** + * Returns a sequence value producer by name, or throw PersistException if not found. + * + *

    Note: this method throws PersistException even for fetch failures + * since this method is called by insert operations. Insert operations can + * only throw a PersistException. + */ + SequenceValueProducer getSequenceValueProducer(String name) throws PersistException; +} diff --git a/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java b/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java new file mode 100644 index 0000000..cbfeab9 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java @@ -0,0 +1,3671 @@ +/* + * Copyright 2006 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.gen; + +import java.lang.annotation.Annotation; +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.math.BigInteger; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.Label; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodDesc; +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.WeakIdentityMap; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.FetchNoneException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.PersistNoneException; +import com.amazon.carbonado.Query; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.Transaction; +import com.amazon.carbonado.Trigger; +import com.amazon.carbonado.UniqueConstraintException; + +import com.amazon.carbonado.lob.Lob; + +import com.amazon.carbonado.info.ChainedProperty; +import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableInfo; +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableKey; +import com.amazon.carbonado.info.StorableProperty; +import com.amazon.carbonado.info.StorablePropertyAdapter; +import com.amazon.carbonado.info.StorablePropertyAnnotation; +import com.amazon.carbonado.info.StorablePropertyConstraint; + +import static com.amazon.carbonado.gen.CommonMethodNames.*; + +/** + * Generates and caches abstract and wrapped implementations of {@link + * Storable} types. This greatly simplifies the process of defining new kinds + * of {@link Repository Repositories}, since most of the mundane code + * generation is taken care of. + * + * @author Brian S O'Neill + * @author Don Schneider + * @see MasterStorableGenerator + */ +public final class StorableGenerator { + + // Note: All generated fields/methods have a "$" character in them to + // prevent name collisions with any inherited fields/methods. User storable + // properties are defined as fields which exactly match the property + // name. We don't want collisions with those either. Legal bean properties + // cannot have "$" in them, so there's nothing to worry about. + + /** Name of protected abstract method in generated storable */ + public static final String + DO_TRY_LOAD_METHOD_NAME = "doTryLoad$", + DO_TRY_INSERT_METHOD_NAME = "doTryInsert$", + DO_TRY_UPDATE_METHOD_NAME = "doTryUpdate$", + DO_TRY_DELETE_METHOD_NAME = "doTryDelete$"; + + /** + * Name of protected method in generated storable which checks that + * primary keys are initialized, throwing an exception otherwise. + */ + public static final String + CHECK_PK_FOR_INSERT_METHOD_NAME = "checkPkForInsert$", + CHECK_PK_FOR_UPDATE_METHOD_NAME = "checkPkForUpdate$", + CHECK_PK_FOR_DELETE_METHOD_NAME = "checkPkForDelete$"; + + /** + * Name of protected method in generated storable that returns false if any + * primary keys are uninitialized. + */ + public static final String IS_PK_INITIALIZED_METHOD_NAME = "isPkInitialized$"; + + /** + * Name prefix of protected method in generated storable that returns false + * if a specific alternate key is uninitialized. The complete name is + * formed by the prefix appended with the zero-based alternate key ordinal. + */ + public static final String IS_ALT_KEY_INITIALIZED_PREFIX = "isAltKeyInitialized$"; + + /** + * Name of protected method in generated storable that returns false if any + * non-nullable, non-pk properties are uninitialized. + */ + public static final String IS_REQUIRED_DATA_INITIALIZED_METHOD_NAME = + "isRequiredDataInitialized$"; + + /** + * Name of protected method in generated storable that returns false if + * version property is uninitialized. If no version property exists, then + * this method is not defined. + */ + public static final String IS_VERSION_INITIALIZED_METHOD_NAME = "isVersionInitialized$"; + + /** + * Prefix of protected field in generated storable that holds property + * states. Each property consumes two bits to hold its state, and so each + * 32-bit field holds states for up to 16 properties. + */ + public static final String PROPERTY_STATE_FIELD_NAME = "propertyState$"; + + /** Adapter field names are propertyName + "$adapter$" + ordinal */ + public static final String ADAPTER_FIELD_ELEMENT = "$adapter$"; + + /** Constraint field names are propertyName + "$constraint$" + ordinal */ + public static final String CONSTRAINT_FIELD_ELEMENT = "$constraint$"; + + /** Reference to TriggerSupport or WrappedSupport instance */ + public static final String SUPPORT_FIELD_NAME = "support$"; + + /** Property state indicating that property has never been set, loaded, or saved */ + public static final int PROPERTY_STATE_UNINITIALIZED = 0; + /** Property state indicating that property has been set, but not saved */ + public static final int PROPERTY_STATE_DIRTY = 3; + /** Property state indicating that property value reflects a clean value */ + public static final int PROPERTY_STATE_CLEAN = 1; + /** Property state mask is 3, to cover the two bits used by a property state */ + public static final int PROPERTY_STATE_MASK = 3; + + // Private method which returns a property's state. + private static final String PROPERTY_STATE_EXTRACT_METHOD_NAME = "extractState$"; + + private static final String PRIVATE_INSERT_METHOD_NAME = "insert$"; + private static final String PRIVATE_UPDATE_METHOD_NAME = "update$"; + private static final String PRIVATE_DELETE_METHOD_NAME = "delete$"; + + // Cache of generated abstract classes. + private static Map>> cAbstractCache; + // Cache of generated wrapped classes. + private static Map>> cWrappedCache; + + static { + cAbstractCache = new WeakIdentityMap(); + cWrappedCache = new WeakIdentityMap(); + } + + // There are three flavors of equals methods, used by addEqualsMethod. + private static final int EQUAL_KEYS = 0; + private static final int EQUAL_PROPERTIES = 1; + private static final int EQUAL_FULL = 2; + + // Operation mode for generating Storable. + private static final int GEN_ABSTRACT = 1; + private static final int GEN_WRAPPED = 2; + + private static final String WRAPPED_STORABLE_FIELD_NAME = "wrappedStorable$"; + + private static final String UNCAUGHT_METHOD_NAME = "uncaught$"; + + private static final String INSERT_OP = "Insert"; + private static final String UPDATE_OP = "Update"; + private static final String DELETE_OP = "Delete"; + + /** + * Returns an abstract implementation of the given Storable type, which is + * fully thread-safe. The Storable type itself may be an interface or a + * class. If it is a class, then it must not be final, and it must have a + * public, no-arg constructor. The constructor signature for the returned + * abstract is defined as follows: + * + *

    +     * /**
    +     *  * @param support  Access to triggers
    +     *  */
    +     * public <init>(TriggerSupport support);
    +     * 
    + * + *

    Subclasses must implement the following abstract protected methods, + * whose exact names are defined by constants in this class: + * + *

    +     * // Load the object by examining the primary key.
    +     * protected abstract boolean doTryLoad() throws FetchException;
    +     *
    +     * // Insert the object into the storage layer.
    +     * protected abstract boolean doTryInsert() throws PersistException;
    +     *
    +     * // Update the object in the storage.
    +     * protected abstract boolean doTryUpdate() throws PersistException;
    +     *
    +     * // Delete the object from the storage layer by the primary key.
    +     * protected abstract boolean doTryDelete() throws PersistException;
    +     * 
    + * + * A set of protected hook methods are provided which ensure that all + * primary keys are initialized before performing a repository + * operation. Subclasses may override them, if they are capable of filling + * in unspecified primary keys. One such example is applying a sequence on + * insert. + * + *
    +     * // Throws exception if any primary keys are uninitialized.
    +     * // Actual method name defined by CHECK_PK_FOR_INSERT_METHOD_NAME.
    +     * protected void checkPkForInsert() throws IllegalStateException;
    +     *
    +     * // Throws exception if any primary keys are uninitialized.
    +     * // Actual method name defined by CHECK_PK_FOR_UPDATE_METHOD_NAME.
    +     * protected void checkPkForUpdate() throws IllegalStateException;
    +     *
    +     * // Throws exception if any primary keys are uninitialized.
    +     * // Actual method name defined by CHECK_PK_FOR_DELETE_METHOD_NAME.
    +     * protected void checkPkForDelete() throws IllegalStateException;
    +     * 
    + * + * Each property value is defined as a protected field whose name and type + * matches the property. Subclasses should access these fields directly + * during loading and storing. For loading, it bypasses constraint + * checks. For both, it provides better performance. + * + *

    Subclasses also have access to a set of property state bits stored + * in protected int fields. Subclasses are not responsible for updating + * these values. The intention is that these states may be used by + * subclasses to support partial updates. They may otherwise be ignored. + * + *

    As a convenience, protected methods are provided to test and alter + * the property state bits. Subclass constructors that fill all properties + * with loaded values must call markAllPropertiesClean to ensure all + * properties are identified as being valid. + * + *

    +     * // Returns true if all primary key properties have been set.
    +     * protected boolean isPkInitialized();
    +     *
    +     * // Returns true if all required data properties are set.
    +     * // A required data property is a non-nullable, non-primary key.
    +     * protected boolean isRequiredDataInitialized();
    +     *
    +     * // Returns true if a version property has been set.
    +     * // Note: This method is not generated if there is no version property.
    +     * protected boolean isVersionInitialized();
    +     * 
    + * + * Property state field names are defined by the concatenation of + * {@code PROPERTY_STATE_FIELD_NAME} and a zero-based decimal + * ordinal. To determine which field holds a particular property's state, + * the field ordinal is computed as the property ordinal divided by 16. The + * specific two-bit state position is the remainder of this division times 2. + * + * @throws com.amazon.carbonado.MalformedTypeException if Storable type is not well-formed + * @throws IllegalArgumentException if type is null + */ + @SuppressWarnings("unchecked") + public static Class getAbstractClass(Class type) + throws IllegalArgumentException + { + synchronized (cAbstractCache) { + Class abstractClass; + Reference> ref = cAbstractCache.get(type); + if (ref != null) { + abstractClass = (Class) ref.get(); + if (abstractClass != null) { + return abstractClass; + } + } + abstractClass = new StorableGenerator(type, GEN_ABSTRACT).generateAndInjectClass(); + cAbstractCache.put(type, new SoftReference>(abstractClass)); + return abstractClass; + } + } + + /** + * Returns a concrete Storable implementation of the given type which wraps + * another Storable. The Storable type itself may be an interface or a + * class. If it is a class, then it must not be final, and it must have a + * public, no-arg constructor. The constructor signature for the returned + * class is defined as follows: + * + *
    +     * /**
    +     *  * @param support  Custom implementation for Storable CRUD operations
    +     *  * @param storable Storable being wrapped
    +     *  */
    +     * public <init>(WrappedSupport support, Storable storable);
    +     * 
    + * + *

    Instances of the wrapped Storable delegate to the WrappedSupport for + * all CRUD operations: + * + *

      + *
    • load and tryLoad + *
    • insert and tryInsert + *
    • update and tryUpdate + *
    • delete and tryDelete + *
    + * + *

    Methods which delegate to wrapped Storable: + * + *

      + *
    • all ordinary user-defined properties + *
    • copyAllProperties + *
    • copyPrimaryKeyProperties + *
    • copyVersionProperty + *
    • copyUnequalProperties + *
    • copyDirtyProperties + *
    • hasDirtyProperties + *
    • markPropertiesClean + *
    • markAllPropertiesClean + *
    • markPropertiesDirty + *
    • markAllPropertiesDirty + *
    • hashCode + *
    • equalPrimaryKeys + *
    • equalProperties + *
    • toString + *
    • toStringKeyOnly + *
    + * + *

    Methods with special implementation: + * + *

      + *
    • all user-defined join properties (join properties query using wrapper's Storage) + *
    • storage (returns Storage used by wrapper) + *
    • storableType (returns literal class) + *
    • copy (delegates to wrapped storable, but bridge methods must be defined as well) + *
    • equals (compares Storage instance and properties) + *
    + * + * @throws com.amazon.carbonado.MalformedTypeException if Storable type is not well-formed + * @throws IllegalArgumentException if type is null + */ + @SuppressWarnings("unchecked") + public static Class getWrappedClass(Class type) + throws IllegalArgumentException + { + synchronized (cWrappedCache) { + Class wrappedClass; + Reference> ref = cWrappedCache.get(type); + if (ref != null) { + wrappedClass = (Class) ref.get(); + if (wrappedClass != null) { + return wrappedClass; + } + } + wrappedClass = new StorableGenerator(type, GEN_WRAPPED).generateAndInjectClass(); + cWrappedCache.put(type, new SoftReference>(wrappedClass)); + return wrappedClass; + } + } + + private final Class mStorableType; + private final int mGenMode; + private final TypeDesc mSupportType; + private final StorableInfo mInfo; + private final Map> mAllProperties; + private final boolean mHasJoins; + + private final ClassInjector mClassInjector; + private final ClassFile mClassFile; + + private StorableGenerator(Class storableType, int genMode) { + mStorableType = storableType; + mGenMode = genMode; + if (genMode == GEN_WRAPPED) { + mSupportType = TypeDesc.forClass(WrappedSupport.class); + } else { + mSupportType = TypeDesc.forClass(TriggerSupport.class); + } + mInfo = StorableIntrospector.examine(storableType); + mAllProperties = mInfo.getAllProperties(); + + boolean hasJoins = false; + for (StorableProperty property : mAllProperties.values()) { + if (property.isJoin()) { + hasJoins = true; + break; + } + } + mHasJoins = hasJoins; + + mClassInjector = ClassInjector.create + (storableType.getName(), storableType.getClassLoader()); + mClassFile = CodeBuilderUtil.createStorableClassFile + (mClassInjector, storableType, genMode == GEN_ABSTRACT, + StorableGenerator.class.getName()); + } + + private Class generateAndInjectClass() { + generateClass(); + Class abstractClass = mClassInjector.defineClass(mClassFile); + return (Class) abstractClass; + } + + private void generateClass() { + // Use this static method for passing uncaught exceptions. + defineUncaughtExceptionHandler(); + + // private final TriggerSupport support; + // Field is not final for GEN_WRAPPED, so that copy method can + // change WrappedSupport after calling clone. + mClassFile.addField(Modifiers.PROTECTED.toFinal(mGenMode == GEN_ABSTRACT), + SUPPORT_FIELD_NAME, + mSupportType); + + if (mGenMode == GEN_WRAPPED) { + // Add a few more fields to hold arguments passed from constructor. + + // private final wrappedStorable; + // Field is not final for GEN_WRAPPED, so that copy method can + // change wrapped Storable after calling clone. + mClassFile.addField(Modifiers.PRIVATE.toFinal(false), + WRAPPED_STORABLE_FIELD_NAME, + TypeDesc.forClass(mStorableType)); + } + + if (mGenMode == GEN_ABSTRACT) { + // Add protected constructor. + TypeDesc[] params = {mSupportType}; + + final int supportParam = 0; + MethodInfo mi = mClassFile.addConstructor(Modifiers.PROTECTED, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.invokeSuperConstructor(null); + + //// this.support = support + b.loadThis(); + b.loadLocal(b.getParameter(supportParam)); + b.storeField(SUPPORT_FIELD_NAME, mSupportType); + + b.returnVoid(); + } else if (mGenMode == GEN_WRAPPED) { + // Add public constructor. + TypeDesc[] params = {mSupportType, TypeDesc.forClass(Storable.class)}; + + final int wrappedSupportParam = 0; + final int wrappedStorableParam = 1; + MethodInfo mi = mClassFile.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.invokeSuperConstructor(null); + + //// this.wrappedSupport = wrappedSupport + b.loadThis(); + b.loadLocal(b.getParameter(wrappedSupportParam)); + b.storeField(SUPPORT_FIELD_NAME, mSupportType); + + //// this.wrappedStorable = wrappedStorable + b.loadThis(); + b.loadLocal(b.getParameter(wrappedStorableParam)); + b.checkCast(TypeDesc.forClass(mStorableType)); + b.storeField(WRAPPED_STORABLE_FIELD_NAME, TypeDesc.forClass(mStorableType)); + + b.returnVoid(); + } + + // Add static fields for adapters and constraints, and create static + // initializer to populate fields. + if (mGenMode == GEN_ABSTRACT) { + // CodeBuilder for static initializer, defined only if there's + // something to put in it. + CodeBuilder clinit = null; + + // Adapter and constraint fields are protected static. + final Modifiers fieldModifiers = Modifiers.PROTECTED.toStatic(true).toFinal(true); + + // Add adapter field. + for (StorableProperty property : mAllProperties.values()) { + StorablePropertyAdapter spa = property.getAdapter(); + if (spa == null) { + continue; + } + + String fieldName = property.getName() + ADAPTER_FIELD_ELEMENT + 0; + TypeDesc adapterType = TypeDesc.forClass + (spa.getAdapterConstructor().getDeclaringClass()); + mClassFile.addField(fieldModifiers, fieldName, adapterType); + + if (clinit == null) { + clinit = new CodeBuilder(mClassFile.addInitializer()); + } + + // Assign value to new field. + // admin$adapter$0 = new YesNoAdapter.Adapter + // (UserInfo.class, "admin", annotation); + + clinit.newObject(adapterType); + clinit.dup(); + clinit.loadConstant(TypeDesc.forClass(mStorableType)); + clinit.loadConstant(property.getName()); + + // Generate code to load property annotation third parameter. + loadPropertyAnnotation(clinit, property, spa.getAnnotation()); + + clinit.invoke(spa.getAdapterConstructor()); + clinit.storeStaticField(fieldName, adapterType); + } + + // Add contraint fields. + for (StorableProperty property : mAllProperties.values()) { + int count = property.getConstraintCount(); + for (int i=0; i property : mAllProperties.values()) { + ordinal++; + + if (property.isVersion()) { + versionOrdinal = ordinal; + } + + final String name = property.getName(); + final TypeDesc type = TypeDesc.forClass(property.getType()); + + if (property.isJoin()) { + // If generating wrapper, property access is not guarded by + // synchronization. Mark as volatile instead. + mClassFile.addField(Modifiers.PRIVATE.toVolatile(mGenMode == GEN_WRAPPED), + name, type); + requireStateField = true; + } else if (mGenMode == GEN_ABSTRACT) { + // Only define regular property fields if abstract + // class. Wrapped class doesn't reference them. Double + // words are volatile to prevent word tearing without + // explicit synchronization. + mClassFile.addField(Modifiers.PROTECTED.toVolatile(type.isDoubleWord()), + name, type); + requireStateField = true; + } + + final String stateFieldName = PROPERTY_STATE_FIELD_NAME + (ordinal >> 4); + if (ordinal == maxOrdinal || ((ordinal & 0xf) == 0xf)) { + if (requireStateField) { + // If generating wrapper, property state access is not guarded by + // synchronization. Mark as volatile instead. + mClassFile.addField + (Modifiers.PROTECTED.toVolatile(mGenMode == GEN_WRAPPED), + stateFieldName, TypeDesc.INT); + } + requireStateField = false; + } + + // Add read method. + buildReadMethod: { + Method readMethod = property.getReadMethod(); + + MethodInfo mi; + if (readMethod != null) { + mi = mClassFile.addMethod(readMethod); + } else { + // Add a synthetic protected read method. + String readName = property.getReadMethodName(); + mi = mClassFile.addMethod(Modifiers.PROTECTED, readName, type, null); + mi.markSynthetic(); + if (property.isJoin()) { + mi.addException(TypeDesc.forClass(FetchException.class)); + } + } + + if (mGenMode == GEN_ABSTRACT && property.isJoin()) { + // Synchronization is required for join property + // accessors, as they may alter bit masks. + mi.setModifiers(mi.getModifiers().toSynchronized(true)); + } + + // Now add code that actually gets the property value. + CodeBuilder b = new CodeBuilder(mi); + + if (property.isJoin()) { + // Join properties support on-demand loading. + + // Check if property has been loaded. + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.loadConstant(PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2)); + b.math(Opcode.IAND); + Label isLoaded = b.createLabel(); + b.ifZeroComparisonBranch(isLoaded, "!="); + + // Store loaded join result here. + LocalVariable join = b.createLocalVariable(name, type); + + // Check if any internal properties are nullable, but + // the matching external property is not. If so, load + // each of these special internal values and check if + // null. If null, short-circuit the load and use null + // as the join result. + + Label shortCircuit = b.createLabel(); + buildShortCircuit: { + int count = property.getJoinElementCount(); + nullPossible: { + for (int i=0; i internal = property.getInternalJoinElement(i); + if (mGenMode == GEN_ABSTRACT) { + b.loadThis(); + b.loadField(internal.getName(), + TypeDesc.forClass(internal.getType())); + } else { + b.loadThis(); + b.loadField(WRAPPED_STORABLE_FIELD_NAME, + TypeDesc.forClass(mStorableType)); + b.invoke(internal.getReadMethod()); + } + TypeDesc bindType = + CodeBuilderUtil.bindQueryParam(internal.getType()); + CodeBuilderUtil.convertValue + (b, internal.getType(), bindType.toClass()); + b.invokeInterface(queryType, WITH_METHOD_NAME, queryType, + new TypeDesc[]{bindType}); + } + + // Now run the query. + if (property.isQuery()) { + // Just save and return the query. + b.storeLocal(join); + } else { + String loadMethod = + property.isNullable() ? + TRY_LOAD_ONE_METHOD_NAME : + LOAD_ONE_METHOD_NAME; + b.invokeInterface(queryType, loadMethod, storableDesc, null); + b.checkCast(type); + b.storeLocal(join); + } + } + + // Store loaded property. + shortCircuit.setLocation(); + b.loadThis(); + b.loadLocal(join); + b.storeField(property.getName(), type); + + // Add code to identify this property as being loaded. + b.loadThis(); + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.loadConstant(PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2)); + b.math(Opcode.IOR); + b.storeField(stateFieldName, TypeDesc.INT); + + isLoaded.setLocation(); + } + + // Load property value and return it. + + if (mGenMode == GEN_ABSTRACT || property.isJoin()) { + b.loadThis(); + b.loadField(property.getName(), type); + } else { + b.loadThis(); + b.loadField(WRAPPED_STORABLE_FIELD_NAME, TypeDesc.forClass(mStorableType)); + b.invoke(readMethod); + } + + b.returnValue(type); + } + + // Add write method. + if (!property.isQuery()) { + Method writeMethod = property.getWriteMethod(); + + MethodInfo mi; + if (writeMethod != null) { + mi = mClassFile.addMethod(writeMethod); + } else { + // Add a synthetic protected write method. + String writeName = property.getWriteMethodName(); + mi = mClassFile.addMethod(Modifiers.PROTECTED, writeName, null, + new TypeDesc[]{type}); + mi.markSynthetic(); + } + + if (mGenMode == GEN_ABSTRACT) { + mi.setModifiers(mi.getModifiers().toSynchronized(true)); + } + CodeBuilder b = new CodeBuilder(mi); + + // Primary keys cannot be altered if state is "clean". + if (mGenMode == GEN_ABSTRACT && property.isPrimaryKeyMember()) { + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.loadConstant(PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2)); + b.math(Opcode.IAND); + b.loadConstant(PROPERTY_STATE_CLEAN << ((ordinal & 0xf) * 2)); + Label isMutable = b.createLabel(); + b.ifComparisonBranch(isMutable, "!="); + CodeBuilderUtil.throwException + (b, IllegalStateException.class, "Cannot alter primary key"); + isMutable.setLocation(); + } + + int spcCount = property.getConstraintCount(); + + boolean nullNotAllowed = + !property.getType().isPrimitive() && + !property.isJoin() && !property.isNullable(); + + if (mGenMode == GEN_ABSTRACT && (nullNotAllowed || spcCount > 0)) { + // Add constraint checks. + Label skipConstraints = b.createLabel(); + + if (nullNotAllowed) { + // Don't allow null value to be set. + b.loadLocal(b.getParameter(0)); + Label notNull = b.createLabel(); + b.ifNullBranch(notNull, false); + CodeBuilderUtil.throwException + (b, IllegalArgumentException.class, + "Cannot set property \"" + property.getName() + + "\" to null"); + notNull.setLocation(); + } else { + // Don't invoke constraints if value is null. + if (!property.getType().isPrimitive()) { + b.loadLocal(b.getParameter(0)); + b.ifNullBranch(skipConstraints, true); + } + } + + // Add code to invoke constraints. + + for (int spcIndex = 0; spcIndex < spcCount; spcIndex++) { + StorablePropertyConstraint spc = property.getConstraint(spcIndex); + String fieldName = + property.getName() + CONSTRAINT_FIELD_ELEMENT + spcIndex; + TypeDesc constraintType = TypeDesc.forClass + (spc.getConstraintConstructor().getDeclaringClass()); + b.loadStaticField(fieldName, constraintType); + b.loadLocal(b.getParameter(0)); + b.convert + (b.getParameter(0).getType(), TypeDesc.forClass + (spc.getConstrainMethod().getParameterTypes()[0])); + b.invoke(spc.getConstrainMethod()); + } + + skipConstraints.setLocation(); + } + + Label setValue = b.createLabel(); + + if (!property.isJoin() || Lob.class.isAssignableFrom(property.getType())) { + if (mGenMode == GEN_ABSTRACT) { + if (Lob.class.isAssignableFrom(property.getType())) { + // Contrary to how standard properties are managed, + // only mark dirty if value changed. + b.loadThis(); + b.loadField(property.getName(), type); + b.loadLocal(b.getParameter(0)); + CodeBuilderUtil.addValuesEqualCall(b, type, true, setValue, true); + } + } + + markOrdinaryPropertyDirty(b, property); + } else { + // If passed value is null, throw an + // IllegalArgumentException. Passing in null could also + // indicate that the property should be unloaded, but + // that is non-intuitive. + + b.loadLocal(b.getParameter(0)); + Label notNull = b.createLabel(); + b.ifNullBranch(notNull, false); + CodeBuilderUtil.throwException(b, IllegalArgumentException.class, null); + notNull.setLocation(); + + // Copy internal properties from joined object. + int count = property.getJoinElementCount(); + for (int i=0; i> 4), TypeDesc.INT); + b.loadConstant(PROPERTY_STATE_MASK << ((ord & 0xf) * 2)); + b.math(Opcode.IAND); + b.loadConstant(PROPERTY_STATE_CLEAN << ((ord & 0xf) * 2)); + // If not clean, skip equal check. + b.ifComparisonBranch(setInternalProp, "!="); + } else { + // Call the public isPropertyClean method since + // the raw state bits are hidden. + b.loadThis(); + b.loadConstant(internal.getName()); + b.invokeVirtual(IS_PROPERTY_CLEAN, TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.STRING}); + // If not clean, skip equal check. + b.ifZeroComparisonBranch(setInternalProp, "=="); + } + + // If new internal property value is equal to + // existing value, skip setting it. + b.loadThis(); + b.invoke(internal.getReadMethod()); + b.loadLocal(newInternalPropVar); + Label skipSetInternalProp = b.createLabel(); + CodeBuilderUtil.addValuesEqualCall + (b, TypeDesc.forClass(internal.getType()), + true, skipSetInternalProp, true); + + setInternalProp.setLocation(); + + // Call set method to ensure that state bits are + // properly adjusted. + b.loadThis(); + b.loadLocal(newInternalPropVar); + b.invoke(internal.getWriteMethod()); + + skipSetInternalProp.setLocation(); + } + + // Add code to identify this property as being loaded. + b.loadThis(); + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.loadConstant(PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2)); + b.math(Opcode.IOR); + b.storeField(stateFieldName, TypeDesc.INT); + } + + // Now add code that actually sets the property value. + + setValue.setLocation(); + + if (mGenMode == GEN_ABSTRACT || property.isJoin()) { + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.storeField(property.getName(), type); + } else { + b.loadThis(); + b.loadField(WRAPPED_STORABLE_FIELD_NAME, TypeDesc.forClass(mStorableType)); + b.loadLocal(b.getParameter(0)); + b.invoke(writeMethod); + } + + b.returnVoid(); + } + + // Add optional protected adapted read methods. + if (mGenMode == GEN_ABSTRACT && property.getAdapter() != null) { + // End name with '$' to prevent any possible collisions. + String readName = property.getReadMethodName() + '$'; + + StorablePropertyAdapter adapter = property.getAdapter(); + + for (Method adaptMethod : adapter.findAdaptMethodsFrom(type.toClass())) { + TypeDesc toType = TypeDesc.forClass(adaptMethod.getReturnType()); + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED, readName, toType, null); + mi.markSynthetic(); + + // Now add code that actually gets the property value and + // then invokes adapt method. + CodeBuilder b = new CodeBuilder(mi); + + // Push adapter class to stack. + String fieldName = property.getName() + ADAPTER_FIELD_ELEMENT + 0; + TypeDesc adapterType = TypeDesc.forClass + (adapter.getAdapterConstructor().getDeclaringClass()); + b.loadStaticField(fieldName, adapterType); + + // Load property value. + b.loadThis(); + b.loadField(property.getName(), type); + + b.invoke(adaptMethod); + b.returnValue(toType); + } + } + + // Add optional protected adapted write methods. + + // Note: Calling these methods does not affect any state bits. + // They are only intended to be used by subclasses during loading. + + if (mGenMode == GEN_ABSTRACT && property.getAdapter() != null) { + // End name with '$' to prevent any possible collisions. + String writeName = property.getWriteMethodName() + '$'; + + StorablePropertyAdapter adapter = property.getAdapter(); + + for (Method adaptMethod : adapter.findAdaptMethodsTo(type.toClass())) { + TypeDesc fromType = TypeDesc.forClass(adaptMethod.getParameterTypes()[0]); + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED, writeName, null, new TypeDesc[] {fromType}); + mi.markSynthetic(); + mi.setModifiers(mi.getModifiers().toSynchronized(true)); + + // Now add code that actually adapts parameter and then + // stores the property value. + CodeBuilder b = new CodeBuilder(mi); + + // Push this in preparation for storing a field. + b.loadThis(); + + // Push adapter class to stack. + String fieldName = property.getName() + ADAPTER_FIELD_ELEMENT + 0; + TypeDesc adapterType = TypeDesc.forClass + (adapter.getAdapterConstructor().getDeclaringClass()); + b.loadStaticField(fieldName, adapterType); + + b.loadLocal(b.getParameter(0)); + b.invoke(adaptMethod); + b.storeField(property.getName(), type); + + b.returnVoid(); + } + } + } + } + + // Add tryLoad method which delegates to abstract doTryLoad method. + addTryLoad: { + // Define the tryLoad method. + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC.toSynchronized(mGenMode == GEN_ABSTRACT), + TRY_LOAD_METHOD_NAME, TypeDesc.BOOLEAN, null); + + if (mi == null) { + break addTryLoad; + } + + mi.addException(TypeDesc.forClass(FetchException.class)); + + if (mGenMode == GEN_WRAPPED) { + callWrappedSupport(mi, null, false, null); + break addTryLoad; + } + + CodeBuilder b = new CodeBuilder(mi); + + // Check that primary key is initialized. + b.loadThis(); + b.invokeVirtual(IS_PK_INITIALIZED_METHOD_NAME, TypeDesc.BOOLEAN, null); + Label pkInitialized = b.createLabel(); + b.ifZeroComparisonBranch(pkInitialized, "!="); + + Label loaded = b.createLabel(); + Label notLoaded = b.createLabel(); + + if (mInfo.getAlternateKeyCount() == 0) { + CodeBuilderUtil.throwException(b, IllegalStateException.class, + "Primary key not fully specified"); + } else { + // If any alternate keys, check them too. + + // Load our Storage, in preparation for query against it. + loadStorageForFetch(b, TypeDesc.forClass(mStorableType)); + + Label runQuery = b.createLabel(); + TypeDesc queryType = TypeDesc.forClass(Query.class); + + for (int i=0; i altKey = mInfo.getAlternateKey(i); + + // Form query filter. + StringBuilder queryBuilder = new StringBuilder(); + for (OrderedProperty op : altKey.getProperties()) { + if (queryBuilder.length() > 0) { + queryBuilder.append(" & "); + } + queryBuilder.append(op.getChainedProperty().toString()); + queryBuilder.append(" = ?"); + } + + // Get query instance from Storage already loaded on stack. + b.loadConstant(queryBuilder.toString()); + b.invokeInterface(TypeDesc.forClass(Storage.class), + QUERY_METHOD_NAME, queryType, + new TypeDesc[]{TypeDesc.STRING}); + + // Now fill in the parameters of the query. + for (OrderedProperty op : altKey.getProperties()) { + StorableProperty prop = op.getChainedProperty().getPrimeProperty(); + b.loadThis(); + TypeDesc propType = TypeDesc.forClass(prop.getType()); + b.loadField(prop.getName(), propType); + TypeDesc bindType = CodeBuilderUtil.bindQueryParam(prop.getType()); + CodeBuilderUtil.convertValue(b, prop.getType(), bindType.toClass()); + b.invokeInterface(queryType, WITH_METHOD_NAME, queryType, + new TypeDesc[]{bindType}); + } + + b.branch(runQuery); + + noAltKey.setLocation(); + } + + CodeBuilderUtil.throwException(b, IllegalStateException.class, + "Primary or alternate key not fully specified"); + + // Run query sitting on the stack. + runQuery.setLocation(); + + b.invokeInterface(queryType, TRY_LOAD_ONE_METHOD_NAME, + TypeDesc.forClass(Storable.class), null); + LocalVariable fetchedVar = b.createLocalVariable(null, TypeDesc.OBJECT); + b.storeLocal(fetchedVar); + + // If query fetch is null, then object not found. Return false. + b.loadLocal(fetchedVar); + b.ifNullBranch(notLoaded, true); + + // Copy all properties from fetched object into this one. + + // Allow copy to destroy everything, including primary key. + b.loadThis(); + b.invokeVirtual(MARK_ALL_PROPERTIES_DIRTY, null, null); + + b.loadLocal(fetchedVar); + b.checkCast(TypeDesc.forClass(mStorableType)); + b.loadThis(); + b.invokeInterface(TypeDesc.forClass(Storable.class), + COPY_ALL_PROPERTIES, null, + new TypeDesc[] {TypeDesc.forClass(Storable.class)}); + + b.branch(loaded); + } + + pkInitialized.setLocation(); + + // Call doTryLoad and mark all properties as clean if load succeeded. + b.loadThis(); + b.invokeVirtual(DO_TRY_LOAD_METHOD_NAME, TypeDesc.BOOLEAN, null); + + b.ifZeroComparisonBranch(notLoaded, "=="); + + loaded.setLocation(); + // Only mark properties clean if doTryLoad returned true. + b.loadThis(); + b.invokeVirtual(MARK_ALL_PROPERTIES_CLEAN, null, null); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + notLoaded.setLocation(); + // Mark properties dirty, to be consistent with a delete side-effect. + b.loadThis(); + b.invokeVirtual(MARK_PROPERTIES_DIRTY, null, null); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + if (mGenMode == GEN_ABSTRACT) { + // Define the abstract method. + mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_LOAD_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(FetchException.class)); + } + } + + // Add load method which calls tryLoad. + addLoad: { + // Define the load method. + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC.toSynchronized(mGenMode == GEN_ABSTRACT), + LOAD_METHOD_NAME, null, null); + + if (mi == null) { + break addLoad; + } + + mi.addException(TypeDesc.forClass(FetchException.class)); + + if (mGenMode == GEN_WRAPPED) { + callWrappedSupport(mi, null, false, FetchNoneException.class); + break addLoad; + } + + CodeBuilder b = new CodeBuilder(mi); + + // Call tryLoad and throw an exception if false returned. + b.loadThis(); + b.invokeVirtual(TRY_LOAD_METHOD_NAME, TypeDesc.BOOLEAN, null); + + Label wasNotLoaded = b.createLabel(); + b.ifZeroComparisonBranch(wasNotLoaded, "=="); + b.returnVoid(); + + wasNotLoaded.setLocation(); + + TypeDesc noMatchesType = TypeDesc.forClass(FetchNoneException.class); + b.newObject(noMatchesType); + b.dup(); + b.loadThis(); + b.invokeVirtual(TO_STRING_KEY_ONLY_METHOD_NAME, TypeDesc.STRING, null); + b.invokeConstructor(noMatchesType, new TypeDesc[] {TypeDesc.STRING}); + b.throwObject(); + } + + final TypeDesc triggerType = TypeDesc.forClass(Trigger.class); + final TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + + // Add insert(boolean forTry) method which delegates to abstract doTryInsert method. + if (mGenMode == GEN_ABSTRACT) { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PRIVATE.toSynchronized(true), + PRIVATE_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, new TypeDesc[] {TypeDesc.BOOLEAN}); + mi.addException(TypeDesc.forClass(PersistException.class)); + + CodeBuilder b = new CodeBuilder(mi); + + LocalVariable forTryVar = b.getParameter(0); + LocalVariable triggerVar = b.createLocalVariable(null, triggerType); + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + LocalVariable stateVar = b.createLocalVariable(null, TypeDesc.OBJECT); + + Label tryStart = addGetTriggerAndEnterTxn + (b, INSERT_OP, forTryVar, false, triggerVar, txnVar, stateVar); + + // Perform pk check after trigger has run, to allow it to define pk. + requirePkInitialized(b, CHECK_PK_FOR_INSERT_METHOD_NAME); + + // Call doTryInsert. + b.loadThis(); + b.invokeVirtual(DO_TRY_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, null); + + Label notInserted = b.createLabel(); + b.ifZeroComparisonBranch(notInserted, "=="); + + addTriggerAfterAndExitTxn + (b, INSERT_OP, forTryVar, false, triggerVar, txnVar, stateVar); + + // Only mark properties clean if doTryInsert returned true. + b.loadThis(); + b.invokeVirtual(MARK_ALL_PROPERTIES_CLEAN, null, null); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + notInserted.setLocation(); + addTriggerFailedAndExitTxn(b, INSERT_OP, triggerVar, txnVar, stateVar); + + b.loadLocal(forTryVar); + Label isForTry = b.createLabel(); + b.ifZeroComparisonBranch(isForTry, "!="); + + TypeDesc constraintType = TypeDesc.forClass(UniqueConstraintException.class); + b.newObject(constraintType); + b.dup(); + b.loadThis(); + b.invokeVirtual(TO_STRING_METHOD_NAME, TypeDesc.STRING, null); + b.invokeConstructor(constraintType, new TypeDesc[] {TypeDesc.STRING}); + b.throwObject(); + + isForTry.setLocation(); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + addTriggerFailedAndExitTxn + (b, INSERT_OP, forTryVar, false, triggerVar, txnVar, stateVar, tryStart); + + // Define the abstract method. + mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(PersistException.class)); + } + + // Add insert method which calls insert(forTry = false) + addInsert: { + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC, INSERT_METHOD_NAME, null, null); + + if (mi == null) { + break addInsert; + } + + mi.addException(TypeDesc.forClass(PersistException.class)); + + if (mGenMode == GEN_WRAPPED) { + callWrappedSupport(mi, INSERT_OP, false, UniqueConstraintException.class); + break addInsert; + } + + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadConstant(false); + b.invokePrivate(PRIVATE_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.BOOLEAN}); + b.pop(); + b.returnVoid(); + } + + // Add tryInsert method which calls insert(forTry = true) + addTryInsert: { + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC, TRY_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, null); + + if (mi == null) { + break addTryInsert; + } + + mi.addException(TypeDesc.forClass(PersistException.class)); + + if (mGenMode == GEN_WRAPPED) { + callWrappedSupport(mi, INSERT_OP, true, null); + break addTryInsert; + } + + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadConstant(true); + b.invokePrivate(PRIVATE_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.BOOLEAN}); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Add update(boolean forTry) method which delegates to abstract doTryUpdate method. + if (mGenMode == GEN_ABSTRACT) { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PRIVATE.toSynchronized(true), + PRIVATE_UPDATE_METHOD_NAME, TypeDesc.BOOLEAN, new TypeDesc[] {TypeDesc.BOOLEAN}); + mi.addException(TypeDesc.forClass(PersistException.class)); + + CodeBuilder b = new CodeBuilder(mi); + + requirePkInitialized(b, CHECK_PK_FOR_UPDATE_METHOD_NAME); + + // If version property is present, it too must be initialized. The + // versionOrdinal variable was set earlier, when properties were defined. + if (versionOrdinal >= 0) { + b.loadThis(); + b.loadField(PROPERTY_STATE_FIELD_NAME + (versionOrdinal >> 4), TypeDesc.INT); + b.loadConstant(PROPERTY_STATE_MASK << ((versionOrdinal & 0xf) * 2)); + b.math(Opcode.IAND); + Label versionIsSet = b.createLabel(); + b.ifZeroComparisonBranch(versionIsSet, "!="); + CodeBuilderUtil.throwException + (b, IllegalStateException.class, "Version not set"); + versionIsSet.setLocation(); + } + + LocalVariable forTryVar = b.getParameter(0); + LocalVariable triggerVar = b.createLocalVariable(null, triggerType); + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + LocalVariable stateVar = b.createLocalVariable(null, TypeDesc.OBJECT); + + Label tryStart = addGetTriggerAndEnterTxn + (b, UPDATE_OP, forTryVar, false, triggerVar, txnVar, stateVar); + + // If no properties are dirty, then don't update. + Label doUpdate = b.createLabel(); + branchIfDirty(b, true, doUpdate); + + // Even though there was no update, still need tryLoad side-effect. + { + Label tryStart2 = b.createLabel().setLocation(); + b.loadThis(); + b.invokeVirtual(DO_TRY_LOAD_METHOD_NAME, TypeDesc.BOOLEAN, null); + + Label notUpdated = b.createLabel(); + b.ifZeroComparisonBranch(notUpdated, "=="); + + // Only mark properties clean if doTryLoad returned true. + b.loadThis(); + b.invokeVirtual(MARK_ALL_PROPERTIES_CLEAN, null, null); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + notUpdated.setLocation(); + + // Mark properties dirty, to be consistent with a delete side-effect. + b.loadThis(); + b.invokeVirtual(MARK_PROPERTIES_DIRTY, null, null); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + Label tryEnd = b.createLabel().setLocation(); + b.exceptionHandler(tryStart2, tryEnd, FetchException.class.getName()); + b.invokeVirtual(FetchException.class.getName(), "toPersistException", + TypeDesc.forClass(PersistException.class), null); + b.throwObject(); + } + + doUpdate.setLocation(); + + // Call doTryUpdate. + b.loadThis(); + b.invokeVirtual(DO_TRY_UPDATE_METHOD_NAME, TypeDesc.BOOLEAN, null); + + Label notUpdated = b.createLabel(); + b.ifZeroComparisonBranch(notUpdated, "=="); + + addTriggerAfterAndExitTxn + (b, UPDATE_OP, forTryVar, false, triggerVar, txnVar, stateVar); + + // Only mark properties clean if doUpdate returned true. + b.loadThis(); + // Note: all properties marked clean because doUpdate should have + // loaded values for all properties. + b.invokeVirtual(MARK_ALL_PROPERTIES_CLEAN, null, null); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + notUpdated.setLocation(); + addTriggerFailedAndExitTxn(b, UPDATE_OP, triggerVar, txnVar, stateVar); + + // Mark properties dirty, to be consistent with a delete side-effect. + b.loadThis(); + b.invokeVirtual(MARK_PROPERTIES_DIRTY, null, null); + + b.loadLocal(forTryVar); + Label isForTry = b.createLabel(); + b.ifZeroComparisonBranch(isForTry, "!="); + + TypeDesc persistNoneType = TypeDesc.forClass(PersistNoneException.class); + b.newObject(persistNoneType); + b.dup(); + b.loadConstant("Cannot update missing object: "); + b.loadThis(); + b.invokeVirtual(TO_STRING_METHOD_NAME, TypeDesc.STRING, null); + b.invokeVirtual(TypeDesc.STRING, "concat", + TypeDesc.STRING, new TypeDesc[] {TypeDesc.STRING}); + b.invokeConstructor(persistNoneType, new TypeDesc[] {TypeDesc.STRING}); + b.throwObject(); + + isForTry.setLocation(); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + addTriggerFailedAndExitTxn + (b, UPDATE_OP, forTryVar, false, triggerVar, txnVar, stateVar, tryStart); + + // Define the abstract method. + mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_UPDATE_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(PersistException.class)); + } + + // Add update method which calls update(forTry = false) + addUpdate: { + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC, UPDATE_METHOD_NAME, null, null); + + if (mi == null) { + break addUpdate; + } + + mi.addException(TypeDesc.forClass(PersistException.class)); + + if (mGenMode == GEN_WRAPPED) { + callWrappedSupport(mi, UPDATE_OP, false, PersistNoneException.class); + break addUpdate; + } + + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadConstant(false); + b.invokePrivate(PRIVATE_UPDATE_METHOD_NAME, TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.BOOLEAN}); + b.pop(); + b.returnVoid(); + } + + // Add tryUpdate method which calls update(forTry = true) + addTryUpdate: { + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC, TRY_UPDATE_METHOD_NAME, TypeDesc.BOOLEAN, null); + + if (mi == null) { + break addTryUpdate; + } + + mi.addException(TypeDesc.forClass(PersistException.class)); + + if (mGenMode == GEN_WRAPPED) { + callWrappedSupport(mi, UPDATE_OP, true, null); + break addTryUpdate; + } + + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadConstant(true); + b.invokePrivate(PRIVATE_UPDATE_METHOD_NAME, TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.BOOLEAN}); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Add delete(boolean forTry) method which delegates to abstract doTryDelete method. + if (mGenMode == GEN_ABSTRACT) { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PRIVATE.toSynchronized(true), + PRIVATE_DELETE_METHOD_NAME, TypeDesc.BOOLEAN, new TypeDesc[] {TypeDesc.BOOLEAN}); + mi.addException(TypeDesc.forClass(PersistException.class)); + + CodeBuilder b = new CodeBuilder(mi); + + requirePkInitialized(b, CHECK_PK_FOR_DELETE_METHOD_NAME); + + LocalVariable forTryVar = b.getParameter(0); + LocalVariable triggerVar = b.createLocalVariable(null, triggerType); + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + LocalVariable stateVar = b.createLocalVariable(null, TypeDesc.OBJECT); + + Label tryStart = addGetTriggerAndEnterTxn + (b, DELETE_OP, forTryVar, false, triggerVar, txnVar, stateVar); + + // Call doTryDelete. + b.loadThis(); + b.invokeVirtual(DO_TRY_DELETE_METHOD_NAME, TypeDesc.BOOLEAN, null); + + b.loadThis(); + b.invokeVirtual(MARK_PROPERTIES_DIRTY, null, null); + + Label notDeleted = b.createLabel(); + b.ifZeroComparisonBranch(notDeleted, "=="); + + addTriggerAfterAndExitTxn + (b, DELETE_OP, forTryVar, false, triggerVar, txnVar, stateVar); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + notDeleted.setLocation(); + addTriggerFailedAndExitTxn(b, DELETE_OP, triggerVar, txnVar, stateVar); + + b.loadLocal(forTryVar); + Label isForTry = b.createLabel(); + b.ifZeroComparisonBranch(isForTry, "!="); + + TypeDesc persistNoneType = TypeDesc.forClass(PersistNoneException.class); + b.newObject(persistNoneType); + b.dup(); + b.loadConstant("Cannot delete missing object: "); + b.loadThis(); + b.invokeVirtual(TO_STRING_METHOD_NAME, TypeDesc.STRING, null); + b.invokeVirtual(TypeDesc.STRING, "concat", + TypeDesc.STRING, new TypeDesc[] {TypeDesc.STRING}); + b.invokeConstructor(persistNoneType, new TypeDesc[] {TypeDesc.STRING}); + b.throwObject(); + + isForTry.setLocation(); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + addTriggerFailedAndExitTxn + (b, DELETE_OP, forTryVar, false, triggerVar, txnVar, stateVar, tryStart); + + // Define the abstract method. + mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_DELETE_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(PersistException.class)); + } + + // Add delete method which calls delete(forTry = false) + addDelete: { + // Define the delete method. + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC, DELETE_METHOD_NAME, null, null); + + if (mi == null) { + break addDelete; + } + + mi.addException(TypeDesc.forClass(PersistException.class)); + + if (mGenMode == GEN_WRAPPED) { + callWrappedSupport(mi, DELETE_OP, false, PersistNoneException.class); + break addDelete; + } + + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadConstant(false); + b.invokePrivate(PRIVATE_DELETE_METHOD_NAME, TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.BOOLEAN}); + b.pop(); + b.returnVoid(); + } + + // Add tryDelete method which calls delete(forTry = true) + addTryDelete: { + // Define the delete method. + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC, TRY_DELETE_METHOD_NAME, TypeDesc.BOOLEAN, null); + + if (mi == null) { + break addTryDelete; + } + + mi.addException(TypeDesc.forClass(PersistException.class)); + + if (mGenMode == GEN_WRAPPED) { + callWrappedSupport(mi, DELETE_OP, true, null); + break addTryDelete; + } + + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadConstant(true); + b.invokePrivate(PRIVATE_DELETE_METHOD_NAME, TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.BOOLEAN}); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Add storableType method + addStorableType: { + final TypeDesc type = TypeDesc.forClass(mStorableType); + final TypeDesc storableClassType = TypeDesc.forClass(Class.class); + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC, STORABLE_TYPE_METHOD_NAME, storableClassType, null); + + if (mi == null) { + break addStorableType; + } + + CodeBuilder b = new CodeBuilder(mi); + b.loadConstant(type); + b.returnValue(storableClassType); + } + + // Add copy method. + addCopy: { + TypeDesc type = TypeDesc.forClass(mInfo.getStorableType()); + + // Add copy method. + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC.toSynchronized(mGenMode == GEN_ABSTRACT), + COPY_METHOD_NAME, mClassFile.getType(), null); + + if (mi == null) { + break addCopy; + } + + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.invokeVirtual(CLONE_METHOD_NAME, TypeDesc.OBJECT, null); + b.checkCast(mClassFile.getType()); + + if (mGenMode == GEN_WRAPPED) { + // Need to do a deeper copy. + + LocalVariable copiedVar = b.createLocalVariable(null, mClassFile.getType()); + b.storeLocal(copiedVar); + + // First copy the wrapped Storable. + b.loadLocal(copiedVar); // storeField later + b.loadThis(); + b.loadField(WRAPPED_STORABLE_FIELD_NAME, TypeDesc.forClass(mStorableType)); + b.invoke(lookupMethod(mStorableType, COPY_METHOD_NAME, null)); + b.checkCast(TypeDesc.forClass(mStorableType)); + b.storeField(WRAPPED_STORABLE_FIELD_NAME, TypeDesc.forClass(mStorableType)); + + // Replace the WrappedSupport, passing in copy of wrapped Storable. + b.loadLocal(copiedVar); // storeField later + b.loadThis(); + b.loadField(SUPPORT_FIELD_NAME, mSupportType); + b.loadLocal(copiedVar); + b.loadField(WRAPPED_STORABLE_FIELD_NAME, TypeDesc.forClass(mStorableType)); + + b.invokeInterface(WrappedSupport.class.getName(), + CREATE_WRAPPED_SUPPORT_METHOD_NAME, + mSupportType, + new TypeDesc[] {TypeDesc.forClass(Storable.class)}); + + // Store new WrappedSupport in copy. + b.storeField(SUPPORT_FIELD_NAME, mSupportType); + + b.loadLocal(copiedVar); + } + + b.returnValue(type); + } + + // Part of properly defining copy method, except needs to be added even + // if copy method was not added because it is inherited and final. + CodeBuilderUtil.defineCopyBridges(mClassFile, mInfo.getStorableType()); + + // Create all the property copier methods. + // Boolean params: pkProperties, versionProperty, dataProperties, unequalOnly, dirtyOnly + addCopyPropertiesMethod(COPY_ALL_PROPERTIES, + true, true, true, false, false); + addCopyPropertiesMethod(COPY_PRIMARY_KEY_PROPERTIES, + true, false, false, false, false); + addCopyPropertiesMethod(COPY_VERSION_PROPERTY, + false, true, false, false, false); + addCopyPropertiesMethod(COPY_UNEQUAL_PROPERTIES, + false, true, true, true, false); + addCopyPropertiesMethod(COPY_DIRTY_PROPERTIES, + false, true, true, false, true); + + // Define hasDirtyProperties method. + addHasDirtyProps: { + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC, HAS_DIRTY_PROPERTIES, TypeDesc.BOOLEAN, null); + + if (mi == null) { + break addHasDirtyProps; + } + + if (mGenMode == GEN_WRAPPED) { + callWrappedStorable(mi); + break addHasDirtyProps; + } + + CodeBuilder b = new CodeBuilder(mi); + Label isDirty = b.createLabel(); + branchIfDirty(b, false, isDirty); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + isDirty.setLocation(); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Define isPropertyUninitialized, isPropertyDirty, and isPropertyClean methods. + addPropertyStateExtractMethod(); + addPropertyStateCheckMethod(IS_PROPERTY_UNINITIALIZED, PROPERTY_STATE_UNINITIALIZED); + addPropertyStateCheckMethod(IS_PROPERTY_DIRTY, PROPERTY_STATE_DIRTY); + addPropertyStateCheckMethod(IS_PROPERTY_CLEAN, PROPERTY_STATE_CLEAN); + + // Define isPropertySupported method. + addIsPropertySupported: { + MethodInfo mi = addMethodIfNotFinal + (Modifiers.PUBLIC, IS_PROPERTY_SUPPORTED, + TypeDesc.BOOLEAN, new TypeDesc[] {TypeDesc.STRING}); + + if (mi == null) { + break addIsPropertySupported; + } + + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(SUPPORT_FIELD_NAME, mSupportType); + b.loadLocal(b.getParameter(0)); + b.invokeInterface(mSupportType, "isPropertySupported", TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.STRING}); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Define standard object methods. + addHashCodeMethod(); + addEqualsMethod(EQUAL_FULL); + addEqualsMethod(EQUAL_KEYS); + addEqualsMethod(EQUAL_PROPERTIES); + addToStringMethod(false); + addToStringMethod(true); + + addMarkCleanMethod(MARK_PROPERTIES_CLEAN); + addMarkCleanMethod(MARK_ALL_PROPERTIES_CLEAN); + addMarkDirtyMethod(MARK_PROPERTIES_DIRTY); + addMarkDirtyMethod(MARK_ALL_PROPERTIES_DIRTY); + + if (mGenMode == GEN_ABSTRACT) { + // Define protected isPkInitialized method. + addIsInitializedMethod + (IS_PK_INITIALIZED_METHOD_NAME, mInfo.getPrimaryKeyProperties()); + + // Define protected methods to check if alternate key is initialized. + addAltKeyMethods: + for (int i=0; i> altProps = + new HashMap>(); + + StorableKey altKey = mInfo.getAlternateKey(i); + + for (OrderedProperty op : altKey.getProperties()) { + ChainedProperty cp = op.getChainedProperty(); + if (cp.getChainCount() > 0) { + // This should not be possible. + continue addAltKeyMethods; + } + StorableProperty property = cp.getPrimeProperty(); + altProps.put(property.getName(), property); + } + + addIsInitializedMethod(IS_ALT_KEY_INITIALIZED_PREFIX + i, altProps); + } + + // Define protected isRequiredDataInitialized method. + defineIsRequiredDataInitialized: { + Map> requiredProperties = + new HashMap>(); + + for (StorableProperty property : mAllProperties.values()) { + if (!property.isPrimaryKeyMember() && + !property.isJoin() && + !property.isNullable()) { + + requiredProperties.put(property.getName(), property); + } + } + + addIsInitializedMethod + (IS_REQUIRED_DATA_INITIALIZED_METHOD_NAME, requiredProperties); + } + + // Define optional protected isVersionInitialized method. The + // versionOrdinal variable was set earlier, when properties were defined. + if (versionOrdinal >= 0) { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED, IS_VERSION_INITIALIZED_METHOD_NAME, + TypeDesc.BOOLEAN, null); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadField(PROPERTY_STATE_FIELD_NAME + (versionOrdinal >> 4), TypeDesc.INT); + b.loadConstant(PROPERTY_STATE_MASK << ((versionOrdinal & 0xf) * 2)); + b.math(Opcode.IAND); + // zero == false, not zero == true + b.returnValue(TypeDesc.BOOLEAN); + } + } + } + + /** + * If GEN_WRAPPED, generates a method implementation which delgates to the + * WrappedSupport. Also clears join property state if called method + * returns normally. + * + * @param opType optional, is one of INSERT_OP, UPDATE_OP, or DELETE_OP, for trigger support + * @param forTry used for INSERT_OP, UPDATE_OP, or DELETE_OP + * @param exceptionType optional - if called method throws this exception, + * join property state is still cleared. + */ + private void callWrappedSupport(MethodInfo mi, + String opType, + boolean forTry, + Class exceptionType) + { + if (mGenMode == GEN_ABSTRACT || !mHasJoins) { + // Don't need to clear state bits. + exceptionType = null; + } + + CodeBuilder b = new CodeBuilder(mi); + + final TypeDesc triggerType = TypeDesc.forClass(Trigger.class); + final TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + + LocalVariable triggerVar = b.createLocalVariable(null, triggerType); + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + LocalVariable stateVar = b.createLocalVariable(null, TypeDesc.OBJECT); + + Label tryStart; + if (opType == null) { + tryStart = b.createLabel().setLocation(); + } else { + tryStart = addGetTriggerAndEnterTxn + (b, opType, null, forTry, triggerVar, txnVar, stateVar); + } + + b.loadThis(); + b.loadField(SUPPORT_FIELD_NAME, mSupportType); + Method method = lookupMethod(WrappedSupport.class, mi); + b.invoke(method); + + Label tryEnd = b.createLabel().setLocation(); + + clearState(b); + + if (method.getReturnType() == void.class) { + if (opType != null) { + addTriggerAfterAndExitTxn(b, opType, null, forTry, triggerVar, txnVar, stateVar); + } + b.returnVoid(); + } else { + if (opType != null) { + Label notDone = b.createLabel(); + b.ifZeroComparisonBranch(notDone, "=="); + addTriggerAfterAndExitTxn(b, opType, null, forTry, triggerVar, txnVar, stateVar); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + notDone.setLocation(); + addTriggerFailedAndExitTxn(b, opType, triggerVar, txnVar, stateVar); + b.loadConstant(false); + } + b.returnValue(TypeDesc.forClass(method.getReturnType())); + } + + if (opType != null) { + addTriggerFailedAndExitTxn + (b, opType, null, forTry, triggerVar, txnVar, stateVar, tryStart); + } + + if (exceptionType != null) { + b.exceptionHandler(tryStart, tryEnd, exceptionType.getName()); + clearState(b); + b.throwObject(); + } + } + + /** + * If GEN_WRAPPED, generates a method implementation which delgates to the + * wrapped Storable. + */ + private void callWrappedStorable(MethodInfo mi) { + callWrappedStorable(mi, new CodeBuilder(mi)); + } + + /** + * If GEN_WRAPPED, generates a method implementation which delgates to the + * wrapped Storable. + */ + private void callWrappedStorable(MethodInfo mi, CodeBuilder b) { + b.loadThis(); + b.loadField(WRAPPED_STORABLE_FIELD_NAME, TypeDesc.forClass(mStorableType)); + + int count = mi.getMethodDescriptor().getParameterCount(); + for (int j=0; j> 4); + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.storeLocal(stateBits); + } + + Label skipCopy = b.createLabel(); + + // Check if independent property is supported, and skip if not. + if (property.isIndependent()) { + addSkipIndependent(b, target, property, skipCopy); + } + + // Skip property if uninitialized. + b.loadLocal(stateBits); + b.loadConstant(mask); + b.math(Opcode.IAND); + b.ifZeroComparisonBranch(skipCopy, "=="); + + if (dirtyOnly) { + // Add code to find out if property has been dirty. + b.loadLocal(stateBits); + b.loadConstant(mask); + b.math(Opcode.IAND); + b.loadConstant(PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2)); + b.ifComparisonBranch(skipCopy, "!="); + } + + TypeDesc type = TypeDesc.forClass(property.getType()); + + if (unequalOnly) { + // Add code to find out if they're equal. + b.loadThis(); + b.loadField(property.getName(), type); // [this.propValue + b.loadLocal(target); // [this.propValue, target + b.invoke(property.getReadMethod()); // [this.propValue, target.propValue + CodeBuilderUtil.addValuesEqualCall + (b, TypeDesc.forClass(property.getType()), true, skipCopy, true); + } + + b.loadLocal(target); // [target + b.loadThis(); // [target, this + b.loadField(property.getName(), type); // [target, this.propValue + mutateProperty(b, property, type); + + skipCopy.setLocation(); + } + + ordinal++; + if ((mask <<= 2) == 0) { + mask = 3; + stateBits = null; + } + } + + b.returnVoid(); + } + + private void addSkipIndependent(CodeBuilder b, + LocalVariable target, + StorableProperty property, + Label skipCopy) + { + TypeDesc storableTypeDesc = TypeDesc.forClass(Storable.class); + + if (target != null) { + b.loadLocal(target); + b.loadConstant(property.getName()); + b.invokeInterface(storableTypeDesc, + "isPropertySupported", + TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.STRING}); + b.ifZeroComparisonBranch(skipCopy, "=="); + } + + b.loadThis(); + b.loadConstant(property.getName()); + b.invokeInterface(storableTypeDesc, + "isPropertySupported", + TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.STRING}); + b.ifZeroComparisonBranch(skipCopy, "=="); + } + + /** + * Puts the value on the stack into the specified storable. If a write method is defined + * uses it, otherwise just shoves the value into the appropriate field. + * + * entry stack: [storable, value + * exit stack: [ + * + * @param b - {@link CodeBuilder} to which to add the mutation code + * @param property - property to mutate + * @param type - type of the property + */ + private void mutateProperty(CodeBuilder b, StorableProperty property, TypeDesc type) { + if (property.getWriteMethod() == null) { + b.storeField(property.getName(), type); + } else { + b.invoke(property.getWriteMethod()); + } + } + + /** + * Generates code that loads a property annotation to the stack. + */ + private void loadPropertyAnnotation(CodeBuilder b, + StorableProperty property, + StorablePropertyAnnotation annotation) { + /* Example + UserInfo.class.getMethod("setFirstName", new Class[] {String.class}) + .getAnnotation(LengthConstraint.class) + */ + + String methodName = annotation.getAnnotatedMethod().getName(); + boolean isAccessor = !methodName.startsWith("set"); + + b.loadConstant(TypeDesc.forClass(property.getEnclosingType())); + b.loadConstant(methodName); + if (isAccessor) { + // Accessor method has no parameters. + b.loadNull(); + } else { + // Mutator method has one parameter. + b.loadConstant(1); + b.newObject(TypeDesc.forClass(Class[].class)); + b.dup(); + b.loadConstant(0); + b.loadConstant(TypeDesc.forClass(property.getType())); + b.storeToArray(TypeDesc.forClass(Class[].class)); + } + b.invokeVirtual(Class.class.getName(), "getMethod", + TypeDesc.forClass(Method.class), new TypeDesc[] { + TypeDesc.STRING, TypeDesc.forClass(Class[].class) + }); + b.loadConstant(TypeDesc.forClass(annotation.getAnnotationType())); + b.invokeVirtual(Method.class.getName(), "getAnnotation", + TypeDesc.forClass(Annotation.class), new TypeDesc[] { + TypeDesc.forClass(Class.class) + }); + b.checkCast(TypeDesc.forClass(annotation.getAnnotationType())); + } + + /** + * Generates code that loads a Storage instance on the stack, throwing a + * FetchException if Storage request fails. + * + * @param type type of Storage to request + */ + private void loadStorageForFetch(CodeBuilder b, TypeDesc type) { + b.loadThis(); + b.loadField(SUPPORT_FIELD_NAME, mSupportType); + TypeDesc storageType = TypeDesc.forClass(Storage.class); + + TypeDesc repositoryType = TypeDesc.forClass(Repository.class); + b.invokeInterface + (mSupportType, "getRootRepository", repositoryType, null); + b.loadConstant(type); + + // This may throw a RepositoryException. + Label tryStart = b.createLabel().setLocation(); + b.invokeInterface(repositoryType, STORAGE_FOR_METHOD_NAME, storageType, + new TypeDesc[]{TypeDesc.forClass(Class.class)}); + Label tryEnd = b.createLabel().setLocation(); + Label noException = b.createLabel(); + b.branch(noException); + + b.exceptionHandler(tryStart, tryEnd, + RepositoryException.class.getName()); + b.invokeVirtual + (RepositoryException.class.getName(), "toFetchException", + TypeDesc.forClass(FetchException.class), null); + b.throwObject(); + + noException.setLocation(); + } + + /** + * For the given join property, marks all of its dependent internal join + * element properties as dirty. + */ + /* + private void markInternalJoinElementsDirty(CodeBuilder b, StorableProperty joinProperty) { + int count = mAllProperties.size(); + + int ordinal = 0; + int mask = 0; + for (StorableProperty property : mAllProperties.values()) { + if (property != joinProperty && !property.isJoin()) { + // Check to see if property is an internal member of joinProperty. + for (int i=joinProperty.getJoinElementCount(); --i>=0; ) { + if (property == joinProperty.getInternalJoinElement(i)) { + mask |= PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2); + } + } + } + ordinal++; + if (((ordinal & 0xf) == 0 || ordinal >= count) && mask != 0) { + String stateFieldName = PROPERTY_STATE_FIELD_NAME + ((ordinal - 1) >> 4); + b.loadThis(); + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.loadConstant(mask); + b.math(Opcode.IOR); + b.storeField(stateFieldName, TypeDesc.INT); + mask = 0; + } + } + } + */ + + /** + * Generates code to set all state properties to zero. + */ + private void clearState(CodeBuilder b) { + int ordinal = -1; + int maxOrdinal = mAllProperties.size() - 1; + boolean requireStateField = false; + + for (StorableProperty property : mAllProperties.values()) { + ordinal++; + + if (property.isJoin() || mGenMode == GEN_ABSTRACT) { + requireStateField = true; + } + + if (ordinal == maxOrdinal || ((ordinal & 0xf) == 0xf)) { + if (requireStateField) { + String stateFieldName = PROPERTY_STATE_FIELD_NAME + (ordinal >> 4); + + b.loadThis(); + b.loadConstant(0); + b.storeField(stateFieldName, TypeDesc.INT); + } + requireStateField = false; + } + } + } + + private void addMarkCleanMethod(String name) { + MethodInfo mi = + addMethodIfNotFinal (Modifiers.PUBLIC.toSynchronized(true), name, null, null); + + if (mi == null) { + return; + } + + CodeBuilder b = new CodeBuilder(mi); + + if (mGenMode == GEN_WRAPPED) { + clearState(b); + callWrappedStorable(mi, b); + return; + } + + final int count = mAllProperties.size(); + int ordinal = 0; + int andMask = 0; + int orMask = 0; + + for (StorableProperty property : mAllProperties.values()) { + if (property.isQuery()) { + // Don't erase cached query. + andMask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2); + } else if (!property.isJoin()) { + if (name == MARK_ALL_PROPERTIES_CLEAN) { + // Force clean state (1) always. + orMask |= PROPERTY_STATE_CLEAN << ((ordinal & 0xf) * 2); + } else if (name == MARK_PROPERTIES_CLEAN) { + // Mask will convert dirty (3) to clean (1). State 2, which + // is illegal, is converted to 0. + andMask |= PROPERTY_STATE_CLEAN << ((ordinal & 0xf) * 2); + } + } + + ordinal++; + if ((ordinal & 0xf) == 0 || ordinal >= count) { + String stateFieldName = PROPERTY_STATE_FIELD_NAME + ((ordinal - 1) >> 4); + b.loadThis(); + if (andMask == 0) { + b.loadConstant(orMask); + } else { + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.loadConstant(andMask); + b.math(Opcode.IAND); + if (orMask != 0) { + b.loadConstant(orMask); + b.math(Opcode.IOR); + } + } + b.storeField(stateFieldName, TypeDesc.INT); + andMask = 0; + orMask = 0; + } + } + + b.returnVoid(); + } + + private void addMarkDirtyMethod(String name) { + MethodInfo mi = + addMethodIfNotFinal(Modifiers.PUBLIC.toSynchronized(true), name, null, null); + + if (mi == null) { + return; + } + + CodeBuilder b = new CodeBuilder(mi); + + if (mGenMode == GEN_WRAPPED) { + clearState(b); + callWrappedStorable(mi, b); + return; + } + + final int count = mAllProperties.size(); + int ordinal = 0; + int andMask = 0; + int orMask = 0; + + for (StorableProperty property : mAllProperties.values()) { + if (property.isJoin()) { + // Erase cached join properties, but don't erase cached query. + if (!property.isQuery()) { + andMask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2); + } + } else if (name == MARK_ALL_PROPERTIES_DIRTY) { + // Force dirty state (3). + orMask |= PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2); + } + + ordinal++; + if ((ordinal & 0xf) == 0 || ordinal >= count) { + String stateFieldName = PROPERTY_STATE_FIELD_NAME + ((ordinal - 1) >> 4); + if (name == MARK_ALL_PROPERTIES_DIRTY) { + if (orMask != 0 || andMask != 0) { + b.loadThis(); // [this + b.loadThis(); // [this, this + b.loadField(stateFieldName, TypeDesc.INT); // [this, this.stateField + if (andMask != 0) { + b.loadConstant(~andMask); + b.math(Opcode.IAND); + } + if (orMask != 0) { + b.loadConstant(orMask); + b.math(Opcode.IOR); + } + b.storeField(stateFieldName, TypeDesc.INT); + } + } else { + // This is a great trick to convert all states of value 1 + // (clean) into value 3 (dirty). States 0, 2, and 3 stay the + // same. Since joins cannot have state 1, they aren't affected. + // stateField |= ((stateField & 0x55555555) << 1); + + b.loadThis(); // [this + b.loadThis(); // [this, this + b.loadField(stateFieldName, TypeDesc.INT); // [this, this.stateField + if (andMask != 0) { + b.loadConstant(~andMask); + b.math(Opcode.IAND); + } + b.dup(); // [this, this.stateField, this.stateField + b.loadConstant(0x55555555); + b.math(Opcode.IAND); // [this, this.stateField, this.stateField & 0x55555555 + b.loadConstant(1); + b.math(Opcode.ISHL); // [this, this.stateField, orMaskValue + b.math(Opcode.IOR); // [this, newStateFieldValue + b.storeField(stateFieldName, TypeDesc.INT); + } + + andMask = 0; + orMask = 0; + } + } + + b.returnVoid(); + } + + /** + * For the given ordinary key property, marks all of its dependent join + * element properties as uninitialized, and marks given property as dirty. + */ + private void markOrdinaryPropertyDirty + (CodeBuilder b, StorableProperty ordinaryProperty) + { + int count = mAllProperties.size(); + + int ordinal = 0; + int andMask = 0xffffffff; + int orMask = 0; + for (StorableProperty property : mAllProperties.values()) { + if (property == ordinaryProperty) { + if (mGenMode == GEN_ABSTRACT) { + // Only GEN_ABSTRACT mode uses these state bits. + orMask |= PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2); + } + } else if (property.isJoin()) { + // Check to see if ordinary is an internal member of join property. + for (int i=property.getJoinElementCount(); --i>=0; ) { + if (ordinaryProperty == property.getInternalJoinElement(i)) { + andMask &= ~(PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2)); + } + } + } + ordinal++; + if ((ordinal & 0xf) == 0 || ordinal >= count) { + if (andMask != 0xffffffff || orMask != 0) { + String stateFieldName = PROPERTY_STATE_FIELD_NAME + ((ordinal - 1) >> 4); + b.loadThis(); + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + if (andMask != 0xffffffff) { + b.loadConstant(andMask); + b.math(Opcode.IAND); + } + if (orMask != 0) { + b.loadConstant(orMask); + b.math(Opcode.IOR); + } + b.storeField(stateFieldName, TypeDesc.INT); + } + andMask = 0xffffffff; + orMask = 0; + } + } + } + + // Generates code that branches to the given label if any properties are dirty. + private void branchIfDirty(CodeBuilder b, boolean includePk, Label label) { + int count = mAllProperties.size(); + int ordinal = 0; + int andMask = 0; + for (StorableProperty property : mAllProperties.values()) { + if (!property.isJoin() && (!property.isPrimaryKeyMember() || includePk)) { + // Logical 'and' will convert state 1 (clean) to state 0, so + // that it will be ignored. State 3 (dirty) is what we're + // looking for, and it turns into 2. Essentially, we leave the + // high order bit on, since there is no state which has the + // high order bit on unless the low order bit is also on. + andMask |= 2 << ((ordinal & 0xf) * 2); + } + ordinal++; + if ((ordinal & 0xf) == 0 || ordinal >= count) { + String stateFieldName = PROPERTY_STATE_FIELD_NAME + ((ordinal - 1) >> 4); + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.loadConstant(andMask); + b.math(Opcode.IAND); + // At least one property is dirty, so short circuit. + b.ifZeroComparisonBranch(label, "!="); + andMask = 0; + } + } + } + + private void addIsInitializedMethod + (String name, Map> properties) + { + // Don't check Automatic properties. + { + boolean cloned = false; + for (StorableProperty prop : properties.values()) { + if (prop.isAutomatic() || prop.isVersion()) { + if (!cloned) { + properties = new LinkedHashMap>(properties); + cloned = true; + } + // This isn't concurrent modification since the loop is + // still operating on the original properties map. + properties.remove(prop.getName()); + } + } + } + + MethodInfo mi = mClassFile.addMethod(Modifiers.PROTECTED, name, TypeDesc.BOOLEAN, null); + CodeBuilder b = new CodeBuilder(mi); + + if (properties.size() == 0) { + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + return; + } + + if (properties.size() == 1) { + int ordinal = findPropertyOrdinal(properties.values().iterator().next()); + b.loadThis(); + b.loadField(PROPERTY_STATE_FIELD_NAME + (ordinal >> 4), TypeDesc.INT); + b.loadConstant(PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2)); + b.math(Opcode.IAND); + // zero == false, not zero == true + b.returnValue(TypeDesc.BOOLEAN); + return; + } + + // Multiple properties is a bit more tricky. The goal here is to + // minimize the amount of work that needs to be done at runtime. + + int ordinal = 0; + int mask = 0; + for (StorableProperty property : mAllProperties.values()) { + if (properties.containsKey(property.getName())) { + mask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2); + } + ordinal++; + if (((ordinal & 0xf) == 0 || ordinal >= mAllProperties.size()) && mask != 0) { + // This is a great trick to convert all states of value 1 + // (clean) into value 3 (dirty). States 0, 2, and 3 stay the + // same. Since joins cannot have state 1, they aren't affected. + // stateField | ((stateField & 0x55555555) << 1); + + b.loadThis(); + b.loadField(PROPERTY_STATE_FIELD_NAME + ((ordinal - 1) >> 4), TypeDesc.INT); + b.dup(); // [this.stateField, this.stateField + b.loadConstant(0x55555555); + b.math(Opcode.IAND); // [this.stateField, this.stateField & 0x55555555 + b.loadConstant(1); + b.math(Opcode.ISHL); // [this.stateField, orMaskValue + b.math(Opcode.IOR); // [newStateFieldValue + + // Flip all bits for property states. If final result is + // non-zero, then there were uninitialized properties. + + b.loadConstant(mask); + b.math(Opcode.IXOR); + if (mask != 0xffffffff) { + b.loadConstant(mask); + b.math(Opcode.IAND); + } + + Label cont = b.createLabel(); + b.ifZeroComparisonBranch(cont, "=="); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + cont.setLocation(); + + mask = 0; + } + } + + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + } + + private int findPropertyOrdinal(StorableProperty property) { + int ordinal = 0; + for (StorableProperty p : mAllProperties.values()) { + if (p == property) { + return ordinal; + } + ordinal++; + } + throw new IllegalArgumentException(); + } + + /** + * Generates code that verifies that all primary keys are initialized. + * + * @param b builder that will invoke generated method + * @param methodName name to give to generated method + */ + private void requirePkInitialized(CodeBuilder b, String methodName) { + // Add code to call method which we are about to define. + b.loadThis(); + b.invokeVirtual(methodName, null, null); + + // Now define new method, discarding original builder object. + b = new CodeBuilder(mClassFile.addMethod(Modifiers.PROTECTED, methodName, null, null)); + b.loadThis(); + b.invokeVirtual(IS_PK_INITIALIZED_METHOD_NAME, TypeDesc.BOOLEAN, null); + Label pkInitialized = b.createLabel(); + b.ifZeroComparisonBranch(pkInitialized, "!="); + CodeBuilderUtil.throwException + (b, IllegalStateException.class, "Primary key not fully specified"); + pkInitialized.setLocation(); + b.returnVoid(); + } + + /** + * Generates a private method which accepts a property name and returns + * PROPERTY_STATE_UNINITIALIZED, PROPERTY_STATE_DIRTY, or + * PROPERTY_STATE_CLEAN. + */ + private void addPropertyStateExtractMethod() { + if (mGenMode == GEN_WRAPPED) { + return; + } + + MethodInfo mi = mClassFile.addMethod(Modifiers.PRIVATE, PROPERTY_STATE_EXTRACT_METHOD_NAME, + TypeDesc.INT, new TypeDesc[] {TypeDesc.STRING}); + CodeBuilder b = new CodeBuilder(mi); + + // Generate big switch statement that operates on Strings. See also + // org.cojen.util.BeanPropertyAccessor, which also generates this kind of + // switch. + + // For switch case count, obtain a prime number, at least twice as + // large as needed. This should minimize hash collisions. Since all the + // hash keys are known up front, the capacity could be tweaked until + // there are no collisions, but this technique is easier and + // deterministic. + + int caseCount; + { + BigInteger capacity = BigInteger.valueOf(mAllProperties.size() * 2 + 1); + while (!capacity.isProbablePrime(100)) { + capacity = capacity.add(BigInteger.valueOf(2)); + } + caseCount = capacity.intValue(); + } + + int[] cases = new int[caseCount]; + for (int i=0; i>[] caseMatches = caseMatches(caseCount); + + for (int i=0; i matches = caseMatches[i]; + if (matches == null || matches.size() == 0) { + switchLabels[i] = noMatch; + } else { + switchLabels[i] = b.createLabel(); + } + } + + b.loadLocal(b.getParameter(0)); + b.invokeVirtual(String.class.getName(), "hashCode", TypeDesc.INT, null); + b.loadConstant(0x7fffffff); + b.math(Opcode.IAND); + b.loadConstant(caseCount); + b.math(Opcode.IREM); + + b.switchBranch(cases, switchLabels, noMatch); + + // Gather property ordinals. + Map, Integer> ordinalMap = new HashMap, Integer>(); + { + int ordinal = 0; + for (StorableProperty prop : mAllProperties.values()) { + ordinalMap.put(prop, ordinal++); + } + } + + // Params to invoke String.equals. + TypeDesc[] params = {TypeDesc.OBJECT}; + + Label joinMatch = null; + + for (int i=0; i> matches = caseMatches[i]; + if (matches == null || matches.size() == 0) { + continue; + } + + switchLabels[i].setLocation(); + + int matchCount = matches.size(); + for (int j=0; j prop = matches.get(j); + + // Test against name to find exact match. + + b.loadConstant(prop.getName()); + b.loadLocal(b.getParameter(0)); + b.invokeVirtual(String.class.getName(), "equals", TypeDesc.BOOLEAN, params); + + Label notEqual; + + if (j == matchCount - 1) { + notEqual = null; + b.ifZeroComparisonBranch(noMatch, "=="); + } else { + notEqual = b.createLabel(); + b.ifZeroComparisonBranch(notEqual, "=="); + } + + if (prop.isJoin()) { + if (joinMatch == null) { + joinMatch = b.createLabel(); + } + b.branch(joinMatch); + } else { + int ordinal = ordinalMap.get(prop); + + b.loadThis(); + b.loadField(PROPERTY_STATE_FIELD_NAME + (ordinal >> 4), TypeDesc.INT); + int shift = (ordinal & 0xf) * 2; + if (shift != 0) { + b.loadConstant(shift); + b.math(Opcode.ISHR); + } + b.loadConstant(PROPERTY_STATE_MASK); + b.math(Opcode.IAND); + b.returnValue(TypeDesc.INT); + } + + if (notEqual != null) { + notEqual.setLocation(); + } + } + } + + TypeDesc exceptionType = TypeDesc.forClass(IllegalArgumentException.class); + params = new TypeDesc[] {TypeDesc.STRING}; + + noMatch.setLocation(); + + b.newObject(exceptionType); + b.dup(); + b.loadConstant("Unknown property: "); + b.loadLocal(b.getParameter(0)); + b.invokeVirtual(TypeDesc.STRING, "concat", TypeDesc.STRING, params); + b.invokeConstructor(exceptionType, params); + b.throwObject(); + + if (joinMatch != null) { + joinMatch.setLocation(); + + b.newObject(exceptionType); + b.dup(); + b.loadConstant("Cannot get state for join property: "); + b.loadLocal(b.getParameter(0)); + b.invokeVirtual(TypeDesc.STRING, "concat", TypeDesc.STRING, params); + b.invokeConstructor(exceptionType, params); + b.throwObject(); + } + } + + /** + * Returns the properties that match on a given case. The array length is + * the same as the case count. Each list represents the matches. The lists + * themselves may be null if no matches for that case. + */ + private List>[] caseMatches(int caseCount) { + List>[] cases = new List[caseCount]; + + for (StorableProperty prop : mAllProperties.values()) { + int hashCode = prop.getName().hashCode(); + int caseValue = (hashCode & 0x7fffffff) % caseCount; + List matches = cases[caseValue]; + if (matches == null) { + matches = cases[caseValue] = new ArrayList>(); + } + matches.add(prop); + } + + return cases; + } + + /** + * Generates public method which accepts a property name and returns a + * boolean true, if the given state matches the property's actual state. + * + * @param name name of method + * @param state property state to check + */ + private void addPropertyStateCheckMethod(String name, int state) { + MethodInfo mi = addMethodIfNotFinal(Modifiers.PUBLIC, name, + TypeDesc.BOOLEAN, new TypeDesc[] {TypeDesc.STRING}); + + if (mi == null) { + return; + } + + CodeBuilder b = new CodeBuilder(mi); + + if (mGenMode == GEN_WRAPPED) { + callWrappedStorable(mi, b); + return; + } + + // Call private method to extract state and compare. + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokePrivate(PROPERTY_STATE_EXTRACT_METHOD_NAME, + TypeDesc.INT, new TypeDesc[] {TypeDesc.STRING}); + Label isFalse = b.createLabel(); + if (state == 0) { + b.ifZeroComparisonBranch(isFalse, "!="); + } else { + b.loadConstant(state); + b.ifComparisonBranch(isFalse, "!="); + } + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + isFalse.setLocation(); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + } + + /** + * Defines a hashCode method. + */ + private void addHashCodeMethod() { + Modifiers modifiers = Modifiers.PUBLIC.toSynchronized(mGenMode == GEN_ABSTRACT); + MethodInfo mi = addMethodIfNotFinal(modifiers, "hashCode", TypeDesc.INT, null); + + if (mi == null) { + return; + } + + if (mGenMode == GEN_WRAPPED) { + callWrappedStorable(mi); + return; + } + + CodeBuilder b = new CodeBuilder(mi); + + boolean mixIn = false; + for (StorableProperty property : mAllProperties.values()) { + if (property.isJoin()) { + continue; + } + addHashCodeCall(b, property.getName(), + TypeDesc.forClass(property.getType()), true, mixIn); + mixIn = true; + } + + b.returnValue(TypeDesc.INT); + } + + private void addHashCodeCall(CodeBuilder b, String fieldName, + TypeDesc fieldType, boolean testForNull, + boolean mixIn) + { + if (mixIn) { + // Multiply current hashcode by 31 before adding more to it. + b.loadConstant(5); + b.math(Opcode.ISHL); + b.loadConstant(1); + b.math(Opcode.ISUB); + } + + b.loadThis(); + b.loadField(fieldName, fieldType); + + switch (fieldType.getTypeCode()) { + case TypeDesc.FLOAT_CODE: + b.invokeStatic(TypeDesc.FLOAT.toObjectType(), "floatToIntBits", + TypeDesc.INT, new TypeDesc[]{TypeDesc.FLOAT}); + // Fall through + case TypeDesc.INT_CODE: + case TypeDesc.CHAR_CODE: + case TypeDesc.SHORT_CODE: + case TypeDesc.BYTE_CODE: + case TypeDesc.BOOLEAN_CODE: + if (mixIn) { + b.math(Opcode.IADD); + } + break; + + case TypeDesc.DOUBLE_CODE: + b.invokeStatic(TypeDesc.DOUBLE.toObjectType(), "doubleToLongBits", + TypeDesc.LONG, new TypeDesc[]{TypeDesc.DOUBLE}); + // Fall through + case TypeDesc.LONG_CODE: + b.dup2(); + b.loadConstant(32); + b.math(Opcode.LUSHR); + b.math(Opcode.LXOR); + b.convert(TypeDesc.LONG, TypeDesc.INT); + if (mixIn) { + b.math(Opcode.IADD); + } + break; + + case TypeDesc.OBJECT_CODE: + default: + LocalVariable value = null; + if (testForNull) { + value = b.createLocalVariable(null, fieldType); + b.storeLocal(value); + b.loadLocal(value); + } + if (mixIn) { + Label isNull = b.createLabel(); + if (testForNull) { + b.ifNullBranch(isNull, true); + b.loadLocal(value); + } + addHashCodeCallTo(b, fieldType); + b.math(Opcode.IADD); + if (testForNull) { + isNull.setLocation(); + } + } else { + Label cont = b.createLabel(); + if (testForNull) { + Label notNull = b.createLabel(); + b.ifNullBranch(notNull, false); + b.loadConstant(0); + b.branch(cont); + notNull.setLocation(); + b.loadLocal(value); + } + addHashCodeCallTo(b, fieldType); + if (testForNull) { + cont.setLocation(); + } + } + break; + } + } + + private void addHashCodeCallTo(CodeBuilder b, TypeDesc fieldType) { + if (fieldType.isArray()) { + if (!fieldType.getComponentType().isPrimitive()) { + b.invokeStatic("java.util.Arrays", "deepHashCode", + TypeDesc.INT, new TypeDesc[] {TypeDesc.forClass(Object[].class)}); + } else { + b.invokeStatic("java.util.Arrays", "hashCode", + TypeDesc.INT, new TypeDesc[] {fieldType}); + } + } else { + b.invokeVirtual(TypeDesc.OBJECT, "hashCode", TypeDesc.INT, null); + } + } + + /** + * Defines an equals method. + * + * @param equalityType Type of equality to define - {@link EQUAL_KEYS} for "equalKeys", + * {@link EQUAL_PROPERTIES} for "equalProperties", and {@link EQUAL_FULL} for "equals" + */ + private void addEqualsMethod(int equalityType) { + TypeDesc[] objectParam = {TypeDesc.OBJECT}; + + String equalsMethodName; + switch (equalityType) { + default: + throw new IllegalArgumentException(); + case EQUAL_KEYS: + equalsMethodName = EQUAL_PRIMARY_KEYS_METHOD_NAME; + break; + case EQUAL_PROPERTIES: + equalsMethodName = EQUAL_PROPERTIES_METHOD_NAME; + break; + case EQUAL_FULL: + equalsMethodName = EQUALS_METHOD_NAME; + } + + Modifiers modifiers = Modifiers.PUBLIC.toSynchronized(mGenMode == GEN_ABSTRACT); + MethodInfo mi = addMethodIfNotFinal + (modifiers, equalsMethodName, TypeDesc.BOOLEAN, objectParam); + + if (mi == null) { + return; + } + + if (mGenMode == GEN_WRAPPED && equalityType != EQUAL_FULL) { + callWrappedStorable(mi); + return; + } + + CodeBuilder b = new CodeBuilder(mi); + + // if (this == target) return true; + b.loadThis(); + b.loadLocal(b.getParameter(0)); + Label notEqual = b.createLabel(); + b.ifEqualBranch(notEqual, false); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + notEqual.setLocation(); + + // if (! target instanceof this) return false; + TypeDesc userStorableTypeDesc = TypeDesc.forClass(mStorableType); + b.loadLocal(b.getParameter(0)); + b.instanceOf(userStorableTypeDesc); + Label fail = b.createLabel(); + b.ifZeroComparisonBranch(fail, "=="); + + // this.class other = (this.class)target; + LocalVariable other = b.createLocalVariable(null, userStorableTypeDesc); + b.loadLocal(b.getParameter(0)); + b.checkCast(userStorableTypeDesc); + b.storeLocal(other); + + for (StorableProperty property : mAllProperties.values()) { + if (property.isJoin()) { + continue; + } + // If we're only comparing keys, and this isn't a key, skip it + if ((equalityType == EQUAL_KEYS) && !property.isPrimaryKeyMember()) { + continue; + } + + // Check if independent property is supported, and skip if not. + Label skipCheck = b.createLabel(); + if (equalityType != EQUAL_KEYS && property.isIndependent()) { + addSkipIndependent(b, other, property, skipCheck); + } + + TypeDesc fieldType = TypeDesc.forClass(property.getType()); + b.loadThis(); + if (mGenMode == GEN_ABSTRACT) { + b.loadField(property.getName(), fieldType); + } else { + b.loadField(WRAPPED_STORABLE_FIELD_NAME, TypeDesc.forClass(mStorableType)); + b.invoke(property.getReadMethod()); + } + + b.loadLocal(other); + b.invoke(property.getReadMethod()); + CodeBuilderUtil.addValuesEqualCall(b, fieldType, true, fail, false); + + skipCheck.setLocation(); + } + + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + fail.setLocation(); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + } + + /** + * Defines a toString method, which assumes that the ClassFile is targeting + * version 1.5 of Java. + * + * @param keyOnly when true, generate a toStringKeyOnly method instead + */ + private void addToStringMethod(boolean keyOnly) { + TypeDesc stringBuilder = TypeDesc.forClass(StringBuilder.class); + TypeDesc[] stringParam = {TypeDesc.STRING}; + TypeDesc[] charParam = {TypeDesc.CHAR}; + + Modifiers modifiers = Modifiers.PUBLIC.toSynchronized(mGenMode == GEN_ABSTRACT); + MethodInfo mi = addMethodIfNotFinal(modifiers, + keyOnly ? + TO_STRING_KEY_ONLY_METHOD_NAME : + TO_STRING_METHOD_NAME, + TypeDesc.STRING, null); + + if (mi == null) { + return; + } + + if (mGenMode == GEN_WRAPPED) { + callWrappedStorable(mi); + return; + } + + CodeBuilder b = new CodeBuilder(mi); + b.newObject(stringBuilder); + b.dup(); + b.invokeConstructor(stringBuilder, null); + b.loadConstant(mStorableType.getName()); + invokeAppend(b, stringParam); + + String detail; + if (keyOnly) { + detail = " (key only) {"; + } else { + detail = " {"; + } + + b.loadConstant(detail); + invokeAppend(b, stringParam); + + // First pass, just print primary keys. + int ordinal = 0; + for (StorableProperty property : mAllProperties.values()) { + if (property.isPrimaryKeyMember()) { + Label skipPrint = b.createLabel(); + + // Check if independent property is supported, and skip if not. + if (property.isIndependent()) { + addSkipIndependent(b, null, property, skipPrint); + } + + if (ordinal++ > 0) { + b.loadConstant(", "); + invokeAppend(b, stringParam); + } + addPropertyAppendCall(b, property, stringParam, charParam); + + skipPrint.setLocation(); + } + } + + // Second pass, print non-primary keys. + if (!keyOnly) { + for (StorableProperty property : mAllProperties.values()) { + // Don't print join properties if they may throw an exception. + if (!property.isPrimaryKeyMember() && (!property.isJoin())) { + Label skipPrint = b.createLabel(); + + // Check if independent property is supported, and skip if not. + if (property.isIndependent()) { + addSkipIndependent(b, null, property, skipPrint); + } + + b.loadConstant(", "); + invokeAppend(b, stringParam); + addPropertyAppendCall(b, property, stringParam, charParam); + + skipPrint.setLocation(); + } + } + } + + b.loadConstant('}'); + invokeAppend(b, charParam); + + // For key string, also show all the alternate keys. This makes the + // FetchNoneException message more helpful. + if (keyOnly) { + int altKeyCount = mInfo.getAlternateKeyCount(); + for (int i=0; i key = mInfo.getAlternateKey(i); + + ordinal = 0; + for (OrderedProperty op : key.getProperties()) { + StorableProperty property = op.getChainedProperty().getPrimeProperty(); + + Label skipPrint = b.createLabel(); + + // Check if independent property is supported, and skip if not. + if (property.isIndependent()) { + addSkipIndependent(b, null, property, skipPrint); + } + + if (ordinal++ > 0) { + b.loadConstant(", "); + invokeAppend(b, stringParam); + } + addPropertyAppendCall(b, property, stringParam, charParam); + + skipPrint.setLocation(); + } + + b.loadConstant('}'); + invokeAppend(b, charParam); + } + } + + b.invokeVirtual(stringBuilder, TO_STRING_METHOD_NAME, TypeDesc.STRING, null); + b.returnValue(TypeDesc.STRING); + } + + private void invokeAppend(CodeBuilder b, TypeDesc[] paramType) { + TypeDesc stringBuilder = TypeDesc.forClass(StringBuilder.class); + b.invokeVirtual(stringBuilder, "append", stringBuilder, paramType); + } + + private void addPropertyAppendCall(CodeBuilder b, + StorableProperty property, + TypeDesc[] stringParam, + TypeDesc[] charParam) + { + b.loadConstant(property.getName()); + invokeAppend(b, stringParam); + b.loadConstant('='); + invokeAppend(b, charParam); + b.loadThis(); + TypeDesc type = TypeDesc.forClass(property.getType()); + b.loadField(property.getName(), type); + if (type.isPrimitive()) { + if (type == TypeDesc.BYTE || type == TypeDesc.SHORT) { + type = TypeDesc.INT; + } + } else { + if (type != TypeDesc.STRING) { + if (type.isArray()) { + if (!type.getComponentType().isPrimitive()) { + b.invokeStatic("java.util.Arrays", "deepToString", + TypeDesc.STRING, + new TypeDesc[] {TypeDesc.OBJECT.toArrayType()}); + } else { + b.invokeStatic("java.util.Arrays", TO_STRING_METHOD_NAME, + TypeDesc.STRING, new TypeDesc[] {type}); + } + } + type = TypeDesc.OBJECT; + } + } + invokeAppend(b, new TypeDesc[]{type}); + } + + /** + * Generates code to get a trigger, forcing a transaction if trigger is not + * null. Also, if there is a trigger, the "before" method is called. + * + * @param opType type of operation, Insert, Update, or Delete + * @param forTryVar optional boolean variable for selecting whether to call + * "before" or "beforeTry" method + * @param forTry used if forTryVar is null + * @param triggerVar required variable of type Trigger for storing trigger + * @param txnVar required variable of type Transaction for storing transaction + * @param stateVar variable of type Object for storing state + * @return try start label for transaction + */ + private Label addGetTriggerAndEnterTxn(CodeBuilder b, + String opType, + LocalVariable forTryVar, + boolean forTry, + LocalVariable triggerVar, + LocalVariable txnVar, + LocalVariable stateVar) + { + // trigger = support$.getXxxTrigger(); + b.loadThis(); + b.loadField(SUPPORT_FIELD_NAME, mSupportType); + Method m = lookupMethod(mSupportType.toClass(), "get" + opType + "Trigger", null); + b.invoke(m); + b.storeLocal(triggerVar); + // state = null; + b.loadNull(); + b.storeLocal(stateVar); + + // if (trigger == null) { + // txn = null; + // } else { + // txn = support.getRootRepository().enterTransaction(); + // tryStart: + // if (forTry) { + // state = trigger.beforeTryXxx(this); + // } else { + // state = trigger.beforeXxx(this); + // } + // } + b.loadLocal(triggerVar); + Label hasTrigger = b.createLabel(); + b.ifNullBranch(hasTrigger, false); + + // txn = null + b.loadNull(); + b.storeLocal(txnVar); + Label cont = b.createLabel(); + b.branch(cont); + + hasTrigger.setLocation(); + + // txn = support.getRootRepository().enterTransaction(); + TypeDesc repositoryType = TypeDesc.forClass(Repository.class); + TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + b.loadThis(); + b.loadField(SUPPORT_FIELD_NAME, mSupportType); + b.invokeInterface(mSupportType, "getRootRepository", repositoryType, null); + b.invokeInterface(repositoryType, ENTER_TRANSACTION_METHOD_NAME, transactionType, null); + b.storeLocal(txnVar); + + Label tryStart = b.createLabel().setLocation(); + + // if (forTry) { + // state = trigger.beforeTryXxx(this); + // } else { + // state = trigger.beforeXxx(this); + // } + b.loadLocal(triggerVar); + b.loadThis(); + + if (forTryVar == null) { + if (forTry) { + b.invokeVirtual(triggerVar.getType(), "beforeTry" + opType, + TypeDesc.OBJECT, new TypeDesc[] {TypeDesc.OBJECT}); + } else { + b.invokeVirtual(triggerVar.getType(), "before" + opType, + TypeDesc.OBJECT, new TypeDesc[] {TypeDesc.OBJECT}); + } + b.storeLocal(stateVar); + } else { + b.loadLocal(forTryVar); + Label isForTry = b.createLabel(); + + b.ifZeroComparisonBranch(isForTry, "!="); + b.invokeVirtual(triggerVar.getType(), "before" + opType, + TypeDesc.OBJECT, new TypeDesc[] {TypeDesc.OBJECT}); + b.storeLocal(stateVar); + b.branch(cont); + + isForTry.setLocation(); + b.invokeVirtual(triggerVar.getType(), "beforeTry" + opType, + TypeDesc.OBJECT, new TypeDesc[] {TypeDesc.OBJECT}); + b.storeLocal(stateVar); + } + + cont.setLocation(); + + return tryStart; + } + + /** + * Generates code to call a trigger after the persistence operation has + * been invoked. + * + * @param opType type of operation, Insert, Update, or Delete + * @param forTryVar optional boolean variable for selecting whether to call + * "after" or "afterTry" method + * @param forTry used if forTryVar is null + * @param triggerVar required variable of type Trigger for retrieving trigger + * @param txnVar required variable of type Transaction for storing transaction + * @param stateVar required variable of type Object for retrieving state + */ + private void addTriggerAfterAndExitTxn(CodeBuilder b, + String opType, + LocalVariable forTryVar, + boolean forTry, + LocalVariable triggerVar, + LocalVariable txnVar, + LocalVariable stateVar) + { + // if (trigger != null) { + b.loadLocal(triggerVar); + Label cont = b.createLabel(); + b.ifNullBranch(cont, true); + + // if (forTry) { + // trigger.afterTryXxx(this, state); + // } else { + // trigger.afterXxx(this, state); + // } + b.loadLocal(triggerVar); + b.loadThis(); + b.loadLocal(stateVar); + + if (forTryVar == null) { + if (forTry) { + b.invokeVirtual(TypeDesc.forClass(Trigger.class), "afterTry" + opType, null, + new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT}); + } else { + b.invokeVirtual(TypeDesc.forClass(Trigger.class), "after" + opType, null, + new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT}); + } + } else { + b.loadLocal(forTryVar); + Label isForTry = b.createLabel(); + + b.ifZeroComparisonBranch(isForTry, "!="); + b.invokeVirtual(TypeDesc.forClass(Trigger.class), "after" + opType, null, + new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT}); + Label commitAndExit = b.createLabel(); + b.branch(commitAndExit); + + isForTry.setLocation(); + b.invokeVirtual(TypeDesc.forClass(Trigger.class), "afterTry" + opType, null, + new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT}); + commitAndExit.setLocation(); + } + + // txn.commit(); + // txn.exit(); + TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + b.loadLocal(txnVar); + b.invokeInterface(transactionType, COMMIT_METHOD_NAME, null, null); + b.loadLocal(txnVar); + b.invokeInterface(transactionType, EXIT_METHOD_NAME, null, null); + + cont.setLocation(); + } + + /** + * Generates code to call a trigger after the persistence operation has + * failed. + * + * @param opType type of operation, Insert, Update, or Delete + * @param triggerVar required variable of type Trigger for retrieving trigger + * @param txnVar required variable of type Transaction for storing transaction + * @param stateVar required variable of type Object for retrieving state + */ + private void addTriggerFailedAndExitTxn(CodeBuilder b, + String opType, + LocalVariable triggerVar, + LocalVariable txnVar, + LocalVariable stateVar) + { + TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + + // if (trigger != null) { + b.loadLocal(triggerVar); + Label isNull = b.createLabel(); + b.ifNullBranch(isNull, true); + + // try { + // trigger.failedXxx(this, state); + // } catch (Throwable e) { + // uncaught(e); + // } + Label tryStart = b.createLabel().setLocation(); + b.loadLocal(triggerVar); + b.loadThis(); + b.loadLocal(stateVar); + b.invokeVirtual(TypeDesc.forClass(Trigger.class), "failed" + opType, null, + new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT}); + Label tryEnd = b.createLabel().setLocation(); + Label cont = b.createLabel(); + b.branch(cont); + b.exceptionHandler(tryStart, tryEnd, Throwable.class.getName()); + b.invokeStatic(UNCAUGHT_METHOD_NAME, null, + new TypeDesc[] {TypeDesc.forClass(Throwable.class)}); + cont.setLocation(); + + // txn.exit(); + b.loadLocal(txnVar); + b.invokeInterface(transactionType, EXIT_METHOD_NAME, null, null); + + isNull.setLocation(); + } + + /** + * Generates exception handler code to call a trigger after the persistence + * operation has failed. + * + * @param opType type of operation, Insert, Update, or Delete + * @param forTryVar optional boolean variable for selecting whether to + * throw or catch Trigger.Abort. + * @param forTry used if forTryVar is null + * @param triggerVar required variable of type Trigger for retrieving trigger + * @param txnVar required variable of type Transaction for storing transaction + * @param stateVar required variable of type Object for retrieving state + * @param tryStart start of exception handler around transaction + */ + private void addTriggerFailedAndExitTxn(CodeBuilder b, + String opType, + LocalVariable forTryVar, + boolean forTry, + LocalVariable triggerVar, + LocalVariable txnVar, + LocalVariable stateVar, + Label tryStart) + { + if (tryStart == null) { + addTriggerFailedAndExitTxn(b, opType, triggerVar, txnVar, stateVar); + return; + } + + // } catch (... e) { + // if (trigger != null) { + // try { + // trigger.failedXxx(this, state); + // } catch (Throwable e) { + // uncaught(e); + // } + // } + // txn.exit(); + // if (e instanceof Trigger.Abort) { + // if (forTryVar) { + // return false; + // } else { + // // Try to add some trace for context + // throw ((Trigger.Abort) e).withStackTrace(); + // } + // } + // if (e instanceof RepositoryException) { + // throw ((RepositoryException) e).toPersistException(); + // } + // throw e; + // } + + Label tryEnd = b.createLabel().setLocation(); + b.exceptionHandler(tryStart, tryEnd, null); + LocalVariable exceptionVar = b.createLocalVariable(null, TypeDesc.OBJECT); + b.storeLocal(exceptionVar); + + addTriggerFailedAndExitTxn(b, opType, triggerVar, txnVar, stateVar); + + b.loadLocal(exceptionVar); + TypeDesc abortException = TypeDesc.forClass(Trigger.Abort.class); + b.instanceOf(abortException); + Label nextCheck = b.createLabel(); + b.ifZeroComparisonBranch(nextCheck, "=="); + if (forTryVar == null) { + if (forTry) { + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + } else { + b.loadLocal(exceptionVar); + b.checkCast(abortException); + b.invokeVirtual(abortException, "withStackTrace", abortException, null); + b.throwObject(); + } + } else { + b.loadLocal(forTryVar); + Label isForTry = b.createLabel(); + b.ifZeroComparisonBranch(isForTry, "!="); + b.loadLocal(exceptionVar); + b.checkCast(abortException); + b.invokeVirtual(abortException, "withStackTrace", abortException, null); + b.throwObject(); + isForTry.setLocation(); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + } + + nextCheck.setLocation(); + b.loadLocal(exceptionVar); + TypeDesc repException = TypeDesc.forClass(RepositoryException.class); + b.instanceOf(repException); + Label throwAny = b.createLabel(); + b.ifZeroComparisonBranch(throwAny, "=="); + b.loadLocal(exceptionVar); + b.checkCast(repException); + b.invokeVirtual(repException, "toPersistException", + TypeDesc.forClass(PersistException.class), null); + b.throwObject(); + + throwAny.setLocation(); + b.loadLocal(exceptionVar); + b.throwObject(); + } + + /** + * Generates method which passes exception to uncaught exception handler. + */ + private void defineUncaughtExceptionHandler() { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PRIVATE.toStatic(true), UNCAUGHT_METHOD_NAME, null, + new TypeDesc[] {TypeDesc.forClass(Throwable.class)}); + CodeBuilder b = new CodeBuilder(mi); + + // Thread t = Thread.currentThread(); + // t.getUncaughtExceptionHandler().uncaughtException(t, e); + TypeDesc threadType = TypeDesc.forClass(Thread.class); + b.invokeStatic(Thread.class.getName(), "currentThread", threadType, null); + LocalVariable threadVar = b.createLocalVariable(null, threadType); + b.storeLocal(threadVar); + b.loadLocal(threadVar); + TypeDesc handlerType = TypeDesc.forClass(Thread.UncaughtExceptionHandler.class); + b.invokeVirtual(threadType, "getUncaughtExceptionHandler", handlerType, null); + b.loadLocal(threadVar); + b.loadLocal(b.getParameter(0)); + b.invokeInterface(handlerType, "uncaughtException", null, + new TypeDesc[] {threadType, TypeDesc.forClass(Throwable.class)}); + b.returnVoid(); + } + + /** + * @return MethodInfo for completing definition or null if superclass + * already implements method as final. + */ + private MethodInfo addMethodIfNotFinal(Modifiers modifiers, String name, + TypeDesc retType, TypeDesc[] params) + { + if (CodeBuilderUtil.isPublicMethodFinal(mStorableType, name, retType, params)) { + return null; + } + + return mClassFile.addMethod(modifiers, name, retType, params); + } +} diff --git a/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java b/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java new file mode 100644 index 0000000..5115854 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java @@ -0,0 +1,337 @@ +/* + * Copyright 2006 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.gen; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; + +import java.lang.reflect.UndeclaredThrowableException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.ClassInjector; +import org.cojen.util.WeakIdentityMap; + +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableProperty; + +import static com.amazon.carbonado.gen.CommonMethodNames.*; + +import com.amazon.carbonado.raw.GenericEncodingStrategy; + +/** + * Support for general-purpose serialization of storables. + *

    + * TODO: This class is unable to determine state of properties, and so they are + * lost during serialization. Upon deserialization, all properties are assumed + * dirty. To fix this, serialization might need to be supported directly by + * Storables. When that happens, this class will be deprecated. + * + * @author Brian S O'Neill + */ +public abstract class StorableSerializer { + private static final String ENCODE_METHOD_NAME = "encode"; + private static final String DECODE_METHOD_NAME = "decode"; + private static final String WRITE_METHOD_NAME = "write"; + private static final String READ_METHOD_NAME = "read"; + + @SuppressWarnings("unchecked") + private static Map>> cCache = new WeakIdentityMap(); + + /** + * @param type type of storable to serialize + */ + @SuppressWarnings("unchecked") + public static StorableSerializer forType(Class type) + throws SupportException + { + synchronized (cCache) { + StorableSerializer serializer; + Reference> ref = cCache.get(type); + if (ref != null) { + serializer = (StorableSerializer) ref.get(); + if (serializer != null) { + return serializer; + } + } + serializer = generateSerializer(type); + cCache.put(type, new SoftReference>(serializer)); + return serializer; + } + } + + @SuppressWarnings("unchecked") + private static StorableSerializer generateSerializer(Class type) + throws SupportException + { + Class abstractClass = StorableGenerator.getAbstractClass(type); + + // Use abstract class ClassLoader in order to access adapter instances. + ClassInjector ci = ClassInjector.create + (type.getName(), abstractClass.getClassLoader()); + ClassFile cf = new ClassFile(ci.getClassName(), StorableSerializer.class); + cf.markSynthetic(); + cf.setSourceFile(StorableSerializer.class.getName()); + cf.setTarget("1.5"); + + cf.addDefaultConstructor(); + + Map> propertyMap = + StorableIntrospector.examine(type).getAllProperties(); + + StorableProperty[] properties; + { + // Exclude joins. + List> list = + new ArrayList>(propertyMap.size()); + for (StorableProperty property : propertyMap.values()) { + if (!property.isJoin()) { + list.add(property); + } + } + properties = new StorableProperty[list.size()]; + list.toArray(properties); + } + + GenericEncodingStrategy ges = new GenericEncodingStrategy(type, null); + + TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + TypeDesc storableType = TypeDesc.forClass(Storable.class); + TypeDesc userStorableType = TypeDesc.forClass(type); + TypeDesc storageType = TypeDesc.forClass(Storage.class); + + // Build method to encode storable into a byte array. + { + MethodInfo mi = cf.addMethod + (Modifiers.PRIVATE.toStatic(true), ENCODE_METHOD_NAME, byteArrayType, + new TypeDesc[] {userStorableType}); + CodeBuilder b = new CodeBuilder(mi); + LocalVariable encodedVar = + ges.buildDataEncoding(b, properties, b.getParameter(0), abstractClass, true, -1); + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } + + // Build method to decode storable from a byte array. + { + MethodInfo mi = cf.addMethod + (Modifiers.PRIVATE.toStatic(true), DECODE_METHOD_NAME, userStorableType, + new TypeDesc[] {storageType, byteArrayType}); + CodeBuilder b = new CodeBuilder(mi); + LocalVariable instanceVar = b.createLocalVariable(null, userStorableType); + b.loadLocal(b.getParameter(0)); + b.invokeInterface(storageType, PREPARE_METHOD_NAME, + storableType, null); + b.checkCast(userStorableType); + b.storeLocal(instanceVar); + LocalVariable encodedVar = b.getParameter(1); + ges.buildDataDecoding + (b, properties, instanceVar, abstractClass, true, -1, null, encodedVar); + b.loadLocal(instanceVar); + b.returnValue(storableType); + } + + // Build write method for DataOutput. + { + TypeDesc dataOutputType = TypeDesc.forClass(DataOutput.class); + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, WRITE_METHOD_NAME, null, + new TypeDesc[] {storableType, dataOutputType}); + + CodeBuilder b = new CodeBuilder(mi); + LocalVariable storableVar = b.getParameter(0); + LocalVariable doutVar = b.getParameter(1); + + b.loadLocal(storableVar); + b.checkCast(userStorableType); + b.invokeStatic(ENCODE_METHOD_NAME, byteArrayType, new TypeDesc[] {userStorableType}); + LocalVariable encodedVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(encodedVar); + + b.loadLocal(doutVar); + b.loadLocal(encodedVar); + b.arrayLength(); + b.invokeInterface(dataOutputType, "writeInt", null, new TypeDesc[] {TypeDesc.INT}); + + b.loadLocal(doutVar); + b.loadLocal(encodedVar); + b.invokeInterface(dataOutputType, "write", null, new TypeDesc[] {byteArrayType}); + b.returnVoid(); + } + + final TypeDesc storableSerializerType = TypeDesc.forClass(StorableSerializer.class); + + // Build write method for OutputStream. + { + TypeDesc outputStreamType = TypeDesc.forClass(OutputStream.class); + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, WRITE_METHOD_NAME, null, + new TypeDesc[] {storableType, outputStreamType}); + + CodeBuilder b = new CodeBuilder(mi); + LocalVariable storableVar = b.getParameter(0); + LocalVariable outVar = b.getParameter(1); + + b.loadLocal(storableVar); + b.checkCast(userStorableType); + b.invokeStatic(ENCODE_METHOD_NAME, byteArrayType, new TypeDesc[] {userStorableType}); + LocalVariable encodedVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(encodedVar); + + b.loadLocal(outVar); + b.loadLocal(encodedVar); + b.arrayLength(); + b.invokeStatic(storableSerializerType, "writeInt", null, + new TypeDesc[] {outputStreamType, TypeDesc.INT}); + + b.loadLocal(outVar); + b.loadLocal(encodedVar); + b.invokeVirtual(outputStreamType, "write", null, new TypeDesc[] {byteArrayType}); + b.returnVoid(); + } + + // Build read method for DataInput. + { + TypeDesc dataInputType = TypeDesc.forClass(DataInput.class); + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, READ_METHOD_NAME, storableType, + new TypeDesc[] {storageType, dataInputType}); + + CodeBuilder b = new CodeBuilder(mi); + LocalVariable storageVar = b.getParameter(0); + LocalVariable dinVar = b.getParameter(1); + + b.loadLocal(dinVar); + b.invokeInterface(dataInputType, "readInt", TypeDesc.INT, null); + b.newObject(byteArrayType); + LocalVariable byteArrayVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(byteArrayVar); + + b.loadLocal(dinVar); + b.loadLocal(byteArrayVar); + b.invokeInterface(dataInputType, "readFully", null, new TypeDesc[] {byteArrayType}); + + b.loadLocal(storageVar); + b.loadLocal(byteArrayVar); + b.invokeStatic(DECODE_METHOD_NAME, userStorableType, + new TypeDesc[] {storageType, byteArrayType}); + b.returnValue(storableType); + } + + // Build read method for InputStream. + { + TypeDesc inputStreamType = TypeDesc.forClass(InputStream.class); + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, READ_METHOD_NAME, storableType, + new TypeDesc[] {storageType, inputStreamType}); + + CodeBuilder b = new CodeBuilder(mi); + LocalVariable storageVar = b.getParameter(0); + LocalVariable inVar = b.getParameter(1); + + b.loadLocal(inVar); + b.invokeStatic(storableSerializerType, "readInt", TypeDesc.INT, + new TypeDesc[] {inputStreamType}); + b.newObject(byteArrayType); + LocalVariable byteArrayVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(byteArrayVar); + + b.loadLocal(inVar); + b.loadLocal(byteArrayVar); + b.invokeStatic(storableSerializerType, "readFully", null, + new TypeDesc[] {inputStreamType, byteArrayType}); + + b.loadLocal(storageVar); + b.loadLocal(byteArrayVar); + b.invokeStatic(DECODE_METHOD_NAME, userStorableType, + new TypeDesc[] {storageType, byteArrayType}); + b.returnValue(storableType); + } + + Class clazz = (Class) ci.defineClass(cf); + + try { + return clazz.newInstance(); + } catch (InstantiationException e) { + throw new UndeclaredThrowableException(e); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } + } + + protected StorableSerializer() { + } + + public abstract void write(S storable, DataOutput out) throws IOException; + + public abstract void write(S storable, OutputStream out) throws IOException; + + public abstract S read(Storage storage, DataInput in) throws IOException, EOFException; + + public abstract S read(Storage storage, InputStream in) throws IOException, EOFException; + + public static void writeInt(OutputStream out, int v) throws IOException { + out.write((v >>> 24) & 0xff); + out.write((v >>> 16) & 0xff); + out.write((v >>> 8) & 0xff); + out.write(v & 0xff); + } + + public static int readInt(InputStream in) throws IOException { + int a = in.read(); + int b = in.read(); + int c = in.read(); + int d = in.read(); + if ((a | b | c | d) < 0) { + throw new EOFException(); + } + return (a << 24) | (b << 16) | (c << 8) | d; + } + + public static void readFully(InputStream in, byte[] b) throws IOException { + int length = b.length; + int n = 0; + while (n < length) { + int count = in.read(b, n, length - n); + if (count < 0) { + throw new EOFException(); + } + n += count; + } + } +} diff --git a/src/main/java/com/amazon/carbonado/gen/StorableSupport.java b/src/main/java/com/amazon/carbonado/gen/StorableSupport.java new file mode 100644 index 0000000..16fed67 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/StorableSupport.java @@ -0,0 +1,41 @@ +/* + * Copyright 2006 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.gen; + +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.Storable; + +/** + * Provides runtime support for Storable classes generated by {@link StorableGenerator}. + * + * @author Brian S O'Neill + */ +public interface StorableSupport { + /** + * Returns the root parent Repository that the Storable came from. + */ + Repository getRootRepository(); + + /** + * Returns true if the given property exists and is supported. + * + * @param propertyName name of property to check + */ + boolean isPropertySupported(String propertyName); +} diff --git a/src/main/java/com/amazon/carbonado/gen/TriggerSupport.java b/src/main/java/com/amazon/carbonado/gen/TriggerSupport.java new file mode 100644 index 0000000..39b7806 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/TriggerSupport.java @@ -0,0 +1,50 @@ +/* + * Copyright 2006 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.gen; + +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Trigger; + +/** + * Provides runtime support for Storable classes generated by {@link StorableGenerator}. + * + * @author Brian S O'Neill + */ +public interface TriggerSupport extends StorableSupport { + /** + * Returns a trigger which must be run for all insert operations. + * + * @return null if no trigger + */ + Trigger getInsertTrigger(); + + /** + * Returns a trigger which must be run for all update operations. + * + * @return null if no trigger + */ + Trigger getUpdateTrigger(); + + /** + * Returns a trigger which must be run for all delete operations. + * + * @return null if no trigger + */ + Trigger getDeleteTrigger(); +} diff --git a/src/main/java/com/amazon/carbonado/gen/WrappedSupport.java b/src/main/java/com/amazon/carbonado/gen/WrappedSupport.java new file mode 100644 index 0000000..78f58b3 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/WrappedSupport.java @@ -0,0 +1,75 @@ +/* + * Copyright 2006 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.gen; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; + +/** + * Provides runtime support for Storable classes generated by {@link StorableGenerator}. + * + * @author Brian S O'Neill + */ +public interface WrappedSupport extends TriggerSupport { + /** + * @see Storable#load + */ + void load() throws FetchException; + + /** + * @see Storable#tryLoad + */ + boolean tryLoad() throws FetchException; + + /** + * @see Storable#insert + */ + void insert() throws PersistException; + + /** + * @see Storable#tryInsert + */ + boolean tryInsert() throws PersistException; + + /** + * @see Storable#update + */ + void update() throws PersistException; + + /** + * @see Storable#tryUpdate + */ + boolean tryUpdate() throws PersistException; + + /** + * @see Storable#delete + */ + void delete() throws PersistException; + + /** + * @see Storable#tryDelete + */ + boolean tryDelete() throws PersistException; + + /** + * Return another Support instance for the given Storable. + */ + WrappedSupport createSupport(S storable); +} diff --git a/src/main/java/com/amazon/carbonado/gen/package-info.java b/src/main/java/com/amazon/carbonado/gen/package-info.java new file mode 100644 index 0000000..007ee0b --- /dev/null +++ b/src/main/java/com/amazon/carbonado/gen/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2006 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. + */ + +/** + * Storable code generation support intended for use by repository implementations. + */ +package com.amazon.carbonado.gen; -- cgit v1.2.3