summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian S. O'Neill <bronee@gmail.com>2006-08-30 01:52:10 +0000
committerBrian S. O'Neill <bronee@gmail.com>2006-08-30 01:52:10 +0000
commitedcd84a1cb3c2009b0596939f30cee35028afcfc (patch)
treebac64b7d8e96af10fd041a5b8d0759f74f59e14e
parent8365fcc3a5b2285fc1fe442d6f2eb8a90cbbab3a (diff)
Add introspection support
-rw-r--r--src/main/java/com/amazon/carbonado/info/AutomaticAdapterSelector.java73
-rw-r--r--src/main/java/com/amazon/carbonado/info/ChainedProperty.java310
-rw-r--r--src/main/java/com/amazon/carbonado/info/Direction.java67
-rw-r--r--src/main/java/com/amazon/carbonado/info/OrderedProperty.java182
-rw-r--r--src/main/java/com/amazon/carbonado/info/StorableIndex.java641
-rw-r--r--src/main/java/com/amazon/carbonado/info/StorableInfo.java129
-rw-r--r--src/main/java/com/amazon/carbonado/info/StorableIntrospector.java1949
-rw-r--r--src/main/java/com/amazon/carbonado/info/StorableKey.java43
-rw-r--r--src/main/java/com/amazon/carbonado/info/StorableProperty.java206
-rw-r--r--src/main/java/com/amazon/carbonado/info/StorablePropertyAdapter.java382
-rw-r--r--src/main/java/com/amazon/carbonado/info/StorablePropertyAnnotation.java86
-rw-r--r--src/main/java/com/amazon/carbonado/info/StorablePropertyConstraint.java66
-rw-r--r--src/main/java/com/amazon/carbonado/info/package-info.java24
13 files changed, 4158 insertions, 0 deletions
diff --git a/src/main/java/com/amazon/carbonado/info/AutomaticAdapterSelector.java b/src/main/java/com/amazon/carbonado/info/AutomaticAdapterSelector.java
new file mode 100644
index 0000000..2ab66c2
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/AutomaticAdapterSelector.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+import org.joda.time.DateMidnight;
+import org.joda.time.DateTime;
+
+import org.cojen.util.BeanProperty;
+
+import com.amazon.carbonado.adapter.DateTimeAdapter;
+import com.amazon.carbonado.adapter.TextAdapter;
+
+/**
+ * Some property types are not expected to be supported by most repositories,
+ * except a standard adapter can be applied automatically. Since no annotation
+ * is present in the storable definition, selected StorablePropertyAdapter
+ * instances return null from getAnnotation. Automatically selected adapters
+ * must cope without having an annotation instance, applying defaults.
+ *
+ * @author Brian S O'Neill
+ */
+class AutomaticAdapterSelector {
+ /**
+ * @param property bean property which must have a read method
+ * @return adapter with a null annotation, or null if nothing applicable
+ */
+ static StorablePropertyAdapter selectAdapterFor(final BeanProperty property) {
+ final Method readMethod = property.getReadMethod();
+ if (readMethod == null) {
+ throw new IllegalArgumentException();
+ }
+ final Class propertyType = property.getType();
+
+ if (DateTime.class.isAssignableFrom(propertyType) ||
+ DateMidnight.class.isAssignableFrom(propertyType))
+ {
+ return selectAdapter(property, DateTimeAdapter.class, readMethod);
+ } else if (String.class.isAssignableFrom(propertyType)) {
+ return selectAdapter(property, TextAdapter.class, readMethod);
+ } // else if ...
+
+ return null;
+ }
+
+ private static StorablePropertyAdapter selectAdapter
+ (BeanProperty property,
+ Class<? extends Annotation> annotationType,
+ Method annotatedMethod)
+ {
+ StorablePropertyAnnotation annotation =
+ new StorablePropertyAnnotation(annotationType, annotatedMethod);
+ return new StorablePropertyAdapter(property, annotation);
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/info/ChainedProperty.java b/src/main/java/com/amazon/carbonado/info/ChainedProperty.java
new file mode 100644
index 0000000..e0b1121
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/ChainedProperty.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.cojen.util.WeakCanonicalSet;
+
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.util.Appender;
+
+/**
+ * Represents a property to query against or to order by. Properties may be
+ * specified in a simple form, like "firstName", or in a chained form, like
+ * "address.state". In both forms, the first property is the "prime"
+ * property. All properties that follow are chained.
+ *
+ * @author Brian S O'Neill
+ */
+public class ChainedProperty<S extends Storable> implements Appender {
+ static WeakCanonicalSet cCanonical = new WeakCanonicalSet();
+
+ /**
+ * Returns a canonical instance which has no chain.
+ *
+ * @throws IllegalArgumentException if prime is null
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> ChainedProperty<S> get(StorableProperty<S> prime) {
+ return (ChainedProperty<S>) cCanonical.put(new ChainedProperty<S>(prime, null));
+ }
+
+ /**
+ * Returns a canonical instance.
+ *
+ * @throws IllegalArgumentException if prime is null or if chained
+ * properties are not formed properly
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> ChainedProperty<S> get(StorableProperty<S> prime,
+ StorableProperty<?>... chain) {
+ return (ChainedProperty<S>) cCanonical.put(new ChainedProperty<S>(prime, chain));
+ }
+
+ /**
+ * Parses a chained property.
+ *
+ * @param info Info for Storable type containing property
+ * @param str string to parse
+ * @throws IllegalArgumentException if any parameter is null or string
+ * format is incorrect
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> ChainedProperty<S> parse(StorableInfo<S> info, String str)
+ throws IllegalArgumentException
+ {
+ if (info == null || str == null) {
+ throw new IllegalArgumentException();
+ }
+
+ int pos = 0;
+ int dot = str.indexOf('.', pos);
+
+ String name;
+ if (dot < 0) {
+ name = str;
+ } else {
+ name = str.substring(pos, dot);
+ pos = dot + 1;
+ }
+
+ StorableProperty<S> prime = info.getAllProperties().get(name);
+
+ if (prime == null) {
+ throw new IllegalArgumentException
+ ("Property \"" + name + "\" not found for type: \"" +
+ info.getStorableType().getName() + '"');
+ }
+
+ if (pos <= 0) {
+ return get(prime);
+ }
+
+ List<StorableProperty<?>> chain = new ArrayList<StorableProperty<?>>(4);
+ Class<?> type = prime.isJoin() ? prime.getJoinedType() : prime.getType();
+
+ while (pos > 0) {
+ dot = str.indexOf('.', pos);
+ if (dot < 0) {
+ name = str.substring(pos);
+ pos = -1;
+ } else {
+ name = str.substring(pos, dot);
+ pos = dot + 1;
+ }
+ if (Storable.class.isAssignableFrom(type)) {
+ StorableInfo propInfo =
+ StorableIntrospector.examine((Class<? extends Storable>) type);
+ Map<String, ? extends StorableProperty<?>> props = propInfo.getAllProperties();
+ StorableProperty<?> prop = props.get(name);
+ if (prop == null) {
+ throw new IllegalArgumentException
+ ("Property \"" + name + "\" not found for type: \"" +
+ type.getName() + '"');
+ }
+ chain.add(prop);
+ type = prop.isJoin() ? prop.getJoinedType() : prop.getType();
+ } else {
+ throw new IllegalArgumentException
+ ("Property \"" + name + "\" not found for type \"" +
+ type.getName() + "\" because type has no properties");
+ }
+ }
+
+ return get(prime,
+ (StorableProperty<?>[]) chain.toArray(new StorableProperty[chain.size()]));
+ }
+
+ private final StorableProperty<S> mPrime;
+ private final StorableProperty<?>[] mChain;
+
+ /**
+ * @param prime must not be null
+ * @param chain can be null if none
+ * @throws IllegalArgumentException if prime is null
+ */
+ private ChainedProperty(StorableProperty<S> prime, StorableProperty<?>[] chain) {
+ if (prime == null) {
+ throw new IllegalArgumentException();
+ }
+ mPrime = prime;
+ mChain = (chain == null || chain.length == 0) ? null : chain.clone();
+ }
+
+ public StorableProperty<S> getPrimeProperty() {
+ return mPrime;
+ }
+
+ /**
+ * Returns the type of the last property in the chain, or of the prime
+ * property if the chain is empty.
+ */
+ public Class<?> getType() {
+ return getLastProperty().getType();
+ }
+
+ /**
+ * Returns the last property in the chain, or the prime property if chain
+ * is empty.
+ */
+ public StorableProperty<?> getLastProperty() {
+ return mChain == null ? mPrime : mChain[mChain.length - 1];
+ }
+
+ /**
+ * Returns amount of properties chained from prime property, which may be
+ * zero.
+ */
+ public int getChainCount() {
+ return mChain == null ? 0 : mChain.length;
+ }
+
+ public StorableProperty<?> getChainedProperty(int index) throws IndexOutOfBoundsException {
+ if (mChain == null) {
+ throw new IndexOutOfBoundsException();
+ } else {
+ return mChain[index];
+ }
+ }
+
+ /**
+ * Returns a new ChainedProperty with another property appended.
+ */
+ public ChainedProperty<S> append(StorableProperty<?> property) {
+ StorableProperty<?>[] newChain = new StorableProperty[getChainCount() + 1];
+ if (newChain.length > 1) {
+ System.arraycopy(mChain, 0, newChain, 0, mChain.length);
+ }
+ newChain[newChain.length - 1] = property;
+ return get(mPrime, newChain);
+ }
+
+ /**
+ * Returns a new ChainedProperty with another property appended.
+ */
+ public ChainedProperty<S> append(ChainedProperty<?> property) {
+ final int propChainCount = property.getChainCount();
+ if (propChainCount == 0) {
+ return append(property.getPrimeProperty());
+ }
+
+ StorableProperty<?>[] newChain =
+ new StorableProperty[getChainCount() + 1 + propChainCount];
+
+ int pos = 0;
+ if (newChain.length > 1) {
+ System.arraycopy(mChain, 0, newChain, 0, mChain.length);
+ pos = mChain.length;
+ }
+
+ newChain[pos++] = property.getPrimeProperty();
+ for (int i=0; i<propChainCount; i++) {
+ newChain[pos++] = property.getChainedProperty(i);
+ }
+
+ return get(mPrime, newChain);
+ }
+
+ /**
+ * Returns a new ChainedProperty with the last property in the chain removed.
+ *
+ * @throws IllegalStateException if chain count is zero
+ */
+ public ChainedProperty<S> trim() {
+ if (getChainCount() == 0) {
+ throw new IllegalStateException();
+ }
+ if (getChainCount() == 1) {
+ return get(mPrime);
+ }
+ StorableProperty<?>[] newChain = new StorableProperty[getChainCount() - 1];
+ System.arraycopy(mChain, 0, newChain, 0, newChain.length);
+ return get(mPrime, newChain);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = mPrime.hashCode();
+ StorableProperty<?>[] chain = mChain;
+ if (chain != null) {
+ for (int i=chain.length; --i>=0; ) {
+ hash = hash * 31 + chain[i].hashCode();
+ }
+ }
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof ChainedProperty) {
+ ChainedProperty<?> other = (ChainedProperty<?>) obj;
+ if (getType() == other.getType() && mPrime.equals(other.mPrime)) {
+ if (mChain == null) {
+ return other.mChain == null;
+ }
+ if (other.mChain != null) {
+ return Arrays.equals(mChain, other.mChain);
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the chained property in a parseable form.
+ */
+ @Override
+ public String toString() {
+ if (mChain == null) {
+ return mPrime.getName();
+ }
+ StringBuilder buf = new StringBuilder();
+ try {
+ appendTo(buf);
+ } catch (IOException e) {
+ // Not gonna happen.
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Appends the chained property in a parseable form.
+ */
+ public void appendTo(Appendable app) throws IOException {
+ app.append(mPrime.getName());
+ StorableProperty<?>[] chain = mChain;
+ if (chain != null) {
+ app.append('.');
+ for (int i=0; i<chain.length; i++) {
+ if (i > 0) {
+ app.append('.');
+ }
+ app.append(chain[i].getName());
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/info/Direction.java b/src/main/java/com/amazon/carbonado/info/Direction.java
new file mode 100644
index 0000000..67d003a
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/Direction.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+/**
+ * Describes a property sorting direction.
+ *
+ * @author Brian S O'Neill
+ */
+public enum Direction {
+ ASCENDING('+'), DESCENDING('-'), UNSPECIFIED('~');
+
+ private char mCharValue;
+
+ private Direction(char charValue) {
+ mCharValue = charValue;
+ }
+
+ /**
+ * Returns the reverse direction of this.
+ */
+ public Direction reverse() {
+ if (this == ASCENDING) {
+ return DESCENDING;
+ } else if (this == DESCENDING) {
+ return ASCENDING;
+ }
+ return this;
+ }
+
+ /**
+ * Returns '+' for ASCENDING, '-' for DESCENDING, and '~' for UNSPECIFIED.
+ */
+ public char toCharacter() {
+ return mCharValue;
+ }
+
+ /**
+ * Returns ASCENDING for '+', DESCENDING for '-', UNSPECIFIED for anything
+ * else.
+ */
+ public static Direction fromCharacter(char c) {
+ if (c == '+') {
+ return ASCENDING;
+ }
+ if (c == '-') {
+ return DESCENDING;
+ }
+ return UNSPECIFIED;
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/info/OrderedProperty.java b/src/main/java/com/amazon/carbonado/info/OrderedProperty.java
new file mode 100644
index 0000000..7de07c0
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/OrderedProperty.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.io.IOException;
+
+import org.cojen.util.WeakCanonicalSet;
+
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.util.Appender;
+
+/**
+ * Represents a property paired with a preferred ordering direction.
+ *
+ * @author Brian S O'Neill
+ */
+public class OrderedProperty<S extends Storable> implements Appender {
+ static WeakCanonicalSet cCanonical = new WeakCanonicalSet();
+
+ /**
+ * Returns a canonical instance.
+ *
+ * @throws IllegalArgumentException if property is null
+ */
+ public static <S extends Storable> OrderedProperty<S> get(StorableProperty<S> property,
+ Direction direction) {
+ return get(ChainedProperty.get(property), direction);
+ }
+
+ /**
+ * Returns a canonical instance.
+ *
+ * @throws IllegalArgumentException if property is null
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> OrderedProperty<S> get(ChainedProperty<S> property,
+ Direction direction) {
+ return (OrderedProperty<S>) cCanonical.put(new OrderedProperty<S>(property, direction));
+ }
+
+ /**
+ * Parses an ordering property, which may start with a '+' or '-' to
+ * indicate direction. If ordering prefix not specified, default direction
+ * is ascending.
+ *
+ * @param info Info for Storable type containing property
+ * @param str string to parse
+ * @throws IllegalArgumentException if any required parameter is null or
+ * string format is incorrect
+ */
+ public static <S extends Storable> OrderedProperty<S> parse(StorableInfo<S> info,
+ String str)
+ throws IllegalArgumentException
+ {
+ return parse(info, str, Direction.ASCENDING);
+ }
+
+ /**
+ * Parses an ordering property, which may start with a '+' or '-' to
+ * indicate direction.
+ *
+ * @param info Info for Storable type containing property
+ * @param str string to parse
+ * @param defaultDirection default direction if not specified in
+ * string. If null, ascending order is defaulted.
+ * @throws IllegalArgumentException if any required parameter is null or
+ * string format is incorrect
+ */
+ public static <S extends Storable> OrderedProperty<S> parse(StorableInfo<S> info,
+ String str,
+ Direction defaultDirection)
+ throws IllegalArgumentException
+ {
+ if (info == null || str == null || defaultDirection == null) {
+ throw new IllegalArgumentException();
+ }
+ Direction direction = defaultDirection;
+ if (str.length() > 0) {
+ if (str.charAt(0) == '+') {
+ direction = Direction.ASCENDING;
+ str = str.substring(1);
+ } else if (str.charAt(0) == '-') {
+ direction = Direction.DESCENDING;
+ str = str.substring(1);
+ }
+ }
+ if (direction == null) {
+ direction = Direction.ASCENDING;
+ }
+ return get(ChainedProperty.parse(info, str), direction);
+ }
+
+ private final ChainedProperty<S> mProperty;
+ private final Direction mDirection;
+
+ private OrderedProperty(ChainedProperty<S> property, Direction direction) {
+ if (property == null) {
+ throw new IllegalArgumentException();
+ }
+ mProperty = property;
+ mDirection = direction == null ? Direction.UNSPECIFIED : direction;
+ }
+
+ public ChainedProperty<S> getChainedProperty() {
+ return mProperty;
+ }
+
+ public Direction getDirection() {
+ return mDirection;
+ }
+
+ public OrderedProperty<S> reverse() {
+ if (mDirection == Direction.UNSPECIFIED) {
+ return this;
+ }
+ return get(mProperty, mDirection.reverse());
+ }
+
+ public OrderedProperty<S> direction(Direction direction) {
+ return get(mProperty, direction);
+ }
+
+ @Override
+ public int hashCode() {
+ return mProperty.hashCode() + mDirection.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof OrderedProperty) {
+ OrderedProperty<?> other = (OrderedProperty<?>) obj;
+ return mProperty.equals(other.mProperty) && mDirection.equals(other.mDirection);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the chained property in a parseable form.
+ */
+ @Override
+ public String toString() {
+ if (mDirection == Direction.UNSPECIFIED) {
+ return mProperty.toString();
+ }
+ StringBuilder buf = new StringBuilder();
+ buf.append(mDirection.toCharacter());
+ try {
+ mProperty.appendTo(buf);
+ } catch (IOException e) {
+ // Not gonna happen.
+ }
+ return buf.toString();
+ }
+
+ public void appendTo(Appendable app) throws IOException {
+ if (mDirection == Direction.UNSPECIFIED) {
+ mProperty.appendTo(app);
+ } else {
+ app.append(mDirection.toCharacter());
+ mProperty.appendTo(app);
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/info/StorableIndex.java b/src/main/java/com/amazon/carbonado/info/StorableIndex.java
new file mode 100644
index 0000000..c1a5c47
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/StorableIndex.java
@@ -0,0 +1,641 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.io.IOException;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.cojen.classfile.TypeDesc;
+
+import com.amazon.carbonado.capability.IndexInfo;
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.util.Appender;
+
+/**
+ * Represents an index that must be defined for a specific {@link Storable} type.
+ *
+ * @author Brian S O'Neill
+ * @see com.amazon.carbonado.Index
+ */
+public class StorableIndex<S extends Storable> implements Appender {
+ /**
+ * Parses an index descriptor and returns an index object.
+ *
+ * @param desc name descriptor, as created by {@link #getNameDescriptor}
+ * @param info info on storable type
+ * @return index represented by descriptor
+ * @throws IllegalArgumentException if error in descriptor syntax or if it
+ * refers to unknown properties
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> StorableIndex<S> parseNameDescriptor
+ (String desc, StorableInfo<S> info)
+ throws IllegalArgumentException
+ {
+ String name = info.getStorableType().getName();
+ if (!desc.startsWith(name)) {
+ throw new IllegalArgumentException("Descriptor starts with wrong type name: \"" +
+ desc + "\", \"" + name + '"');
+ }
+
+ Map<String, ? extends StorableProperty<S>> allProperties = info.getAllProperties();
+
+ List<StorableProperty<S>> properties = new ArrayList<StorableProperty<S>>();
+ List<Direction> directions = new ArrayList<Direction>();
+ boolean unique;
+
+ try {
+ int pos = name.length();
+ if (desc.charAt(pos++) != '~') {
+ throw new IllegalArgumentException("Invalid syntax");
+ }
+
+ {
+ int pos2 = nextSep(desc, pos);
+
+ String attr = desc.substring(pos, pos2);
+ if (attr.equals("U")) {
+ unique = true;
+ } else if (attr.equals("N")) {
+ unique = false;
+ } else {
+ throw new IllegalArgumentException("Unknown attribute");
+ }
+
+ pos = pos2;
+ }
+
+ while (pos < desc.length()) {
+ char sign = desc.charAt(pos++);
+ if (sign == '+') {
+ directions.add(Direction.ASCENDING);
+ } else if (sign == '-') {
+ directions.add(Direction.DESCENDING);
+ } else if (sign == '~') {
+ directions.add(Direction.UNSPECIFIED);
+ } else {
+ throw new IllegalArgumentException("Unknown property direction");
+ }
+
+ int pos2 = nextSep(desc, pos);
+
+
+ String propertyName = desc.substring(pos, pos2);
+ StorableProperty<S> property = allProperties.get(propertyName);
+ if (property == null) {
+ throw new IllegalArgumentException("Unknown property: " + propertyName);
+ }
+ properties.add(property);
+ pos = pos2;
+ }
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException("Invalid syntax");
+ }
+
+ int size = properties.size();
+ if (size == 0 || size != directions.size()) {
+ new IllegalArgumentException("No properties specified");
+ }
+
+ StorableIndex<S> index = new StorableIndex<S>
+ (properties.toArray(new StorableProperty[size]),
+ directions.toArray(new Direction[size]));
+
+ return index.unique(unique);
+ }
+
+ /**
+ * Find the first subsequent occurrance of '+', '-', or '~' in the string
+ * or the end of line if none are there
+ * @param desc string to search
+ * @param pos starting position in string
+ * @return position of next separator, or end of string if none present
+ */
+ private static int nextSep(String desc, int pos) {
+ int pos2 = desc.length(); // assume we'll find none
+ int candidate = desc.indexOf('+', pos);
+ if (candidate > 0) {
+ pos2=candidate;
+ }
+
+ candidate = desc.indexOf('-', pos);
+ if (candidate>0) {
+ pos2 = Math.min(candidate, pos2);
+ }
+
+ candidate = desc.indexOf('~', pos);
+ if (candidate>0) {
+ pos2 = Math.min(candidate, pos2);
+ }
+ return pos2;
+ }
+
+ private final StorableProperty<S>[] mProperties;
+ private final Direction[] mDirections;
+ private final boolean mUnique;
+ private final boolean mClustered;
+
+ /**
+ * Creates a StorableIndex from the given properties and matching
+ * directions. Both arrays must match length.
+ *
+ * @throws IllegalArgumentException if any argument is null, if lengths
+ * do not match, or if any length is zero.
+ */
+ public StorableIndex(StorableProperty<S>[] properties, Direction[] directions) {
+ this(properties, directions, false);
+ }
+
+ /**
+ * Creates a StorableIndex from the given properties and matching
+ * directions. Both arrays must match length. Allows specification of the
+ * uniqueness of the index.
+ *
+ * @param properties
+ * @param directions
+ * @param unique
+ */
+ public StorableIndex(StorableProperty<S>[] properties,
+ Direction[] directions,
+ boolean unique)
+ {
+ this(properties, directions, unique, false, true);
+ }
+
+ /**
+ * Creates a StorableIndex from the given properties and matching
+ * directions. Both arrays must match length. Allows specification of the
+ * uniqueness of the index as well as clustered option.
+ *
+ * @param properties
+ * @param directions
+ * @param unique
+ * @param clustered
+ */
+ public StorableIndex(StorableProperty<S>[] properties,
+ Direction[] directions,
+ boolean unique,
+ boolean clustered)
+ {
+ this(properties, directions, unique, clustered, true);
+ }
+
+ /**
+ * The guts of it. All the calls within this class specify doClone=false.
+ * @param properties
+ * @param directions
+ * @param unique
+ * @param clustered
+ * @param doClone
+ */
+ private StorableIndex(StorableProperty<S>[] properties,
+ Direction[] directions,
+ boolean unique,
+ boolean clustered,
+ boolean doClone) {
+ if (properties == null || directions == null) {
+ throw new IllegalArgumentException();
+ }
+ if (properties.length != directions.length) {
+ throw new IllegalArgumentException();
+ }
+ if (properties.length < 1) {
+ throw new IllegalArgumentException();
+ }
+ mProperties = doClone ? properties.clone() : properties;
+ mDirections = doClone ? directions.clone() : directions;
+
+ mUnique = unique;
+ mClustered = clustered;
+ }
+
+ /**
+ * Creates a StorableIndex from a StorableKey.
+ *
+ * @param direction optional direction to apply to each key property that
+ * has an unspecified direction
+ * @throws IllegalArgumentException if key is null or it has
+ * no properties
+ */
+ @SuppressWarnings("unchecked")
+ public StorableIndex(StorableKey<S> key, Direction direction) {
+ if (key == null) {
+ throw new IllegalArgumentException();
+ }
+ Set<? extends OrderedProperty<S>> properties = key.getProperties();
+ if (properties.size() < 1) {
+ throw new IllegalArgumentException();
+ }
+
+ if (direction == null) {
+ direction = Direction.UNSPECIFIED;
+ }
+
+ mProperties = new StorableProperty[properties.size()];
+ mDirections = new Direction[properties.size()];
+
+ int i = 0;
+ for (OrderedProperty<S> prop : properties) {
+ mProperties[i] = prop.getChainedProperty().getPrimeProperty();
+ if (prop.getDirection() == Direction.UNSPECIFIED) {
+ mDirections[i] = direction;
+ } else {
+ mDirections[i] = prop.getDirection();
+ }
+ i++;
+ }
+
+ mUnique = true;
+ mClustered = false;
+ }
+
+ /**
+ * Creates a StorableIndex from OrderedProperties.
+ *
+ * @param direction optional direction to apply to each property that
+ * has an unspecified direction
+ * @throws IllegalArgumentException if no properties supplied
+ */
+ @SuppressWarnings("unchecked")
+ public StorableIndex(OrderedProperty<S>[] properties, Direction direction) {
+ if (properties == null || properties.length == 0) {
+ throw new IllegalArgumentException();
+ }
+
+ if (direction == null) {
+ direction = Direction.UNSPECIFIED;
+ }
+
+ mProperties = new StorableProperty[properties.length];
+ mDirections = new Direction[properties.length];
+
+ int i = 0;
+ for (OrderedProperty<S> prop : properties) {
+ mProperties[i] = prop.getChainedProperty().getPrimeProperty();
+ if (prop.getDirection() == Direction.UNSPECIFIED) {
+ mDirections[i] = direction;
+ } else {
+ mDirections[i] = prop.getDirection();
+ }
+ i++;
+ }
+
+ mUnique = false;
+ mClustered = false;
+ }
+
+ /**
+ * Creates a StorableIndex from an IndexInfo.
+ *
+ * @param type type of storable index is defined for
+ * @param indexInfo IndexInfo returned from storage object
+ * @throws IllegalArgumentException if any argument is null, if any
+ * properties are invalid, or if index info has no properties
+ */
+ @SuppressWarnings("unchecked")
+ public StorableIndex(Class<S> type, IndexInfo indexInfo) {
+ if (indexInfo == null) {
+ throw new IllegalArgumentException();
+ }
+
+ Map<String, ? extends StorableProperty<S>> allProperties =
+ StorableIntrospector.examine(type).getAllProperties();
+ String[] propertyNames = indexInfo.getPropertyNames();
+ if (propertyNames.length == 0) {
+ throw new IllegalArgumentException("No properties in index info");
+ }
+
+ mProperties = new StorableProperty[propertyNames.length];
+ for (int i=0; i<propertyNames.length; i++) {
+ StorableProperty<S> property = allProperties.get(propertyNames[i]);
+ if (property == null) {
+ throw new IllegalArgumentException("Property not found: " + propertyNames[i]);
+ }
+ mProperties[i] = property;
+ }
+
+ mDirections = indexInfo.getPropertyDirections();
+ mUnique = indexInfo.isUnique();
+ mClustered = indexInfo.isClustered();
+ }
+
+ /**
+ * Returns the type of storable this index applies to.
+ */
+ public Class<S> getStorableType() {
+ return getProperty(0).getEnclosingType();
+ }
+
+ /**
+ * Returns the count of properties in this index.
+ */
+ public int getPropertyCount() {
+ return mProperties.length;
+ }
+
+ /**
+ * Returns a specific property in this index.
+ */
+ public StorableProperty<S> getProperty(int index) {
+ return mProperties[index];
+ }
+
+ /**
+ * Returns a new array with all the properties in it.
+ */
+ public StorableProperty<S>[] getProperties() {
+ return mProperties.clone();
+ }
+
+ /**
+ * Returns the requested direction of a specific property in this index.
+ */
+ public Direction getPropertyDirection(int index) {
+ return mDirections[index];
+ }
+
+ /**
+ * Returns a new array with all the property directions in it.
+ */
+ public Direction[] getPropertyDirections() {
+ return mDirections.clone();
+ }
+
+ /**
+ * Returns a specific property in this index, with the direction folded in.
+ */
+ public OrderedProperty<S> getOrderedProperty(int index) {
+ return OrderedProperty.get(mProperties[index], mDirections[index]);
+ }
+
+ /**
+ * Returns a new array with all the properties in it, with directions
+ * folded in.
+ */
+ @SuppressWarnings("unchecked")
+ public OrderedProperty<S>[] getOrderedProperties() {
+ OrderedProperty<S>[] ordered = new OrderedProperty[mProperties.length];
+ for (int i=mProperties.length; --i>=0; ) {
+ ordered[i] = OrderedProperty.get(mProperties[i], mDirections[i]);
+ }
+ return ordered;
+ }
+
+ public boolean isUnique() {
+ return mUnique;
+ }
+
+ /**
+ * Returns true if index is known to be clustered, which means it defines
+ * the physical ordering of storables.
+ */
+ public boolean isClustered() {
+ return mClustered;
+ }
+
+ /**
+ * Returns a StorableIndex instance which is unique or not.
+ */
+ public StorableIndex<S> unique(boolean unique) {
+ if (unique == mUnique) {
+ return this;
+ }
+ return new StorableIndex<S>(mProperties, mDirections, unique, mClustered, false);
+ }
+
+ /**
+ * Returns a StorableIndex instance which is clustered or not.
+ */
+ public StorableIndex<S> clustered(boolean clustered) {
+ if (clustered == mClustered) {
+ return this;
+ }
+ return new StorableIndex<S>(mProperties, mDirections, mUnique, clustered, false);
+ }
+
+ /**
+ * Returns a StorableIndex instance with all the properties reversed.
+ */
+ public StorableIndex<S> reverse() {
+ Direction[] directions = mDirections;
+
+ specified: {
+ for (int i=directions.length; --i>=0; ) {
+ if (directions[i] != Direction.UNSPECIFIED) {
+ break specified;
+ }
+ }
+ // Completely unspecified direction, so nothing to reverse.
+ return this;
+ }
+
+ directions = directions.clone();
+ for (int i=directions.length; --i>=0; ) {
+ directions[i] = directions[i].reverse();
+ }
+
+ return new StorableIndex<S>(mProperties, directions, mUnique, mClustered, false);
+ }
+
+ /**
+ * Returns a StorableIndex instance with all unspecified directions set to
+ * the given direction. Returns this if all directions are already
+ * specified.
+ *
+ * @param direction direction to replace all unspecified directions
+ */
+ public StorableIndex<S> setDefaultDirection(Direction direction) {
+ Direction[] directions = mDirections;
+
+ unspecified: {
+ for (int i=directions.length; --i>=0; ) {
+ if (directions[i] == Direction.UNSPECIFIED) {
+ break unspecified;
+ }
+ }
+ // Completely specified direction, so nothing to alter.
+ return this;
+ }
+
+ directions = directions.clone();
+ for (int i=directions.length; --i>=0; ) {
+ if (directions[i] == Direction.UNSPECIFIED) {
+ directions[i] = direction;
+ }
+ }
+
+ return new StorableIndex<S>(mProperties, directions, mUnique, mClustered, false);
+ }
+
+ /**
+ * Returns a StorableIndex with the given property added. If this index
+ * already contained the given property (regardless of sort direction),
+ * this index is returned.
+ *
+ * @param property property to add unless already in this index
+ * @param direction direction to apply to property, if added
+ * @return new index with added property or this if index already contained property
+ */
+ public StorableIndex<S> addProperty(StorableProperty<S> property, Direction direction) {
+ for (int i=mProperties.length; --i>=0; ) {
+ if (mProperties[i].equals(property)) {
+ return this;
+ }
+ }
+
+ StorableProperty<S>[] properties = new StorableProperty[mProperties.length + 1];
+ Direction[] directions = new Direction[mDirections.length + 1];
+
+ System.arraycopy(mProperties, 0, properties, 0, mProperties.length);
+ System.arraycopy(mDirections, 0, directions, 0, mDirections.length);
+
+ properties[properties.length - 1] = property;
+ directions[directions.length - 1] = direction;
+
+ return new StorableIndex<S>(properties, directions, mUnique, mClustered, false);
+ }
+
+ /**
+ * Returns a StorableIndex which is unique, possibly by appending
+ * properties from the given key. If index is already unique, it is
+ * returned as-is.
+ */
+ public StorableIndex<S> uniquify(StorableKey<S> key) {
+ if (key == null) {
+ throw new IllegalArgumentException();
+ }
+
+ if (isUnique()) {
+ return this;
+ }
+
+ StorableIndex<S> index = this;
+
+ for (OrderedProperty<S> keyProp : key.getProperties()) {
+ index = index.addProperty
+ (keyProp.getChainedProperty().getPrimeProperty(), keyProp.getDirection());
+ }
+
+ return index.unique(true);
+ }
+
+ /**
+ * Converts this index into a parseable name descriptor string, whose
+ * general format is:
+ * <pre>&lt;storable type&gt;~&lt;attr&gt;&lt;+|-|~&gt;&lt;property&gt;&lt;+|-|~&gt;&lt;property&gt;...</pre>
+ * Attr is "U" for a unique index, "N" for a non-unique index.
+ *
+ * <p>Example: {@code my.pkg.UserInfo~N+lastName+firstName-birthDate}
+ *
+ * @see #parseNameDescriptor(String, StorableInfo)
+ */
+ public String getNameDescriptor() {
+ StringBuilder b = new StringBuilder();
+ b.append(getStorableType().getName());
+ b.append('~');
+ b.append(isUnique() ? 'U': 'N');
+
+ int count = getPropertyCount();
+ for (int i=0; i<count; i++) {
+ b.append(getPropertyDirection(i).toCharacter());
+ b.append(getProperty(i).getName());
+ }
+
+ return b.toString();
+ }
+
+ /**
+ * Converts this index into a parseable type descriptor string, which
+ * basically consists of Java type descriptors appended together. There is
+ * one slight difference. Types which may be null are prefixed with a 'N'
+ * character.
+ */
+ public String getTypeDescriptor() {
+ StringBuilder b = new StringBuilder();
+
+ int count = getPropertyCount();
+ for (int i=0; i<count; i++) {
+ StorableProperty property = getProperty(i);
+ if (property.isNullable()) {
+ b.append('N');
+ }
+ b.append(TypeDesc.forClass(property.getType()).getDescriptor());
+ }
+
+ return b.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return (mUnique ? 0 : 31)
+ + Arrays.hashCode(mProperties) * 31
+ + Arrays.hashCode(mDirections);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof StorableIndex) {
+ StorableIndex<?> other = (StorableIndex<?>) obj;
+ return isUnique() == other.isUnique()
+ && Arrays.equals(mProperties, other.mProperties)
+ && Arrays.equals(mDirections, other.mDirections);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder b = new StringBuilder();
+ b.append("StorableIndex ");
+ try {
+ appendTo(b);
+ } catch (IOException e) {
+ // Not gonna happen.
+ }
+ return b.toString();
+ }
+
+ /**
+ * Appends the same results as toString, but without the "StorableIndex"
+ * prefix.
+ */
+ public void appendTo(Appendable app) throws IOException {
+ app.append("{properties=[");
+ int length = mProperties.length;
+ for (int i=0; i<length; i++) {
+ if (i > 0) {
+ app.append(", ");
+ }
+ app.append(mDirections[i].toCharacter());
+ app.append(mProperties[i].getName());
+ }
+ app.append(']');
+ app.append(", unique=");
+ app.append(String.valueOf(isUnique()));
+ app.append('}');
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/info/StorableInfo.java b/src/main/java/com/amazon/carbonado/info/StorableInfo.java
new file mode 100644
index 0000000..1d6ed3e
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/StorableInfo.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.util.Map;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Contains all the metadata describing a specific {@link Storable} type.
+ *
+ * @author Brian S O'Neill
+ * @see StorableIntrospector
+ */
+public interface StorableInfo<S extends Storable> {
+ /**
+ * Returns the name of the Storable described by this StorableInfo,
+ * which is an abbreviated form of the type's class name.
+ */
+ String getName();
+
+ /**
+ * Returns the type of Storable described by this StorableInfo.
+ */
+ Class<S> getStorableType();
+
+ /**
+ * Returns all the storable properties in an unmodifiable map. Properties
+ * are always ordered, case-sensitive, by name. Primary key properties are
+ * grouped first.
+ *
+ * @return maps property names to property objects
+ */
+ Map<String, ? extends StorableProperty<S>> getAllProperties();
+
+ /**
+ * Returns a subset of the storable properties in an unmodifiable map
+ * that define the primary key. Properties are always ordered,
+ * case-sensitive, by name.
+ *
+ * @return maps property names to property objects
+ */
+ Map<String, ? extends StorableProperty<S>> getPrimaryKeyProperties();
+
+ /**
+ * Returns a subset of the storable properties in an unmodifiable map
+ * that define the basic data properties. Primary keys and joins are
+ * excluded. Properties are always ordered, case-sensitive, by name.
+ *
+ * @return maps property names to property objects
+ */
+ Map<String, ? extends StorableProperty<S>> getDataProperties();
+
+ /**
+ * Returns the designated version property, or null if none.
+ */
+ StorableProperty<S> getVersionProperty();
+
+ /**
+ * Returns the primary key for the Storable, never null.
+ */
+ StorableKey<S> getPrimaryKey();
+
+ /**
+ * Returns the count of alternate keys for the Storable.
+ */
+ int getAlternateKeyCount();
+
+ /**
+ * Returns a specific alternate key for the Storable.
+ */
+ StorableKey<S> getAlternateKey(int index);
+
+ /**
+ * Returns a new array with all the alternate keys in it.
+ */
+ StorableKey<S>[] getAlternateKeys();
+
+ /**
+ * Returns the count of aliases for the Storable.
+ */
+ int getAliasCount();
+
+ /**
+ * Returns a specific alias for the Storable.
+ */
+ String getAlias(int index) throws IndexOutOfBoundsException;
+
+ /**
+ * Returns a new array with all the alias names in it.
+ */
+ String[] getAliases();
+
+ /**
+ * Returns the count of indexes defined for the Storable.
+ */
+ int getIndexCount();
+
+ /**
+ * Returns a specific index for the Storable.
+ */
+ StorableIndex<S> getIndex(int index) throws IndexOutOfBoundsException;
+
+ /**
+ * Returns a new array with all the indexes in it.
+ */
+ StorableIndex<S>[] getIndexes();
+
+ /**
+ * @see com.amazon.carbonado.Independent
+ */
+ boolean isIndependent();
+}
diff --git a/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java b/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java
new file mode 100644
index 0000000..1a015f4
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/StorableIntrospector.java
@@ -0,0 +1,1949 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.cojen.classfile.TypeDesc;
+import org.cojen.util.BeanComparator;
+import org.cojen.util.BeanProperty;
+import org.cojen.util.BeanIntrospector;
+import org.cojen.util.WeakIdentityMap;
+
+import com.amazon.carbonado.Alias;
+import com.amazon.carbonado.AlternateKeys;
+import com.amazon.carbonado.FetchException;
+import com.amazon.carbonado.Index;
+import com.amazon.carbonado.Indexes;
+import com.amazon.carbonado.Join;
+import com.amazon.carbonado.Key;
+import com.amazon.carbonado.MalformedTypeException;
+import com.amazon.carbonado.Nullable;
+import com.amazon.carbonado.Independent;
+import com.amazon.carbonado.PrimaryKey;
+import com.amazon.carbonado.Query;
+import com.amazon.carbonado.Sequence;
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.Version;
+import com.amazon.carbonado.adapter.AdapterDefinition;
+import com.amazon.carbonado.constraint.ConstraintDefinition;
+import com.amazon.carbonado.lob.Lob;
+
+import com.amazon.carbonado.spi.CodeBuilderUtil;
+import com.amazon.carbonado.spi.ConversionComparator;
+
+/**
+ * Supports examination of {@link Storable} types, returning all metadata
+ * associated with it. As part of the examination, all annotations are gathered
+ * up. All examined data is cached, so repeat examinations are fast, unless the
+ * examination failed.
+ *
+ * @author Brian S O'Neill
+ */
+public class StorableIntrospector {
+ // TODO: Improve error messages to have consistent format and provide more
+ // context.
+
+ // Weakly maps Class objects to softly referenced StorableInfo objects.
+ @SuppressWarnings("unchecked")
+ private static Map<Class<?>, Reference<StorableInfo<?>>> cCache = new WeakIdentityMap();
+
+ /**
+ * Examines the given class and returns a StorableInfo describing it. A
+ * MalformedTypeException is thrown for a variety of reasons if the given
+ * class is not a well-defined Storable type.
+ *
+ * @param type Storable type to examine
+ * @throws MalformedTypeException if Storable type is not well-formed
+ * @throws IllegalArgumentException if type is null
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> StorableInfo<S> examine(Class<S> type) {
+ if (type == null) {
+ throw new IllegalArgumentException("Storable type must not be null");
+ }
+ synchronized (cCache) {
+ StorableInfo<S> info;
+ Reference<StorableInfo<?>> ref = cCache.get(type);
+ if (ref != null) {
+ info = (StorableInfo<S>) ref.get();
+ if (info != null) {
+ return info;
+ }
+ }
+
+ List<String> errorMessages = new ArrayList<String>();
+
+ // Pull these annotations out but finish processing later.
+ List<NameAndDirection> primaryKeyProps;
+ List<List<NameAndDirection>> alternateKeyProps;
+ List<List<NameAndDirection>> indexProps;
+
+ {
+ try {
+ primaryKeyProps = gatherListProperties
+ (errorMessages, null, null, type.getAnnotation(PrimaryKey.class)).get(0);
+ } catch (IndexOutOfBoundsException e) {
+ errorMessages.add("No primary key defined for type: " + type);
+ primaryKeyProps = Collections.emptyList();
+ }
+ alternateKeyProps = gatherListProperties
+ (errorMessages, null, type.getAnnotation(AlternateKeys.class), null);
+ indexProps = gatherListProperties
+ (errorMessages, type.getAnnotation(Indexes.class), null, null);
+ }
+
+ // Get all the properties.
+ Map<String, StorableProperty<S>> properties =
+ examineProperties(type, primaryKeyProps, alternateKeyProps);
+
+ // Resolve keys and indexes.
+
+ StorableKey<S> primaryKey;
+ {
+ Set<OrderedProperty<S>> propSet =
+ resolveKey(errorMessages, type, properties, "primary key", primaryKeyProps);
+ primaryKey = new SKey<S>(true, propSet);
+ }
+
+ StorableKey<S>[] alternateKeys;
+ {
+ alternateKeys = new StorableKey[alternateKeyProps.size()];
+ int i = 0;
+ for (List<NameAndDirection> nameAndDirs : alternateKeyProps) {
+ Set<OrderedProperty<S>> propSet =
+ resolveKey(errorMessages, type, properties, "alternate key", nameAndDirs);
+ alternateKeys[i++] = new SKey<S>(false, propSet);
+ }
+ }
+
+ StorableIndex<S>[] indexes;
+ {
+ indexes = new StorableIndex[indexProps.size()];
+ int i = 0;
+ for (List<NameAndDirection> nameAndDirs : indexProps) {
+ int errorCount = errorMessages.size();
+ Set<OrderedProperty<S>> propSet =
+ resolveKey(errorMessages, type, properties, "index", nameAndDirs);
+ if (errorMessages.size() <= errorCount) {
+ // If index property not found, error message has been
+ // added to list, but propSet might end up being
+ // empty. Rather than get an exception thrown from the
+ // StorableIndex constructor, just don't try to define
+ // the bogus index at all.
+ OrderedProperty<S>[] propArray = new OrderedProperty[propSet.size()];
+ propSet.toArray(propArray);
+ indexes[i] = new StorableIndex<S>(propArray, null);
+ }
+ i++;
+ }
+ }
+
+ // Sort properties by name, grouped with primary keys first. This
+ // ensures a consistent arrangement, even if methods move around in
+ // the class file.
+ {
+ // Store results in a LinkedHashMap to preserve sort order.
+ Map<String, StorableProperty<S>> arrangedProperties =
+ new LinkedHashMap<String, StorableProperty<S>>();
+
+ // First dump in primary key properties, in their proper order.
+ for (OrderedProperty<S> orderedProp : primaryKey.getProperties()) {
+ StorableProperty<S> prop = orderedProp.getChainedProperty().getPrimeProperty();
+ arrangedProperties.put(prop.getName(), prop);
+ }
+
+ // Gather all remaining properties, and then sort them.
+ List<StorableProperty<S>> nonPkProperties = new ArrayList<StorableProperty<S>>();
+
+ for (StorableProperty<S> prop : properties.values()) {
+ if (!arrangedProperties.containsKey(prop.getName())) {
+ nonPkProperties.add(prop);
+ }
+ }
+
+ Collections.sort(nonPkProperties,
+ BeanComparator.forClass(StorableProperty.class).orderBy("name"));
+
+ for (StorableProperty<S> prop : nonPkProperties) {
+ arrangedProperties.put(prop.getName(), prop);
+ }
+
+ properties = Collections.unmodifiableMap(arrangedProperties);
+ }
+
+ // Process type aliases
+
+ String[] aliases;
+ Alias alias = type.getAnnotation(Alias.class);
+ if (alias == null) {
+ aliases = null;
+ } else {
+ aliases = alias.value();
+ if (aliases.length == 0) {
+ errorMessages.add
+ ("Alias list is empty for type \"" + type.getName() + '"');
+ }
+ }
+
+ info = new Info<S>(type, aliases, indexes, properties,
+ primaryKey, alternateKeys,
+ type.getAnnotation(Independent.class) != null);
+ cCache.put(type, new SoftReference<StorableInfo<?>>(info));
+
+ // Finish resolving join properties, after properties have been
+ // added to cache. This makes it possible for joins to (directly or
+ // indirectly) reference their own enclosing type. If not resolved
+ // late, then there would be a stack overflow.
+ for (StorableProperty property : properties.values()) {
+ if (property instanceof JoinProperty) {
+ ((JoinProperty)property).resolve(errorMessages, properties);
+ }
+ }
+ if (errorMessages.size() > 0) {
+ cCache.remove(type);
+ throw new MalformedTypeException(type, errorMessages);
+ }
+
+ return info;
+ }
+ }
+
+ private static class NameAndDirection {
+ final String name;
+ final Direction direction;
+ NameAndDirection(String name, Direction direction) {
+ this.name = name;
+ this.direction = direction;
+ }
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof NameAndDirection) {
+ // Only compare name.
+ return name.equals(((NameAndDirection) obj).name);
+ }
+ return false;
+ }
+ }
+
+ /**
+ * @param indexes pass in just this for gathering index properties
+ * @param keys pass in just this for gathering alternate key properties
+ * @param primaryKey pass in just this for gathering primary key properties
+ */
+ private static List<List<NameAndDirection>> gatherListProperties(List<String> errorMessages,
+ Indexes indexes,
+ AlternateKeys keys,
+ PrimaryKey primaryKey)
+ {
+ List<List<NameAndDirection>> listlist = new ArrayList<List<NameAndDirection>>();
+
+ if (indexes != null) {
+ Index[] ixs = indexes.value();
+ if (ixs != null && ixs.length > 0) {
+ for (int i=0; i < ixs.length; i++) {
+ String[] propNames = ixs[i].value();
+ if (propNames == null || propNames.length == 0) {
+ errorMessages.add("Empty index defined");
+ continue;
+ }
+ gatherListProperties(errorMessages, "index", propNames, listlist);
+ }
+ }
+ } else if (keys != null) {
+ Key[] ixs = keys.value();
+ if (ixs != null && ixs.length > 0) {
+ for (int i=0; i < ixs.length; i++) {
+ String[] propNames = ixs[i].value();
+ if (propNames == null || propNames.length == 0) {
+ errorMessages.add("Empty alternate key defined");
+ continue;
+ }
+ gatherListProperties(errorMessages, "alternate key", propNames, listlist);
+ }
+ }
+ } else if (primaryKey != null) {
+ String[] propNames = primaryKey.value();
+ if (propNames == null || propNames.length == 0) {
+ errorMessages.add("Empty primary key defined");
+ } else {
+ gatherListProperties(errorMessages, "primary key", propNames, listlist);
+ }
+ }
+
+ return listlist;
+ }
+
+ private static void gatherListProperties(List<String> errorMessages,
+ String listName,
+ String[] propNames,
+ List<List<NameAndDirection>> listlist)
+ {
+ int length = propNames.length;
+ List<NameAndDirection> nameAndDirs = new ArrayList<NameAndDirection>(length);
+
+ for (int i=0; i<length; i++) {
+ String name = propNames[i];
+ Direction dir = Direction.UNSPECIFIED;
+ if (name.length() > 0) {
+ if (name.charAt(0) == '+') {
+ name = name.substring(1);
+ dir = Direction.ASCENDING;
+ } else if (name.charAt(0) == '-') {
+ name = name.substring(1);
+ dir = Direction.DESCENDING;
+ }
+ }
+
+ NameAndDirection nameAndDir = new NameAndDirection(name, dir);
+
+ if (nameAndDirs.contains(nameAndDir)) {
+ errorMessages.add
+ ("Duplicate property in " + listName + ": " + Arrays.toString(propNames));
+ continue;
+ } else {
+ nameAndDirs.add(nameAndDir);
+ }
+ }
+
+ if (nameAndDirs.size() == 0) {
+ return;
+ }
+
+ if (listlist.contains(nameAndDirs)) {
+ errorMessages.add
+ ("Duplicate " + listName + " specification: " + Arrays.toString(propNames));
+ return;
+ }
+
+ listlist.add(nameAndDirs);
+ }
+
+ private static <S extends Storable> Set<OrderedProperty<S>>
+ resolveKey(List<String> errorMessages,
+ Class<S> type,
+ Map<String, StorableProperty<S>> properties,
+ String elementName,
+ List<NameAndDirection> nameAndDirs)
+ {
+ Set<OrderedProperty<S>> orderedProps = new LinkedHashSet<OrderedProperty<S>>();
+
+ for (NameAndDirection nameAndDir : nameAndDirs) {
+ String name = nameAndDir.name;
+ if (name.indexOf('.') > 0) {
+ errorMessages.add("Chained property not allowed in " + elementName + ": " + name);
+ continue;
+ }
+ StorableProperty<S> prop = properties.get(name);
+ if (prop == null) {
+ errorMessages.add
+ ("Property \"" + name + "\" for " + elementName +
+ " not found in type: " + type.getName());
+ continue;
+ }
+ if (prop.isJoin()) {
+ errorMessages.add
+ ("Property of " + elementName + " cannot reference a join property: " + name);
+ continue;
+ }
+ if (Lob.class.isAssignableFrom(prop.getType())) {
+ errorMessages.add
+ ("Property of " + elementName + " cannot reference a LOB property: " + name);
+ continue;
+ }
+
+ orderedProps.add(OrderedProperty.get(prop, nameAndDir.direction));
+ }
+
+ if (orderedProps.size() == 0) {
+ return Collections.emptySet();
+ }
+ if (orderedProps.size() == 1) {
+ return Collections.singleton(orderedProps.iterator().next());
+ }
+ return Collections.unmodifiableSet(orderedProps);
+ }
+
+ /**
+ * Does the real work in examining the given type. The join properties and
+ * alternate keys must still be resolved afterwards.
+ */
+ private static <S extends Storable> Map<String, StorableProperty<S>>
+ examineProperties(Class<S> type,
+ List<NameAndDirection> primaryKeyProps,
+ List<List<NameAndDirection>> alternateKeyProps)
+ throws MalformedTypeException
+ {
+ if (Storable.class.isAssignableFrom(type)) {
+ if (Storable.class == type) {
+ throw new MalformedTypeException(type, "Storable interface must be extended");
+ }
+ } else {
+ throw new MalformedTypeException
+ (type, "Does not implement Storable interface: " + type);
+ }
+ int modifiers = type.getModifiers();
+ if (Modifier.isFinal(modifiers)) {
+ throw new MalformedTypeException(type, "Class is declared final: " + type);
+ }
+ if (!Modifier.isPublic(modifiers)) {
+ throw new MalformedTypeException(type, "Class is not public: " + type);
+ }
+
+ List<String> errorMessages = new ArrayList<String>();
+
+ checkTypeParameter(errorMessages, type);
+
+ // If type is a class, it must have a public or protected no-arg
+ // constructor.
+
+ if (!type.isInterface()) {
+ Constructor[] ctors = type.getDeclaredConstructors();
+ findCtor: {
+ for (Constructor c : ctors) {
+ if (c.getParameterTypes().length == 0) {
+ modifiers = c.getModifiers();
+ if (!Modifier.isPublic(modifiers) && !Modifier.isProtected(modifiers)) {
+ errorMessages.add("Cannot call constructor: " + c);
+ }
+ break findCtor;
+ }
+ }
+ errorMessages.add
+ ("Class must have an accesible no-arg constructor: " + type);
+ }
+ }
+
+ // All methods to be implemented must be bean property methods that
+ // operate on a supported type.
+
+ // First, gather all methods that must be implemented.
+
+ // Gather all methods. We'll be removing them as we implement them,
+ // and if there are any abstract ones left over at the end, why,
+ // that would be bad.
+ Map<String, Method> methods = CodeBuilderUtil.gatherAllDeclaredMethods(type);
+
+ // Remove methods not abstract or defined explicitly in
+ // Storable. Storable methods still must be implemented, but not as
+ // properties.
+ for (Iterator<Method> it = methods.values().iterator(); it.hasNext(); ) {
+ Method m = it.next();
+ if (!Modifier.isAbstract(m.getModifiers()) ||
+ m.getDeclaringClass() == Storable.class) {
+ it.remove();
+ continue;
+ }
+ // 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();
+ }
+ } catch (NoSuchMethodException e) {
+ // Not defined in Storable.
+ }
+ }
+
+ // Identify which properties are members of a primary or alternate key.
+ Set<String> pkPropertyNames, altKeyPropertyNames;
+ {
+ pkPropertyNames = new HashSet<String>();
+ altKeyPropertyNames = new HashSet<String>();
+ for (NameAndDirection nameAndDir : primaryKeyProps) {
+ pkPropertyNames.add(nameAndDir.name);
+ }
+ for (List<NameAndDirection> list : alternateKeyProps) {
+ for (NameAndDirection nameAndDir : list) {
+ altKeyPropertyNames.add(nameAndDir.name);
+ }
+ }
+ }
+
+ Map allProperties = BeanIntrospector.getAllProperties(type);
+
+ // Copy only the properties that should be implemented here.
+ Map<String, StorableProperty<S>> properties =
+ new HashMap<String, StorableProperty<S>>();
+
+ // Remove methods for properties that can be implemented.
+ Iterator it = allProperties.values().iterator();
+ while (it.hasNext()) {
+ BeanProperty property = BeanProperty.class.cast(it.next());
+ Method readMethod = property.getReadMethod();
+ Method writeMethod = property.getWriteMethod();
+
+ if (readMethod == null && writeMethod == null) {
+ continue;
+ }
+
+ boolean pk = pkPropertyNames.contains(property.getName());
+ boolean altKey = altKeyPropertyNames.contains(property.getName());
+
+ StorableProperty<S> storableProp = makeStorableProperty
+ (errorMessages, property, type, pk, altKey);
+
+ if (storableProp == null) {
+ // Errors.
+ continue;
+ }
+
+ if (readMethod != null) {
+ String sig = CodeBuilderUtil.createSig(readMethod);
+ if (methods.containsKey(sig)) {
+ methods.remove(sig);
+ properties.put(property.getName(), storableProp);
+ } else {
+ continue;
+ }
+ }
+
+ if (writeMethod != null) {
+ String sig = CodeBuilderUtil.createSig(writeMethod);
+ if (methods.containsKey(sig)) {
+ methods.remove(sig);
+ properties.put(property.getName(), storableProp);
+ } else {
+ continue;
+ }
+ }
+ }
+
+ // Only include errors on unimplementable methods if there are no other
+ // errors. This prevents producing errors caused by other errors.
+ Iterator<Method> iter = methods.values().iterator();
+ while (iter.hasNext()) {
+ Method m = iter.next();
+ int methodModifiers = m.getModifiers();
+ if (Modifier.isAbstract(methodModifiers )) {
+ if (!Modifier.isPublic(methodModifiers) && !Modifier.isProtected(methodModifiers))
+ {
+ errorMessages.add("Abstract method cannot be defined (neither public or " +
+ "protected): " + m);
+ } else {
+ errorMessages.add
+ ("Abstract method cannot be defined (not a bean property):" + m);
+ }
+ // We've reported the error, nothing more to say about it
+ iter.remove();
+ }
+ }
+
+ // Verify at most one version property exists.
+ {
+ boolean hasVersionProp = false;
+ for (StorableProperty property : properties.values()) {
+ if (property.isVersion()) {
+ if (hasVersionProp) {
+ errorMessages.add
+ ("At most one property may be designated as the version number");
+ break;
+ }
+ hasVersionProp = true;
+ }
+ }
+ }
+
+ // Only include errors on unimplementable methods if there are no other
+ // errors. This prevents producing errors caused by other errors.
+ if (errorMessages.size() == 0 && methods.size() > 0) {
+ for (Method m : methods.values()) {
+ errorMessages.add("Method cannot be implemented: " + m);
+ }
+ }
+
+ if (errorMessages.size() > 0) {
+ throw new MalformedTypeException(type, errorMessages);
+ }
+
+ return Collections.unmodifiableMap(properties);
+ }
+
+ /**
+ * Make sure that the parameter type that is specified to Storable can be
+ * assigned to a Storable, and that the given type can be assigned to
+ * it. Put another way, the upper bound is Storable, and the lower bound
+ * is the given type. type <= parameterized type <= Storable
+ */
+ @SuppressWarnings("unchecked")
+ private static void checkTypeParameter(List<String> errorMessages, Class type) {
+ // Only check classes and interfaces that extend Storable.
+ if (type != null && Storable.class.isAssignableFrom(type)) {
+ if (Storable.class == type) {
+ return;
+ }
+ } else {
+ return;
+ }
+
+ // Check all superclasses and interfaces.
+ checkTypeParameter(errorMessages, type.getSuperclass());
+
+ for (Class c : type.getInterfaces()) {
+ checkTypeParameter(errorMessages, c);
+ }
+
+ for (Type t : type.getGenericInterfaces()) {
+ if (t instanceof ParameterizedType) {
+ ParameterizedType pt = (ParameterizedType)t;
+ if (pt.getRawType() == Storable.class) {
+ // Found exactly which parameter is passed directly to
+ // Storable. Make sure that it is in the proper bounds.
+ Type arg = pt.getActualTypeArguments()[0];
+ Class param;
+ if (arg instanceof ParameterizedType) {
+ Type raw = ((ParameterizedType)arg).getRawType();
+ if (raw instanceof Class) {
+ param = (Class)raw;
+ } else {
+ continue;
+ }
+ } else if (arg instanceof Class) {
+ param = (Class)arg;
+ } else if (arg instanceof TypeVariable) {
+ // TODO
+ continue;
+ } else {
+ continue;
+ }
+ if (Storable.class.isAssignableFrom(param)) {
+ if (!param.isAssignableFrom(type)) {
+ errorMessages.add
+ ("Type parameter passed from " + type +
+ " to Storable must be a " + type.getName() + ": " + param);
+ return;
+ }
+ } else {
+ errorMessages.add
+ ("Type parameter passed from " + type +
+ " to Storable must be a Storable: " + param);
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * If property is a join, then it is not yet completely resolved. Returns
+ * null if there are any errors.
+ *
+ * @param errorMessages error messages go here
+ * @param property property to examine
+ * @param enclosing enclosing class
+ * @param pk true if member of primary key
+ * @param altKey true if member of alternate key
+ */
+ @SuppressWarnings("unchecked")
+ private static <S extends Storable> StorableProperty<S> makeStorableProperty
+ (List<String> errorMessages,
+ BeanProperty property,
+ Class<S> enclosing,
+ boolean pk, boolean altKey)
+ {
+ Nullable nullable = null;
+ Alias alias = null;
+ Version version = null;
+ Sequence sequence = null;
+ Independent independent = null;
+ Join join = null;
+
+ Method readMethod = property.getReadMethod();
+ Method writeMethod = property.getWriteMethod();
+
+ if (readMethod == null) {
+ if (writeMethod == null || Modifier.isAbstract(writeMethod.getModifiers())) {
+ // If we got here, the onus is on us to create this property. It's never
+ // ok for the read method (get) to be null.
+ errorMessages.add("Property must define 'get' method: " + property.getName());
+ }
+ } else {
+ nullable = readMethod.getAnnotation(Nullable.class);
+ alias = readMethod.getAnnotation(Alias.class);
+ version = readMethod.getAnnotation(Version.class);
+ sequence = readMethod.getAnnotation(Sequence.class);
+ independent = readMethod.getAnnotation(Independent.class);
+ join = readMethod.getAnnotation(Join.class);
+ }
+
+ if (writeMethod == null) {
+ if (readMethod == null || Modifier.isAbstract(readMethod.getModifiers())) {
+ // Last chance: can have an abstract read method (which we implement) and no
+ // write method for join properties
+ if (join == null) {
+ errorMessages.add("Property must define 'set' method: " + property.getName());
+ }
+ }
+ } else {
+ if (writeMethod.getAnnotation(Nullable.class) != null) {
+ errorMessages.add
+ ("Nullable annotation not allowed on mutator: " + writeMethod);
+ }
+ if (writeMethod.getAnnotation(Alias.class) != null) {
+ errorMessages.add
+ ("Alias annotation not allowed on mutator: " + writeMethod);
+ }
+ if (writeMethod.getAnnotation(Version.class) != null) {
+ errorMessages.add
+ ("Version annotation not allowed on mutator: " + writeMethod);
+ }
+ if (writeMethod.getAnnotation(Sequence.class) != null) {
+ errorMessages.add
+ ("Sequence annotation not allowed on mutator: " + writeMethod);
+ }
+ if (writeMethod.getAnnotation(Independent.class) != null) {
+ errorMessages.add
+ ("Independent annotation not allowed on mutator: " + writeMethod);
+ }
+ if (writeMethod.getAnnotation(Join.class) != null) {
+ errorMessages.add
+ ("Join annotation not allowed on mutator: " + writeMethod);
+ }
+ }
+
+ if (nullable != null && property.getType().isPrimitive()) {
+ errorMessages.add
+ ("Properties which have a primitive type cannot be declared nullable: " +
+ "Property \"" + property.getName() + "\" has type \"" +
+ property.getType() + '"');
+ }
+
+ String[] aliases = null;
+ if (alias != null) {
+ aliases = alias.value();
+ if (aliases.length == 0) {
+ errorMessages.add
+ ("Alias list is empty for property \"" + property.getName() + '"');
+ }
+ }
+
+ StorablePropertyConstraint[] constraints = null;
+ if (readMethod != null) {
+ // Constraints not allowed on read method. Look for them and
+ // generate errors if any found.
+ gatherConstraints(property, readMethod, false, errorMessages);
+ }
+ if (writeMethod != null) {
+ constraints = gatherConstraints(property, writeMethod, true, errorMessages);
+ }
+
+ StorablePropertyAdapter[] adapters = null;
+ if (readMethod != null) {
+ adapters = gatherAdapters(property, readMethod, true, errorMessages);
+ if (adapters != null && adapters.length > 0) {
+ if (join != null) {
+ errorMessages.add
+ ("Join properties cannot have adapters: " + property.getName());
+ }
+ if (adapters.length > 1) {
+ errorMessages.add
+ ("Only one adpater allowed per property: " + property.getName());
+ }
+ }
+ if (adapters == null || adapters.length == 0) {
+ StorablePropertyAdapter autoAdapter =
+ AutomaticAdapterSelector.selectAdapterFor(property);
+ if (autoAdapter != null) {
+ adapters = new StorablePropertyAdapter[] {autoAdapter};
+ }
+ }
+ }
+ if (writeMethod != null) {
+ // Adapters not allowed on write method. Look for them and generate
+ // errors if any found.
+ gatherAdapters(property, writeMethod, false, errorMessages);
+ }
+
+ String sequenceName = null;
+ if (sequence != null) {
+ sequenceName = sequence.value();
+ }
+
+ if (join == null) {
+ if (errorMessages.size() > 0) {
+ return null;
+ }
+ return new SimpleProperty<S>
+ (property, enclosing, nullable != null, pk, altKey,
+ aliases, constraints, adapters == null ? null : adapters[0],
+ version != null, sequenceName, independent != null);
+ }
+
+ // Do additional work for join properties.
+
+ String[] internal = join.internal();
+ String[] external = join.external();
+
+ if (internal == null) {
+ internal = new String[0];
+ }
+ if (external == null) {
+ external = new String[0];
+ }
+
+ if (internal.length != external.length) {
+ errorMessages.add
+ ("Internal/external lists on Join property \"" + property.getName() +
+ "\" differ in length: " + internal.length + " != " + external.length);
+ }
+
+ Class joinedType = property.getType();
+ if (Query.class == joinedType) {
+ if (nullable != null) {
+ errorMessages.add
+ ("Join property \"" + property.getName() +
+ "\" cannot be declared as nullable because the type is Query");
+ }
+
+ // Recover the results element type from the accessor. A Mutator is
+ // not allowed.
+
+ if (property.getWriteMethod() != null) {
+ errorMessages.add
+ ("Join property \"" + property.getName() +
+ "\" cannot have a mutator because the type is Query: " +
+ property.getWriteMethod());
+ }
+
+ if (property.getReadMethod() == null) {
+ // Default.
+ joinedType = Storable.class;
+ } else {
+ Type genericType = property.getReadMethod().getGenericReturnType();
+
+ if (genericType instanceof Class) {
+ // Default.
+ joinedType = Storable.class;
+ } else if (genericType instanceof ParameterizedType) {
+ ParameterizedType pt = (ParameterizedType)genericType;
+ Type[] args = pt.getActualTypeArguments();
+ if (args == null || args.length == 0) {
+ // Default.
+ joinedType = Storable.class;
+ } else {
+ Type arg = args[0];
+ while (arg instanceof ParameterizedType) {
+ arg = ((ParameterizedType)arg).getRawType();
+ }
+ if (arg instanceof Class) {
+ joinedType = (Class)arg;
+ }
+ }
+ }
+ }
+ }
+
+ if (!Storable.class.isAssignableFrom(joinedType)) {
+ errorMessages.add
+ ("Type of join property \"" + property.getName() +
+ "\" is not a Storable: " + joinedType);
+ }
+
+ if (property.getReadMethod() != null) {
+ Class exceptionType = FetchException.class;
+
+ Class<?>[] exceptions = property.getReadMethod().getExceptionTypes();
+ check: {
+ for (int i=exceptions.length; --i>=0; ) {
+ if (exceptions[i].isAssignableFrom(exceptionType)) {
+ break check;
+ }
+ }
+
+ String exceptionName = exceptionType.getName();
+ int index = exceptionName.lastIndexOf('.');
+ if (index >= 0) {
+ exceptionName = exceptionName.substring(index + 1);
+ }
+
+ errorMessages.add
+ ("Join property accessor must declare throwing a " +
+ exceptionName + ": " + property.getReadMethod());
+ }
+ }
+
+ if (version != null) {
+ errorMessages.add
+ ("Join property \"" + property.getName() +
+ "\" cannot be declared as a version property");
+ }
+
+ if (errorMessages.size() > 0) {
+ return null;
+ }
+
+ return new JoinProperty<S>
+ (property, enclosing, nullable != null, aliases,
+ constraints, adapters == null ? null : adapters[0],
+ sequenceName, independent != null, joinedType, internal, external);
+ }
+
+ private static StorablePropertyConstraint[] gatherConstraints
+ (BeanProperty property, Method method, boolean isAllowed, List<String> errorMessages)
+ {
+ Annotation[] allAnnotations = method.getAnnotations();
+ if (allAnnotations.length == 0) {
+ return null;
+ }
+
+ List<StorablePropertyConstraint> list = new ArrayList<StorablePropertyConstraint>();
+
+ for (Annotation annotation : allAnnotations) {
+ Class<? extends Annotation> type = annotation.annotationType();
+ ConstraintDefinition cd = type.getAnnotation(ConstraintDefinition.class);
+ if (cd == null) {
+ continue;
+ }
+ if (!isAllowed) {
+ errorMessages.add("Constraint not allowed on method: " + method);
+ return null;
+ }
+
+ Class constraintClass = cd.implementation();
+
+ if (constraintClass == void.class) {
+ // Magic value meaning "use default", which is an inner class
+ // of the annotation.
+
+ constraintClass = null;
+
+ // Search for inner class named "Constraint".
+ Class[] innerClasses = type.getClasses();
+ for (Class c : innerClasses) {
+ if ("Constraint".equals(c.getSimpleName())) {
+ constraintClass = c;
+ break;
+ }
+ }
+
+ if (constraintClass == null) {
+ errorMessages.add
+ ("By default, constraint implementation class must be a static inner " +
+ "class of the annotation named \"Constraint\". Fully qualified name: " +
+ type.getCanonicalName() + ".Constraint");
+ continue;
+ }
+ }
+
+ int modifiers = constraintClass.getModifiers();
+
+ if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) ||
+ !Modifier.isPublic(modifiers)) {
+
+ errorMessages.add
+ ("Constraint implementation class must be a concrete public class: " +
+ constraintClass.getName());
+ continue;
+ }
+
+ Constructor ctor;
+ try {
+ ctor = constraintClass.getConstructor(Class.class, String.class, type);
+ } catch (NoSuchMethodException e) {
+ errorMessages.add
+ ("Constraint implementation class does not have proper constructor: " +
+ constraintClass.getName());
+ continue;
+ }
+
+ // Find best constrain method to bind to.
+
+ ConversionComparator cc = new ConversionComparator(property.getType());
+ Class bestMatchingType = null;
+ Method bestConstrainMethod = null;
+
+ for (Method constrainMethod : constraintClass.getMethods()) {
+ if (!constrainMethod.getName().equals("constrain")) {
+ continue;
+ }
+ if (constrainMethod.getReturnType() != void.class) {
+ continue;
+ }
+ Class<?>[] paramTypes = constrainMethod.getParameterTypes();
+ if (paramTypes.length != 1) {
+ continue;
+ }
+
+ Class candidateType = paramTypes[0];
+
+ if (!cc.isConversionPossible(candidateType)) {
+ continue;
+ }
+
+ if (bestMatchingType == null || cc.compare(bestMatchingType, candidateType) > 0) {
+ bestMatchingType = candidateType;
+ bestConstrainMethod = constrainMethod;
+ }
+ }
+
+ if (bestConstrainMethod == null) {
+ errorMessages.add("Constraint does not support property type: " +
+ property.getType().getName() + "; constraint type: " +
+ annotation.annotationType().getName());
+ } else {
+ StorablePropertyAnnotation spa =
+ new StorablePropertyAnnotation(annotation, method);
+ list.add(new StorablePropertyConstraint(spa, ctor, bestConstrainMethod));
+ }
+ }
+
+ if (list.size() == 0) {
+ return null;
+ }
+
+ return (StorablePropertyConstraint[]) list.toArray
+ (new StorablePropertyConstraint[list.size()]);
+ }
+
+ private static StorablePropertyAdapter[] gatherAdapters
+ (BeanProperty property, Method method, boolean isAllowed, List<String> errorMessages)
+ {
+ Annotation[] allAnnotations = method.getAnnotations();
+ if (allAnnotations.length == 0) {
+ return null;
+ }
+
+ List<StorablePropertyAdapter> list = new ArrayList<StorablePropertyAdapter>();
+
+ for (Annotation annotation : allAnnotations) {
+ Class<? extends Annotation> type = annotation.annotationType();
+ AdapterDefinition ad = type.getAnnotation(AdapterDefinition.class);
+ if (ad == null) {
+ continue;
+ }
+ if (!isAllowed) {
+ errorMessages.add("Adapter not allowed on method: " + method);
+ return null;
+ }
+
+ Class adapterClass = StorablePropertyAdapter.findAdapterClass(type);
+
+ if (adapterClass == null) {
+ errorMessages.add
+ ("By default, adapter implementation class must be a static inner " +
+ "class of the annotation named \"Adapter\". Fully qualified name: " +
+ type.getCanonicalName() + ".Adapter");
+ continue;
+ }
+
+ int modifiers = adapterClass.getModifiers();
+
+ if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) ||
+ !Modifier.isPublic(modifiers)) {
+
+ errorMessages.add
+ ("Adapter implementation class must be a concrete public class: " +
+ adapterClass.getName());
+ continue;
+ }
+
+ Constructor ctor;
+ try {
+ ctor = adapterClass.getConstructor(Class.class, String.class, type);
+ } catch (NoSuchMethodException e) {
+ errorMessages.add
+ ("Adapter implementation class does not have proper constructor: " +
+ adapterClass.getName());
+ continue;
+ }
+
+ Method[] adaptMethods =
+ StorablePropertyAdapter.findAdaptMethods(property.getType(), adapterClass);
+
+ if (adaptMethods.length == 0) {
+ errorMessages.add("Adapter does not support property type: " +
+ property.getType().getName() + "; adapter type: " +
+ annotation.annotationType().getName());
+ } else {
+ StorablePropertyAnnotation spa =
+ new StorablePropertyAnnotation(annotation, method);
+ list.add(new StorablePropertyAdapter(property, spa, ad, ctor, adaptMethods));
+ }
+ }
+
+ if (list.size() == 0) {
+ return null;
+ }
+
+ return (StorablePropertyAdapter[]) list.toArray(new StorablePropertyAdapter[list.size()]);
+ }
+
+ private static final class Info<S extends Storable> implements StorableInfo<S> {
+ private final Class<S> mType;
+ private final String[] mAliases;
+ private final StorableIndex<S>[] mIndexes;
+ private final Map<String, StorableProperty<S>> mAllProperties;
+ private final StorableKey<S> mPrimaryKey;
+ private final StorableKey<S>[] mAltKeys;
+ private final boolean mIndependent;
+
+ private transient String mName;
+ private transient Map<String, StorableProperty<S>> mPrimaryKeyProperties;
+ private transient Map<String, StorableProperty<S>> mDataProperties;
+ private transient StorableProperty<S> mVersionProperty;
+
+ Info(Class<S> type, String[] aliases, StorableIndex<S>[] indexes,
+ Map<String, StorableProperty<S>> properties,
+ StorableKey<S> primaryKey,
+ StorableKey<S>[] altKeys,
+ boolean independent)
+ {
+ mType = type;
+ mAliases = aliases;
+ mIndexes = indexes;
+ mAllProperties = properties;
+ mPrimaryKey = primaryKey;
+ mAltKeys = altKeys;
+ mIndependent = independent;
+ }
+
+ public String getName() {
+ String name = mName;
+ if (name == null) {
+ name = getStorableType().getName();
+ int index = name.lastIndexOf('.');
+ if (index >= 0) {
+ name = name.substring(index + 1);
+ }
+ mName = name;
+ }
+ return name;
+ }
+
+ public Class<S> getStorableType() {
+ return mType;
+ }
+
+ public Map<String, StorableProperty<S>> getAllProperties() {
+ return mAllProperties;
+ }
+
+ public Map<String, StorableProperty<S>> getPrimaryKeyProperties() {
+ if (mPrimaryKeyProperties == null) {
+ Set<? extends OrderedProperty<S>> pkSet = mPrimaryKey.getProperties();
+ Map<String, StorableProperty<S>> pkProps =
+ new LinkedHashMap<String, StorableProperty<S>>(pkSet.size());
+ for (OrderedProperty<S> prop : pkSet) {
+ StorableProperty<S> prime = prop.getChainedProperty().getPrimeProperty();
+ pkProps.put(prime.getName(), prime);
+ }
+ mPrimaryKeyProperties = Collections.unmodifiableMap(pkProps);
+ }
+ return mPrimaryKeyProperties;
+ }
+
+ public Map<String, StorableProperty<S>> getDataProperties() {
+ if (mDataProperties == null) {
+ Map<String, StorableProperty<S>> dataProps =
+ new LinkedHashMap<String, StorableProperty<S>>(mAllProperties.size());
+ for (Map.Entry<String, StorableProperty<S>> entry : mAllProperties.entrySet()) {
+ StorableProperty<S> property = entry.getValue();
+ if (!property.isPrimaryKeyMember() && !property.isJoin()) {
+ dataProps.put(entry.getKey(), property);
+ }
+ }
+ mDataProperties = Collections.unmodifiableMap(dataProps);
+ }
+ return mDataProperties;
+ }
+
+ public StorableProperty<S> getVersionProperty() {
+ if (mVersionProperty == null) {
+ for (StorableProperty<S> property : mAllProperties.values()) {
+ if (property.isVersion()) {
+ mVersionProperty = property;
+ break;
+ }
+ }
+ }
+ return mVersionProperty;
+ }
+
+ public StorableKey<S> getPrimaryKey() {
+ return mPrimaryKey;
+ }
+
+ public int getAlternateKeyCount() {
+ StorableKey<S>[] keys = mAltKeys;
+ return keys == null ? 0 : keys.length;
+ }
+
+ public StorableKey<S> getAlternateKey(int index) {
+ StorableKey<S>[] keys = mAltKeys;
+ if (keys == null) {
+ throw new IndexOutOfBoundsException();
+ } else {
+ return keys[index];
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public StorableKey<S>[] getAlternateKeys() {
+ StorableKey<S>[] keys = mAltKeys;
+ if (keys == null) {
+ return new StorableKey[0];
+ } else {
+ return keys.clone();
+ }
+ }
+
+ public int getAliasCount() {
+ String[] aliases = mAliases;
+ return aliases == null ? 0 : aliases.length;
+ }
+
+ public String getAlias(int index) {
+ String[] aliases = mAliases;
+ if (aliases == null) {
+ throw new IndexOutOfBoundsException();
+ } else {
+ return aliases[index];
+ }
+ }
+
+ public String[] getAliases() {
+ String[] aliases = mAliases;
+ if (aliases == null) {
+ return new String[0];
+ } else {
+ return aliases.clone();
+ }
+ }
+
+ public int getIndexCount() {
+ StorableIndex<S>[] indexes = mIndexes;
+ return indexes == null ? 0 : indexes.length;
+ }
+
+ public StorableIndex<S> getIndex(int index) {
+ StorableIndex<S>[] indexes = mIndexes;
+ if (indexes == null) {
+ throw new IndexOutOfBoundsException();
+ } else {
+ return indexes[index];
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public StorableIndex<S>[] getIndexes() {
+ StorableIndex<S>[] indexes = mIndexes;
+ if (indexes == null) {
+ return new StorableIndex[0];
+ } else {
+ return indexes.clone();
+ }
+ }
+
+ public final boolean isIndependent() {
+ return mIndependent;
+ }
+ }
+
+ private static class SKey<S extends Storable> implements StorableKey<S> {
+ private final boolean mPrimary;
+ private final Set<OrderedProperty<S>> mProperties;
+
+ SKey(boolean primary, Set<OrderedProperty<S>> properties) {
+ mPrimary = primary;
+ mProperties = properties;
+ }
+
+ public boolean isPrimary() {
+ return mPrimary;
+ }
+
+ public Set<OrderedProperty<S>> getProperties() {
+ return mProperties;
+ }
+
+ @Override
+ public int hashCode() {
+ return mProperties.hashCode();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof StorableKey) {
+ StorableKey<S> other = (StorableKey<S>) obj;
+ return isPrimary() == other.isPrimary()
+ && getProperties().equals(other.getProperties());
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder b = new StringBuilder();
+ b.append("StorableKey ");
+ try {
+ appendTo(b);
+ } catch (IOException e) {
+ // Not gonna happen.
+ }
+ return b.toString();
+ }
+
+ /**
+ * Appends the same results as toString, but without the "StorableKey"
+ * prefix.
+ */
+ public void appendTo(Appendable app) throws IOException {
+ app.append("{properties=[");
+ int i = 0;
+ for (OrderedProperty<S> prop : mProperties) {
+ if (i++ > 0) {
+ app.append(", ");
+ }
+ prop.appendTo(app);
+ }
+ app.append(']');
+ app.append(", primary=");
+ app.append(String.valueOf(isPrimary()));
+ app.append('}');
+ }
+ }
+
+ private static class SimpleProperty<S extends Storable> implements StorableProperty<S> {
+ private final BeanProperty mBeanProperty;
+ private final Class<S> mEnclosingType;
+ private final boolean mNullable;
+ private final boolean mPrimaryKey;
+ private final boolean mAlternateKey;
+ private final String[] mAliases;
+ private final StorablePropertyConstraint[] mConstraints;
+ private final StorablePropertyAdapter mAdapter;
+ private final boolean mIsVersion;
+ private final String mSequence;
+ private final boolean mIndependent;
+
+ SimpleProperty(BeanProperty property, Class<S> enclosing,
+ boolean nullable, boolean primaryKey, boolean alternateKey,
+ String[] aliases, StorablePropertyConstraint[] constraints,
+ StorablePropertyAdapter adapter,
+ boolean isVersion, String sequence, boolean independent) {
+ mBeanProperty = property;
+ mEnclosingType = enclosing;
+ mNullable = property.getType().isPrimitive() ? false : nullable;
+ mPrimaryKey = primaryKey;
+ mAlternateKey = alternateKey;
+ mAliases = aliases;
+ mConstraints = constraints;
+ mAdapter = adapter;
+ mIsVersion = isVersion;
+ mSequence = sequence;
+ mIndependent = independent;
+ }
+
+ public final String getName() {
+ return mBeanProperty.getName();
+ }
+
+ public final Class<?> getType() {
+ return mBeanProperty.getType();
+ }
+
+ public final Class<S> getEnclosingType() {
+ return mEnclosingType;
+ }
+
+ public final Method getReadMethod() {
+ return mBeanProperty.getReadMethod();
+ }
+
+ public final String getReadMethodName() {
+ Method m = mBeanProperty.getReadMethod();
+ if (m != null) {
+ return m.getName();
+ }
+ // Return synthetic name.
+ return "get" + getWriteMethod().getName().substring(3);
+ }
+
+ public final Method getWriteMethod() {
+ return mBeanProperty.getWriteMethod();
+ }
+
+ public final String getWriteMethodName() {
+ Method m = mBeanProperty.getWriteMethod();
+ if (m != null) {
+ return m.getName();
+ }
+ // Return synthetic name.
+ String readName = getReadMethod().getName();
+ return "set" + readName.substring(readName.startsWith("is") ? 2 : 3);
+ }
+
+ public final boolean isNullable() {
+ return mNullable;
+ }
+
+ public final boolean isPrimaryKeyMember() {
+ return mPrimaryKey;
+ }
+
+ public final boolean isAlternateKeyMember() {
+ return mAlternateKey;
+ }
+
+ public final int getAliasCount() {
+ String[] aliases = mAliases;
+ return aliases == null ? 0 : aliases.length;
+ }
+
+ public final String getAlias(int index) {
+ String[] aliases = mAliases;
+ if (aliases == null) {
+ throw new IndexOutOfBoundsException();
+ } else {
+ return aliases[index];
+ }
+ }
+
+ public final String[] getAliases() {
+ String[] aliases = mAliases;
+ if (aliases == null) {
+ return new String[0];
+ } else {
+ return aliases.clone();
+ }
+ }
+
+ public final String getSequenceName() {
+ return mSequence;
+ }
+
+ public final boolean isIndependent() {
+ return mIndependent;
+ }
+
+ public final boolean isVersion() {
+ return mIsVersion;
+ }
+
+ public boolean isJoin() {
+ return false;
+ }
+
+ public Class<? extends Storable> getJoinedType() {
+ return null;
+ }
+
+ public int getJoinElementCount() {
+ return 0;
+ }
+
+ public StorableProperty<S> getInternalJoinElement(int index) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ @SuppressWarnings("unchecked")
+ public StorableProperty<S>[] getInternalJoinElements() {
+ return new StorableProperty[0];
+ }
+
+ public StorableProperty<?> getExternalJoinElement(int index) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ public StorableProperty<?>[] getExternalJoinElements() {
+ return new StorableProperty[0];
+ }
+
+ public boolean isQuery() {
+ return false;
+ }
+
+ public int getConstraintCount() {
+ StorablePropertyConstraint[] constraints = mConstraints;
+ return constraints == null ? 0 : constraints.length;
+ }
+
+ public StorablePropertyConstraint getConstraint(int index) {
+ StorablePropertyConstraint[] constraints = mConstraints;
+ if (constraints == null) {
+ throw new IndexOutOfBoundsException();
+ } else {
+ return constraints[index];
+ }
+ }
+
+ public StorablePropertyConstraint[] getConstraints() {
+ StorablePropertyConstraint[] constraints = mConstraints;
+ if (constraints == null) {
+ return new StorablePropertyConstraint[0];
+ } else {
+ return constraints.clone();
+ }
+ }
+
+ public StorablePropertyAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ public String toString() {
+ StringBuilder b = new StringBuilder();
+ try {
+ appendTo(b);
+ } catch (IOException e) {
+ // Not gonna happen
+ }
+ return b.toString();
+ }
+
+ public void appendTo(Appendable app) throws IOException {
+ app.append("StorableProperty {name=");
+ app.append(getName());
+ app.append(", type=");
+ app.append(TypeDesc.forClass(getType()).getFullName());
+ app.append(", enclosing=");
+ app.append(getEnclosingType().getName());
+ app.append('}');
+ }
+ }
+
+ private static final class JoinProperty<S extends Storable> extends SimpleProperty<S> {
+ private final Class<? extends Storable> mJoinedType;
+
+ // Just the names of the join properties, held here until properties
+ // are fully resolved. After properties are resolved, arrays are thrown away.
+ private String[] mInternalNames;
+ private String[] mExternalNames;
+
+ // Resolved join properties.
+ private StorableProperty<S>[] mInternal;
+ private StorableProperty<?>[] mExternal;
+
+ JoinProperty(BeanProperty property, Class<S> enclosing,
+ boolean nullable,
+ String[] aliases, StorablePropertyConstraint[] constraints,
+ StorablePropertyAdapter adapter,
+ String sequence, boolean independent,
+ Class<? extends Storable> joinedType,
+ String[] internal, String[] external) {
+ super(property, enclosing, nullable, false, false,
+ aliases, constraints, adapter, false, sequence, independent);
+ mJoinedType = joinedType;
+
+ int length = internal.length;
+ if (length != external.length) {
+ throw new IllegalArgumentException();
+ }
+
+ mInternalNames = internal;
+ mExternalNames = external;
+ }
+
+ public boolean isJoin() {
+ return true;
+ }
+
+ public Class<? extends Storable> getJoinedType() {
+ return mJoinedType;
+ }
+
+ public int getJoinElementCount() {
+ return mInternal.length;
+ }
+
+ public StorableProperty<S> getInternalJoinElement(int index) {
+ return mInternal[index];
+ }
+
+ public StorableProperty<S>[] getInternalJoinElements() {
+ return mInternal.clone();
+ }
+
+ public StorableProperty<?> getExternalJoinElement(int index) {
+ return mExternal[index];
+ }
+
+ public StorableProperty<?>[] getExternalJoinElements() {
+ return mExternal.clone();
+ }
+
+ public boolean isQuery() {
+ return getType() == Query.class;
+ }
+
+ /**
+ * 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());
+
+ 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;
+
+ if (isQuery()) {
+ primaryKeys = examine(getEnclosingType()).getPrimaryKeyProperties();
+ } else {
+ primaryKeys = joinedInfo.getPrimaryKeyProperties();
+ }
+
+ 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++;
+ }
+ }
+
+ mInternal = new StorableProperty[mInternalNames.length];
+ mExternal = new StorableProperty[mExternalNames.length];
+
+ // 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);
+ if (property == null) {
+ errorMessages.add
+ ("Cannot find internal join element: \"" +
+ getName() + "\" internally joins to property \"" +
+ internalName + '"');
+ continue;
+ }
+ if (property.isJoin()) {
+ errorMessages.add
+ ("Join properties cannot join to other join properties: \"" +
+ getName() + "\" internally joins to property \"" +
+ internalName + '"');
+ continue;
+ }
+ if (Lob.class.isAssignableFrom(property.getType())) {
+ errorMessages.add
+ ("Join properties cannot join to LOB properties: \"" +
+ getName() + "\" internally joins to LOB property \"" +
+ internalName + '"');
+ continue;
+ }
+ /* this check is too restrictive
+ if (property.isNullable() && !isNullable()) {
+ errorMessages.add
+ ("Join must be declared nullable since internal element " +
+ "is nullable: \"" + getName() +
+ "\" internally joins to nullable property \"" + internalName + '"');
+ }
+ */
+ mInternal[i] = property;
+ }
+
+ // Verify that external properties exist and are not themselves joins.
+
+ Map<String, ? extends StorableProperty<?>> externalProperties =
+ joinedInfo.getAllProperties();
+
+ for (int i=0; i<mExternalNames.length; i++) {
+ String externalName = mExternalNames[i];
+ StorableProperty property = externalProperties.get(externalName);
+ if (property == null) {
+ errorMessages.add
+ ("Cannot find external join element: \"" +
+ getName() + "\" externally joins to property \"" +
+ externalName + '"');
+ continue;
+ }
+ if (property.isJoin()) {
+ errorMessages.add
+ ("Join properties cannot join to other join properties: \"" +
+ getName() + "\" externally joins to property \"" +
+ externalName + '"');
+ continue;
+ }
+ if (Lob.class.isAssignableFrom(property.getType())) {
+ errorMessages.add
+ ("Join properties cannot join to LOB properties: \"" +
+ getName() + "\" externally joins to LOB property \"" +
+ externalName + '"');
+ continue;
+ }
+ if (property.getReadMethod() == null && getWriteMethod() != null) {
+ errorMessages.add
+ ("Join property cannot have a mutator if external property " +
+ "has no accessor: Mutator = \"" + getWriteMethod() +
+ "\", external property = \"" + property.getName() + '"');
+ continue;
+ }
+ mExternal[i] = property;
+ }
+
+ if (errorMessages.size() > 0) {
+ return;
+ }
+
+ // Verify that join types match type.
+ for (int i=0; i<mInternal.length; i++) {
+ StorableProperty internalProperty = getInternalJoinElement(i);
+ StorableProperty externalProperty = getExternalJoinElement(i);
+
+ if (!internalProperty.isNullable() && externalProperty.isNullable() &&
+ getWriteMethod() != null) {
+
+ errorMessages.add
+ ("Join property cannot have a mutator if internal property " +
+ "is required, but external property is nullable: Mutator = \"" +
+ getWriteMethod() +
+ "\", internal property = \"" + internalProperty.getName() +
+ "\", external property = \"" + externalProperty.getName() + '"');
+ }
+
+ Class internalClass = internalProperty.getType();
+ Class externalClass = externalProperty.getType();
+
+ if (internalClass == externalClass) {
+ continue;
+ }
+
+ TypeDesc internalType = TypeDesc.forClass(internalClass).toObjectType();
+ TypeDesc externalType = TypeDesc.forClass(externalClass).toObjectType();
+
+ if (internalType == externalType) {
+ continue;
+ }
+
+ compatibilityCheck: {
+ // Conversion to String, CharSequence, or Object is always
+ // allowed.
+ if (externalClass == String.class || externalClass == Object.class ||
+ externalClass == CharSequence.class) {
+ break compatibilityCheck;
+ }
+
+ // Allow internal type to be "narrower" than external type.
+
+ // (byte) ==> (Number | short | int | long)
+ // (byte | short) ==> (Number | int | long)
+ // (byte | short | int) ==> (Number | long)
+ // (float) ==> (Number | double)
+
+ TypeDesc primInternal = internalType.toPrimitiveType();
+ TypeDesc primExternal = externalType.toPrimitiveType();
+
+ if (primInternal != null) {
+ switch (primInternal.getTypeCode()) {
+ case TypeDesc.BYTE_CODE:
+ if (primExternal == null) {
+ if (externalType.toClass() == Number.class) {
+ break compatibilityCheck;
+ }
+ } else {
+ switch (primExternal.getTypeCode()) {
+ case TypeDesc.SHORT_CODE:
+ case TypeDesc.INT_CODE:
+ case TypeDesc.LONG_CODE:
+ break compatibilityCheck;
+ }
+ }
+ break;
+ case TypeDesc.SHORT_CODE:
+ if (primExternal == null) {
+ if (externalType.toClass() == Number.class) {
+ break compatibilityCheck;
+ }
+ } else {
+ switch (primExternal.getTypeCode()) {
+ case TypeDesc.INT_CODE:
+ case TypeDesc.LONG_CODE:
+ break compatibilityCheck;
+ }
+ }
+ break;
+ case TypeDesc.INT_CODE:
+ if (primExternal == null) {
+ if (externalType.toClass() == Number.class) {
+ break compatibilityCheck;
+ }
+ } else {
+ if (primExternal == TypeDesc.LONG) {
+ break compatibilityCheck;
+ }
+ }
+ break;
+ case TypeDesc.FLOAT_CODE:
+ if (primExternal == null) {
+ if (externalType.toClass() == Number.class) {
+ break compatibilityCheck;
+ }
+ } else {
+ if (primExternal == TypeDesc.DOUBLE) {
+ break compatibilityCheck;
+ }
+ }
+ break;
+ }
+ }
+
+ errorMessages.add
+ ("Join property internal/external type mismatch for \"" +
+ getName() + "\": internal join \"" + getInternalJoinElement(i).getName() +
+ "\" is of type \"" + getInternalJoinElement(i).getType() +
+ "\" and external join \"" + getExternalJoinElement(i).getName() +
+ "\" is of type \"" + getExternalJoinElement(i).getType() + '"');
+ continue;
+ }
+
+ // If this point is reached, then types differ, but they are
+ // compatible. Still, a mutator on this join property is not
+ // allowed due to the difference.
+
+ if (getWriteMethod() != null) {
+ errorMessages.add
+ ("Join property cannot have a mutator if external type cannot " +
+ "be reliably converted to internal type: Mutator = \"" +
+ getWriteMethod() + "\", internal join \"" +
+ getInternalJoinElement(i).getName() +
+ "\" is of type \"" + getInternalJoinElement(i).getType() +
+ "\" and external join \"" + getExternalJoinElement(i).getName() +
+ "\" is of type \"" + getExternalJoinElement(i).getType() + '"');
+ }
+ }
+
+ if (errorMessages.size() > 0) {
+ return;
+ }
+
+ // Test which keys of joined object are specified.
+
+ // Create a copy of all the primary keys of joined object.
+ Set<StorableProperty> primaryKeys =
+ 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));
+ }
+
+ // Do similar test for alternate keys.
+
+ int altKeyCount = joinedInfo.getAlternateKeyCount();
+ List<Set<StorableProperty>> altKeys =
+ new ArrayList<Set<StorableProperty>>(altKeyCount);
+
+ altKeyScan:
+ for (int i=0; i<altKeyCount; i++) {
+ Set<StorableProperty> altKey = new HashSet<StorableProperty>();
+
+ for (OrderedProperty op : joinedInfo.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());
+ }
+
+ altKeys.add(altKey);
+
+ // Remove external properties from the alternate key set.
+ for (int j=0; j<mInternal.length; j++) {
+ altKey.remove(getExternalJoinElement(j));
+ }
+ }
+
+ if (isQuery()) {
+ // Key of joined object must not be completely specified.
+
+ if (primaryKeys.size() <= 0) {
+ errorMessages.add
+ ("Join property \"" + getName() +
+ "\" completely specifies primary key of joined object; " +
+ "consider declaring the property type as just " +
+ getJoinedType().getName());
+ }
+
+ for (Set<StorableProperty> altKey : altKeys) {
+ if (altKey.size() <= 0) {
+ errorMessages.add
+ ("Join property \"" + getName() +
+ "\" completely specifies an alternate key of joined object; " +
+ "consider declaring the property type as just " +
+ getJoinedType().getName());
+ break;
+ }
+ }
+ } else {
+ // Key of joined object must be completely specified.
+
+ fullKeyCheck:
+ {
+ if (primaryKeys.size() <= 0) {
+ break fullKeyCheck;
+ }
+
+ for (Set<StorableProperty> altKey : altKeys) {
+ if (altKey.size() <= 0) {
+ break fullKeyCheck;
+ }
+ }
+
+ errorMessages.add
+ ("Join property \"" + getName() +
+ "\" doesn't completely specify any of key of joined object; consider " +
+ "declaring the property type as Query<" +
+ getJoinedType().getName() + '>');
+ }
+ }
+
+ if (errorMessages.size() == 0) {
+ // No errors, throw away names arrays.
+ mInternalNames = null;
+ mExternalNames = null;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/info/StorableKey.java b/src/main/java/com/amazon/carbonado/info/StorableKey.java
new file mode 100644
index 0000000..a6e5582
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/StorableKey.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.util.Set;
+
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.util.Appender;
+
+/**
+ * Represents a primary or alternate key of a specific {@link Storable} type.
+ *
+ * @author Brian S O'Neill
+ * @see StorableIntrospector
+ */
+public interface StorableKey<S extends Storable> extends Appender {
+ /**
+ * Returns true if this key is primary, false if an alternate.
+ */
+ boolean isPrimary();
+
+ /**
+ * Returns all the properties of the key in a properly ordered,
+ * unmodifiable set.
+ */
+ Set<? extends OrderedProperty<S>> getProperties();
+}
diff --git a/src/main/java/com/amazon/carbonado/info/StorableProperty.java b/src/main/java/com/amazon/carbonado/info/StorableProperty.java
new file mode 100644
index 0000000..c9cc8e1
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/StorableProperty.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.lang.reflect.Method;
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.util.Appender;
+
+/**
+ * Contains all the metadata describing a property of a specific {@link Storable} type.
+ *
+ * @author Brian S O'Neill
+ * @see StorableIntrospector
+ */
+public interface StorableProperty<S extends Storable> extends Appender {
+ /**
+ * Returns the name of this property.
+ */
+ String getName();
+
+ /**
+ * Returns the type of this property.
+ */
+ Class<?> getType();
+
+ /**
+ * Returns the enclosing type of this property.
+ */
+ Class<S> getEnclosingType();
+
+ /**
+ * Returns a no-arg method used to read the property value, or null if
+ * reading is not allowed. The return type matches the type of this
+ * property.
+ */
+ Method getReadMethod();
+
+ /**
+ * Returns the name of the read method, even if no read method was actually
+ * declared. That is, this method always returns a method name, but
+ * getReadMethod may still return null.
+ */
+ String getReadMethodName();
+
+ /**
+ * Returns a one argument method used to write the property value, or null
+ * if writing is not allowed. The first argument is the value to set, which
+ * is the type of this property.
+ */
+ Method getWriteMethod();
+
+ /**
+ * Returns the name of the write method, even if no write method was
+ * actually declared. That is, this method always returns a method name,
+ * but getWriteMethod may still return null.
+ */
+ String getWriteMethodName();
+
+ /**
+ * Returns true if this property can be null.
+ *
+ * @see com.amazon.carbonado.Nullable
+ */
+ boolean isNullable();
+
+ /**
+ * Returns true if this property is a member of a primary key.
+ *
+ * @see com.amazon.carbonado.PrimaryKey
+ */
+ boolean isPrimaryKeyMember();
+
+ /**
+ * Returns true if this property is a member of an alternate key.
+ *
+ * @see com.amazon.carbonado.AlternateKeys
+ */
+ boolean isAlternateKeyMember();
+
+ /**
+ * Returns the count of aliases for this property.
+ *
+ * @see com.amazon.carbonado.Alias
+ */
+ int getAliasCount();
+
+ /**
+ * Returns a specific alias for this property.
+ *
+ * @see com.amazon.carbonado.Alias
+ */
+ String getAlias(int index) throws IndexOutOfBoundsException;
+
+ /**
+ * Returns a new array with all the alias names in it.
+ *
+ * @see com.amazon.carbonado.Alias
+ */
+ String[] getAliases();
+
+ /**
+ * Returns true if this property is joined to another Storable.
+ *
+ * @see com.amazon.carbonado.Join
+ */
+ boolean isJoin();
+
+ /**
+ * Returns the type of property this is joined to, or null if not joined.
+ */
+ Class<? extends Storable> getJoinedType();
+
+ /**
+ * Returns the count of properties that participate in this property's
+ * join. If this property is not a join, then zero is returned.
+ */
+ int getJoinElementCount();
+
+ /**
+ * Returns a specific property in this property's class that participates
+ * in the join.
+ */
+ StorableProperty<S> getInternalJoinElement(int index) throws IndexOutOfBoundsException;
+
+ /**
+ * Returns a new array with all the internal join elements in it.
+ */
+ StorableProperty<S>[] getInternalJoinElements();
+
+ /**
+ * Returns a specific property in the joined class that participates in the
+ * join.
+ */
+ StorableProperty<?> getExternalJoinElement(int index) throws IndexOutOfBoundsException;
+
+ /**
+ * Returns a new array with all the external join elements in it.
+ */
+ StorableProperty<?>[] getExternalJoinElements();
+
+ /**
+ * Returns true if this property is a query, which also implies that it is
+ * a join property.
+ *
+ * @see com.amazon.carbonado.Query
+ */
+ boolean isQuery();
+
+ /**
+ * Returns the count of constraints for this property.
+ */
+ int getConstraintCount();
+
+ /**
+ * Returns a specific constraint for this property.
+ */
+ StorablePropertyConstraint getConstraint(int index) throws IndexOutOfBoundsException;
+
+ /**
+ * Returns a new array with all the constraints in it.
+ */
+ StorablePropertyConstraint[] getConstraints();
+
+ /**
+ * Returns this property's adapter, or null if none.
+ */
+ StorablePropertyAdapter getAdapter();
+
+ /**
+ * Returns the property's sequence name, or null if none.
+ */
+ String getSequenceName();
+
+ /**
+ * Returns true if this property is the designated version number for the
+ * Storable.
+ *
+ * @see com.amazon.carbonado.Version
+ */
+ boolean isVersion();
+
+ /**
+ * Returns true if this property has been designated independent.
+ *
+ * @see com.amazon.carbonado.Independent
+ */
+ boolean isIndependent();
+
+ String toString();
+}
diff --git a/src/main/java/com/amazon/carbonado/info/StorablePropertyAdapter.java b/src/main/java/com/amazon/carbonado/info/StorablePropertyAdapter.java
new file mode 100644
index 0000000..9ad5553
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/StorablePropertyAdapter.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.cojen.util.BeanProperty;
+
+import com.amazon.carbonado.adapter.AdapterDefinition;
+import com.amazon.carbonado.util.ThrowUnchecked;
+
+/**
+ * Information about an {@link com.amazon.carbonado.adapter.AdapterDefinition
+ * adapter} annotation applied to a property.
+ *
+ * @author Brian S O'Neill
+ */
+public class StorablePropertyAdapter {
+ static Class getEnclosingType(BeanProperty property) {
+ Method m = property.getReadMethod();
+ if (m == null) {
+ m = property.getWriteMethod();
+ }
+ return m.getDeclaringClass();
+ }
+
+ /**
+ * @return null if not found
+ */
+ static Class findAdapterClass(Class<? extends Annotation> annotationType) {
+ AdapterDefinition ad = annotationType.getAnnotation(AdapterDefinition.class);
+ if (ad == null) {
+ return null;
+ }
+
+ Class adapterClass = ad.implementation();
+
+ if (adapterClass == void.class) {
+ // Magic value meaning "use default", which is an inner class of
+ // the annotation.
+
+ adapterClass = null;
+
+ // Search for inner class named "Adapter".
+ Class[] innerClasses = annotationType.getClasses();
+ for (Class c : innerClasses) {
+ if ("Adapter".equals(c.getSimpleName())) {
+ adapterClass = c;
+ break;
+ }
+ }
+ }
+
+ return adapterClass;
+ }
+
+ /**
+ * @return empty array if none found
+ */
+ @SuppressWarnings("unchecked")
+ static Method[] findAdaptMethods(Class<?> propertyType, Class<?> adapterClass) {
+ List<Method> adaptMethods = new ArrayList<Method>();
+
+ for (Method adaptMethod : adapterClass.getMethods()) {
+ if (!adaptMethod.getName().startsWith("adapt")) {
+ continue;
+ }
+ Class<?> toType = adaptMethod.getReturnType();
+ if (toType == void.class) {
+ continue;
+ }
+ Class<?>[] paramTypes = adaptMethod.getParameterTypes();
+ Class<?> fromType;
+ if (paramTypes.length != 1) {
+ continue;
+ } else {
+ fromType = paramTypes[0];
+ }
+
+ if (!fromType.isAssignableFrom(propertyType) &&
+ !propertyType.isAssignableFrom(toType)) {
+ continue;
+ }
+
+ adaptMethods.add(adaptMethod);
+ }
+
+ return (Method[]) adaptMethods.toArray(new Method[adaptMethods.size()]);
+ }
+
+ private final Class mEnclosingType;
+ private final String mPropertyName;
+ private final StorablePropertyAnnotation mAnnotation;
+ private final Class[] mStorageTypePreferences;
+ private final Constructor mConstructor;
+ private final Method[] mAdaptMethods;
+
+ private transient Object mAdapterInstance;
+
+ /**
+ * Construct a generic StorablePropertyAdapter instance not attached to a
+ * storable definition. Call {@link StorableProperty#getAdapter} to gain
+ * access to adapter information on actual storable definitions.
+ *
+ * @param propertyName name of property with adapter
+ * @param propertyType declated type of adapted property
+ * @param adapterType adapter type
+ * @throws IllegalArgumentException if adapterType is not an adapter
+ * definition.
+ */
+ public StorablePropertyAdapter(String propertyName,
+ Class<?> propertyType,
+ Class<? extends Annotation> adapterType)
+ {
+ this(null, propertyName, propertyType, null, adapterType);
+ }
+
+ /**
+ * Used by StorableIntrospector.
+ *
+ * @see StorableIntrospector
+ */
+ StorablePropertyAdapter(BeanProperty property,
+ StorablePropertyAnnotation annotation,
+ AdapterDefinition ad,
+ Constructor ctor,
+ Method[] adaptMethods)
+ {
+ mEnclosingType = getEnclosingType(property);
+ mPropertyName = property.getName();
+ mAnnotation = annotation;
+ mConstructor = ctor;
+ mAdaptMethods = adaptMethods;
+
+ Class[] storageTypePreferences = ad.storageTypePreferences();
+ if (storageTypePreferences != null && storageTypePreferences.length == 0) {
+ storageTypePreferences = null;
+ }
+ mStorageTypePreferences = storageTypePreferences;
+ }
+
+ /**
+ * Used with automatic adapter selection.
+ *
+ * @see AutomaticAdapterSeletor
+ */
+ StorablePropertyAdapter(BeanProperty property,
+ StorablePropertyAnnotation annotation)
+ {
+ this(getEnclosingType(property),
+ property.getName(),
+ property.getType(),
+ annotation,
+ annotation.getAnnotationType());
+ }
+
+ private StorablePropertyAdapter(Class enclosingType,
+ String propertyName,
+ Class<?> propertyType,
+ StorablePropertyAnnotation annotation,
+ Class<? extends Annotation> adapterType)
+ {
+ mEnclosingType = enclosingType;
+ mPropertyName = propertyName;
+ mAnnotation = annotation;
+
+ AdapterDefinition ad = adapterType.getAnnotation(AdapterDefinition.class);
+ if (ad == null) {
+ throw new IllegalArgumentException();
+ }
+
+ Class[] storageTypePreferences = ad.storageTypePreferences();
+ if (storageTypePreferences != null && storageTypePreferences.length == 0) {
+ storageTypePreferences = null;
+ }
+ mStorageTypePreferences = storageTypePreferences;
+
+ Class adapterClass = findAdapterClass(adapterType);
+ if (adapterClass == null) {
+ throw new IllegalArgumentException();
+ }
+
+ try {
+ mConstructor = adapterClass.getConstructor(Class.class, String.class, adapterType);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException(e);
+ }
+
+ mAdaptMethods = findAdaptMethods(propertyType, adapterClass);
+ if (mAdaptMethods.length == 0) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Returns the annotation that applied this adapter, or null if none.
+ */
+ public StorablePropertyAnnotation getAnnotation() {
+ return mAnnotation;
+ }
+
+ /**
+ * Returns the constructor for the adapter class. It has the signature
+ * <code>(Class type, String propertyName, <i>Annotation</i>)</code>, where
+ * <i>Annotation</i> is the fully resolved annotation.
+ */
+ public Constructor getAdapterConstructor() {
+ return mConstructor;
+ }
+
+ /**
+ * Returns an instance of the adapter, for which an adapt method is applied to.
+ */
+ public Object getAdapterInstance() {
+ if (mAdapterInstance == null) {
+ try {
+ mAdapterInstance = mConstructor.newInstance
+ (mEnclosingType, mPropertyName, mAnnotation.getAnnotation());
+ } catch (Exception e) {
+ ThrowUnchecked.fireFirstDeclaredCause(e);
+ }
+ }
+ return mAdapterInstance;
+ }
+
+ /**
+ * Returns the adapter's storage type preferences.
+ *
+ * @see com.amazon.carbonado.adapter.AdapterDefinition#storageTypePreferences
+ */
+ public Class[] getStorageTypePreferences() {
+ if (mStorageTypePreferences == null) {
+ return new Class[0];
+ }
+ return mStorageTypePreferences.clone();
+ }
+
+ /**
+ * Returns an adapt method that supports the given conversion, or null if
+ * none.
+ */
+ @SuppressWarnings("unchecked")
+ public Method findAdaptMethod(Class from, Class to) {
+ Method[] methods = mAdaptMethods;
+ List<Method> candidates = new ArrayList<Method>(methods.length);
+ for (int i=methods.length; --i>=0; ) {
+ Method method = methods[i];
+ if (to.isAssignableFrom(method.getReturnType()) &&
+ method.getParameterTypes()[0].isAssignableFrom(from)) {
+ candidates.add(method);
+ }
+ }
+ reduceCandidates(candidates, to);
+ if (candidates.size() == 0) {
+ return null;
+ }
+ return candidates.get(0);
+ }
+
+ /**
+ * Returns all the adapt methods that convert from the given type.
+ */
+ public Method[] findAdaptMethodsFrom(Class from) {
+ Method[] methods = mAdaptMethods;
+ List<Method> candidates = new ArrayList<Method>(methods.length);
+ for (int i=methods.length; --i>=0; ) {
+ Method method = methods[i];
+ if (method.getParameterTypes()[0].isAssignableFrom(from)) {
+ candidates.add(method);
+ }
+ }
+ return (Method[]) candidates.toArray(new Method[candidates.size()]);
+ }
+
+ /**
+ * Returns all the adapt methods that convert to the given type.
+ */
+ @SuppressWarnings("unchecked")
+ public Method[] findAdaptMethodsTo(Class to) {
+ Method[] methods = mAdaptMethods;
+ List<Method> candidates = new ArrayList<Method>(methods.length);
+ for (int i=methods.length; --i>=0; ) {
+ Method method = methods[i];
+ if (to.isAssignableFrom(method.getReturnType())) {
+ candidates.add(method);
+ }
+ }
+ reduceCandidates(candidates, to);
+ return (Method[]) candidates.toArray(new Method[candidates.size()]);
+ }
+
+ /**
+ * Returns the count of all defined adapt methods.
+ */
+ public int getAdaptMethodCount() {
+ return mAdaptMethods.length;
+ }
+
+ /**
+ * Returns a specific adapt method.
+ */
+ public Method getAdaptMethod(int index) throws IndexOutOfBoundsException {
+ return mAdaptMethods[index];
+ }
+
+ /**
+ * Returns a new array with all the adapt methods in it.
+ */
+ public Method[] getAdaptMethods() {
+ return mAdaptMethods.clone();
+ }
+
+ private void reduceCandidates(List<Method> candidates, Class to) {
+ if (candidates.size() <= 1) {
+ // Shortcut.
+ return;
+ }
+
+ // Map "from" type to all methods that convert from it. When reduced,
+ // the list lengths are one.
+ Map<Class, List<Method>> fromMap = new LinkedHashMap<Class, List<Method>>();
+
+ for (Method method : candidates) {
+ Class from = method.getParameterTypes()[0];
+ List<Method> matches = fromMap.get(from);
+ if (matches == null) {
+ matches = new ArrayList<Method>();
+ fromMap.put(from, matches);
+ }
+ matches.add(method);
+ }
+
+ candidates.clear();
+
+ for (List<Method> matches : fromMap.values()) {
+ Method best = null;
+ int bestDistance = Integer.MAX_VALUE;
+ for (Method method : matches) {
+ int distance = distance(method.getReturnType(), to);
+ if (best == null || distance < bestDistance) {
+ best = method;
+ bestDistance = distance;
+ }
+ }
+ candidates.add(best);
+ }
+ }
+
+ private static int distance(Class from, Class to) {
+ int distance = 0;
+ while (from != to) {
+ from = from.getSuperclass();
+ if (from == null) {
+ return Integer.MAX_VALUE;
+ }
+ distance++;
+ }
+ return distance;
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/info/StorablePropertyAnnotation.java b/src/main/java/com/amazon/carbonado/info/StorablePropertyAnnotation.java
new file mode 100644
index 0000000..9f218c8
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/StorablePropertyAnnotation.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+
+/**
+ * Information about a custom annotation applied to a property.
+ *
+ * @author Brian S O'Neill
+ */
+public class StorablePropertyAnnotation {
+ private final Annotation mAnnotation;
+ private final Class<? extends Annotation> mAnnotationType;
+ private final Method mMethod;
+
+ /**
+ * Use this constructor if an annotation was actually defined.
+ *
+ * @param annotation annotation on method
+ * @param method method with annotation
+ */
+ public StorablePropertyAnnotation(Annotation annotation, Method method) {
+ if (annotation == null || method == null) {
+ throw new IllegalArgumentException();
+ }
+ mAnnotation = annotation;
+ mAnnotationType = annotation.annotationType();
+ mMethod = method;
+ }
+
+ /**
+ * Use this constructor if an annotation was not defined, but instead is
+ * being automatically applied.
+ *
+ * @param annotationType annotation type on method
+ * @param method method with annotation
+ */
+ public StorablePropertyAnnotation(Class<? extends Annotation> annotationType, Method method) {
+ if (annotationType == null || method == null) {
+ throw new IllegalArgumentException();
+ }
+ mAnnotation = method.getAnnotation(annotationType);
+ mAnnotationType = annotationType;
+ mMethod = method;
+ }
+
+ /**
+ * Returns the actual annotation instance, which may be null if annotation
+ * was automatically applied.
+ */
+ public Annotation getAnnotation() {
+ return mAnnotation;
+ }
+
+ /**
+ * Returns the type of annotation that was applied to the property method.
+ */
+ public Class<? extends Annotation> getAnnotationType() {
+ return mAnnotationType;
+ }
+
+ /**
+ * Returns the method that has the annotation.
+ */
+ public Method getAnnotatedMethod() {
+ return mMethod;
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/info/StorablePropertyConstraint.java b/src/main/java/com/amazon/carbonado/info/StorablePropertyConstraint.java
new file mode 100644
index 0000000..4736df2
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/StorablePropertyConstraint.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.info;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+
+/**
+ * Information about a {@link com.amazon.carbonado.constraint.ConstraintDefinition
+ * constraint} annotation applied to a property.
+ *
+ * @author Brian S O'Neill
+ */
+public class StorablePropertyConstraint {
+ private final StorablePropertyAnnotation mAnnotation;
+ private final Constructor mConstructor;
+ private final Method mConstrainMethod;
+
+ StorablePropertyConstraint(StorablePropertyAnnotation annotation,
+ Constructor ctor,
+ Method constrainMethod)
+ {
+ mAnnotation = annotation;
+ mConstructor = ctor;
+ mConstrainMethod = constrainMethod;
+ }
+
+ /**
+ * Returns the annotation that applied this constraint.
+ */
+ public StorablePropertyAnnotation getAnnotation() {
+ return mAnnotation;
+ }
+
+ /**
+ * Returns the constructor for the constraint class. It has the signature
+ * <code>(Class type, String propertyName, <i>Annotation</i>)</code>, where
+ * <i>Annotation</i> is the fully resolved annotation.
+ */
+ public Constructor getConstraintConstructor() {
+ return mConstructor;
+ }
+
+ /**
+ * Returns the best matching property checking method in the validator.
+ */
+ public Method getConstrainMethod() {
+ return mConstrainMethod;
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/info/package-info.java b/src/main/java/com/amazon/carbonado/info/package-info.java
new file mode 100644
index 0000000..7ae6ede
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/info/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Introspection support for Storables.
+ *
+ * @see com.amazon.carbonado.info.StorableIntrospector
+ */
+package com.amazon.carbonado.info;