From edcd84a1cb3c2009b0596939f30cee35028afcfc Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Wed, 30 Aug 2006 01:52:10 +0000 Subject: Add introspection support --- .../carbonado/info/AutomaticAdapterSelector.java | 73 + .../com/amazon/carbonado/info/ChainedProperty.java | 310 ++++ .../java/com/amazon/carbonado/info/Direction.java | 67 + .../com/amazon/carbonado/info/OrderedProperty.java | 182 ++ .../com/amazon/carbonado/info/StorableIndex.java | 641 +++++++ .../com/amazon/carbonado/info/StorableInfo.java | 129 ++ .../carbonado/info/StorableIntrospector.java | 1949 ++++++++++++++++++++ .../com/amazon/carbonado/info/StorableKey.java | 43 + .../amazon/carbonado/info/StorableProperty.java | 206 +++ .../carbonado/info/StorablePropertyAdapter.java | 382 ++++ .../carbonado/info/StorablePropertyAnnotation.java | 86 + .../carbonado/info/StorablePropertyConstraint.java | 66 + .../com/amazon/carbonado/info/package-info.java | 24 + 13 files changed, 4158 insertions(+) create mode 100644 src/main/java/com/amazon/carbonado/info/AutomaticAdapterSelector.java create mode 100644 src/main/java/com/amazon/carbonado/info/ChainedProperty.java create mode 100644 src/main/java/com/amazon/carbonado/info/Direction.java create mode 100644 src/main/java/com/amazon/carbonado/info/OrderedProperty.java create mode 100644 src/main/java/com/amazon/carbonado/info/StorableIndex.java create mode 100644 src/main/java/com/amazon/carbonado/info/StorableInfo.java create mode 100644 src/main/java/com/amazon/carbonado/info/StorableIntrospector.java create mode 100644 src/main/java/com/amazon/carbonado/info/StorableKey.java create mode 100644 src/main/java/com/amazon/carbonado/info/StorableProperty.java create mode 100644 src/main/java/com/amazon/carbonado/info/StorablePropertyAdapter.java create mode 100644 src/main/java/com/amazon/carbonado/info/StorablePropertyAnnotation.java create mode 100644 src/main/java/com/amazon/carbonado/info/StorablePropertyConstraint.java create mode 100644 src/main/java/com/amazon/carbonado/info/package-info.java (limited to 'src') 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 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 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 ChainedProperty get(StorableProperty prime) { + return (ChainedProperty) cCanonical.put(new ChainedProperty(prime, null)); + } + + /** + * Returns a canonical instance. + * + * @throws IllegalArgumentException if prime is null or if chained + * properties are not formed properly + */ + @SuppressWarnings("unchecked") + public static ChainedProperty get(StorableProperty prime, + StorableProperty... chain) { + return (ChainedProperty) cCanonical.put(new ChainedProperty(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 ChainedProperty parse(StorableInfo 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 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> chain = new ArrayList>(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) type); + Map> 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 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 prime, StorableProperty[] chain) { + if (prime == null) { + throw new IllegalArgumentException(); + } + mPrime = prime; + mChain = (chain == null || chain.length == 0) ? null : chain.clone(); + } + + public StorableProperty 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 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 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 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 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 implements Appender { + static WeakCanonicalSet cCanonical = new WeakCanonicalSet(); + + /** + * Returns a canonical instance. + * + * @throws IllegalArgumentException if property is null + */ + public static OrderedProperty get(StorableProperty property, + Direction direction) { + return get(ChainedProperty.get(property), direction); + } + + /** + * Returns a canonical instance. + * + * @throws IllegalArgumentException if property is null + */ + @SuppressWarnings("unchecked") + public static OrderedProperty get(ChainedProperty property, + Direction direction) { + return (OrderedProperty) cCanonical.put(new OrderedProperty(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 OrderedProperty parse(StorableInfo 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 OrderedProperty parse(StorableInfo 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 mProperty; + private final Direction mDirection; + + private OrderedProperty(ChainedProperty property, Direction direction) { + if (property == null) { + throw new IllegalArgumentException(); + } + mProperty = property; + mDirection = direction == null ? Direction.UNSPECIFIED : direction; + } + + public ChainedProperty getChainedProperty() { + return mProperty; + } + + public Direction getDirection() { + return mDirection; + } + + public OrderedProperty reverse() { + if (mDirection == Direction.UNSPECIFIED) { + return this; + } + return get(mProperty, mDirection.reverse()); + } + + public OrderedProperty 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 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 StorableIndex parseNameDescriptor + (String desc, StorableInfo info) + throws IllegalArgumentException + { + String name = info.getStorableType().getName(); + if (!desc.startsWith(name)) { + throw new IllegalArgumentException("Descriptor starts with wrong type name: \"" + + desc + "\", \"" + name + '"'); + } + + Map> allProperties = info.getAllProperties(); + + List> properties = new ArrayList>(); + List directions = new ArrayList(); + 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 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 index = new StorableIndex + (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[] 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[] 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[] 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[] 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[] 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 key, Direction direction) { + if (key == null) { + throw new IllegalArgumentException(); + } + Set> 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 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[] 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 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 type, IndexInfo indexInfo) { + if (indexInfo == null) { + throw new IllegalArgumentException(); + } + + Map> 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 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 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 getProperty(int index) { + return mProperties[index]; + } + + /** + * Returns a new array with all the properties in it. + */ + public StorableProperty[] 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 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[] getOrderedProperties() { + OrderedProperty[] 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 unique(boolean unique) { + if (unique == mUnique) { + return this; + } + return new StorableIndex(mProperties, mDirections, unique, mClustered, false); + } + + /** + * Returns a StorableIndex instance which is clustered or not. + */ + public StorableIndex clustered(boolean clustered) { + if (clustered == mClustered) { + return this; + } + return new StorableIndex(mProperties, mDirections, mUnique, clustered, false); + } + + /** + * Returns a StorableIndex instance with all the properties reversed. + */ + public StorableIndex 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(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 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(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 addProperty(StorableProperty property, Direction direction) { + for (int i=mProperties.length; --i>=0; ) { + if (mProperties[i].equals(property)) { + return this; + } + } + + StorableProperty[] 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(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 uniquify(StorableKey key) { + if (key == null) { + throw new IllegalArgumentException(); + } + + if (isUnique()) { + return this; + } + + StorableIndex index = this; + + for (OrderedProperty 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: + *
<storable type>~<attr><+|-|~><property><+|-|~><property>...
+ * Attr is "U" for a unique index, "N" for a non-unique index. + * + *

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 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 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 { + /** + * 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 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> 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> 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> getDataProperties(); + + /** + * Returns the designated version property, or null if none. + */ + StorableProperty getVersionProperty(); + + /** + * Returns the primary key for the Storable, never null. + */ + StorableKey getPrimaryKey(); + + /** + * Returns the count of alternate keys for the Storable. + */ + int getAlternateKeyCount(); + + /** + * Returns a specific alternate key for the Storable. + */ + StorableKey getAlternateKey(int index); + + /** + * Returns a new array with all the alternate keys in it. + */ + StorableKey[] 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 getIndex(int index) throws IndexOutOfBoundsException; + + /** + * Returns a new array with all the indexes in it. + */ + StorableIndex[] 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, Reference>> 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 StorableInfo examine(Class type) { + if (type == null) { + throw new IllegalArgumentException("Storable type must not be null"); + } + synchronized (cCache) { + StorableInfo info; + Reference> ref = cCache.get(type); + if (ref != null) { + info = (StorableInfo) ref.get(); + if (info != null) { + return info; + } + } + + List errorMessages = new ArrayList(); + + // Pull these annotations out but finish processing later. + List primaryKeyProps; + List> alternateKeyProps; + List> 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> properties = + examineProperties(type, primaryKeyProps, alternateKeyProps); + + // Resolve keys and indexes. + + StorableKey primaryKey; + { + Set> propSet = + resolveKey(errorMessages, type, properties, "primary key", primaryKeyProps); + primaryKey = new SKey(true, propSet); + } + + StorableKey[] alternateKeys; + { + alternateKeys = new StorableKey[alternateKeyProps.size()]; + int i = 0; + for (List nameAndDirs : alternateKeyProps) { + Set> propSet = + resolveKey(errorMessages, type, properties, "alternate key", nameAndDirs); + alternateKeys[i++] = new SKey(false, propSet); + } + } + + StorableIndex[] indexes; + { + indexes = new StorableIndex[indexProps.size()]; + int i = 0; + for (List nameAndDirs : indexProps) { + int errorCount = errorMessages.size(); + Set> 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[] propArray = new OrderedProperty[propSet.size()]; + propSet.toArray(propArray); + indexes[i] = new StorableIndex(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> arrangedProperties = + new LinkedHashMap>(); + + // First dump in primary key properties, in their proper order. + for (OrderedProperty orderedProp : primaryKey.getProperties()) { + StorableProperty prop = orderedProp.getChainedProperty().getPrimeProperty(); + arrangedProperties.put(prop.getName(), prop); + } + + // Gather all remaining properties, and then sort them. + List> nonPkProperties = new ArrayList>(); + + for (StorableProperty prop : properties.values()) { + if (!arrangedProperties.containsKey(prop.getName())) { + nonPkProperties.add(prop); + } + } + + Collections.sort(nonPkProperties, + BeanComparator.forClass(StorableProperty.class).orderBy("name")); + + for (StorableProperty 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(type, aliases, indexes, properties, + primaryKey, alternateKeys, + type.getAnnotation(Independent.class) != null); + cCache.put(type, new SoftReference>(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> gatherListProperties(List errorMessages, + Indexes indexes, + AlternateKeys keys, + PrimaryKey primaryKey) + { + List> listlist = new ArrayList>(); + + 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 errorMessages, + String listName, + String[] propNames, + List> listlist) + { + int length = propNames.length; + List nameAndDirs = new ArrayList(length); + + for (int i=0; i 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 Set> + resolveKey(List errorMessages, + Class type, + Map> properties, + String elementName, + List nameAndDirs) + { + Set> orderedProps = new LinkedHashSet>(); + + for (NameAndDirection nameAndDir : nameAndDirs) { + String name = nameAndDir.name; + if (name.indexOf('.') > 0) { + errorMessages.add("Chained property not allowed in " + elementName + ": " + name); + continue; + } + StorableProperty 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 Map> + examineProperties(Class type, + List primaryKeyProps, + List> 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 errorMessages = new ArrayList(); + + 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 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 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 pkPropertyNames, altKeyPropertyNames; + { + pkPropertyNames = new HashSet(); + altKeyPropertyNames = new HashSet(); + for (NameAndDirection nameAndDir : primaryKeyProps) { + pkPropertyNames.add(nameAndDir.name); + } + for (List 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> properties = + new HashMap>(); + + // 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 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 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 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 StorableProperty makeStorableProperty + (List errorMessages, + BeanProperty property, + Class 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 + (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 + (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 errorMessages) + { + Annotation[] allAnnotations = method.getAnnotations(); + if (allAnnotations.length == 0) { + return null; + } + + List list = new ArrayList(); + + for (Annotation annotation : allAnnotations) { + Class 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 errorMessages) + { + Annotation[] allAnnotations = method.getAnnotations(); + if (allAnnotations.length == 0) { + return null; + } + + List list = new ArrayList(); + + for (Annotation annotation : allAnnotations) { + Class 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 implements StorableInfo { + private final Class mType; + private final String[] mAliases; + private final StorableIndex[] mIndexes; + private final Map> mAllProperties; + private final StorableKey mPrimaryKey; + private final StorableKey[] mAltKeys; + private final boolean mIndependent; + + private transient String mName; + private transient Map> mPrimaryKeyProperties; + private transient Map> mDataProperties; + private transient StorableProperty mVersionProperty; + + Info(Class type, String[] aliases, StorableIndex[] indexes, + Map> properties, + StorableKey primaryKey, + StorableKey[] 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 getStorableType() { + return mType; + } + + public Map> getAllProperties() { + return mAllProperties; + } + + public Map> getPrimaryKeyProperties() { + if (mPrimaryKeyProperties == null) { + Set> pkSet = mPrimaryKey.getProperties(); + Map> pkProps = + new LinkedHashMap>(pkSet.size()); + for (OrderedProperty prop : pkSet) { + StorableProperty prime = prop.getChainedProperty().getPrimeProperty(); + pkProps.put(prime.getName(), prime); + } + mPrimaryKeyProperties = Collections.unmodifiableMap(pkProps); + } + return mPrimaryKeyProperties; + } + + public Map> getDataProperties() { + if (mDataProperties == null) { + Map> dataProps = + new LinkedHashMap>(mAllProperties.size()); + for (Map.Entry> entry : mAllProperties.entrySet()) { + StorableProperty property = entry.getValue(); + if (!property.isPrimaryKeyMember() && !property.isJoin()) { + dataProps.put(entry.getKey(), property); + } + } + mDataProperties = Collections.unmodifiableMap(dataProps); + } + return mDataProperties; + } + + public StorableProperty getVersionProperty() { + if (mVersionProperty == null) { + for (StorableProperty property : mAllProperties.values()) { + if (property.isVersion()) { + mVersionProperty = property; + break; + } + } + } + return mVersionProperty; + } + + public StorableKey getPrimaryKey() { + return mPrimaryKey; + } + + public int getAlternateKeyCount() { + StorableKey[] keys = mAltKeys; + return keys == null ? 0 : keys.length; + } + + public StorableKey getAlternateKey(int index) { + StorableKey[] keys = mAltKeys; + if (keys == null) { + throw new IndexOutOfBoundsException(); + } else { + return keys[index]; + } + } + + @SuppressWarnings("unchecked") + public StorableKey[] getAlternateKeys() { + StorableKey[] 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[] indexes = mIndexes; + return indexes == null ? 0 : indexes.length; + } + + public StorableIndex getIndex(int index) { + StorableIndex[] indexes = mIndexes; + if (indexes == null) { + throw new IndexOutOfBoundsException(); + } else { + return indexes[index]; + } + } + + @SuppressWarnings("unchecked") + public StorableIndex[] getIndexes() { + StorableIndex[] indexes = mIndexes; + if (indexes == null) { + return new StorableIndex[0]; + } else { + return indexes.clone(); + } + } + + public final boolean isIndependent() { + return mIndependent; + } + } + + private static class SKey implements StorableKey { + private final boolean mPrimary; + private final Set> mProperties; + + SKey(boolean primary, Set> properties) { + mPrimary = primary; + mProperties = properties; + } + + public boolean isPrimary() { + return mPrimary; + } + + public Set> 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 other = (StorableKey) 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 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 implements StorableProperty { + private final BeanProperty mBeanProperty; + private final Class 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 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 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 getJoinedType() { + return null; + } + + public int getJoinElementCount() { + return 0; + } + + public StorableProperty getInternalJoinElement(int index) { + throw new IndexOutOfBoundsException(); + } + + @SuppressWarnings("unchecked") + public StorableProperty[] 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 extends SimpleProperty { + private final Class 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[] mInternal; + private StorableProperty[] mExternal; + + JoinProperty(BeanProperty property, Class enclosing, + boolean nullable, + String[] aliases, StorablePropertyConstraint[] constraints, + StorablePropertyAdapter adapter, + String sequence, boolean independent, + Class 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 getJoinedType() { + return mJoinedType; + } + + public int getJoinElementCount() { + return mInternal.length; + } + + public StorableProperty getInternalJoinElement(int index) { + return mInternal[index]; + } + + public StorableProperty[] 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 errorMessages, Map> 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> 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> externalProperties = + joinedInfo.getAllProperties(); + + for (int i=0; i 0) { + return; + } + + // Verify that join types match type. + for (int i=0; i (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 primaryKeys = + new HashSet(joinedInfo.getPrimaryKeyProperties().values()); + + // Remove external properties from the primary key set. + for (int i=0; i> altKeys = + new ArrayList>(altKeyCount); + + altKeyScan: + for (int i=0; i altKey = new HashSet(); + + 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 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 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 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> 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 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 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 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 getInternalJoinElement(int index) throws IndexOutOfBoundsException; + + /** + * Returns a new array with all the internal join elements in it. + */ + StorableProperty[] 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 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 adaptMethods = new ArrayList(); + + 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 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 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 + * (Class type, String propertyName, Annotation), where + * Annotation 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 candidates = new ArrayList(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 candidates = new ArrayList(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 candidates = new ArrayList(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 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> fromMap = new LinkedHashMap>(); + + for (Method method : candidates) { + Class from = method.getParameterTypes()[0]; + List matches = fromMap.get(from); + if (matches == null) { + matches = new ArrayList(); + fromMap.put(from, matches); + } + matches.add(method); + } + + candidates.clear(); + + for (List 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 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 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 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 + * (Class type, String propertyName, Annotation), where + * Annotation 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; -- cgit v1.2.3