From 97af4be638e371a2f693bde2798fc233a143f3f9 Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" <bronee@gmail.com> Date: Sun, 29 Apr 2007 17:47:50 +0000 Subject: Merged in support for derived properties. --- src/main/java/com/amazon/carbonado/Derived.java | 62 +++ .../amazon/carbonado/OptimisticLockException.java | 32 ++ src/main/java/com/amazon/carbonado/Version.java | 42 +- .../com/amazon/carbonado/gen/CodeBuilderUtil.java | 50 +- .../carbonado/gen/MasterStorableGenerator.java | 178 +++++-- .../amazon/carbonado/gen/StorableGenerator.java | 181 ++++--- .../amazon/carbonado/gen/StorableSerializer.java | 4 +- .../carbonado/info/StorableIntrospector.java | 527 +++++++++++++++++++-- .../amazon/carbonado/info/StorableProperty.java | 36 +- .../java/com/amazon/carbonado/layout/Layout.java | 2 +- .../com/amazon/carbonado/qe/AbstractQuery.java | 6 + .../java/com/amazon/carbonado/qe/EmptyQuery.java | 18 + .../carbonado/raw/GenericEncodingStrategy.java | 11 +- .../amazon/carbonado/raw/GenericStorableCodec.java | 2 +- .../repo/indexed/DependentStorableFetcher.java | 174 +++++++ .../repo/indexed/DerivedIndexesTrigger.java | 164 +++++++ .../carbonado/repo/indexed/IndexAnalysis.java | 132 ++++++ .../carbonado/repo/indexed/IndexEntryAccessor.java | 2 +- .../carbonado/repo/indexed/IndexedRepository.java | 9 +- .../carbonado/repo/indexed/IndexedStorage.java | 25 +- .../carbonado/repo/indexed/ManagedIndex.java | 23 +- .../carbonado/repo/jdbc/JDBCStorableGenerator.java | 18 +- .../repo/jdbc/JDBCStorableIntrospector.java | 21 +- .../com/amazon/carbonado/spi/WrappedQuery.java | 17 + .../SyntheticStorableReferenceBuilder.java | 17 +- 25 files changed, 1524 insertions(+), 229 deletions(-) create mode 100644 src/main/java/com/amazon/carbonado/Derived.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/DependentStorableFetcher.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/DerivedIndexesTrigger.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexAnalysis.java (limited to 'src/main/java') diff --git a/src/main/java/com/amazon/carbonado/Derived.java b/src/main/java/com/amazon/carbonado/Derived.java new file mode 100644 index 0000000..77ffd2a --- /dev/null +++ b/src/main/java/com/amazon/carbonado/Derived.java @@ -0,0 +1,62 @@ +/* + * Copyright 2007 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; + +import java.lang.annotation.*; + +/** + * Identifies a {@link Storable} property which is not directly persisted, but + * is instead derived from other property values. A derived property cannot be + * abstract, and a "set" method is optional. + * + * <p>Derived properties can be used just like a normal property in most + * cases. They can be used in query filters, indexes, alternate keys, and they + * can also be used to define a {@link Version} property. + * + * <p>If the derived property depends on {@link Join} properties and is also + * used in an index or alternate key, dependencies must be listed in order for + * the index to be properly updated. + * + * <p>Example:<pre> + * @Indexes(@Index("uppercaseName")) + * public abstract class UserInfo implements Storable<UserInfo> { + * /** + * * Derive an uppercase name for case-insensitive searches. + * */ + * <b>@Derived</b> + * public String getUppercaseName() { + * String name = getName(); + * return name == null ? null : name.toUpperCase(); + * } + * + * ... + * } + * </pre> + * + * @author Brian S O'Neill + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Derived { + /** + * List of properties that this property is derived from. + */ + String[] from() default {}; +} diff --git a/src/main/java/com/amazon/carbonado/OptimisticLockException.java b/src/main/java/com/amazon/carbonado/OptimisticLockException.java index ccd894c..03004c2 100644 --- a/src/main/java/com/amazon/carbonado/OptimisticLockException.java +++ b/src/main/java/com/amazon/carbonado/OptimisticLockException.java @@ -88,6 +88,18 @@ public class OptimisticLockException extends PersistException { mStorable = s; } + /** + * Construct exception for when new version was expected to have increased. + * + * @param savedVersion actual persistent version number of storable + * @param s Storable which was acted upon + * @param newVersion new version which was provided + */ + public OptimisticLockException(Object savedVersion, Storable s, Object newVersion) { + super(makeMessage(savedVersion, s, newVersion)); + mStorable = s; + } + /** * Returns the Storable which was acted upon, or null if not available. */ @@ -114,4 +126,24 @@ public class OptimisticLockException extends PersistException { return message; } + + private static String makeMessage(Object savedVersion, Storable s, Object newVersion) { + String message; + if (savedVersion == null && newVersion == null) { + message = "New version is not larger than existing version"; + } else { + message = "New version of " + newVersion + + " is not larger than existing version of " + savedVersion; + } + + if (s != null) { + if (message == null) { + message = s.toStringKeyOnly(); + } else { + message = message + ": " + s.toStringKeyOnly(); + } + } + + return message; + } } diff --git a/src/main/java/com/amazon/carbonado/Version.java b/src/main/java/com/amazon/carbonado/Version.java index 7c3c2b4..cb5dcb8 100644 --- a/src/main/java/com/amazon/carbonado/Version.java +++ b/src/main/java/com/amazon/carbonado/Version.java @@ -25,34 +25,22 @@ import java.lang.annotation.*; * number for the entire Storable instance. Only one property can have this * designation. * - * <p>Support for the version property falls into three categories. A - * repository may manage the version; it may respect the version; or it may - * merely check the version. + * <p>Philosophically, a version property can be considered part of the + * identity of the storable. Unless the version is {@link Derived}, the + * repository is responsibile for establishing the version on insert, and for + * auto-incrementing it on update. Under no circumstances should a normal + * version property be incremented manually; this can result in a false {@link + * OptimisticLockException}, or worse may allow the persistent record to become + * corrupted. * - * <p><b>Manage</b>: Each storable with a version property must have one and - * only one repository which is responsible for managing the version property. - * That repository takes responsibility for establishing the version on insert, - * and for auto-incrementing it on update. Under no circumstances should the - * version property be incremented manually; this can result in a false - * optimistic lock exception, or worse may allow the persistent record to - * become corrupted. Prior to incrementing, these repositories will verify - * that the version exactly matches the version of the current record, throwing - * an {@link OptimisticLockException} otherwise. The JDBC repository is the - * canonical example of this sort of repository. - * - * <p><b>Respect</b>: Repositories which respect the version use the version to - * guarantee that updates are idempotent -- that is, that an update is applied - * once and only once. These repositories will check that the version property - * is strictly greater than the version of the current record, and will - * (silently) ignore changes which fail this check. - * - * <p><b>Check</b>: Philosophically, a version property can be considered part - * of the identity of the storable. That is, if the storable has a version - * property, it cannot be considered fully specified unless that property is - * specified. Thus, the minimal required support for all repositories is to - * check that the version is specified on update. All repositories -- even - * those which neither check nor manage the version -- will throw an {@link - * IllegalStateException} if the version property is not set before update. + * <p>When updating a storable which has a normal version property, a value for + * the version must be specified along with its primary key. Otherwise, an + * {@link IllegalStateException} is thrown when calling update. If the update + * operation detects that the specified version doesn't exactly match the + * version of the existing persisted storable, an {@link + * OptimisticLockException} is thrown. For {@link Derived} versions, an {@link + * OptimisticLockException} is thrown only if the update detects that the new + * version hasn't incremented. * * <p>The actual type of the version property can be anything, but some * repositories might only support integers. For maximum portability, version diff --git a/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java b/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java index c1f26a0..1b754fc 100644 --- a/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java +++ b/src/main/java/com/amazon/carbonado/gen/CodeBuilderUtil.java @@ -267,7 +267,9 @@ public class CodeBuilderUtil { /** * 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. + * provided Label if they are not equal. Both values must be of the same + * type. If they are floating point values, NaN is considered equal to NaN, + * which is inconsistent with the usual treatment for NaN. * * <P>The generated instruction consumes both values on the stack. * @@ -286,18 +288,25 @@ public class CodeBuilderUtil { final boolean choice) { if (valueType.getTypeCode() != TypeDesc.OBJECT_CODE) { - b.ifComparisonBranch(label, choice ? "==" : "!=", valueType); + if (valueType.getTypeCode() == TypeDesc.FLOAT_CODE) { + // Special treatment to handle NaN. + b.invokeStatic(TypeDesc.FLOAT.toObjectType(), "compare", TypeDesc.INT, + new TypeDesc[] {TypeDesc.FLOAT, TypeDesc.FLOAT}); + b.ifZeroComparisonBranch(label, choice ? "==" : "!="); + } else if (valueType.getTypeCode() == TypeDesc.DOUBLE_CODE) { + // Special treatment to handle NaN. + b.invokeStatic(TypeDesc.DOUBLE.toObjectType(), "compare", TypeDesc.INT, + new TypeDesc[] {TypeDesc.DOUBLE, TypeDesc.DOUBLE}); + b.ifZeroComparisonBranch(label, choice ? "==" : "!="); + } else { + 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); + String op = addEqualsCallTo(b, valueType, choice); + b.ifZeroComparisonBranch(label, op); return; } @@ -318,14 +327,19 @@ public class CodeBuilderUtil { isNotNull.setLocation(); b.loadLocal(value); b.swap(); - addEqualsCallTo(b, valueType); - b.ifZeroComparisonBranch(label, equalsBranchOp); + String op = addEqualsCallTo(b, valueType, choice); + b.ifZeroComparisonBranch(label, op); cont.setLocation(); } - public static void addEqualsCallTo(CodeBuilder b, TypeDesc fieldType) { + /** + * @param fieldType must be an object type + * @return zero comparison branch operator + */ + private static String addEqualsCallTo(CodeBuilder b, TypeDesc fieldType, boolean choice) { if (fieldType.isArray()) { + // FIXME: Array comparisons don't handle desired comparison of NaN. if (!fieldType.getComponentType().isPrimitive()) { TypeDesc type = TypeDesc.forClass(Object[].class); b.invokeStatic("java.util.Arrays", "deepEquals", @@ -334,6 +348,17 @@ public class CodeBuilderUtil { b.invokeStatic("java.util.Arrays", "equals", TypeDesc.BOOLEAN, new TypeDesc[] {fieldType, fieldType}); } + return choice ? "!=" : "=="; + } else if (fieldType.toPrimitiveType() == TypeDesc.FLOAT) { + // Special treatment to handle NaN. + b.invokeVirtual(TypeDesc.FLOAT.toObjectType(), "compareTo", TypeDesc.INT, + new TypeDesc[] {TypeDesc.FLOAT.toObjectType()}); + return choice ? "==" : "!="; + } else if (fieldType.toPrimitiveType() == TypeDesc.DOUBLE) { + // Special treatment to handle NaN. + b.invokeVirtual(TypeDesc.DOUBLE.toObjectType(), "compareTo", TypeDesc.INT, + new TypeDesc[] {TypeDesc.DOUBLE.toObjectType()}); + return choice ? "==" : "!="; } else { TypeDesc[] params = {TypeDesc.OBJECT}; if (fieldType.toClass() != null) { @@ -345,6 +370,7 @@ public class CodeBuilderUtil { } else { b.invokeVirtual(TypeDesc.OBJECT, "equals", TypeDesc.BOOLEAN, params); } + return choice ? "!=" : "=="; } } diff --git a/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java b/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java index 17efb70..581a7d6 100644 --- a/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java +++ b/src/main/java/com/amazon/carbonado/gen/MasterStorableGenerator.java @@ -126,7 +126,7 @@ public final class MasterStorableGenerator<S extends Storable> { anySequences: if (features.contains(MasterFeature.INSERT_SEQUENCES)) { for (StorableProperty<S> property : info.getAllProperties().values()) { - if (property.getSequenceName() != null) { + if (!property.isDerived() && property.getSequenceName() != null) { break anySequences; } } @@ -266,7 +266,7 @@ public final class MasterStorableGenerator<S extends Storable> { int ordinal = 0; for (StorableProperty<S> property : mAllProperties.values()) { - if (property.getSequenceName() != null) { + if (!property.isDerived() && property.getSequenceName() != null) { // Check the state of this property, to see if it is // uninitialized. Uninitialized state has value zero. @@ -362,14 +362,16 @@ public final class MasterStorableGenerator<S extends Storable> { 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 (!mInfo.getVersionProperty().isDerived()) { + // 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)) { @@ -410,7 +412,8 @@ public final class MasterStorableGenerator<S extends Storable> { for (StorableProperty<S> property : mAllProperties.values()) { ordinal++; - if (property.isJoin() || property.isPrimaryKeyMember() + if (property.isDerived() + || property.isJoin() || property.isPrimaryKeyMember() || property.isNullable() || property.isAutomatic() || property.isVersion()) { @@ -548,33 +551,109 @@ public final class MasterStorableGenerator<S extends Storable> { b.ifZeroComparisonBranch(failed, "=="); // if (version support enabled) { - // if (this.getVersionNumber() != saved.getVersionNumber()) { - // throw new OptimisticLockException - // (this.getVersionNumber(), saved.getVersionNumber(), this); + // if (!derived version) { + // if (this.getVersionNumber() != saved.getVersionNumber()) { + // throw new OptimisticLockException + // (this.getVersionNumber(), saved.getVersionNumber(), this); + // } + // } else { + // if (this.getVersionNumber() <= saved.getVersionNumber()) { + // throw new OptimisticLockException + // (saved.getVersionNumber(), this, this.getVersionNumber()); + // } // } // } 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(); + StorableProperty<S> versionProperty = mInfo.getVersionProperty(); + TypeDesc versionType = TypeDesc.forClass(versionProperty.getType()); + + Label allowedVersion = b.createLabel(); + + if (!versionProperty.isDerived()) { + b.loadThis(); + b.invoke(versionProperty.getReadMethod()); + b.loadLocal(savedVar); + b.invoke(versionProperty.getReadMethod()); + CodeBuilderUtil.addValuesEqualCall + (b, versionType, true, allowedVersion, true); + + b.newObject(optimisticLockType); + b.dup(); + b.loadThis(); + b.invoke(versionProperty.getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.loadLocal(savedVar); + b.invoke(versionProperty.getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.loadThis(); + b.invokeConstructor + (optimisticLockType, + new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT, storableType}); + b.throwObject(); + } else { + b.loadThis(); + b.invoke(versionProperty.getReadMethod()); + LocalVariable newVersion = b.createLocalVariable(null, versionType); + b.storeLocal(newVersion); + + b.loadLocal(savedVar); + b.invoke(versionProperty.getReadMethod()); + LocalVariable savedVersion = b.createLocalVariable(null, versionType); + b.storeLocal(savedVersion); + + // Skip check if new or saved version is null. + branchIfNull(b, newVersion, allowedVersion); + branchIfNull(b, savedVersion, allowedVersion); + + TypeDesc primVersionType = versionType.toPrimitiveType(); + if (primVersionType != null) { + if (versionType != primVersionType) { + b.loadLocal(newVersion); + b.convert(versionType, primVersionType); + newVersion = b.createLocalVariable(null, primVersionType); + b.storeLocal(newVersion); + + b.loadLocal(savedVersion); + b.convert(versionType, primVersionType); + savedVersion = b.createLocalVariable(null, primVersionType); + b.storeLocal(savedVersion); + } + + // Skip check if new or saved version is NaN. + branchIfNaN(b, newVersion, allowedVersion); + branchIfNaN(b, savedVersion, allowedVersion); + + b.loadLocal(newVersion); + b.loadLocal(savedVersion); + b.ifComparisonBranch(allowedVersion, ">", primVersionType); + } else if (Comparable.class.isAssignableFrom(versionProperty.getType())) { + b.loadLocal(newVersion); + b.loadLocal(savedVersion); + b.invokeInterface(TypeDesc.forClass(Comparable.class), "compareTo", + TypeDesc.INT, new TypeDesc[] {TypeDesc.OBJECT}); + b.ifZeroComparisonBranch(allowedVersion, ">"); + } else { + throw new SupportException + ("Derived version property must be Comparable: " + + versionProperty); + } + + b.newObject(optimisticLockType); + b.dup(); + b.loadLocal(savedVar); + b.invoke(versionProperty.getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.loadThis(); + b.loadThis(); + b.invoke(versionProperty.getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.invokeConstructor + (optimisticLockType, + new TypeDesc[] {TypeDesc.OBJECT, storableType, TypeDesc.OBJECT}); + b.throwObject(); + } + + allowedVersion.setLocation(); } // this.copyDirtyProperties(saved); @@ -585,7 +664,9 @@ public final class MasterStorableGenerator<S extends Storable> { b.loadLocal(savedVar); b.invokeVirtual(COPY_DIRTY_PROPERTIES, null, new TypeDesc[] {storableType}); if (mFeatures.contains(MasterFeature.VERSIONING)) { - addAdjustVersionProperty(b, savedVar, -1); + if (!mInfo.getVersionProperty().isDerived()) { + addAdjustVersionProperty(b, savedVar, -1); + } } // if (!saved.doTryUpdateMaster()) { @@ -661,6 +742,31 @@ public final class MasterStorableGenerator<S extends Storable> { } } + private void branchIfNull(CodeBuilder b, LocalVariable value, Label isNull) { + if (!value.getType().isPrimitive()) { + b.loadLocal(value); + b.ifNullBranch(isNull, true); + } + } + + private void branchIfNaN(CodeBuilder b, LocalVariable value, Label isNaN) { + TypeDesc type = value.getType(); + if (type == TypeDesc.FLOAT || type == TypeDesc.DOUBLE) { + b.loadLocal(value); + if (type == TypeDesc.FLOAT) { + b.invokeStatic(TypeDesc.FLOAT.toObjectType(), + "isNaN", TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.FLOAT}); + b.ifZeroComparisonBranch(isNaN, "!="); + } else { + b.invokeStatic(TypeDesc.DOUBLE.toObjectType(), + "isNaN", TypeDesc.BOOLEAN, + new TypeDesc[] {TypeDesc.DOUBLE}); + b.ifZeroComparisonBranch(isNaN, "!="); + } + } + } + /** * Generates code to enter a transaction, if required. * diff --git a/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java b/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java index 4934db3..a28f7e6 100644 --- a/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java +++ b/src/main/java/com/amazon/carbonado/gen/StorableGenerator.java @@ -193,7 +193,7 @@ public final class StorableGenerator<S extends Storable> { * 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: + * abstract class is defined as follows: * * <pre> * /** @@ -569,27 +569,29 @@ public final class StorableGenerator<S extends Storable> { for (StorableProperty<S> property : mAllProperties.values()) { ordinal++; - if (property.isVersion()) { + if (!property.isDerived() && 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; + if (!property.isDerived()) { + 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); @@ -605,7 +607,7 @@ public final class StorableGenerator<S extends Storable> { } // Add read method. - buildReadMethod: { + buildReadMethod: if (!property.isDerived()) { Method readMethod = property.getReadMethod(); MethodInfo mi; @@ -849,7 +851,7 @@ public final class StorableGenerator<S extends Storable> { } // Add write method. - if (!property.isQuery()) { + buildWriteMethod: if (!property.isDerived() && !property.isQuery()) { Method writeMethod = property.getWriteMethod(); MethodInfo mi; @@ -1901,7 +1903,8 @@ public final class StorableGenerator<S extends Storable> { new HashMap<String, StorableProperty<S>>(); for (StorableProperty property : mAllProperties.values()) { - if (!property.isPrimaryKeyMember() && + if (!property.isDerived() && + !property.isPrimaryKeyMember() && !property.isJoin() && !property.isNullable()) { @@ -2111,7 +2114,7 @@ public final class StorableGenerator<S extends Storable> { for (StorableProperty property : mAllProperties.values()) { // Decide if property should be part of the copy. - boolean shouldCopy = !property.isJoin() && + boolean shouldCopy = !property.isDerived() && !property.isJoin() && (property.isPrimaryKeyMember() && pkProperties || property.isVersion() && versionProperty || !property.isPrimaryKeyMember() && dataProperties); @@ -2309,7 +2312,7 @@ public final class StorableGenerator<S extends Storable> { int ordinal = 0; int mask = 0; for (StorableProperty property : mAllProperties.values()) { - if (property != joinProperty && !property.isJoin()) { + if (property != joinProperty && !property.isDerived() && !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)) { @@ -2343,19 +2346,21 @@ public final class StorableGenerator<S extends Storable> { for (StorableProperty property : mAllProperties.values()) { ordinal++; - if (property.isJoin() || mGenMode == GEN_ABSTRACT) { - requireStateField = true; - } + if (!property.isDerived()) { + if (property.isJoin() || mGenMode == GEN_ABSTRACT) { + requireStateField = true; + } - if (ordinal == maxOrdinal || ((ordinal & 0xf) == 0xf)) { - if (requireStateField) { - String stateFieldName = PROPERTY_STATE_FIELD_NAME + (ordinal >> 4); + 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); + b.loadThis(); + b.loadConstant(0); + b.storeField(stateFieldName, TypeDesc.INT); + } + requireStateField = false; } - requireStateField = false; } } } @@ -2382,17 +2387,19 @@ public final class StorableGenerator<S extends Storable> { 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); + if (!property.isDerived()) { + 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); + } } } @@ -2443,14 +2450,16 @@ public final class StorableGenerator<S extends Storable> { 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); + if (!property.isDerived()) { + 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); } - } else if (name == MARK_ALL_PROPERTIES_DIRTY) { - // Force dirty state (3). - orMask |= PROPERTY_STATE_DIRTY << ((ordinal & 0xf) * 2); } ordinal++; @@ -2514,16 +2523,18 @@ public final class StorableGenerator<S extends Storable> { 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)); + if (!property.isDerived()) { + 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)); + } } } } @@ -2556,13 +2567,15 @@ public final class StorableGenerator<S extends Storable> { 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); + if (!property.isDerived()) { + 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) { @@ -2623,8 +2636,10 @@ public final class StorableGenerator<S extends Storable> { int ordinal = 0; int mask = 0; for (StorableProperty property : mAllProperties.values()) { - if (properties.containsKey(property.getName())) { - mask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2); + if (!property.isDerived()) { + if (properties.containsKey(property.getName())) { + mask |= PROPERTY_STATE_MASK << ((ordinal & 0xf) * 2); + } } ordinal++; if (((ordinal & 0xf) == 0 || ordinal >= mAllProperties.size()) && mask != 0) { @@ -2772,6 +2787,7 @@ public final class StorableGenerator<S extends Storable> { // Params to invoke String.equals. TypeDesc[] params = {TypeDesc.OBJECT}; + Label derivedMatch = null; Label joinMatch = null; for (int i=0; i<caseCount; i++) { @@ -2802,7 +2818,12 @@ public final class StorableGenerator<S extends Storable> { b.ifZeroComparisonBranch(notEqual, "=="); } - if (prop.isJoin()) { + if (prop.isDerived()) { + if (derivedMatch == null) { + derivedMatch = b.createLabel(); + } + b.branch(derivedMatch); + } else if (prop.isJoin()) { if (joinMatch == null) { joinMatch = b.createLabel(); } @@ -2841,6 +2862,18 @@ public final class StorableGenerator<S extends Storable> { b.invokeConstructor(exceptionType, params); b.throwObject(); + if (derivedMatch != null) { + derivedMatch.setLocation(); + + b.newObject(exceptionType); + b.dup(); + b.loadConstant("Cannot get state for derived 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(); @@ -2936,7 +2969,7 @@ public final class StorableGenerator<S extends Storable> { boolean mixIn = false; for (StorableProperty property : mAllProperties.values()) { - if (property.isJoin()) { + if (property.isDerived() || property.isJoin()) { continue; } addHashCodeCall(b, property.getName(), @@ -3105,7 +3138,7 @@ public final class StorableGenerator<S extends Storable> { b.storeLocal(other); for (StorableProperty property : mAllProperties.values()) { - if (property.isJoin()) { + if (property.isDerived() || property.isJoin()) { continue; } // If we're only comparing keys, and this isn't a key, skip it @@ -3211,8 +3244,10 @@ public final class StorableGenerator<S extends Storable> { // 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())) { + // Don't print any derived or join properties since they may throw an exception. + if (!property.isPrimaryKeyMember() && + (!property.isDerived()) && (!property.isJoin())) + { Label skipPrint = b.createLabel(); // Check if independent property is supported, and skip if not. diff --git a/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java b/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java index 5115854..09eff96 100644 --- a/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java +++ b/src/main/java/com/amazon/carbonado/gen/StorableSerializer.java @@ -116,11 +116,11 @@ public abstract class StorableSerializer<S extends Storable> { StorableProperty<S>[] properties; { - // Exclude joins. + // Exclude derived properties and joins. List<StorableProperty<S>> list = new ArrayList<StorableProperty<S>>(propertyMap.size()); for (StorableProperty<S> property : propertyMap.values()) { - if (!property.isJoin()) { + if (!property.isDerived() && !property.isJoin()) { list.add(property); } } diff --git a/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java b/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java index f318c33..7451fbd 100644 --- a/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java +++ b/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java @@ -51,6 +51,7 @@ import com.amazon.carbonado.Alias; import com.amazon.carbonado.AlternateKeys; import com.amazon.carbonado.Authoritative; import com.amazon.carbonado.Automatic; +import com.amazon.carbonado.Derived; import com.amazon.carbonado.FetchException; import com.amazon.carbonado.Index; import com.amazon.carbonado.Indexes; @@ -230,9 +231,54 @@ public class StorableIntrospector { // late, then there would be a stack overflow. for (StorableProperty property : properties.values()) { if (property instanceof JoinProperty) { - ((JoinProperty)property).resolve(errorMessages, properties); + ((JoinProperty)property).resolveJoin(errorMessages, info); } } + + // Resolve derived properties after join properties, since they may + // depend on them. + boolean anyDerived = false; + for (StorableProperty<S> property : properties.values()) { + if (property instanceof SimpleProperty && property.isDerived()) { + anyDerived = true; + ((SimpleProperty)property).resolveDerivedFrom(errorMessages, info); + } + } + + if (anyDerived && errorMessages.size() == 0) { + // Make sure that any indexes which refer to derived properties + // throwing FetchException have derived-from properties + // listed. Why? The exception likely indicates that a join + // property is being fetched. + + for (StorableIndex<S> index : indexes) { + for (StorableProperty<S> property : index.getProperties()) { + if (property.isDerived() && property.getReadMethod() != null && + property.getDerivedFromProperties().length == 0) + { + Class exceptionType = FetchException.class; + + Class<?>[] exceptions = property.getReadMethod().getExceptionTypes(); + boolean fetches = false; + for (int i=exceptions.length; --i>=0; ) { + if (exceptions[i].isAssignableFrom(exceptionType)) { + fetches = true; + break; + } + } + + if (fetches) { + errorMessages.add + ("Index refers to a derived property which declares " + + "throwing a FetchException, but property does not " + + "list any derived-from properties: \"" + + property.getName() + "'"); + } + } + } + } + } + if (errorMessages.size() > 0) { cCache.remove(type); throw new MalformedTypeException(type, errorMessages); @@ -482,14 +528,15 @@ public class StorableIntrospector { } // Check if abstract method is just redefining a method in // Storable. - // TODO: Check if abstract method is redefining return type, which - // is allowed for copy method. The return type must be within its - // bounds. try { Method m2 = Storable.class.getMethod(m.getName(), (Class[]) m.getParameterTypes()); if (m.getReturnType() == m2.getReturnType()) { it.remove(); } + // Copy method can be redefined with specialized return type. + if (m.getName().equals("copy") && type.isAssignableFrom(m.getReturnType())) { + it.remove(); + } } catch (NoSuchMethodException e) { // Not defined in Storable. } @@ -523,7 +570,20 @@ public class StorableIntrospector { Method readMethod = property.getReadMethod(); Method writeMethod = property.getWriteMethod(); - if (readMethod == null && writeMethod == null) { + boolean isAbstract; + if (readMethod == null) { + if (writeMethod == null) { + continue; + } else if (!Modifier.isAbstract(writeMethod.getModifiers()) && + writeMethod.getAnnotation(Derived.class) == null) + { + // Ignore concrete property methods unless they're derived. + continue; + } + } else if (!Modifier.isAbstract(readMethod.getModifiers()) && + readMethod.getAnnotation(Derived.class) == null) + { + // Ignore concrete property methods unless they're derived. continue; } @@ -540,7 +600,7 @@ public class StorableIntrospector { if (readMethod != null) { String sig = createSig(readMethod); - if (methods.containsKey(sig)) { + if (storableProp.isDerived() || methods.containsKey(sig)) { methods.remove(sig); properties.put(property.getName(), storableProp); } else { @@ -550,7 +610,7 @@ public class StorableIntrospector { if (writeMethod != null) { String sig = createSig(writeMethod); - if (methods.containsKey(sig)) { + if (storableProp.isDerived() || methods.containsKey(sig)) { methods.remove(sig); properties.put(property.getName(), storableProp); } else { @@ -698,6 +758,7 @@ public class StorableIntrospector { Automatic automatic = null; Independent independent = null; Join join = null; + Derived derived = null; Method readMethod = property.getReadMethod(); Method writeMethod = property.getWriteMethod(); @@ -717,14 +778,16 @@ public class StorableIntrospector { automatic = readMethod.getAnnotation(Automatic.class); independent = readMethod.getAnnotation(Independent.class); join = readMethod.getAnnotation(Join.class); + derived = readMethod.getAnnotation(Derived.class); } if (writeMethod == null) { if (readMethod == null || Modifier.isAbstract(readMethod.getModifiers())) { // Set method is always required for non-join properties. More // work is done later on join properties, and sometimes the - // write method is required. - if (join == null) { + // write method is required. Derived properties don't need a + // set method. + if (join == null && derived == null) { errorMessages.add("Must define proper 'set' method for property: " + property.getName()); } @@ -758,6 +821,35 @@ public class StorableIntrospector { errorMessages.add ("Join annotation not allowed on mutator: " + writeMethod); } + if (writeMethod.getAnnotation(Derived.class) != null) { + errorMessages.add + ("Derived annotation not allowed on mutator: " + writeMethod); + } + } + + if (derived != null) { + if (readMethod != null && Modifier.isAbstract(readMethod.getModifiers()) || + writeMethod != null && Modifier.isAbstract(writeMethod.getModifiers())) + { + errorMessages.add("Derived properties cannot be abstract: " + + property.getName()); + } + if (pk) { + errorMessages.add("Derived properties cannot be a member of primary key: " + + property.getName()); + } + if (sequence != null) { + errorMessages.add("Derived properties cannot have a Sequence annotation: " + + property.getName()); + } + if (automatic != null) { + errorMessages.add("Derived properties cannot have an Automatic annotation: " + + property.getName()); + } + if (join != null) { + errorMessages.add("Derived properties cannot have a Join annotation: " + + property.getName()); + } } if (nullable != null && property.getType().isPrimitive()) { @@ -813,6 +905,48 @@ public class StorableIntrospector { gatherAdapters(property, writeMethod, false, errorMessages); } + // Check that declared checked exceptions are allowed. + if (readMethod != null) { + for (Class<?> ex : readMethod.getExceptionTypes()) { + if (RuntimeException.class.isAssignableFrom(ex) + || Error.class.isAssignableFrom(ex)) + { + continue; + } + if (join != null || derived != null) { + if (FetchException.class.isAssignableFrom(ex)) { + continue; + } + errorMessages.add + ("Checked exceptions thrown by join or derived property accessors " + + "must be of type FetchException: \"" + readMethod.getName() + + "\" declares throwing \"" + ex.getName() + '"'); + break; + } else { + errorMessages.add + ("Only join and derived property accessors can throw checked " + + "exceptions: \"" + readMethod.getName() + "\" declares throwing \"" + + ex.getName() + '"'); + break; + } + } + } + + // Check that declared checked exceptions are allowed. + if (writeMethod != null) { + for (Class<?> ex : writeMethod.getExceptionTypes()) { + if (RuntimeException.class.isAssignableFrom(ex) + || Error.class.isAssignableFrom(ex)) + { + continue; + } + errorMessages.add + ("Mutators cannot throw checked exceptions: \"" + writeMethod.getName() + + "\" declares throwing \"" + ex.getName() + '"'); + break; + } + } + String sequenceName = null; if (sequence != null) { sequenceName = sequence.value(); @@ -825,7 +959,8 @@ public class StorableIntrospector { return new SimpleProperty<S> (property, enclosing, nullable != null, pk, altKey, aliases, constraints, adapters == null ? null : adapters[0], - version != null, sequenceName, independent != null, automatic != null); + version != null, sequenceName, + independent != null, automatic != null, derived); } // Do additional work for join properties. @@ -933,7 +1068,7 @@ public class StorableIntrospector { return new JoinProperty<S> (property, enclosing, nullable != null, aliases, constraints, adapters == null ? null : adapters[0], - sequenceName, independent != null, automatic != null, + sequenceName, independent != null, automatic != null, derived, joinedType, internal, external); } @@ -1412,6 +1547,8 @@ public class StorableIntrospector { } private static class SimpleProperty<S extends Storable> implements StorableProperty<S> { + private static final ChainedProperty[] EMPTY_CHAIN_ARRAY = new ChainedProperty[0]; + private final BeanProperty mBeanProperty; private final Class<S> mEnclosingType; private final boolean mNullable; @@ -1424,13 +1561,24 @@ public class StorableIntrospector { private final String mSequence; private final boolean mIndependent; private final boolean mAutomatic; + private final boolean mIsDerived; + + // Temporary reference until derived from is resolved. + private Derived mDerived; + + // Resolved derived from properties. + private ChainedProperty<S>[] mDerivedFrom; + + // Resolved derived to properties. + private ChainedProperty<S>[] mDerivedTo; SimpleProperty(BeanProperty property, Class<S> enclosing, boolean nullable, boolean primaryKey, boolean alternateKey, String[] aliases, StorablePropertyConstraint[] constraints, StorablePropertyAdapter adapter, boolean isVersion, String sequence, - boolean independent, boolean automatic) + boolean independent, boolean automatic, + Derived derived) { mBeanProperty = property; mEnclosingType = enclosing; @@ -1444,6 +1592,8 @@ public class StorableIntrospector { mSequence = sequence; mIndependent = independent; mAutomatic = automatic; + mIsDerived = derived != null; + mDerived = derived; } public final String getName() { @@ -1536,10 +1686,45 @@ public class StorableIntrospector { return mIsVersion; } + public final boolean isDerived() { + return mIsDerived; + } + + public final ChainedProperty<S>[] getDerivedFromProperties() { + return (!mIsDerived || mDerivedFrom == null) ? + EMPTY_CHAIN_ARRAY : mDerivedFrom.clone(); + } + + public final ChainedProperty<?>[] getDerivedToProperties() { + if (mDerivedTo == null) { + // Derived-to properties must be determined on demand because + // introspection might have been initiated by a dependency. If + // that dependency is asked for derived properties, it will not + // yet have resolved derived-from properties. + + Set<ChainedProperty<?>> derivedToSet = new LinkedHashSet<ChainedProperty<?>>(); + Set<Class<?>> examinedSet = new HashSet<Class<?>>(); + + addToDerivedToSet(derivedToSet, examinedSet, examine(getEnclosingType())); + + if (derivedToSet.size() > 0) { + mDerivedTo = derivedToSet.toArray(new ChainedProperty[derivedToSet.size()]); + } else { + mDerivedTo = EMPTY_CHAIN_ARRAY; + } + } + + return mDerivedTo.clone(); + } + public boolean isJoin() { return false; } + public boolean isOneToOneJoin() { + return false; + } + public Class<? extends Storable> getJoinedType() { return null; } @@ -1635,6 +1820,216 @@ public class StorableIntrospector { app.append(getEnclosingType().getName()); app.append('}'); } + + void resolveDerivedFrom(List<String> errorMessages, StorableInfo<S> info) { + Derived derived = mDerived; + // Don't need this anymore. + mDerived = null; + + if (!mIsDerived || derived == null) { + return; + } + String[] fromNames = derived.from(); + if (fromNames == null || fromNames.length == 0) { + return; + } + + Set<ChainedProperty<S>> derivedFromSet = new LinkedHashSet<ChainedProperty<S>>(); + + for (String fromName : fromNames) { + ChainedProperty<S> from; + try { + from = ChainedProperty.parse(info, fromName); + } catch (IllegalArgumentException e) { + errorMessages.add + ("Cannot find derived-from property: \"" + + getName() + "\" reports being derived from \"" + + fromName + '"'); + continue; + } + addToDerivedFromSet(errorMessages, derivedFromSet, from); + } + + if (derivedFromSet.size() > 0) { + if (derivedFromSet.contains(ChainedProperty.get(this))) { + errorMessages.add + ("Derived-from dependency cycle detected: \"" + getName() + '"'); + } + + mDerivedFrom = derivedFromSet + .toArray(new ChainedProperty[derivedFromSet.size()]); + } else { + mDerivedFrom = null; + } + } + + private boolean addToDerivedFromSet(List<String> errorMessages, + Set<ChainedProperty<S>> derivedFromSet, + ChainedProperty<S> from) + { + if (derivedFromSet.contains(from)) { + return false; + } + + derivedFromSet.add(from); + + ChainedProperty<S> trimmed = from.getChainCount() == 0 ? null : from.trim(); + + if (trimmed != null) { + // Include all join properties as dependencies. + addToDerivedFromSet(errorMessages, derivedFromSet, trimmed); + } + + StorableProperty<?> lastInChain = from.getLastProperty(); + + if (lastInChain.isDerived()) { + // Expand derived dependencies. + ((SimpleProperty) lastInChain) + .resolveDerivedFrom(errorMessages, examine(lastInChain.getEnclosingType())); + for (ChainedProperty<?> lastFrom : lastInChain.getDerivedFromProperties()) { + ChainedProperty<S> dep; + if (trimmed == null) { + dep = (ChainedProperty<S>) lastFrom; + } else { + dep = trimmed.append(lastFrom); + } + addToDerivedFromSet(errorMessages, derivedFromSet, dep); + } + } + + if (lastInChain.isJoin() && errorMessages.size() == 0) { + // Make sure that join is doubly specified. Why? Consider the + // case where the derived property is a member of an index or + // key. If the joined Storable class gets loaded first, it will + // not know that an index exists that it should keep + // up-to-date. With the double join, it can check to see if + // there are any foreign indexes. This check could probably be + // skipped if the derived property doesn't belong to an index + // or key, but consistent error checking behavior is desirable. + + Class<? extends Storable> joined = lastInChain.getJoinedType(); + + doubly: { + for (StorableProperty<?> prop : examine(joined).getAllProperties().values()) { + if (prop.isJoin() && + prop.getJoinedType() == lastInChain.getEnclosingType()) + { + break doubly; + } + } + + StringBuilder suggest = new StringBuilder(); + + suggest.append("@Join"); + + int count = lastInChain.getJoinElementCount(); + boolean naturalJoin = true; + for (int i=0; i<count; i++) { + if (!lastInChain.getInternalJoinElement(i).getName().equals + (lastInChain.getExternalJoinElement(i).getName())) + { + naturalJoin = false; + break; + } + } + + if (!naturalJoin) { + suggest.append("(internal="); + if (count > 1) { + suggest.append('{'); + } + for (int i=0; i<count; i++) { + if (i > 0) { + suggest.append(", "); + } + suggest.append('"'); + // This property's external is other's internal. + suggest.append(lastInChain.getExternalJoinElement(i).getName()); + suggest.append('"'); + } + if (count > 1) { + suggest.append('}'); + } + + suggest.append(", external="); + if (count > 1) { + suggest.append('{'); + } + for (int i=0; i<count; i++) { + if (i > 0) { + suggest.append(", "); + } + suggest.append('"'); + // This property's internal is other's external. + suggest.append(lastInChain.getInternalJoinElement(i).getName()); + suggest.append('"'); + } + if (count > 1) { + suggest.append('}'); + } + + suggest.append(")"); + } + + suggest.append(' '); + + if (!joined.isInterface()) { + suggest.append("public abstract "); + } + + if (lastInChain.isOneToOneJoin() || lastInChain.isQuery()) { + suggest.append(lastInChain.getEnclosingType().getName()); + } else { + suggest.append("Query<"); + suggest.append(lastInChain.getEnclosingType().getName()); + suggest.append('>'); + } + + suggest.append(" getXxx() throws FetchException"); + + errorMessages.add + ("Derived-from property is a join, but it is not doubly joined: \"" + + getName() + "\" is derived from \"" + from + + "\". Consider defining a join property in " + joined + " as: " + suggest); + } + } + + return true; + } + + private boolean addToDerivedToSet(Set<ChainedProperty<?>> derivedToSet, + Set<Class<?>> examinedSet, + StorableInfo<?> info) + { + if (examinedSet.contains(info.getStorableType())) { + return false; + } + + // Prevent infinite loop while following join paths. + examinedSet.add(info.getStorableType()); + + final int originalSize = derivedToSet.size(); + + for (StorableProperty<?> property : info.getAllProperties().values()) { + if (property.isDerived()) { + for (ChainedProperty<?> from : property.getDerivedFromProperties()) { + if (from.getLastProperty().equals(this)) { + ChainedProperty<?> path = ChainedProperty.get(property); + if (from.getChainCount() > 0) { + path = path.append(from.trim()); + } + derivedToSet.add(path); + } + } + } + if (property.isJoin()) { + addToDerivedToSet(derivedToSet, examinedSet, + examine(property.getJoinedType())); + } + } + + return derivedToSet.size() > originalSize; + } } private static final class JoinProperty<S extends Storable> extends SimpleProperty<S> { @@ -1649,16 +2044,19 @@ public class StorableIntrospector { private StorableProperty<S>[] mInternal; private StorableProperty<?>[] mExternal; + private boolean mOneToOne; + JoinProperty(BeanProperty property, Class<S> enclosing, boolean nullable, String[] aliases, StorablePropertyConstraint[] constraints, StorablePropertyAdapter adapter, String sequence, boolean independent, boolean automatic, + Derived derived, Class<? extends Storable> joinedType, String[] internal, String[] external) { super(property, enclosing, nullable, false, false, - aliases, constraints, adapter, false, sequence, independent, automatic); + aliases, constraints, adapter, false, sequence, independent, automatic, derived); mJoinedType = joinedType; int length = internal.length; @@ -1674,6 +2072,10 @@ public class StorableIntrospector { return true; } + public boolean isOneToOneJoin() { + return mOneToOne; + } + public Class<? extends Storable> getJoinedType() { return mJoinedType; } @@ -1706,32 +2108,39 @@ public class StorableIntrospector { * Finishes the definition of this join property. Can only be called once. */ @SuppressWarnings("unchecked") - void resolve(List<String> errorMessages, Map<String, StorableProperty<S>> properties) { - StorableInfo<?> joinedInfo = examine(getJoinedType()); + void resolveJoin(List<String> errorMessages, StorableInfo<S> info) { + StorableInfo<?> joinedInfo; + try { + joinedInfo = examine(getJoinedType()); - if (mInternalNames.length == 0) { - // Since no join elements specified, perform a natural join. - // If the joined type is a list, then the join elements are - // defined by this enclosing type's primary keys. Otherwise, - // they are defined by the joined type's primary keys. + if (mInternalNames.length == 0) { + // Since no join elements specified, perform a natural join. + // If the joined type is a list, then the join elements are + // defined by this enclosing type's primary keys. Otherwise, + // they are defined by the joined type's primary keys. - Map<String, ? extends StorableProperty<?>> primaryKeys; + Map<String, ? extends StorableProperty<?>> primaryKeys; - if (isQuery()) { - primaryKeys = examine(getEnclosingType()).getPrimaryKeyProperties(); - } else { - primaryKeys = joinedInfo.getPrimaryKeyProperties(); - } + if (isQuery()) { + primaryKeys = examine(getEnclosingType()).getPrimaryKeyProperties(); + } else { + primaryKeys = joinedInfo.getPrimaryKeyProperties(); + } - mInternalNames = new String[primaryKeys.size()]; - mExternalNames = new String[primaryKeys.size()]; + mInternalNames = new String[primaryKeys.size()]; + mExternalNames = new String[primaryKeys.size()]; - int i = 0; - for (String name : primaryKeys.keySet()) { - mInternalNames[i] = name; - mExternalNames[i] = name; - i++; + int i = 0; + for (String name : primaryKeys.keySet()) { + mInternalNames[i] = name; + mExternalNames[i] = name; + i++; + } } + } catch (MalformedTypeException e) { + mInternal = new StorableProperty[0]; + mExternal = new StorableProperty[0]; + throw e; } mInternal = new StorableProperty[mInternalNames.length]; @@ -1740,7 +2149,7 @@ public class StorableIntrospector { // Verify that internal properties exist and are not themselves joins. for (int i=0; i<mInternalNames.length; i++) { String internalName = mInternalNames[i]; - StorableProperty property = properties.get(internalName); + StorableProperty property = info.getAllProperties().get(internalName); if (property == null) { errorMessages.add ("Cannot find internal join element: \"" + @@ -1958,12 +2367,12 @@ public class StorableIntrospector { // Test which keys of joined object are specified. // Create a copy of all the primary keys of joined object. - Set<StorableProperty> primaryKeys = + Set<StorableProperty> primaryKey = new HashSet<StorableProperty>(joinedInfo.getPrimaryKeyProperties().values()); // Remove external properties from the primary key set. for (int i=0; i<mInternal.length; i++) { - primaryKeys.remove(getExternalJoinElement(i)); + primaryKey.remove(getExternalJoinElement(i)); } // Do similar test for alternate keys. @@ -1996,7 +2405,7 @@ public class StorableIntrospector { if (isQuery()) { // Key of joined object must not be completely specified. - if (primaryKeys.size() <= 0) { + if (primaryKey.size() <= 0) { errorMessages.add ("Join property \"" + getName() + "\" completely specifies primary key of joined object; " + @@ -2019,7 +2428,7 @@ public class StorableIntrospector { fullKeyCheck: { - if (primaryKeys.size() <= 0) { + if (primaryKey.size() <= 0) { break fullKeyCheck; } @@ -2035,6 +2444,48 @@ public class StorableIntrospector { "declaring the property type as Query<" + getJoinedType().getName() + '>'); } + + // Determine if one-to-one join. If internal properties + // completely specify any key, then it is one-to-one. + + boolean oneToOne = false; + + oneToOneCheck: { + Set<StorableProperty> internalPrimaryKey = + new HashSet<StorableProperty>(info.getPrimaryKeyProperties().values()); + + for (int i=0; i<mInternal.length; i++) { + internalPrimaryKey.remove(getInternalJoinElement(i)); + if (internalPrimaryKey.size() == 0) { + oneToOne = true; + break oneToOneCheck; + } + } + + altKeyScan: + for (int i=0; i<info.getAlternateKeyCount(); i++) { + Set<StorableProperty> altKey = new HashSet<StorableProperty>(); + + for (OrderedProperty op : info.getAlternateKey(i).getProperties()) { + ChainedProperty chained = op.getChainedProperty(); + if (chained.getChainCount() > 0) { + // Funny alt key. Pretend it does not exist. + continue altKeyScan; + } + altKey.add(chained.getPrimeProperty()); + } + + for (int j=0; j<mInternal.length; j++) { + altKey.remove(getInternalJoinElement(j)); + if (altKey.size() == 0) { + oneToOne = true; + break oneToOneCheck; + } + } + } + } + + mOneToOne = oneToOne; } if (mutatorAllowed && getWriteMethod() == null) { diff --git a/src/main/java/com/amazon/carbonado/info/StorableProperty.java b/src/main/java/com/amazon/carbonado/info/StorableProperty.java index ce374ac..b3362c6 100644 --- a/src/main/java/com/amazon/carbonado/info/StorableProperty.java +++ b/src/main/java/com/amazon/carbonado/info/StorableProperty.java @@ -115,12 +115,19 @@ public interface StorableProperty<S extends Storable> extends Appender { String[] getAliases(); /** - * Returns true if this property is joined to another Storable. + * Returns true if this property is joined in any way to another Storable. * * @see com.amazon.carbonado.Join */ boolean isJoin(); + /** + * Returns true if this property is one-to-one joined to another Storable. + * + * @see com.amazon.carbonado.Join + */ + boolean isOneToOneJoin(); + /** * Returns the type of property this is joined to, or null if not joined. */ @@ -211,5 +218,32 @@ public interface StorableProperty<S extends Storable> extends Appender { */ boolean isIndependent(); + /** + * Returns true if this property is derived. + * + * @see com.amazon.carbonado.Derived + */ + boolean isDerived(); + + /** + * Returns a new array with all the derived-from properties, which is empty + * if this is not a derived property. Otherwise, the set is the transitive + * closure of all dependent properties. This set may include joins and + * other derived properties. + */ + ChainedProperty<S>[] getDerivedFromProperties(); + + /** + * Returns a new array with all the properties which are derived from this + * one. The set is the transitive closure of all derived properties which + * depend on this one. + * + * <p>Each property in the set is represented as a chain, where the prime + * property is the actual dependent property, and the tail is the path to + * reach this property's enclosing type. If a derived property resides in + * the same enclosing type as this one, the chain count is zero. + */ + ChainedProperty<?>[] getDerivedToProperties(); + String toString(); } diff --git a/src/main/java/com/amazon/carbonado/layout/Layout.java b/src/main/java/com/amazon/carbonado/layout/Layout.java index 1c6b63d..ca50172 100644 --- a/src/main/java/com/amazon/carbonado/layout/Layout.java +++ b/src/main/java/com/amazon/carbonado/layout/Layout.java @@ -172,7 +172,7 @@ public class Layout { List<LayoutProperty> list = new ArrayList<LayoutProperty>(properties.size()); int ordinal = 0; for (StorableProperty<?> property : properties) { - if (property.isJoin()) { + if (property.isDerived() || property.isJoin()) { continue; } StoredLayoutProperty storedLayoutProperty = mLayoutFactory.mPropertyStorage.prepare(); diff --git a/src/main/java/com/amazon/carbonado/qe/AbstractQuery.java b/src/main/java/com/amazon/carbonado/qe/AbstractQuery.java index 436c2c2..7639348 100644 --- a/src/main/java/com/amazon/carbonado/qe/AbstractQuery.java +++ b/src/main/java/com/amazon/carbonado/qe/AbstractQuery.java @@ -123,4 +123,10 @@ public abstract class AbstractQuery<S extends Storable> implements Query<S>, App } return b.toString(); } + + @Override + public abstract int hashCode(); + + @Override + public abstract boolean equals(Object obj); } diff --git a/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java b/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java index 161ad90..d9969c5 100644 --- a/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java +++ b/src/main/java/com/amazon/carbonado/qe/EmptyQuery.java @@ -273,6 +273,24 @@ public final class EmptyQuery<S extends Storable> extends AbstractQuery<S> { return false; } + @Override + public int hashCode() { + return mFactory.hashCode() * 31 + mOrdering.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof EmptyQuery) { + EmptyQuery<?> other = (EmptyQuery<?>) obj; + return mFactory.equals(other.mFactory) + && mOrdering.equals(other.mOrdering); + } + return false; + } + private IllegalStateException error() { return new IllegalStateException("Query doesn't have any parameters"); } diff --git a/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java b/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java index cba6776..c0fa0af 100644 --- a/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java +++ b/src/main/java/com/amazon/carbonado/raw/GenericEncodingStrategy.java @@ -19,7 +19,9 @@ package com.amazon.carbonado.raw; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; import org.cojen.classfile.CodeAssembler; @@ -397,14 +399,15 @@ public class GenericEncodingStrategy<S extends Storable> { Map<String, ? extends StorableProperty<S>> map = StorableIntrospector.examine(mType).getDataProperties(); - StorableProperty<S>[] properties = new StorableProperty[map.size()]; + List<StorableProperty<S>> list = new ArrayList<StorableProperty<S>>(map.size()); - int ordinal = 0; for (StorableProperty<S> property : map.values()) { - properties[ordinal++] = property; + if (!property.isDerived()) { + list.add(property); + } } - return properties; + return list.toArray(new StorableProperty[list.size()]); } protected StorablePropertyInfo checkSupport(StorableProperty<S> property) diff --git a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java index 43677f2..2532556 100644 --- a/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java +++ b/src/main/java/com/amazon/carbonado/raw/GenericStorableCodec.java @@ -797,7 +797,7 @@ public class GenericStorableCodec<S extends Storable> implements StorableCodec<S StorableIntrospector.examine(altStorable).getAllProperties(); for (StorableProperty prop : currentProps.values()) { - if (prop.isJoin()) { + if (prop.isDerived() || prop.isJoin()) { continue; } diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/DependentStorableFetcher.java b/src/main/java/com/amazon/carbonado/repo/indexed/DependentStorableFetcher.java new file mode 100644 index 0000000..4c24c04 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/DependentStorableFetcher.java @@ -0,0 +1,174 @@ +/* + * Copyright 2007 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.carbonado.repo.indexed; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; + +import org.cojen.util.BeanPropertyAccessor; + +import com.amazon.carbonado.Cursor; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Query; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.Transaction; + +import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.filter.RelOp; + +import com.amazon.carbonado.info.ChainedProperty; +import com.amazon.carbonado.info.StorableProperty; + +/** + * Fetches Storables that have indexed derived-to properties which depend on S. + * + * @author Brian S O'Neill + */ +class DependentStorableFetcher<S extends Storable, D extends Storable> { + private final IndexedRepository mRepository; + private final IndexEntryAccessor<D>[] mIndexEntryAccessors; + private final Query<D> mQuery; + private final String[] mJoinProperties; + private final BeanPropertyAccessor mPropertyAccessor; + + /** + * @param derivedTo special chained property from StorableProperty.getDerivedToProperties + */ + DependentStorableFetcher(IndexedRepository repository, + Class<S> sType, ChainedProperty<D> derivedTo) + throws RepositoryException + { + if (derivedTo.getChainCount() == 0) { + throw new IllegalArgumentException(); + } + if (derivedTo.getLastProperty().getType() != sType) { + throw new IllegalArgumentException(); + } + if (!derivedTo.getLastProperty().isJoin()) { + throw new IllegalArgumentException(); + } + + Class<D> dType = derivedTo.getPrimeProperty().getEnclosingType(); + + // Find the indexes that contain the prime derivedTo property. + List<IndexEntryAccessor<D>> accessorList = new ArrayList<IndexEntryAccessor<D>>(); + for (IndexEntryAccessor<D> acc : repository.getIndexEntryAccessors(dType)) { + for (String indexPropName : acc.getPropertyNames()) { + if (indexPropName.equals(derivedTo.getPrimeProperty().getName())) { + accessorList.add(acc); + break; + } + } + } + + if (accessorList.size() == 0) { + throw new SupportException + ("Unable to find index accessors for derived-to property: " + derivedTo + + ", enclosing type: " + dType); + } + + // Build a query on D joined to S. + + StorableProperty<S> join = (StorableProperty<S>) derivedTo.getLastProperty(); + + ChainedProperty<?> base; + if (derivedTo.getChainCount() <= 1) { + base = null; + } else { + base = derivedTo.tail().trim(); + } + + int joinElementCount = join.getJoinElementCount(); + String[] joinProperties = new String[joinElementCount]; + + Filter<D> dFilter = Filter.getOpenFilter(dType); + for (int i=0; i<joinElementCount; i++) { + StorableProperty<S> element = join.getInternalJoinElement(i); + joinProperties[i] = element.getName(); + if (base == null) { + dFilter = dFilter.and(element.getName(), RelOp.EQ); + } else { + dFilter = dFilter.and(base.append(element).toString(), RelOp.EQ); + } + } + + mRepository = repository; + mIndexEntryAccessors = accessorList.toArray(new IndexEntryAccessor[accessorList.size()]); + mQuery = repository.storageFor(dType).query(dFilter); + mJoinProperties = joinProperties; + mPropertyAccessor = BeanPropertyAccessor.forClass(sType); + } + + public Transaction enterTransaction() { + return mRepository.enterTransaction(); + } + + public Cursor<D> fetchDependenentStorables(S storable) throws FetchException { + Query<D> query = mQuery; + for (String property : mJoinProperties) { + query = query.with(mPropertyAccessor.getPropertyValue(storable, property)); + } + return query.fetch(); + } + + /** + * @return amount added to list + */ + public int createIndexEntries(D master, List<Storable> indexEntries) { + IndexEntryAccessor[] accessors = mIndexEntryAccessors; + int length = accessors.length; + for (int i=0; i<length; i++) { + IndexEntryAccessor accessor = accessors[i]; + Storable indexEntry = accessor.getIndexEntryStorage().prepare(); + accessor.copyFromMaster(indexEntry, master); + indexEntries.add(indexEntry); + } + return length; + } + + @Override + public int hashCode() { + return mQuery.getFilter().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof DependentStorableFetcher) { + DependentStorableFetcher other = (DependentStorableFetcher) obj; + return mQuery.getFilter().equals(other.mQuery.getFilter()) + && Arrays.equals(mJoinProperties, other.mJoinProperties) + && Arrays.equals(mIndexEntryAccessors, other.mIndexEntryAccessors); + } + return false; + } + + @Override + public String toString() { + return "DependentStorableFetcher: {indexes=" + Arrays.toString(mIndexEntryAccessors) + + ", query=" + mQuery + + ", join properties=" + Arrays.toString(mJoinProperties) + '}'; + } +} diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/DerivedIndexesTrigger.java b/src/main/java/com/amazon/carbonado/repo/indexed/DerivedIndexesTrigger.java new file mode 100644 index 0000000..0a99fdd --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/DerivedIndexesTrigger.java @@ -0,0 +1,164 @@ +/* + * Copyright 2007 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.carbonado.repo.indexed; + +import java.util.ArrayList; +import java.util.List; + +import com.amazon.carbonado.Cursor; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Transaction; +import com.amazon.carbonado.Trigger; + +import com.amazon.carbonado.info.ChainedProperty; + +/** + * Handles index updates for derived-to properties. + * + * @author Brian S O'Neill + */ +class DerivedIndexesTrigger<S extends Storable, D extends Storable> extends Trigger<S> { + private final DependentStorableFetcher<S, D> mFetcher; + + /** + * @param derivedTo special chained property from StorableProperty.getDerivedToProperties + */ + DerivedIndexesTrigger(IndexedRepository repository, + Class<S> sType, ChainedProperty<D> derivedTo) + throws RepositoryException + { + this(new DependentStorableFetcher(repository, sType, derivedTo)); + } + + DerivedIndexesTrigger(DependentStorableFetcher<S, D> fetcher) { + mFetcher = fetcher; + } + + @Override + public Object beforeInsert(S storable) throws PersistException { + return createDependentIndexEntries(storable); + } + + @Override + public void afterInsert(S storable, Object state) throws PersistException { + updateValues(storable, state); + } + + @Override + public Object beforeUpdate(S storable) throws PersistException { + return createDependentIndexEntries(storable); + } + + @Override + public void afterUpdate(S storable, Object state) throws PersistException { + updateValues(storable, state); + } + + @Override + public Object beforeDelete(S storable) throws PersistException { + try { + if (storable.copy().tryLoad()) { + return createDependentIndexEntries(storable); + } + } catch (FetchException e) { + throw e.toPersistException(); + } + return null; + } + + @Override + public void afterDelete(S storable, Object state) throws PersistException { + updateValues(storable, state); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof DerivedIndexesTrigger) { + DerivedIndexesTrigger other = (DerivedIndexesTrigger) obj; + return mFetcher.equals(other.mFetcher); + } + return false; + } + + private List<Storable> createDependentIndexEntries(S storable) throws PersistException { + List<Storable> dependentIndexEntries = new ArrayList<Storable>(); + createDependentIndexEntries(storable, dependentIndexEntries); + return dependentIndexEntries; + } + + private void createDependentIndexEntries(S storable, List<Storable> dependentIndexEntries) + throws PersistException + { + try { + Transaction txn = mFetcher.enterTransaction(); + try { + // Make sure write lock is acquired when reading dependencies + // since they might be updated later. Locks are held after this + // transaction exits since it is nested in the trigger's transaction. + txn.setForUpdate(true); + + Cursor<D> dependencies = mFetcher.fetchDependenentStorables(storable); + try { + while (dependencies.hasNext()) { + mFetcher.createIndexEntries(dependencies.next(), dependentIndexEntries); + } + } finally { + dependencies.close(); + } + } finally { + txn.exit(); + } + } catch (FetchException e) { + throw e.toPersistException(); + } + } + + private void updateValues(S storable, Object state) throws PersistException { + if (state == null) { + return; + } + + List<Storable> oldIndexEntries = (List<Storable>) state; + int size = oldIndexEntries.size(); + + List<Storable> newIndexEntries = new ArrayList<Storable>(size); + createDependentIndexEntries(storable, newIndexEntries); + + if (size != newIndexEntries.size()) { + // This is not expected to happen. + throw new PersistException("Amount of affected dependent indexes changed: " + + size + " != " + newIndexEntries.size()); + } + + for (int i=0; i<size; i++) { + Storable oldIndexEntry = oldIndexEntries.get(i); + Storable newIndexEntry = newIndexEntries.get(i); + if (!oldIndexEntry.equalProperties(newIndexEntry)) { + oldIndexEntry.delete(); + newIndexEntry.tryInsert(); + } + } + } +} diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexAnalysis.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexAnalysis.java new file mode 100644 index 0000000..d0e3405 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexAnalysis.java @@ -0,0 +1,132 @@ +/* + * Copyright 2007 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.carbonado.repo.indexed; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.amazon.carbonado.Storable; + +import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.filter.RelOp; + +import com.amazon.carbonado.info.ChainedProperty; +import com.amazon.carbonado.info.Direction; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.info.StorableInfo; +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableProperty; + +import com.amazon.carbonado.qe.FilteringScore; + +import com.amazon.carbonado.spi.StorableIndexSet; + +/** + * Collection of static methods which perform index analysis. + * + * @author Brian S O'Neill + */ +class IndexAnalysis { + static <S extends Storable> StorableIndexSet<S> gatherDesiredIndexes(StorableInfo<S> info) { + StorableIndexSet<S> indexSet = new StorableIndexSet<S>(); + indexSet.addIndexes(info); + indexSet.addAlternateKeys(info); + + // If any join properties are used by indexed derived properties, make + // sure join internal properties are indexed. + + for (StorableProperty<S> property : info.getAllProperties().values()) { + if (!isJoinAndUsedByIndexedDerivedProperty(property)) { + continue; + } + + // Internal properties of join need to be indexed. Check if a + // suitable index exists before defining a new one. + + Filter<S> filter = Filter.getOpenFilter(info.getStorableType()); + for (int i=property.getJoinElementCount(); --i>=0; ) { + filter = filter.and(property.getInternalJoinElement(i).getName(), RelOp.EQ); + } + + for (int i=info.getIndexCount(); --i>=0; ) { + FilteringScore<S> score = FilteringScore.evaluate(info.getIndex(i), filter); + if (score.getIdentityCount() == property.getJoinElementCount()) { + // Suitable index already exists. + continue; + } + } + + Direction[] directions = new Direction[property.getJoinElementCount()]; + Arrays.fill(directions, Direction.UNSPECIFIED); + + StorableIndex<S> index = + new StorableIndex<S>(property.getInternalJoinElements(), directions); + + indexSet.add(index); + } + + return indexSet; + } + + static boolean isUsedByIndex(StorableProperty<?> property) { + StorableInfo<?> info = StorableIntrospector.examine(property.getEnclosingType()); + for (int i=info.getIndexCount(); --i>=0; ) { + StorableIndex<?> index = info.getIndex(i); + int propertyCount = index.getPropertyCount(); + for (int j=0; j<propertyCount; j++) { + if (index.getProperty(j).equals(property)) { + return true; + } + } + } + return false; + } + + static boolean isJoinAndUsedByIndexedDerivedProperty(StorableProperty<?> property) { + if (property.isJoin()) { + for (ChainedProperty<?> derivedTo : property.getDerivedToProperties()) { + if (isUsedByIndex(derivedTo.getPrimeProperty())) { + return true; + } + } + } + return false; + } + + /** + * Returns derived-to properties in external storables that are used by indexes. + * + * @return null if none + */ + static Set<ChainedProperty<?>> gatherDerivedToDependencies(StorableInfo<?> info) { + Set<ChainedProperty<?>> set = null; + for (StorableProperty<?> property : info.getAllProperties().values()) { + for (ChainedProperty<?> derivedTo : property.getDerivedToProperties()) { + if (derivedTo.getChainCount() > 0 && isUsedByIndex(derivedTo.getPrimeProperty())) { + if (set == null) { + set = new HashSet<ChainedProperty<?>>(); + } + set.add(derivedTo); + } + } + } + return set; + } +} diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java index 0c1e6b8..5f27aa8 100644 --- a/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java @@ -59,7 +59,7 @@ public interface IndexEntryAccessor<S extends Storable> extends IndexInfo { /** * Returns true if the properties of the given index entry match those * contained in the master, exluding any version property. This will always - * return true after a call to setAllProperties. + * return true after a call to copyFromMaster. * * @param indexEntry index entry whose properties will be tested * @param master source of property values diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java index 097185a..6bea049 100644 --- a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java @@ -86,7 +86,7 @@ class IndexedRepository implements Repository, if (Unindexed.class.isAssignableFrom(type)) { // Verify no indexes. - int indexCount = IndexedStorage + int indexCount = IndexAnalysis .gatherDesiredIndexes(StorableIntrospector.examine(type)).size(); if (indexCount > 0) { throw new MalformedTypeException @@ -152,7 +152,12 @@ class IndexedRepository implements Repository, getIndexEntryAccessors(Class<S> storableType) throws RepositoryException { - return ((IndexedStorage<S>) storageFor(storableType)).getIndexEntryAccessors(); + Storage<S> storage = storageFor(storableType); + if (storage instanceof IndexedStorage) { + return ((IndexedStorage<S>) storage).getIndexEntryAccessors(); + } else { + return new IndexEntryAccessor[0]; + } } public String[] getUserStorableTypeNames() throws RepositoryException { diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java index fe0cfe8..28a9d35 100644 --- a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -32,9 +33,11 @@ import com.amazon.carbonado.FetchException; import com.amazon.carbonado.IsolationLevel; import com.amazon.carbonado.PersistException; 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.SupportException; import com.amazon.carbonado.Transaction; import com.amazon.carbonado.Trigger; import com.amazon.carbonado.capability.IndexInfo; @@ -45,6 +48,7 @@ import com.amazon.carbonado.cursor.MergeSortBuffer; import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.Direction; import com.amazon.carbonado.info.StorableInfo; import com.amazon.carbonado.info.StorableIntrospector; @@ -53,6 +57,7 @@ import com.amazon.carbonado.info.StorableIndex; import com.amazon.carbonado.cursor.SortBuffer; import com.amazon.carbonado.qe.BoundaryType; +import com.amazon.carbonado.qe.FilteringScore; import com.amazon.carbonado.qe.QueryEngine; import com.amazon.carbonado.qe.QueryExecutorFactory; import com.amazon.carbonado.qe.StorageAccess; @@ -69,13 +74,6 @@ import static com.amazon.carbonado.repo.indexed.ManagedIndex.*; * @author Brian S O'Neill */ class IndexedStorage<S extends Storable> implements Storage<S>, StorageAccess<S> { - static <S extends Storable> StorableIndexSet<S> gatherDesiredIndexes(StorableInfo<S> info) { - StorableIndexSet<S> indexSet = new StorableIndexSet<S>(); - indexSet.addIndexes(info); - indexSet.addAlternateKeys(info); - return indexSet; - } - final IndexedRepository mRepository; final Storage<S> mMasterStorage; @@ -102,7 +100,7 @@ class IndexedStorage<S extends Storable> implements Storage<S>, StorageAccess<S> // The set of indexes that the Storable defines, reduced. final StorableIndexSet<S> desiredIndexSet; { - desiredIndexSet = gatherDesiredIndexes(info); + desiredIndexSet = IndexAnalysis.gatherDesiredIndexes(info); desiredIndexSet.reduce(Direction.ASCENDING); } @@ -299,6 +297,17 @@ class IndexedStorage<S extends Storable> implements Storage<S>, StorageAccess<S> mQueryableIndexSet = queryableIndexSet; mQueryEngine = new QueryEngine<S>(masterStorage.getStorableType(), repository); + + // Install triggers to manage derived properties in external Storables. + + Set<ChainedProperty<?>> derivedToDependencies = + IndexAnalysis.gatherDerivedToDependencies(info); + + if (derivedToDependencies != null) { + for (ChainedProperty<?> derivedTo : derivedToDependencies) { + addTrigger(new DerivedIndexesTrigger(repository, getStorableType(), derivedTo)); + } + } } public Class<S> getStorableType() { diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java b/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java index 2e4cea3..b1e76cc 100644 --- a/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java +++ b/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java @@ -18,6 +18,8 @@ package com.amazon.carbonado.repo.indexed; +import java.lang.reflect.UndeclaredThrowableException; + import java.util.Comparator; import org.apache.commons.logging.Log; @@ -451,10 +453,23 @@ class ManagedIndex<S extends Storable> implements IndexEntryAccessor<S> { } } - private Storable makeIndexEntry(S userStorable) { - Storable indexEntry = mIndexEntryStorage.prepare(); - mGenerator.copyFromMaster(indexEntry, userStorable); - return indexEntry; + private Storable makeIndexEntry(S userStorable) throws PersistException { + try { + Storable indexEntry = mIndexEntryStorage.prepare(); + mGenerator.copyFromMaster(indexEntry, userStorable); + return indexEntry; + } catch (UndeclaredThrowableException e) { + Throwable cause = e.getCause(); + if (cause instanceof PersistException) { + throw (PersistException) cause; + } + throw new PersistException(cause); + } catch (Exception e) { + if (e instanceof PersistException) { + throw (PersistException) e; + } + throw new PersistException(e); + } } /** Assumes caller is in a transaction */ diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java index 2528cbf..57f7bbc 100644 --- a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java @@ -223,7 +223,7 @@ class JDBCStorableGenerator<S extends Storable> { // UnsupportedOperationException. { for (JDBCStorableProperty<S> property : mAllProperties.values()) { - if (property.isJoin() || property.isSupported()) { + if (property.isDerived() || property.isJoin() || property.isSupported()) { continue; } String message = "Independent property \"" + property.getName() + @@ -444,7 +444,7 @@ class JDBCStorableGenerator<S extends Storable> { sb.append(" ( "); int ordinal = 0; - for (JDBCStorableProperty<?> property : mInfo.getAllProperties().values()) { + for (JDBCStorableProperty<?> property : mAllProperties.values()) { if (!property.isSelectable()) { continue; } @@ -470,10 +470,12 @@ class JDBCStorableGenerator<S extends Storable> { } boolean useStaticInsertStatement = true; - for (JDBCStorableProperty<?> property : mInfo.getAllProperties().values()) { - if (property.isVersion() || property.isAutomatic()) { - useStaticInsertStatement = false; - break; + for (JDBCStorableProperty<?> property : mAllProperties.values()) { + if (!property.isDerived()) { + if (property.isVersion() || property.isAutomatic()) { + useStaticInsertStatement = false; + break; + } } } @@ -489,7 +491,7 @@ class JDBCStorableGenerator<S extends Storable> { insertCountVar = b.createLocalVariable(null, TypeDesc.INT); int initialCount = 0; - for (JDBCStorableProperty<?> property : mInfo.getAllProperties().values()) { + for (JDBCStorableProperty<?> property : mAllProperties.values()) { if (!property.isSelectable()) { continue; } @@ -517,7 +519,7 @@ class JDBCStorableGenerator<S extends Storable> { CodeBuilderUtil.callStringBuilderAppendString(b); int propNumber = -1; - for (JDBCStorableProperty<?> property : mInfo.getAllProperties().values()) { + for (JDBCStorableProperty<?> property : mAllProperties.values()) { propNumber++; if (!property.isSelectable()) { continue; diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java index aa17f11..5ff713d 100644 --- a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java @@ -55,6 +55,7 @@ import com.amazon.carbonado.RepositoryException; import com.amazon.carbonado.Storable; import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.info.ChainedProperty; import com.amazon.carbonado.info.OrderedProperty; import com.amazon.carbonado.info.StorableInfo; import com.amazon.carbonado.info.StorableIntrospector; @@ -277,7 +278,7 @@ public class JDBCStorableIntrospector extends StorableIntrospector { ArrayList<String> errorMessages = new ArrayList<String>(); for (StorableProperty<S> mainProperty : mainProperties.values()) { - if (mainProperty.isJoin() || tableName == null) { + if (mainProperty.isDerived() || mainProperty.isJoin() || tableName == null) { jProperties.put(mainProperty.getName(), new JProperty<S>(mainProperty)); continue; } @@ -1271,6 +1272,10 @@ public class JDBCStorableIntrospector extends StorableIntrospector { return mMainProperty.isJoin(); } + public boolean isOneToOneJoin() { + return mMainProperty.isOneToOneJoin(); + } + public Class<? extends Storable> getJoinedType() { return mMainProperty.getJoinedType(); } @@ -1315,6 +1320,18 @@ public class JDBCStorableIntrospector extends StorableIntrospector { return mMainProperty.isIndependent(); } + public boolean isDerived() { + return mMainProperty.isDerived(); + } + + public ChainedProperty<S>[] getDerivedFromProperties() { + return mMainProperty.getDerivedFromProperties(); + } + + public ChainedProperty<?>[] getDerivedToProperties() { + return mMainProperty.getDerivedToProperties(); + } + public boolean isSupported() { if (isJoin()) { // TODO: Check if joined type is supported @@ -1325,7 +1342,7 @@ public class JDBCStorableIntrospector extends StorableIntrospector { } public boolean isSelectable() { - return mColumnName != null && !isJoin(); + return mColumnName != null && !isJoin() && !isDerived(); } public boolean isAutoIncrement() { diff --git a/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java b/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java index ec3ae34..ba3c013 100644 --- a/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java +++ b/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java @@ -199,6 +199,23 @@ public abstract class WrappedQuery<S extends Storable> implements Query<S> { return mQuery.toString(); } + @Override + public int hashCode() { + return mQuery.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof WrappedQuery) { + WrappedQuery<?> other = (WrappedQuery<?>) obj; + return mQuery.equals(other.mQuery); + } + return false; + } + protected Query<S> getWrappedQuery() { return mQuery; } diff --git a/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java b/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java index 2c9c0dc..0832aa9 100644 --- a/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java +++ b/src/main/java/com/amazon/carbonado/synthetic/SyntheticStorableReferenceBuilder.java @@ -493,23 +493,22 @@ public class SyntheticStorableReferenceBuilder<S extends Storable> continue; } - if (prop.getReadMethod() == null) { - throw new SupportException - ("Property does not have a public accessor method: " + prop); - } - if (prop.getWriteMethod() == null) { - throw new SupportException - ("Property does not have a public mutator method: " + prop); - } - TypeDesc propType = TypeDesc.forClass(prop.getType()); if (toMasterPk) { + if (prop.getWriteMethod() == null) { + throw new SupportException + ("Property does not have a public mutator method: " + prop); + } b.loadLocal(b.getParameter(0)); b.loadThis(); b.invokeVirtual(prop.getReadMethodName(), propType, null); b.invoke(prop.getWriteMethod()); } else if (methodName.equals(mCopyFromMasterMethodName)) { + if (prop.getReadMethod() == null) { + throw new SupportException + ("Property does not have a public accessor method: " + prop); + } b.loadThis(); b.loadLocal(b.getParameter(0)); b.invoke(prop.getReadMethod()); -- cgit v1.2.3