From 8f88478b4be9c3165d678c43640052c8fc7d8943 Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Wed, 30 Aug 2006 02:19:09 +0000 Subject: Added spi package --- .../carbonado/spi/AbstractRepositoryBuilder.java | 81 + .../spi/AbstractSequenceValueProducer.java | 79 + .../java/com/amazon/carbonado/spi/BaseQuery.java | 378 +++ .../amazon/carbonado/spi/BaseQueryCompiler.java | 248 ++ .../com/amazon/carbonado/spi/BaseQueryEngine.java | 1412 ++++++++ .../carbonado/spi/BelatedRepositoryCreator.java | 149 + .../carbonado/spi/BelatedStorageCreator.java | 138 + .../com/amazon/carbonado/spi/BlobProperty.java | 55 + .../com/amazon/carbonado/spi/ClobProperty.java | 55 + .../com/amazon/carbonado/spi/CodeBuilderUtil.java | 400 +++ .../amazon/carbonado/spi/CommonMethodNames.java | 87 + .../amazon/carbonado/spi/ConversionComparator.java | 212 ++ .../amazon/carbonado/spi/ExceptionTransformer.java | 189 ++ .../com/amazon/carbonado/spi/IndexInfoImpl.java | 117 + .../com/amazon/carbonado/spi/IndexSelector.java | 1204 +++++++ .../java/com/amazon/carbonado/spi/LobEngine.java | 1059 ++++++ .../com/amazon/carbonado/spi/LobEngineTrigger.java | 181 + .../java/com/amazon/carbonado/spi/LobProperty.java | 44 + .../com/amazon/carbonado/spi/MasterFeature.java | 56 + .../carbonado/spi/MasterStorableGenerator.java | 767 +++++ .../com/amazon/carbonado/spi/MasterSupport.java | 38 + .../com/amazon/carbonado/spi/RAFInputStream.java | 62 + .../com/amazon/carbonado/spi/RAFOutputStream.java | 55 + .../com/amazon/carbonado/spi/RepairExecutor.java | 183 + .../amazon/carbonado/spi/RunnableTransaction.java | 114 + .../carbonado/spi/SequenceValueGenerator.java | 288 ++ .../carbonado/spi/SequenceValueProducer.java | 87 + .../amazon/carbonado/spi/StorableGenerator.java | 3534 ++++++++++++++++++++ .../com/amazon/carbonado/spi/StorableIndexSet.java | 512 +++ .../amazon/carbonado/spi/StorableSerializer.java | 337 ++ .../com/amazon/carbonado/spi/StorableSupport.java | 41 + .../java/com/amazon/carbonado/spi/StoredLob.java | 96 + .../com/amazon/carbonado/spi/StoredSequence.java | 49 + .../amazon/carbonado/spi/TransactionManager.java | 642 ++++ .../com/amazon/carbonado/spi/TransactionPair.java | 89 + .../com/amazon/carbonado/spi/TriggerManager.java | 691 ++++ .../com/amazon/carbonado/spi/TriggerSupport.java | 50 + .../com/amazon/carbonado/spi/WrappedQuery.java | 236 ++ .../com/amazon/carbonado/spi/WrappedStorage.java | 228 ++ .../com/amazon/carbonado/spi/WrappedSupport.java | 75 + .../com/amazon/carbonado/spi/package-info.java | 24 + .../carbonado/spi/raw/CustomStorableCodec.java | 337 ++ .../spi/raw/CustomStorableCodecFactory.java | 70 + .../com/amazon/carbonado/spi/raw/DataDecoder.java | 567 ++++ .../com/amazon/carbonado/spi/raw/DataEncoder.java | 595 ++++ .../carbonado/spi/raw/GenericEncodingStrategy.java | 1963 +++++++++++ .../carbonado/spi/raw/GenericInstanceFactory.java | 36 + .../carbonado/spi/raw/GenericPropertyInfo.java | 60 + .../carbonado/spi/raw/GenericStorableCodec.java | 813 +++++ .../spi/raw/GenericStorableCodecFactory.java | 76 + .../com/amazon/carbonado/spi/raw/KeyDecoder.java | 646 ++++ .../com/amazon/carbonado/spi/raw/KeyEncoder.java | 741 ++++ .../carbonado/spi/raw/LayoutPropertyInfo.java | 86 + .../com/amazon/carbonado/spi/raw/RawCursor.java | 743 ++++ .../carbonado/spi/raw/RawStorableGenerator.java | 355 ++ .../com/amazon/carbonado/spi/raw/RawSupport.java | 97 + .../java/com/amazon/carbonado/spi/raw/RawUtil.java | 66 + .../amazon/carbonado/spi/raw/StorableCodec.java | 118 + .../carbonado/spi/raw/StorableCodecFactory.java | 54 + .../carbonado/spi/raw/StorablePropertyInfo.java | 132 + .../com/amazon/carbonado/spi/raw/package-info.java | 23 + 61 files changed, 21920 insertions(+) create mode 100644 src/main/java/com/amazon/carbonado/spi/AbstractRepositoryBuilder.java create mode 100644 src/main/java/com/amazon/carbonado/spi/AbstractSequenceValueProducer.java create mode 100644 src/main/java/com/amazon/carbonado/spi/BaseQuery.java create mode 100644 src/main/java/com/amazon/carbonado/spi/BaseQueryCompiler.java create mode 100644 src/main/java/com/amazon/carbonado/spi/BaseQueryEngine.java create mode 100644 src/main/java/com/amazon/carbonado/spi/BelatedRepositoryCreator.java create mode 100644 src/main/java/com/amazon/carbonado/spi/BelatedStorageCreator.java create mode 100644 src/main/java/com/amazon/carbonado/spi/BlobProperty.java create mode 100644 src/main/java/com/amazon/carbonado/spi/ClobProperty.java create mode 100644 src/main/java/com/amazon/carbonado/spi/CodeBuilderUtil.java create mode 100644 src/main/java/com/amazon/carbonado/spi/CommonMethodNames.java create mode 100644 src/main/java/com/amazon/carbonado/spi/ConversionComparator.java create mode 100644 src/main/java/com/amazon/carbonado/spi/ExceptionTransformer.java create mode 100644 src/main/java/com/amazon/carbonado/spi/IndexInfoImpl.java create mode 100644 src/main/java/com/amazon/carbonado/spi/IndexSelector.java create mode 100644 src/main/java/com/amazon/carbonado/spi/LobEngine.java create mode 100644 src/main/java/com/amazon/carbonado/spi/LobEngineTrigger.java create mode 100644 src/main/java/com/amazon/carbonado/spi/LobProperty.java create mode 100644 src/main/java/com/amazon/carbonado/spi/MasterFeature.java create mode 100644 src/main/java/com/amazon/carbonado/spi/MasterStorableGenerator.java create mode 100644 src/main/java/com/amazon/carbonado/spi/MasterSupport.java create mode 100644 src/main/java/com/amazon/carbonado/spi/RAFInputStream.java create mode 100644 src/main/java/com/amazon/carbonado/spi/RAFOutputStream.java create mode 100644 src/main/java/com/amazon/carbonado/spi/RepairExecutor.java create mode 100644 src/main/java/com/amazon/carbonado/spi/RunnableTransaction.java create mode 100644 src/main/java/com/amazon/carbonado/spi/SequenceValueGenerator.java create mode 100644 src/main/java/com/amazon/carbonado/spi/SequenceValueProducer.java create mode 100644 src/main/java/com/amazon/carbonado/spi/StorableGenerator.java create mode 100644 src/main/java/com/amazon/carbonado/spi/StorableIndexSet.java create mode 100644 src/main/java/com/amazon/carbonado/spi/StorableSerializer.java create mode 100644 src/main/java/com/amazon/carbonado/spi/StorableSupport.java create mode 100644 src/main/java/com/amazon/carbonado/spi/StoredLob.java create mode 100644 src/main/java/com/amazon/carbonado/spi/StoredSequence.java create mode 100644 src/main/java/com/amazon/carbonado/spi/TransactionManager.java create mode 100644 src/main/java/com/amazon/carbonado/spi/TransactionPair.java create mode 100644 src/main/java/com/amazon/carbonado/spi/TriggerManager.java create mode 100644 src/main/java/com/amazon/carbonado/spi/TriggerSupport.java create mode 100644 src/main/java/com/amazon/carbonado/spi/WrappedQuery.java create mode 100644 src/main/java/com/amazon/carbonado/spi/WrappedStorage.java create mode 100644 src/main/java/com/amazon/carbonado/spi/WrappedSupport.java create mode 100644 src/main/java/com/amazon/carbonado/spi/package-info.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodec.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodecFactory.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/DataDecoder.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/DataEncoder.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/GenericEncodingStrategy.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/GenericInstanceFactory.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/GenericPropertyInfo.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodec.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodecFactory.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/KeyDecoder.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/KeyEncoder.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/LayoutPropertyInfo.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/RawCursor.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/RawStorableGenerator.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/RawSupport.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/RawUtil.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/StorableCodec.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/StorableCodecFactory.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/StorablePropertyInfo.java create mode 100644 src/main/java/com/amazon/carbonado/spi/raw/package-info.java (limited to 'src/main/java/com/amazon/carbonado') diff --git a/src/main/java/com/amazon/carbonado/spi/AbstractRepositoryBuilder.java b/src/main/java/com/amazon/carbonado/spi/AbstractRepositoryBuilder.java new file mode 100644 index 0000000..902d516 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/AbstractRepositoryBuilder.java @@ -0,0 +1,81 @@ +/* + * 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.spi; + +import java.util.ArrayList; +import java.util.Collection; + +import com.amazon.carbonado.ConfigurationException; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.RepositoryBuilder; +import com.amazon.carbonado.RepositoryException; + +/** + * Abstract builder class for opening repositories. + * + * @author Don Schneider + * @author Brian S O'Neill + */ +public abstract class AbstractRepositoryBuilder implements RepositoryBuilder { + protected AbstractRepositoryBuilder() { + } + + public Repository build() throws ConfigurationException, RepositoryException { + return build(new RepositoryReference()); + } + + /** + * Throw a configuration exception if the configuration is not filled out + * sufficiently and correctly such that a repository could be instantiated + * from it. + */ + public final void assertReady() throws ConfigurationException { + ArrayList messages = new ArrayList(); + errorCheck(messages); + int size = messages.size(); + if (size == 0) { + return; + } + StringBuilder b = new StringBuilder(); + if (size > 1) { + b.append("Multiple problems: "); + } + for (int i=0; i 0) { + b.append("; "); + } + b.append(messages.get(i)); + } + throw new ConfigurationException(b.toString()); + } + + /** + * This method is called by assertReady, and subclasses must override to + * perform custom checks. Be sure to call {@code super.errorCheck} as well. + * + * @param messages add any error messages to this list + * @throws ConfigurationException if error checking indirectly caused + * another exception + */ + public void errorCheck(Collection messages) throws ConfigurationException { + if (getName() == null) { + messages.add("name missing"); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/AbstractSequenceValueProducer.java b/src/main/java/com/amazon/carbonado/spi/AbstractSequenceValueProducer.java new file mode 100644 index 0000000..5badf0f --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/AbstractSequenceValueProducer.java @@ -0,0 +1,79 @@ +/* + * 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.spi; + +import java.math.BigInteger; + +import com.amazon.carbonado.PersistException; + +/** + * + * + * @author Brian S O'Neill + */ +public abstract class AbstractSequenceValueProducer implements SequenceValueProducer { + protected AbstractSequenceValueProducer() { + } + + public int nextIntValue() throws PersistException { + return (int) nextLongValue(); + } + + public String nextDecimalValue() throws PersistException { + return nextNumericalValue(10, 0); + } + + public String nextNumericalValue(int radix, int minLength) throws PersistException { + long next = nextLongValue(); + String str; + + if (next >= 0) { + str = Long.toString(next, radix); + } else { + // Use BigInteger to print negative values as positive by expanding + // precision to 72 bits + + byte[] bytes = new byte[9]; + bytes[8] = (byte) (next & 0xff); + bytes[7] = (byte) ((next >>= 8) & 0xff); + bytes[6] = (byte) ((next >>= 8) & 0xff); + bytes[5] = (byte) ((next >>= 8) & 0xff); + bytes[4] = (byte) ((next >>= 8) & 0xff); + bytes[3] = (byte) ((next >>= 8) & 0xff); + bytes[2] = (byte) ((next >>= 8) & 0xff); + bytes[1] = (byte) ((next >>= 8) & 0xff); + //bytes[0] = 0; + + str = new BigInteger(bytes).toString(radix); + } + + int pad = minLength - str.length(); + + if (pad > 0) { + StringBuilder b = new StringBuilder(minLength); + while (--pad >= 0) { + b.append('0'); + } + b.append(str); + str = b.toString(); + } + + return str; + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/BaseQuery.java b/src/main/java/com/amazon/carbonado/spi/BaseQuery.java new file mode 100644 index 0000000..e1117cd --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/BaseQuery.java @@ -0,0 +1,378 @@ +/* + * 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.spi; + +import java.io.IOException; + +import org.cojen.util.BeanPropertyAccessor; + +import com.amazon.carbonado.Cursor; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.IsolationLevel; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.PersistMultipleException; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Transaction; +import com.amazon.carbonado.Query; +import com.amazon.carbonado.util.Appender; + +import com.amazon.carbonado.filter.ClosedFilter; +import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.filter.FilterValues; +import com.amazon.carbonado.filter.OpenFilter; +import com.amazon.carbonado.filter.RelOp; + +import com.amazon.carbonado.info.OrderedProperty; + +import com.amazon.carbonado.qe.AbstractQuery; +import com.amazon.carbonado.qe.EmptyQuery; + +/** + * BaseQuery supports binding filters to values. + * + * @author Brian S O'Neill + */ +public abstract class BaseQuery extends AbstractQuery implements Appender { + /** + * Appends spaces to the given appendable. Useful for implementing + * printNative and printPlan. + */ + public static void indent(Appendable app, int indentLevel) throws IOException { + for (int i=0; i mStorage; + // Values for this query. + private final FilterValues mValues; + // Properties that this query is ordered by. + private final String[] mOrderings; + + // Note: Since constructor has parameters, this class is called Base + // instead of Abstract. + /** + * @param storage required storage object + * @param values optional values object, defaults to open filter if null + * @param orderings optional order-by properties + */ + protected BaseQuery(Repository repo, + Storage storage, + FilterValues values, + OrderedProperty[] orderings) + { + if (repo == null || storage == null) { + throw new IllegalArgumentException(); + } + mRepository = repo; + mStorage = storage; + mValues = values; + mOrderings = extractOrderingNames(orderings); + } + + /** + * @param storage required storage object + * @param values optional values object, defaults to open filter if null + * @param orderings optional order-by properties, not cloned + */ + protected BaseQuery(Repository repo, + Storage storage, + FilterValues values, + String[] orderings) + { + if (repo == null || storage == null) { + throw new IllegalArgumentException(); + } + mRepository = repo; + mStorage = storage; + mValues = values; + mOrderings = orderings == null ? EMPTY_ORDERINGS : orderings; + } + + public Class getStorableType() { + return mStorage.getStorableType(); + } + + public Filter getFilter() { + FilterValues values = mValues; + if (values != null) { + return values.getFilter(); + } + return Filter.getOpenFilter(mStorage.getStorableType()); + } + + public FilterValues getFilterValues() { + return mValues; + } + + public int getBlankParameterCount() { + return mValues == null ? 0 : mValues.getBlankParameterCount(); + } + + public Query with(int value) { + return newInstance(requireValues().with(value)); + } + + public Query with(long value) { + return newInstance(requireValues().with(value)); + } + + public Query with(float value) { + return newInstance(requireValues().with(value)); + } + + public Query with(double value) { + return newInstance(requireValues().with(value)); + } + + public Query with(boolean value) { + return newInstance(requireValues().with(value)); + } + + public Query with(char value) { + return newInstance(requireValues().with(value)); + } + + public Query with(byte value) { + return newInstance(requireValues().with(value)); + } + + public Query with(short value) { + return newInstance(requireValues().with(value)); + } + + public Query with(Object value) { + return newInstance(requireValues().with(value)); + } + + public Query withValues(Object... values) { + if (values == null || values.length == 0) { + return this; + } + return newInstance(requireValues().withValues(values)); + } + + public Query and(Filter filter) throws FetchException { + FilterValues values = getFilterValues(); + Query newQuery; + if (values == null) { + newQuery = mStorage.query(filter); + } else { + newQuery = mStorage.query(values.getFilter().and(filter)); + newQuery = newQuery.withValues(values.getValues()); + } + return mOrderings.length == 0 ? newQuery : newQuery.orderBy(mOrderings); + } + + public Query or(Filter filter) throws FetchException { + FilterValues values = getFilterValues(); + if (values == null) { + throw new IllegalStateException("Query is already guaranteed to fetch everything"); + } + Query newQuery = mStorage.query(values.getFilter().or(filter)); + newQuery = newQuery.withValues(values.getValues()); + return mOrderings.length == 0 ? newQuery : newQuery.orderBy(mOrderings); + } + + public Query not() throws FetchException { + FilterValues values = getFilterValues(); + if (values == null) { + return new EmptyQuery(mStorage, mOrderings); + } + Query newQuery = mStorage.query(values.getFilter().not()); + newQuery = newQuery.withValues(values.getSuppliedValues()); + return mOrderings.length == 0 ? newQuery : newQuery.orderBy(mOrderings); + } + + public Cursor fetchAfter(S start) throws FetchException { + String[] orderings; + if (start == null || (orderings = mOrderings).length == 0) { + return fetch(); + } + + Class storableType = mStorage.getStorableType(); + Filter orderFilter = Filter.getClosedFilter(storableType); + Filter lastSubFilter = Filter.getOpenFilter(storableType); + BeanPropertyAccessor accessor = BeanPropertyAccessor.forClass(storableType); + + Object[] values = new Object[orderings.length]; + + for (int i=0;;) { + String propertyName = orderings[i]; + RelOp operator = RelOp.GT; + char c = propertyName.charAt(0); + if (c == '-') { + propertyName = propertyName.substring(1); + operator = RelOp.LT; + } else if (c == '+') { + propertyName = propertyName.substring(1); + } + + values[i] = accessor.getPropertyValue(start, propertyName); + + orderFilter = orderFilter.or(lastSubFilter.and(propertyName, operator)); + + if (++i >= orderings.length) { + break; + } + + lastSubFilter = lastSubFilter.and(propertyName, RelOp.EQ); + } + + Query newQuery = this.and(orderFilter); + + for (int i=0; i cursor = fetch(); + boolean result; + try { + if (cursor.hasNext()) { + S obj = cursor.next(); + if (cursor.hasNext()) { + throw new PersistMultipleException(toString()); + } + result = obj.tryDelete(); + } else { + return false; + } + } finally { + cursor.close(); + } + txn.commit(); + return result; + } catch (FetchException e) { + throw e.toPersistException(); + } finally { + txn.exit(); + } + } + + public void deleteAll() throws PersistException { + Transaction txn = mRepository.enterTransaction(IsolationLevel.READ_COMMITTED); + try { + Cursor cursor = fetch(); + try { + while (cursor.hasNext()) { + cursor.next().tryDelete(); + } + } finally { + cursor.close(); + } + txn.commit(); + } catch (FetchException e) { + throw e.toPersistException(); + } finally { + txn.exit(); + } + } + + /** + * Returns the query ordering properties, never null. The returned array is + * not cloned, only for performance reasons. Subclasses should not alter it. + */ + protected String[] getOrderings() { + return mOrderings; + } + + protected final Repository getRepository() { + return mRepository; + } + + protected final Storage getStorage() { + return mStorage; + } + + @Override + public int hashCode() { + return mStorage.hashCode() * 31 + getFilterValues().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BaseQuery) { + BaseQuery other = (BaseQuery) obj; + return mStorage.equals(other.mStorage) && + getFilterValues().equals(other.getFilterValues()); + } + return false; + } + + public void appendTo(Appendable app) throws IOException { + app.append("Query {type="); + app.append(getStorableType().getName()); + app.append(", filter="); + Filter filter = getFilter(); + if (filter instanceof OpenFilter || filter instanceof ClosedFilter) { + filter.appendTo(app); + } else { + app.append('"'); + filter.appendTo(app, getFilterValues()); + app.append('"'); + } + + if (mOrderings != null && mOrderings.length > 0) { + app.append(", orderBy=["); + for (int i=0; i 0) { + app.append(", "); + } + app.append(mOrderings[i]); + } + app.append(']'); + } + + app.append('}'); + } + + private FilterValues requireValues() { + FilterValues values = getFilterValues(); + if (values == null) { + throw new IllegalStateException("Query doesn't have any parameters"); + } + return values; + } + + /** + * Return a new instance of BaseQuery implementation, using new filter + * values. The Filter in the FilterValues is the same as was passed in the + * constructor. + * + *

Any orderings in this query must also be applied in the new + * query. Call getOrderings to get them. + * + * @param values never null + */ + protected abstract BaseQuery newInstance(FilterValues values); +} diff --git a/src/main/java/com/amazon/carbonado/spi/BaseQueryCompiler.java b/src/main/java/com/amazon/carbonado/spi/BaseQueryCompiler.java new file mode 100644 index 0000000..fdc1533 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/BaseQueryCompiler.java @@ -0,0 +1,248 @@ +/* + * 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.spi; + +import java.util.Map; + +import org.cojen.util.KeyFactory; +import org.cojen.util.SoftValuedHashMap; +import org.cojen.util.WeakIdentityMap; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.MalformedFilterException; +import com.amazon.carbonado.Query; + +import com.amazon.carbonado.filter.ClosedFilter; +import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.filter.FilterValues; + +import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableInfo; + +/** + * BaseQueryCompiler caches compiled queries, and calls an abstract method + * to compile queries it doesn't have cached. + * + * @author Brian S O'Neill + */ +public abstract class BaseQueryCompiler { + private final StorableInfo mInfo; + private final Map> mStringToQuery; + private final Map, Queries> mFilterToQueries; + + /** + * @throws IllegalArgumentException if type is null + */ + // Note: Since constructor has parameters, this class is called Base + // instead of Abstract. + @SuppressWarnings("unchecked") + protected BaseQueryCompiler(StorableInfo info) { + if (info == null) { + throw new IllegalArgumentException(); + } + mInfo = info; + mStringToQuery = new SoftValuedHashMap(7); + mFilterToQueries = new WeakIdentityMap(7); + } + + /** + * Looks up compiled query in the cache, and returns it. If not found, then + * one is created and cached for later retrieval. + * + * @return cached compiled query which returns everything from storage + */ + public synchronized Query getCompiledQuery() throws FetchException { + return getCompiledQuery(Filter.getOpenFilter(mInfo.getStorableType())); + } + + /** + * Looks up compiled query in the cache, and returns it. If not found, then + * the filter expression is parsed, and compileQuery is invoked on the + * result. The compiled query is cached for later retrieval. + * + * @param filter query filter expression to parse + * @return cached compiled query + * @throws IllegalArgumentException if query filter expression is null + * @throws MalformedFilterException if query filter expression is malformed + */ + public synchronized Query getCompiledQuery(String filter) throws FetchException { + if (filter == null) { + throw new IllegalArgumentException("Query filter must not be null"); + } + Query query = mStringToQuery.get(filter); + if (query == null) { + query = getCompiledQuery(Filter.filterFor(mInfo.getStorableType(), filter)); + mStringToQuery.put(filter, query); + } + return query; + } + + /** + * Looks up compiled query in the cache, and returns it. If not found, then + * compileQuery is invoked on the result. The compiled query is cached for + * later retrieval. + * + * @param filter root filter tree + * @return cached compiled query + * @throws IllegalArgumentException if root filter is null + */ + public synchronized Query getCompiledQuery(Filter filter) throws FetchException { + if (filter == null) { + throw new IllegalArgumentException("Filter is null"); + } + Queries queries = mFilterToQueries.get(filter); + if (queries == null) { + Query query; + FilterValues values = filter.initialFilterValues(); + if (values != null) { + // FilterValues applies to bound filter. Use that instead. + Filter altFilter = values.getFilter(); + if (altFilter != filter) { + return getCompiledQuery(altFilter); + } + query = compileQuery(values, null); + } else { + query = compileQuery(null, null); + if (filter instanceof ClosedFilter) { + query = query.not(); + } + } + queries = new Queries(query); + mFilterToQueries.put(filter, queries); + } + return queries.mPlainQuery; + } + + /** + * Used by implementations to retrieve cached queries that have order-by + * properties. + * + * @param values filter values produced earlier by this compiler, or null, + * or a derived instance + * @param propertyNames optional property names to order by, which may be + * prefixed with '+' or '-' + * @throws IllegalArgumentException if properties are not supported or if + * filter did not originate from this compiler + */ + @SuppressWarnings("unchecked") + public Query getOrderedQuery(FilterValues values, String... propertyNames) + throws FetchException, IllegalArgumentException, UnsupportedOperationException + { + final Filter filter = + values == null ? Filter.getOpenFilter(mInfo.getStorableType()) : values.getFilter(); + + final Queries queries = mFilterToQueries.get(filter); + + if (queries == null) { + throw new IllegalArgumentException("Unknown filter provided"); + } + + if (propertyNames == null || propertyNames.length == 0) { + return queries.mPlainQuery; + } + + final Object key = KeyFactory.createKey(propertyNames); + Query query = queries.mOrderingsToQuery.get(key); + + if (query != null) { + // Now transfer property values. + if (values != null) { + query = query.withValues(values.getSuppliedValues()); + } + + return query; + } + + // Try again with property names that have an explicit direction, + // hoping for a cache hit. + + boolean propertyNamesChanged = false; + final int length = propertyNames.length; + for (int i=0; i[] orderings = new OrderedProperty[length]; + + for (int i=0; i initialValues = filter.initialFilterValues(); + + query = compileQuery(initialValues, orderings); + queries.mOrderingsToQuery.put(key, query); + + // Now transfer property values. + if (values != null) { + query = query.withValues(values.getSuppliedValues()); + } + + return query; + } + + /** + * Returns the StorableInfo object in this object. + */ + protected StorableInfo getStorableInfo() { + return mInfo; + } + + /** + * Compile the query represented by the type checked root node. If any + * order-by properties are supplied, they have been checked as well. + * + * @param values values and filter for query, which may be null if + * unfiltered + * @param orderings optional list of properties to order by + */ + protected abstract Query compileQuery(FilterValues values, + OrderedProperty[] orderings) + throws FetchException, UnsupportedOperationException; + + private static class Queries { + final Query mPlainQuery; + + final Map> mOrderingsToQuery; + + @SuppressWarnings("unchecked") + Queries(Query query) { + mPlainQuery = query; + mOrderingsToQuery = new SoftValuedHashMap(7); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/BaseQueryEngine.java b/src/main/java/com/amazon/carbonado/spi/BaseQueryEngine.java new file mode 100644 index 0000000..10dffa7 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/BaseQueryEngine.java @@ -0,0 +1,1412 @@ +/* + * 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.spi; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.cojen.util.BeanComparator; + +import com.amazon.carbonado.Cursor; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Query; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; + +import com.amazon.carbonado.filter.AndFilter; +import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.filter.FilterValues; +import com.amazon.carbonado.filter.OrFilter; +import com.amazon.carbonado.filter.PropertyFilter; +import com.amazon.carbonado.filter.Visitor; + +import com.amazon.carbonado.info.Direction; +import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.info.StorableInfo; + +import com.amazon.carbonado.cursor.FilteredCursor; +import com.amazon.carbonado.cursor.MergeSortBuffer; +import com.amazon.carbonado.cursor.SortBuffer; +import com.amazon.carbonado.cursor.SortedCursor; +import com.amazon.carbonado.cursor.UnionCursor; + +import com.amazon.carbonado.qe.BoundaryType; + +/** + * Basis for a rule-based query engine. It takes care of index selection, + * filtering, sorting, and unions. + * + * @author Brian S O'Neill + */ +public abstract class BaseQueryEngine extends BaseQueryCompiler { + private static PropertyFilter[] NO_FILTERS = new PropertyFilter[0]; + + /** + * Compares two objects which are assumed to be Comparable. If one value is + * null, it is treated as being higher. This consistent with all other + * property value comparisons in carbonado. + */ + static int compareWithNullHigh(Object a, Object b) { + return a == null ? (b == null ? 0 : -1) : (b == null ? 1 : ((Comparable) a).compareTo(b)); + } + + private final Repository mRepository; + private final Storage mStorage; + private final StorableIndex mPrimaryKeyIndex; + private final StorableIndexSet mIndexSet; + + String mMergeSortTempDir; + + /** + * @param info info for Storable + * @param repo repository for entering transactions + * @param storage source for queried objects + * @param primaryKeyIndex optional parameter representing primary key index + * @param indexSet optional parameter representing all available indexes. + * Constructor makes a local copy of the set. + * @throws IllegalArgumentException if primaryKeyIndex is null and indexSet + * is empty + */ + protected BaseQueryEngine(StorableInfo info, + Repository repo, + Storage storage, + StorableIndex primaryKeyIndex, + StorableIndexSet indexSet) { + super(info); + if (primaryKeyIndex == null && (indexSet == null || indexSet.size() == 0)) { + throw new IllegalArgumentException(); + } + mRepository = repo; + mStorage = storage; + mPrimaryKeyIndex = primaryKeyIndex; + mIndexSet = (indexSet == null || indexSet.size() == 0) ? null + : new StorableIndexSet(indexSet); + } + + /** + * @param tempDir directory to store temp files for merge sorting, or null + * for default + */ + protected void setMergeSortTempDirectory(String tempDir) { + mMergeSortTempDir = tempDir; + } + + @SuppressWarnings("unchecked") + protected Query compileQuery(final FilterValues values, + final OrderedProperty[] orderings) + throws FetchException, UnsupportedOperationException + { + if (values == null) { + // Perform requested full scan. + return fullScan(values, orderings); + } + + final Filter originalFilter = values.getFilter(); + final Filter dnfFilter = originalFilter.disjunctiveNormalForm(); + + // Analyze the disjunctive normal form, breaking down the query into + // separate queries that can be unioned together. + + IndexAnalysis analysis = new IndexAnalysis(mPrimaryKeyIndex, mIndexSet, orderings); + dnfFilter.accept(analysis, null); + + if (analysis.noBestIndex()) { + // Fallback to full scan for everything if no best index found for + // just one query component. + return fullScan(values, orderings); + } + + OrderedProperty[] totalOrderings = null; + ensureTotalOrdering: + if (analysis.getResults().size() > 1) { + // Union will be performed, and so a total ordering is required. + + // TODO: The logic in this section needs to be totally reworked. It + // does a terrible job of finding the best total ordering, often + // performing full sorts when not needed. Essentially, inefficient + // query plans can get generated. + + // If all selected indexes are unique and have the same effective ordering, then + // nothing special needs to be done to ensure total ordering. + OrderedProperty[] effectiveOrderings = null; + totalOrderCheck: + if (orderings == null || orderings.length == 0) { + for (IndexSelector.IndexFitness result : analysis.getResults()) { + StorableIndex index = result.getIndex(); + if (!index.isUnique()) { + break totalOrderCheck; + } + if (effectiveOrderings == null) { + effectiveOrderings = result.getEffectiveOrderings(); + continue; + } + if (!Arrays.equals(effectiveOrderings, result.getEffectiveOrderings())) { + break totalOrderCheck; + } + } + // All indexes already define a total ordering. + totalOrderings = effectiveOrderings; + break ensureTotalOrdering; + } + + // Augment the ordering with elements of a unique index. + + // Count how often an index has been used. + Map, Integer> counts = new LinkedHashMap, Integer>(); + + for (IndexSelector.IndexFitness result : analysis.getResults()) { + StorableIndex index = result.getIndex(); + counts.put(index, (counts.containsKey(index)) ? (counts.get(index) + 1) : 1); + } + + // Find the unique index that has been selected most often. + StorableIndex unique = mPrimaryKeyIndex; + int uniqueCount = 0; + for (Map.Entry, Integer> entry : counts.entrySet()) { + if (entry.getKey().isUnique() && entry.getValue() > uniqueCount) { + unique = entry.getKey(); + uniqueCount = entry.getValue(); + } + } + + if (unique == null) { + // Select first found unique index. + for (StorableIndex index : mIndexSet) { + if (index.isUnique()) { + unique = index; + break; + } + } + if (unique == null) { + throw new UnsupportedOperationException + ("Cannot perform union; sort requires at least one unique index"); + } + } + + // To avoid full sorts, choose an index which is already being used + // for its ordering. It may have a range filter or handled + // orderings. + StorableIndex best = null; + int bestCount = 0; + for (IndexSelector.IndexFitness result : analysis.getResults()) { + if ((result.getInclusiveRangeStartFilters().length > 0 || + result.getExclusiveRangeStartFilters().length > 0 || + result.getInclusiveRangeEndFilters().length > 0 || + result.getExclusiveRangeEndFilters().length > 0) && + (result.getHandledOrderings() != null || + result.getRemainderOrderings() == null)) { + + StorableIndex index = result.getIndex(); + int count = counts.get(index); + + if (count > bestCount) { + best = index; + bestCount = count; + } + } + } + + { + int newLength = (orderings == null ? 0 : orderings.length) + + (best == null ? 0 : best.getPropertyCount()) + + unique.getPropertyCount(); + totalOrderings = new OrderedProperty[newLength]; + + int j = 0; + if (orderings != null) { + for (int i=0; i(mPrimaryKeyIndex, mIndexSet, totalOrderings); + dnfFilter.accept(analysis, null); + + if (analysis.noBestIndex()) { + // Fallback to full scan for everything if no best index found for + // just one query component. + return fullScan(values, orderings); + } + } + + // Attempt to reduce the number of separate cursors need to be opened for union. + analysis.reduceResults(); + + List> subFactories = new ArrayList>(); + + for (IndexSelector.IndexFitness result : analysis.getResults()) { + CursorFactory subFactory; + + // Determine if KeyCursorFactory should be used instead. + boolean isKeyFilter = result.isKeyFilter(); + if (isKeyFilter) { + subFactory = new KeyCursorFactory + (this, result.getIndex(), result.getExactFilter()); + } else { + subFactory = new IndexCursorFactory + (this, result.getIndex(), + result.shouldReverseOrder(), result.shouldReverseRange(), + result.getExactFilter(), + result.getInclusiveRangeStartFilters(), + result.getExclusiveRangeStartFilters(), + result.getInclusiveRangeEndFilters(), + result.getExclusiveRangeEndFilters()); + } + + Filter remainderFilter = result.getRemainderFilter(); + if (remainderFilter != null) { + subFactory = new FilteredCursorFactory(this, subFactory, remainderFilter); + } + + if (!isKeyFilter) { + OrderedProperty[] remainderOrderings = result.getRemainderOrderings(); + if (remainderOrderings != null && remainderOrderings.length > 0) { + subFactory = new SortedCursorFactory + (this, subFactory, result.getHandledOrderings(), remainderOrderings); + } + } + + subFactories.add(subFactory); + } + + CursorFactory factory = UnionedCursorFactory + .createUnion(this, subFactories, totalOrderings); + + return CompiledQuery.create(mRepository, mStorage, values, orderings, this, factory); + } + + private Query fullScan(FilterValues values, OrderedProperty[] orderings) + throws FetchException + { + // Try to select index that has best ordering. + IndexSelector selector = new IndexSelector(null, orderings); + StorableIndex best = mPrimaryKeyIndex; + + if (mIndexSet != null) { + for (StorableIndex candidate : mIndexSet) { + int cmpResult = selector.compare(best, candidate); + if (cmpResult > 0) { + best = candidate; + } + } + } + + IndexSelector.IndexFitness result = selector.examine(best); + + CursorFactory factory; + if (result == null || result.isUseless()) { + factory = new FullScanCursorFactory(this, mPrimaryKeyIndex); + if (values != null) { + factory = new FilteredCursorFactory(this, factory, values.getFilter()); + } + if (orderings != null && orderings.length > 0) { + factory = new SortedCursorFactory(this, factory, null, orderings); + } + } else { + factory = new IndexCursorFactory + (this, result.getIndex(), + result.shouldReverseOrder(), result.shouldReverseRange(), + result.getExactFilter(), + result.getInclusiveRangeStartFilters(), + result.getExclusiveRangeStartFilters(), + result.getInclusiveRangeEndFilters(), + result.getExclusiveRangeEndFilters()); + + Filter remainderFilter = result.getRemainderFilter(); + if (remainderFilter != null) { + factory = new FilteredCursorFactory(this, factory, remainderFilter); + } + + OrderedProperty[] remainderOrderings = result.getRemainderOrderings(); + if (remainderOrderings != null && remainderOrderings.length > 0) { + factory = new SortedCursorFactory + (this, factory, result.getHandledOrderings(), remainderOrderings); + } + } + + return CompiledQuery.create(mRepository, mStorage, values, orderings, this, factory); + } + + /** + * Returns the primary Storage object in this object. + */ + protected final Storage getStorage() { + return mStorage; + } + + /** + * Returns the storage object that the given index applies to. By default, + * this method returns the primary storage. Override if indexes may be + * defined in multiple storages. + */ + protected Storage getStorageFor(StorableIndex index) { + return mStorage; + } + + /** + * Return a new Cursor instance constrained by the given parameters. The + * index values are aligned with the index properties at property index + * 0. An optional start or end boundary matches up with the index property + * following the last of the index values. + * + * @param index index to open, which may be the primary key index + * @param exactValues optional list of exactly matching values to apply to index + * @param rangeStartBoundary start boundary type + * @param rangeStartValue value to start at if boundary is not open + * @param rangeEndBoundary end boundary type + * @param rangeEndValue value to end at if boundary is not open + * @param reverseRange indicates that range operates on a property that is + * ordered in reverse (this parameter might also be true simply because + * reverseOrder is true) + * @param reverseOrder when true, iteration is reversed + */ + protected abstract Cursor openCursor(StorableIndex index, + Object[] exactValues, + BoundaryType rangeStartBoundary, + Object rangeStartValue, + BoundaryType rangeEndBoundary, + Object rangeEndValue, + boolean reverseRange, + boolean reverseOrder) + throws FetchException; + + /** + * Return a new Cursor instance which is expected to fetch at most one + * object. The chosen index is unique, and a primary or alternate key is + * contained within it. + *

+ * Subclasses are encouraged to override this method and provide a more + * efficient implementation. + * + * @param index index to open, which may be the primary key index + * @param exactValues first values to set for index; length may be smaller + * than index property count + */ + protected Cursor openKeyCursor(StorableIndex index, + Object[] exactValues) + throws FetchException + { + return openCursor(index, exactValues, + BoundaryType.OPEN, null, + BoundaryType.OPEN, null, + false, + false); + } + + @SuppressWarnings("unchecked") + Comparator makeComparator(OrderedProperty[] orderings) { + if (orderings == null) { + return null; + } + + BeanComparator bc = BeanComparator.forClass(getStorableInfo().getStorableType()); + + for (OrderedProperty property : orderings) { + bc = bc.orderBy(property.getChainedProperty().toString()); + bc = bc.caseSensitive(); + if (property.getDirection() == Direction.DESCENDING) { + bc = bc.reverse(); + } + } + + return bc; + } + + private static class CompiledQuery extends BaseQuery { + private final BaseQueryEngine mEngine; + private final CursorFactory mFactory; + + static Query create(Repository repo, + Storage storage, + FilterValues values, + OrderedProperty[] orderings, + BaseQueryEngine engine, + CursorFactory factory) + throws FetchException + { + if (factory == null) { + throw new IllegalArgumentException(); + } + factory = factory.getActualFactory(); + return new CompiledQuery(repo, storage, values, orderings, engine, factory); + } + + private CompiledQuery(Repository repo, + Storage storage, + FilterValues values, + OrderedProperty[] orderings, + BaseQueryEngine engine, + CursorFactory factory) + throws FetchException + { + super(repo, storage, values, orderings); + mEngine = engine; + mFactory = factory; + } + + private CompiledQuery(Repository repo, + Storage storage, + FilterValues values, + String[] orderings, + BaseQueryEngine engine, + CursorFactory factory) + { + super(repo, storage, values, orderings); + mEngine = engine; + mFactory = factory; + } + + public Query orderBy(String property) + throws FetchException, UnsupportedOperationException + { + return mEngine.getOrderedQuery(getFilterValues(), property); + } + + public Query orderBy(String... properties) + throws FetchException, UnsupportedOperationException + { + return mEngine.getOrderedQuery(getFilterValues(), properties); + } + + public Cursor fetch() throws FetchException { + return mFactory.openCursor(getFilterValues()); + } + + public long count() throws FetchException { + return mFactory.count(getFilterValues()); + } + + public boolean printNative(Appendable app, int indentLevel) throws IOException { + return mFactory.printNative(app, indentLevel, getFilterValues()); + } + + public boolean printPlan(Appendable app, int indentLevel) throws IOException { + return mFactory.printPlan(app, indentLevel, getFilterValues()); + } + + protected BaseQuery newInstance(FilterValues values) { + return new CompiledQuery + (getRepository(), getStorage(), values, getOrderings(), mEngine, mFactory); + } + } + + private static interface CursorFactory { + Cursor openCursor(FilterValues values) throws FetchException; + + long count(FilterValues values) throws FetchException; + + /** + * Append filter rules to the given filter. + * + * @param filter initial filter, might be null. + */ + Filter buildFilter(Filter filter); + + /** + * Applies an ordering to the given query in a new query. + */ + Query applyOrderBy(Query query) throws FetchException; + + /** + * Returns the storage object that this factory needs to use. Usually, + * this is the same as the primary. If multiple storages are needed, + * then null is returned. In either case, if the storage is not the + * primary, then this factory cannot be used. Use the factory from + * getActualFactory instead. + */ + Storage getActualStorage(); + + /** + * Returns another instance of this factory that uses the proper + * storage. + */ + CursorFactory getActualFactory() throws FetchException; + + /** + * @param values optional + */ + boolean printNative(Appendable app, int indentLevel, FilterValues values) + throws IOException; + + /** + * @param values optional + */ + boolean printPlan(Appendable app, int indentLevel, FilterValues values) + throws IOException; + } + + private abstract static class AbstractCursorFactory + implements CursorFactory + { + protected final BaseQueryEngine mEngine; + + AbstractCursorFactory(BaseQueryEngine engine) { + mEngine = engine; + } + + public long count(FilterValues values) throws FetchException { + Cursor cursor = openCursor(values); + try { + long count = cursor.skipNext(Integer.MAX_VALUE); + if (count == Integer.MAX_VALUE) { + int amt; + while ((amt = cursor.skipNext(Integer.MAX_VALUE)) > 0) { + count += amt; + } + } + return count; + } finally { + cursor.close(); + } + } + + public CursorFactory getActualFactory() throws FetchException { + Storage storage = getActualStorage(); + if (storage == mEngine.getStorage()) { + return this; + } + return new QueryCursorFactory(this, storage); + } + + public boolean printNative(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + return false; + } + + void indent(Appendable app, int indentLevel) throws IOException { + for (int i=0; i + extends AbstractCursorFactory + { + protected final StorableIndex mIndex; + + private final boolean mReverseOrder; + private final boolean mReverseRange; + private final Filter mExactFilter; + private final PropertyFilter[] mInclusiveRangeStartFilters; + private final PropertyFilter[] mExclusiveRangeStartFilters; + private final PropertyFilter[] mInclusiveRangeEndFilters; + private final PropertyFilter[] mExclusiveRangeEndFilters; + + IndexCursorFactory(BaseQueryEngine engine, + StorableIndex index, + boolean reverseOrder, + boolean reverseRange, + Filter exactFilter, + PropertyFilter[] inclusiveRangeStartFilters, + PropertyFilter[] exclusiveRangeStartFilters, + PropertyFilter[] inclusiveRangeEndFilters, + PropertyFilter[] exclusiveRangeEndFilters) + { + super(engine); + mIndex = index; + mExactFilter = exactFilter; + mReverseOrder = reverseOrder; + mReverseRange = reverseRange; + mInclusiveRangeStartFilters = inclusiveRangeStartFilters; + mExclusiveRangeStartFilters = exclusiveRangeStartFilters; + mInclusiveRangeEndFilters = inclusiveRangeEndFilters; + mExclusiveRangeEndFilters = exclusiveRangeEndFilters; + } + + public Cursor openCursor(FilterValues values) throws FetchException { + Object[] exactValues = null; + Object rangeStartValue = null; + Object rangeEndValue = null; + BoundaryType rangeStartBoundary = BoundaryType.OPEN; + BoundaryType rangeEndBoundary = BoundaryType.OPEN; + + if (values != null) { + if (mExactFilter != null) { + exactValues = values.getValuesFor(mExactFilter); + } + + // In determining the proper range values and boundary types, + // the order in which this code runs is important. The exclusive + // filters must be checked before the inclusive filters. + + for (PropertyFilter p : mExclusiveRangeStartFilters) { + Object value = values.getValue(p); + if (rangeStartBoundary == BoundaryType.OPEN || + compareWithNullHigh(value, rangeStartValue) > 0) + { + rangeStartValue = value; + rangeStartBoundary = BoundaryType.EXCLUSIVE; + } + } + + for (PropertyFilter p : mInclusiveRangeStartFilters) { + Object value = values.getValue(p); + if (rangeStartBoundary == BoundaryType.OPEN || + compareWithNullHigh(value, rangeStartValue) > 0) + { + rangeStartValue = value; + rangeStartBoundary = BoundaryType.INCLUSIVE; + } + } + + for (PropertyFilter p : mExclusiveRangeEndFilters) { + Object value = values.getValue(p); + if (rangeEndBoundary == BoundaryType.OPEN || + compareWithNullHigh(value, rangeEndValue) < 0) + { + rangeEndValue = value; + rangeEndBoundary = BoundaryType.EXCLUSIVE; + } + } + + for (PropertyFilter p : mInclusiveRangeEndFilters) { + Object value = values.getValue(p); + if (rangeEndBoundary == BoundaryType.OPEN || + compareWithNullHigh(value, rangeEndValue) < 0) + { + rangeEndValue = value; + rangeEndBoundary = BoundaryType.INCLUSIVE; + } + } + } + + return mEngine.openCursor(mIndex, exactValues, + rangeStartBoundary, rangeStartValue, + rangeEndBoundary, rangeEndValue, + mReverseRange, + mReverseOrder); + } + + public Filter buildFilter(Filter filter) { + if (mExactFilter != null) { + filter = filter == null ? mExactFilter : filter.and(mExactFilter); + } + for (PropertyFilter p : mInclusiveRangeStartFilters) { + filter = filter == null ? p : filter.and(p); + } + for (PropertyFilter p : mExclusiveRangeStartFilters) { + filter = filter == null ? p : filter.and(p); + } + for (PropertyFilter p : mInclusiveRangeEndFilters) { + filter = filter == null ? p : filter.and(p); + } + for (PropertyFilter p : mExclusiveRangeEndFilters) { + filter = filter == null ? p : filter.and(p); + } + return filter; + } + + public Query applyOrderBy(Query query) throws FetchException { + if (mIndex == null) { + // Index is null if this is a full scan with no ordering specified. + return query; + } + + int count = mIndex.getPropertyCount(); + String[] orderBy = new String[count]; + + for (int i=0; i getActualStorage() { + return mEngine.getStorageFor(mIndex); + } + + public boolean printPlan(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + indent(app, indentLevel); + if (mReverseOrder) { + app.append("reverse "); + } + if (mIndex.isClustered()) { + app.append("clustered "); + } + app.append("index scan: "); + app.append(mEngine.getStorableInfo().getStorableType().getName()); + app.append('\n'); + indent(app, indentLevel); + app.append("...index: "); + mIndex.appendTo(app); + app.append('\n'); + if (mExactFilter != null) { + indent(app, indentLevel); + app.append("...exact filter: "); + mExactFilter.appendTo(app, values); + app.append('\n'); + } + if (mInclusiveRangeStartFilters.length > 0 || mExclusiveRangeStartFilters.length > 0 || + mInclusiveRangeEndFilters.length > 0 || mExclusiveRangeEndFilters.length > 0) + { + indent(app, indentLevel); + app.append("...range filter: "); + int count = 0; + for (PropertyFilter p : mExclusiveRangeStartFilters) { + if (count++ > 0) { + app.append(" & "); + } + p.appendTo(app, values); + } + for (PropertyFilter p : mInclusiveRangeStartFilters) { + if (count++ > 0) { + app.append(" & "); + } + p.appendTo(app, values); + } + for (PropertyFilter p : mExclusiveRangeEndFilters) { + if (count++ > 0) { + app.append(" & "); + } + p.appendTo(app, values); + } + for (PropertyFilter p : mInclusiveRangeEndFilters) { + if (count++ > 0) { + app.append(" & "); + } + p.appendTo(app, values); + } + app.append('\n'); + } + return true; + } + } + + private static class FullScanCursorFactory extends IndexCursorFactory { + FullScanCursorFactory(BaseQueryEngine engine, StorableIndex index) { + super(engine, index, false, false, + null, NO_FILTERS, NO_FILTERS, NO_FILTERS, NO_FILTERS); + } + + @Override + public Filter buildFilter(Filter filter) { + // Full scan doesn't filter anything. + return filter; + } + + @Override + public boolean printPlan(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + indent(app, indentLevel); + app.append("full scan: "); + app.append(mEngine.getStorableInfo().getStorableType().getName()); + app.append('\n'); + return true; + } + } + + private static class KeyCursorFactory extends AbstractCursorFactory { + private final StorableIndex mIndex; + private final Filter mExactFilter; + + KeyCursorFactory(BaseQueryEngine engine, + StorableIndex index, Filter exactFilter) { + super(engine); + mIndex = index; + mExactFilter = exactFilter; + } + + public Cursor openCursor(FilterValues values) throws FetchException { + return mEngine.openKeyCursor(mIndex, values.getValuesFor(mExactFilter)); + } + + public Filter buildFilter(Filter filter) { + if (mExactFilter != null) { + filter = filter == null ? mExactFilter : filter.and(mExactFilter); + } + return filter; + } + + public Query applyOrderBy(Query query) { + return query; + } + + public Storage getActualStorage() { + return mEngine.getStorageFor(mIndex); + } + + public boolean printPlan(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + indent(app, indentLevel); + app.append("index key: "); + app.append(mEngine.getStorableInfo().getStorableType().getName()); + app.append('\n'); + indent(app, indentLevel); + app.append("...index: "); + mIndex.appendTo(app); + app.append('\n'); + indent(app, indentLevel); + app.append("...exact filter: "); + mExactFilter.appendTo(app, values); + app.append('\n'); + return true; + } + } + + private static class FilteredCursorFactory + extends AbstractCursorFactory + { + private final CursorFactory mFactory; + private final Filter mFilter; + + FilteredCursorFactory(BaseQueryEngine engine, + CursorFactory factory, Filter filter) { + super(engine); + mFactory = factory; + mFilter = filter; + } + + public Cursor openCursor(FilterValues values) throws FetchException { + return FilteredCursor.applyFilter(mFilter, + values, + mFactory.openCursor(values)); + } + + public Filter buildFilter(Filter filter) { + filter = mFactory.buildFilter(filter); + filter = filter == null ? mFilter : filter.and(mFilter); + return filter; + } + + public Query applyOrderBy(Query query) throws FetchException { + return mFactory.applyOrderBy(query); + } + + public Storage getActualStorage() { + return mFactory.getActualStorage(); + } + + public boolean printPlan(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + indent(app, indentLevel); + app.append("filter: "); + mFilter.appendTo(app, values); + app.append('\n'); + mFactory.printPlan(app, indentLevel + 2, values); + return true; + } + } + + private static class SortedCursorFactory extends AbstractCursorFactory { + private final CursorFactory mFactory; + private final OrderedProperty[] mHandledOrderings; + private final OrderedProperty[] mRemainderOrderings; + + private final Comparator mHandledComparator; + private final Comparator mFinisherComparator; + + SortedCursorFactory(BaseQueryEngine engine, + CursorFactory factory, + OrderedProperty[] handledOrderings, + OrderedProperty[] remainderOrderings) { + super(engine); + mFactory = factory; + if (handledOrderings != null && handledOrderings.length == 0) { + handledOrderings = null; + } + if (remainderOrderings != null && remainderOrderings.length == 0) { + remainderOrderings = null; + } + if (handledOrderings == null && remainderOrderings == null) { + throw new IllegalArgumentException(); + } + mHandledOrderings = handledOrderings; + mRemainderOrderings = remainderOrderings; + + mHandledComparator = engine.makeComparator(handledOrderings); + mFinisherComparator = engine.makeComparator(remainderOrderings); + } + + public Cursor openCursor(FilterValues values) throws FetchException { + Cursor cursor = mFactory.openCursor(values); + + SortBuffer buffer = new MergeSortBuffer + (getActualStorage(), mEngine.mMergeSortTempDir); + + return new SortedCursor(cursor, buffer, mHandledComparator, mFinisherComparator); + } + + @Override + public long count(FilterValues values) throws FetchException { + return mFactory.count(values); + } + + + public Filter buildFilter(Filter filter) { + return mFactory.buildFilter(filter); + } + + public Query applyOrderBy(Query query) throws FetchException { + int handledLength = mHandledOrderings == null ? 0 : mHandledOrderings.length; + int remainderLength = mRemainderOrderings == null ? 0 : mRemainderOrderings.length; + String[] orderBy = new String[handledLength + remainderLength]; + int pos = 0; + for (int i=0; i getActualStorage() { + return mFactory.getActualStorage(); + } + + public boolean printPlan(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + indent(app, indentLevel); + if (mHandledOrderings == null) { + app.append("full sort: "); + } else { + app.append("finish sort: "); + } + app.append(Arrays.toString(mRemainderOrderings)); + app.append('\n'); + mFactory.printPlan(app, indentLevel + 2, values); + return true; + } + } + + private static class UnionedCursorFactory + extends AbstractCursorFactory + { + static CursorFactory createUnion + (BaseQueryEngine engine, + List> factories, + OrderedProperty[] totalOrderings) + { + Comparator orderComparator = engine.makeComparator(totalOrderings); + return createUnion(engine, factories, totalOrderings, orderComparator); + } + + @SuppressWarnings("unchecked") + static CursorFactory createUnion + (BaseQueryEngine engine, + List> factories, + OrderedProperty[] totalOrderings, + Comparator orderComparator) + { + if (factories.size() > 1) { + CursorFactory[] array = new CursorFactory[factories.size()]; + factories.toArray(array); + return new UnionedCursorFactory(engine, array, totalOrderings, orderComparator); + } + return factories.get(0); + } + + private final CursorFactory[] mFactories; + private final OrderedProperty[] mTotalOrderings; + private final Comparator mOrderComparator; + + private UnionedCursorFactory(BaseQueryEngine engine, + CursorFactory[] factories, + OrderedProperty[] totalOrderings, + Comparator orderComparator) { + super(engine); + mFactories = factories; + mTotalOrderings = totalOrderings; + mOrderComparator = orderComparator; + } + + public Cursor openCursor(FilterValues values) throws FetchException { + Cursor cursor = null; + for (CursorFactory factory : mFactories) { + Cursor subCursor = factory.openCursor(values); + cursor = (cursor == null) ? subCursor + : new UnionCursor(cursor, subCursor, mOrderComparator); + } + return cursor; + } + + public Filter buildFilter(Filter filter) { + for (CursorFactory factory : mFactories) { + Filter subFilter = factory.buildFilter(null); + filter = filter == null ? subFilter : filter.or(subFilter); + } + return filter; + } + + public Query applyOrderBy(Query query) throws FetchException { + if (mTotalOrderings == null || mTotalOrderings.length == 0) { + return query; + } + + String[] orderBy = new String[mTotalOrderings.length]; + for (int i=mTotalOrderings.length; --i>=0; ) { + orderBy[i] = mTotalOrderings[i].toString(); + } + + return query.orderBy(orderBy); + } + + public Storage getActualStorage() { + Storage storage = null; + for (CursorFactory factory : mFactories) { + Storage subStorage = factory.getActualStorage(); + if (storage == null) { + storage = subStorage; + } else if (storage != subStorage) { + return null; + } + } + return storage; + } + + @Override + public CursorFactory getActualFactory() throws FetchException { + Storage requiredStorage = getActualStorage(); + if (requiredStorage == mEngine.getStorage()) { + // Alternate not really needed. + return this; + } + if (requiredStorage != null) { + // All components require same external storage, so let + // external storage do the union. + return new QueryCursorFactory(this, requiredStorage); + } + + // Group factories by required storage instance, and then create a + // union of unions. + + Comparator> comparator = new Comparator>() { + public int compare(CursorFactory a, CursorFactory b) { + Storage aStorage = a.getActualStorage(); + Storage bStorage = b.getActualStorage(); + if (aStorage == bStorage) { + return 0; + } + Storage engineStorage = mEngine.getStorage(); + if (aStorage == engineStorage) { + return -1; + } else if (bStorage == engineStorage) { + return 1; + } + int aHash = System.identityHashCode(a); + int bHash = System.identityHashCode(b); + if (aHash < bHash) { + return -1; + } else if (aHash > bHash) { + return 1; + } + return 0; + } + }; + + Arrays.sort(mFactories, comparator); + + List> masterList = new ArrayList>(); + + List> subList = new ArrayList>(); + Storage group = null; + for (CursorFactory factory : mFactories) { + Storage storage = factory.getActualStorage(); + if (group != storage) { + if (subList.size() > 0) { + masterList.add(createUnion + (mEngine, subList, mTotalOrderings, mOrderComparator)); + subList.clear(); + } + group = storage; + } + CursorFactory subFactory = new QueryCursorFactory(factory, storage); + subList.add(subFactory); + } + if (subList.size() > 0) { + masterList.add(createUnion(mEngine, subList, mTotalOrderings, mOrderComparator)); + subList.clear(); + } + + return createUnion(mEngine, masterList, mTotalOrderings, mOrderComparator); + } + + public boolean printPlan(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + indent(app, indentLevel); + app.append("union"); + app.append('\n'); + for (CursorFactory factory : mFactories) { + factory.printPlan(app, indentLevel + 2, values); + } + return true; + } + } + + /** + * CursorFactory implementation that reconstructs and calls an external + * Query. + */ + private static class QueryCursorFactory implements CursorFactory { + private final CursorFactory mFactory; + private final Storage mStorage; + private final Query mQuery; + + /** + * @param factory factory to derive this factory from + * @param storage actual storage to query against + */ + QueryCursorFactory(CursorFactory factory, Storage storage) throws FetchException { + mFactory = factory; + mStorage = storage; + + Filter filter = factory.buildFilter(null); + + Query query; + if (filter == null) { + query = storage.query(); + } else { + query = storage.query(filter); + } + + mQuery = factory.applyOrderBy(query); + } + + public Cursor openCursor(FilterValues values) throws FetchException { + return applyFilterValues(values).fetch(); + } + + public long count(FilterValues values) throws FetchException { + return applyFilterValues(values).count(); + } + + public Filter buildFilter(Filter filter) { + return mFactory.buildFilter(filter); + } + + public Query applyOrderBy(Query query) throws FetchException { + return mFactory.applyOrderBy(query); + } + + public Storage getActualStorage() { + return mStorage; + } + + public CursorFactory getActualFactory() { + return this; + } + + public boolean printNative(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + return applyFilterValues(values).printNative(app, indentLevel); + } + + public boolean printPlan(Appendable app, int indentLevel, FilterValues values) + throws IOException + { + Query query; + try { + query = applyFilterValues(values); + } catch (IllegalStateException e) { + query = mQuery; + } + return query.printPlan(app, indentLevel); + } + + private Query applyFilterValues(FilterValues values) { + // FIXME: figure out how to transfer values directly to query. + + Query query = mQuery; + Filter filter = query.getFilter(); + // FIXME: this code can get confused if filter has constants. + if (values != null && filter != null && query.getBlankParameterCount() != 0) { + query = query.withValues(values.getValuesFor(filter)); + } + return query; + } + } + + private static class IndexAnalysis extends Visitor + implements Comparable> + { + private final StorableIndex mPrimaryKeyIndex; + private final StorableIndexSet mIndexSet; + private final OrderedProperty[] mOrderings; + + private List> mResults; + + IndexAnalysis(StorableIndex primaryKeyIndex, + StorableIndexSet indexSet, + OrderedProperty[] orderings) + { + mPrimaryKeyIndex = primaryKeyIndex; + mIndexSet = indexSet; + mOrderings = orderings; + mResults = new ArrayList>(); + } + + public Object visit(OrFilter filter, Object param) { + Filter left = filter.getLeftFilter(); + if (!(left instanceof OrFilter)) { + selectIndex(left); + } else { + left.accept(this, param); + } + Filter right = filter.getRightFilter(); + if (!(right instanceof OrFilter)) { + selectIndex(right); + } else { + right.accept(this, param); + } + return null; + } + + // This method should only be called if root filter has no 'or' operators. + public Object visit(AndFilter filter, Object param) { + selectIndex(filter); + return null; + } + + // This method should only be called if root filter has no logical operators. + public Object visit(PropertyFilter filter, Object param) { + selectIndex(filter); + return null; + } + + /** + * Compares this analysis to another which belongs to a different + * Storable type. Filters that reference a joined property may be best + * served by an index defined in the joined type, and this method aids + * in that selection. + * + * @return <0 if these results are better, 0 if equal, or >0 if other is better + */ + public int compareTo(IndexAnalysis otherAnalysis) { + if (noBestIndex()) { + if (otherAnalysis.noBestIndex()) { + return 0; + } + return 1; + } else if (otherAnalysis.noBestIndex()) { + return -1; + } else { + return IndexSelector.listCompare(mResults, otherAnalysis.mResults); + } + } + + /** + * If more than one result returned, then a union must be performed. + * This is because there exist 'or' operators in the full filter. + */ + List> getResults() { + return mResults; + } + + /** + * If more than one result, then a union must be performed. Attempt to + * reduce the result list by performing unions at the index layer. This + * reduces the number of cursors that need to be opened for a query, + * eliminating duplicate work. + */ + void reduceResults() { + if (mResults.size() <= 1) { + return; + } + + List> reduced = + new ArrayList>(mResults.size()); + + gather: + for (int i=0; i result : mResults) { + if (result.isUseless()) { + return true; + } + } + return false; + } + + private void selectIndex(Filter filter) { + IndexSelector selector = new IndexSelector(filter, mOrderings); + + StorableIndex best = mPrimaryKeyIndex; + if (mIndexSet != null) { + for (StorableIndex candidate : mIndexSet) { + int result = selector.compare(best, candidate); + if (result > 0) { + best = candidate; + } + } + } + + mResults.add(selector.examine(best)); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/BelatedRepositoryCreator.java b/src/main/java/com/amazon/carbonado/spi/BelatedRepositoryCreator.java new file mode 100644 index 0000000..9674560 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/BelatedRepositoryCreator.java @@ -0,0 +1,149 @@ +/* + * 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.spi; + +import org.apache.commons.logging.Log; + +import com.amazon.carbonado.IsolationLevel; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.RepositoryBuilder; +import static com.amazon.carbonado.RepositoryBuilder.RepositoryReference; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.Transaction; + +import com.amazon.carbonado.capability.Capability; + +import com.amazon.carbonado.util.BelatedCreator; + +/** + * Generic one-shot Repository builder which supports late object creation. If + * the Repository building results in an exception or is taking too long, the + * Repository produced instead is a bogus one. Many operations result in an + * IllegalStateException. After retrying, if the real Repository is created, + * then the bogus Repository turns into a wrapper to the real Repository. + * + * @author Brian S O'Neill + * @see BelatedStorageCreator + */ +public class BelatedRepositoryCreator extends BelatedCreator { + final Log mLog; + final RepositoryBuilder mBuilder; + final RepositoryReference mRootRef; + + /** + * @param log error reporting log + * @param builder builds real Repository + * @param minRetryDelayMillis minimum milleseconds to wait before retrying + * to create object after failure; if negative, never retry + */ + public BelatedRepositoryCreator(Log log, RepositoryBuilder builder, int minRetryDelayMillis) { + this(log, builder, new RepositoryReference(), minRetryDelayMillis); + } + + /** + * @param log error reporting log + * @param builder builds real Repository + * @param rootRef reference to root repository + * @param minRetryDelayMillis minimum milleseconds to wait before retrying + * to create object after failure; if negative, never retry + */ + public BelatedRepositoryCreator(Log log, + RepositoryBuilder builder, + RepositoryReference rootRef, + int minRetryDelayMillis) + { + super(Repository.class, minRetryDelayMillis); + mLog = log; + mBuilder = builder; + mRootRef = rootRef; + } + + protected Repository createReal() throws SupportException { + Exception error; + try { + return mBuilder.build(mRootRef); + } catch (SupportException e) { + // Cannot recover from this. + throw e; + } catch (RepositoryException e) { + Throwable cause = e.getCause(); + if (cause instanceof ClassNotFoundException) { + // If a class cannot be loaded, then I don't expect this to be + // a recoverable situation. + throw new SupportException(cause); + } + error = e; + } catch (Exception e) { + error = e; + } + mLog.error("Error building Repository \"" + mBuilder.getName() + '"', error); + return null; + } + + protected Repository createBogus() { + return new BogusRepository(); + } + + protected void timedOutNotification(long timedOutMillis) { + mLog.error("Timed out waiting for Repository \"" + mBuilder.getName() + + "\" to build after waiting " + timedOutMillis + + " milliseconds"); + } + + private class BogusRepository implements Repository { + public String getName() { + return mBuilder.getName(); + } + + public synchronized Storage storageFor(Class type) { + throw error(); + } + + public Transaction enterTransaction() { + throw error(); + } + + public Transaction enterTransaction(IsolationLevel level) { + throw error(); + } + + public Transaction enterTopTransaction(IsolationLevel level) { + throw error(); + } + + public IsolationLevel getTransactionIsolationLevel() { + return null; + } + + public C getCapability(Class capabilityType) { + throw error(); + } + + public void close() { + } + + private IllegalStateException error() { + return new IllegalStateException + ("Creation of Repository \"" + mBuilder.getName() + "\" is delayed"); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/BelatedStorageCreator.java b/src/main/java/com/amazon/carbonado/spi/BelatedStorageCreator.java new file mode 100644 index 0000000..88d0f61 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/BelatedStorageCreator.java @@ -0,0 +1,138 @@ +/* + * 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.spi; + +import org.apache.commons.logging.Log; + +import com.amazon.carbonado.Query; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.Trigger; + +import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.filter.FilterValues; + +import com.amazon.carbonado.util.BelatedCreator; + +/** + * Generic one-shot Storage creator which supports late object creation. If + * getting the Storage results in an exception or is taking too long, the + * Storage produced instead is a bogus one. Many operations result in an + * IllegalStateException. After retrying, if the real Storage is accessed, then + * the bogus Storage turns into a wrapper to the real Storage. + * + * @author Brian S O'Neill + * @see BelatedRepositoryCreator + */ +public class BelatedStorageCreator + extends BelatedCreator, SupportException> +{ + final Log mLog; + final Repository mRepo; + final Class mStorableType; + + /** + * @param log error reporting log + * @param repo Repository to get Storage from + * @param storableType type of Storable to get Storage for + * @param minRetryDelayMillis minimum milleseconds to wait before retrying + * to create object after failure; if negative, never retry + */ + public BelatedStorageCreator(Log log, Repository repo, Class storableType, + int minRetryDelayMillis) { + // Nice double cast hack, eh? + super((Class>) ((Class) Storage.class), minRetryDelayMillis); + mLog = log; + mRepo = repo; + mStorableType = storableType; + } + + protected Storage createReal() throws SupportException { + Exception error; + try { + return mRepo.storageFor(mStorableType); + } catch (SupportException e) { + // Cannot recover from this. + throw e; + } catch (RepositoryException e) { + Throwable cause = e.getCause(); + if (cause instanceof ClassNotFoundException) { + // If a class cannot be loaded, then I don't expect this to be + // a recoverable situation. + throw new SupportException(cause); + } + error = e; + } catch (Exception e) { + error = e; + } + mLog.error("Error getting Storage of type \"" + mStorableType.getName() + '"', error); + return null; + } + + protected Storage createBogus() { + return new BogusStorage(); + } + + protected void timedOutNotification(long timedOutMillis) { + mLog.error("Timed out waiting to get Storage of type \"" + mStorableType.getName() + + "\" after waiting " + timedOutMillis + " milliseconds"); + } + + private class BogusStorage implements Storage { + public Class getStorableType() { + return mStorableType; + } + + public S prepare() { + throw error(); + } + + public Query query() { + throw error(); + } + + public Query query(String filter) { + throw error(); + } + + public Query query(Filter filter) { + throw error(); + } + + public Repository getRepository() { + return mRepo; + } + + public boolean addTrigger(Trigger trigger) { + throw error(); + } + + public boolean removeTrigger(Trigger trigger) { + throw error(); + } + + private IllegalStateException error() { + return new IllegalStateException + ("Creation of Storage for type \"" + mStorableType.getName() + "\" is delayed"); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/BlobProperty.java b/src/main/java/com/amazon/carbonado/spi/BlobProperty.java new file mode 100644 index 0000000..b4ea500 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/BlobProperty.java @@ -0,0 +1,55 @@ +/* + * 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.spi; + +import java.io.IOException; + +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.RepositoryException; + +import com.amazon.carbonado.lob.Blob; + +/** + * + * + * @author Brian S O'Neill + * @see LobEngine + * @see LobEngineTrigger + */ +class BlobProperty extends LobProperty { + BlobProperty(LobEngine engine, String propertyName) { + super(engine, propertyName); + } + + Blob createNewLob(int blockSize) throws PersistException { + return mEngine.createNewBlob(blockSize); + } + + void setLobValue(long locator, Blob data) throws PersistException { + try { + mEngine.setBlobValue(locator, data); + } catch (IOException e) { + Throwable cause = e.getCause(); + if (cause instanceof RepositoryException) { + throw ((RepositoryException) cause).toPersistException(); + } + throw new PersistException(e); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/ClobProperty.java b/src/main/java/com/amazon/carbonado/spi/ClobProperty.java new file mode 100644 index 0000000..dc9d196 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/ClobProperty.java @@ -0,0 +1,55 @@ +/* + * 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.spi; + +import java.io.IOException; + +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.RepositoryException; + +import com.amazon.carbonado.lob.Clob; + +/** + * + * + * @author Brian S O'Neill + * @see LobEngine + * @see LobEngineTrigger + */ +class ClobProperty extends LobProperty { + ClobProperty(LobEngine engine, String propertyName) { + super(engine, propertyName); + } + + Clob createNewLob(int blockSize) throws PersistException { + return mEngine.createNewClob(blockSize); + } + + void setLobValue(long locator, Clob data) throws PersistException { + try { + mEngine.setClobValue(locator, data); + } catch (IOException e) { + Throwable cause = e.getCause(); + if (cause instanceof RepositoryException) { + throw ((RepositoryException) cause).toPersistException(); + } + throw new PersistException(e); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/CodeBuilderUtil.java b/src/main/java/com/amazon/carbonado/spi/CodeBuilderUtil.java new file mode 100644 index 0000000..4e2e777 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/CodeBuilderUtil.java @@ -0,0 +1,400 @@ +/* + * 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.spi; + +import java.util.HashSet; +import java.util.Set; +import java.util.Map; +import java.util.HashMap; +import java.lang.reflect.Method; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Label; +import org.cojen.classfile.TypeDesc; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodDesc; +import org.cojen.util.ClassInjector; + +import com.amazon.carbonado.Storable; + +import static com.amazon.carbonado.spi.CommonMethodNames.*; + +/** + * Collection of useful utilities for generating Carbonado code. + * + * @author Don Schneider + * @author Brian S O'Neill + */ +public class CodeBuilderUtil { + + /** + * Generate code to throw an exception if a parameter is null + * @param b CodeBuilder into which to append the code + * @param paramIndex index of the parameter to check + */ + public static void assertParameterNotNull(CodeBuilder b, int paramIndex) { + b.loadLocal(b.getParameter(paramIndex)); + Label notNull = b.createLabel(); + b.ifNullBranch(notNull, false); + throwException(b, IllegalArgumentException.class, null); + notNull.setLocation(); + } + + /** + * Generate code to create a local variable containing the specified parameter coerced + * to the specified type. This is useful for re-interpreting erased generics into + * the more specific genericized type. + * + * @param b CodeBuilder into which to append the code + * @param paramType the more specific type which was erased during compilation + * @param paramIndex index of the parameter to unerase + * @return a local variable referencing the type-cast parameter + */ + public static LocalVariable uneraseGenericParameter( + CodeBuilder b, TypeDesc paramType, final int paramIndex) + { + b.loadLocal(b.getParameter(paramIndex)); + b.checkCast(paramType); + LocalVariable result = b.createLocalVariable(null, paramType); + b.storeLocal(result); + return result; + } + + /** + * Generate code to throw an exception with an optional message. + * @param b {@link CodeBuilder} to which to add code + * @param type type of the object to throw + * @param message optional message to provide to the constructor + */ + public static void throwException(CodeBuilder b, Class type, String message) { + TypeDesc desc = TypeDesc.forClass(type); + b.newObject(desc); + b.dup(); + if (message == null) { + b.invokeConstructor(desc, null); + } else { + b.loadConstant(message); + b.invokeConstructor(desc, new TypeDesc[] {TypeDesc.STRING}); + } + b.throwObject(); + } + + /** + * Collect a set of all the interfaces and recursively all superclasses for the leaf + * (genericised class) and root (genericised base class). Eg, for Object, all + * classes and implemented interfaces for every superclass between foo (the leaf) and + * Object (the base). + *

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

Note that the official documentation for this is in draft form, and you have to be + * psychic to have figured out the necessity in the first place. + * + * @param set set into which the class types will be collected + * @param leaf leaf class + * @return same set as was passed in + */ + public static Set gatherAllBridgeTypes(Set set, Class leaf) { + set.add(leaf); + for (Class c : leaf.getInterfaces()) { + gatherAllBridgeTypes(set, c); + } + if ((leaf = leaf.getSuperclass()) != null) { + gatherAllBridgeTypes(set, leaf); + } + return set; + } + + /** + * Add copy bridge methods for all classes/interfaces between the leaf (genericised class) + * and the root (genericised baseclass). + * + * @param cf file to which to add the copy bridge + * @param leaf leaf class + */ + public static void defineCopyBridges(ClassFile cf, Class leaf) { + for (Class c : gatherAllBridgeTypes(new HashSet(), leaf)) { + if (c != Object.class) { + defineCopyBridge(cf, c); + } + } + } + + /** + * Add a copy bridge method to the classfile for the given type. This is needed to allow + * the genericised class make a copy itself -- which will be erased to the base type -- and + * return it as the correct type. + * + * @param cf file to which to add the copy bridge + * @param returnClass type returned from generated bridge method + */ + public static void defineCopyBridge(ClassFile cf, Class returnClass) { + TypeDesc returnType = TypeDesc.forClass(returnClass); + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC.toBridge(true), + COPY_METHOD_NAME, returnType, null); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.invokeVirtual(COPY_METHOD_NAME, cf.getType(), null); + b.returnValue(returnType); + } + + /** + * Returns a new modifiable mapping of method signatures to methods. + * + * @return map of {@link #createSig signatures} to methods + */ + public static Map gatherAllDeclaredMethods(Class clazz) { + Map methods = new HashMap(); + gatherAllDeclaredMethods(methods, clazz); + return methods; + } + + private static void gatherAllDeclaredMethods(Map methods, Class clazz) { + for (Method m : clazz.getDeclaredMethods()) { + String desc = createSig(m); + if (!methods.containsKey(desc)) { + methods.put(desc, m); + } + } + + Class superclass = clazz.getSuperclass(); + if (superclass != null) { + gatherAllDeclaredMethods(methods, superclass); + } + for (Class c : clazz.getInterfaces()) { + gatherAllDeclaredMethods(methods, c); + } + } + + /** + * Define a classfile appropriate for most Storables. Specifically: + *

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

The generated instruction consumes both values on the stack. + * + * @param b {@link CodeBuilder} to which to add the code + * @param valueType the type of the values + * @param testForNull if true and the values are references, they will be considered + * unequal unless neither or both are null. If false, assume neither is null. + * @param label the label to branch to + * @param choice when true, branch to label if values are equal, else + * branch to label if values are unequal. + */ + public static void addValuesEqualCall(final CodeBuilder b, + final TypeDesc valueType, + final boolean testForNull, + final Label label, + final boolean choice) + { + if (valueType.getTypeCode() != TypeDesc.OBJECT_CODE) { + b.ifComparisonBranch(label, choice ? "==" : "!=", valueType); + return; + } + + // Equals method returns zero for false, so if choice is true, branch + // if not zero. Note that operator selection is opposite when invoking + // a direct ifComparisonBranch method. + String equalsBranchOp = choice ? "!=" : "=="; + + if (!testForNull) { + addEqualsCallTo(b, valueType); + b.ifZeroComparisonBranch(label, equalsBranchOp); + return; + } + + Label isNotNull = b.createLabel(); + LocalVariable value = b.createLocalVariable(null, valueType); + b.storeLocal(value); + b.loadLocal(value); + b.ifNullBranch(isNotNull, false); + + // First value popped off stack is null. Just test remaining one for null. + b.ifNullBranch(label, choice); + Label cont = b.createLabel(); + b.branch(cont); + + // First value popped off stack is not null, but second one might + // be. Call equals method, but swap values so that the second value is + // an argument into the equals method. + isNotNull.setLocation(); + b.loadLocal(value); + b.swap(); + addEqualsCallTo(b, valueType); + b.ifZeroComparisonBranch(label, equalsBranchOp); + + cont.setLocation(); + } + + public static void addEqualsCallTo(CodeBuilder b, TypeDesc fieldType) { + if (fieldType.isArray()) { + if (!fieldType.getComponentType().isPrimitive()) { + TypeDesc type = TypeDesc.forClass(Object[].class); + b.invokeStatic("java.util.Arrays", "deepEquals", + TypeDesc.BOOLEAN, new TypeDesc[] {type, type}); + } else { + b.invokeStatic("java.util.Arrays", "equals", + TypeDesc.BOOLEAN, new TypeDesc[] {fieldType, fieldType}); + } + } else { + TypeDesc[] params = {TypeDesc.OBJECT}; + if (fieldType.toClass() != null) { + if (fieldType.toClass().isInterface()) { + b.invokeInterface(fieldType, "equals", TypeDesc.BOOLEAN, params); + } else { + b.invokeVirtual(fieldType, "equals", TypeDesc.BOOLEAN, params); + } + } else { + b.invokeVirtual(TypeDesc.OBJECT, "equals", TypeDesc.BOOLEAN, params); + } + } + } + + /** + * Create a representation of the signature which includes the method name. + * This uniquely identifies the method. + * + * @param m method to describe + */ + public static String createSig(Method m) { + return m.getName() + ':' + MethodDesc.forMethod(m).getDescriptor(); + } + + /** + * Converts a value on the stack. If "to" type is a String, then conversion + * may call the String.valueOf(from). + */ + public static void convertValue(CodeBuilder b, Class from, Class to) { + if (from == to) { + return; + } + + TypeDesc fromType = TypeDesc.forClass(from); + TypeDesc toType = TypeDesc.forClass(to); + + // Let CodeBuilder have a crack at the conversion first. + try { + b.convert(fromType, toType); + return; + } catch (IllegalArgumentException e) { + if (to != String.class && to != Object.class && to != CharSequence.class) { + throw e; + } + } + + // Fallback case is to convert to a String. + + if (fromType.isPrimitive()) { + b.invokeStatic(TypeDesc.STRING, "valueOf", TypeDesc.STRING, new TypeDesc[]{fromType}); + } else { + // If object on stack is null, then just leave it alone. + b.dup(); + Label isNull = b.createLabel(); + b.ifNullBranch(isNull, true); + b.invokeStatic(TypeDesc.STRING, "valueOf", TypeDesc.STRING, + new TypeDesc[]{TypeDesc.OBJECT}); + isNull.setLocation(); + } + } + + /** + * Determines which overloaded "with" method on Query should be bound to. + */ + public static TypeDesc bindQueryParam(Class clazz) { + if (clazz.isPrimitive()) { + TypeDesc type = TypeDesc.forClass(clazz); + switch (type.getTypeCode()) { + case TypeDesc.INT_CODE: + case TypeDesc.LONG_CODE: + case TypeDesc.FLOAT_CODE: + case TypeDesc.DOUBLE_CODE: + return type; + } + } + return TypeDesc.OBJECT; + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/CommonMethodNames.java b/src/main/java/com/amazon/carbonado/spi/CommonMethodNames.java new file mode 100644 index 0000000..8a011c9 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/CommonMethodNames.java @@ -0,0 +1,87 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.carbonado.spi; + +/** + * Collection of constant method names for the public API. + * + * @author Brian S O'Neill + */ +public class CommonMethodNames { + /** Storable API method name */ + public static final String + LOAD_METHOD_NAME = "load", + INSERT_METHOD_NAME = "insert", + UPDATE_METHOD_NAME = "update", + DELETE_METHOD_NAME = "delete", + TRY_LOAD_METHOD_NAME = "tryLoad", + TRY_INSERT_METHOD_NAME = "tryInsert", + TRY_UPDATE_METHOD_NAME = "tryUpdate", + TRY_DELETE_METHOD_NAME = "tryDelete", + STORABLE_TYPE_METHOD_NAME = "storableType", + COPY_METHOD_NAME = "copy", + CLONE_METHOD_NAME = "clone", + COPY_ALL_PROPERTIES = "copyAllProperties", + COPY_PRIMARY_KEY_PROPERTIES = "copyPrimaryKeyProperties", + COPY_VERSION_PROPERTY = "copyVersionProperty", + COPY_UNEQUAL_PROPERTIES = "copyUnequalProperties", + COPY_DIRTY_PROPERTIES = "copyDirtyProperties", + HAS_DIRTY_PROPERTIES = "hasDirtyProperties", + MARK_PROPERTIES_CLEAN = "markPropertiesClean", + MARK_ALL_PROPERTIES_CLEAN = "markAllPropertiesClean", + MARK_PROPERTIES_DIRTY = "markPropertiesDirty", + MARK_ALL_PROPERTIES_DIRTY = "markAllPropertiesDirty", + IS_PROPERTY_UNINITIALIZED = "isPropertyUninitialized", + IS_PROPERTY_DIRTY = "isPropertyDirty", + IS_PROPERTY_CLEAN = "isPropertyClean", + IS_PROPERTY_SUPPORTED = "isPropertySupported", + TO_STRING_KEY_ONLY_METHOD_NAME = "toStringKeyOnly", + TO_STRING_METHOD_NAME = "toString", + HASHCODE_METHOD_NAME = "hashCode", + EQUALS_METHOD_NAME = "equals", + EQUAL_PRIMARY_KEYS_METHOD_NAME = "equalPrimaryKeys", + EQUAL_PROPERTIES_METHOD_NAME = "equalProperties"; + + /** Storage API method name */ + public static final String + QUERY_METHOD_NAME = "query", + PREPARE_METHOD_NAME = "prepare"; + + /** Query API method name */ + public static final String + LOAD_ONE_METHOD_NAME = "loadOne", + TRY_LOAD_ONE_METHOD_NAME = "tryLoadOne", + WITH_METHOD_NAME = "with", + FETCH_METHOD_NAME = "fetch"; + + /** Repository API method name */ + public static final String + STORAGE_FOR_METHOD_NAME = "storageFor", + ENTER_TRANSACTION_METHOD_NAME = "enterTransaction", + GET_TRANSACTION_ISOLATION_LEVEL_METHOD_NAME = "getTransactionIsolationLevel"; + + /** Transaction API method name */ + public static final String + SET_FOR_UPDATE_METHOD_NAME = "setForUpdate", + COMMIT_METHOD_NAME = "commit", + EXIT_METHOD_NAME = "exit"; + + /** WrappedStorage.Support API method name */ + public static final String CREATE_WRAPPED_SUPPORT_METHOD_NAME = "createSupport"; +} diff --git a/src/main/java/com/amazon/carbonado/spi/ConversionComparator.java b/src/main/java/com/amazon/carbonado/spi/ConversionComparator.java new file mode 100644 index 0000000..aed00b1 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/ConversionComparator.java @@ -0,0 +1,212 @@ +/* + * 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.spi; + +import java.util.Comparator; + +import org.cojen.classfile.TypeDesc; + +/** + * Compares type conversions, finding the one that is nearest. + * + * @author Brian S O'Neill + */ +public class ConversionComparator implements Comparator { + private final TypeDesc mFrom; + + public ConversionComparator(Class fromType) { + mFrom = TypeDesc.forClass(fromType); + } + + /** + * Returns true if a coversion is possible to the given type. + */ + public boolean isConversionPossible(Class toType) { + return isConversionPossible(mFrom, TypeDesc.forClass(toType)); + } + + @SuppressWarnings("unchecked") + private static boolean isConversionPossible(TypeDesc from, TypeDesc to) { + if (from == to) { + return true; + } + + if (from.toPrimitiveType() != null && to.toPrimitiveType() != null) { + from = from.toPrimitiveType(); + to = to.toPrimitiveType(); + } else { + from = from.toObjectType(); + to = to.toObjectType(); + } + + switch (from.getTypeCode()) { + case TypeDesc.OBJECT_CODE: default: + return to.toClass().isAssignableFrom(from.toClass()); + case TypeDesc.BOOLEAN_CODE: + return to == TypeDesc.BOOLEAN; + case TypeDesc.BYTE_CODE: + return to == TypeDesc.BYTE || to == TypeDesc.SHORT + || to == TypeDesc.INT || to == TypeDesc.LONG + || to == TypeDesc.FLOAT || to == TypeDesc.DOUBLE; + case TypeDesc.SHORT_CODE: + return to == TypeDesc.SHORT + || to == TypeDesc.INT || to == TypeDesc.LONG + || to == TypeDesc.FLOAT || to == TypeDesc.DOUBLE; + case TypeDesc.CHAR_CODE: + return to == TypeDesc.CHAR; + case TypeDesc.INT_CODE: + return to == TypeDesc.INT || to == TypeDesc.LONG || to == TypeDesc.DOUBLE; + case TypeDesc.FLOAT_CODE: + return to == TypeDesc.FLOAT || to == TypeDesc.DOUBLE; + case TypeDesc.LONG_CODE: + return to == TypeDesc.LONG; + case TypeDesc.DOUBLE_CODE: + return to == TypeDesc.DOUBLE; + } + } + + /** + * Evaluates two types, to see which one is nearest to the from type. + * Return <0 if "a" is nearest, 0 if both are equally good, >0 if "b" is + * nearest. + */ + public int compare(Class toType_a, Class toType_b) { + TypeDesc from = mFrom; + TypeDesc a = TypeDesc.forClass(toType_a); + TypeDesc b = TypeDesc.forClass(toType_b); + + if (from == a) { + if (from == b) { + return 0; + } + return -1; + } else if (from == b) { + return 1; + } + + int result = compare(from, a, b); + if (result != 0) { + return result; + } + + if (from.isPrimitive()) { + // Try boxing. + if (from.toObjectType() != null) { + from = from.toObjectType(); + return compare(from, a, b); + } + } else { + // Try unboxing. + if (from.toPrimitiveType() != null) { + from = from.toPrimitiveType(); + result = compare(from, a, b); + if (result != 0) { + return result; + } + // Try boxing back up. Test by unboxing 'to' types. + if (!toType_a.isPrimitive() && a.toPrimitiveType() != null) { + a = a.toPrimitiveType(); + } + if (!toType_b.isPrimitive() && b.toPrimitiveType() != null) { + b = b.toPrimitiveType(); + } + return compare(from, a, b); + } + } + + return 0; + } + + private static int compare(TypeDesc from, TypeDesc a, TypeDesc b) { + if (isConversionPossible(from, a)) { + if (isConversionPossible(from, b)) { + if (from.isPrimitive()) { + if (a.isPrimitive()) { + if (b.isPrimitive()) { + // Choose the one with the least amount of widening. + return primitiveWidth(a) - primitiveWidth(b); + } else { + return -1; + } + } else if (b.isPrimitive()) { + return 1; + } + } else { + // Choose the one with the shortest distance up the class + // hierarchy. + Class fromClass = from.toClass(); + if (!fromClass.isInterface()) { + if (a.toClass().isInterface()) { + if (!b.toClass().isInterface()) { + return -1; + } + } else if (b.toClass().isInterface()) { + return 1; + } else { + return distance(fromClass, a.toClass()) + - distance(fromClass, b.toClass()); + } + } + } + } else { + return -1; + } + } else if (isConversionPossible(from, b)) { + return 1; + } + + return 0; + } + + // 1 = boolean, 2 = byte, 3 = short, 4 = char, 5 = int, 6 = float, 7 = long, 8 = double + private static int primitiveWidth(TypeDesc type) { + switch (type.getTypeCode()) { + default: + return 0; + case TypeDesc.BOOLEAN_CODE: + return 1; + case TypeDesc.BYTE_CODE: + return 2; + case TypeDesc.SHORT_CODE: + return 3; + case TypeDesc.CHAR_CODE: + return 4; + case TypeDesc.INT_CODE: + return 5; + case TypeDesc.FLOAT_CODE: + return 6; + case TypeDesc.LONG_CODE: + return 7; + case TypeDesc.DOUBLE_CODE: + return 8; + } + } + + 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/spi/ExceptionTransformer.java b/src/main/java/com/amazon/carbonado/spi/ExceptionTransformer.java new file mode 100644 index 0000000..2064e68 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/ExceptionTransformer.java @@ -0,0 +1,189 @@ +/* + * 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.spi; + +import java.io.InterruptedIOException; +import java.nio.channels.ClosedByInterruptException; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.FetchInterruptedException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.RepositoryException; + +/** + * Supports transforming arbitrary exceptions into appropriate repository + * exceptions. Repositories will likely extend this class, providing custom + * transformation rules. + * + * @author Brian S O'Neill + */ +public class ExceptionTransformer { + private static ExceptionTransformer cInstance; + + /** + * Returns a generic instance. + */ + public static ExceptionTransformer getInstance() { + if (cInstance == null) { + cInstance = new ExceptionTransformer(); + } + return cInstance; + } + + public ExceptionTransformer() { + } + + /** + * Transforms the given throwable into an appropriate fetch exception. If + * it already is a fetch exception, it is simply casted. + * + * @param e required exception to transform + * @return FetchException, never null + */ + public FetchException toFetchException(Throwable e) { + FetchException fe = transformIntoFetchException(e); + if (fe != null) { + return fe; + } + + Throwable cause = e.getCause(); + if (cause != null) { + fe = transformIntoFetchException(cause); + if (fe != null) { + return fe; + } + } else { + cause = e; + } + + return new FetchException(cause); + } + + /** + * Transforms the given throwable into an appropriate persist exception. If + * it already is a persist exception, it is simply casted. + * + * @param e required exception to transform + * @return PersistException, never null + */ + public PersistException toPersistException(Throwable e) { + PersistException pe = transformIntoPersistException(e); + if (pe != null) { + return pe; + } + + Throwable cause = e.getCause(); + if (cause != null) { + pe = transformIntoPersistException(cause); + if (pe != null) { + return pe; + } + } else { + cause = e; + } + + return new PersistException(cause); + } + + /** + * Transforms the given throwable into an appropriate repository + * exception. If it already is a repository exception, it is simply casted. + * + * @param e required exception to transform + * @return RepositoryException, never null + */ + public RepositoryException toRepositoryException(Throwable e) { + RepositoryException re = transformIntoRepositoryException(e); + if (re != null) { + return re; + } + + Throwable cause = e.getCause(); + if (cause != null) { + re = transformIntoRepositoryException(cause); + if (re != null) { + return re; + } + } else { + cause = e; + } + + return new RepositoryException(cause); + } + + /** + * Override to support custom transformations, returning null if none is + * applicable. Be sure to call super first. If it returns non-null, return + * that result. + * + * @param e required exception to transform + * @return FetchException, or null if no applicable transform + */ + protected FetchException transformIntoFetchException(Throwable e) { + if (e instanceof FetchException) { + return (FetchException) e; + } + if (e instanceof InterruptedIOException || + e instanceof ClosedByInterruptException) { + return new FetchInterruptedException(e); + } + return null; + } + + /** + * Override to support custom transformations, returning null if none is + * applicable. Be sure to call super first. If it returns non-null, return + * that result. + * + * @param e required exception to transform + * @return PersistException, or null if no applicable transform + */ + protected PersistException transformIntoPersistException(Throwable e) { + if (e instanceof PersistException) { + return (PersistException) e; + } + if (e instanceof FetchException) { + return ((FetchException) e).toPersistException(); + } + return null; + } + + /** + * Override to support custom transformations, returning null if none is + * applicable. Be sure to call super first. If it returns non-null, return + * that result. + * + * @param e required exception to transform + * @return RepositoryException, or null if no applicable transform + */ + protected RepositoryException transformIntoRepositoryException(Throwable e) { + if (e instanceof RepositoryException) { + return (RepositoryException) e; + } + RepositoryException re = transformIntoFetchException(e); + if (re != null) { + return re; + } + re = transformIntoPersistException(e); + if (re != null) { + return re; + } + return null; + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/IndexInfoImpl.java b/src/main/java/com/amazon/carbonado/spi/IndexInfoImpl.java new file mode 100644 index 0000000..de16f75 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/IndexInfoImpl.java @@ -0,0 +1,117 @@ +/* + * 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.spi; + +import java.util.Arrays; + +import com.amazon.carbonado.capability.IndexInfo; + +import com.amazon.carbonado.info.Direction; + +/** + * Basic implementation of an {@link IndexInfo}. + * + * @author Brian S O'Neill + */ +public class IndexInfoImpl implements IndexInfo { + private final String mName; + private final boolean mUnique; + private final boolean mClustered; + private final String[] mPropertyNames; + private final Direction[] mPropertyDirections; + + /** + * @param name optional name for index + * @param unique true if index requires unique values + * @param propertyNames required list of property names, must have at least + * one name + * @param propertyDirections optional property directions, may be null or + * same length as property names array + * @throws IllegalArgumentException + */ + public IndexInfoImpl(String name, boolean unique, boolean clustered, + String[] propertyNames, Direction[] propertyDirections) { + mName = name; + mUnique = unique; + mClustered = clustered; + + if (propertyNames == null || propertyNames.length == 0) { + throw new IllegalArgumentException(); + } + + for (int i=propertyNames.length; --i>=0; ) { + if (propertyNames[i] == null) { + throw new IllegalArgumentException(); + } + } + + propertyNames = propertyNames.clone(); + + if (propertyDirections == null) { + propertyDirections = new Direction[propertyNames.length]; + } else { + if (propertyNames.length != propertyDirections.length) { + throw new IllegalArgumentException(); + } + propertyDirections = propertyDirections.clone(); + } + for (int i=propertyDirections.length; --i>=0; ) { + if (propertyDirections[i] == null) { + propertyDirections[i] = Direction.UNSPECIFIED; + } + } + + mPropertyNames = propertyNames; + mPropertyDirections = propertyDirections; + } + + public String getName() { + return mName; + } + + public boolean isUnique() { + return mUnique; + } + + public boolean isClustered() { + return mClustered; + } + + public String[] getPropertyNames() { + return mPropertyNames.clone(); + } + + public Direction[] getPropertyDirections() { + return mPropertyDirections.clone(); + } + + public String toString() { + StringBuilder b = new StringBuilder(); + b.append("IndexInfo {name="); + b.append(mName); + b.append(", unique="); + b.append(mUnique); + b.append(", propertyNames="); + b.append(Arrays.toString(mPropertyNames)); + b.append(", propertyDirections="); + b.append(Arrays.toString(mPropertyDirections)); + b.append('}'); + return b.toString(); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/IndexSelector.java b/src/main/java/com/amazon/carbonado/spi/IndexSelector.java new file mode 100644 index 0000000..660cfdb --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/IndexSelector.java @@ -0,0 +1,1204 @@ +/* + * 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.spi; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.amazon.carbonado.Storable; + +import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.filter.OrFilter; +import com.amazon.carbonado.filter.PropertyFilter; +import com.amazon.carbonado.filter.RelOp; +import com.amazon.carbonado.filter.Visitor; + +import com.amazon.carbonado.info.ChainedProperty; +import com.amazon.carbonado.info.Direction; +import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.info.StorableProperty; + +/** + * Tries to find the best index to use for a query. When used to sort a list of + * indexes, the first in the list (the lowest) is the best index. + * + * @author Brian S O'Neill + */ +public class IndexSelector implements Comparator> { + static int intCompare(int a, int b) { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + } + + // Also called by BaseQueryEngine. + @SuppressWarnings("unchecked") + static int listCompare(List a, + List b) { + int size = Math.min(a.size(), b.size()); + for (int i=0; i size) { + return 1; + } + return 0; + } + + // Original filter passed into constructor + private final Filter mFilter; + + // Elements of original filter, which are combined by logical 'and's. Filters + // which are likely to remove more results are ordered first in the array. + final PropertyFilter[] mFilters; + + final OrderedProperty[] mOrderings; + + /** + * @param filter filter which cannot contain any logical 'or' operations. + * @throws IllegalArgumentException if filter not supported + */ + public IndexSelector(Filter filter) { + this(filter, (OrderedProperty[]) null); + } + + /** + * @param filter optional filter which cannot contain any logical 'or' operations. + * @param orderings optional orderings + * @throws IllegalArgumentException if filter not supported + */ + @SuppressWarnings("unchecked") + public IndexSelector(Filter filter, OrderedProperty... orderings) { + mFilter = filter; + + // Copy property filters. + final List> filterList = new ArrayList>(); + + if (filter != null) { + filter.accept(new Visitor() { + public Object visit(OrFilter filter, Object param) { + throw new IllegalArgumentException("Logical 'or' not allowed"); + } + + public Object visit(PropertyFilter filter, Object param) { + filterList.add(filter); + return null; + } + }, null); + } + + mFilters = filterList.toArray(new PropertyFilter[filterList.size()]); + // Ensure all '=' operators are first, and all '!=' operators are last. + Arrays.sort(mFilters, new PropertyFilterComparator()); + + if (orderings == null || orderings.length == 0) { + mOrderings = null; + } else { + // Copy ordering properties, but don't duplicate properties. + int length = orderings.length; + Map, OrderedProperty> orderingMap = + new LinkedHashMap, OrderedProperty>(length); + for (int i=0; i ordering = orderings[i]; + if (ordering != null) { + ChainedProperty prop = ordering.getChainedProperty(); + if (!orderingMap.containsKey(prop)) { + orderingMap.put(prop, ordering); + } + } + } + + // Drop orderings against exact matches in filter since they aren't needed. + for (PropertyFilter propFilter : filterList) { + if (propFilter.getOperator() == RelOp.EQ) { + orderingMap.remove(propFilter.getChainedProperty()); + } + } + + mOrderings = orderingMap.values().toArray(new OrderedProperty[orderingMap.size()]); + } + } + + /** + * Returns <0 if the current index is better than the candidate index, 0 + * if they are equally good, or >0 if the candidate index is + * better. + *

+ * Note: the best index may sort results totally reversed. The cursor that + * uses this index must iterate in reverse to compensate. + * + * @param currentIndex current "best" index + * @param candidateIndex index to test against + */ + public int compare(StorableIndex currentIndex, StorableIndex candidateIndex) { + if (currentIndex == null) { + if (candidateIndex == null) { + return 0; + } else { + return 1; + } + } else if (candidateIndex == null) { + return -1; + } + + IndexScore currentScore = new IndexScore(this, currentIndex); + IndexScore candidateScore = new IndexScore(this, candidateIndex); + + return currentScore.compareTo(candidateScore); + } + + /** + * Examines the given index for overall fitness. + */ + public IndexFitness examine(StorableIndex index) { + return new IndexFitness(this, index, mFilter, mFilters, mOrderings); + } + + /** + * Provides information regarding the overall fitness of an index for use + * in a query, and gives us information about how we can properly apply it. That is, + * if the index provides 3 out of 7 properties, we'll have to scan the output and apply the + * remaining four by hand. If an index does not sort the property for which we're doing an + * inexact match, we'll have to subsort -- and so on. + */ + public static class IndexFitness implements Comparable> { + private final StorableIndex mIndex; + private final IndexScore mIndexScore; + + private final Filter mExactFilter; + private final PropertyFilter[] mInclusiveRangeStartFilters; + private final PropertyFilter[] mExclusiveRangeStartFilters; + private final PropertyFilter[] mInclusiveRangeEndFilters; + private final PropertyFilter[] mExclusiveRangeEndFilters; + private final Filter mRemainderFilter; + + private final OrderedProperty[] mHandledOrderings; + private final OrderedProperty[] mRemainderOrderings; + + private final boolean mShouldReverseOrder; + private final boolean mShouldReverseRange; + + @SuppressWarnings("unchecked") + IndexFitness(IndexSelector selector, StorableIndex index, + Filter fullFilter, PropertyFilter[] fullFilters, + OrderedProperty[] fullOrderings) + { + mIndex = index; + mIndexScore = new IndexScore(selector, index); + + FilterScore filterScore = mIndexScore.getFilterScore(); + + Filter exactFilter; + List> inclusiveRangeStartFilters = + new ArrayList>(); + List> exclusiveRangeStartFilters = + new ArrayList>(); + List> inclusiveRangeEndFilters = new ArrayList>(); + List> exclusiveRangeEndFilters = new ArrayList>(); + Filter remainderFilter; + + Direction rangeDirection = null; + buildFilters: { + if (fullFilter == null) { + exactFilter = null; + remainderFilter = fullFilter; + break buildFilters; + } + + int exactMatches = filterScore.exactMatches(); + int indexPos = 0; + + LinkedList> filterList = + new LinkedList>(Arrays.asList(fullFilters)); + + if (exactMatches <= 0) { + exactFilter = null; + } else { + exactFilter = null; + // Build filter whose left-to-right property order matches + // the order of the index. + for (int i=0; i indexProp = index.getProperty(indexPos++); + Filter next = removeIndexProp(filterList, indexProp, RelOp.EQ); + if (next != null) { + exactFilter = (exactFilter == null) ? next : exactFilter.and(next); + } + } + } + + if (filterScore.hasInexactMatch()) { + // All matches must be consecutive, so first inexact match + // is index property after all exact matches. + StorableProperty indexProp = index.getProperty(indexPos); + rangeDirection = index.getPropertyDirection(indexPos); + + while (true) { + PropertyFilter p = removeIndexProp(filterList, indexProp, RelOp.GE); + if (p == null) { + break; + } + inclusiveRangeStartFilters.add(p); + } + + while (true) { + PropertyFilter p = removeIndexProp(filterList, indexProp, RelOp.GT); + if (p == null) { + break; + } + exclusiveRangeStartFilters.add(p); + } + + while (true) { + PropertyFilter p = removeIndexProp(filterList, indexProp, RelOp.LE); + if (p == null) { + break; + } + inclusiveRangeEndFilters.add(p); + } + + while (true) { + PropertyFilter p = removeIndexProp(filterList, indexProp, RelOp.LT); + if (p == null) { + break; + } + exclusiveRangeEndFilters.add(p); + } + } + + remainderFilter = null; + while (filterList.size() > 0) { + Filter next = filterList.removeFirst(); + remainderFilter = (remainderFilter == null) ? next : remainderFilter.and(next); + } + } + + mExactFilter = exactFilter; + mInclusiveRangeStartFilters = + inclusiveRangeStartFilters.toArray(new PropertyFilter[0]); + mExclusiveRangeStartFilters = + exclusiveRangeStartFilters.toArray(new PropertyFilter[0]); + mInclusiveRangeEndFilters = inclusiveRangeEndFilters.toArray(new PropertyFilter[0]); + mExclusiveRangeEndFilters = exclusiveRangeEndFilters.toArray(new PropertyFilter[0]); + mRemainderFilter = remainderFilter; + + OrderingScore orderingScore = mIndexScore.getOrderingScore(); + + OrderedProperty[] handledOrderings; + OrderedProperty[] remainderOrderings; + boolean shouldReverseOrder; + + buildOrderings: { + int totalMatches = orderingScore.totalMatches(); + if (fullOrderings == null || fullOrderings.length == 0 || totalMatches == 0) { + handledOrderings = null; + remainderOrderings = fullOrderings; + shouldReverseOrder = false; + break buildOrderings; + } + + shouldReverseOrder = totalMatches < 0; + totalMatches = Math.abs(totalMatches); + + if (totalMatches >= fullOrderings.length) { + handledOrderings = fullOrderings; + remainderOrderings = null; + break buildOrderings; + } + + final int pos = orderingScore.startPosition(); + + if (index.isUnique() && (pos + totalMatches) >= index.getPropertyCount()) { + // Since all properties of unique index have been used, additional + // remainder ordering is superfluous, and so it is handled. + handledOrderings = fullOrderings; + remainderOrderings = null; + break buildOrderings; + } + + Set> handledSet = new LinkedHashSet>(); + Set> remainderSet = + new LinkedHashSet>(Arrays.asList(fullOrderings)); + + for (int i=0; i chainedProp = + ChainedProperty.get(index.getProperty(pos + i)); + OrderedProperty op; + op = OrderedProperty.get(chainedProp, Direction.ASCENDING); + if (remainderSet.remove(op)) { + handledSet.add(op); + } + op = OrderedProperty.get(chainedProp, Direction.DESCENDING); + if (remainderSet.remove(op)) { + handledSet.add(op); + } + op = OrderedProperty.get(chainedProp, Direction.UNSPECIFIED); + if (remainderSet.remove(op)) { + handledSet.add(op); + } + } + + if (remainderSet.size() == 0) { + handledOrderings = fullOrderings; + remainderOrderings = null; + break buildOrderings; + } + + if (handledSet.size() == 0) { + handledOrderings = null; + remainderOrderings = fullOrderings; + break buildOrderings; + } + + handledOrderings = handledSet.toArray + (new OrderedProperty[handledSet.size()]); + remainderOrderings = remainderSet.toArray + (new OrderedProperty[remainderSet.size()]); + } + + // If using range match, but index direction is backwards. Flipping + // "shouldReverseOrder" doesn't fix the problem. Instead, swap the + // ranges around. + boolean shouldReverseRange = rangeDirection == Direction.DESCENDING; + + mHandledOrderings = handledOrderings; + mRemainderOrderings = remainderOrderings; + mShouldReverseOrder = shouldReverseOrder; + mShouldReverseRange = shouldReverseRange; + } + + private IndexFitness(StorableIndex index, IndexScore indexScore, + Filter exactFilter, + PropertyFilter[] inclusiveRangeStartFilters, + PropertyFilter[] exclusiveRangeStartFilters, + PropertyFilter[] inclusiveRangeEndFilters, + PropertyFilter[] exclusiveRangeEndFilters, + Filter remainderFilter, + OrderedProperty[] handledOrderings, + OrderedProperty[] remainderOrderings, + boolean shouldReverseOrder, + boolean shouldReverseRange) + { + mIndex = index; + mIndexScore = indexScore; + + mExactFilter = exactFilter; + mInclusiveRangeStartFilters = inclusiveRangeStartFilters; + mExclusiveRangeStartFilters = exclusiveRangeStartFilters; + mInclusiveRangeEndFilters = inclusiveRangeEndFilters; + mExclusiveRangeEndFilters = exclusiveRangeEndFilters; + mRemainderFilter = remainderFilter; + + mHandledOrderings = handledOrderings; + mRemainderOrderings = remainderOrderings; + + mShouldReverseOrder = shouldReverseOrder; + mShouldReverseRange = shouldReverseRange; + } + + private PropertyFilter removeIndexProp(List> filterList, + StorableProperty indexProp, + RelOp operator) + { + Iterator> it = filterList.iterator(); + while (it.hasNext()) { + PropertyFilter filter = it.next(); + + if (operator != filter.getOperator()) { + continue; + } + + ChainedProperty chainedProp = filter.getChainedProperty(); + if (chainedProp.getChainCount() == 0) { + StorableProperty prime = chainedProp.getPrimeProperty(); + if (indexProp.equals(prime)) { + it.remove(); + return filter; + } + } + } + return null; + } + + /** + * Returns the index that this fitness object applies to. + */ + public StorableIndex getIndex() { + return mIndex; + } + + /** + * Returns true if the index doesn't actually do anything to filter + * results or to order them. + */ + public boolean isUseless() { + return mExactFilter == null + && mInclusiveRangeStartFilters.length == 0 + && mExclusiveRangeStartFilters.length == 0 + && mInclusiveRangeEndFilters.length == 0 + && mExclusiveRangeEndFilters.length == 0 + && (mHandledOrderings == null || mHandledOrderings.length == 0); + } + + /** + * Returns true if the index results should be iterated in reverse. + */ + public boolean shouldReverseOrder() { + return mShouldReverseOrder; + } + + /** + * Returns true if start and end ranges should be reversed. + */ + public boolean shouldReverseRange() { + return mShouldReverseRange; + } + + /** + * Returns the filter handled by the applicable index for exact + * matches. Is null if none. + */ + public Filter getExactFilter() { + return mExactFilter; + } + + /** + * Returns true if index is unique and exact filter matches each index + * property. Using this index guarantees one fetch result. + */ + public boolean isKeyFilter() { + if (mExactFilter == null || !mIndex.isUnique()) { + return false; + } + + final Set> properties; + { + int propertyCount = mIndex.getPropertyCount(); + properties = new HashSet>(propertyCount); + for (int i=0; i() { + public Object visit(PropertyFilter filter, Object param) { + ChainedProperty chained = filter.getChainedProperty(); + if (chained.getChainCount() == 0) { + properties.remove(chained.getPrimeProperty()); + } + return null; + } + }, null); + + return properties.size() == 0; + } + + /** + * Returns the filters handled by the applicable index for range + * matches. All property names are the same and operator is GE. + */ + public PropertyFilter[] getInclusiveRangeStartFilters() { + return mInclusiveRangeStartFilters; + } + + /** + * Returns the filters handled by the applicable index for range + * matches. All property names are the same and operator is GT. + */ + public PropertyFilter[] getExclusiveRangeStartFilters() { + return mExclusiveRangeStartFilters; + } + + /** + * Returns the filters handled by the applicable index for range + * matches. All property names are the same and operator is LE. + */ + public PropertyFilter[] getInclusiveRangeEndFilters() { + return mInclusiveRangeEndFilters; + } + + /** + * Returns the filters handled by the applicable index for range + * matches. All property names are the same and operator is LT. + */ + public PropertyFilter[] getExclusiveRangeEndFilters() { + return mExclusiveRangeEndFilters; + } + + /** + * Returns a filter which contains terms not handled by the applicable + * index. If the selector has no filter or if the index is complete, + * null is returned. If the index filters nothing required by the + * selector, the complete filter is returned. + */ + public Filter getRemainderFilter() { + return mRemainderFilter; + } + + /** + * Returns the desired orderings handled by the applicable index, + * possibly when reversed. + */ + public OrderedProperty[] getHandledOrderings() { + return (mHandledOrderings == null) ? null : mHandledOrderings.clone(); + } + + /** + * Returns desired orderings not handled by the applicable index, + * possibly when reversed. If the selector has no ordering or the index + * is complete, null is returned. If the index orders nothing required + * by the selector, the complete reduced ordering is returned. + */ + public OrderedProperty[] getRemainderOrderings() { + return (mRemainderOrderings == null) ? null : mRemainderOrderings.clone(); + } + + /** + * Returns the orderings actually provided by the applicable index, + * possibly when reversed. Natural order is not a total order unless + * index is unique. + */ + public OrderedProperty[] getNaturalOrderings() { + return getActualOrderings(false); + } + + /** + * Returns the orderings actually provided by the applicable index, + * excluding exact filter matches, possibly when reversed. Effective + * order is not a total order unless index is unique. + */ + public OrderedProperty[] getEffectiveOrderings() { + return getActualOrderings(true); + } + + @SuppressWarnings("unchecked") + private OrderedProperty[] getActualOrderings(boolean excludeExactMatches) { + int exactMatches = 0; + if (excludeExactMatches) { + exactMatches = mIndexScore.getFilterScore().exactMatches(); + } + + int count = mIndex.getPropertyCount(); + OrderedProperty[] orderings = new OrderedProperty[count - exactMatches]; + for (int i=exactMatches; i property = mIndex.getProperty(i); + Direction direction = mIndex.getPropertyDirection(i); + if (mShouldReverseOrder) { + direction = direction.reverse(); + } + orderings[i - exactMatches] = OrderedProperty.get(property, direction); + } + return orderings; + } + + /** + * Compares this fitness to another which belongs to a different + * Storable type. Filters that reference a joined property may be best + * served by an index defined in the joined type, and this method aids + * in that selection. + * + * @return <0 if this score is better, 0 if equal, or >0 if other is better + */ + public int compareTo(IndexFitness otherFitness) { + return mIndexScore.compareTo(otherFitness.mIndexScore); + } + + /** + * Returns true if given fitness result uses the same index, and in the + * same way. + */ + public boolean canUnion(IndexFitness fitness) { + if (this == fitness) { + return true; + } + + return mIndex.equals(fitness.mIndex) && + (mExactFilter == null ? + fitness.mExactFilter == null : + (mExactFilter.equals(fitness.mExactFilter))) && + Arrays.equals(mInclusiveRangeStartFilters, + fitness.mInclusiveRangeStartFilters) && + Arrays.equals(mExclusiveRangeStartFilters, + fitness.mExclusiveRangeStartFilters) && + Arrays.equals(mInclusiveRangeEndFilters, + fitness.mInclusiveRangeEndFilters) && + Arrays.equals(mExclusiveRangeEndFilters, + fitness.mExclusiveRangeEndFilters) && + mShouldReverseOrder == fitness.mShouldReverseOrder && + mShouldReverseRange == fitness.mShouldReverseRange && + (mHandledOrderings == null ? + fitness.mHandledOrderings == null : + (Arrays.equals(mHandledOrderings, fitness.mHandledOrderings))); + } + + /** + * If the given fitness can union with this one, return a new unioned + * one. If union not possible, null is returned. + */ + public IndexFitness union(IndexFitness fitness) { + if (this == fitness) { + return this; + } + + if (!canUnion(fitness)) { + return null; + } + + // Union the remainder filter and orderings. + + Filter remainderFilter; + if (mRemainderFilter == null) { + if (fitness.mRemainderFilter == null) { + remainderFilter = null; + } else { + remainderFilter = fitness.mRemainderFilter; + } + } else { + if (fitness.mRemainderFilter == null) { + remainderFilter = mRemainderFilter; + } else if (mRemainderFilter.equals(fitness.mRemainderFilter)) { + remainderFilter = mRemainderFilter; + } else { + remainderFilter = mRemainderFilter.or(fitness.mRemainderFilter); + } + } + + OrderedProperty[] remainderOrderings; + if (mRemainderOrderings == null) { + if (fitness.mRemainderOrderings == null) { + remainderOrderings = null; + } else { + remainderOrderings = fitness.mRemainderOrderings; + } + } else { + if (fitness.mRemainderOrderings == null) { + remainderOrderings = mRemainderOrderings; + } else if (mRemainderOrderings.length >= fitness.mRemainderOrderings.length) { + remainderOrderings = mRemainderOrderings; + } else { + remainderOrderings = fitness.mRemainderOrderings; + } + } + + return new IndexFitness(mIndex, mIndexScore, + mExactFilter, + mInclusiveRangeStartFilters, + mExclusiveRangeStartFilters, + mInclusiveRangeEndFilters, + mExclusiveRangeEndFilters, + remainderFilter, + mHandledOrderings, + remainderOrderings, + mShouldReverseOrder, + mShouldReverseRange); + } + + public String toString() { + return "IndexFitness [index=" + mIndex + + ", filterScore=" + mIndexScore.getFilterScore() + + ", orderingScore=" + mIndexScore.getOrderingScore() + + ", exactFilter=" + quoteNonNull(mExactFilter) + + ", inclusiveRangeStartFilters=" + mInclusiveRangeStartFilters + + ", exclusiveRangeStartFilters=" + mExclusiveRangeStartFilters + + ", inclusiveRangeEndFilters=" + mInclusiveRangeEndFilters + + ", exclusiveRangeEndFilters=" + mExclusiveRangeEndFilters + + ", remainderFilter=" + quoteNonNull(mRemainderFilter) + + ", handledOrderings=" + Arrays.toString(mHandledOrderings) + + ", remainderOrderings=" + Arrays.toString(mRemainderOrderings) + + ", shouldReverse=" + mShouldReverseOrder + + ']'; + } + + private static String quoteNonNull(Filter value) { + return value == null ? null : ('"' + String.valueOf(value) + '"'); + } + } + + /** + * Composite of filter score and ordering score. The filter score measures + * how well an index performs the desired level of filtering. Likewise, the + * ordering score measures how well an index performs the desired ordering. + */ + private static class IndexScore implements Comparable> { + private final IndexSelector mSelector; + private final StorableIndex mIndex; + + private FilterScore mFilterScore; + private OrderingScore mOrderingScore; + + IndexScore(IndexSelector selector, StorableIndex index) { + mSelector = selector; + mIndex = index; + } + + @SuppressWarnings("unchecked") + public int compareTo(IndexScore candidateScore) { + final FilterScore thisFilterScore = this.getFilterScore(); + final FilterScore candidateFilterScore = candidateScore.getFilterScore(); + + // Compare total count of exact matching properties. + { + int result = thisFilterScore.compareExact(candidateFilterScore); + if (result != 0) { + return result; + } + } + + // Exact matches same, choose clustered index if more than one match. + if (thisFilterScore.exactMatches() > 1) { + if (mIndex.isClustered()) { + if (!candidateScore.mIndex.isClustered()) { + return -1; + } + } else if (candidateScore.mIndex.isClustered()) { + return 1; + } + } + + // Compare range match. (index can have at most one range match) + if (thisFilterScore.hasRangeMatch()) { + if (candidateFilterScore.hasRangeMatch()) { + // Both have range match, choose clustered index. + if (mIndex.isClustered()) { + if (!candidateScore.mIndex.isClustered()) { + return -1; + } + } else if (candidateScore.mIndex.isClustered()) { + return 1; + } + } else { + return -1; + } + } else if (candidateFilterScore.hasRangeMatch()) { + return 1; + } + + final OrderingScore thisOrderingScore = this.getOrderingScore(); + final OrderingScore candidateOrderingScore = candidateScore.getOrderingScore(); + + int finalResult = 0; + + // Compare orderings, but only if candidate filters anything. It is + // generally slower to scan an index just for correct ordering, + // than it is to sort the results of a full scan. Why? Because an + // index scan results in a lot of random file accesses, and disk is + // so slow. There is an exception to this rule if the candidate is + // a clustered index, in which case there are no random file + // accesses. + if (candidateFilterScore.anyMatches() || candidateScore.mIndex.isClustered()) { + int currentMatches = thisOrderingScore.totalMatches(); + int candidateMatches = candidateOrderingScore.totalMatches(); + if (currentMatches != candidateMatches) { + if (Math.abs(currentMatches) > Math.abs(candidateMatches)) { + // Only select current filter if it filters anything. + if (thisFilterScore.anyMatches()) { + return -1; + } + // Potentially use this result later. + finalResult = -1; + } else if (Math.abs(currentMatches) < Math.abs(candidateMatches)) { + return 1; + } else { + // Magnitudes are equal, but sign differs. Choose positive, + // but not yet. + finalResult = (currentMatches > 0) ? -1 : 1; + } + } + } + + // Compare total count of inexact matching properties. + { + int result = thisFilterScore.compareInexact(candidateFilterScore); + if (result != 0) { + return result; + } + } + + // Compare positioning of matching properties (favor index that best + // matches specified property sequence of filter) + { + int result = thisFilterScore.compareExactPositions(candidateFilterScore); + if (result != 0) { + return result; + } + result = thisFilterScore.compareInexactPosition(candidateFilterScore); + if (result != 0) { + return result; + } + } + + // If both indexes have a non-zero score (that is, either index would + // actually be useful), choose the one that has the least number of + // properties in it. The theory being that smaller index keys mean more + // nodes will fit into the memory cache during an index scan. This + // extra test doesn't try to estimate the average size of properties, + // so it may choose wrong. + { + if ((thisFilterScore.anyMatches() && candidateFilterScore.anyMatches()) || + (thisOrderingScore.anyMatches() && candidateOrderingScore.anyMatches())) + { + if (mIndex.getPropertyCount() < candidateScore.mIndex.getPropertyCount()) { + return -1; + } + if (mIndex.getPropertyCount() > candidateScore.mIndex.getPropertyCount()) { + return 1; + } + } + } + + // Final result derived from ordering comparison earlier. + return finalResult; + } + + /** + * Total matches on score indicates how many consecutive index + * properties (from the start) match the filter requirements. + */ + public FilterScore getFilterScore() { + if (mFilterScore != null) { + return mFilterScore; + } + + mFilterScore = new FilterScore(); + + int indexPropCount = mIndex.getPropertyCount(); + PropertyFilter[] filters = mSelector.mFilters; + int filterCount = filters.length; + + for (int i=0; i prop = mIndex.getProperty(i); + int matchesBefore = mFilterScore.totalMatches(); + for (int pos=0; pos[] orderings = mSelector.mOrderings; + + if (orderings == null || orderings.length == 0) { + return mOrderingScore = new OrderingScore(0, 0); + } + + int indexPropCount = mIndex.getPropertyCount(); + if (indexPropCount <= 0) { + return mOrderingScore = new OrderingScore(0, 0); + } + + // Make sure first ordering property follows exact matches. + + if (orderings[0].getChainedProperty().getChainCount() > 0) { + // Indexes don't currently support chained properties. + return mOrderingScore = new OrderingScore(0, 0); + } + + final StorableProperty first = + orderings[0].getChainedProperty().getPrimeProperty(); + + // Start pos after all exact matching filter properties + int pos = getFilterScore().exactMatches(); + + if (pos >= indexPropCount || !mIndex.getProperty(pos).equals(first)) { + return mOrderingScore = new OrderingScore(0, 0); + } + + boolean reverse = false; + switch (mIndex.getPropertyDirection(pos)) { + case ASCENDING: + reverse = (orderings[0].getDirection() == Direction.DESCENDING); + break; + case DESCENDING: + reverse = (orderings[0].getDirection() == Direction.ASCENDING); + break; + } + + // Match count is the run length of matching properties. + int matches = 1; + final int startPos = pos; + + calcMatches: + for (int i=1; i 0) { + // Indexes don't currently support chained properties. + break; + } + + if (mIndex.getProperty(pos).equals + (orderings[i].getChainedProperty().getPrimeProperty())) { + if (orderings[i].getDirection() != Direction.UNSPECIFIED) { + Direction expected = mIndex.getPropertyDirection(pos); + if (reverse) { + expected = expected.reverse(); + } + if (orderings[i].getDirection() != expected) { + break calcMatches; + } + } + matches++; + } + } + + return mOrderingScore = new OrderingScore(startPos, reverse ? -matches : matches); + } + } + + /** + * One of the scores that evaluates an index's fitness for a given filter. + *

A filter mentions properties, either as exact ("=") or inexact (">" "<", et al) + *

An index contains properties, in order. + *

An index property matches a filter if the filter uses that property, and if all of the + * properties in the index to the left of the property are also in the filter (since holes + * in the index make the index useless for subsequent properties) + *

Then the index filter score is a function of the number of matches it contains, + * and how early in the filter they appear. + *

Any exact filter match beats an inexact filter. + *

More exact filter matches beats fewer. + *

Inexact will be selected if there are no exact matches + * + *

Note that there will be only one inexact match, since once we're in an inexact range we + * have to scan the entire range (and a later inexact match will be arbitrarily ordered within + * that range). + * + *

For example: + *

+     * user query: "a>? & b=? & c = ?"
+     * will be presorted to
+     * "b=? & c=? & a>?
+     * a, b, c     == a[inexact]->2, b->0, c->1
+     * d, a, b, c  == useless
+     * c, d, b     == c->1
+     * 
+ * ...so the "c,d,b" index will win. + */ + private static class FilterScore { + // Positions of exact matches + private List mExactMatches = new ArrayList(); + + // Properties which have been used for exact matching -- these should + // show up only once per filter set + private Set mExactMatchProps = new HashSet(); + + // Position of inexact match + private int mInexactMatchPos; + + // Property used for inexact match + private StorableProperty mInexactMatch; + + private boolean mHasRangeStart; + private boolean mHasRangeEnd; + + FilterScore() { + } + + /** + * Tally up filter score. + * + * @param prop property of candidate index + * @param filter property filter to check for index fitness + * @param pos position of filter in filter list + */ + void tally(StorableProperty prop, PropertyFilter filter, int pos) { + ChainedProperty chained = filter.getChainedProperty(); + + if (chained.getChainCount() == 0 && chained.getPrimeProperty().equals(prop)) { + switch (filter.getOperator()) { + case EQ: + // Exact match + if (mInexactMatch == null) { + mExactMatches.add(pos); + mExactMatchProps.add(prop); + } + + break; + + case LT: case GE: case GT: case LE: + // Inexact match + + if (mInexactMatch == null) { + // If for some reason the query contains an exact and + // an inexact match on the same property (a>1 & a=14) + // we'll never care about the inexact match. + if (!mExactMatchProps.contains(prop)) { + mInexactMatchPos = pos; + mInexactMatch = prop; + } + } + + // Check for range match + if (prop.equals(mInexactMatch)) { + switch (filter.getOperator()) { + case LT: case LE: + mHasRangeStart = true; + break; + case GT: case GE: + mHasRangeEnd = true; + break; + } + } + + break; + } + } + } + + int compareExact(FilterScore candidate) { + return -intCompare(mExactMatches.size(), candidate.mExactMatches.size()); + } + + int compareInexact(FilterScore candidate) { + if (mInexactMatch == null && candidate.mInexactMatch != null) { + return 1; + } else if (mInexactMatch != null && candidate.mInexactMatch == null) { + return -1; + } + return 0; + } + + int compareExactPositions(FilterScore candidate) { + return listCompare(mExactMatches, candidate.mExactMatches); + } + + int compareInexactPosition(FilterScore candidate) { + return intCompare(mInexactMatchPos, candidate.mInexactMatchPos); + } + + boolean anyMatches() { + return mExactMatches.size() > 0 || mInexactMatch != null; + } + + int exactMatches() { + return mExactMatches.size(); + } + + boolean hasRangeMatch() { + return mHasRangeStart && mHasRangeEnd; + } + + boolean hasInexactMatch() { + return mInexactMatch != null; + } + + int totalMatches() { + return mExactMatches.size() + (mInexactMatch == null ? 0 : 1); + } + + public String toString() { + return "FilterScore [exactMatches=" + mExactMatches + + ", exactMatchProps=" + mExactMatchProps + + ", inexactMatch=" + (mInexactMatch == null ? null : mInexactMatch.getName()) + + ", rangeMatch=" + hasRangeMatch() + + ']'; + } + } + + /** + * How well does this index help me sort things + */ + private static class OrderingScore { + private final int mStartPos; + private final int mTotalMatches; + + OrderingScore(int startPos, int totalMatches) { + mStartPos = startPos; + mTotalMatches = totalMatches; + } + + /** + * Returns start position of index. + */ + int startPosition() { + return mStartPos; + } + + boolean anyMatches() { + return mTotalMatches > 0; + } + + /** + * Magnitude represents count of matching orderings. If negative + * result, index produces reversed ordering. + */ + int totalMatches() { + return mTotalMatches; + } + + public String toString() { + return "OrderingScore [startPos=" + mStartPos + + ", totalMatches=" + mTotalMatches + + ']'; + } + } + + /** + * Sorts property filters such that '==' operations come before '!=' + * operations. Assuming a stable sort is used, all other property filters + * are left in place + */ + private static class PropertyFilterComparator + implements Comparator> + { + public int compare(PropertyFilter a, PropertyFilter b) { + if (a.getOperator() != b.getOperator()) { + if (a.getOperator() == RelOp.EQ) { + return -1; + } + if (a.getOperator() == RelOp.NE) { + return 1; + } + if (b.getOperator() == RelOp.EQ) { + return 1; + } + if (b.getOperator() == RelOp.NE) { + return -1; + } + } + return 0; + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/LobEngine.java b/src/main/java/com/amazon/carbonado/spi/LobEngine.java new file mode 100644 index 0000000..167c5b1 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/LobEngine.java @@ -0,0 +1,1059 @@ +/* + * 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.spi; + +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.List; + +import org.cojen.util.KeyFactory; +import org.cojen.util.SoftValuedHashMap; + +import com.amazon.carbonado.Cursor; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.FetchNoneException; +import com.amazon.carbonado.IsolationLevel; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.PersistNoneException; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.Transaction; +import com.amazon.carbonado.Trigger; + +import com.amazon.carbonado.info.StorableInfo; +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableProperty; + +import com.amazon.carbonado.lob.AbstractBlob; +import com.amazon.carbonado.lob.Blob; +import com.amazon.carbonado.lob.BlobClob; +import com.amazon.carbonado.lob.Clob; +import com.amazon.carbonado.lob.Lob; + +/** + * Complete Lob support for repositories, although repository is responsible + * for binding Lob properties to this engine. Lobs are referenced by locators, + * which are non-zero long integers. A zero locator is equivalent to null. + * + * @author Brian S O'Neill + * @see #getSupportTrigger(Class, int) + */ +public class LobEngine { + public static boolean hasLobs(Class type) { + StorableInfo info = StorableIntrospector.examine(type); + for (StorableProperty prop : info.getAllProperties().values()) { + if (Lob.class.isAssignableFrom(prop.getType())) { + return true; + } + } + return false; + } + + static IOException toIOException(RepositoryException e) { + IOException ioe = new IOException(e.getMessage()); + ioe.initCause(e); + return ioe; + } + + final Repository mRepo; + final Storage mLobStorage; + final Storage mLobBlockStorage; + + private Map mTriggers; + + /** + * @param repo storage for Lobs + */ + public LobEngine(Repository repo) throws RepositoryException { + mRepo = repo; + mLobStorage = repo.storageFor(StoredLob.class); + mLobBlockStorage = repo.storageFor(StoredLob.Block.class); + } + + /** + * Returns a new Blob whose length is zero. + * + * @param blockSize block size (in bytes) to use + * @return new empty Blob + */ + public Blob createNewBlob(int blockSize) throws PersistException { + StoredLob lob = mLobStorage.prepare(); + lob.setBlockSize(blockSize); + lob.setLength(0); + lob.insert(); + return new BlobImpl(lob.getLocator()); + } + + /** + * Returns a new Clob whose length is zero. + * + * @param blockSize block size (in bytes) to use + * @return new empty Clob + */ + public Clob createNewClob(int blockSize) throws PersistException { + StoredLob lob = mLobStorage.prepare(); + lob.setBlockSize(blockSize); + lob.setLength(0); + lob.insert(); + return new ClobImpl(lob.getLocator()); + } + + /** + * Returns the locator for the given Lob, or zero if null. + * + * @throws ClassCastException if Lob is unrecognized + */ + public long getLocator(Lob lob) { + if (lob == null) { + return 0; + } + return ((LobImpl) lob).getLocator(); + } + + /** + * Deletes Lob data, freeing up all space consumed by it. + */ + public void deleteLob(long locator) throws PersistException { + if (locator == 0) { + return; + } + + Transaction txn = mRepo.enterTransaction(IsolationLevel.READ_COMMITTED); + try { + StoredLob lob = mLobStorage.prepare(); + lob.setLocator(locator); + if (lob.tryDelete()) { + try { + lob.getBlocks().deleteAll(); + } catch (FetchException e) { + throw e.toPersistException(); + } + } + txn.commit(); + } finally { + txn.exit(); + } + } + + /** + * Deletes Lob data, freeing up all space consumed by it. + */ + public void deleteLob(Lob lob) throws PersistException { + deleteLob(getLocator(lob)); + } + + /** + * Loads a Blob value, without checking if it exists or not. + * + * @param locator lob locator as returned by getLocator + * @return Blob value or null + */ + public Blob getBlobValue(long locator) { + if (locator == 0) { + return null; + } + return new BlobImpl(locator); + } + + /** + * Loads a Clob value, without checking if it exists or not. + * + * @param locator lob locator as returned by getLocator + * @return Clob value or null + */ + public Clob getClobValue(long locator) { + if (locator == 0) { + return null; + } + return new ClobImpl(locator); + } + + /** + * Stores a value into a Blob, replacing anything that was there + * before. Passing null deletes the Blob, which is a convenience for + * auto-generated code that may call this method. + * + * @param locator lob locator as created by createNewBlob + * @param data source of data for Blob, which may be null to delete + * @throws IllegalArgumentException if locator is zero + */ + public void setBlobValue(long locator, Blob data) throws PersistException, IOException { + if (data == null) { + deleteLob(locator); + return; + } + + if (locator == 0) { + throw new IllegalArgumentException("Cannot use locator zero"); + } + + if (data instanceof BlobImpl) { + BlobImpl impl = (BlobImpl) data; + if (impl.getEnclosing() == this && impl.mLocator == locator) { + // Blob is ours and locator is the same, so nothing to do. + return; + } + } + + try { + setBlobValue(locator, data.openInputStream(0, 0)); + } catch (FetchException e) { + throw e.toPersistException(); + } + } + + /** + * Stores a value into a Blob, replacing anything that was there + * before. Passing null deletes the Blob, which is a convenience for + * auto-generated code that may call this method. + * + * @param locator lob locator as created by createNewBlob + * @param data source of data for Blob, which may be null to delete + * @throws IllegalArgumentException if locator is zero + */ + public void setBlobValue(long locator, InputStream data) throws PersistException, IOException { + if (data == null) { + deleteLob(locator); + return; + } + + if (locator == 0) { + throw new IllegalArgumentException("Cannot use locator zero"); + } + + Transaction txn = mRepo.enterTransaction(IsolationLevel.READ_COMMITTED); + txn.setForUpdate(true); + try { + StoredLob lob = mLobStorage.prepare(); + lob.setLocator(locator); + try { + lob.load(); + } catch (FetchNoneException e) { + throw new PersistNoneException("Lob deleted: " + this); + } + + OutputStream out = new Output(lob, 0, txn); + + byte[] buffer = new byte[lob.getBlockSize()]; + + long total = 0; + int amt; + try { + while ((amt = data.read(buffer)) > 0) { + out.write(buffer, 0, amt); + total += amt; + } + } finally { + data.close(); + } + out.close(); + + if (total < lob.getLength()) { + new BlobImpl(lob).setLength(total); + } + + txn.commit(); + } catch (IOException e) { + if (e.getCause() instanceof RepositoryException) { + RepositoryException re = (RepositoryException) e.getCause(); + throw re.toPersistException(); + } + throw e; + } catch (FetchException e) { + throw e.toPersistException(); + } finally { + txn.exit(); + } + } + + /** + * Stores a value into a Clob, replacing anything that was there + * before. Passing null deletes the Clob, which is a convenience for + * auto-generated code that may call this method. + * + * @param locator lob locator as created by createNewClob + * @param data source of data for Clob, which may be null to delete + * @throws IllegalArgumentException if locator is zero + */ + public void setClobValue(long locator, Clob data) throws PersistException, IOException { + if (data == null) { + deleteLob(locator); + return; + } + + if (locator == 0) { + throw new IllegalArgumentException("Cannot use locator zero"); + } + + if (data instanceof ClobImpl) { + BlobImpl impl = ((ClobImpl) data).getWrappedBlob(); + if (impl.getEnclosing() == this && impl.mLocator == locator) { + // Blob is ours and locator is the same, so nothing to do. + return; + } + } + + try { + setClobValue(locator, data.openReader(0, 0)); + } catch (FetchException e) { + throw e.toPersistException(); + } + } + + /** + * Stores a value into a Clob, replacing anything that was there + * before. Passing null deletes the Clob, which is a convenience for + * auto-generated code that may call this method. + * + * @param locator lob locator as created by createNewClob + * @param data source of data for Clob, which may be null to delete + * @throws IllegalArgumentException if locator is zero + */ + public void setClobValue(long locator, Reader data) throws PersistException, IOException { + if (data == null) { + deleteLob(locator); + return; + } + + if (locator == 0) { + throw new IllegalArgumentException("Cannot use locator zero"); + } + + Transaction txn = mRepo.enterTransaction(IsolationLevel.READ_COMMITTED); + txn.setForUpdate(true); + try { + StoredLob lob = mLobStorage.prepare(); + lob.setLocator(locator); + try { + lob.load(); + } catch (FetchNoneException e) { + throw new PersistNoneException("Lob deleted: " + this); + } + + ClobImpl clob = new ClobImpl(lob); + Writer writer = clob.openWriter(0, 0); + + char[] buffer = new char[lob.getBlockSize() >> 1]; + + long total = 0; + int amt; + try { + while ((amt = data.read(buffer)) > 0) { + writer.write(buffer, 0, amt); + total += amt; + } + } finally { + data.close(); + } + writer.close(); + + if (total < lob.getLength()) { + clob.setLength(total); + } + + txn.commit(); + } catch (IOException e) { + if (e.getCause() instanceof RepositoryException) { + RepositoryException re = (RepositoryException) e.getCause(); + throw re.toPersistException(); + } + throw e; + } catch (FetchException e) { + throw e.toPersistException(); + } finally { + txn.exit(); + } + } + + /** + * Returns a Trigger for binding to this LobEngine. Storage implementations + * which use LobEngine must install this Trigger. Trigger instances are + * cached, so subsequent calls for the same trigger return the same + * instance. + * + * @param type type of Storable to create trigger for + * @param blockSize block size to use + * @return support trigger or null if storable type has no lob properties + */ + public synchronized Trigger + getSupportTrigger(Class type, int blockSize) + { + Object key = KeyFactory.createKey(new Object[] {type, blockSize}); + + Trigger trigger = (mTriggers == null) ? null : (Trigger) mTriggers.get(key); + + if (trigger == null) { + StorableInfo info = StorableIntrospector.examine(type); + + List> lobProperties = null; + + for (StorableProperty prop : info.getAllProperties().values()) { + if (Blob.class.isAssignableFrom(prop.getType())) { + if (lobProperties == null) { + lobProperties = new ArrayList>(); + } + lobProperties.add(new BlobProperty(this, prop.getName())); + } else if (Clob.class.isAssignableFrom(prop.getType())) { + if (lobProperties == null) { + lobProperties = new ArrayList>(); + } + lobProperties.add(new ClobProperty(this, prop.getName())); + } + } + + if (lobProperties != null) { + trigger = new LobEngineTrigger(this, type, blockSize, lobProperties); + } + + if (mTriggers == null) { + mTriggers = new SoftValuedHashMap(); + } + + mTriggers.put(key, trigger); + } + + return trigger; + } + + private interface LobImpl extends Lob { + long getLocator(); + } + + private class BlobImpl extends AbstractBlob implements LobImpl { + final long mLocator; + final StoredLob mStoredLob; + + BlobImpl(long locator) { + super(mRepo); + mLocator = locator; + mStoredLob = null; + } + + BlobImpl(StoredLob lob) { + super(mRepo); + mLocator = lob.getLocator(); + mStoredLob = lob; + } + + public InputStream openInputStream() throws FetchException { + return openInputStream(0); + } + + public InputStream openInputStream(long pos) throws FetchException { + if (pos < 0) { + throw new IllegalArgumentException("Position is negative: " + pos); + } + StoredLob lob = mStoredLob; + Transaction txn = mRepo.enterTransaction(IsolationLevel.READ_COMMITTED); + if (lob == null) { + lob = mLobStorage.prepare(); + lob.setLocator(mLocator); + try { + lob.load(); + } catch (FetchException e) { + try { + txn.exit(); + } catch (PersistException e2) { + // Don't care. + } + if (e instanceof FetchNoneException) { + throw new FetchNoneException("Lob deleted: " + this); + } + throw e; + } + } + return new Input(lob, pos, txn); + } + + public InputStream openInputStream(long pos, int bufferSize) throws FetchException { + return openInputStream(pos); + } + + public long getLength() throws FetchException { + StoredLob lob = mStoredLob; + if (lob == null) { + lob = mLobStorage.prepare(); + lob.setLocator(mLocator); + try { + lob.load(); + } catch (FetchNoneException e) { + throw new FetchNoneException("Lob deleted: " + this); + } + } + return lob.getLength(); + } + + public OutputStream openOutputStream() throws PersistException { + return openOutputStream(0); + } + + public OutputStream openOutputStream(long pos) throws PersistException { + if (pos < 0) { + throw new IllegalArgumentException("Position is negative: " + pos); + } + StoredLob lob = mStoredLob; + Transaction txn = mRepo.enterTransaction(IsolationLevel.READ_COMMITTED); + txn.setForUpdate(true); + try { + if (lob == null) { + lob = mLobStorage.prepare(); + lob.setLocator(mLocator); + try { + lob.load(); + } catch (FetchNoneException e) { + throw new PersistNoneException("Lob deleted: " + this); + } catch (FetchException e) { + throw e.toPersistException(); + } + } + return new Output(lob, pos, txn); + } catch (PersistException e) { + try { + txn.exit(); + } catch (PersistException e2) { + // Don't care. + } + throw e; + } + } + + public OutputStream openOutputStream(long pos, int bufferSize) throws PersistException { + return openOutputStream(pos); + } + + public void setLength(long length) throws PersistException { + if (length < 0) { + throw new IllegalArgumentException("Length is negative: " + length); + } + + Transaction txn = mRepo.enterTransaction(); + try { + StoredLob lob = mStoredLob; + if (lob == null) { + lob = mLobStorage.prepare(); + lob.setLocator(mLocator); + txn.setForUpdate(true); + try { + lob.load(); + } catch (FetchNoneException e) { + throw new PersistNoneException("Lob deleted: " + this); + } + txn.setForUpdate(false); + } + + long oldLength = lob.getLength(); + + if (length == oldLength) { + return; + } + + long oldBlockCount = lob.getBlockCount(); + lob.setLength(length); + + if (length < oldLength) { + // Free unused blocks. + long newBlockCount = lob.getBlockCount(); + if (newBlockCount < oldBlockCount) { + lob.getBlocks().and("blockNumber >= ?") + // Subtract 0x80000000 such that block zero is + // physically stored with the smallest integer. + .with(((int) newBlockCount) - 0x80000000) + .deleteAll(); + } + + // Clear space in last block. + int lastBlockLength = lob.getLastBlockLength(); + if (lastBlockLength != 0) { + StoredLob.Block block = mLobBlockStorage.prepare(); + block.setLocator(mLocator); + // Subtract 0x80000000 such that block zero is + // physically stored with the smallest + // integer. Subtract one more to convert one-based + // count to zero-based index. + block.setBlockNumber(((int) newBlockCount) - 0x80000001); + txn.setForUpdate(true); + if (block.tryLoad()) { + byte[] data = block.getData(); + if (data.length > lastBlockLength) { + byte[] newData = new byte[lastBlockLength]; + System.arraycopy(data, 0, newData, 0, lastBlockLength); + block.setData(newData); + block.update(); + } + } + txn.setForUpdate(false); + } + } + + lob.update(); + txn.commit(); + } catch (FetchException e) { + throw e.toPersistException(); + } finally { + txn.exit(); + } + } + + @Override + public int hashCode() { + return ((int) (mLocator >> 32)) ^ ((int) mLocator); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof BlobImpl) { + BlobImpl other = (BlobImpl) obj; + return LobEngine.this == other.getEnclosing() && mLocator == other.mLocator; + } + return false; + } + + @Override + public String toString() { + return "Blob@" + getLocator(); + } + + public long getLocator() { + return mLocator; + } + + LobEngine getEnclosing() { + return LobEngine.this; + } + } + + private class ClobImpl extends BlobClob implements LobImpl { + ClobImpl(long locator) { + super(new BlobImpl(locator)); + } + + ClobImpl(StoredLob lob) { + super(new BlobImpl(lob)); + } + + @Override + public int hashCode() { + return super.getWrappedBlob().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ClobImpl) { + return getWrappedBlob().equals(((ClobImpl) obj).getWrappedBlob()); + } + return false; + } + + @Override + public String toString() { + return "Clob@" + getLocator(); + } + + public long getLocator() { + return ((BlobImpl) super.getWrappedBlob()).getLocator(); + } + + // Override to gain permission. + protected BlobImpl getWrappedBlob() { + return (BlobImpl) super.getWrappedBlob(); + } + } + + private class Input extends InputStream { + private final long mLocator; + private final int mBlockSize; + private final long mLength; + + private long mPos; + private int mBlockNumber; + private int mBlockPos; + + private Transaction mTxn; + private Cursor mCursor; + private StoredLob.Block mStoredBlock; + + Input(StoredLob lob, long pos, Transaction txn) throws FetchException { + mLocator = lob.getLocator(); + mBlockSize = lob.getBlockSize(); + mLength = lob.getLength(); + + mPos = pos; + mBlockNumber = ((int) (pos / mBlockSize)) - 0x80000000; + mBlockPos = (int) (pos % mBlockSize); + + mTxn = txn; + + mCursor = mLobBlockStorage.query("locator = ? & blockNumber >= ?") + .with(mLocator).with(mBlockNumber) + .fetch(); + } + + @Override + public synchronized int read() throws IOException { + if (mCursor == null) { + throw new IOException("Closed"); + } + if (mPos >= mLength) { + return -1; + } + + byte[] block = getBlockData(); + int blockPos = mBlockPos; + + int b; + if (block == null || blockPos >= block.length) { + b = 0; + } else { + b = block[blockPos] & 0xff; + } + + mPos++; + if (++blockPos >= mBlockSize) { + mBlockNumber++; + blockPos = 0; + } + mBlockPos = blockPos; + + return b; + } + + @Override + public int read(byte[] bytes) throws IOException { + return read(bytes, 0, bytes.length); + } + + @Override + public synchronized int read(byte[] bytes, int offset, int length) throws IOException { + if (length <= 0) { + return 0; + } + if (mCursor == null) { + throw new IOException("Closed"); + } + int avail = Math.min((int) (mLength - mPos), mBlockSize - mBlockPos); + if (avail <= 0) { + return -1; + } + if (length > avail) { + length = avail; + } + + byte[] block = getBlockData(); + int blockPos = mBlockPos; + + if (block == null) { + Arrays.fill(bytes, offset, offset + length, (byte) 0); + } else { + int blockAvail = block.length - blockPos; + if (blockAvail >= length) { + System.arraycopy(block, blockPos, bytes, offset, length); + } else { + System.arraycopy(block, blockPos, bytes, offset, blockAvail); + Arrays.fill(bytes, offset + blockAvail, offset + length, (byte) 0); + } + } + + mPos += length; + if ((blockPos += length) >= mBlockSize) { + mBlockNumber++; + blockPos = 0; + } + mBlockPos = blockPos; + + return length; + } + + @Override + public synchronized long skip(long n) throws IOException { + if (n <= 0) { + return 0; + } + if (mCursor == null) { + throw new IOException("Closed"); + } + long oldPos = mPos; + if (n > Integer.MAX_VALUE) { + n = Integer.MAX_VALUE; + } + long newPos = oldPos + n; + if (newPos >= mLength) { + newPos = mLength; + n = newPos - oldPos; + if (n <= 0) { + return 0; + } + } + // Note: could open a new cursor here, but we'd potentially lose + // the thread-local transaction. The next call to getBlockData will + // detect that the desired block number differs from the actual one + // and will skip one block at a time until cursor is at the correct + // position. + mPos = newPos; + mBlockNumber = ((int) (newPos / mBlockSize)) - 0x80000000; + mBlockPos = (int) (newPos % mBlockSize); + return n; + } + + @Override + public synchronized void close() throws IOException { + if (mTxn != null) { + try { + // This should also cause the cursor to close. + mTxn.exit(); + } catch (PersistException e) { + throw toIOException(e); + } + mTxn = null; + } + if (mCursor != null) { + try { + mCursor.close(); + } catch (FetchException e) { + throw toIOException(e); + } + mCursor = null; + mStoredBlock = null; + } + } + + // Caller must be synchronized and have checked if stream is closed + private byte[] getBlockData() throws IOException { + while (mStoredBlock == null || mBlockNumber > mStoredBlock.getBlockNumber()) { + try { + if (!mCursor.hasNext()) { + mStoredBlock = null; + return null; + } + mStoredBlock = mCursor.next(); + } catch (FetchException e) { + try { + close(); + } catch (IOException e2) { + // Don't care. + } + throw toIOException(e); + } + } + if (mBlockNumber < mStoredBlock.getBlockNumber()) { + return null; + } + return mStoredBlock.getData(); + } + } + + private class Output extends OutputStream { + private final StoredLob mStoredLob; + + private long mPos; + private int mBlockNumber; + private int mBlockPos; + + private Transaction mTxn; + private StoredLob.Block mStoredBlock; + private byte[] mBlockData; + private int mBlockLength; + private boolean mDoInsert; + + Output(StoredLob lob, long pos, Transaction txn) throws PersistException { + mStoredLob = lob; + + mPos = pos; + mBlockNumber = ((int) (pos / lob.getBlockSize())) - 0x80000000; + mBlockPos = (int) (pos % lob.getBlockSize()); + + mTxn = txn; + } + + @Override + public synchronized void write(int b) throws IOException { + if (mTxn == null) { + throw new IOException("Closed"); + } + + prepareBlockData(); + + int blockPos = mBlockPos; + if (blockPos >= mBlockData.length) { + byte[] newBlockData = new byte[mStoredLob.getBlockSize()]; + System.arraycopy(mBlockData, 0, newBlockData, 0, mBlockData.length); + mBlockData = newBlockData; + } + mBlockData[blockPos++] = (byte) b; + if (blockPos > mBlockLength) { + mBlockLength = blockPos; + } + if (blockPos >= mStoredLob.getBlockSize()) { + mBlockNumber++; + blockPos = 0; + } + mBlockPos = blockPos; + mPos++; + } + + @Override + public void write(byte[] bytes) throws IOException { + write(bytes, 0, bytes.length); + } + + @Override + public synchronized void write(byte[] bytes, int offset, int length) throws IOException { + if (length <= 0) { + return; + } + if (mTxn == null) { + throw new IOException("Closed"); + } + + while (length > 0) { + prepareBlockData(); + + int avail = mStoredLob.getBlockSize() - mBlockPos; + if (avail > length) { + avail = length; + } + + if ((mBlockPos + avail) >= mBlockData.length) { + byte[] newBlockData = new byte[mStoredLob.getBlockSize()]; + System.arraycopy(mBlockData, 0, newBlockData, 0, mBlockData.length); + mBlockData = newBlockData; + } + + System.arraycopy(bytes, offset, mBlockData, mBlockPos, avail); + offset += avail; + length -= avail; + mBlockPos += avail; + if (mBlockPos > mBlockLength) { + mBlockLength = mBlockPos; + } + if (mBlockPos >= mStoredLob.getBlockSize()) { + mBlockNumber++; + mBlockPos = 0; + } + mPos += avail; + } + } + + @Override + public synchronized void flush() throws IOException { + if (mTxn == null) { + throw new IOException("Closed"); + } + try { + updateBlock(); + } catch (PersistException e) { + try { + close(); + } catch (IOException e2) { + // Don't care. + } + throw toIOException(e); + } + } + + @Override + public synchronized void close() throws IOException { + if (mTxn != null) { + try { + updateBlock(); + if (mPos > mStoredLob.getLength()) { + mStoredLob.setLength(mPos); + mStoredLob.update(); + } + mTxn.commit(); + } catch (PersistException e) { + throw toIOException(e); + } finally { + try { + mTxn.exit(); + } catch (PersistException e) { + throw toIOException(e); + } + } + mTxn = null; + } + } + + // Caller must be synchronized and have checked if stream is closed + private void updateBlock() throws PersistException { + if (mStoredBlock != null) { + byte[] blockData = mBlockData; + if (blockData.length != mBlockLength) { + byte[] truncated = new byte[mBlockLength]; + System.arraycopy(blockData, 0, truncated, 0, truncated.length); + blockData = truncated; + } + mStoredBlock.setData(blockData); + if (mDoInsert) { + mStoredBlock.insert(); + mDoInsert = false; + } else { + mStoredBlock.update(); + } + } + } + + // Caller must be synchronized and have checked if stream is closed + private void prepareBlockData() throws IOException { + if (mStoredBlock == null || mBlockNumber > mStoredBlock.getBlockNumber()) { + try { + updateBlock(); + + mStoredBlock = mLobBlockStorage.prepare(); + mStoredBlock.setLocator(mStoredLob.getLocator()); + mStoredBlock.setBlockNumber(mBlockNumber); + try { + if (mStoredBlock.tryLoad()) { + mBlockData = mStoredBlock.getData(); + mBlockLength = mBlockData.length; + mDoInsert = false; + } else { + mBlockData = new byte[mStoredLob.getBlockSize()]; + mBlockLength = 0; + mDoInsert = true; + } + } catch (FetchException e) { + throw e.toPersistException(); + } + } catch (PersistException e) { + try { + close(); + } catch (IOException e2) { + // Don't care. + } + throw toIOException(e); + } + } + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/LobEngineTrigger.java b/src/main/java/com/amazon/carbonado/spi/LobEngineTrigger.java new file mode 100644 index 0000000..2e42d8e --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/LobEngineTrigger.java @@ -0,0 +1,181 @@ +/* + * 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.spi; + +import java.util.List; + +import org.cojen.util.BeanPropertyAccessor; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Trigger; + +import com.amazon.carbonado.lob.Lob; + +/** + * + * + * @author Brian S O'Neill + * @see LobEngine + */ +class LobEngineTrigger extends Trigger { + final LobEngine mEngine; + private final int mBlockSize; + private final BeanPropertyAccessor mAccessor; + private final LobProperty[] mLobProperties; + + LobEngineTrigger(LobEngine engine, Class type, int blockSize, + List> lobProperties) + { + mEngine = engine; + mAccessor = BeanPropertyAccessor.forClass(type); + mBlockSize = blockSize; + + mLobProperties = new LobProperty[lobProperties.size()]; + lobProperties.toArray(mLobProperties); + } + + // Returns user specified Lob values + public Object beforeInsert(S storable) throws PersistException { + // Capture user lob values for later and replace with new locators. + int length = mLobProperties.length; + Object[] userLobs = new Object[length]; + for (int i=0; i prop = mLobProperties[i]; + Object userLob = mAccessor.getPropertyValue(storable, prop.mName); + userLobs[i] = userLob; + if (userLob != null) { + Object lob = prop.createNewLob(mBlockSize); + mAccessor.setPropertyValue(storable, prop.mName, lob); + } + } + return userLobs; + } + + public void afterInsert(S storable, Object state) throws PersistException { + // Save user lob value contents into new lobs. + Object[] userLobs = (Object[]) state; + int length = mLobProperties.length; + for (int i=0; i prop = mLobProperties[i]; + Lob lob = (Lob) mAccessor.getPropertyValue(storable, prop.mName); + prop.setLobValue(mEngine.getLocator(lob), (Lob) userLob); + } + } + } + + public void failedInsert(S storable, Object state) { + unreplaceLobs(storable, state); + } + + public Object beforeUpdate(S storable) throws PersistException { + // For each dirty lob property, capture it in case update fails. All + // lob updates are made in this method. + + int length = mLobProperties.length; + Object[] userLobs = new Object[length]; + S existing = null; + + for (int i=0; i prop = mLobProperties[i]; + if (!storable.isPropertyDirty(prop.mName)) { + continue; + } + + try { + if (existing == null && (existing = loadExisting(storable)) == null) { + // Update will fail so don't touch lobs. + return null; + } + } catch (FetchException e) { + throw e.toPersistException(); + } + + Object userLob = mAccessor.getPropertyValue(storable, prop.mName); + userLobs[i] = userLob; + Lob existingLob = (Lob) mAccessor.getPropertyValue(existing, prop.mName); + if (userLob == null) { + if (existingLob != null) { + // User is setting existing lob to null, so delete it. + mEngine.deleteLob(existingLob); + } + } else { + if (existingLob == null) { + // User is setting a lob that has no locator yet, so make one. + existingLob = prop.createNewLob(mBlockSize); + } + prop.setLobValue(mEngine.getLocator(existingLob), (Lob) userLob); + mAccessor.setPropertyValue(storable, prop.mName, existingLob); + } + } + + return userLobs; + } + + public void failedUpdate(S storable, Object state) { + unreplaceLobs(storable, state); + } + + // Returns existing Storable or null + public Object beforeDelete(S storable) throws PersistException { + S existing = (S) storable.copy(); + try { + if (!existing.tryLoad()) { + existing = null; + } + return existing; + } catch (FetchException e) { + throw e.toPersistException(); + } + } + + public void afterDelete(S storable, Object existing) throws PersistException { + if (existing != null) { + // After successful delete of master storable, delete all the lobs. + for (LobProperty prop : mLobProperties) { + Lob lob = (Lob) mAccessor.getPropertyValue(existing, prop.mName); + mEngine.deleteLob(lob); + } + } + } + + private S loadExisting(S storable) throws FetchException { + S existing = (S) storable.copy(); + if (!existing.tryLoad()) { + return null; + } + return existing; + } + + private void unreplaceLobs(S storable, Object state) { + if (state != null) { + Object[] userLobs = (Object[]) state; + int length = mLobProperties.length; + for (int i=0; i { + final LobEngine mEngine; + final String mName; + + LobProperty(LobEngine engine, String name) { + mEngine = engine; + mName = name; + } + + abstract L createNewLob(int blockSize) throws PersistException; + + abstract void setLobValue(long locator, L data) throws PersistException; +} diff --git a/src/main/java/com/amazon/carbonado/spi/MasterFeature.java b/src/main/java/com/amazon/carbonado/spi/MasterFeature.java new file mode 100644 index 0000000..f1d68f7 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/MasterFeature.java @@ -0,0 +1,56 @@ +/* + * 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.spi; + +/** + * Master feature to enable when using {@link MasterStorableGenerator}. + * + * @author Brian S O'Neill + */ +public enum MasterFeature { + /** Insert and update operations implement record versioning, if version property exists */ + VERSIONING, + + /** Update operations load clean copy first, to prevent destructive update */ + UPDATE_FULL, + + /** Ensure update operation always is in a transaction */ + UPDATE_TXN, + + /** Ensure update operation always is in a transaction, "for update" */ + UPDATE_TXN_FOR_UPDATE, + + /** Insert operation applies any sequences to unset properties */ + INSERT_SEQUENCES, + + /** Insert operation checks that all required data properties have been set */ + INSERT_CHECK_REQUIRED, + + /** Ensure insert operation always is in a transaction */ + INSERT_TXN, + + /** Ensure insert operation always is in a transaction, "for update" */ + INSERT_TXN_FOR_UPDATE, + + /** Ensure delete operation always is in a transaction */ + DELETE_TXN, + + /** Ensure delete operation always is in a transaction, "for update" */ + DELETE_TXN_FOR_UPDATE, +} diff --git a/src/main/java/com/amazon/carbonado/spi/MasterStorableGenerator.java b/src/main/java/com/amazon/carbonado/spi/MasterStorableGenerator.java new file mode 100644 index 0000000..5ada2bd --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/MasterStorableGenerator.java @@ -0,0 +1,767 @@ +/* + * 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.spi; + +import java.lang.reflect.Method; +import java.util.EnumSet; +import java.util.Map; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.Label; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.Opcode; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.ClassInjector; +import org.cojen.util.KeyFactory; +import org.cojen.util.SoftValuedHashMap; + +import com.amazon.carbonado.ConstraintException; +import com.amazon.carbonado.OptimisticLockException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.Transaction; + +import com.amazon.carbonado.info.StorableInfo; +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableProperty; + +import static com.amazon.carbonado.spi.CommonMethodNames.*; + +/** + * Generates and caches abstract implementations of {@link Storable} types + * suitable for use by master repositories. The generated classes extend those + * generated by {@link StorableGenerator}. Subclasses need not worry about + * transactions since this class takes care of that. + * + * @author Brian S O'Neill + */ +public final class MasterStorableGenerator { + // Note: All generated fields/methods have a "$" character in them to + // prevent name collisions with any inherited fields/methods. User storable + // properties are defined as fields which exactly match the property + // name. We don't want collisions with those either. Legal bean properties + // cannot have "$" in them, so there's nothing to worry about. + + /** Name of protected abstract method in generated storable */ + public static final String + DO_TRY_LOAD_MASTER_METHOD_NAME = StorableGenerator.DO_TRY_LOAD_METHOD_NAME, + DO_TRY_INSERT_MASTER_METHOD_NAME = "doTryInsert$master", + DO_TRY_UPDATE_MASTER_METHOD_NAME = "doTryUpdate$master", + DO_TRY_DELETE_MASTER_METHOD_NAME = "doTryDelete$master"; + + private static final String INSERT_OP = "Insert"; + private static final String UPDATE_OP = "Update"; + private static final String DELETE_OP = "Delete"; + + // Cache of generated abstract classes. + private static Map> cCache = new SoftValuedHashMap(); + + /** + * Returns an abstract implementation of the given Storable type, which + * is fully thread-safe. The Storable type itself may be an interface or + * a class. If it is a class, then it must not be final, and it must have a + * public, no-arg constructor. The constructor for the returned abstract + * class looks like this: + * + *
+     * public <init>(MasterSupport);
+     * 
+ * + * Subclasses must implement the following abstract protected methods, + * whose exact names are defined by constants in this class: + * + *
+     * // Load the object by examining the primary key.
+     * protected abstract boolean doTryLoad() throws FetchException;
+     *
+     * // Insert the object into the storage layer.
+     * protected abstract boolean doTryInsert_master() throws PersistException;
+     *
+     * // Update the object in the storage.
+     * protected abstract boolean doTryUpdate_master() throws PersistException;
+     *
+     * // Delete the object from the storage layer by the primary key.
+     * protected abstract boolean doTryDelete_master() throws PersistException;
+     * 
+ * + * Subclasses can access the MasterSupport instance via the protected field + * named by MASTER_SUPPORT_FIELD_NAME. + * + * @throws com.amazon.carbonado.MalformedTypeException if Storable type is not well-formed + * @throws IllegalArgumentException if type is null + * @see MasterSupport + */ + public static Class + getAbstractClass(Class type, EnumSet features) + throws SupportException, IllegalArgumentException + { + StorableInfo info = StorableIntrospector.examine(type); + + anySequences: + if (features.contains(MasterFeature.INSERT_SEQUENCES)) { + for (StorableProperty property : info.getAllProperties().values()) { + if (property.getSequenceName() != null) { + break anySequences; + } + } + features.remove(MasterFeature.INSERT_SEQUENCES); + } + + if (info.getVersionProperty() == null) { + features.remove(MasterFeature.VERSIONING); + } + + if (features.contains(MasterFeature.VERSIONING)) { + // Implied feature. + features.add(MasterFeature.UPDATE_FULL); + } + + if (alwaysHasTxn(INSERT_OP, features)) { + // Implied feature. + features.add(MasterFeature.INSERT_TXN); + } + if (alwaysHasTxn(UPDATE_OP, features)) { + // Implied feature. + features.add(MasterFeature.UPDATE_TXN); + } + if (alwaysHasTxn(DELETE_OP, features)) { + // Implied feature. + features.add(MasterFeature.DELETE_TXN); + } + + if (requiresTxnForUpdate(INSERT_OP, features)) { + // Implied feature. + features.add(MasterFeature.INSERT_TXN_FOR_UPDATE); + } + if (requiresTxnForUpdate(UPDATE_OP, features)) { + // Implied feature. + features.add(MasterFeature.UPDATE_TXN_FOR_UPDATE); + } + if (requiresTxnForUpdate(DELETE_OP, features)) { + // Implied feature. + features.add(MasterFeature.DELETE_TXN_FOR_UPDATE); + } + + Object key = KeyFactory.createKey(new Object[] {type, features}); + + synchronized (cCache) { + Class abstractClass = (Class) cCache.get(key); + if (abstractClass != null) { + return abstractClass; + } + abstractClass = + new MasterStorableGenerator(type, features).generateAndInjectClass(); + cCache.put(key, abstractClass); + return abstractClass; + } + } + + private final EnumSet mFeatures; + private final StorableInfo mInfo; + private final Map> mAllProperties; + + private final ClassInjector mClassInjector; + private final ClassFile mClassFile; + + private MasterStorableGenerator(Class storableType, EnumSet features) { + mFeatures = features; + mInfo = StorableIntrospector.examine(storableType); + mAllProperties = mInfo.getAllProperties(); + + final Class abstractClass = StorableGenerator.getAbstractClass(storableType); + + mClassInjector = ClassInjector.create + (storableType.getName(), abstractClass.getClassLoader()); + + mClassFile = new ClassFile(mClassInjector.getClassName(), abstractClass); + mClassFile.setModifiers(mClassFile.getModifiers().toAbstract(true)); + mClassFile.markSynthetic(); + mClassFile.setSourceFile(MasterStorableGenerator.class.getName()); + mClassFile.setTarget("1.5"); + } + + private Class generateAndInjectClass() throws SupportException { + generateClass(); + Class abstractClass = mClassInjector.defineClass(mClassFile); + return (Class) abstractClass; + } + + private void generateClass() throws SupportException { + // Declare some types. + final TypeDesc storableType = TypeDesc.forClass(Storable.class); + final TypeDesc storageType = TypeDesc.forClass(Storage.class); + final TypeDesc triggerSupportType = TypeDesc.forClass(TriggerSupport.class); + final TypeDesc masterSupportType = TypeDesc.forClass(MasterSupport.class); + final TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + final TypeDesc optimisticLockType = TypeDesc.forClass(OptimisticLockException.class); + final TypeDesc persistExceptionType = TypeDesc.forClass(PersistException.class); + + // Add constructor that accepts a MasterSupport. + { + TypeDesc[] params = {masterSupportType}; + MethodInfo mi = mClassFile.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeSuperConstructor(new TypeDesc[] {triggerSupportType}); + + b.returnVoid(); + } + + // Declare protected abstract methods. + { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_INSERT_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + + mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + + mi = mClassFile.addMethod + (Modifiers.PROTECTED.toAbstract(true), + DO_TRY_DELETE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + } + + // Add required protected doTryInsert method. + { + // If sequence support requested, implement special insert hook to + // call sequences for properties which are UNINITIALIZED. User may + // provide explicit values for properties with sequences. + + if (mFeatures.contains(MasterFeature.INSERT_SEQUENCES)) { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED, + StorableGenerator.CHECK_PK_FOR_INSERT_METHOD_NAME, + null, null); + CodeBuilder b = new CodeBuilder(mi); + + int ordinal = 0; + for (StorableProperty property : mAllProperties.values()) { + if (property.getSequenceName() != null) { + // Check the state of this property, to see if it is + // uninitialized. Uninitialized state has value zero. + + String stateFieldName = + StorableGenerator.PROPERTY_STATE_FIELD_NAME + (ordinal >> 4); + + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + int shift = (ordinal & 0xf) * 2; + b.loadConstant(StorableGenerator.PROPERTY_STATE_MASK << shift); + b.math(Opcode.IAND); + + Label isInitialized = b.createLabel(); + b.ifZeroComparisonBranch(isInitialized, "!="); + + // Load this in preparation for storing value to property. + b.loadThis(); + + // Call MasterSupport.getSequenceValueProducer(String). + TypeDesc seqValueProdType = TypeDesc.forClass(SequenceValueProducer.class); + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(masterSupportType); + b.loadConstant(property.getSequenceName()); + b.invokeInterface + (masterSupportType, "getSequenceValueProducer", + seqValueProdType, new TypeDesc[] {TypeDesc.STRING}); + + // Find appropriate method to call for getting next sequence value. + TypeDesc propertyType = TypeDesc.forClass(property.getType()); + TypeDesc propertyObjType = propertyType.toObjectType(); + Method method; + + try { + if (propertyObjType == TypeDesc.LONG.toObjectType()) { + method = SequenceValueProducer.class + .getMethod("nextLongValue", (Class[]) null); + } else if (propertyObjType == TypeDesc.INT.toObjectType()) { + method = SequenceValueProducer.class + .getMethod("nextIntValue", (Class[]) null); + } else if (propertyObjType == TypeDesc.STRING) { + method = SequenceValueProducer.class + .getMethod("nextDecimalValue", (Class[]) null); + } else { + throw new SupportException + ("Unable to support sequence of type \"" + + property.getType().getName() + "\" for property: " + + property.getName()); + } + } catch (NoSuchMethodException e) { + Error err = new NoSuchMethodError(); + err.initCause(e); + throw err; + } + + b.invoke(method); + b.convert(TypeDesc.forClass(method.getReturnType()), propertyType); + + // Store property + b.storeField(property.getName(), propertyType); + + // Set state to dirty. + b.loadThis(); + b.loadThis(); + b.loadField(stateFieldName, TypeDesc.INT); + b.loadConstant(StorableGenerator.PROPERTY_STATE_DIRTY << shift); + b.math(Opcode.IOR); + b.storeField(stateFieldName, TypeDesc.INT); + + isInitialized.setLocation(); + } + + ordinal++; + } + + // We've tried our best to fill in missing values, now run the + // original check method. + b.loadThis(); + b.invokeSuper(mClassFile.getSuperClassName(), + StorableGenerator.CHECK_PK_FOR_INSERT_METHOD_NAME, + null, null); + b.returnVoid(); + } + + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED.toFinal(true), + StorableGenerator.DO_TRY_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + CodeBuilder b = new CodeBuilder(mi); + + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + + Label tryStart = addEnterTransaction(b, INSERT_OP, txnVar); + + if (mFeatures.contains(MasterFeature.VERSIONING)) { + // Only set if uninitialized. + b.loadThis(); + b.invokeVirtual(StorableGenerator.IS_VERSION_INITIALIZED_METHOD_NAME, + TypeDesc.BOOLEAN, null); + Label isInitialized = b.createLabel(); + b.ifZeroComparisonBranch(isInitialized, "!="); + addAdjustVersionProperty(b, null, 1); + isInitialized.setLocation(); + } + + if (mFeatures.contains(MasterFeature.INSERT_CHECK_REQUIRED)) { + // Ensure that required properties have been set. + b.loadThis(); + b.invokeVirtual(StorableGenerator.IS_REQUIRED_DATA_INITIALIZED_METHOD_NAME, + TypeDesc.BOOLEAN, null); + Label isInitialized = b.createLabel(); + b.ifZeroComparisonBranch(isInitialized, "!="); + CodeBuilderUtil.throwException(b, ConstraintException.class, + "Not all required properties have been set"); + isInitialized.setLocation(); + } + + b.loadThis(); + b.invokeVirtual(DO_TRY_INSERT_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + + if (tryStart == null) { + b.returnValue(TypeDesc.BOOLEAN); + } else { + Label failed = b.createLabel(); + b.ifZeroComparisonBranch(failed, "=="); + + addCommitAndExitTransaction(b, INSERT_OP, txnVar); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + failed.setLocation(); + addExitTransaction(b, INSERT_OP, txnVar); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + addExitTransaction(b, INSERT_OP, txnVar, tryStart); + } + } + + // Add required protected doTryUpdate method. + addDoTryUpdate: { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED.toFinal(true), + StorableGenerator.DO_TRY_UPDATE_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + CodeBuilder b = new CodeBuilder(mi); + + if ((!mFeatures.contains(MasterFeature.VERSIONING)) && + (!mFeatures.contains(MasterFeature.UPDATE_FULL))) + { + // Nothing special needs to be done, so just delegate and return. + b.loadThis(); + b.invokeVirtual(DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + b.returnValue(TypeDesc.BOOLEAN); + break addDoTryUpdate; + } + + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + LocalVariable savedVar = null; + + Label tryStart = addEnterTransaction(b, UPDATE_OP, txnVar); + + Label failed = b.createLabel(); + + if (mFeatures.contains(MasterFeature.UPDATE_FULL)) { + // Storable saved = copy(); + b.loadThis(); + b.invokeVirtual(COPY_METHOD_NAME, storableType, null); + b.checkCast(mClassFile.getType()); + savedVar = b.createLocalVariable(null, mClassFile.getType()); + b.storeLocal(savedVar); + + // if (!saved.tryLoad()) { + // goto failed; + // } + b.loadLocal(savedVar); + b.invokeInterface(storableType, TRY_LOAD_METHOD_NAME, TypeDesc.BOOLEAN, null); + b.ifZeroComparisonBranch(failed, "=="); + + // if (version support enabled) { + // if (this.getVersionNumber() != saved.getVersionNumber()) { + // throw new OptimisticLockException + // (this.getVersionNumber(), saved.getVersionNumber()); + // } + // } + if (mFeatures.contains(MasterFeature.VERSIONING)) { + TypeDesc versionType = TypeDesc.forClass(mInfo.getVersionProperty().getType()); + b.loadThis(); + b.invoke(mInfo.getVersionProperty().getReadMethod()); + b.loadLocal(savedVar); + b.invoke(mInfo.getVersionProperty().getReadMethod()); + Label sameVersion = b.createLabel(); + CodeBuilderUtil.addValuesEqualCall(b, versionType, true, sameVersion, true); + b.newObject(optimisticLockType); + b.dup(); + b.loadThis(); + b.invoke(mInfo.getVersionProperty().getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.loadLocal(savedVar); + b.invoke(mInfo.getVersionProperty().getReadMethod()); + b.convert(versionType, TypeDesc.OBJECT); + b.invokeConstructor(optimisticLockType, + new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT}); + b.throwObject(); + sameVersion.setLocation(); + } + + // this.copyDirtyProperties(saved); + // if (version support enabled) { + // saved.setVersionNumber(saved.getVersionNumber() + 1); + // } + b.loadThis(); + b.loadLocal(savedVar); + b.invokeVirtual(COPY_DIRTY_PROPERTIES, null, new TypeDesc[] {storableType}); + if (mFeatures.contains(MasterFeature.VERSIONING)) { + addAdjustVersionProperty(b, savedVar, -1); + } + + // if (!saved.doTryUpdateMaster()) { + // goto failed; + // } + b.loadLocal(savedVar); + b.invokeVirtual(DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + b.ifZeroComparisonBranch(failed, "=="); + + // saved.copyUnequalProperties(this); + b.loadLocal(savedVar); + b.loadThis(); + b.invokeInterface + (storableType, COPY_UNEQUAL_PROPERTIES, null, new TypeDesc[] {storableType}); + } else { + // if (!this.doTryUpdateMaster()) { + // goto failed; + // } + b.loadThis(); + b.invokeVirtual(DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + b.ifZeroComparisonBranch(failed, "=="); + } + + // txn.commit(); + // txn.exit(); + // return true; + addCommitAndExitTransaction(b, UPDATE_OP, txnVar); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + // failed: + // txn.exit(); + failed.setLocation(); + addExitTransaction(b, UPDATE_OP, txnVar); + // return false; + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + addExitTransaction(b, UPDATE_OP, txnVar, tryStart); + } + + // Add required protected doTryDelete method. + { + MethodInfo mi = mClassFile.addMethod + (Modifiers.PROTECTED.toFinal(true), + StorableGenerator.DO_TRY_DELETE_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(persistExceptionType); + CodeBuilder b = new CodeBuilder(mi); + + LocalVariable txnVar = b.createLocalVariable(null, transactionType); + + Label tryStart = addEnterTransaction(b, DELETE_OP, txnVar); + + b.loadThis(); + b.invokeVirtual(DO_TRY_DELETE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + + if (tryStart == null) { + b.returnValue(TypeDesc.BOOLEAN); + } else { + Label failed = b.createLabel(); + b.ifZeroComparisonBranch(failed, "=="); + addCommitAndExitTransaction(b, DELETE_OP, txnVar); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + + failed.setLocation(); + addExitTransaction(b, DELETE_OP, txnVar); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + + addExitTransaction(b, DELETE_OP, txnVar, tryStart); + } + } + } + + /** + * Generates code to enter a transaction, if required. + * + * @param opType type of operation, Insert, Update, or Delete + * @param txnVar required variable of type Transaction for storing transaction + * @return optional try start label for transaction + */ + private Label addEnterTransaction(CodeBuilder b, String opType, LocalVariable txnVar) { + if (!alwaysHasTxn(opType)) { + return null; + } + + // txn = masterSupport.getRootRepository().enterTransaction(); + + TypeDesc repositoryType = TypeDesc.forClass(Repository.class); + TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + TypeDesc triggerSupportType = TypeDesc.forClass(TriggerSupport.class); + TypeDesc masterSupportType = TypeDesc.forClass(MasterSupport.class); + + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.invokeInterface(masterSupportType, "getRootRepository", + repositoryType, null); + b.invokeInterface(repositoryType, ENTER_TRANSACTION_METHOD_NAME, + transactionType, null); + b.storeLocal(txnVar); + if (requiresTxnForUpdate(opType)) { + // txn.setForUpdate(true); + b.loadLocal(txnVar); + b.loadConstant(true); + b.invokeInterface(transactionType, SET_FOR_UPDATE_METHOD_NAME, null, + new TypeDesc[] {TypeDesc.BOOLEAN}); + } + + return b.createLabel().setLocation(); + } + + private boolean alwaysHasTxn(String opType) { + return alwaysHasTxn(opType, mFeatures); + } + + private static boolean alwaysHasTxn(String opType, EnumSet features) { + if (opType == UPDATE_OP) { + return + features.contains(MasterFeature.UPDATE_TXN) || + features.contains(MasterFeature.UPDATE_TXN_FOR_UPDATE) || + features.contains(MasterFeature.VERSIONING) || + features.contains(MasterFeature.UPDATE_FULL); + } else if (opType == INSERT_OP) { + return + features.contains(MasterFeature.INSERT_TXN) || + features.contains(MasterFeature.INSERT_TXN_FOR_UPDATE); + } else if (opType == DELETE_OP) { + return + features.contains(MasterFeature.DELETE_TXN) || + features.contains(MasterFeature.DELETE_TXN_FOR_UPDATE); + } + return false; + } + + private boolean requiresTxnForUpdate(String opType) { + return requiresTxnForUpdate(opType, mFeatures); + } + + private static boolean requiresTxnForUpdate(String opType, EnumSet features) { + if (opType == UPDATE_OP) { + return + features.contains(MasterFeature.UPDATE_TXN_FOR_UPDATE) || + features.contains(MasterFeature.VERSIONING) || + features.contains(MasterFeature.UPDATE_FULL); + } else if (opType == INSERT_OP) { + return features.contains(MasterFeature.INSERT_TXN_FOR_UPDATE); + } else if (opType == DELETE_OP) { + return features.contains(MasterFeature.DELETE_TXN_FOR_UPDATE); + } + return false; + } + + private void addCommitAndExitTransaction(CodeBuilder b, String opType, LocalVariable txnVar) { + if (!alwaysHasTxn(opType)) { + return; + } + + TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + + // txn.commit(); + // txn.exit(); + b.loadLocal(txnVar); + b.invokeInterface(transactionType, COMMIT_METHOD_NAME, null, null); + b.loadLocal(txnVar); + b.invokeInterface(transactionType, EXIT_METHOD_NAME, null, null); + } + + /** + * + * @param opType type of operation, Insert, Update, or Delete + */ + private void addExitTransaction(CodeBuilder b, String opType, LocalVariable txnVar) { + if (!alwaysHasTxn(opType)) { + return; + } + + TypeDesc transactionType = TypeDesc.forClass(Transaction.class); + + // txn.exit(); + b.loadLocal(txnVar); + b.invokeInterface(transactionType, EXIT_METHOD_NAME, null, null); + } + + /** + * + * @param opType type of operation, Insert, Update, or Delete + */ + private void addExitTransaction(CodeBuilder b, String opType, LocalVariable txnVar, + Label tryStart) + { + if (tryStart == null) { + addExitTransaction(b, opType, txnVar); + return; + } + + // } catch (... e) { + // txn.exit(); + // throw e; + // } + + Label tryEnd = b.createLabel().setLocation(); + b.exceptionHandler(tryStart, tryEnd, null); + addExitTransaction(b, opType, txnVar); + b.throwObject(); + } + + /* + * Generates code to adjust the version property. If value parameter is negative, then + * version is incremented as follows: + * + * storable.setVersionNumber(storable.getVersionNumber() + 1); + * + * Otherwise, the version is set: + * + * storable.setVersionNumber(value); + * + * @param storableVar references storable instance, or null if this + * @param value if negative, increment version, else, set version to this value + */ + private void addAdjustVersionProperty(CodeBuilder b, + LocalVariable storableVar, + int value) + throws SupportException + { + StorableProperty versionProperty = mInfo.getVersionProperty(); + + TypeDesc versionType = TypeDesc.forClass(versionProperty.getType()); + TypeDesc versionPrimitiveType = versionType.toPrimitiveType(); + supportCheck: { + if (versionPrimitiveType != null) { + switch (versionPrimitiveType.getTypeCode()) { + case TypeDesc.INT_CODE: + case TypeDesc.LONG_CODE: + break supportCheck; + } + } + throw new SupportException + ("Unsupported version type: " + versionType.getFullName()); + } + + if (storableVar == null) { + b.loadThis(); + } else { + b.loadLocal(storableVar); + } + + if (value >= 0) { + if (versionPrimitiveType == TypeDesc.LONG) { + b.loadConstant((long) value); + } else { + b.loadConstant(value); + } + } else { + b.dup(); + b.invoke(versionProperty.getReadMethod()); + Label setVersion = b.createLabel(); + if (!versionType.isPrimitive()) { + b.dup(); + Label versionNotNull = b.createLabel(); + b.ifNullBranch(versionNotNull, false); + b.pop(); + if (versionPrimitiveType == TypeDesc.LONG) { + b.loadConstant(1L); + } else { + b.loadConstant(1); + } + b.branch(setVersion); + versionNotNull.setLocation(); + b.convert(versionType, versionPrimitiveType); + } + if (versionPrimitiveType == TypeDesc.LONG) { + b.loadConstant(1L); + b.math(Opcode.LADD); + } else { + b.loadConstant(1); + b.math(Opcode.IADD); + } + setVersion.setLocation(); + } + + b.convert(versionPrimitiveType, versionType); + b.invoke(versionProperty.getWriteMethod()); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/MasterSupport.java b/src/main/java/com/amazon/carbonado/spi/MasterSupport.java new file mode 100644 index 0000000..de1e521 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/MasterSupport.java @@ -0,0 +1,38 @@ +/* + * 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.spi; + +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; + +/** + * Provides runtime support for Storable classes generated by {@link MasterStorableGenerator}. + * + * @author Brian S O'Neill + */ +public interface MasterSupport extends TriggerSupport { + /** + * Returns a sequence value producer by name, or throw PersistException if not found. + * + *

Note: this method throws PersistException even for fetch failures + * since this method is called by insert operations. Insert operations can + * only throw a PersistException. + */ + SequenceValueProducer getSequenceValueProducer(String name) throws PersistException; +} diff --git a/src/main/java/com/amazon/carbonado/spi/RAFInputStream.java b/src/main/java/com/amazon/carbonado/spi/RAFInputStream.java new file mode 100644 index 0000000..c13763e --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/RAFInputStream.java @@ -0,0 +1,62 @@ +/* + * 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.spi; + +import java.io.InputStream; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * InputStream that wraps a RandomAccessFile. A stream can be obtained for a + * RandomAccessFile by getting the file descriptor and creating a + * FileInputStream on it. Problem is that FileInputStream has a finalizer that + * closes the RandomAccessFile. + * + * @author Brian S O'Neill + */ +public class RAFInputStream extends InputStream { + private final RandomAccessFile mRAF; + + public RAFInputStream(RandomAccessFile raf) { + mRAF = raf; + } + + public int read() throws IOException { + return mRAF.read(); + } + + public int read(byte[] b) throws IOException { + return mRAF.read(b); + } + + public int read(byte[] b, int offset, int length) throws IOException { + return mRAF.read(b, offset, length); + } + + public long skip(long n) throws IOException { + if (n > Integer.MAX_VALUE) { + n = Integer.MAX_VALUE; + } + return mRAF.skipBytes((int) n); + } + + public void close() throws IOException { + mRAF.close(); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/RAFOutputStream.java b/src/main/java/com/amazon/carbonado/spi/RAFOutputStream.java new file mode 100644 index 0000000..1f2954f --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/RAFOutputStream.java @@ -0,0 +1,55 @@ +/* + * 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.spi; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +/** + * OutputStream that wraps a RandomAccessFile. A stream can be obtained for a + * RandomAccessFile by getting the file descriptor and creating a + * FileOutputStream on it. Problem is that FileOutputStream has a finalizer + * that closes the RandomAccessFile. + * + * @author Brian S O'Neill + */ +public class RAFOutputStream extends OutputStream { + private final RandomAccessFile mRAF; + + public RAFOutputStream(RandomAccessFile raf) { + mRAF = raf; + } + + public void write(int b) throws IOException { + mRAF.write(b); + } + + public void write(byte[] b) throws IOException { + mRAF.write(b); + } + + public void write(byte[] b, int offset, int length) throws IOException { + mRAF.write(b, offset, length); + } + + public void close() throws IOException { + mRAF.close(); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/RepairExecutor.java b/src/main/java/com/amazon/carbonado/spi/RepairExecutor.java new file mode 100644 index 0000000..adf2f34 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/RepairExecutor.java @@ -0,0 +1,183 @@ +/* + * 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.spi; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * A convenience class for repositories to run dynamic repairs in separate + * threads. When a repository detects a consistency error during a user + * operation, it should not perform the repair in the same thread. + * + *

If the repair was initiated by an exception, but the original exception + * is re-thrown, a transaction exit will rollback the repair! Executing the + * repair in a separate thread allows it to wait until the transaction has + * exited. + * + *

Other kinds of inconsistencies might be detected during cursor + * iteration. The repair will need to acquire write locks, but the open cursor + * might not allow that, resulting in deadlock. Executing the repair in a + * separate thread allows it to wait until the cursor has released locks. + * + *

This class keeps thread-local references to single-threaded executors. In + * other words, each user thread has at most one associated repair thread. Each + * repair thread has a fixed size queue, and they exit when they are idle. If + * the queue is full, newly added repair tasks are silently discarded. + * + *

The following system properties are supported: + * + *

    + *
  • com.amazon.carbonado.spi.RepairExecutor.keepAliveSeconds (default is 10) + *
  • com.amazon.carbonado.spi.RepairExecutor.queueSize (default is 10000) + *
+ * + * @author Brian S O'Neill + */ +public class RepairExecutor { + static final ThreadLocal cExecutor; + + static { + final int keepAliveSeconds = Integer.getInteger + ("com.amazon.carbonado.spi.RepairExecutor.keepAliveSeconds", 10); + final int queueSize = Integer.getInteger + ("com.amazon.carbonado.spi.RepairExecutor.queueSize", 10000); + + cExecutor = new ThreadLocal() { + protected RepairExecutor initialValue() { + return new RepairExecutor(keepAliveSeconds, queueSize); + } + }; + } + + public static void execute(Runnable repair) { + cExecutor.get().executeIt(repair); + } + + /** + * Waits for repairs that were executed from the current thread to finish. + * + * @return true if all repairs are finished + */ + public static boolean waitForRepairsToFinish(long timeoutMillis) throws InterruptedException { + return cExecutor.get().waitToFinish(timeoutMillis); + } + + private final int mKeepAliveSeconds; + private BlockingQueue mQueue; + private Worker mWorker; + private boolean mIdle = true; + + private RepairExecutor(int keepAliveSeconds, int queueSize) { + mKeepAliveSeconds = keepAliveSeconds; + mQueue = new LinkedBlockingQueue(queueSize); + } + + private synchronized void executeIt(Runnable repair) { + mQueue.offer(repair); + if (mWorker == null) { + mWorker = new Worker(); + mWorker.start(); + } + } + + private synchronized boolean waitToFinish(long timeoutMillis) throws InterruptedException { + if (mIdle && mQueue.size() == 0) { + return true; + } + + if (mWorker == null) { + // The worker should never be null if the queue has elements. + mWorker = new Worker(); + mWorker.start(); + } + + if (timeoutMillis != 0) { + if (timeoutMillis < 0) { + while (!mIdle || mQueue.size() > 0) { + wait(); + } + } else { + long start = System.currentTimeMillis(); + while (timeoutMillis > 0 && (!mIdle || mQueue.size() > 0)) { + wait(timeoutMillis); + long now = System.currentTimeMillis(); + timeoutMillis -= (now - start); + start = now; + } + } + } + + return mQueue.size() == 0; + } + + Runnable dequeue() throws InterruptedException { + while (true) { + synchronized (this) { + mIdle = true; + notify(); + } + Runnable task = mQueue.poll(mKeepAliveSeconds, TimeUnit.SECONDS); + synchronized (this) { + if (task != null) { + mIdle = false; + return task; + } + if (mQueue.size() == 0) { + notify(); + mWorker = null; + return null; + } + } + } + } + + private class Worker extends Thread { + Worker() { + setDaemon(true); + setName(Thread.currentThread().getName() + " (repository repair)"); + } + + public void run() { + while (true) { + try { + Runnable task = dequeue(); + if (task == null) { + break; + } + task.run(); + } catch (InterruptedException e) { + break; + } catch (ThreadDeath e) { + break; + } catch (Throwable e) { + try { + Thread t = Thread.currentThread(); + t.getUncaughtExceptionHandler().uncaughtException(t, e); + } catch (ThreadDeath e2) { + break; + } catch (Throwable e2) { + // Ignore exceptions thrown while reporting exceptions. + } + } + } + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/RunnableTransaction.java b/src/main/java/com/amazon/carbonado/spi/RunnableTransaction.java new file mode 100644 index 0000000..5ad3517 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/RunnableTransaction.java @@ -0,0 +1,114 @@ +/* + * 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.spi; + +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.Transaction; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.PersistException; + +/** + * Neatly scope a transactional operation. To use, a subclass of RunnableTransaction should be + * provided which implements any one of the three flavors of the body method. + * The default implementations pass control from most specific to least specific -- that is, + * from {@link #body(Storable)} to {@link #body()} -- so the + * implementor is free to override whichever makes the most sense. + * + *

A typical use pattern would be: + * + *

+ *  RunnableTransaction rt = new RunnableTransaction(repository) {
+ *      public void body() throws PersistException {
+ *        for (Storable s : someFieldContainingStorables) {
+ *            s.insert();
+ *       }
+ *  };
+ *  rt.run();
+ * 
+ * + * @author Don Schneider + * @author Todd V. Jonker (jonker) + */ +public class RunnableTransaction { + final Repository mRepo; + + public RunnableTransaction(Repository repo) { + mRepo = repo; + } + + /** + * Enter a transaction, execute {@link #body(Storable)} for each storable, commit + * if no exception, and exit the transaction. + * @param storables array of storables on which to operate + * @throws PersistException + */ + public final void run(S storable, S... storables) + throws PersistException + { + Transaction txn = mRepo.enterTransaction(); + try { + for (S s : storables) { + body(s); + } + txn.commit(); + } finally { + txn.exit(); + } + } + + /** + * Enter a transaction, execute {@link #body(Storable)} on the provided storable, commit if no + * exception, and exit the transaction. + * @param storable on which to operate + * @throws PersistException + */ + public final void run(S storable) throws PersistException { + Transaction txn = mRepo.enterTransaction(); + try { + body(storable); + txn.commit(); + } finally { + txn.exit(); + } + } + + /** + * Enter a transaction on the provided repository, execute {@link #body()} + * @throws PersistException + */ + public final void run() throws PersistException { + Transaction txn = mRepo.enterTransaction(); + try { + body(); + txn.commit(); + } finally { + txn.exit(); + } + } + + public void body(S s) throws PersistException { + body(); + } + + public void body() throws PersistException { + } + + public String toString() { + return "RunnableTransaction(" + mRepo + ')'; + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/SequenceValueGenerator.java b/src/main/java/com/amazon/carbonado/spi/SequenceValueGenerator.java new file mode 100644 index 0000000..c7bfc78 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/SequenceValueGenerator.java @@ -0,0 +1,288 @@ +/* + * 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.spi; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.Transaction; + +/** + * General purpose implementation of a sequence value generator. + * + * @author Brian S O'Neill + * @see com.amazon.carbonado.Sequence + * @see StoredSequence + */ +public class SequenceValueGenerator extends AbstractSequenceValueProducer { + private static final int DEFAULT_RESERVE_AMOUNT = 100; + private static final int DEFAULT_INITIAL_VALUE = 1; + private static final int DEFAULT_INCREMENT = 1; + + private final Repository mRepository; + private final Storage mStorage; + private final StoredSequence mStoredSequence; + private final int mIncrement; + private final int mReserveAmount; + + private boolean mHasReservedValues; + private long mNextValue; + + /** + * Construct a new SequenceValueGenerator which might create persistent + * sequence data if it does not exist. The initial sequence value is one, + * and the increment is one. + * + * @param repo repository to persist sequence data + * @param name name of sequence + */ + public SequenceValueGenerator(Repository repo, String name) + throws RepositoryException + { + this(repo, name, DEFAULT_INITIAL_VALUE, DEFAULT_INCREMENT); + } + + /** + * Construct a new SequenceValueGenerator which might create persistent + * sequence data if it does not exist. + * + * @param repo repository to persist sequence data + * @param name name of sequence + * @param initialValue initial sequence value, if sequence needs to be created + * @param increment amount to increment sequence by + */ + public SequenceValueGenerator(Repository repo, String name, long initialValue, int increment) + throws RepositoryException + { + this(repo, name, initialValue, increment, DEFAULT_RESERVE_AMOUNT); + } + + /** + * Construct a new SequenceValueGenerator which might create persistent + * sequence data if it does not exist. + * + * @param repo repository to persist sequence data + * @param name name of sequence + * @param initialValue initial sequence value, if sequence needs to be created + * @param increment amount to increment sequence by + * @param reserveAmount amount of sequence values to reserve + */ + public SequenceValueGenerator(Repository repo, String name, + long initialValue, int increment, int reserveAmount) + throws RepositoryException + { + if (repo == null || name == null || increment < 1 || reserveAmount < 1) { + throw new IllegalArgumentException(); + } + + mRepository = repo; + + mIncrement = increment; + mReserveAmount = reserveAmount; + + mStorage = repo.storageFor(StoredSequence.class); + + mStoredSequence = mStorage.prepare(); + mStoredSequence.setName(name); + + Transaction txn = repo.enterTopTransaction(null); + txn.setForUpdate(true); + try { + if (!mStoredSequence.tryLoad()) { + mStoredSequence.setInitialValue(initialValue); + // Start as small as possible to allow signed long comparisons to work. + mStoredSequence.setNextValue(Long.MIN_VALUE); + mStoredSequence.insert(); + } + txn.commit(); + } finally { + txn.exit(); + } + } + + /** + * Reset the sequence. + * + * @param initialValue first value produced by sequence + */ + public void reset(int initialValue) throws FetchException, PersistException { + synchronized (mStoredSequence) { + Transaction txn = mRepository.enterTopTransaction(null); + txn.setForUpdate(true); + try { + boolean doUpdate = mStoredSequence.tryLoad(); + mStoredSequence.setInitialValue(initialValue); + // Start as small as possible to allow signed long comparisons to work. + mStoredSequence.setNextValue(Long.MIN_VALUE); + if (doUpdate) { + mStoredSequence.update(); + } else { + mStoredSequence.insert(); + } + txn.commit(); + mHasReservedValues = false; + } finally { + txn.exit(); + } + } + } + + /** + * Returns the next value from the sequence, which may wrap negative if all + * positive values are exhausted. When sequence wraps back to initial + * value, the sequence is fully exhausted, and an exception is thrown to + * indicate this. + * + *

Note: this method throws PersistException even for fetch failures + * since this method is called by insert operations. Insert operations can + * only throw a PersistException. + * + * @throws PersistException for fetch/persist failure or if sequence is exhausted. + */ + public long nextLongValue() throws PersistException { + try { + synchronized (mStoredSequence) { + return nextUnadjustedValue() + Long.MIN_VALUE + mStoredSequence.getInitialValue(); + } + } catch (FetchException e) { + throw e.toPersistException(); + } + } + + /** + * Returns the next value from the sequence, which may wrap negative if all + * positive values are exhausted. When sequence wraps back to initial + * value, the sequence is fully exhausted, and an exception is thrown to + * indicate this. + * + *

Note: this method throws PersistException even for fetch failures + * since this method is called by insert operations. Insert operations can + * only throw a PersistException. + * + * @throws PersistException for fetch/persist failure or if sequence is + * exhausted for int values. + */ + public int nextIntValue() throws PersistException { + try { + synchronized (mStoredSequence) { + long initial = mStoredSequence.getInitialValue(); + if (initial >= 0x100000000L) { + throw new PersistException + ("Sequence initial value too large to support 32-bit ints: " + + mStoredSequence.getName() + ", initial: " + initial); + } + long next = nextUnadjustedValue(); + if (next >= Long.MIN_VALUE + 0x100000000L) { + // Everytime we throw this exception, a long sequence value + // has been lost. This seems fairly benign. + throw new PersistException + ("Sequence exhausted for 32-bit ints: " + mStoredSequence.getName() + + ", next: " + (next + Long.MIN_VALUE + initial)); + } + return (int) (next + Long.MIN_VALUE + initial); + } + } catch (FetchException e) { + throw e.toPersistException(); + } + } + + /** + * Allow any unused reserved values to be returned for re-use. If the + * repository is shared by other processes, then reserved values might not + * be returnable. + * + *

This method should be called during the shutdown process of a + * repository, although calling it does not invalidate this + * SequenceValueGenerator. If getNextValue is called again, it will reserve + * values again. + * + * @return true if reserved values were returned + */ + public boolean returnReservedValues() throws FetchException, PersistException { + synchronized (mStoredSequence) { + if (mHasReservedValues) { + Transaction txn = mRepository.enterTopTransaction(null); + txn.setForUpdate(true); + try { + // Compare known StoredSequence with current persistent + // one. If same, then reserved values can be returned. + StoredSequence current = mStorage.prepare(); + current.setName(mStoredSequence.getName()); + if (current.tryLoad() && current.equals(mStoredSequence)) { + mStoredSequence.setNextValue(mNextValue + mIncrement); + mStoredSequence.update(); + txn.commit(); + mHasReservedValues = false; + return true; + } + } finally { + txn.exit(); + } + } + } + return false; + } + + // Assumes caller has synchronized on mStoredSequence + private long nextUnadjustedValue() throws FetchException, PersistException { + if (mHasReservedValues) { + long next = mNextValue + mIncrement; + mNextValue = next; + if (next < mStoredSequence.getNextValue()) { + return next; + } + mHasReservedValues = false; + } + + Transaction txn = mRepository.enterTopTransaction(null); + txn.setForUpdate(true); + try { + // Assume that StoredSequence is stale, so reload. + mStoredSequence.load(); + long next = mStoredSequence.getNextValue(); + long nextStored = next + mReserveAmount * mIncrement; + + if (next >= 0 & nextStored < 0) { + // Wrapped around. There might be just a few values left. + long avail = (Long.MAX_VALUE - next) / mIncrement; + if (avail > 0) { + nextStored = next + avail * mIncrement; + } else { + // Throw a PersistException since sequences are applied during + // insert operations, and inserts can only throw PersistExceptions. + throw new PersistException + ("Sequence exhausted: " + mStoredSequence.getName()); + } + } + + mStoredSequence.setNextValue(nextStored); + mStoredSequence.update(); + + txn.commit(); + + mNextValue = next; + mHasReservedValues = true; + return next; + } finally { + txn.exit(); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/SequenceValueProducer.java b/src/main/java/com/amazon/carbonado/spi/SequenceValueProducer.java new file mode 100644 index 0000000..659de89 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/SequenceValueProducer.java @@ -0,0 +1,87 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.carbonado.spi; + +import com.amazon.carbonado.PersistException; + +/** + * Produces values for sequences. + * + * @author Brian S O'Neill + * @see com.amazon.carbonado.Sequence + */ +public interface SequenceValueProducer { + /** + * Returns the next value from the sequence, which may wrap negative if all + * positive values are exhausted. When sequence wraps back to initial + * value, the sequence is fully exhausted, and an exception is thrown to + * indicate this. + * + *

Note: this method throws PersistException even for fetch failures + * since this method is called by insert operations. Insert operations can + * only throw a PersistException. + * + * @throws PersistException for fetch/persist failure or if sequence is exhausted. + */ + public long nextLongValue() throws PersistException; + + /** + * Returns the next value from the sequence, which may wrap negative if all + * positive values are exhausted. When sequence wraps back to initial + * value, the sequence is fully exhausted, and an exception is thrown to + * indicate this. + * + *

Note: this method throws PersistException even for fetch failures + * since this method is called by insert operations. Insert operations can + * only throw a PersistException. + * + * @throws PersistException for fetch/persist failure or if sequence is + * exhausted for int values. + */ + public int nextIntValue() throws PersistException; + + /** + * Returns the next decimal string value from the sequence, which remains + * positive. When sequence wraps back to initial value, the sequence is + * fully exhausted, and an exception is thrown to indicate this. + * + *

Note: this method throws PersistException even for fetch failures + * since this method is called by insert operations. Insert operations can + * only throw a PersistException. + * + * @throws PersistException for fetch/persist failure or if sequence is exhausted. + */ + public String nextDecimalValue() throws PersistException; + + /** + * Returns the next numerical string value from the sequence, which remains + * positive. When sequence wraps back to initial value, the sequence is + * fully exhausted, and an exception is thrown to indicate this. + * + *

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

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

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

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

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

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

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

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

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

Methods which delegate to wrapped Storable: + * + *

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

Methods with special implementation: + * + *

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

After adding the primary key via this method and after reducing the + * set, call {@link #findPrimaryKeyIndex findPrimaryKeyIndex} to get the + * best index to represent the primary key. + * + * @throws IllegalArgumentException if info is null + */ + public void addPrimaryKey(StorableInfo info) { + if (info == null) { + throw new IllegalArgumentException(); + } + addKey(info.getPrimaryKey()); + } + + /** + * Adds the key as a unique index, preserving the property arrangement. + * + * @throws IllegalArgumentException if key is null + */ + @SuppressWarnings("unchecked") + public void addKey(StorableKey key) { + if (key == null) { + throw new IllegalArgumentException(); + } + add(new StorableIndex(key, Direction.UNSPECIFIED)); + } + + /** + * Reduces the size of the set by removing redundant indexes, and merges + * others together. + */ + public void reduce() { + reduce(Direction.UNSPECIFIED); + } + + /** + * Reduces the size of the set by removing redundant indexes, and merges + * others together. + * + * @param defaultDirection replace unspecified property directions with this + */ + public void reduce(Direction defaultDirection) { + List> group = new ArrayList>(); + Map, StorableIndex> mergedReplacements = + new TreeMap, StorableIndex>(STORABLE_INDEX_COMPARATOR); + + Iterator> it = iterator(); + while (it.hasNext()) { + StorableIndex candidate = it.next(); + + if (group.size() == 0 || isDifferentGroup(group.get(0), candidate)) { + group.clear(); + group.add(candidate); + continue; + } + + if (isRedundant(group, candidate, mergedReplacements)) { + it.remove(); + } else { + group.add(candidate); + } + } + + // Now replace merged indexes. + replaceEntries(mergedReplacements); + + // Apply default sort direction to those unspecified. + if (defaultDirection != Direction.UNSPECIFIED) { + Map, StorableIndex> replacements = null; + for (StorableIndex index : this) { + StorableIndex replacement = index.setDefaultDirection(defaultDirection); + if (replacement != index) { + if (replacements == null) { + replacements = new HashMap, StorableIndex>(); + } + replacements.put(index, replacement); + } + } + replaceEntries(replacements); + } + } + + /** + * Augment non-unique indexes with primary key properties, thus making them + * unique. + * + * @throws IllegalArgumentException if info is null + */ + public void uniquify(StorableInfo info) { + if (info == null) { + throw new IllegalArgumentException(); + } + uniquify(info.getPrimaryKey()); + } + + /** + * Augment non-unique indexes with key properties, thus making them unique. + * + * @throws IllegalArgumentException if key is null + */ + public void uniquify(StorableKey key) { + if (key == null) { + throw new IllegalArgumentException(); + } + + + // Replace indexes which were are implied unique, even if they are not + // declared as such. + { + Map, StorableIndex> replacements = null; + for (StorableIndex index : this) { + if (!index.isUnique() && isUniqueImplied(index)) { + if (replacements == null) { + replacements = new HashMap, StorableIndex>(); + } + replacements.put(index, index.unique(true)); + } + } + replaceEntries(replacements); + } + + // Now augment with key properties. + { + Map, StorableIndex> replacements = null; + for (StorableIndex index : this) { + StorableIndex replacement = index.uniquify(key); + if (replacement != index) { + if (replacements == null) { + replacements = new HashMap, StorableIndex>(); + } + replacements.put(index, replacement); + } + } + replaceEntries(replacements); + } + } + + /** + * Finds the best index to represent the primary key. Should be called + * after calling reduce. As long as the primary key was added via {@link + * #addPrimaryKey addPrimaryKey}, this method should never return null. + * + * @throws IllegalArgumentException if info is null + */ + public StorableIndex findPrimaryKeyIndex(StorableInfo info) { + if (info == null) { + throw new IllegalArgumentException(); + } + return findKeyIndex(info.getPrimaryKey()); + } + + /** + * Finds the best index to represent the given key. Should be called after + * calling reduce. As long as the key was added via {@link #addKey addKey}, + * this method should never return null. + * + * @throws IllegalArgumentException if key is null + */ + public StorableIndex findKeyIndex(StorableKey key) { + if (key == null) { + throw new IllegalArgumentException(); + } + + Set> orderedProps = key.getProperties(); + + Set> keyProps = new HashSet>(); + for (OrderedProperty orderedProp : orderedProps) { + keyProps.add(orderedProp.getChainedProperty().getPrimeProperty()); + } + + search: for (StorableIndex index : this) { + if (!index.isUnique() || index.getPropertyCount() != keyProps.size()) { + continue search; + } + for (int i=index.getPropertyCount(); --i>=0; ) { + if (!keyProps.contains(index.getProperty(i))) { + continue search; + } + } + return index; + } + + return null; + } + + /** + * Return true if index is unique or fully contains the members of a unique index. + */ + private boolean isUniqueImplied(StorableIndex candidate) { + if (candidate.isUnique()) { + return true; + } + if (this.size() <= 1) { + return false; + } + + Set> candidateProps = new HashSet>(); + for (int i=candidate.getPropertyCount(); --i>=0; ) { + candidateProps.add(candidate.getProperty(i)); + } + + search: for (StorableIndex index : this) { + if (!index.isUnique()) { + continue search; + } + for (int i=index.getPropertyCount(); --i>=0; ) { + if (!candidateProps.contains(index.getProperty(i))) { + continue search; + } + } + return true; + } + + return false; + } + + private boolean isDifferentGroup(StorableIndex groupLeader, StorableIndex candidate) { + int count = candidate.getPropertyCount(); + if (count > groupLeader.getPropertyCount()) { + return true; + } + for (int i=0; i> group, StorableIndex candidate, + Map, StorableIndex> mergedReplacements) { + // All visited group members will have an equal or greater number of + // properties. This is ensured by the ordering of the set. + int count = candidate.getPropertyCount(); + + ListIterator> it = group.listIterator(); + groupScan: + while (it.hasNext()) { + StorableIndex member = it.next(); + + boolean moreQualified = false; + boolean canReverse = true; + boolean reverse = false; + + for (int i=0; i merged = + new StorableIndex(member.getProperties(), directions) + .unique(member.isUnique()); + mergedReplacements.put(member, merged); + it.set(merged); + } + + // Candidate is redundant. + return true; + } + + return false; + } + + private void replaceEntries(Map, StorableIndex> replacements) { + if (replacements != null) { + for (Map.Entry, StorableIndex> e : replacements.entrySet()) { + remove(e.getKey()); + add(e.getValue()); + } + } + } + + /** + * Orders indexes such that they are grouped by property names. Within + * those groups, indexes are ordered most qualified to least qualified. + */ + private static class StorableIndexComparator implements Comparator> { + public int compare(StorableIndex a, StorableIndex b) { + if (a == b) { + return 0; + } + + int aCount = a.getPropertyCount(); + int bCount = b.getPropertyCount(); + + int count = Math.min(aCount, bCount); + + for (int i=0; i bCount) { + return -1; + } else if (aCount < bCount) { + return 1; + } + + // Counts are the same, property names are the same. Unique indexes + // are first, followed by index with more leading directions. Favor + // ascending direction. + + for (int i=0; i + * TODO: This class is unable to determine state of properties, and so they are + * lost during serialization. Upon deserialization, all properties are assumed + * dirty. To fix this, serialization might need to be supported directly by + * Storables. When that happens, this class will be deprecated. + * + * @author Brian S O'Neill + */ +public abstract class StorableSerializer { + private static final String ENCODE_METHOD_NAME = "encode"; + private static final String DECODE_METHOD_NAME = "decode"; + private static final String WRITE_METHOD_NAME = "write"; + private static final String READ_METHOD_NAME = "read"; + + @SuppressWarnings("unchecked") + private static Map>> cCache = new WeakIdentityMap(); + + /** + * @param type type of storable to serialize + */ + @SuppressWarnings("unchecked") + public static StorableSerializer forType(Class type) + throws SupportException + { + synchronized (cCache) { + StorableSerializer serializer; + Reference> ref = cCache.get(type); + if (ref != null) { + serializer = (StorableSerializer) ref.get(); + if (serializer != null) { + return serializer; + } + } + serializer = generateSerializer(type); + cCache.put(type, new SoftReference>(serializer)); + return serializer; + } + } + + @SuppressWarnings("unchecked") + private static StorableSerializer generateSerializer(Class type) + throws SupportException + { + Class abstractClass = StorableGenerator.getAbstractClass(type); + + // Use abstract class ClassLoader in order to access adapter instances. + ClassInjector ci = ClassInjector.create + (type.getName(), abstractClass.getClassLoader()); + ClassFile cf = new ClassFile(ci.getClassName(), StorableSerializer.class); + cf.markSynthetic(); + cf.setSourceFile(StorableSerializer.class.getName()); + cf.setTarget("1.5"); + + cf.addDefaultConstructor(); + + Map> propertyMap = + StorableIntrospector.examine(type).getAllProperties(); + + StorableProperty[] properties; + { + // Exclude joins. + List> list = + new ArrayList>(propertyMap.size()); + for (StorableProperty property : propertyMap.values()) { + if (!property.isJoin()) { + list.add(property); + } + } + properties = new StorableProperty[list.size()]; + list.toArray(properties); + } + + GenericEncodingStrategy ges = new GenericEncodingStrategy(type, null); + + TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + TypeDesc storableType = TypeDesc.forClass(Storable.class); + TypeDesc userStorableType = TypeDesc.forClass(type); + TypeDesc storageType = TypeDesc.forClass(Storage.class); + + // Build method to encode storable into a byte array. + { + MethodInfo mi = cf.addMethod + (Modifiers.PRIVATE.toStatic(true), ENCODE_METHOD_NAME, byteArrayType, + new TypeDesc[] {userStorableType}); + CodeBuilder b = new CodeBuilder(mi); + LocalVariable encodedVar = + ges.buildDataEncoding(b, properties, b.getParameter(0), abstractClass, true, -1); + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } + + // Build method to decode storable from a byte array. + { + MethodInfo mi = cf.addMethod + (Modifiers.PRIVATE.toStatic(true), DECODE_METHOD_NAME, userStorableType, + new TypeDesc[] {storageType, byteArrayType}); + CodeBuilder b = new CodeBuilder(mi); + LocalVariable instanceVar = b.createLocalVariable(null, userStorableType); + b.loadLocal(b.getParameter(0)); + b.invokeInterface(storageType, PREPARE_METHOD_NAME, + storableType, null); + b.checkCast(userStorableType); + b.storeLocal(instanceVar); + LocalVariable encodedVar = b.getParameter(1); + ges.buildDataDecoding + (b, properties, instanceVar, abstractClass, true, -1, null, encodedVar); + b.loadLocal(instanceVar); + b.returnValue(storableType); + } + + // Build write method for DataOutput. + { + TypeDesc dataOutputType = TypeDesc.forClass(DataOutput.class); + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, WRITE_METHOD_NAME, null, + new TypeDesc[] {storableType, dataOutputType}); + + CodeBuilder b = new CodeBuilder(mi); + LocalVariable storableVar = b.getParameter(0); + LocalVariable doutVar = b.getParameter(1); + + b.loadLocal(storableVar); + b.checkCast(userStorableType); + b.invokeStatic(ENCODE_METHOD_NAME, byteArrayType, new TypeDesc[] {userStorableType}); + LocalVariable encodedVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(encodedVar); + + b.loadLocal(doutVar); + b.loadLocal(encodedVar); + b.arrayLength(); + b.invokeInterface(dataOutputType, "writeInt", null, new TypeDesc[] {TypeDesc.INT}); + + b.loadLocal(doutVar); + b.loadLocal(encodedVar); + b.invokeInterface(dataOutputType, "write", null, new TypeDesc[] {byteArrayType}); + b.returnVoid(); + } + + final TypeDesc storableSerializerType = TypeDesc.forClass(StorableSerializer.class); + + // Build write method for OutputStream. + { + TypeDesc outputStreamType = TypeDesc.forClass(OutputStream.class); + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, WRITE_METHOD_NAME, null, + new TypeDesc[] {storableType, outputStreamType}); + + CodeBuilder b = new CodeBuilder(mi); + LocalVariable storableVar = b.getParameter(0); + LocalVariable outVar = b.getParameter(1); + + b.loadLocal(storableVar); + b.checkCast(userStorableType); + b.invokeStatic(ENCODE_METHOD_NAME, byteArrayType, new TypeDesc[] {userStorableType}); + LocalVariable encodedVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(encodedVar); + + b.loadLocal(outVar); + b.loadLocal(encodedVar); + b.arrayLength(); + b.invokeStatic(storableSerializerType, "writeInt", null, + new TypeDesc[] {outputStreamType, TypeDesc.INT}); + + b.loadLocal(outVar); + b.loadLocal(encodedVar); + b.invokeVirtual(outputStreamType, "write", null, new TypeDesc[] {byteArrayType}); + b.returnVoid(); + } + + // Build read method for DataInput. + { + TypeDesc dataInputType = TypeDesc.forClass(DataInput.class); + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, READ_METHOD_NAME, storableType, + new TypeDesc[] {storageType, dataInputType}); + + CodeBuilder b = new CodeBuilder(mi); + LocalVariable storageVar = b.getParameter(0); + LocalVariable dinVar = b.getParameter(1); + + b.loadLocal(dinVar); + b.invokeInterface(dataInputType, "readInt", TypeDesc.INT, null); + b.newObject(byteArrayType); + LocalVariable byteArrayVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(byteArrayVar); + + b.loadLocal(dinVar); + b.loadLocal(byteArrayVar); + b.invokeInterface(dataInputType, "readFully", null, new TypeDesc[] {byteArrayType}); + + b.loadLocal(storageVar); + b.loadLocal(byteArrayVar); + b.invokeStatic(DECODE_METHOD_NAME, userStorableType, + new TypeDesc[] {storageType, byteArrayType}); + b.returnValue(storableType); + } + + // Build read method for InputStream. + { + TypeDesc inputStreamType = TypeDesc.forClass(InputStream.class); + + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC, READ_METHOD_NAME, storableType, + new TypeDesc[] {storageType, inputStreamType}); + + CodeBuilder b = new CodeBuilder(mi); + LocalVariable storageVar = b.getParameter(0); + LocalVariable inVar = b.getParameter(1); + + b.loadLocal(inVar); + b.invokeStatic(storableSerializerType, "readInt", TypeDesc.INT, + new TypeDesc[] {inputStreamType}); + b.newObject(byteArrayType); + LocalVariable byteArrayVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(byteArrayVar); + + b.loadLocal(inVar); + b.loadLocal(byteArrayVar); + b.invokeStatic(storableSerializerType, "readFully", null, + new TypeDesc[] {inputStreamType, byteArrayType}); + + b.loadLocal(storageVar); + b.loadLocal(byteArrayVar); + b.invokeStatic(DECODE_METHOD_NAME, userStorableType, + new TypeDesc[] {storageType, byteArrayType}); + b.returnValue(storableType); + } + + Class clazz = (Class) ci.defineClass(cf); + + try { + return clazz.newInstance(); + } catch (InstantiationException e) { + throw new UndeclaredThrowableException(e); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } + } + + protected StorableSerializer() { + } + + public abstract void write(S storable, DataOutput out) throws IOException; + + public abstract void write(S storable, OutputStream out) throws IOException; + + public abstract S read(Storage storage, DataInput in) throws IOException, EOFException; + + public abstract S read(Storage storage, InputStream in) throws IOException, EOFException; + + public static void writeInt(OutputStream out, int v) throws IOException { + out.write((v >>> 24) & 0xff); + out.write((v >>> 16) & 0xff); + out.write((v >>> 8) & 0xff); + out.write(v & 0xff); + } + + public static int readInt(InputStream in) throws IOException { + int a = in.read(); + int b = in.read(); + int c = in.read(); + int d = in.read(); + if ((a | b | c | d) < 0) { + throw new EOFException(); + } + return (a << 24) | (b << 16) | (c << 8) | d; + } + + public static void readFully(InputStream in, byte[] b) throws IOException { + int length = b.length; + int n = 0; + while (n < length) { + int count = in.read(b, n, length - n); + if (count < 0) { + throw new EOFException(); + } + n += count; + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/StorableSupport.java b/src/main/java/com/amazon/carbonado/spi/StorableSupport.java new file mode 100644 index 0000000..bf2609c --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/StorableSupport.java @@ -0,0 +1,41 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.carbonado.spi; + +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.Storable; + +/** + * + * + * @author Brian S O'Neill + */ +public interface StorableSupport { + /** + * Returns the root parent Repository that the Storable came from. + */ + Repository getRootRepository(); + + /** + * Returns true if the given property exists and is supported. + * + * @param propertyName name of property to check + */ + boolean isPropertySupported(String propertyName); +} diff --git a/src/main/java/com/amazon/carbonado/spi/StoredLob.java b/src/main/java/com/amazon/carbonado/spi/StoredLob.java new file mode 100644 index 0000000..e081bb3 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/StoredLob.java @@ -0,0 +1,96 @@ +/* + * 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.spi; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Join; +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.constraint.IntegerConstraint; + +/** + * Can be used internally by repositories for supporting Lobs. + * + * @author Brian S O'Neill + * @see LobEngine + */ +@PrimaryKey("locator") +public abstract class StoredLob implements Storable { + @Sequence("com.amazon.carbonado.spi.StoredLob") + public abstract long getLocator(); + public abstract void setLocator(long locator); + + public abstract int getBlockSize(); + @IntegerConstraint(min=1) + public abstract void setBlockSize(int size); + + public abstract long getLength(); + @IntegerConstraint(min=0) + public abstract void setLength(long length); + + @Version + public abstract int getVersion(); + public abstract void setVersion(int version); + + @Join + public abstract Query getBlocks() throws FetchException; + + /** + * Returns number of blocks required to store Lob. + */ + public long getBlockCount() { + int blockSize = getBlockSize(); + return (getLength() + (blockSize - 1)) / blockSize; + } + + /** + * Returns expected length of last block. If zero, last block should be + * full, unless the total length of Lob is zero. + */ + public int getLastBlockLength() { + return (int) (getLength() % getBlockSize()); + } + + /** + * Blocks stored here. + */ + @PrimaryKey({"locator", "+blockNumber"}) + public static abstract class Block implements Storable { + public abstract long getLocator(); + public abstract void setLocator(long locator); + + /** + * First block number is logically zero, but subtract 0x80000000 to get + * actual number. This effectively makes the block number unsigned. + */ + public abstract int getBlockNumber(); + public abstract void setBlockNumber(int number); + + public abstract byte[] getData(); + public abstract void setData(byte[] data); + + @Version + public abstract int getVersion(); + public abstract void setVersion(int version); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/StoredSequence.java b/src/main/java/com/amazon/carbonado/spi/StoredSequence.java new file mode 100644 index 0000000..bcd0a3c --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/StoredSequence.java @@ -0,0 +1,49 @@ +/* + * 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.spi; + +import com.amazon.carbonado.Alias; +import com.amazon.carbonado.PrimaryKey; +import com.amazon.carbonado.Storable; + +/** + * Stores data for {@link SequenceValueGenerator}. + * + * @author Brian S O'Neill + */ +@PrimaryKey("name") +@Alias("CARBONADO_SEQUENCE") +public interface StoredSequence extends Storable { + String getName(); + void setName(String name); + + /** + * Returns the initial value for the sequence. + */ + long getInitialValue(); + void setInitialValue(long value); + + /** + * Returns the pre-adjusted next value of the sequence. This value is + * initially Long.MIN_VALUE, and it increments up to Long.MAX_VALUE. The actual + * next value for the sequence is: (getNextValue() + Long.MIN_VALUE + getInitialValue()). + */ + long getNextValue(); + void setNextValue(long value); +} diff --git a/src/main/java/com/amazon/carbonado/spi/TransactionManager.java b/src/main/java/com/amazon/carbonado/spi/TransactionManager.java new file mode 100644 index 0000000..144b411 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/TransactionManager.java @@ -0,0 +1,642 @@ +/* + * 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.spi; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.amazon.carbonado.Cursor; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.IsolationLevel; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Transaction; + +/** + * Generic transaction manager for repositories. Repositories should only have + * thread local instances. + * + * @author Brian S O'Neill + */ +public abstract class TransactionManager { + + final Lock mLock; + final ExceptionTransformer mExTransformer; + + TransactionImpl mCurrent; + + // Tracks all registered cursors by storage type. + private Map, CursorList>> mCursors; + + private boolean mClosed; + + public TransactionManager(ExceptionTransformer exTransformer) { + // The use of a fair lock is essential for shutdown hooks that attempt + // to acquire the locks of all TransactionManagers. Otherwise, the + // shutdown can take a long time. + mLock = new ReentrantLock(true); + mExTransformer = exTransformer; + } + + /** + * Returns the exception transformer in use. + */ + public ExceptionTransformer getExceptionTransformer() { + return mExTransformer; + } + + /** + * Enters a new transaction scope. + * + * @param level desired isolation level (may be null) + * @throws UnsupportedOperationException if isolation level higher than + * supported by repository + */ + public Transaction enter(IsolationLevel level) { + mLock.lock(); + try { + TransactionImpl parent = mCurrent; + IsolationLevel actualLevel = selectIsolationLevel(parent, level); + if (actualLevel == null) { + if (parent == null) { + throw new UnsupportedOperationException + ("Desired isolation level not supported: " + level); + } else { + throw new UnsupportedOperationException + ("Desired isolation level not supported: " + level + + "; parent isolation level: " + parent.getIsolationLevel()); + } + } + + return mCurrent = new TransactionImpl(this, parent, false, actualLevel); + } finally { + mLock.unlock(); + } + } + + /** + * Enters a new top-level transaction scope. + * + * @param level desired isolation level (may be null) + * @throws UnsupportedOperationException if isolation level higher than + * supported by repository + */ + public Transaction enterTop(IsolationLevel level) { + mLock.lock(); + try { + IsolationLevel actualLevel = selectIsolationLevel(null, level); + if (actualLevel == null) { + throw new UnsupportedOperationException + ("Desired isolation level not supported: " + level); + } + + return mCurrent = new TransactionImpl(this, mCurrent, true, actualLevel); + } finally { + mLock.unlock(); + } + } + + /** + * Registers the given cursor against the current transaction, allowing + * it to be closed on transaction exit or transaction manager close. If + * there is no current transaction scope, the cursor is registered as not + * part of a transaction. Cursors should register when created. + */ + public void register(Class type, Cursor cursor) { + mLock.lock(); + try { + checkState(); + if (mCursors == null) { + mCursors = new IdentityHashMap, CursorList>>(); + } + + CursorList> cursorList = mCursors.get(type); + if (cursorList == null) { + cursorList = new CursorList>(); + mCursors.put(type, cursorList); + } + + cursorList.register(cursor, mCurrent); + + if (mCurrent != null) { + mCurrent.register(cursor); + } + } finally { + mLock.unlock(); + } + } + + /** + * Unregisters a previously registered cursor. Cursors should unregister + * when closed. + */ + public void unregister(Class type, Cursor cursor) { + mLock.lock(); + try { + if (mCursors != null) { + CursorList> cursorList = mCursors.get(type); + if (cursorList != null) { + TransactionImpl txnImpl = cursorList.unregister(cursor); + if (txnImpl != null) { + txnImpl.unregister(cursor); + } + } + } + } finally { + mLock.unlock(); + } + } + + /** + * Returns the count of registered cursors of a specific type. + */ + public int getRegisteredCount(Class type) { + mLock.lock(); + try { + if (mCursors != null) { + CursorList> cursorList = mCursors.get(type); + if (cursorList != null) { + return cursorList.size(); + } + } + } finally { + mLock.unlock(); + } + return 0; + } + + /** + * Returns a registered cursor of the given type, or null if none at given index. + */ + @SuppressWarnings("unchecked") + public Cursor getRegisteredCursor(Class type, int index) { + mLock.lock(); + try { + if (mCursors != null) { + CursorList> cursorList = mCursors.get(type); + if (cursorList != null) { + if (index < cursorList.size()) { + return (Cursor) cursorList.getCursor(index); + } + } + } + } finally { + mLock.unlock(); + } + return null; + } + + /** + * Returns lock used by TransactionManager. While holding lock, operations + * are suspended. + */ + public Lock getLock() { + return mLock; + } + + /** + * Exits all transactions and closes all cursors. Should be called only + * when repository is closed. + */ + public void close() throws RepositoryException { + mLock.lock(); + try { + if (!mClosed) { + while (mCurrent != null) { + mCurrent.exit(); + } + if (mCursors != null) { + for (CursorList> cursorList : mCursors.values()) { + cursorList.closeCursors(); + } + } + } + } finally { + mClosed = true; + mLock.unlock(); + } + } + + /** + * Returns null if no transaction is in progress. + * + * @throws Exception thrown by createTxn + */ + public Txn getTxn() throws Exception { + mLock.lock(); + try { + checkState(); + return mCurrent == null ? null : mCurrent.getTxn(); + } finally { + mLock.unlock(); + } + } + + /** + * Returns true if a transaction is in progress and it is for update. + */ + public boolean isForUpdate() { + mLock.lock(); + try { + return (mClosed || mCurrent == null) ? false : mCurrent.isForUpdate(); + } finally { + mLock.unlock(); + } + } + + /** + * Returns the isolation level of the current transaction, or null if there + * is no transaction in the current thread. + */ + public IsolationLevel getIsolationLevel() { + mLock.lock(); + try { + return (mClosed || mCurrent == null) ? null : mCurrent.getIsolationLevel(); + } finally { + mLock.unlock(); + } + } + + /** + * Caller must hold mLock. + */ + private void checkState() { + if (mClosed) { + throw new IllegalStateException("Repository is closed"); + } + } + + /** + * Returns supported isolation level, which may be higher. If isolation + * level cannot go higher (or lower than parent) then return null. + * + * @param parent optional parent transaction + * @param level desired isolation level (may be null) + */ + protected abstract IsolationLevel selectIsolationLevel(Transaction parent, + IsolationLevel level); + + /** + * Creates an internal transaction representation, with the optional parent + * transaction. If parent is not null and real nested transactions are not + * supported, simply return parent transaction for supporting fake nested + * transactions. + * + * @param parent optional parent transaction + * @param level required isolation level + */ + protected abstract Txn createTxn(Txn parent, IsolationLevel level) throws Exception; + + /** + * Creates an internal transaction representation, with the optional parent + * transaction. If parent is not null and real nested transactions are not + * supported, simply return parent transaction for supporting fake nested + * transactions. + * + *

The default implementation of this method just calls the regular + * createTxn method, ignoring the timeout parameter. + * + * @param parent optional parent transaction + * @param level required isolation level + * @param timeout desired timeout for lock acquisition, never negative + * @param unit timeout unit, never null + */ + protected Txn createTxn(Txn parent, IsolationLevel level, + int timeout, TimeUnit unit) + throws Exception + { + return createTxn(parent, level); + } + + /** + * Commits and closes the given internal transaction. + * + * @return true if transaction object is still valid + */ + protected abstract boolean commitTxn(Txn txn) throws Exception; + + /** + * Aborts and closes the given internal transaction. + */ + protected abstract void abortTxn(Txn txn) throws Exception; + + private static class TransactionImpl implements Transaction { + private final TransactionManager mTxnMgr; + private final TransactionImpl mParent; + private final boolean mTop; + private final IsolationLevel mLevel; + + private boolean mForUpdate; + private int mDesiredLockTimeout; + private TimeUnit mTimeoutUnit; + + private TransactionImpl mChild; + private boolean mExited; + private Txn mTxn; + + // Tracks all registered cursors. + private CursorList mCursorList; + + TransactionImpl(TransactionManager txnMgr, + TransactionImpl parent, + boolean top, + IsolationLevel level) { + mTxnMgr = txnMgr; + mParent = parent; + mTop = top; + mLevel = level; + if (!top && parent != null) { + parent.mChild = this; + mDesiredLockTimeout = parent.mDesiredLockTimeout; + mTimeoutUnit = parent.mTimeoutUnit; + } + } + + public void commit() throws PersistException { + TransactionManager txnMgr = mTxnMgr; + txnMgr.mLock.lock(); + try { + if (!mExited) { + if (mChild != null) { + mChild.commit(); + } + + closeCursors(); + + if (mTxn != null) { + if (mParent == null || mParent.mTxn != mTxn) { + try { + if (!txnMgr.commitTxn(mTxn)) { + mTxn = null; + } + } catch (Throwable e) { + mTxn = null; + throw txnMgr.mExTransformer.toPersistException(e); + } + } else { + // Indicate fake nested transaction committed. + mTxn = null; + } + } + } + } finally { + txnMgr.mLock.unlock(); + } + } + + public void exit() throws PersistException { + TransactionManager txnMgr = mTxnMgr; + txnMgr.mLock.lock(); + try { + if (!mExited) { + if (mChild != null) { + mChild.exit(); + } + + closeCursors(); + + if (mTxn != null) { + try { + if (mParent == null || mParent.mTxn != mTxn) { + try { + txnMgr.abortTxn(mTxn); + } catch (Throwable e) { + throw txnMgr.mExTransformer.toPersistException(e); + } + } + } finally { + mTxn = null; + } + } + + txnMgr.mCurrent = mParent; + + mExited = true; + } + } finally { + txnMgr.mLock.unlock(); + } + } + + public void setForUpdate(boolean forUpdate) { + mForUpdate = forUpdate; + } + + public boolean isForUpdate() { + return mForUpdate; + } + + public void setDesiredLockTimeout(int timeout, TimeUnit unit) { + if (timeout < 0) { + mDesiredLockTimeout = 0; + mTimeoutUnit = null; + } else { + mDesiredLockTimeout = timeout; + mTimeoutUnit = unit; + } + } + + public IsolationLevel getIsolationLevel() { + return mLevel; + } + + void register(Cursor cursor) { + if (mCursorList == null) { + mCursorList = new CursorList(); + } + mCursorList.register(cursor, null); + } + + void unregister(Cursor cursor) { + if (mCursorList != null) { + mCursorList.unregister(cursor); + } + } + + Txn getTxn() throws Exception { + TransactionManager txnMgr = mTxnMgr; + txnMgr.mLock.lock(); + try { + if (mTxn == null) { + Txn parent = (mParent == null || mTop) ? null : mParent.getTxn(); + if (mTimeoutUnit == null) { + mTxn = txnMgr.createTxn(parent, mLevel); + } else { + mTxn = txnMgr.createTxn(parent, mLevel, mDesiredLockTimeout, mTimeoutUnit); + } + } + return mTxn; + } finally { + txnMgr.mLock.unlock(); + } + } + + private void closeCursors() throws PersistException { + if (mCursorList != null) { + mCursorList.closeCursors(); + } + } + } + + /** + * Simple fast list/map for holding a small amount of cursors. + */ + static class CursorList { + private int mSize; + private Cursor[] mCursors; + private V[] mValues; + + CursorList() { + mCursors = new Cursor[8]; + } + + /** + * @param value optional value to associate + */ + @SuppressWarnings("unchecked") + void register(Cursor cursor, V value) { + int size = mSize; + Cursor[] cursors = mCursors; + + if (size == cursors.length) { + int newLength = size << 1; + + Cursor[] newCursors = new Cursor[newLength]; + System.arraycopy(cursors, 0, newCursors, 0, size); + mCursors = cursors = newCursors; + + if (mValues != null) { + V[] newValues = (V[]) new Object[newLength]; + System.arraycopy(mValues, 0, newValues, 0, size); + mValues = newValues; + } + } + + cursors[size] = cursor; + + if (value != null) { + V[] values = mValues; + if (values == null) { + mValues = values = (V[]) new Object[cursors.length]; + } + values[size] = value; + } + + mSize = size + 1; + } + + V unregister(Cursor cursor) { + // Assuming that cursors are opened and closed in LIFO order + // (stack order), search backwards to optimize. + Cursor[] cursors = mCursors; + int size = mSize; + int i = size; + search: { + while (--i >= 0) { + if (cursors[i] == cursor) { + break search; + } + } + // Not found. + return null; + } + + V[] values = mValues; + V value; + + if (values == null) { + value = null; + if (i == size - 1) { + // Clear reference so that it can be garbage collected. + cursors[i] = null; + } else { + // Shift array elements down. + System.arraycopy(cursors, i + 1, cursors, i, size - i - 1); + } + } else { + value = values[i]; + if (i == size - 1) { + // Clear references so that they can be garbage collected. + cursors[i] = null; + values[i] = null; + } else { + // Shift array elements down. + System.arraycopy(cursors, i + 1, cursors, i, size - i - 1); + System.arraycopy(values, i + 1, values, i, size - i - 1); + } + } + + mSize = size - 1; + return value; + } + + int size() { + return mSize; + } + + Cursor getCursor(int index) { + return mCursors[index]; + } + + V getValue(int index) { + V[] values = mValues; + return values == null ? null : values[index]; + } + + /** + * Closes all cursors and resets the size of this list to 0. + */ + void closeCursors() throws PersistException { + // Note: Iteration must be in reverse order. Calling close on the + // cursor should cause it to unregister from this list. This will + // cause only a modification to the end of the list, which is no + // longer needed by this method. + try { + Cursor[] cursors = mCursors; + V[] values = mValues; + int i = mSize; + if (values == null) { + while (--i >= 0) { + Cursor cursor = cursors[i]; + if (cursor != null) { + cursor.close(); + cursors[i] = null; + } + } + } else { + while (--i >= 0) { + Cursor cursor = cursors[i]; + if (cursor != null) { + cursor.close(); + cursors[i] = null; + values[i] = null; + } + } + } + } catch (FetchException e) { + throw e.toPersistException(); + } + mSize = 0; + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/TransactionPair.java b/src/main/java/com/amazon/carbonado/spi/TransactionPair.java new file mode 100644 index 0000000..d97aef4 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/TransactionPair.java @@ -0,0 +1,89 @@ +/* + * 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.spi; + +import java.util.concurrent.TimeUnit; + +import com.amazon.carbonado.IsolationLevel; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Transaction; + +/** + * Pairs two transaction together into one. The transaction cannot be atomic, + * however. Inconsistencies can result if the primary transaction succeeds in + * committing, but the secondary fails. Therefore, the designated primary + * transaction should be the one that is more likely to fail. For example, the + * primary transaction might rely on the network, but the secondary operates + * locally. + * + * @author Don Schneider + * @author Brian S O'Neill + */ +public class TransactionPair implements Transaction { + private final Transaction mPrimaryTransaction; + private final Transaction mSecondaryTransaction; + + /** + * @param primaryTransaction is committed first, exited last + * @param secondaryTransaction is exited first, commited last + */ + public TransactionPair(Transaction primaryTransaction, Transaction secondaryTransaction) { + mPrimaryTransaction = primaryTransaction; + mSecondaryTransaction = secondaryTransaction; + } + + public void commit() throws PersistException { + mPrimaryTransaction.commit(); + try { + mSecondaryTransaction.commit(); + } catch (Exception e) { + throw new PersistException + ("Failure to commit secondary transaction has likely caused an inconsistency", e); + } + } + + public void exit() throws PersistException { + try { + mSecondaryTransaction.exit(); + } finally { + // Do this second so if there is an exception, the user sees the + // primary exception, which is presumably more important. + mPrimaryTransaction.exit(); + } + } + + public void setForUpdate(boolean forUpdate) { + mPrimaryTransaction.setForUpdate(forUpdate); + mSecondaryTransaction.setForUpdate(forUpdate); + } + + public boolean isForUpdate() { + return mPrimaryTransaction.isForUpdate() && mSecondaryTransaction.isForUpdate(); + } + + public void setDesiredLockTimeout(int timeout, TimeUnit unit) { + mPrimaryTransaction.setDesiredLockTimeout(timeout, unit); + mSecondaryTransaction.setDesiredLockTimeout(timeout, unit); + } + + public IsolationLevel getIsolationLevel() { + return mPrimaryTransaction.getIsolationLevel() + .lowestCommon(mSecondaryTransaction.getIsolationLevel()); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/TriggerManager.java b/src/main/java/com/amazon/carbonado/spi/TriggerManager.java new file mode 100644 index 0000000..4382429 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/TriggerManager.java @@ -0,0 +1,691 @@ +/* + * 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.spi; + +import java.lang.reflect.Method; + +import java.util.ArrayList; +import java.util.Arrays; + +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Trigger; + +/** + * Used by Storage implementations to manage triggers and consolidate them into + * single logical triggers. This class is thread-safe and ensures that changes + * to the trigger set do not affect transactions in progress. + * + * @author Brian S O'Neill + */ +public class TriggerManager { + // Bit masks returned by selectTypes. + private static final int FOR_INSERT = 1; + private static final int FOR_UPDATE = 2; + private static final int FOR_DELETE = 4; + + private static final Method + BEFORE_INSERT_METHOD, + BEFORE_TRY_INSERT_METHOD, + AFTER_INSERT_METHOD, + AFTER_TRY_INSERT_METHOD, + FAILED_INSERT_METHOD, + + BEFORE_UPDATE_METHOD, + BEFORE_TRY_UPDATE_METHOD, + AFTER_UPDATE_METHOD, + AFTER_TRY_UPDATE_METHOD, + FAILED_UPDATE_METHOD, + + BEFORE_DELETE_METHOD, + BEFORE_TRY_DELETE_METHOD, + AFTER_DELETE_METHOD, + AFTER_TRY_DELETE_METHOD, + FAILED_DELETE_METHOD; + + static { + Class triggerClass = Trigger.class; + Class[] ONE_PARAM = {Object.class}; + Class[] TWO_PARAMS = {Object.class, Object.class}; + + try { + BEFORE_INSERT_METHOD = triggerClass.getMethod("beforeInsert", ONE_PARAM); + BEFORE_TRY_INSERT_METHOD = triggerClass.getMethod("beforeTryInsert", ONE_PARAM); + AFTER_INSERT_METHOD = triggerClass.getMethod("afterInsert", TWO_PARAMS); + AFTER_TRY_INSERT_METHOD = triggerClass.getMethod("afterTryInsert", TWO_PARAMS); + FAILED_INSERT_METHOD = triggerClass.getMethod("failedInsert", TWO_PARAMS); + + BEFORE_UPDATE_METHOD = triggerClass.getMethod("beforeUpdate", ONE_PARAM); + BEFORE_TRY_UPDATE_METHOD = triggerClass.getMethod("beforeTryUpdate", ONE_PARAM); + AFTER_UPDATE_METHOD = triggerClass.getMethod("afterUpdate", TWO_PARAMS); + AFTER_TRY_UPDATE_METHOD = triggerClass.getMethod("afterTryUpdate", TWO_PARAMS); + FAILED_UPDATE_METHOD = triggerClass.getMethod("failedUpdate", TWO_PARAMS); + + BEFORE_DELETE_METHOD = triggerClass.getMethod("beforeDelete", ONE_PARAM); + BEFORE_TRY_DELETE_METHOD = triggerClass.getMethod("beforeTryDelete", ONE_PARAM); + AFTER_DELETE_METHOD = triggerClass.getMethod("afterDelete", TWO_PARAMS); + AFTER_TRY_DELETE_METHOD = triggerClass.getMethod("afterTryDelete", TWO_PARAMS); + FAILED_DELETE_METHOD = triggerClass.getMethod("failedDelete", TWO_PARAMS); + } catch (NoSuchMethodException e) { + Error error = new NoSuchMethodError(); + error.initCause(e); + throw error; + } + } + + private volatile ForInsert mForInsert; + private volatile ForUpdate mForUpdate; + private volatile ForDelete mForDelete; + + public TriggerManager() { + } + + /** + * Returns consolidated trigger to call for insert operations, or null if + * none. + */ + public Trigger getInsertTrigger() { + return mForInsert; + } + + /** + * Returns consolidated trigger to call for update operations, or null if + * none. + */ + public Trigger getUpdateTrigger() { + return mForUpdate; + } + + /** + * Returns consolidated trigger to call for delete operations, or null if + * none. + */ + public Trigger getDeleteTrigger() { + return mForDelete; + } + + public synchronized boolean addTrigger(Trigger trigger) { + if (trigger == null) { + throw new IllegalArgumentException(); + } + + int types = selectTypes(trigger); + if (types == 0) { + return false; + } + + boolean retValue = false; + + if ((types & FOR_INSERT) != 0) { + if (mForInsert == null) { + mForInsert = new ForInsert(); + } + retValue |= mForInsert.add(trigger); + } + + if ((types & FOR_UPDATE) != 0) { + if (mForUpdate == null) { + mForUpdate = new ForUpdate(); + } + retValue |= mForUpdate.add(trigger); + } + + if ((types & FOR_DELETE) != 0) { + if (mForDelete == null) { + mForDelete = new ForDelete(); + } + retValue |= mForDelete.add(trigger); + } + + return retValue; + } + + public synchronized boolean removeTrigger(Trigger trigger) { + if (trigger == null) { + throw new IllegalArgumentException(); + } + + int types = selectTypes(trigger); + if (types == 0) { + return false; + } + + boolean retValue = false; + + if ((types & FOR_INSERT) != 0) { + if (mForInsert != null && mForInsert.remove(trigger)) { + retValue = true; + if (mForInsert.isEmpty()) { + mForInsert = null; + } + } + } + + if ((types & FOR_UPDATE) != 0) { + if (mForUpdate != null && mForUpdate.remove(trigger)) { + retValue = true; + if (mForUpdate.isEmpty()) { + mForUpdate = null; + } + } + } + + if ((types & FOR_DELETE) != 0) { + if (mForDelete != null && mForDelete.remove(trigger)) { + retValue = true; + if (mForDelete.isEmpty()) { + mForDelete = null; + } + } + } + + return retValue; + } + + /** + * Determines which operations the given trigger overrides. + */ + private int selectTypes(Trigger trigger) { + Class triggerClass = trigger.getClass(); + + int types = 0; + + if (overridesMethod(triggerClass, BEFORE_INSERT_METHOD) || + overridesMethod(triggerClass, AFTER_INSERT_METHOD) || + overridesMethod(triggerClass, FAILED_INSERT_METHOD)) + { + types |= FOR_INSERT; + } + + if (overridesMethod(triggerClass, BEFORE_UPDATE_METHOD) || + overridesMethod(triggerClass, AFTER_UPDATE_METHOD) || + overridesMethod(triggerClass, FAILED_UPDATE_METHOD)) + { + types |= FOR_UPDATE; + } + + if (overridesMethod(triggerClass, BEFORE_DELETE_METHOD) || + overridesMethod(triggerClass, AFTER_DELETE_METHOD) || + overridesMethod(triggerClass, FAILED_DELETE_METHOD)) + { + types |= FOR_DELETE; + } + + return types; + } + + private boolean overridesMethod(Class triggerClass, Method method) { + try { + return !method.equals(triggerClass.getMethod(method.getName(), + method.getParameterTypes())); + } catch (NoSuchMethodException e) { + return false; + } + } + + private static class TriggerStates { + final Trigger[] mTriggers; + final Object[] mStates; + + TriggerStates(Trigger[] triggers) { + mTriggers = triggers; + mStates = new Object[triggers.length]; + } + } + + private static abstract class ForSomething extends Trigger { + private static Trigger[] NO_TRIGGERS = new Trigger[0]; + + protected volatile Trigger[] mTriggers; + + ForSomething() { + mTriggers = NO_TRIGGERS; + } + + boolean add(Trigger trigger) { + ArrayList> list = + new ArrayList>(Arrays.asList(mTriggers)); + if (list.contains(trigger)) { + return false; + } + list.add(trigger); + mTriggers = list.toArray(new Trigger[list.size()]); + return true; + } + + boolean remove(Trigger trigger) { + ArrayList> list = + new ArrayList>(Arrays.asList(mTriggers)); + if (!list.remove(trigger)) { + return false; + } + mTriggers = list.toArray(new Trigger[list.size()]); + return true; + } + + boolean isEmpty() { + return mTriggers.length == 0; + } + } + + private static class ForInsert extends ForSomething { + @Override + public Object beforeInsert(S storable) throws PersistException { + TriggerStates triggerStates = null; + Trigger[] triggers = mTriggers; + + for (int i=triggers.length; --i>=0; ) { + Object state = triggers[i].beforeInsert(storable); + if (state != null) { + if (triggerStates == null) { + triggerStates = new TriggerStates(triggers); + } + triggerStates.mStates[i] = state; + } + } + + return triggerStates == null ? triggers : triggerStates; + } + + @Override + public Object beforeTryInsert(S storable) throws PersistException { + TriggerStates triggerStates = null; + Trigger[] triggers = mTriggers; + + for (int i=triggers.length; --i>=0; ) { + Object state = triggers[i].beforeTryInsert(storable); + if (state != null) { + if (triggerStates == null) { + triggerStates = new TriggerStates(triggers); + } + triggerStates.mStates[i] = state; + } + } + + return triggerStates == null ? triggers : triggerStates; + } + + @Override + public void afterInsert(S storable, Object state) throws PersistException { + TriggerStates triggerStates; + Trigger[] triggers; + + if (state == null) { + triggerStates = null; + triggers = mTriggers; + } else if (state instanceof TriggerStates) { + triggerStates = (TriggerStates) state; + triggers = triggerStates.mTriggers; + } else { + triggerStates = null; + triggers = (Trigger[]) state; + } + + int length = triggers.length; + + if (triggerStates == null) { + for (int i=0; i triggerStates; + Trigger[] triggers; + + if (state == null) { + triggerStates = null; + triggers = mTriggers; + } else if (state instanceof TriggerStates) { + triggerStates = (TriggerStates) state; + triggers = triggerStates.mTriggers; + } else { + triggerStates = null; + triggers = (Trigger[]) state; + } + + int length = triggers.length; + + if (triggerStates == null) { + for (int i=0; i triggerStates; + Trigger[] triggers; + + if (state == null) { + triggerStates = null; + triggers = mTriggers; + } else if (state instanceof TriggerStates) { + triggerStates = (TriggerStates) state; + triggers = triggerStates.mTriggers; + } else { + triggerStates = null; + triggers = (Trigger[]) state; + } + + int length = triggers.length; + + if (triggerStates == null) { + for (int i=0; i extends ForSomething { + @Override + public Object beforeUpdate(S storable) throws PersistException { + TriggerStates triggerStates = null; + Trigger[] triggers = mTriggers; + + for (int i=triggers.length; --i>=0; ) { + Object state = triggers[i].beforeUpdate(storable); + if (state != null) { + if (triggerStates == null) { + triggerStates = new TriggerStates(triggers); + } + triggerStates.mStates[i] = state; + } + } + + return triggerStates == null ? triggers : triggerStates; + } + + @Override + public Object beforeTryUpdate(S storable) throws PersistException { + TriggerStates triggerStates = null; + Trigger[] triggers = mTriggers; + + for (int i=triggers.length; --i>=0; ) { + Object state = triggers[i].beforeTryUpdate(storable); + if (state != null) { + if (triggerStates == null) { + triggerStates = new TriggerStates(triggers); + } + triggerStates.mStates[i] = state; + } + } + + return triggerStates == null ? triggers : triggerStates; + } + + @Override + public void afterUpdate(S storable, Object state) throws PersistException { + TriggerStates triggerStates; + Trigger[] triggers; + + if (state == null) { + triggerStates = null; + triggers = mTriggers; + } else if (state instanceof TriggerStates) { + triggerStates = (TriggerStates) state; + triggers = triggerStates.mTriggers; + } else { + triggerStates = null; + triggers = (Trigger[]) state; + } + + int length = triggers.length; + + if (triggerStates == null) { + for (int i=0; i triggerStates; + Trigger[] triggers; + + if (state == null) { + triggerStates = null; + triggers = mTriggers; + } else if (state instanceof TriggerStates) { + triggerStates = (TriggerStates) state; + triggers = triggerStates.mTriggers; + } else { + triggerStates = null; + triggers = (Trigger[]) state; + } + + int length = triggers.length; + + if (triggerStates == null) { + for (int i=0; i triggerStates; + Trigger[] triggers; + + if (state == null) { + triggerStates = null; + triggers = mTriggers; + } else if (state instanceof TriggerStates) { + triggerStates = (TriggerStates) state; + triggers = triggerStates.mTriggers; + } else { + triggerStates = null; + triggers = (Trigger[]) state; + } + + int length = triggers.length; + + if (triggerStates == null) { + for (int i=0; i extends ForSomething { + @Override + public Object beforeDelete(S storable) throws PersistException { + TriggerStates triggerStates = null; + Trigger[] triggers = mTriggers; + + for (int i=triggers.length; --i>=0; ) { + Object state = triggers[i].beforeDelete(storable); + if (state != null) { + if (triggerStates == null) { + triggerStates = new TriggerStates(triggers); + } + triggerStates.mStates[i] = state; + } + } + + return triggerStates == null ? triggers : triggerStates; + } + + @Override + public Object beforeTryDelete(S storable) throws PersistException { + TriggerStates triggerStates = null; + Trigger[] triggers = mTriggers; + + for (int i=triggers.length; --i>=0; ) { + Object state = triggers[i].beforeTryDelete(storable); + if (state != null) { + if (triggerStates == null) { + triggerStates = new TriggerStates(triggers); + } + triggerStates.mStates[i] = state; + } + } + + return triggerStates == null ? triggers : triggerStates; + } + + @Override + public void afterDelete(S storable, Object state) throws PersistException { + TriggerStates triggerStates; + Trigger[] triggers; + + if (state == null) { + triggerStates = null; + triggers = mTriggers; + } else if (state instanceof TriggerStates) { + triggerStates = (TriggerStates) state; + triggers = triggerStates.mTriggers; + } else { + triggerStates = null; + triggers = (Trigger[]) state; + } + + int length = triggers.length; + + if (triggerStates == null) { + for (int i=0; i triggerStates; + Trigger[] triggers; + + if (state == null) { + triggerStates = null; + triggers = mTriggers; + } else if (state instanceof TriggerStates) { + triggerStates = (TriggerStates) state; + triggers = triggerStates.mTriggers; + } else { + triggerStates = null; + triggers = (Trigger[]) state; + } + + int length = triggers.length; + + if (triggerStates == null) { + for (int i=0; i triggerStates; + Trigger[] triggers; + + if (state == null) { + triggerStates = null; + triggers = mTriggers; + } else if (state instanceof TriggerStates) { + triggerStates = (TriggerStates) state; + triggers = triggerStates.mTriggers; + } else { + triggerStates = null; + triggers = (Trigger[]) state; + } + + int length = triggers.length; + + if (triggerStates == null) { + for (int i=0; i extends StorableSupport { + /** + * Returns a trigger which must be run for all insert operations. + * + * @return null if no trigger + */ + Trigger getInsertTrigger(); + + /** + * Returns a trigger which must be run for all update operations. + * + * @return null if no trigger + */ + Trigger getUpdateTrigger(); + + /** + * Returns a trigger which must be run for all delete operations. + * + * @return null if no trigger + */ + Trigger getDeleteTrigger(); +} diff --git a/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java b/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java new file mode 100644 index 0000000..ec3ae34 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/WrappedQuery.java @@ -0,0 +1,236 @@ +/* + * 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.spi; + +import java.io.IOException; + +import com.amazon.carbonado.Cursor; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Query; +import com.amazon.carbonado.Storable; + +import com.amazon.carbonado.cursor.AbstractCursor; + +import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.filter.FilterValues; + +/** + * Abstract query that wraps all returned Storables into another Storable. + * + * @author Don Schneider + * @author Brian S O'Neill + */ +public abstract class WrappedQuery implements Query { + + // The query to which this query will delegate + private final Query mQuery; + + /** + * @param query query to wrap + */ + public WrappedQuery(Query query) { + mQuery = query; + } + + public Class getStorableType() { + return mQuery.getStorableType(); + } + + public Filter getFilter() { + return mQuery.getFilter(); + } + + public FilterValues getFilterValues() { + return mQuery.getFilterValues(); + } + + public int getBlankParameterCount() { + return mQuery.getBlankParameterCount(); + } + + public Query with(int value) { + return newInstance(mQuery.with(value)); + } + + public Query with(long value) { + return newInstance(mQuery.with(value)); + } + + public Query with(float value) { + return newInstance(mQuery.with(value)); + } + + public Query with(double value) { + return newInstance(mQuery.with(value)); + } + + public Query with(boolean value) { + return newInstance(mQuery.with(value)); + } + + public Query with(char value) { + return newInstance(mQuery.with(value)); + } + + public Query with(byte value) { + return newInstance(mQuery.with(value)); + } + + public Query with(short value) { + return newInstance(mQuery.with(value)); + } + + public Query with(Object value) { + return newInstance(mQuery.with(value)); + } + + public Query withValues(Object... objects) { + return newInstance(mQuery.withValues(objects)); + } + + public Query and(String filter) throws FetchException { + return newInstance(mQuery.and(filter)); + } + + public Query and(Filter filter) throws FetchException { + return newInstance(mQuery.and(filter)); + } + + public Query or(String filter) throws FetchException { + return newInstance(mQuery.or(filter)); + } + + public Query or(Filter filter) throws FetchException { + return newInstance(mQuery.or(filter)); + } + + public Query not() throws FetchException { + return newInstance(mQuery.not()); + } + + public Query orderBy(String property) throws FetchException, UnsupportedOperationException { + return newInstance(mQuery.orderBy(property)); + } + + public Query orderBy(String... strings) + throws FetchException, UnsupportedOperationException + { + return newInstance(mQuery.orderBy(strings)); + } + + public Cursor fetch() throws FetchException { + return new WrappedCursor(mQuery.fetch()); + } + + public Cursor fetchAfter(S start) throws FetchException { + return new WrappedCursor(mQuery.fetchAfter(start)); + } + + public S loadOne() throws FetchException { + return wrap(mQuery.loadOne()); + } + + public S tryLoadOne() throws FetchException { + S one = mQuery.tryLoadOne(); + return one == null ? null : wrap(one); + } + + public void deleteOne() throws PersistException { + mQuery.tryDeleteOne(); + } + + public boolean tryDeleteOne() throws PersistException { + return mQuery.tryDeleteOne(); + } + + public void deleteAll() throws PersistException { + mQuery.deleteAll(); + } + + public long count() throws FetchException { + return mQuery.count(); + } + + public boolean printNative() { + return mQuery.printNative(); + } + + public boolean printNative(Appendable app) throws IOException { + return mQuery.printNative(app); + } + + public boolean printNative(Appendable app, int indentLevel) throws IOException { + return mQuery.printNative(app, indentLevel); + } + + public boolean printPlan() { + return mQuery.printPlan(); + } + + public boolean printPlan(Appendable app) throws IOException { + return mQuery.printPlan(app); + } + + public boolean printPlan(Appendable app, int indentLevel) throws IOException { + return mQuery.printPlan(app, indentLevel); + } + + public void appendTo(Appendable appendable) throws IOException { + appendable.append(mQuery.toString()); + } + + public String toString() { + return mQuery.toString(); + } + + protected Query getWrappedQuery() { + return mQuery; + } + + /** + * Called to wrap the given Storable. + */ + protected abstract S wrap(S storable); + + protected abstract WrappedQuery newInstance(Query query); + + private class WrappedCursor extends AbstractCursor { + private Cursor mCursor; + + public WrappedCursor(Cursor cursor) { + mCursor = cursor; + } + + public void close() throws FetchException { + mCursor.close(); + } + + public boolean hasNext() throws FetchException { + return mCursor.hasNext(); + } + + public S next() throws FetchException { + return wrap(mCursor.next()); + } + + public int skipNext(int amount) throws FetchException { + return mCursor.skipNext(amount); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/WrappedStorage.java b/src/main/java/com/amazon/carbonado/spi/WrappedStorage.java new file mode 100644 index 0000000..00fe5a7 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/WrappedStorage.java @@ -0,0 +1,228 @@ +/* + * 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.spi; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Query; +import com.amazon.carbonado.Repository; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.Trigger; + +import com.amazon.carbonado.filter.Filter; +import com.amazon.carbonado.filter.FilterValues; + +import com.amazon.carbonado.util.QuickConstructorGenerator; + +/** + * Abstract storage that wraps all returned Storables and Queries, including + * those returned from joins. Property access methods (get and set) are + * delegated directly to the wrapped storable. Other operations are delegated + * to a special {@link WrappedStorage.Support handler}. + * + * @author Brian S O'Neill + */ +public abstract class WrappedStorage implements Storage { + private final Storage mStorage; + private final WrappedStorableFactory mFactory; + final TriggerManager mTriggerManager; + + /** + * @param storage storage to wrap + */ + public WrappedStorage(Storage storage) { + mStorage = storage; + Class wrappedClass = StorableGenerator + .getWrappedClass(storage.getStorableType()); + mFactory = QuickConstructorGenerator + .getInstance(wrappedClass, WrappedStorableFactory.class); + mTriggerManager = new TriggerManager(); + } + + public Class getStorableType() { + return mStorage.getStorableType(); + } + + public S prepare() { + return wrap(mStorage.prepare()); + } + + public Query query() throws FetchException { + return wrap(mStorage.query()); + } + + public Query query(String filter) throws FetchException { + return wrap(mStorage.query(filter)); + } + + public Query query(Filter filter) throws FetchException { + return wrap(mStorage.query(filter)); + } + + public boolean addTrigger(Trigger trigger) { + return mTriggerManager.addTrigger(trigger); + } + + public boolean removeTrigger(Trigger trigger) { + return mTriggerManager.removeTrigger(trigger); + } + + /** + * Wraps the storable into one which delegates some operations to the + * storable handler. + * + * @param storable storable being wrapped + * @see #createSupport + */ + protected S wrap(S storable) { + if (storable == null) { + throw new IllegalArgumentException("Storable to wrap is null"); + } + return mFactory.newWrappedStorable(createSupport(storable), storable); + } + + /** + * Wraps the query such that all storables returned by it are wrapped as + * well. + * + * @param query query being wrapped + * @see WrappedQuery + */ + protected Query wrap(Query query) { + return new QueryWrapper(query); + } + + /** + * Create a handler used by wrapped storables. + * + * @param storable storable being wrapped + */ + protected abstract Support createSupport(S storable); + + protected Storage getWrappedStorage() { + return mStorage; + } + + /** + * Support for use with {@link WrappedStorage}. Most of the methods defined + * here are a subset of those defined in Storable. + * + * @author Brian S O'Neill + */ + public abstract class Support implements WrappedSupport { + public Trigger getInsertTrigger() { + return mTriggerManager.getInsertTrigger(); + } + + public Trigger getUpdateTrigger() { + return mTriggerManager.getUpdateTrigger(); + } + + public Trigger getDeleteTrigger() { + return mTriggerManager.getDeleteTrigger(); + } + } + + /** + * Support implementation which delegates all calls to a Storable. + */ + public class BasicSupport extends Support { + private final Repository mRepository; + private final S mStorable; + + public BasicSupport(Repository repo, S storable) { + mRepository = repo; + mStorable = storable; + } + + public Support createSupport(S storable) { + return new BasicSupport(mRepository, storable); + } + + public Repository getRepository() { + return mRepository; + } + + public boolean isPropertySupported(String propertyName) { + return mStorable.isPropertySupported(propertyName); + } + + public void load() throws FetchException { + mStorable.load(); + } + + public boolean tryLoad() throws FetchException { + return mStorable.tryLoad(); + } + + public void insert() throws PersistException { + mStorable.insert(); + } + + public boolean tryInsert() throws PersistException { + return mStorable.tryInsert(); + } + + public void update() throws PersistException { + mStorable.update(); + } + + public boolean tryUpdate() throws PersistException { + return mStorable.tryUpdate(); + } + + public void delete() throws PersistException { + mStorable.delete(); + } + + public boolean tryDelete() throws PersistException { + return mStorable.tryDelete(); + } + + protected S getWrappedStorable() { + return mStorable; + } + } + + private class QueryWrapper extends WrappedQuery { + QueryWrapper(Query query) { + super(query); + } + + protected S wrap(S storable) { + return WrappedStorage.this.wrap(storable); + } + + protected WrappedQuery newInstance(Query query) { + return new QueryWrapper(query); + } + } + + /** + * Used with QuickConstructorGenerator. + */ + public static interface WrappedStorableFactory { + /** + * @param storable storable being wrapped + * @param support handler for persistence methods + */ + S newWrappedStorable(WrappedSupport support, S storable); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/WrappedSupport.java b/src/main/java/com/amazon/carbonado/spi/WrappedSupport.java new file mode 100644 index 0000000..24e2c02 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/WrappedSupport.java @@ -0,0 +1,75 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.carbonado.spi; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; + +/** + * + * + * @author Brian S O'Neill + */ +public interface WrappedSupport extends TriggerSupport { + /** + * @see Storable#load + */ + void load() throws FetchException; + + /** + * @see Storable#tryLoad + */ + boolean tryLoad() throws FetchException; + + /** + * @see Storable#insert + */ + void insert() throws PersistException; + + /** + * @see Storable#tryInsert + */ + boolean tryInsert() throws PersistException; + + /** + * @see Storable#update + */ + void update() throws PersistException; + + /** + * @see Storable#tryUpdate + */ + boolean tryUpdate() throws PersistException; + + /** + * @see Storable#delete + */ + void delete() throws PersistException; + + /** + * @see Storable#tryDelete + */ + boolean tryDelete() throws PersistException; + + /** + * Return another Support instance for the given Storable. + */ + WrappedSupport createSupport(S storable); +} diff --git a/src/main/java/com/amazon/carbonado/spi/package-info.java b/src/main/java/com/amazon/carbonado/spi/package-info.java new file mode 100644 index 0000000..2a6abad --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/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. + */ + +/** + * Core Service Provider Interface for Carbonado. Repositories are free to use + * this package to aid in their implementation. User-level applications have no + * need to use this package. + */ +package com.amazon.carbonado.spi; diff --git a/src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodec.java b/src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodec.java new file mode 100644 index 0000000..1aff3a2 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodec.java @@ -0,0 +1,337 @@ +/* + * Copyright 2006 Amazon Technologies, Inc. or its affiliates. + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks + * of Amazon Technologies, Inc. or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.amazon.carbonado.spi.raw; + +import java.util.Map; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.ClassInjector; +import org.cojen.util.WeakIdentityMap; + +import com.amazon.carbonado.CorruptEncodingException; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.info.Direction; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableProperty; + +import com.amazon.carbonado.util.QuickConstructorGenerator; + +/** + * Allows codecs to be defined for storables that have a custom encoding. + * + * @author Brian S O'Neill + * @see CustomStorableCodecFactory + */ +public abstract class CustomStorableCodec implements StorableCodec { + // Generated storable instances maintain a reference to user-defined + // concrete subclass of this class. + private static final String CUSTOM_STORABLE_CODEC_FIELD_NAME = "customStorableCodec$"; + + @SuppressWarnings("unchecked") + private static Map> cCache = + new WeakIdentityMap(); + + /** + * Returns a storable implementation that calls into CustomStorableCodec + * implementation for encoding and decoding. + */ + @SuppressWarnings("unchecked") + static Class + getStorableClass(Class type, boolean isMaster) + throws SupportException + { + synchronized (cCache) { + Class storableClass; + + RawStorableGenerator.Flavors flavors = + (RawStorableGenerator.Flavors) cCache.get(type); + + if (flavors == null) { + flavors = new RawStorableGenerator.Flavors(); + cCache.put(type, flavors); + } else if ((storableClass = flavors.getClass(isMaster)) != null) { + return storableClass; + } + + storableClass = generateStorableClass(type, isMaster); + flavors.setClass(storableClass, isMaster); + + return storableClass; + } + } + + @SuppressWarnings("unchecked") + private static Class + generateStorableClass(Class type, boolean isMaster) + throws SupportException + { + final Class abstractClass = + RawStorableGenerator.getAbstractClass(type, isMaster); + + ClassInjector ci = ClassInjector.create + (type.getName(), abstractClass.getClassLoader()); + + ClassFile cf = new ClassFile(ci.getClassName(), abstractClass); + cf.markSynthetic(); + cf.setSourceFile(CustomStorableCodec.class.getName()); + cf.setTarget("1.5"); + + // Declare some types. + final TypeDesc storageType = TypeDesc.forClass(Storage.class); + final TypeDesc rawSupportType = TypeDesc.forClass(RawSupport.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + final TypeDesc[] byteArrayParam = {byteArrayType}; + final TypeDesc customStorableCodecType = TypeDesc.forClass(CustomStorableCodec.class); + + // Add field for saving reference to concrete CustomStorableCodec. + cf.addField(Modifiers.PRIVATE.toFinal(true), + CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + + // Add constructor that accepts a Storage, a RawSupport, and a + // CustomStorableCodec. + { + TypeDesc[] params = {storageType, rawSupportType, + customStorableCodecType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + + // Call super class constructor. + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.loadLocal(b.getParameter(1)); + params = new TypeDesc[] {storageType, rawSupportType}; + b.invokeSuperConstructor(params); + + // Set private reference to customStorableCodec. + b.loadThis(); + b.loadLocal(b.getParameter(2)); + b.storeField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + + b.returnVoid(); + } + + // Add constructor that accepts a Storage, a RawSupport, an encoded + // key, an encoded data, and a CustomStorableCodec. + { + TypeDesc[] params = {storageType, rawSupportType, byteArrayType, byteArrayType, + customStorableCodecType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + + // Set private reference to customStorableCodec before calling + // super constructor. This is necessary because super class + // constructor will call our decode methods, which need the + // customStorableCodec. This trick is not allowed in Java, but the + // virtual machine verifier allows it. + b.loadThis(); + b.loadLocal(b.getParameter(4)); + b.storeField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + + // Now call super class constructor. + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.loadLocal(b.getParameter(1)); + b.loadLocal(b.getParameter(2)); + b.loadLocal(b.getParameter(3)); + params = new TypeDesc[] {storageType, rawSupportType, byteArrayType, byteArrayType}; + b.invokeSuperConstructor(params); + + b.returnVoid(); + } + + // Implement protected abstract methods inherited from parent class. + + // byte[] encodeKey() + { + // Encode the primary key into a byte array that supports correct + // ordering. No special key comparator is needed. + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.ENCODE_KEY_METHOD_NAME, + byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + TypeDesc[] params = {TypeDesc.forClass(Storable.class)}; + b.loadThis(); + b.invokeVirtual(customStorableCodecType, "encodePrimaryKey", byteArrayType, params); + b.returnValue(byteArrayType); + } + + // byte[] encodeData() + { + // Encoding non-primary key data properties. + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.ENCODE_DATA_METHOD_NAME, + byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + TypeDesc[] params = {TypeDesc.forClass(Storable.class)}; + b.loadThis(); + b.invokeVirtual(customStorableCodecType, "encodeData", byteArrayType, params); + b.returnValue(byteArrayType); + } + + // void decodeKey(byte[]) + { + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.DECODE_KEY_METHOD_NAME, + null, byteArrayParam); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + TypeDesc[] params = {TypeDesc.forClass(Storable.class), byteArrayType}; + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeVirtual(customStorableCodecType, "decodePrimaryKey", null, params); + b.returnVoid(); + } + + // void decodeData(byte[]) + { + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.DECODE_DATA_METHOD_NAME, + null, byteArrayParam); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(CUSTOM_STORABLE_CODEC_FIELD_NAME, customStorableCodecType); + TypeDesc[] params = {TypeDesc.forClass(Storable.class), byteArrayType}; + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeVirtual(customStorableCodecType, "decodeData", null, params); + b.returnVoid(); + } + + return ci.defineClass(cf); + } + + private final Class mType; + private final int mPkPropertyCount; + private final InstanceFactory mInstanceFactory; + + public interface InstanceFactory { + Storable instantiate(RawSupport support, CustomStorableCodec codec); + + Storable instantiate(RawSupport support, byte[] key, byte[] value, + CustomStorableCodec codec) + throws FetchException; + } + + /** + * @param isMaster when true, version properties and sequences are managed + * @throws SupportException if Storable is not supported + */ + public CustomStorableCodec(Class type, boolean isMaster) throws SupportException { + mType = type; + mPkPropertyCount = getPrimaryKeyIndex().getPropertyCount(); + Class storableClass = getStorableClass(type, isMaster); + mInstanceFactory = QuickConstructorGenerator + .getInstance(storableClass, InstanceFactory.class); + } + + public Class getStorableType() { + return mType; + } + + @SuppressWarnings("unchecked") + public S instantiate(RawSupport support) { + return (S) mInstanceFactory.instantiate(support, this); + } + + @SuppressWarnings("unchecked") + public S instantiate(RawSupport support, byte[] key, byte[] value) + throws FetchException + { + return (S) mInstanceFactory.instantiate(support, key, value, this); + } + + public byte[] encodePrimaryKey(S storable) { + return encodePrimaryKey(storable, 0, mPkPropertyCount); + } + + public byte[] encodePrimaryKey(Object[] values) { + return encodePrimaryKey(values, 0, mPkPropertyCount); + } + + /** + * Convenient access to all the storable properties. + */ + public Map> getAllProperties() { + return StorableIntrospector.examine(getStorableType()).getAllProperties(); + } + + /** + * Convenient way to define the clustered primary key index + * descriptor. Direction can be specified by prefixing the property name + * with a '+' or '-'. If unspecified, direction is assumed to be ascending. + */ + @SuppressWarnings("unchecked") + public StorableIndex buildPkIndex(String... propertyNames) { + Map> map = getAllProperties(); + int length = propertyNames.length; + StorableProperty[] properties = new StorableProperty[length]; + Direction[] directions = new Direction[length]; + for (int i=0; i(properties, directions, true, true); + } + + /** + * Decode the primary key into properties of the storable. + */ + public abstract void decodePrimaryKey(S storable, byte[] bytes) + throws CorruptEncodingException; + + /** + * Encode all properties of the storable excluding the primary key. + */ + public abstract byte[] encodeData(S storable); + + /** + * Decode the data into properties of the storable. + */ + public abstract void decodeData(S storable, byte[] bytes) + throws CorruptEncodingException; +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodecFactory.java b/src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodecFactory.java new file mode 100644 index 0000000..64d19e8 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodecFactory.java @@ -0,0 +1,70 @@ +/* + * 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.spi.raw; + +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.layout.Layout; + +/** + * Factory for custom storable codecs. + * + * @author Brian S O'Neill + */ +public abstract class CustomStorableCodecFactory implements StorableCodecFactory { + public CustomStorableCodecFactory() { + } + + /** + * Returns null to let repository decide what the name should be. + */ + public String getStorageName(Class type) throws SupportException { + return null; + } + + /** + * @param type type of storable to create codec for + * @param pkIndex ignored + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, attempt to encode a storable layout + * generation value in each storable + * @throws SupportException if type is not supported + */ + public CustomStorableCodec createCodec(Class type, + StorableIndex pkIndex, + boolean isMaster, + Layout layout) + throws SupportException + { + return createCodec(type, isMaster, layout); + } + + /** + * @param type type of storable to create codec for + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, attempt to encode a storable layout + * generation value in each storable + * @throws SupportException if type is not supported + */ + protected abstract CustomStorableCodec + createCodec(Class type, boolean isMaster, Layout layout) + throws SupportException; +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/DataDecoder.java b/src/main/java/com/amazon/carbonado/spi/raw/DataDecoder.java new file mode 100644 index 0000000..66f2f2f --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/DataDecoder.java @@ -0,0 +1,567 @@ +/* + * 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.spi.raw; + +import com.amazon.carbonado.CorruptEncodingException; + +import static com.amazon.carbonado.spi.raw.DataEncoder.*; + +/** + * A very low-level class that decodes key components encoded by methods of + * {@link DataEncoder}. + * + * @author Brian S O'Neill + */ +public class DataDecoder { + static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** + * Decodes a signed integer from exactly 4 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed integer value + */ + public static int decodeInt(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int value = (src[srcOffset] << 24) | ((src[srcOffset + 1] & 0xff) << 16) | + ((src[srcOffset + 2] & 0xff) << 8) | (src[srcOffset + 3] & 0xff); + return value ^ 0x80000000; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Integer object from exactly 1 or 5 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Integer object or null + */ + public static Integer decodeIntegerObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeInt(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed long from exactly 8 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed long value + */ + public static long decodeLong(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return + (((long)(((src[srcOffset ] ) << 24) | + ((src[srcOffset + 1] & 0xff) << 16) | + ((src[srcOffset + 2] & 0xff) << 8 ) | + ((src[srcOffset + 3] & 0xff) )) ^ 0x80000000 ) << 32) | + (((long)(((src[srcOffset + 4] ) << 24) | + ((src[srcOffset + 5] & 0xff) << 16) | + ((src[srcOffset + 6] & 0xff) << 8 ) | + ((src[srcOffset + 7] & 0xff) )) & 0xffffffffL) ); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Long object from exactly 1 or 9 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Long object or null + */ + public static Long decodeLongObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeLong(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed byte from exactly 1 byte. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed byte value + */ + public static byte decodeByte(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (byte)(src[srcOffset] ^ 0x80); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Byte object from exactly 1 or 2 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Byte object or null + */ + public static Byte decodeByteObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeByte(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed short from exactly 2 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed short value + */ + public static short decodeShort(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (short)(((src[srcOffset] << 8) | (src[srcOffset + 1] & 0xff)) ^ 0x8000); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Short object from exactly 1 or 3 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Short object or null + */ + public static Short decodeShortObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeShort(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a char from exactly 2 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return char value + */ + public static char decodeChar(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (char)((src[srcOffset] << 8) | (src[srcOffset + 1] & 0xff)); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a Character object from exactly 1 or 3 bytes. If null is + * returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Character object or null + */ + public static Character decodeCharacterObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeChar(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a boolean from exactly 1 byte. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return boolean value + */ + public static boolean decodeBoolean(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return src[srcOffset] == (byte)128; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a Boolean object from exactly 1 byte. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Boolean object or null + */ + public static Boolean decodeBooleanObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + switch (src[srcOffset]) { + case NULL_BYTE_LOW: case NULL_BYTE_HIGH: + return null; + case (byte)128: + return Boolean.TRUE; + default: + return Boolean.FALSE; + } + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a float from exactly 4 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return float value + */ + public static float decodeFloat(byte[] src, int srcOffset) + throws CorruptEncodingException + { + int bits = decodeFloatBits(src, srcOffset); + bits ^= (bits < 0) ? 0x80000000 : 0xffffffff; + return Float.intBitsToFloat(bits); + } + + /** + * Decodes a Float object from exactly 4 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Float object or null + */ + public static Float decodeFloatObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + int bits = decodeFloatBits(src, srcOffset); + bits ^= (bits < 0) ? 0x80000000 : 0xffffffff; + return bits == 0x7fffffff ? null : Float.intBitsToFloat(bits); + } + + protected static int decodeFloatBits(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (src[srcOffset] << 24) | ((src[srcOffset + 1] & 0xff) << 16) | + ((src[srcOffset + 2] & 0xff) << 8) | (src[srcOffset + 3] & 0xff); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a double from exactly 8 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return double value + */ + public static double decodeDouble(byte[] src, int srcOffset) + throws CorruptEncodingException + { + long bits = decodeDoubleBits(src, srcOffset); + bits ^= (bits < 0) ? 0x8000000000000000L : 0xffffffffffffffffL; + return Double.longBitsToDouble(bits); + } + + /** + * Decodes a Double object from exactly 8 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Double object or null + */ + public static Double decodeDoubleObj(byte[] src, int srcOffset) + throws CorruptEncodingException + { + long bits = decodeDoubleBits(src, srcOffset); + bits ^= (bits < 0) ? 0x8000000000000000L : 0xffffffffffffffffL; + return bits == 0x7fffffffffffffffL ? null : Double.longBitsToDouble(bits); + } + + protected static long decodeDoubleBits(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return + (((long)(((src[srcOffset ] ) << 24) | + ((src[srcOffset + 1] & 0xff) << 16) | + ((src[srcOffset + 2] & 0xff) << 8 ) | + ((src[srcOffset + 3] & 0xff) )) ) << 32) | + (((long)(((src[srcOffset + 4] ) << 24) | + ((src[srcOffset + 5] & 0xff) << 16) | + ((src[srcOffset + 6] & 0xff) << 8 ) | + ((src[srcOffset + 7] & 0xff) )) & 0xffffffffL)); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded byte array is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decode(byte[] src, int srcOffset, byte[][] valueRef) + throws CorruptEncodingException + { + try { + final int originalOffset = srcOffset; + + int b = src[srcOffset++] & 0xff; + if (b >= 0xf8) { + valueRef[0] = null; + return 1; + } + + int valueLength; + if (b <= 0x7f) { + valueLength = b; + } else if (b <= 0xbf) { + valueLength = ((b & 0x3f) << 8) | (src[srcOffset++] & 0xff); + } else if (b <= 0xdf) { + valueLength = ((b & 0x1f) << 16) | ((src[srcOffset++] & 0xff) << 8) | + (src[srcOffset++] & 0xff); + } else if (b <= 0xef) { + valueLength = ((b & 0x0f) << 24) | ((src[srcOffset++] & 0xff) << 16) | + ((src[srcOffset++] & 0xff) << 8) | (src[srcOffset++] & 0xff); + } else { + valueLength = ((b & 0x07) << 24) | ((src[srcOffset++] & 0xff) << 16) | + ((src[srcOffset++] & 0xff) << 8) | (src[srcOffset++] & 0xff); + } + + if (valueLength == 0) { + valueRef[0] = EMPTY_BYTE_ARRAY; + } else { + byte[] value = new byte[valueLength]; + System.arraycopy(src, srcOffset, value, 0, valueLength); + valueRef[0]= value; + } + + return srcOffset - originalOffset + valueLength; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes an encoded string from the given byte array. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded string is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decodeString(byte[] src, int srcOffset, String[] valueRef) + throws CorruptEncodingException + { + try { + final int originalOffset = srcOffset; + + int b = src[srcOffset++] & 0xff; + if (b >= 0xf8) { + valueRef[0] = null; + return 1; + } + + int valueLength; + if (b <= 0x7f) { + valueLength = b; + } else if (b <= 0xbf) { + valueLength = ((b & 0x3f) << 8) | (src[srcOffset++] & 0xff); + } else if (b <= 0xdf) { + valueLength = ((b & 0x1f) << 16) | ((src[srcOffset++] & 0xff) << 8) | + (src[srcOffset++] & 0xff); + } else if (b <= 0xef) { + valueLength = ((b & 0x0f) << 24) | ((src[srcOffset++] & 0xff) << 16) | + ((src[srcOffset++] & 0xff) << 8) | (src[srcOffset++] & 0xff); + } else { + valueLength = ((src[srcOffset++] & 0xff) << 24) | + ((src[srcOffset++] & 0xff) << 16) | + ((src[srcOffset++] & 0xff) << 8) | (src[srcOffset++] & 0xff); + } + + if (valueLength == 0) { + valueRef[0] = ""; + return srcOffset - originalOffset; + } + + char[] value = new char[valueLength]; + int valueOffset = 0; + + while (valueOffset < valueLength) { + int c = src[srcOffset++] & 0xff; + switch (c >> 5) { + case 0: case 1: case 2: case 3: + // 0xxxxxxx + value[valueOffset++] = (char)c; + break; + case 4: case 5: + // 10xxxxxx xxxxxxxx + value[valueOffset++] = (char)(((c & 0x3f) << 8) | (src[srcOffset++] & 0xff)); + break; + case 6: + // 110xxxxx xxxxxxxx xxxxxxxx + c = ((c & 0x1f) << 16) | ((src[srcOffset++] & 0xff) << 8) + | (src[srcOffset++] & 0xff); + if (c >= 0x10000) { + // Split into surrogate pair. + c -= 0x10000; + value[valueOffset++] = (char)(0xd800 | ((c >> 10) & 0x3ff)); + value[valueOffset++] = (char)(0xdc00 | (c & 0x3ff)); + } else { + value[valueOffset++] = (char)c; + } + break; + default: + // 111xxxxx + // Illegal. + throw new CorruptEncodingException + ("Corrupt encoded string data (source offset = " + + (srcOffset - 1) + ')'); + } + } + + valueRef[0] = new String(value); + + return srcOffset - originalOffset; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array which was encoded by {@link + * DataEncoder#encodeSingle}. + * + * @param prefixPadding amount of extra bytes to skip from start of encoded byte array + * @param suffixPadding amount of extra bytes to skip at end of encoded byte array + */ + public static byte[] decodeSingle(byte[] src, int prefixPadding, int suffixPadding) + throws CorruptEncodingException + { + try { + int length = src.length - suffixPadding - prefixPadding; + if (length == 0) { + return EMPTY_BYTE_ARRAY; + } + if (prefixPadding <= 0 && suffixPadding <= 0) { + return src; + } + byte[] dst = new byte[length]; + System.arraycopy(src, prefixPadding, dst, 0, length); + return dst; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array which was encoded by {@link + * DataEncoder#encodeSingleNullable}. + */ + public static byte[] decodeSingleNullable(byte[] src) throws CorruptEncodingException { + return decodeSingleNullable(src, 0, 0); + } + + /** + * Decodes the given byte array which was encoded by {@link + * DataEncoder#encodeSingleNullable}. + * + * @param prefixPadding amount of extra bytes to skip from start of encoded byte array + * @param suffixPadding amount of extra bytes to skip at end of encoded byte array + */ + public static byte[] decodeSingleNullable(byte[] src, int prefixPadding, int suffixPadding) + throws CorruptEncodingException + { + try { + byte b = src[prefixPadding]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + int length = src.length - suffixPadding - 1 - prefixPadding; + if (length == 0) { + return EMPTY_BYTE_ARRAY; + } + byte[] value = new byte[length]; + System.arraycopy(src, 1 + prefixPadding, value, 0, length); + return value; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/DataEncoder.java b/src/main/java/com/amazon/carbonado/spi/raw/DataEncoder.java new file mode 100644 index 0000000..15b8dca --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/DataEncoder.java @@ -0,0 +1,595 @@ +/* + * 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.spi.raw; + +/** + * A very low-level class that supports encoding of primitive data. For + * encoding data into keys, see {@link KeyEncoder}. + * + * @author Brian S O'Neill + * @see DataDecoder + */ +public class DataEncoder { + // Note: Most of these methods are inherited by KeyEncoder, which is why + // they are encoded for supporting proper ordering. + + /** Byte to use for null, low ordering */ + static final byte NULL_BYTE_LOW = 0; + + /** Byte to use for null, high ordering */ + static final byte NULL_BYTE_HIGH = (byte)~NULL_BYTE_LOW; + + /** Byte to use for not-null, low ordering */ + static final byte NOT_NULL_BYTE_HIGH = (byte)128; + + /** Byte to use for not-null, high ordering */ + static final byte NOT_NULL_BYTE_LOW = (byte)~NOT_NULL_BYTE_HIGH; + + static final byte[] NULL_BYTE_ARRAY_HIGH = {NULL_BYTE_HIGH}; + static final byte[] NULL_BYTE_ARRAY_LOW = {NULL_BYTE_LOW}; + static final byte[] NOT_NULL_BYTE_ARRAY_HIGH = {NOT_NULL_BYTE_HIGH}; + static final byte[] NOT_NULL_BYTE_ARRAY_LOW = {NOT_NULL_BYTE_LOW}; + + /** + * Encodes the given signed integer into exactly 4 bytes. + * + * @param value signed integer value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(int value, byte[] dst, int dstOffset) { + value ^= 0x80000000; + dst[dstOffset ] = (byte)(value >> 24); + dst[dstOffset + 1] = (byte)(value >> 16); + dst[dstOffset + 2] = (byte)(value >> 8); + dst[dstOffset + 3] = (byte)value; + } + + /** + * Encodes the given signed Integer object into exactly 1 or 5 bytes. If + * the Integer object is never expected to be null, consider encoding as an + * int primitive. + * + * @param value optional signed Integer value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Integer value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + encode(value.intValue(), dst, dstOffset + 1); + return 5; + } + } + + /** + * Encodes the given signed long into exactly 8 bytes. + * + * @param value signed long value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(long value, byte[] dst, int dstOffset) { + int w = ((int)(value >> 32)) ^ 0x80000000; + dst[dstOffset ] = (byte)(w >> 24); + dst[dstOffset + 1] = (byte)(w >> 16); + dst[dstOffset + 2] = (byte)(w >> 8); + dst[dstOffset + 3] = (byte)w; + w = (int)value; + dst[dstOffset + 4] = (byte)(w >> 24); + dst[dstOffset + 5] = (byte)(w >> 16); + dst[dstOffset + 6] = (byte)(w >> 8); + dst[dstOffset + 7] = (byte)w; + } + + /** + * Encodes the given signed Long object into exactly 1 or 9 bytes. If the + * Long object is never expected to be null, consider encoding as a long + * primitive. + * + * @param value optional signed Long value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Long value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + encode(value.longValue(), dst, dstOffset + 1); + return 9; + } + } + + /** + * Encodes the given signed byte into exactly 1 byte. + * + * @param value signed byte value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(byte value, byte[] dst, int dstOffset) { + dst[dstOffset] = (byte)(value ^ 0x80); + } + + /** + * Encodes the given signed Byte object into exactly 1 or 2 bytes. If the + * Byte object is never expected to be null, consider encoding as a byte + * primitive. + * + * @param value optional signed Byte value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Byte value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + dst[dstOffset + 1] = (byte)(value ^ 0x80); + return 2; + } + } + + /** + * Encodes the given signed short into exactly 2 bytes. + * + * @param value signed short value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(short value, byte[] dst, int dstOffset) { + value ^= 0x8000; + dst[dstOffset ] = (byte)(value >> 8); + dst[dstOffset + 1] = (byte)value; + } + + /** + * Encodes the given signed Short object into exactly 1 or 3 bytes. If the + * Short object is never expected to be null, consider encoding as a short + * primitive. + * + * @param value optional signed Short value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Short value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + encode(value.shortValue(), dst, dstOffset + 1); + return 3; + } + } + + /** + * Encodes the given character into exactly 2 bytes. + * + * @param value character value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(char value, byte[] dst, int dstOffset) { + dst[dstOffset ] = (byte)(value >> 8); + dst[dstOffset + 1] = (byte)value; + } + + /** + * Encodes the given Character object into exactly 1 or 3 bytes. If the + * Character object is never expected to be null, consider encoding as a + * char primitive. + * + * @param value optional Character value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(Character value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_HIGH; + encode(value.charValue(), dst, dstOffset + 1); + return 3; + } + } + + /** + * Encodes the given boolean into exactly 1 byte. + * + * @param value boolean value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(boolean value, byte[] dst, int dstOffset) { + dst[dstOffset] = value ? (byte)128 : (byte)127; + } + + /** + * Encodes the given Boolean object into exactly 1 byte. + * + * @param value optional Boolean value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(Boolean value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + } else { + dst[dstOffset] = value.booleanValue() ? (byte)128 : (byte)127; + } + } + + /** + * Encodes the given float into exactly 4 bytes. + * + * @param value float value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(float value, byte[] dst, int dstOffset) { + int bits = Float.floatToIntBits(value); + bits ^= (bits < 0) ? 0xffffffff : 0x80000000; + dst[dstOffset ] = (byte)(bits >> 24); + dst[dstOffset + 1] = (byte)(bits >> 16); + dst[dstOffset + 2] = (byte)(bits >> 8); + dst[dstOffset + 3] = (byte)bits; + } + + /** + * Encodes the given Float object into exactly 4 bytes. A non-canonical NaN + * value is used to represent null. + * + * @param value optional Float value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(Float value, byte[] dst, int dstOffset) { + if (value == null) { + encode(0x7fffffff, dst, dstOffset); + } else { + encode(value.floatValue(), dst, dstOffset); + } + } + + /** + * Encodes the given double into exactly 8 bytes. + * + * @param value double value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(double value, byte[] dst, int dstOffset) { + long bits = Double.doubleToLongBits(value); + bits ^= (bits < 0) ? 0xffffffffffffffffL : 0x8000000000000000L; + int w = (int)(bits >> 32); + dst[dstOffset ] = (byte)(w >> 24); + dst[dstOffset + 1] = (byte)(w >> 16); + dst[dstOffset + 2] = (byte)(w >> 8); + dst[dstOffset + 3] = (byte)w; + w = (int)bits; + dst[dstOffset + 4] = (byte)(w >> 24); + dst[dstOffset + 5] = (byte)(w >> 16); + dst[dstOffset + 6] = (byte)(w >> 8); + dst[dstOffset + 7] = (byte)w; + } + + /** + * Encodes the given Double object into exactly 8 bytes. A non-canonical + * NaN value is used to represent null. + * + * @param value optional Double value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encode(Double value, byte[] dst, int dstOffset) { + if (value == null) { + encode(0x7fffffffffffffffL, dst, dstOffset); + } else { + encode(value.doubleValue(), dst, dstOffset); + } + } + + /** + * Encodes the given optional byte array into a variable amount of + * bytes. If the byte array is null, exactly 1 byte is written. Otherwise, + * the amount written can be determined by calling calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(byte[] value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } + return encode(value, 0, value.length, dst, dstOffset); + } + + /** + * Encodes the given optional byte array into a variable amount of + * bytes. If the byte array is null, exactly 1 byte is written. Otherwise, + * the amount written can be determined by calling calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(byte[] value, int valueOffset, int valueLength, + byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } + + // Write the value length first, in a variable amount of bytes. + int amt = writeLength(valueLength, dst, dstOffset); + + // Now write the value. + System.arraycopy(value, valueOffset, dst, dstOffset + amt, valueLength); + + return amt + valueLength; + } + + /** + * Returns the amount of bytes required to encode the given byte array. + * + * @param value byte array value to encode, may be null + * @return amount of bytes needed to encode + */ + public static int calculateEncodedLength(byte[] value) { + return value == null ? 1 : calculateEncodedLength(value, 0, value.length); + } + + /** + * Returns the amount of bytes required to encode the given byte array. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @return amount of bytes needed to encode + */ + public static int calculateEncodedLength(byte[] value, int valueOffset, int valueLength) { + if (value == null) { + return 1; + } else if (valueLength < 128) { + return 1 + valueLength; + } else if (valueLength < 16384) { + return 2 + valueLength; + } else if (valueLength < 2097152) { + return 3 + valueLength; + } else if (valueLength < 268435456) { + return 4 + valueLength; + } else { + return 5 + valueLength; + } + } + + /** + * Encodes the given optional String into a variable amount of bytes. The + * amount written can be determined by calling + * calculateEncodedStringLength. + *

+ * Strings are encoded in a fashion similar to UTF-8, in that ASCII + * characters are written in one byte. This encoding is more efficient than + * UTF-8, but it isn't compatible with UTF-8. + * + * @param value String value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(String value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } + final int originalOffset = dstOffset; + + int valueLength = value.length(); + + // Write the value length first, in a variable amount of bytes. + dstOffset += writeLength(valueLength, dst, dstOffset); + + for (int i = 0; i < valueLength; i++) { + int c = value.charAt(i); + if (c <= 0x7f) { + dst[dstOffset++] = (byte)c; + } else if (c <= 0x3fff) { + dst[dstOffset++] = (byte)(0x80 | (c >> 8)); + dst[dstOffset++] = (byte)(c & 0xff); + } else { + if (c >= 0xd800 && c <= 0xdbff) { + // Found a high surrogate. Verify that surrogate pair is + // well-formed. Low surrogate must follow high surrogate. + if (i + 1 < valueLength) { + int c2 = value.charAt(i + 1); + if (c2 >= 0xdc00 && c2 <= 0xdfff) { + c = 0x10000 + (((c & 0x3ff) << 10) | (c2 & 0x3ff)); + i++; + } + } + } + dst[dstOffset++] = (byte)(0xc0 | (c >> 16)); + dst[dstOffset++] = (byte)((c >> 8) & 0xff); + dst[dstOffset++] = (byte)(c & 0xff); + } + } + + return dstOffset - originalOffset; + } + + /** + * Returns the amount of bytes required to encode the given String. + * + * @param value String to encode, may be null + */ + public static int calculateEncodedStringLength(String value) { + if (value == null) { + return 1; + } + + int valueLength = value.length(); + int encodedLen; + + if (valueLength < 128) { + encodedLen = 1; + } else if (valueLength < 16384) { + encodedLen = 2; + } else if (valueLength < 2097152) { + encodedLen = 3; + } else if (valueLength < 268435456) { + encodedLen = 4; + } else { + encodedLen = 5; + } + + for (int i = 0; i < valueLength; i++) { + int c = value.charAt(i); + if (c <= 0x7f) { + encodedLen++; + } else if (c <= 0x3fff) { + encodedLen += 2; + } else { + if (c >= 0xd800 && c <= 0xdbff) { + // Found a high surrogate. Verify that surrogate pair is + // well-formed. Low surrogate must follow high surrogate. + if (i + 1 < valueLength) { + int c2 = value.charAt(i + 1); + if (c2 >= 0xdc00 && c2 <= 0xdfff) { + i++; + } + } + } + encodedLen += 3; + } + } + + return encodedLen; + } + + private static int writeLength(int valueLength, byte[] dst, int dstOffset) { + if (valueLength < 128) { + dst[dstOffset] = (byte)valueLength; + return 1; + } else if (valueLength < 16384) { + dst[dstOffset++] = (byte)((valueLength >> 8) | 0x80); + dst[dstOffset] = (byte)valueLength; + return 2; + } else if (valueLength < 2097152) { + dst[dstOffset++] = (byte)((valueLength >> 16) | 0xc0); + dst[dstOffset++] = (byte)(valueLength >> 8); + dst[dstOffset] = (byte)valueLength; + return 3; + } else if (valueLength < 268435456) { + dst[dstOffset++] = (byte)((valueLength >> 24) | 0xe0); + dst[dstOffset++] = (byte)(valueLength >> 16); + dst[dstOffset++] = (byte)(valueLength >> 8); + dst[dstOffset] = (byte)valueLength; + return 4; + } else { + dst[dstOffset++] = (byte)0xf0; + dst[dstOffset++] = (byte)(valueLength >> 24); + dst[dstOffset++] = (byte)(valueLength >> 16); + dst[dstOffset++] = (byte)(valueLength >> 8); + dst[dstOffset] = (byte)valueLength; + return 5; + } + } + + /** + * Encodes the given byte array for use when there is only a single + * property, whose type is a byte array. The original byte array is + * returned if the padding lengths are zero. + * + * @param prefixPadding amount of extra bytes to allocate at start of encoded byte array + * @param suffixPadding amount of extra bytes to allocate at end of encoded byte array + */ + public static byte[] encodeSingle(byte[] value, int prefixPadding, int suffixPadding) { + if (prefixPadding <= 0 && suffixPadding <= 0) { + return value; + } + int length = value.length; + byte[] dst = new byte[prefixPadding + length + suffixPadding]; + System.arraycopy(value, 0, dst, prefixPadding, length); + return dst; + } + + /** + * Encodes the given byte array for use when there is only a single + * nullable property, whose type is a byte array. + */ + public static byte[] encodeSingleNullable(byte[] value) { + return encodeSingleNullable(value, 0, 0); + } + + /** + * Encodes the given byte array for use when there is only a single + * nullable property, whose type is a byte array. + * + * @param prefixPadding amount of extra bytes to allocate at start of encoded byte array + * @param suffixPadding amount of extra bytes to allocate at end of encoded byte array + */ + public static byte[] encodeSingleNullable(byte[] value, int prefixPadding, int suffixPadding) { + if (prefixPadding <= 0 && suffixPadding <= 0) { + if (value == null) { + return NULL_BYTE_ARRAY_HIGH; + } + + int length = value.length; + if (length == 0) { + return NOT_NULL_BYTE_ARRAY_HIGH; + } + + byte[] dst = new byte[1 + length]; + dst[0] = NOT_NULL_BYTE_HIGH; + System.arraycopy(value, 0, dst, 1, length); + return dst; + } + + if (value == null) { + byte[] dst = new byte[prefixPadding + 1 + suffixPadding]; + dst[prefixPadding] = NULL_BYTE_HIGH; + return dst; + } + + int length = value.length; + byte[] dst = new byte[prefixPadding + 1 + length + suffixPadding]; + dst[prefixPadding] = NOT_NULL_BYTE_HIGH; + System.arraycopy(value, 0, dst, prefixPadding + 1, length); + return dst; + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/GenericEncodingStrategy.java b/src/main/java/com/amazon/carbonado/spi/raw/GenericEncodingStrategy.java new file mode 100644 index 0000000..cd408e9 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/GenericEncodingStrategy.java @@ -0,0 +1,1963 @@ +/* + * 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.spi.raw; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; + +import org.cojen.classfile.CodeAssembler; +import org.cojen.classfile.Label; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.Opcode; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.BeanIntrospector; +import org.cojen.util.BeanProperty; + +import com.amazon.carbonado.CorruptEncodingException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.lob.Blob; +import com.amazon.carbonado.lob.Clob; +import com.amazon.carbonado.lob.Lob; + +import com.amazon.carbonado.spi.StorableGenerator; +import com.amazon.carbonado.spi.TriggerSupport; + +import com.amazon.carbonado.info.ChainedProperty; +import com.amazon.carbonado.info.Direction; +import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.info.StorableIntrospector; +import com.amazon.carbonado.info.StorableProperty; +import com.amazon.carbonado.info.StorablePropertyAdapter; + +/** + * Generates bytecode instructions for encoding/decoding Storable properties + * to/from raw bytes. + * + *

Note: subclasses must override and specialize the hashCode and equals + * methods. Failure to do so interferes with {@link StorableCodecFactory}'s + * generated code cache. + * + * @author Brian S O'Neill + */ +public class GenericEncodingStrategy { + private final Class mType; + private final StorableIndex mPkIndex; + + private final int mKeyPrefixPadding; + private final int mKeySuffixPadding; + private final int mDataPrefixPadding; + private final int mDataSuffixPadding; + + /** + * @param type type of Storable to generate code for + * @param pkIndex specifies sequence and ordering of key properties (optional) + */ + public GenericEncodingStrategy(Class type, StorableIndex pkIndex) { + this(type, pkIndex, 0, 0, 0, 0); + } + + /** + * @param type type of Storable to generate code for + * @param pkIndex specifies sequence and ordering of key properties (optional) + * @param keyPrefixPadding amount of padding bytes at start of keys + * @param keySuffixPadding amount of padding bytes at end of keys + * @param dataPrefixPadding amount of padding bytes at start of data values + * @param dataSuffixPadding amount of padding bytes at end of data values + */ + @SuppressWarnings("unchecked") + public GenericEncodingStrategy(Class type, StorableIndex pkIndex, + int keyPrefixPadding, int keySuffixPadding, + int dataPrefixPadding, int dataSuffixPadding) { + mType = type; + + if (keyPrefixPadding < 0 || keySuffixPadding < 0 || + dataPrefixPadding < 0 || dataSuffixPadding < 0) { + throw new IllegalArgumentException(); + } + mKeyPrefixPadding = keyPrefixPadding; + mKeySuffixPadding = keySuffixPadding; + mDataPrefixPadding = dataPrefixPadding; + mDataSuffixPadding = dataSuffixPadding; + + if (pkIndex == null) { + Map> map = + StorableIntrospector.examine(mType).getPrimaryKeyProperties(); + + StorableProperty[] properties = new StorableProperty[map.size()]; + map.values().toArray(properties); + + Direction[] directions = new Direction[map.size()]; + Arrays.fill(directions, Direction.UNSPECIFIED); + + pkIndex = new StorableIndex(properties, directions, true); + } + + mPkIndex = pkIndex; + } + + /** + * Generates bytecode instructions to encode properties. The encoding is + * suitable for "key" encoding, which means it is correctly comparable. + * + *

Note: if a partialStartVar is provided and this strategy has a key + * prefix, the prefix is allocated only if the runtime value of + * partialStartVar is zero. Likewise, if a partialEndVar is provided and + * this strategy has a key suffix, the suffix is allocated only of the + * runtime value of partialEndVar is one less than the property count. + * + * @param assembler code assembler to receive bytecode instructions + * @param properties specific properties to encode, defaults to all key + * properties if null + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are read from the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param useReadMethods when true, access properties by public read + * methods instead of protected fields - should be used if class being + * generated doesn't have access to these fields + * @param partialStartVar optional variable for supporting partial key + * generation. It must be an int, whose runtime value must be less than the + * properties array length. It marks the range start of the partial + * property range. + * @param partialEndVar optional variable for supporting partial key + * generation. It must be an int, whose runtime value must be less than or + * equal to the properties array length. It marks the range end (exclusive) + * of the partial property range. + * + * @return local variable referencing a byte array with encoded key + * + * @throws SupportException if any property type is not supported + * @throws IllegalArgumentException if assembler is null, or if instanceVar + * is not the correct instance type, or if partial variable types are not + * ints + */ + public LocalVariable buildKeyEncoding(CodeAssembler assembler, + OrderedProperty[] properties, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useReadMethods, + LocalVariable partialStartVar, + LocalVariable partialEndVar) + throws SupportException + { + properties = ensureKeyProperties(properties); + return buildEncoding(true, assembler, + extractProperties(properties), extractDirections(properties), + instanceVar, adapterInstanceClass, + useReadMethods, + -1, // no generation support + partialStartVar, partialEndVar); + } + + /** + * Generates bytecode instructions to decode properties. A + * CorruptEncodingException may be thrown from generated code. + * + * @param assembler code assembler to receive bytecode instructions + * @param properties specific properties to decode, defaults to all key + * properties if null + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are placed into the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param useWriteMethods when true, set properties by public write + * methods instead of protected fields - should be used if class being + * generated doesn't have access to these fields + * @param encodedVar required variable, which must be a byte array. At + * runtime, it references an encoded key. + * + * @throws SupportException if any property type is not supported + * @throws IllegalArgumentException if assembler is null, or if instanceVar + * is not the correct instance type, or if encodedVar is not a byte array + */ + public void buildKeyDecoding(CodeAssembler assembler, + OrderedProperty[] properties, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useWriteMethods, + LocalVariable encodedVar) + throws SupportException + { + properties = ensureKeyProperties(properties); + buildDecoding(true, assembler, + extractProperties(properties), extractDirections(properties), + instanceVar, adapterInstanceClass, useWriteMethods, + -1, null, // no generation support + encodedVar); + } + + /** + * Generates bytecode instructions to encode properties. The encoding is + * suitable for "data" encoding, which means it is not correctly + * comparable, but it is more efficient than key encoding. Partial encoding + * is not supported. + * + * @param assembler code assembler to receive bytecode instructions + * @param properties specific properties to encode, defaults to all non-key + * properties if null + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are read from the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param useReadMethods when true, access properties by public read + * methods instead of protected fields + * @param generation when non-negative, write a storable layout generation + * value in one or four bytes. Generation 0..127 is encoded in one byte, and + * 128..max is encoded in four bytes, with the most significant bit set. + * + * @return local variable referencing a byte array with encoded data + * + * @throws SupportException if any property type is not supported + * @throws IllegalArgumentException if assembler is null, or if instanceVar + * is not the correct instance type + */ + public LocalVariable buildDataEncoding(CodeAssembler assembler, + StorableProperty[] properties, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useReadMethods, + int generation) + throws SupportException + { + properties = ensureDataProperties(properties); + return buildEncoding(false, assembler, + properties, null, + instanceVar, adapterInstanceClass, + useReadMethods, generation, null, null); + } + + /** + * Generates bytecode instructions to decode properties. A + * CorruptEncodingException may be thrown from generated code. + * + * @param assembler code assembler to receive bytecode instructions + * @param properties specific properties to decode, defaults to all non-key + * properties if null + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are placed into the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param useWriteMethods when true, set properties by public write + * methods instead of protected fields - should be used if class being + * generated doesn't have access to these fields + * @param generation when non-negative, decoder expects a storable layout + * generation value to match this value. Otherwise, it throws a + * CorruptEncodingException. + * @param altGenerationHandler if non-null and a generation is provided, + * this label defines an alternate generation handler. It is executed + * instead of throwing a CorruptEncodingException if the generation doesn't + * match. The actual generation is available on the top of the stack for + * the handler to consume. + * @param encodedVar required variable, which must be a byte array. At + * runtime, it references encoded data. + * + * @throws SupportException if any property type is not supported + * @throws IllegalArgumentException if assembler is null, or if instanceVar + * is not the correct instance type, or if encodedVar is not a byte array + */ + public void buildDataDecoding(CodeAssembler assembler, + StorableProperty[] properties, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useWriteMethods, + int generation, + Label altGenerationHandler, + LocalVariable encodedVar) + throws SupportException + { + properties = ensureDataProperties(properties); + buildDecoding(false, assembler, properties, null, + instanceVar, adapterInstanceClass, useWriteMethods, + generation, altGenerationHandler, encodedVar); + } + + /** + * Returns the type of Storable that code is generated for. + */ + public final Class getType() { + return mType; + } + + /** + * Returns true if the type of the given property type is supported. The + * types currently supported are primitives, primitive wrapper objects, + * Strings, and byte arrays. + */ + public boolean isSupported(Class propertyType) { + return isSupported(TypeDesc.forClass(propertyType)); + } + + /** + * Returns true if the type of the given property type is supported. The + * types currently supported are primitives, primitive wrapper objects, + * Strings, byte arrays and Lobs. + */ + public boolean isSupported(TypeDesc propertyType) { + if (propertyType.toPrimitiveType() != null) { + return true; + } + return propertyType == TypeDesc.STRING || + propertyType == TypeDesc.forClass(byte[].class) || + propertyType.toClass() != null && Lob.class.isAssignableFrom(propertyType.toClass()); + } + + public int getKeyPrefixPadding() { + return mKeyPrefixPadding; + } + + public int getKeySuffixPadding() { + return mKeySuffixPadding; + } + + public int getDataPrefixPadding() { + return mDataPrefixPadding; + } + + public int getDataSuffixPadding() { + return mDataSuffixPadding; + } + + /** + * Returns amount of prefix key bytes that encoding strategy instance + * produces which are always the same. Default implementation returns 0. + */ + public int getConstantKeyPrefixLength() { + return 0; + } + + @Override + public int hashCode() { + return mType.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof GenericEncodingStrategy) { + GenericEncodingStrategy other = (GenericEncodingStrategy) obj; + return mType == other.mType + && mKeyPrefixPadding == other.mKeyPrefixPadding + && mKeySuffixPadding == other.mKeySuffixPadding + && mDataPrefixPadding == other.mDataPrefixPadding + && mDataSuffixPadding == other.mDataSuffixPadding; + } + return false; + } + + /** + * Returns all key properties in the form of an index. + */ + protected StorableIndex getPrimaryKeyIndex() { + return mPkIndex; + } + + /** + * Returns all key properties as ordered properties, possibly with + * unspecified directions. + */ + protected OrderedProperty[] gatherAllKeyProperties() { + return mPkIndex.getOrderedProperties(); + } + + /** + * Returns all data properties for storable. + */ + @SuppressWarnings("unchecked") + protected StorableProperty[] gatherAllDataProperties() { + Map> map = + StorableIntrospector.examine(mType).getDataProperties(); + + StorableProperty[] properties = new StorableProperty[map.size()]; + + int ordinal = 0; + for (StorableProperty property : map.values()) { + properties[ordinal++] = property; + } + + return properties; + } + + protected StorablePropertyInfo checkSupport(StorableProperty property) + throws SupportException + { + if (isSupported(property.getType())) { + return new StorablePropertyInfo(property); + } + + // Look for an adapter that will allow this property to be supported. + if (property.getAdapter() != null) { + StorablePropertyAdapter adapter = property.getAdapter(); + for (Class storageType : adapter.getStorageTypePreferences()) { + if (!isSupported(storageType)) { + continue; + } + + if (property.isNullable() && storageType.isPrimitive()) { + continue; + } + + Method fromStorage, toStorage; + fromStorage = adapter.findAdaptMethod(storageType, property.getType()); + if (fromStorage == null) { + continue; + } + toStorage = adapter.findAdaptMethod(property.getType(), storageType); + if (toStorage != null) { + return new StorablePropertyInfo(property, storageType, fromStorage, toStorage); + } + } + } + + throw notSupported(property); + } + + @SuppressWarnings("unchecked") + protected StorablePropertyInfo[] checkSupport(StorableProperty[] properties) + throws SupportException + { + int length = properties.length; + StorablePropertyInfo[] infos = new StorablePropertyInfo[length]; + for (int i=0; i property) { + return notSupported(property.getName(), property.getType().getName()); + } + + private SupportException notSupported(String propertyName, String typeName) { + return new SupportException + ("Type \"" + typeName + + "\" not supported for property \"" + propertyName + '"'); + } + + private OrderedProperty[] ensureKeyProperties(OrderedProperty[] properties) { + if (properties == null) { + properties = gatherAllKeyProperties(); + } else { + for (Object prop : properties) { + if (prop == null) { + throw new IllegalArgumentException(); + } + } + } + return properties; + } + + @SuppressWarnings("unchecked") + private StorableProperty[] extractProperties(OrderedProperty[] ordered) { + StorableProperty[] properties = new StorableProperty[ordered.length]; + for (int i=0; i 0) { + throw new IllegalArgumentException(); + } + properties[i] = chained.getPrimeProperty(); + } + return properties; + } + + private Direction[] extractDirections(OrderedProperty[] ordered) { + Direction[] directions = new Direction[ordered.length]; + for (int i=0; i[] ensureDataProperties(StorableProperty[] properties) { + if (properties == null) { + properties = gatherAllDataProperties(); + } else { + for (Object prop : properties) { + if (prop == null) { + throw new IllegalArgumentException(); + } + } + } + return properties; + } + + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + + private LocalVariable buildEncoding(boolean forKey, + CodeAssembler a, + StorableProperty[] properties, + Direction[] directions, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useReadMethods, + int generation, + LocalVariable partialStartVar, + LocalVariable partialEndVar) + throws SupportException + { + if (a == null) { + throw new IllegalArgumentException(); + } + if (partialStartVar != null && partialStartVar.getType() != TypeDesc.INT) { + throw new IllegalArgumentException(); + } + if (partialEndVar != null && partialEndVar.getType() != TypeDesc.INT) { + throw new IllegalArgumentException(); + } + + // Encoding order is: + // + // 1. Prefix + // 2. Generation prefix + // 3. Properties + // 4. Suffix + + final int prefix = forKey ? mKeyPrefixPadding : mDataPrefixPadding; + + final int generationPrefix; + if (generation < 0) { + generationPrefix = 0; + } else if (generation < 128) { + generationPrefix = 1; + } else { + generationPrefix = 4; + } + + final int suffix = forKey ? mKeySuffixPadding : mDataSuffixPadding; + + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + final LocalVariable encodedVar = a.createLocalVariable(null, byteArrayType); + + StorablePropertyInfo[] infos = checkSupport(properties); + + if (properties.length == 1) { + // Ignore partial key encoding variables, since there can't be a + // partial of one property. + partialStartVar = null; + partialEndVar = null; + + StorableProperty property = properties[0]; + StorablePropertyInfo info = infos[0]; + + if (info.getStorageType().toClass() == byte[].class) { + // Since there is only one property, and it is just a byte + // array, optimize by not doing any fancy encoding. If the + // property is optional, then a byte prefix is needed to + // identify a null reference. + + loadPropertyValue(a, info, 0, useReadMethods, + instanceVar, adapterInstanceClass, partialStartVar); + + boolean descending = + forKey && directions != null && directions[0] == Direction.DESCENDING; + + TypeDesc[] params; + if (prefix > 0 || generationPrefix > 0 || suffix > 0) { + a.loadConstant(prefix + generationPrefix); + a.loadConstant(suffix); + params = new TypeDesc[] {byteArrayType, TypeDesc.INT, TypeDesc.INT}; + } else { + params = new TypeDesc[] {byteArrayType}; + } + + if (property.isNullable()) { + if (descending) { + a.invokeStatic(KeyEncoder.class.getName(), "encodeSingleNullableDesc", + byteArrayType, params); + } else { + a.invokeStatic(DataEncoder.class.getName(), "encodeSingleNullable", + byteArrayType, params); + } + } else if (descending) { + a.invokeStatic(KeyEncoder.class.getName(), "encodeSingleDesc", + byteArrayType, params); + } else if (prefix > 0 || generationPrefix > 0 || suffix > 0) { + a.invokeStatic(DataEncoder.class.getName(), "encodeSingle", + byteArrayType, params); + } else { + // Just return raw property value - no need to cache it either. + } + + a.storeLocal(encodedVar); + + encodeGeneration(a, encodedVar, prefix, generation); + + return encodedVar; + } + } + + boolean doPartial = forKey && (partialStartVar != null || partialEndVar != null); + + // Calculate exactly how many bytes are needed to encode. The length + // is composed of a static and a variable amount. The variable amount + // is determined at runtime. + + int staticLength = 0; + if (!forKey || partialStartVar == null) { + // Only include prefix as static if no runtime check is needed + // against runtime partial start value. + staticLength += prefix + generationPrefix; + } + if (!forKey || partialEndVar == null) { + // Only include suffix as static if no runtime check is needed + // against runtime partial end value. + staticLength += suffix; + } + + boolean hasVariableLength; + if (doPartial) { + hasVariableLength = true; + } else { + hasVariableLength = false; + for (GenericPropertyInfo info : infos) { + int len = staticEncodingLength(info); + if (len >= 0) { + staticLength += len; + } else { + staticLength += ~len; + hasVariableLength = true; + } + } + } + + // Generate code that loops over all the properties that have a + // variable length. Load each property and perform the necessary + // tests to determine the exact encoding length. + + boolean hasStackVar = false; + if (hasVariableLength) { + Label[] entryPoints = null; + + if (partialStartVar != null) { + // Will jump into an arbitrary location, so always have a stack + // variable available. + a.loadConstant(0); + hasStackVar = true; + + entryPoints = jumpToPartialEntryPoints(a, partialStartVar, properties.length); + } + + Label exitPoint = a.createLabel(); + + for (int i=0; i property = properties[i]; + StorablePropertyInfo info = infos[i]; + + if (doPartial) { + if (entryPoints != null) { + entryPoints[i].setLocation(); + } + if (partialEndVar != null) { + // Add code to jump out of partial. + a.loadConstant(i); + a.loadLocal(partialEndVar); + a.ifComparisonBranch(exitPoint, ">="); + } + } else if (staticEncodingLength(info) >= 0) { + continue; + } + + TypeDesc propType = info.getStorageType(); + + if (propType.isPrimitive()) { + // This should only ever get executed if implementing + // partial support. Otherwise, the static encoding length + // would have been already calculated. + a.loadConstant(staticEncodingLength(info)); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + } else if (propType.toPrimitiveType() != null) { + int amt = 0; + switch (propType.toPrimitiveType().getTypeCode()) { + case TypeDesc.BYTE_CODE: + case TypeDesc.BOOLEAN_CODE: + amt = 1; + break; + case TypeDesc.SHORT_CODE: + case TypeDesc.CHAR_CODE: + amt = 2; + break; + case TypeDesc.INT_CODE: + case TypeDesc.FLOAT_CODE: + amt = 4; + break; + case TypeDesc.LONG_CODE: + case TypeDesc.DOUBLE_CODE: + amt = 8; + break; + } + + int extra = 0; + if (doPartial) { + // If value is null, then there may be a one byte size + // adjust for the null value. Otherwise it is the extra + // amount plus the size to encode the raw primitive + // value. If doPartial is false, then this extra amount + // was already accounted for in the static encoding + // length. + + switch (propType.toPrimitiveType().getTypeCode()) { + case TypeDesc.BYTE_CODE: + case TypeDesc.SHORT_CODE: + case TypeDesc.CHAR_CODE: + case TypeDesc.INT_CODE: + case TypeDesc.LONG_CODE: + extra = 1; + } + } + + if (!property.isNullable() || (doPartial && extra == 0)) { + a.loadConstant(amt); + if (hasStackVar) { + a.math(Opcode.IADD); + } + hasStackVar = true; + } else { + // Load property to test for null. + loadPropertyValue(a, info, i, useReadMethods, + instanceVar, adapterInstanceClass, partialStartVar); + + Label isNull = a.createLabel(); + a.ifNullBranch(isNull, true); + + a.loadConstant(amt); + + if (hasStackVar) { + a.math(Opcode.IADD); + isNull.setLocation(); + if (extra > 0) { + a.loadConstant(extra); + a.math(Opcode.IADD); + } + } else { + hasStackVar = true; + // Make sure that there is a zero (or extra) value on + // the stack if the isNull branch is taken. + Label notNull = a.createLabel(); + a.branch(notNull); + isNull.setLocation(); + a.loadConstant(extra); + notNull.setLocation(); + } + } + } else if (propType == TypeDesc.STRING) { + // Load property to test for null. + loadPropertyValue(a, info, i, useReadMethods, + instanceVar, adapterInstanceClass, partialStartVar); + + String className = + (forKey ? KeyEncoder.class : DataEncoder.class).getName(); + a.invokeStatic(className, "calculateEncodedStringLength", + TypeDesc.INT, new TypeDesc[] {TypeDesc.STRING}); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + } else if (propType.toClass() == byte[].class) { + // Load property to test for null. + loadPropertyValue(a, info, i, useReadMethods, + instanceVar, adapterInstanceClass, partialStartVar); + + String className = + (forKey ? KeyEncoder.class : DataEncoder.class).getName(); + a.invokeStatic(className, "calculateEncodedLength", + TypeDesc.INT, new TypeDesc[] {byteArrayType}); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + } else if (info.isLob()) { + // Lob locator is a long, or 8 bytes. + a.loadConstant(8); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + } else { + throw notSupported(property); + } + } + + exitPoint.setLocation(); + + if (forKey && partialStartVar != null && (prefix > 0 || generationPrefix > 0)) { + // Prefix must be allocated only if runtime value of + // partialStartVar is zero. + a.loadLocal(partialStartVar); + Label noPrefix = a.createLabel(); + a.ifZeroComparisonBranch(noPrefix, "!="); + a.loadConstant(prefix + generationPrefix); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + noPrefix.setLocation(); + } + + if (forKey && partialEndVar != null && suffix > 0) { + // Suffix must be allocated only if runtime value of + // partialEndVar is equal to property count. + a.loadLocal(partialEndVar); + Label noSuffix = a.createLabel(); + a.loadConstant(properties.length); + a.ifComparisonBranch(noSuffix, "!="); + a.loadConstant(suffix); + if (hasStackVar) { + a.math(Opcode.IADD); + } else { + hasStackVar = true; + } + noSuffix.setLocation(); + } + } + + // Allocate a byte array of the exact size. + if (hasStackVar) { + if (staticLength > 0) { + a.loadConstant(staticLength); + a.math(Opcode.IADD); + } + } else { + a.loadConstant(staticLength); + } + a.newObject(byteArrayType); + a.storeLocal(encodedVar); + + // Now encode into the byte array. + + int constantOffset = 0; + LocalVariable offset = null; + + if (!forKey || partialStartVar == null) { + // Only include prefix as constant offset if no runtime check is + // needed against runtime partial start value. + constantOffset += prefix + generationPrefix; + encodeGeneration(a, encodedVar, prefix, generation); + } + + Label[] entryPoints = null; + + if (forKey && partialStartVar != null) { + // Will jump into an arbitrary location, so put an initial value + // into offset variable. + + offset = a.createLocalVariable(null, TypeDesc.INT); + a.loadConstant(0); + if (prefix > 0) { + // Prefix is allocated only if partial start is zero. Check if + // offset should be adjusted to skip over it. + a.loadLocal(partialStartVar); + Label noPrefix = a.createLabel(); + a.ifZeroComparisonBranch(noPrefix, "!="); + a.loadConstant(prefix + generationPrefix); + a.math(Opcode.IADD); + encodeGeneration(a, encodedVar, prefix, generation); + noPrefix.setLocation(); + } + a.storeLocal(offset); + + entryPoints = jumpToPartialEntryPoints(a, partialStartVar, properties.length); + } + + Label exitPoint = a.createLabel(); + + for (int i=0; i property = properties[i]; + StorablePropertyInfo info = infos[i]; + + if (doPartial) { + if (entryPoints != null) { + entryPoints[i].setLocation(); + } + if (partialEndVar != null) { + // Add code to jump out of partial. + a.loadConstant(i); + a.loadLocal(partialEndVar); + a.ifComparisonBranch(exitPoint, ">="); + } + } + + if (info.isLob()) { + // Need RawSupport instance for getting locator from Lob. + pushRawSupport(a, instanceVar); + } + + boolean fromInstance = loadPropertyValue + (a, info, i, useReadMethods, instanceVar, adapterInstanceClass, partialStartVar); + + TypeDesc propType = info.getStorageType(); + if (!property.isNullable() && propType.toPrimitiveType() != null) { + // Since property type is a required primitive wrapper, convert + // to a primitive rather than encoding using the form that + // distinguishes null. + + // Property value that was passed in may be null, which is not + // allowed. + if (!fromInstance && !propType.isPrimitive()) { + a.dup(); + Label notNull = a.createLabel(); + a.ifNullBranch(notNull, false); + + TypeDesc errorType = TypeDesc.forClass(IllegalArgumentException.class); + a.newObject(errorType); + a.dup(); + a.loadConstant("Value for property \"" + property.getName() + + "\" cannot be null"); + a.invokeConstructor(errorType, new TypeDesc[] {TypeDesc.STRING}); + a.throwObject(); + + notNull.setLocation(); + } + + a.convert(propType, propType.toPrimitiveType()); + propType = propType.toPrimitiveType(); + } + + if (info.isLob()) { + // Extract locator from RawSupport. + getLobLocator(a, info); + + // Locator is a long, so switch the type to be encoded properly. + propType = TypeDesc.LONG; + } + + // Fill out remaining parameters before calling specific method + // to encode property value. + a.loadLocal(encodedVar); + if (offset == null) { + a.loadConstant(constantOffset); + } else { + a.loadLocal(offset); + } + + boolean descending = + forKey && directions != null && directions[i] == Direction.DESCENDING; + + int amt = encodeProperty(a, propType, forKey, descending); + + if (amt > 0) { + if (i + 1 < properties.length) { + // Only adjust offset if there are more properties. + + if (offset == null) { + constantOffset += amt; + } else { + a.loadConstant(amt); + a.loadLocal(offset); + a.math(Opcode.IADD); + a.storeLocal(offset); + } + } + } else { + if (i + 1 >= properties.length) { + // Don't need to keep track of offset anymore. + a.pop(); + } else { + // Only adjust offset if there are more properties. + if (offset == null) { + if (constantOffset > 0) { + a.loadConstant(constantOffset); + a.math(Opcode.IADD); + } + offset = a.createLocalVariable(null, TypeDesc.INT); + } else { + a.loadLocal(offset); + a.math(Opcode.IADD); + } + a.storeLocal(offset); + } + } + } + + exitPoint.setLocation(); + + return encodedVar; + } + + /** + * Generates code to load a property value onto the operand stack. + * + * @param info info for property to load + * @param ordinal zero-based property ordinal, used only if instanceVar + * refers to an object array. + * @param useReadMethod when true, access property by public read method + * instead of protected field + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are read from the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @param partialStartVar optional variable for supporting partial key + * generation. It must be an int, whose runtime value must be less than the + * properties array length. It marks the range start of the partial + * property range. + * @return true if property was loaded from instance, false if loaded from + * value array + */ + protected boolean loadPropertyValue(CodeAssembler a, + StorablePropertyInfo info, int ordinal, + boolean useReadMethod, + LocalVariable instanceVar, + Class adapterInstanceClass, + LocalVariable partialStartVar) + { + TypeDesc type = info.getPropertyType(); + TypeDesc storageType = info.getStorageType(); + + boolean isObjectArrayInstanceVar = instanceVar != null + && instanceVar.getType() == TypeDesc.forClass(Object[].class); + + boolean useAdapterInstance = adapterInstanceClass != null + && info.getToStorageAdapter() != null + && (useReadMethod || isObjectArrayInstanceVar); + + if (useAdapterInstance) { + // Push adapter instance to stack to be used later. + String fieldName = + info.getPropertyName() + StorableGenerator.ADAPTER_FIELD_ELEMENT + 0; + TypeDesc adapterType = TypeDesc.forClass + (info.getToStorageAdapter().getDeclaringClass()); + a.loadStaticField + (TypeDesc.forClass(adapterInstanceClass), fieldName, adapterType); + } + + if (instanceVar == null) { + a.loadThis(); + if (useReadMethod) { + info.addInvokeReadMethod(a); + } else { + // Access property value directly from protected field of "this". + if (info.getToStorageAdapter() == null) { + a.loadField(info.getPropertyName(), type); + } else { + // Invoke adapter method. + a.invokeVirtual(info.getReadMethodName() + '$', storageType, null); + } + } + } else if (!isObjectArrayInstanceVar) { + a.loadLocal(instanceVar); + if (useReadMethod) { + info.addInvokeReadMethod(a, instanceVar.getType()); + } else { + // Access property value directly from protected field of + // referenced instance. Assumes code is being defined in the + // same package or a subclass. + if (info.getToStorageAdapter() == null) { + a.loadField(instanceVar.getType(), info.getPropertyName(), type); + } else { + // Invoke adapter method. + a.invokeVirtual(instanceVar.getType(), + info.getReadMethodName() + '$', storageType, null); + } + } + } else { + // Access property value from object array. + + a.loadLocal(instanceVar); + a.loadConstant(ordinal); + if (ordinal > 0 && partialStartVar != null) { + a.loadLocal(partialStartVar); + a.math(Opcode.ISUB); + } + + a.loadFromArray(TypeDesc.OBJECT); + a.checkCast(type.toObjectType()); + if (type.isPrimitive()) { + a.convert(type.toObjectType(), type); + } + } + + if (useAdapterInstance) { + // Invoke adapter method on instance pushed earlier. + a.invoke(info.getToStorageAdapter()); + } + + return !isObjectArrayInstanceVar; + } + + /** + * Returns a negative value if encoding is variable. The minimum static + * amount is computed from the one's compliment. Of the types with variable + * encoding lengths, only for primitives is the minimum static amount + * returned more than zero. + */ + private int staticEncodingLength(GenericPropertyInfo info) { + TypeDesc type = info.getStorageType(); + TypeDesc primType = type.toPrimitiveType(); + + if (primType == null) { + if (info.isLob()) { + // Lob locator is stored as a long. + return 8; + } + } else { + if (info.isNullable()) { + // Type is a primitive wrapper. + switch (primType.getTypeCode()) { + case TypeDesc.BYTE_CODE: + return ~1; + case TypeDesc.BOOLEAN_CODE: + return 1; + case TypeDesc.SHORT_CODE: + case TypeDesc.CHAR_CODE: + return ~1; + case TypeDesc.INT_CODE: + return ~1; + case TypeDesc.FLOAT_CODE: + return 4; + case TypeDesc.LONG_CODE: + return ~1; + case TypeDesc.DOUBLE_CODE: + return 8; + } + } else { + // Type is primitive or a required primitive wrapper. + switch (type.getTypeCode()) { + case TypeDesc.BYTE_CODE: + case TypeDesc.BOOLEAN_CODE: + return 1; + case TypeDesc.SHORT_CODE: + case TypeDesc.CHAR_CODE: + return 2; + case TypeDesc.INT_CODE: + case TypeDesc.FLOAT_CODE: + return 4; + case TypeDesc.LONG_CODE: + case TypeDesc.DOUBLE_CODE: + return 8; + } + } + } + + return ~0; + } + + /** + * @param partialStartVar must not be null + */ + private Label[] jumpToPartialEntryPoints(CodeAssembler a, LocalVariable partialStartVar, + int propertyCount) { + // Create all the entry points for offset var, whose locations will be + // set later. + int[] cases = new int[propertyCount]; + Label[] entryPoints = new Label[propertyCount]; + for (int i=0; i> (8 * (3 - i)))); + a.storeToArray(TypeDesc.BYTE); + } + } + } + + /** + * Generates code to push RawSupport instance to the stack. RawSupport is + * available only in Storable instances. If instanceVar is an Object[], a + * SupportException is thrown. + */ + private void pushRawSupport(CodeAssembler a, LocalVariable instanceVar) + throws SupportException + { + boolean isObjectArrayInstanceVar = instanceVar != null + && instanceVar.getType() == TypeDesc.forClass(Object[].class); + + if (isObjectArrayInstanceVar) { + throw new SupportException("Lob properties not supported"); + } + + if (instanceVar == null) { + a.loadThis(); + } else { + a.loadLocal(instanceVar); + } + + a.loadField(StorableGenerator.SUPPORT_FIELD_NAME, + TypeDesc.forClass(TriggerSupport.class)); + a.checkCast(TypeDesc.forClass(RawSupport.class)); + } + + /** + * Generates code to get a Lob locator value from RawSupport. RawSupport + * instance and Lob instance must be on the stack. Result is a long locator + * value on the stack. + */ + private void getLobLocator(CodeAssembler a, StorablePropertyInfo info) { + if (!info.isLob()) { + throw new IllegalArgumentException(); + } + a.invokeInterface(TypeDesc.forClass(RawSupport.class), "getLocator", + TypeDesc.LONG, new TypeDesc[] {info.getStorageType()}); + } + + /** + * Generates code to get a Lob from a locator from RawSupport. RawSupport + * instance and long locator must be on the stack. Result is a Lob on the + * stack, which may be null. + */ + private void getLobFromLocator(CodeAssembler a, StorablePropertyInfo info) { + if (!info.isLob()) { + throw new IllegalArgumentException(); + } + + TypeDesc type = info.getStorageType(); + String name; + if (Blob.class.isAssignableFrom(type.toClass())) { + name = "getBlob"; + } else if (Clob.class.isAssignableFrom(type.toClass())) { + name = "getClob"; + } else { + throw new IllegalArgumentException(); + } + + a.invokeInterface(TypeDesc.forClass(RawSupport.class), name, + type, new TypeDesc[] {TypeDesc.LONG}); + } + + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////// + + private void buildDecoding(boolean forKey, + CodeAssembler a, + StorableProperty[] properties, + Direction[] directions, + LocalVariable instanceVar, + Class adapterInstanceClass, + boolean useWriteMethods, + int generation, + Label altGenerationHandler, + LocalVariable encodedVar) + throws SupportException + { + if (a == null) { + throw new IllegalArgumentException(); + } + if (encodedVar == null || encodedVar.getType() != TypeDesc.forClass(byte[].class)) { + throw new IllegalArgumentException(); + } + + // Decoding order is: + // + // 1. Prefix + // 2. Generation prefix + // 3. Properties + // 4. Suffix + + final int prefix = forKey ? mKeyPrefixPadding : mDataPrefixPadding; + + final int generationPrefix; + if (generation < 0) { + generationPrefix = 0; + } else if (generation < 128) { + generationPrefix = 1; + } else { + generationPrefix = 4; + } + + final int suffix = forKey ? mKeySuffixPadding : mDataSuffixPadding; + + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + + StorablePropertyInfo[] infos = checkSupport(properties); + + decodeGeneration(a, encodedVar, prefix, generation, altGenerationHandler); + + if (properties.length == 1) { + StorableProperty property = properties[0]; + StorablePropertyInfo info = infos[0]; + + if (info.getStorageType().toClass() == byte[].class) { + // Since there is only one property, and it is just a byte + // array, it doesn't have any fancy encoding. + + // Push to stack in preparation for storing a property. + pushDecodingInstanceVar(a, 0, instanceVar); + + a.loadLocal(encodedVar); + + boolean descending = + forKey && directions != null && directions[0] == Direction.DESCENDING; + + TypeDesc[] params; + if (prefix > 0 || generationPrefix > 0 || suffix > 0) { + a.loadConstant(prefix + generationPrefix); + a.loadConstant(suffix); + params = new TypeDesc[] {byteArrayType, TypeDesc.INT, TypeDesc.INT}; + } else { + params = new TypeDesc[] {byteArrayType}; + } + + if (property.isNullable()) { + if (descending) { + a.invokeStatic(KeyDecoder.class.getName(), "decodeSingleNullableDesc", + byteArrayType, params); + } else { + a.invokeStatic(DataDecoder.class.getName(), "decodeSingleNullable", + byteArrayType, params); + } + } else if (descending) { + a.invokeStatic(KeyDecoder.class.getName(), "decodeSingleDesc", + byteArrayType, params); + } else if (prefix > 0 || generationPrefix > 0 || suffix > 0) { + a.invokeStatic(DataDecoder.class.getName(), "decodeSingle", + byteArrayType, params); + } else { + // Just store raw property value. + } + + storePropertyValue(a, info, useWriteMethods, instanceVar, adapterInstanceClass); + return; + } + } + + // Now decode from the byte array. + + int constantOffset = prefix + generationPrefix; + LocalVariable offset = null; + // References to local variables which will hold references. + LocalVariable[] stringRef = new LocalVariable[1]; + LocalVariable[] byteArrayRef = new LocalVariable[1]; + LocalVariable[] valueRefRef = new LocalVariable[1]; + + for (int i=0; i 0) { + if (offset == null) { + constantOffset += amt; + } else { + a.loadConstant(amt); + a.loadLocal(offset); + a.math(Opcode.IADD); + a.storeLocal(offset); + } + } else { + // Offset adjust is one if returned object is null. + a.dup(); + Label notNull = a.createLabel(); + a.ifNullBranch(notNull, false); + a.loadConstant(1 + (offset == null ? constantOffset : 0)); + Label cont = a.createLabel(); + a.branch(cont); + notNull.setLocation(); + a.loadConstant(~amt + (offset == null ? constantOffset : 0)); + cont.setLocation(); + + if (offset == null) { + offset = a.createLocalVariable(null, TypeDesc.INT); + } else { + a.loadLocal(offset); + a.math(Opcode.IADD); + } + a.storeLocal(offset); + } + } + } else { + if (i + 1 >= properties.length) { + // Don't need to keep track of offset anymore. + a.pop(); + } else { + // Only adjust offset if there are more properties. + if (offset == null) { + if (constantOffset > 0) { + a.loadConstant(constantOffset); + a.math(Opcode.IADD); + } + offset = a.createLocalVariable(null, TypeDesc.INT); + } else { + a.loadLocal(offset); + a.math(Opcode.IADD); + } + a.storeLocal(offset); + } + + // Get the value out of the ref array so that it can be stored. + a.loadLocal(valueRefRef[0]); + a.loadConstant(0); + a.loadFromArray(valueRefRef[0].getType()); + } + + storePropertyValue(a, info, useWriteMethods, instanceVar, adapterInstanceClass); + } + } + + /** + * Generates code that calls a decoding method in DataDecoder or + * KeyDecoder. Parameters must already be on the stack. + * + * @return 0 if an int amount is pushed onto the stack, or a positive value + * if offset adjust amount is constant, or a negative value if offset + * adjust is constant or one more + */ + private int decodeProperty(CodeAssembler a, + GenericPropertyInfo info, TypeDesc storageType, + boolean forKey, boolean descending, + LocalVariable[] stringRefRef, LocalVariable[] byteArrayRefRef, + LocalVariable[] valueRefRef) + throws SupportException + { + TypeDesc primType = storageType.toPrimitiveType(); + + if (primType != null) { + String methodName; + TypeDesc returnType; + int adjust; + + if (primType != storageType && info.isNullable()) { + // Property type is a nullable boxed primitive. + returnType = storageType; + + switch (primType.getTypeCode()) { + case TypeDesc.BYTE_CODE: + methodName = "decodeByteObj"; + adjust = ~2; + break; + case TypeDesc.BOOLEAN_CODE: + methodName = "decodeBooleanObj"; + adjust = 1; + break; + case TypeDesc.SHORT_CODE: + methodName = "decodeShortObj"; + adjust = ~3; + break; + case TypeDesc.CHAR_CODE: + methodName = "decodeCharacterObj"; + adjust = ~3; + break; + default: + case TypeDesc.INT_CODE: + methodName = "decodeIntegerObj"; + adjust = ~5; + break; + case TypeDesc.FLOAT_CODE: + methodName = "decodeFloatObj"; + adjust = 4; + break; + case TypeDesc.LONG_CODE: + methodName = "decodeLongObj"; + adjust = ~9; + break; + case TypeDesc.DOUBLE_CODE: + methodName = "decodeDoubleObj"; + adjust = 8; + break; + } + } else { + // Property type is a primitive or a boxed primitive. + returnType = primType; + + switch (primType.getTypeCode()) { + case TypeDesc.BYTE_CODE: + methodName = "decodeByte"; + adjust = 1; + break; + case TypeDesc.BOOLEAN_CODE: + methodName = "decodeBoolean"; + adjust = 1; + break; + case TypeDesc.SHORT_CODE: + methodName = "decodeShort"; + adjust = 2; + break; + case TypeDesc.CHAR_CODE: + methodName = "decodeChar"; + adjust = 2; + break; + default: + case TypeDesc.INT_CODE: + methodName = "decodeInt"; + adjust = 4; + break; + case TypeDesc.FLOAT_CODE: + methodName = "decodeFloat"; + adjust = 4; + break; + case TypeDesc.LONG_CODE: + methodName = "decodeLong"; + adjust = 8; + break; + case TypeDesc.DOUBLE_CODE: + methodName = "decodeDouble"; + adjust = 8; + break; + } + } + + TypeDesc[] params = {TypeDesc.forClass(byte[].class), TypeDesc.INT}; + if (forKey && descending) { + a.invokeStatic + (KeyDecoder.class.getName(), methodName + "Desc", returnType, params); + } else { + a.invokeStatic + (DataDecoder.class.getName(), methodName, returnType, params); + } + + if (returnType.isPrimitive()) { + if (!storageType.isPrimitive()) { + // Wrap it. + a.convert(returnType, storageType); + } + } + + return adjust; + } else { + String className = (forKey ? KeyDecoder.class : DataDecoder.class).getName(); + String methodName; + TypeDesc refType; + + if (storageType == TypeDesc.STRING) { + methodName = (forKey && descending) ? "decodeStringDesc" : "decodeString"; + refType = TypeDesc.forClass(String[].class); + if (stringRefRef[0] == null) { + stringRefRef[0] = a.createLocalVariable(null, refType); + a.loadConstant(1); + a.newObject(refType); + a.storeLocal(stringRefRef[0]); + } + a.loadLocal(stringRefRef[0]); + valueRefRef[0] = stringRefRef[0]; + } else if (storageType.toClass() == byte[].class) { + methodName = (forKey && descending) ? "decodeDesc" : "decode"; + refType = TypeDesc.forClass(byte[][].class); + if (byteArrayRefRef[0] == null) { + byteArrayRefRef[0] = a.createLocalVariable(null, refType); + a.loadConstant(1); + a.newObject(refType); + a.storeLocal(byteArrayRefRef[0]); + } + a.loadLocal(byteArrayRefRef[0]); + valueRefRef[0] = byteArrayRefRef[0]; + } else { + throw notSupported(info.getPropertyName(), storageType.getFullName()); + } + + TypeDesc[] params = {TypeDesc.forClass(byte[].class), TypeDesc.INT, refType}; + a.invokeStatic(className, methodName, TypeDesc.INT, params); + + return 0; + } + } + + /** + * Push decoding instanceVar to stack in preparation to calling + * storePropertyValue. + * + * @param ordinal zero-based property ordinal, used only if instanceVar + * refers to an object array. + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are written to the runtime value of this array instead + * of a Storable instance. + * @see #storePropertyValue storePropertyValue + */ + protected void pushDecodingInstanceVar(CodeAssembler a, int ordinal, + LocalVariable instanceVar) { + if (instanceVar == null) { + // Push this to stack in preparation for storing a property. + a.loadThis(); + } else if (instanceVar.getType() != TypeDesc.forClass(Object[].class)) { + // Push reference to stack in preparation for storing a property. + a.loadLocal(instanceVar); + } else { + // Push array and index to stack in preparation for storing a property. + a.loadLocal(instanceVar); + a.loadConstant(ordinal); + } + } + + /** + * Generates code to store a property value into an instance which is + * already on the operand stack. If instance is an Object array, index into + * array must also be on the operand stack. + * + * @param info info for property to store to + * @param useWriteMethod when true, set property by public write method + * instead of protected field + * @param instanceVar local variable referencing Storable instance, + * defaults to "this" if null. If variable type is an Object array, then + * property values are written to the runtime value of this array instead + * of a Storable instance. + * @param adapterInstanceClass class containing static references to + * adapter instances - defaults to instanceVar + * @see #pushDecodingInstanceVar pushDecodingInstanceVar + */ + protected void storePropertyValue(CodeAssembler a, StorablePropertyInfo info, + boolean useWriteMethod, + LocalVariable instanceVar, + Class adapterInstanceClass) { + TypeDesc type = info.getPropertyType(); + TypeDesc storageType = info.getStorageType(); + + boolean isObjectArrayInstanceVar = instanceVar != null + && instanceVar.getType() == TypeDesc.forClass(Object[].class); + + boolean useAdapterInstance = adapterInstanceClass != null + && info.getToStorageAdapter() != null + && (useWriteMethod || isObjectArrayInstanceVar); + + if (useAdapterInstance) { + // Push adapter instance to adapt property value. It must be on the + // stack before the property value, so swap. + + // Store unadapted property to temp var in order to be swapped. + LocalVariable temp = a.createLocalVariable(null, storageType); + a.storeLocal(temp); + + String fieldName = + info.getPropertyName() + StorableGenerator.ADAPTER_FIELD_ELEMENT + 0; + TypeDesc adapterType = TypeDesc.forClass + (info.getToStorageAdapter().getDeclaringClass()); + a.loadStaticField + (TypeDesc.forClass(adapterInstanceClass), fieldName, adapterType); + + a.loadLocal(temp); + a.invoke(info.getFromStorageAdapter()); + + // Stack now contains property adapted to its publicly declared type. + } + + if (instanceVar == null) { + if (useWriteMethod) { + info.addInvokeWriteMethod(a); + } else { + // Set property value directly to protected field of instance. + if (info.getToStorageAdapter() == null) { + a.storeField(info.getPropertyName(), type); + } else { + // Invoke adapter method. + a.invokeVirtual(info.getWriteMethodName() + '$', + null, new TypeDesc[] {storageType}); + } + } + } else if (!isObjectArrayInstanceVar) { + TypeDesc instanceVarType = instanceVar.getType(); + + // Drop properties that are missing or whose types are incompatible. + doDrop: { + Class instanceVarClass = instanceVarType.toClass(); + if (instanceVarClass != null) { + Map props = + BeanIntrospector.getAllProperties(instanceVarClass); + BeanProperty prop = props.get(info.getPropertyName()); + if (prop != null) { + if (prop.getType() == type.toClass()) { + break doDrop; + } + // Types differ, but if primitive types, perform conversion. + TypeDesc primType = type.toPrimitiveType(); + if (primType != null) { + TypeDesc propType = TypeDesc.forClass(prop.getType()); + TypeDesc primPropType = propType.toPrimitiveType(); + if (primPropType != null) { + // Apply conversion and store property. + a.convert(type, propType); + type = propType; + break doDrop; + } + } + } + } + + // Drop missing or incompatible property. + if (storageType.isDoubleWord()) { + a.pop2(); + } else { + a.pop(); + } + return; + } + + if (useWriteMethod) { + info.addInvokeWriteMethod(a, instanceVarType); + } else { + // Set property value directly to protected field of referenced + // instance. Assumes code is being defined in the same package + // or a subclass. + if (info.getToStorageAdapter() == null) { + a.storeField(instanceVarType, info.getPropertyName(), type); + } else { + // Invoke adapter method. + a.invokeVirtual(instanceVarType, info.getWriteMethodName() + '$', + null, new TypeDesc[] {storageType}); + } + } + } else { + // Set property value to object array. No need to check if we + // should call a write method because arrays don't have write + // methods. + if (type.isPrimitive()) { + a.convert(type, type.toObjectType()); + } + a.storeToArray(TypeDesc.OBJECT); + } + } + + /** + * Generates code that ensures a matching generation value exists in the + * byte array referenced by the local variable, throwing a + * CorruptEncodingException otherwise. + * + * @param generation if less than zero, no code is generated + */ + private void decodeGeneration(CodeAssembler a, LocalVariable encodedVar, + int offset, int generation, Label altGenerationHandler) + { + if (offset < 0) { + throw new IllegalArgumentException(); + } + if (generation < 0) { + return; + } + + LocalVariable actualGeneration = a.createLocalVariable(null, TypeDesc.INT); + a.loadLocal(encodedVar); + a.loadConstant(offset); + a.loadFromArray(TypeDesc.BYTE); + a.storeLocal(actualGeneration); + a.loadLocal(actualGeneration); + Label compareGeneration = a.createLabel(); + a.ifZeroComparisonBranch(compareGeneration, ">="); + + // Decode four byte generation format. + a.loadLocal(actualGeneration); + a.loadConstant(24); + a.math(Opcode.ISHL); + a.loadConstant(0x7fffffff); + a.math(Opcode.IAND); + for (int i=1; i<4; i++) { + a.loadLocal(encodedVar); + a.loadConstant(offset + i); + a.loadFromArray(TypeDesc.BYTE); + a.loadConstant(0xff); + a.math(Opcode.IAND); + int shift = 8 * (3 - i); + if (shift > 0) { + a.loadConstant(shift); + a.math(Opcode.ISHL); + } + a.math(Opcode.IOR); + } + a.storeLocal(actualGeneration); + + compareGeneration.setLocation(); + + a.loadConstant(generation); + a.loadLocal(actualGeneration); + Label generationMatches = a.createLabel(); + a.ifComparisonBranch(generationMatches, "=="); + + if (altGenerationHandler != null) { + a.loadLocal(actualGeneration); + a.branch(altGenerationHandler); + } else { + // Throw CorruptEncodingException. + + TypeDesc corruptEncodingEx = TypeDesc.forClass(CorruptEncodingException.class); + a.newObject(corruptEncodingEx); + a.dup(); + a.loadConstant(generation); // expected generation + a.loadLocal(actualGeneration); // actual generation + a.invokeConstructor(corruptEncodingEx, new TypeDesc[] {TypeDesc.INT, TypeDesc.INT}); + a.throwObject(); + } + + generationMatches.setLocation(); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/GenericInstanceFactory.java b/src/main/java/com/amazon/carbonado/spi/raw/GenericInstanceFactory.java new file mode 100644 index 0000000..5a6a4cb --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/GenericInstanceFactory.java @@ -0,0 +1,36 @@ +/* + * 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.spi.raw; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; + +/** + * Can be used with {@link com.amazon.carbonado.util.QuickConstructorGenerator} + * for instantiating generic storable instances. + * + * @author Brian S O'Neill + */ +public interface GenericInstanceFactory { + Storable instantiate(RawSupport support); + + Storable instantiate(RawSupport support, byte[] key, byte[] value) + throws FetchException; +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/GenericPropertyInfo.java b/src/main/java/com/amazon/carbonado/spi/raw/GenericPropertyInfo.java new file mode 100644 index 0000000..c734f03 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/GenericPropertyInfo.java @@ -0,0 +1,60 @@ +/* + * 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.spi.raw; + +import java.lang.reflect.Method; + +import org.cojen.classfile.TypeDesc; + +/** + * Minimal information required by {@link GenericEncodingStrategy} to encode + * and decode a storable property or layout property. + * + * @author Brian S O'Neill + */ +public interface GenericPropertyInfo { + String getPropertyName(); + + /** + * Returns the user specified property type. + */ + TypeDesc getPropertyType(); + + /** + * Returns the storage supported type. If it differs from the property + * type, then adapter methods must also exist. + */ + TypeDesc getStorageType(); + + boolean isNullable(); + + boolean isLob(); + + /** + * Returns the optional method used to adapt the property from the + * storage supported type to the user visible type. + */ + Method getFromStorageAdapter(); + + /** + * Returns the optional method used to adapt the property from the user + * visible type to the storage supported type. + */ + Method getToStorageAdapter(); +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodec.java b/src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodec.java new file mode 100644 index 0000000..7a98540 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodec.java @@ -0,0 +1,813 @@ +/* + * 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.spi.raw; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Map; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.Label; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.ClassInjector; +import org.cojen.util.IntHashMap; +import org.cojen.util.KeyFactory; +import org.cojen.util.SoftValuedHashMap; + +import com.amazon.carbonado.CorruptEncodingException; +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.FetchNoneException; +import com.amazon.carbonado.RepositoryException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; +import com.amazon.carbonado.info.Direction; +import com.amazon.carbonado.info.OrderedProperty; +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.layout.Layout; +import com.amazon.carbonado.spi.CodeBuilderUtil; +import com.amazon.carbonado.util.ThrowUnchecked; +import com.amazon.carbonado.util.QuickConstructorGenerator; + +/** + * Generic codec that supports any kind of storable by auto-generating and + * caching storable implementations. + * + * @author Brian S O'Neill + * @see GenericStorableCodecFactory + */ +public class GenericStorableCodec implements StorableCodec { + private static final String BLANK_KEY_FIELD_NAME = "blankKey$"; + private static final String CODEC_FIELD_NAME = "codec$"; + private static final String ASSIGN_CODEC_METHOD_NAME = "assignCodec$"; + + // Maps GenericEncodingStrategy instances to GenericStorableCodec instances. + private static final Map cCache = new SoftValuedHashMap(); + + /** + * Returns an instance of the codec. The Storable type itself may be an + * interface or a class. If it is a class, then it must not be final, and + * it must have a public, no-arg constructor. + * + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, encode a storable layout generation + * value in one or four bytes. Generation 0..127 is encoded in one byte, and + * 128..max is encoded in four bytes, with the most significant bit set. + * @throws SupportException if Storable is not supported + * @throws amazon.carbonado.MalformedTypeException if Storable type is not well-formed + * @throws IllegalArgumentException if type is null + */ + @SuppressWarnings("unchecked") + static synchronized GenericStorableCodec getInstance + (GenericStorableCodecFactory factory, + GenericEncodingStrategy encodingStrategy, boolean isMaster, Layout layout) + throws SupportException + { + Object key; + if (layout == null) { + key = KeyFactory.createKey(new Object[] {encodingStrategy, isMaster}); + } else { + key = KeyFactory.createKey + (new Object[] {encodingStrategy, isMaster, factory, layout.getGeneration()}); + } + + GenericStorableCodec codec = (GenericStorableCodec) cCache.get(key); + if (codec == null) { + codec = new GenericStorableCodec + (factory, + encodingStrategy.getType(), + generateStorable(encodingStrategy, isMaster, layout), + encodingStrategy, + layout); + cCache.put(key, codec); + } + + return codec; + } + + @SuppressWarnings("unchecked") + private static Class generateStorable + (GenericEncodingStrategy encodingStrategy, boolean isMaster, Layout layout) + throws SupportException + { + final Class storableClass = encodingStrategy.getType(); + final Class abstractClass = + RawStorableGenerator.getAbstractClass(storableClass, isMaster); + final int generation = layout == null ? -1 : layout.getGeneration(); + + ClassInjector ci = ClassInjector.create + (storableClass.getName(), abstractClass.getClassLoader()); + + ClassFile cf = new ClassFile(ci.getClassName(), abstractClass); + cf.markSynthetic(); + cf.setSourceFile(GenericStorableCodec.class.getName()); + cf.setTarget("1.5"); + + // Declare some types. + final TypeDesc storageType = TypeDesc.forClass(Storage.class); + final TypeDesc rawSupportType = TypeDesc.forClass(RawSupport.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + final TypeDesc[] byteArrayParam = {byteArrayType}; + final TypeDesc codecType = TypeDesc.forClass(GenericStorableCodec.class); + final TypeDesc decoderType = TypeDesc.forClass(Decoder.class); + final TypeDesc weakRefType = TypeDesc.forClass(WeakReference.class); + + // If Layout provided, then keep a (weak) static reference to this + // GenericStorableCodec in order to get decoders for different + // generations. It is assigned a value after the class is loaded via a + // public static method. It can only be assigned once. + if (layout != null) { + cf.addField(Modifiers.PRIVATE.toStatic(true), CODEC_FIELD_NAME, weakRefType); + MethodInfo mi = cf.addMethod(Modifiers.PUBLIC.toStatic(true), ASSIGN_CODEC_METHOD_NAME, + null, new TypeDesc[] {weakRefType}); + CodeBuilder b = new CodeBuilder(mi); + b.loadStaticField(CODEC_FIELD_NAME, weakRefType); + Label done = b.createLabel(); + b.ifNullBranch(done, false); + b.loadLocal(b.getParameter(0)); + b.storeStaticField(CODEC_FIELD_NAME, weakRefType); + done.setLocation(); + b.returnVoid(); + } + + // Add constructor that accepts a RawSupport. + { + TypeDesc[] params = {rawSupportType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeSuperConstructor(params); + b.returnVoid(); + } + + // Add constructor that accepts a RawSupport, an encoded key, and an + // encoded data. + { + TypeDesc[] params = {rawSupportType, byteArrayType, byteArrayType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.loadLocal(b.getParameter(1)); + b.loadLocal(b.getParameter(2)); + b.invokeSuperConstructor(params); + b.returnVoid(); + } + + // Implement protected abstract methods inherited from parent class. + + // byte[] encodeKey() + { + // Encode the primary key into a byte array that supports correct + // ordering. No special key comparator is needed. + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.ENCODE_KEY_METHOD_NAME, + byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + // TODO: Consider caching generated key. Rebuild if null or if pk is dirty. + + // assembler = b + // properties = null (defaults to all key properties) + // instanceVar = null (null means "this") + // adapterInstanceClass = null (null means use instanceVar, in this case is "this") + // useReadMethods = false (will read fields directly) + // partialStartVar = null (only support encoding all properties) + // partialEndVar = null (only support encoding all properties) + LocalVariable encodedVar = + encodingStrategy.buildKeyEncoding(b, null, null, null, false, null, null); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } + + // byte[] encodeData() + { + // Encoding non-primary key data properties. + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.ENCODE_DATA_METHOD_NAME, + byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + // assembler = b + // properties = null (defaults to all non-key properties) + // instanceVar = null (null means "this") + // adapterInstanceClass = null (null means use instanceVar, in this case is "this") + // useReadMethods = false (will read fields directly) + // generation = generation + LocalVariable encodedVar = + encodingStrategy.buildDataEncoding(b, null, null, null, false, generation); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } + + // void decodeKey(byte[]) + { + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.DECODE_KEY_METHOD_NAME, + null, byteArrayParam); + CodeBuilder b = new CodeBuilder(mi); + + // assembler = b + // properties = null (defaults to all key properties) + // instanceVar = null (null means "this") + // adapterInstanceClass = null (null means use instanceVar, in this case is "this") + // useWriteMethods = false (will set fields directly) + // encodedVar = references byte array with encoded key + encodingStrategy.buildKeyDecoding(b, null, null, null, false, b.getParameter(0)); + + b.returnVoid(); + } + + // void decodeData(byte[]) + { + MethodInfo mi = cf.addMethod(Modifiers.PROTECTED, + RawStorableGenerator.DECODE_DATA_METHOD_NAME, + null, byteArrayParam); + CodeBuilder b = new CodeBuilder(mi); + Label altGenerationHandler = b.createLabel(); + + // assembler = b + // properties = null (defaults to all non-key properties) + // instanceVar = null (null means "this") + // adapterInstanceClass = null (null means use instanceVar, in this case is "this") + // useWriteMethods = false (will set fields directly) + // generation = generation + // altGenerationHandler = altGenerationHandler + // encodedVar = references byte array with encoded data + encodingStrategy.buildDataDecoding + (b, null, null, null, false, generation, altGenerationHandler, b.getParameter(0)); + + b.returnVoid(); + + // Support decoding alternate generation. + + altGenerationHandler.setLocation(); + LocalVariable actualGeneration = b.createLocalVariable(null, TypeDesc.INT); + b.storeLocal(actualGeneration); + + b.loadStaticField(CODEC_FIELD_NAME, weakRefType); + b.invokeVirtual(weakRefType, "get", TypeDesc.OBJECT, null); + b.dup(); + Label haveCodec = b.createLabel(); + b.ifNullBranch(haveCodec, false); + + // Codec got reclaimed, which is unlikely to happen during normal + // use since it must be referenced by the storage object. + b.pop(); // Don't need the duped codec instance. + CodeBuilderUtil.throwException(b, IllegalStateException.class, "Codec missing"); + + haveCodec.setLocation(); + b.checkCast(codecType); + b.loadLocal(actualGeneration); + b.invokeVirtual(codecType, "getDecoder", decoderType, new TypeDesc[] {TypeDesc.INT}); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeInterface(decoderType, "decode", null, + new TypeDesc[] {TypeDesc.forClass(Storable.class), byteArrayType}); + + b.returnVoid(); + } + + return ci.defineClass(cf); + } + + private final GenericStorableCodecFactory mFactory; + + private final Class mType; + + private final Class mStorableClass; + + // Weakly reference the encoding strategy because of the way + // GenericStorableCodec instances are cached in a SoftValuedHashMap. + // GenericStorableCodec can still be reclaimed by the garbage collector. + private final WeakReference> mEncodingStrategy; + + private final GenericInstanceFactory mInstanceFactory; + + private final SearchKeyFactory mPrimaryKeyFactory; + + // Maps OrderedProperty[] keys to SearchKeyFactory instances. + private final Map mSearchKeyFactories = new SoftValuedHashMap(); + + private final Layout mLayout; + + // Maps layout generations to Decoders. + private IntHashMap mDecoders; + + private GenericStorableCodec(GenericStorableCodecFactory factory, + Class type, Class storableClass, + GenericEncodingStrategy encodingStrategy, + Layout layout) { + mFactory = factory; + mType = type; + mStorableClass = storableClass; + mEncodingStrategy = new WeakReference>(encodingStrategy); + mInstanceFactory = QuickConstructorGenerator + .getInstance(storableClass, GenericInstanceFactory.class); + mPrimaryKeyFactory = getSearchKeyFactory(encodingStrategy.gatherAllKeyProperties()); + mLayout = layout; + + if (layout != null) { + try { + // Assign static reference back to this codec. + Method m = storableClass.getMethod + (ASSIGN_CODEC_METHOD_NAME, WeakReference.class); + m.invoke(null, new WeakReference(this)); + } catch (Exception e) { + ThrowUnchecked.fireFirstDeclaredCause(e); + } + } + } + + /** + * Returns the type of Storable that code is generated for. + */ + public final Class getStorableType() { + return mType; + } + + /** + * Instantiate a Storable with no key or value defined yet. + * + * @param support binds generated storable with a storage layer + */ + @SuppressWarnings("unchecked") + public S instantiate(RawSupport support) { + return (S) mInstanceFactory.instantiate(support); + } + + /** + * Instantiate a Storable with a specific key and value. + * + * @param support binds generated storable with a storage layer + */ + @SuppressWarnings("unchecked") + public S instantiate(RawSupport support, byte[] key, byte[] value) + throws FetchException + { + return (S) mInstanceFactory.instantiate(support, key, value); + } + + public StorableIndex getPrimaryKeyIndex() { + return getEncodingStrategy().getPrimaryKeyIndex(); + } + + public int getPrimaryKeyPrefixLength() { + return getEncodingStrategy().getConstantKeyPrefixLength(); + } + + public byte[] encodePrimaryKey(S storable) { + return mPrimaryKeyFactory.encodeSearchKey(storable); + } + + public byte[] encodePrimaryKey(S storable, int rangeStart, int rangeEnd) { + return mPrimaryKeyFactory.encodeSearchKey(storable, rangeStart, rangeEnd); + } + + public byte[] encodePrimaryKey(Object[] values) { + return mPrimaryKeyFactory.encodeSearchKey(values); + } + + public byte[] encodePrimaryKey(Object[] values, int rangeStart, int rangeEnd) { + return mPrimaryKeyFactory.encodeSearchKey(values, rangeStart, rangeEnd); + } + + public byte[] encodePrimaryKeyPrefix() { + return mPrimaryKeyFactory.encodeSearchKeyPrefix(); + } + + /** + * Returns a concrete Storable implementation, which is fully + * thread-safe. It has two constructors defined: + * + *

+     * public <init>(Storage, RawSupport);
+     *
+     * public <init>(Storage, RawSupport, byte[] key, byte[] value);
+     * 
+ * + * Convenience methods are provided in this class to instantiate the + * generated Storable. + */ + public Class getStorableClass() { + return mStorableClass; + } + + /** + * Returns a search key factory, which is useful for implementing indexes + * and queries. + * + * @param properties properties to build the search key from + */ + @SuppressWarnings("unchecked") + public SearchKeyFactory getSearchKeyFactory(OrderedProperty[] properties) { + // This KeyFactory makes arrays work as hashtable keys. + Object key = org.cojen.util.KeyFactory.createKey(properties); + + synchronized (mSearchKeyFactories) { + SearchKeyFactory factory = (SearchKeyFactory) mSearchKeyFactories.get(key); + if (factory == null) { + factory = generateSearchKeyFactory(properties); + mSearchKeyFactories.put(key, factory); + } + return factory; + } + } + + /** + * Returns a data decoder for the given generation. + * + * @throws FetchNoneException if generation is unknown + */ + public Decoder getDecoder(int generation) throws FetchNoneException, FetchException { + try { + synchronized (mLayout) { + IntHashMap decoders = mDecoders; + if (decoders == null) { + mDecoders = decoders = new IntHashMap(); + } + Decoder decoder = (Decoder) decoders.get(generation); + if (decoder == null) { + decoder = generateDecoder(generation); + mDecoders.put(generation, decoder); + } + return decoder; + } + } catch (NullPointerException e) { + if (mLayout == null) { + throw new FetchNoneException("Layout evolution not supported"); + } + throw e; + } + } + + private GenericEncodingStrategy getEncodingStrategy() { + // Should never be null, even though it is weakly referenced. As long + // as this class can be reached by the cache, the encoding strategy + // object exists since it is the cache key. + return mEncodingStrategy.get(); + } + + @SuppressWarnings("unchecked") + private SearchKeyFactory generateSearchKeyFactory(OrderedProperty[] properties) { + GenericEncodingStrategy encodingStrategy = getEncodingStrategy(); + + ClassInjector ci; + { + StringBuilder b = new StringBuilder(); + b.append(mType.getName()); + b.append('$'); + for (OrderedProperty property : properties) { + if (property.getDirection() == Direction.UNSPECIFIED) { + property = property.direction(Direction.ASCENDING); + } + try { + property.appendTo(b); + } catch (java.io.IOException e) { + // Not gonna happen + } + } + String prefix = b.toString(); + ci = ClassInjector.create(prefix, mStorableClass.getClassLoader()); + } + + ClassFile cf = new ClassFile(ci.getClassName()); + cf.addInterface(SearchKeyFactory.class); + cf.markSynthetic(); + cf.setSourceFile(GenericStorableCodec.class.getName()); + cf.setTarget("1.5"); + + // Add public no-arg constructor. + cf.addDefaultConstructor(); + + // Declare some types. + final TypeDesc storableType = TypeDesc.forClass(Storable.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + final TypeDesc objectArrayType = TypeDesc.forClass(Object[].class); + final TypeDesc instanceType = TypeDesc.forClass(mStorableClass); + + // Define encodeSearchKey(Storable). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKey", byteArrayType, + new TypeDesc[] {storableType}); + CodeBuilder b = new CodeBuilder(mi); + b.loadLocal(b.getParameter(0)); + b.checkCast(instanceType); + LocalVariable instanceVar = b.createLocalVariable(null, instanceType); + b.storeLocal(instanceVar); + + // assembler = b + // properties = properties to encode + // instanceVar = instanceVar which references storable instance + // adapterInstanceClass = null (null means use instanceVar) + // useReadMethods = false (will read fields directly) + // partialStartVar = null (only support encoding all properties) + // partialEndVar = null (only support encoding all properties) + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, properties, instanceVar, null, false, null, null); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + // Define encodeSearchKey(Storable, int, int). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKey", byteArrayType, + new TypeDesc[] {storableType, TypeDesc.INT, TypeDesc.INT}); + CodeBuilder b = new CodeBuilder(mi); + b.loadLocal(b.getParameter(0)); + b.checkCast(instanceType); + LocalVariable instanceVar = b.createLocalVariable(null, instanceType); + b.storeLocal(instanceVar); + + // assembler = b + // properties = properties to encode + // instanceVar = instanceVar which references storable instance + // adapterInstanceClass = null (null means use instanceVar) + // useReadMethods = false (will read fields directly) + // partialStartVar = int parameter 1, references start property index + // partialEndVar = int parameter 2, references end property index + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, properties, instanceVar, null, false, b.getParameter(1), b.getParameter(2)); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + // The Storable class that we generated earlier is a subclass of the + // abstract class defined by StorableGenerator. StorableGenerator + // creates static final adapter instances, with protected + // access. Calling getSuperclass results in the exact class that + // StorableGenerator made, which is where the fields are. + final Class adapterInstanceClass = getStorableClass().getSuperclass(); + + // Define encodeSearchKey(Object[] values). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKey", byteArrayType, + new TypeDesc[] {objectArrayType}); + CodeBuilder b = new CodeBuilder(mi); + + // assembler = b + // properties = properties to encode + // instanceVar = parameter 0, an object array + // adapterInstanceClass = adapterInstanceClass - see comment above + // useReadMethods = false (will read fields directly) + // partialStartVar = null (only support encoding all properties) + // partialEndVar = null (only support encoding all properties) + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, properties, b.getParameter(0), adapterInstanceClass, false, null, null); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + // Define encodeSearchKey(Object[] values, int, int). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKey", byteArrayType, + new TypeDesc[] {objectArrayType, TypeDesc.INT, TypeDesc.INT}); + CodeBuilder b = new CodeBuilder(mi); + + // assembler = b + // properties = properties to encode + // instanceVar = parameter 0, an object array + // adapterInstanceClass = adapterInstanceClass - see comment above + // useReadMethods = false (will read fields directly) + // partialStartVar = int parameter 1, references start property index + // partialEndVar = int parameter 2, references end property index + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, properties, b.getParameter(0), adapterInstanceClass, + false, b.getParameter(1), b.getParameter(2)); + + b.loadLocal(encodedVar); + b.returnValue(byteArrayType); + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + // Define encodeSearchKeyPrefix(). + try { + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "encodeSearchKeyPrefix", byteArrayType, null); + CodeBuilder b = new CodeBuilder(mi); + + if (encodingStrategy.getKeyPrefixPadding() == 0 && + encodingStrategy.getKeySuffixPadding() == 0) { + // Return null instead of a zero-length array. + b.loadNull(); + b.returnValue(byteArrayType); + } else { + // Build array once and re-use. Trust that no one modifies it. + cf.addField(Modifiers.PRIVATE.toStatic(true).toFinal(true), + BLANK_KEY_FIELD_NAME, byteArrayType); + b.loadStaticField(BLANK_KEY_FIELD_NAME, byteArrayType); + b.returnValue(byteArrayType); + + // Create static initializer to set field. + mi = cf.addInitializer(); + b = new CodeBuilder(mi); + + // assembler = b + // properties = no parameters - we just want the key prefix + // instanceVar = null (no parameters means we don't need this) + // adapterInstanceClass = null (no parameters means we don't need this) + // useReadMethods = false (no parameters means we don't need this) + // partialStartVar = null (no parameters means we don't need this) + // partialEndVar = null (no parameters means we don't need this) + LocalVariable encodedVar = encodingStrategy.buildKeyEncoding + (b, new OrderedProperty[0], null, null, false, null, null); + + b.loadLocal(encodedVar); + b.storeStaticField(BLANK_KEY_FIELD_NAME, byteArrayType); + b.returnVoid(); + } + } catch (SupportException e) { + // Shouldn't happen since all properties were checked in order + // to create this StorableCodec. + throw new UndeclaredThrowableException(e); + } + + Class clazz = ci.defineClass(cf); + try { + return clazz.newInstance(); + } catch (InstantiationException e) { + throw new UndeclaredThrowableException(e); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } + } + + private Decoder generateDecoder(int generation) throws FetchException { + // Create an encoding strategy against the reconstructed storable. + GenericEncodingStrategy altStrategy; + try { + Class altStorable = mLayout.getGeneration(generation) + .reconstruct(mStorableClass.getClassLoader()); + altStrategy = mFactory.createStrategy(altStorable, null); + } catch (RepositoryException e) { + throw new CorruptEncodingException(e); + } + + ClassInjector ci = ClassInjector.create(mType.getName(), mStorableClass.getClassLoader()); + ClassFile cf = new ClassFile(ci.getClassName()); + cf.addInterface(Decoder.class); + cf.markSynthetic(); + cf.setSourceFile(GenericStorableCodec.class.getName()); + cf.setTarget("1.5"); + + // Add public no-arg constructor. + cf.addDefaultConstructor(); + + // Declare some types. + final TypeDesc storableType = TypeDesc.forClass(Storable.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + + // Define the required decode method. + MethodInfo mi = cf.addMethod + (Modifiers.PUBLIC, "decode", null, new TypeDesc[] {storableType, byteArrayType}); + CodeBuilder b = new CodeBuilder(mi); + + LocalVariable uncastDestVar = b.getParameter(0); + b.loadLocal(uncastDestVar); + LocalVariable destVar = b.createLocalVariable(null, TypeDesc.forClass(mStorableClass)); + b.checkCast(destVar.getType()); + b.storeLocal(destVar); + LocalVariable dataVar = b.getParameter(1); + + // assembler = b + // properties = null (defaults to all non-key properties) + // instanceVar = "dest" storable + // adapterInstanceClass = null (null means use instanceVar, in this case is "dest") + // useWriteMethods = false (will set fields directly) + // generation = generation + // altGenerationHandler = null (generation should match) + // encodedVar = "data" byte array + try { + altStrategy.buildDataDecoding + (b, null, destVar, null, false, generation, null, dataVar); + } catch (SupportException e) { + throw new CorruptEncodingException(e); + } + + b.returnVoid(); + + Class clazz = ci.defineClass(cf); + try { + return clazz.newInstance(); + } catch (InstantiationException e) { + throw new UndeclaredThrowableException(e); + } catch (IllegalAccessException e) { + throw new UndeclaredThrowableException(e); + } + } + + /** + * Creates custom raw search keys for {@link Storable} types. It is + * intended for supporting queries and indexes. + */ + public interface SearchKeyFactory { + /** + * Build a search key by extracting all the desired properties from the + * given storable. + * + * @param storable extract a subset of properties from this instance + * @return raw search key + */ + byte[] encodeSearchKey(S storable); + + /** + * Build a search key by extracting all the desired properties from the + * given storable. + * + * @param storable extract a subset of properties from this instance + * @param rangeStart index of first property to use. Its value must be less + * than the count of properties used by this factory. + * @param rangeEnd index of last property to use, exlusive. Its value must + * be less than or equal to the count of properties used by this factory. + * @return raw search key + */ + byte[] encodeSearchKey(S storable, int rangeStart, int rangeEnd); + + /** + * Build a search key by supplying property values without a storable. + * + * @param values values to build into a key. It must be long enough to + * accommodate all of properties used by this factory. + * @return raw search key + */ + byte[] encodeSearchKey(Object[] values); + + /** + * Build a search key by supplying property values without a storable. + * + * @param values values to build into a key. The length may be less than + * the amount of properties used by this factory. It must not be less than the + * difference between rangeStart and rangeEnd. + * @param rangeStart index of first property to use. Its value must be less + * than the count of properties used by this factory. + * @param rangeEnd index of last property to use, exlusive. Its value must + * be less than or equal to the count of properties used by this factory. + * @return raw search key + */ + byte[] encodeSearchKey(Object[] values, int rangeStart, int rangeEnd); + + /** + * Returns the search key for when there are no values. Returned value + * may be null. + */ + byte[] encodeSearchKeyPrefix(); + } + + /** + * Used for decoding different generations of Storable. + */ + public interface Decoder { + /** + * @param dest storable to receive decoded properties + * @param data decoded into properties, some of which may be dropped if + * destination storable doesn't have it + */ + void decode(S dest, byte[] data); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodecFactory.java b/src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodecFactory.java new file mode 100644 index 0000000..fefa880 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodecFactory.java @@ -0,0 +1,76 @@ +/* + * 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.spi.raw; + +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.layout.Layout; + +/** + * Factory for generic codec that supports any kind of storable by + * auto-generating and caching storable implementations. + * + * @author Brian S O'Neill + */ +public class GenericStorableCodecFactory implements StorableCodecFactory { + public GenericStorableCodecFactory() { + } + + /** + * Returns null to let repository decide what the name should be. + */ + public String getStorageName(Class type) throws SupportException { + return null; + } + + /** + * @param type type of storable to create codec for + * @param pkIndex suggested index for primary key (optional) + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, encode a storable layout generation + * value in one or four bytes. Generation 0..127 is encoded in one byte, and + * 128..max is encoded in four bytes, with the most significant bit set. + * @throws SupportException if type is not supported + */ + @SuppressWarnings("unchecked") + public GenericStorableCodec createCodec(Class type, + StorableIndex pkIndex, + boolean isMaster, + Layout layout) + throws SupportException + { + return GenericStorableCodec.getInstance + (this, createStrategy(type, pkIndex), isMaster, layout); + } + + /** + * Override to return a different EncodingStrategy. + * + * @param type type of Storable to generate code for + * @param pkIndex specifies sequence and ordering of key properties (optional) + */ + protected GenericEncodingStrategy createStrategy + (Class type, StorableIndex pkIndex) + throws SupportException + { + return new GenericEncodingStrategy(type, pkIndex); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/KeyDecoder.java b/src/main/java/com/amazon/carbonado/spi/raw/KeyDecoder.java new file mode 100644 index 0000000..127216f --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/KeyDecoder.java @@ -0,0 +1,646 @@ +/* + * 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.spi.raw; + +import com.amazon.carbonado.CorruptEncodingException; + +import static com.amazon.carbonado.spi.raw.KeyEncoder.*; + +/** + * A very low-level class that decodes key components encoded by methods of + * {@link KeyEncoder}. + * + * @author Brian S O'Neill + */ +public class KeyDecoder extends DataDecoder { + + /** + * Decodes a signed integer from exactly 4 bytes, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed integer value + */ + public static int decodeIntDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + return ~decodeInt(src, srcOffset); + } + + /** + * Decodes a signed Integer object from exactly 1 or 5 bytes, as encoded + * for descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Integer object or null + */ + public static Integer decodeIntegerObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeIntDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed long from exactly 8 bytes, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed long value + */ + public static long decodeLongDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + return ~decodeLong(src, srcOffset); + } + + /** + * Decodes a signed Long object from exactly 1 or 9 bytes, as encoded for + * descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Long object or null + */ + public static Long decodeLongObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeLongDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed byte from exactly 1 byte, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed byte value + */ + public static byte decodeByteDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (byte)(src[srcOffset] ^ 0x7f); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Byte object from exactly 1 or 2 bytes, as encoded for + * descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Byte object or null + */ + public static Byte decodeByteObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeByteDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed short from exactly 2 bytes, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed short value + */ + public static short decodeShortDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (short)(((src[srcOffset] << 8) | (src[srcOffset + 1] & 0xff)) ^ 0x7fff); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a signed Short object from exactly 1 or 3 bytes, as encoded for + * descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return signed Short object or null + */ + public static Short decodeShortObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeShortDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a char from exactly 2 bytes, as encoded for descending order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return char value + */ + public static char decodeCharDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return (char)~((src[srcOffset] << 8) | (src[srcOffset + 1] & 0xff)); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a Character object from exactly 1 or 3 bytes, as encoded for + * descending order. If null is returned, then 1 byte was read. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Character object or null + */ + public static Character decodeCharacterObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + int b = src[srcOffset]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + return decodeCharDesc(src, srcOffset + 1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a boolean from exactly 1 byte, as encoded for descending order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return boolean value + */ + public static boolean decodeBooleanDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + return src[srcOffset] == 127; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a Boolean object from exactly 1 byte, as encoded for descending + * order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Boolean object or null + */ + public static Boolean decodeBooleanObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + try { + switch (src[srcOffset]) { + case NULL_BYTE_LOW: case NULL_BYTE_HIGH: + return null; + case (byte)127: + return Boolean.TRUE; + default: + return Boolean.FALSE; + } + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes a float from exactly 4 bytes, as encoded for descending order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return float value + */ + public static float decodeFloatDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + int bits = decodeFloatBits(src, srcOffset); + if (bits >= 0) { + bits ^= 0x7fffffff; + } + return Float.intBitsToFloat(bits); + } + + /** + * Decodes a Float object from exactly 4 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Float object or null + */ + public static Float decodeFloatObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + int bits = decodeFloatBits(src, srcOffset); + if (bits >= 0) { + bits ^= 0x7fffffff; + } + return bits == 0x7fffffff ? null : Float.intBitsToFloat(bits); + } + + /** + * Decodes a double from exactly 8 bytes, as encoded for descending order. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return double value + */ + public static double decodeDoubleDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + long bits = decodeDoubleBits(src, srcOffset); + if (bits >= 0) { + bits ^= 0x7fffffffffffffffL; + } + return Double.longBitsToDouble(bits); + } + + /** + * Decodes a Double object from exactly 8 bytes. + * + * @param src source of encoded bytes + * @param srcOffset offset into source array + * @return Double object or null + */ + public static Double decodeDoubleObjDesc(byte[] src, int srcOffset) + throws CorruptEncodingException + { + long bits = decodeDoubleBits(src, srcOffset); + if (bits >= 0) { + bits ^= 0x7fffffffffffffffL; + } + return bits == 0x7fffffffffffffffL ? null : Double.longBitsToDouble(bits); + } + + /** + * Decodes the given byte array as originally encoded for ascending order. + * The decoding stops when any kind of terminator or illegal byte has been + * read. The decoded bytes are stored in valueRef. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded byte array is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decode(byte[] src, int srcOffset, byte[][] valueRef) + throws CorruptEncodingException + { + try { + return decode(src, srcOffset, valueRef, 0); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array as originally encoded for descending order. + * The decoding stops when any kind of terminator or illegal byte has been + * read. The decoded bytes are stored in valueRef. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded byte array is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decodeDesc(byte[] src, int srcOffset, byte[][] valueRef) + throws CorruptEncodingException + { + try { + return decode(src, srcOffset, valueRef, -1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * @param xorMask 0 for normal decoding, -1 for descending decoding + */ + private static int decode(byte[] src, int srcOffset, byte[][] valueRef, int xorMask) { + // Scan ahead, looking for terminator. + int srcEnd = srcOffset; + while (true) { + byte b = src[srcEnd++]; + if (-32 <= b && b < 32) { + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + if ((srcEnd - 1) <= srcOffset) { + valueRef[0] = null; + return 1; + } + } + break; + } + } + + if (srcEnd - srcOffset == 1) { + valueRef[0] = EMPTY_BYTE_ARRAY; + return 1; + } + + // Value is decoded from base-32768. + + int valueLength = ((srcEnd - srcOffset - 1) * 120) >> 7; + byte[] value = new byte[valueLength]; + int valueOffset = 0; + + final int originalOffset = srcOffset; + + int accumBits = 0; + int accum = 0; + + while (true) { + int d = (src[srcOffset++] ^ xorMask) & 0xff; + int b; + if (srcOffset == srcEnd || + (b = (src[srcOffset++] ^ xorMask) & 0xff) < 32 || b > 223) { + // Handle special case where one byte was emitted for digit. + d -= 32; + // To produce digit, multiply d by 192 and add 191 to adjust + // for missing remainder. The lower bits are discarded anyhow. + d = (d << 7) + (d << 6) + 191; + + // Shift decoded digit into accumulator. + accumBits += 15; + accum = (accum << 15) | d; + + break; + } + + d -= 32; + // To produce digit, multiply d by 192 and add in remainder. + d = ((d << 7) + (d << 6)) + b - 32; + + // Shift decoded digit into accumulator. + accumBits += 15; + accum = (accum << 15) | d; + + if (accumBits == 15) { + value[valueOffset++] = (byte)(accum >> 7); + } else { + value[valueOffset++] = (byte)(accum >> (accumBits - 8)); + accumBits -= 8; + value[valueOffset++] = (byte)(accum >> (accumBits - 8)); + } + accumBits -= 8; + } + + if (accumBits >= 8 && valueOffset < valueLength) { + value[valueOffset] = (byte)(accum >> (accumBits - 8)); + } + + valueRef[0] = value; + + return srcOffset - originalOffset; + } + + /** + * Decodes an encoded string from the given byte array. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded string is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decodeString(byte[] src, int srcOffset, String[] valueRef) + throws CorruptEncodingException + { + try { + return decodeString(src, srcOffset, valueRef, 0); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes an encoded string from the given byte array as originally + * encoded for descending order. + * + * @param src source of encoded data + * @param srcOffset offset into encoded data + * @param valueRef decoded string is stored in element 0, which may be null + * @return amount of bytes read from source + * @throws CorruptEncodingException if source data is corrupt + */ + public static int decodeStringDesc(byte[] src, int srcOffset, String[] valueRef) + throws CorruptEncodingException + { + try { + return decodeString(src, srcOffset, valueRef, -1); + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * @param xorMask 0 for normal decoding, -1 for descending decoding + */ + private static int decodeString(byte[] src, int srcOffset, String[] valueRef, int xorMask) + throws CorruptEncodingException + { + // Scan ahead, looking for terminator. + int srcEnd = srcOffset; + while (true) { + byte b = src[srcEnd++]; + if (-2 <= b && b < 2) { + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + if ((srcEnd - 1) <= srcOffset) { + valueRef[0] = null; + return 1; + } + } + break; + } + } + + if (srcEnd - srcOffset == 1) { + valueRef[0] = ""; + return 1; + } + + // Allocate a character array which may be longer than needed once + // bytes are decoded into characters. + char[] value = new char[srcEnd - srcOffset]; + int valueOffset = 0; + + final int originalOffset = srcOffset; + + while (srcOffset < srcEnd) { + int c = (src[srcOffset++] ^ xorMask) & 0xff; + switch (c >> 5) { + case 0: case 1: case 2: case 3: + // 0xxxxxxx + value[valueOffset++] = (char)(c - 2); + break; + case 4: case 5: + // 10xxxxxx xxxxxxxx + + c = c & 0x3f; + // Multiply by 192, add in remainder, remove offset of 2, and de-normalize. + value[valueOffset++] = + (char)((c << 7) + (c << 6) + ((src[srcOffset++] ^ xorMask) & 0xff) + 94); + + break; + case 6: + // 110xxxxx xxxxxxxx xxxxxxxx + + c = c & 0x1f; + // Multiply by 192, add in remainder... + c = (c << 7) + (c << 6) + ((src[srcOffset++] ^ xorMask) & 0xff) - 32; + // ...multiply by 192, add in remainder, remove offset of 2, and de-normalize. + c = (c << 7) + (c << 6) + ((src[srcOffset++] ^ xorMask) & 0xff) + 12382; + + if (c >= 0x10000) { + // Split into surrogate pair. + c -= 0x10000; + value[valueOffset++] = (char)(0xd800 | ((c >> 10) & 0x3ff)); + value[valueOffset++] = (char)(0xdc00 | (c & 0x3ff)); + } else { + value[valueOffset++] = (char)c; + } + + break; + default: + // 111xxxxx + // Illegal. + throw new CorruptEncodingException + ("Corrupt encoded string data (source offset = " + + (srcOffset - 1) + ')'); + } + } + + valueRef[0] = new String(value, 0, valueOffset - 1); + + return srcEnd - originalOffset; + } + + /** + * Decodes the given byte array which was encoded by {@link + * KeyEncoder#encodeSingleDesc}. + */ + public static byte[] decodeSingleDesc(byte[] src) throws CorruptEncodingException { + return decodeSingleDesc(src, 0, 0); + } + + /** + * Decodes the given byte array which was encoded by {@link + * KeyEncoder#encodeSingleDesc}. + * + * @param prefixPadding amount of extra bytes to skip from start of encoded byte array + * @param suffixPadding amount of extra bytes to skip at end of encoded byte array + */ + public static byte[] decodeSingleDesc(byte[] src, int prefixPadding, int suffixPadding) + throws CorruptEncodingException + { + try { + int length = src.length - suffixPadding - prefixPadding; + if (length == 0) { + return EMPTY_BYTE_ARRAY; + } + byte[] dst = new byte[length]; + while (--length >= 0) { + dst[length] = (byte) (~src[prefixPadding + length]); + } + return dst; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } + + /** + * Decodes the given byte array which was encoded by {@link + * KeyEncoder#encodeSingleNullableDesc}. + */ + public static byte[] decodeSingleNullableDesc(byte[] src) throws CorruptEncodingException { + return decodeSingleNullableDesc(src, 0, 0); + } + + /** + * Decodes the given byte array which was encoded by {@link + * KeyEncoder#encodeSingleNullableDesc}. + * + * @param prefixPadding amount of extra bytes to skip from start of encoded byte array + * @param suffixPadding amount of extra bytes to skip at end of encoded byte array + */ + public static byte[] decodeSingleNullableDesc(byte[] src, int prefixPadding, int suffixPadding) + throws CorruptEncodingException + { + try { + byte b = src[prefixPadding]; + if (b == NULL_BYTE_HIGH || b == NULL_BYTE_LOW) { + return null; + } + int length = src.length - suffixPadding - 1 - prefixPadding; + if (length == 0) { + return EMPTY_BYTE_ARRAY; + } + byte[] dst = new byte[length]; + while (--length >= 0) { + dst[length] = (byte) (~src[1 + prefixPadding + length]); + } + return dst; + } catch (IndexOutOfBoundsException e) { + throw new CorruptEncodingException(null, e); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/KeyEncoder.java b/src/main/java/com/amazon/carbonado/spi/raw/KeyEncoder.java new file mode 100644 index 0000000..dd0faf9 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/KeyEncoder.java @@ -0,0 +1,741 @@ +/* + * 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.spi.raw; + +/** + * A very low-level class that supports encoding of primitive data into unique, + * sortable byte array keys. If the data to encode is of a variable size, then + * it is written in base-32768, using only byte values 32..223. This allows + * special values such as nulls and terminators to be unambiguously + * encoded. Terminators for variable data can be encoded using 1 for ascending + * order and 254 for descending order. Nulls can be encoded as 255 for high + * ordering and 0 for low ordering. + * + * @author Brian S O'Neill + * @see KeyDecoder + */ +public class KeyEncoder extends DataEncoder { + + /** Byte to terminate variable data encoded for ascending order */ + static final byte TERMINATOR = (byte)1; + + /** + * Encodes the given signed integer into exactly 4 bytes for descending + * order. + * + * @param value signed integer value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(int value, byte[] dst, int dstOffset) { + encode(~value, dst, dstOffset); + } + + /** + * Encodes the given signed Integer object into exactly 1 or 5 bytes for + * descending order. If the Integer object is never expected to be null, + * consider encoding as an int primitive. + * + * @param value optional signed Integer value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Integer value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + encode(~value.intValue(), dst, dstOffset + 1); + return 5; + } + } + + /** + * Encodes the given signed long into exactly 8 bytes for descending order. + * + * @param value signed long value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(long value, byte[] dst, int dstOffset) { + encode(~value, dst, dstOffset); + } + + /** + * Encodes the given signed Long object into exactly 1 or 9 bytes for + * descending order. If the Long object is never expected to be null, + * consider encoding as a long primitive. + * + * @param value optional signed Long value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Long value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + encode(~value.longValue(), dst, dstOffset + 1); + return 9; + } + } + + /** + * Encodes the given signed byte into exactly 1 byte for descending order. + * + * @param value signed byte value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(byte value, byte[] dst, int dstOffset) { + dst[dstOffset] = (byte)(value ^ 0x7f); + } + + /** + * Encodes the given signed Byte object into exactly 1 or 2 bytes for + * descending order. If the Byte object is never expected to be null, + * consider encoding as a byte primitive. + * + * @param value optional signed Byte value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Byte value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + dst[dstOffset + 1] = (byte)(value ^ 0x7f); + return 2; + } + } + + /** + * Encodes the given signed short into exactly 2 bytes for descending + * order. + * + * @param value signed short value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(short value, byte[] dst, int dstOffset) { + encode((short) ~value, dst, dstOffset); + } + + /** + * Encodes the given signed Short object into exactly 1 or 3 bytes for + * descending order. If the Short object is never expected to be null, + * consider encoding as a short primitive. + * + * @param value optional signed Short value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Short value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + encode((short) ~value.shortValue(), dst, dstOffset + 1); + return 3; + } + } + + /** + * Encodes the given character into exactly 2 bytes for descending order. + * + * @param value character value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(char value, byte[] dst, int dstOffset) { + encode((char) ~value, dst, dstOffset); + } + + /** + * Encodes the given Character object into exactly 1 or 3 bytes for + * descending order. If the Character object is never expected to be null, + * consider encoding as a char primitive. + * + * @param value optional Character value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(Character value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } else { + dst[dstOffset] = NOT_NULL_BYTE_LOW; + encode((char) ~value.charValue(), dst, dstOffset + 1); + return 3; + } + } + + /** + * Encodes the given boolean into exactly 1 byte for descending order. + * + * @param value boolean value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(boolean value, byte[] dst, int dstOffset) { + dst[dstOffset] = value ? (byte)127 : (byte)128; + } + + /** + * Encodes the given Boolean object into exactly 1 byte for descending + * order. + * + * @param value optional Boolean value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(Boolean value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + } else { + dst[dstOffset] = value.booleanValue() ? (byte)127 : (byte)128; + } + } + + /** + * Encodes the given float into exactly 4 bytes for descending order. + * + * @param value float value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(float value, byte[] dst, int dstOffset) { + int bits = Float.floatToIntBits(value); + if (bits >= 0) { + bits ^= 0x7fffffff; + } + dst[dstOffset ] = (byte)(bits >> 24); + dst[dstOffset + 1] = (byte)(bits >> 16); + dst[dstOffset + 2] = (byte)(bits >> 8); + dst[dstOffset + 3] = (byte)bits; + } + + /** + * Encodes the given Float object into exactly 4 bytes for descending + * order. A non-canonical NaN value is used to represent null. + * + * @param value optional Float value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(Float value, byte[] dst, int dstOffset) { + if (value == null) { + encode(~0x7fffffff, dst, dstOffset); + } else { + encodeDesc(value.floatValue(), dst, dstOffset); + } + } + + /** + * Encodes the given double into exactly 8 bytes for descending order. + * + * @param value double value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(double value, byte[] dst, int dstOffset) { + long bits = Double.doubleToLongBits(value); + if (bits >= 0) { + bits ^= 0x7fffffffffffffffL; + } + int w = (int)(bits >> 32); + dst[dstOffset ] = (byte)(w >> 24); + dst[dstOffset + 1] = (byte)(w >> 16); + dst[dstOffset + 2] = (byte)(w >> 8); + dst[dstOffset + 3] = (byte)w; + w = (int)bits; + dst[dstOffset + 4] = (byte)(w >> 24); + dst[dstOffset + 5] = (byte)(w >> 16); + dst[dstOffset + 6] = (byte)(w >> 8); + dst[dstOffset + 7] = (byte)w; + } + + /** + * Encodes the given Double object into exactly 8 bytes for descending + * order. A non-canonical NaN value is used to represent null. + * + * @param value optional Double value to encode + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + */ + public static void encodeDesc(Double value, byte[] dst, int dstOffset) { + if (value == null) { + encode(~0x7fffffffffffffffL, dst, dstOffset); + } else { + encodeDesc(value.doubleValue(), dst, dstOffset); + } + } + + /** + * Encodes the given optional unsigned byte array into a variable amount of + * bytes. If the byte array is null, exactly 1 byte is written. Otherwise, + * the amount written can be determined by calling calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(byte[] value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_HIGH; + return 1; + } + return encode(value, 0, value.length, dst, dstOffset, 0); + } + + /** + * Encodes the given optional unsigned byte array into a variable amount of + * bytes. If the byte array is null, exactly 1 byte is written. Otherwise, + * the amount written can be determined by calling calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(byte[] value, int valueOffset, int valueLength, + byte[] dst, int dstOffset) { + return encode(value, valueOffset, valueLength, dst, dstOffset, 0); + } + + /** + * Encodes the given optional unsigned byte array into a variable amount of + * bytes for descending order. If the byte array is null, exactly 1 byte is + * written. Otherwise, the amount written is determined by calling + * calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(byte[] value, byte[] dst, int dstOffset) { + if (value == null) { + dst[dstOffset] = NULL_BYTE_LOW; + return 1; + } + return encode(value, 0, value.length, dst, dstOffset, -1); + } + + /** + * Encodes the given optional unsigned byte array into a variable amount of + * bytes for descending order. If the byte array is null, exactly 1 byte is + * written. Otherwise, the amount written is determined by calling + * calculateEncodedLength. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(byte[] value, int valueOffset, int valueLength, + byte[] dst, int dstOffset) { + return encode(value, valueOffset, valueLength, dst, dstOffset, -1); + } + + /** + * @param xorMask 0 for normal encoding, -1 for descending encoding + */ + private static int encode(byte[] value, int valueOffset, int valueLength, + byte[] dst, int dstOffset, int xorMask) { + if (value == null) { + dst[dstOffset] = (byte)(NULL_BYTE_HIGH ^ xorMask); + return 1; + } + + final int originalOffset = dstOffset; + + // Value is encoded in base-32768. + + int accumBits = 0; + int accum = 0; + + final int end = valueOffset + valueLength; + for (int i=valueOffset; i> (8 - supply)); + emitDigit(accum, dst, dstOffset, xorMask); + dstOffset += 2; + accumBits = 8 - supply; + accum = value[i] & ((1 << accumBits) - 1); + } + } + + if (accumBits > 0) { + // Pad with zeros. + accum <<= (15 - accumBits); + if (accumBits <= 7) { + // Since amount of significant bits is small, emit only the + // upper half of the digit. The following code is modified from + // emitDigit. + + int a = (accum * 21845) >> 22; + if (accum - ((a << 7) + (a << 6)) == 192) { + a++; + } + dst[dstOffset++] = (byte)((a + 32) ^ xorMask); + } else { + emitDigit(accum, dst, dstOffset, xorMask); + dstOffset += 2; + } + } + + // Append terminator. + dst[dstOffset++] = (byte)(TERMINATOR ^ xorMask); + + return dstOffset - originalOffset; + } + + /** + * Emits a base-32768 digit using exactly two bytes. The first byte is in the range + * 32..202 and the second byte is in the range 32..223. + * + * @param value digit value in the range 0..32767 + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @param xorMask 0 for normal encoding, -1 for descending encoding + */ + private static void emitDigit(int value, byte[] dst, int dstOffset, int xorMask) { + // The first byte is computed as ((value / 192) + 32) and the second + // byte is computed as ((value % 192) + 32). To speed things up a bit, + // the integer division and remainder operations are replaced with a + // scaled multiplication. + + // approximate value / 192 + int a = (value * 21845) >> 22; + + // approximate value % 192 + // Note: the value 192 was chosen as a divisor because a multiply by + // 192 can be replaced with two summed shifts. + int b = value - ((a << 7) + (a << 6)); + if (b == 192) { + // Fix error. + a++; + b = 0; + } + + dst[dstOffset++] = (byte)((a + 32) ^ xorMask); + dst[dstOffset] = (byte)((b + 32) ^ xorMask); + } + + /** + * Returns the amount of bytes required to encode a byte array of the given + * length. + * + * @param value byte array value to encode, may be null + * @return amount of bytes needed to encode + */ + public static int calculateEncodedLength(byte[] value) { + return value == null ? 1 : calculateEncodedLength(value, 0, value.length); + } + + /** + * Returns the amount of bytes required to encode the given byte array. + * + * @param value byte array value to encode, may be null + * @param valueOffset offset into byte array + * @param valueLength length of data in byte array + * @return amount of bytes needed to encode + */ + public static int calculateEncodedLength(byte[] value, int valueOffset, int valueLength) { + // The add of 119 is used to force ceiling rounding. + return value == null ? 1 : (((valueLength << 7) + 119) / 120 + 1); + } + + /** + * Encodes the given optional String into a variable amount of bytes. The + * amount written can be determined by calling + * calculateEncodedStringLength. + *

+ * Strings are encoded in a fashion similar to UTF-8, in that ASCII + * characters are usually written in one byte. This encoding is more + * efficient than UTF-8, but it isn't compatible with UTF-8. + * + * @param value String value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encode(String value, byte[] dst, int dstOffset) { + return encode(value, dst, dstOffset, 0); + } + + /** + * Encodes the given optional String into a variable amount of bytes for + * descending order. The amount written can be determined by calling + * calculateEncodedStringLength. + *

+ * Strings are encoded in a fashion similar to UTF-8, in that ASCII + * characters are usually written in one byte. This encoding is more + * efficient than UTF-8, but it isn't compatible with UTF-8. + * + * @param value String value to encode, may be null + * @param dst destination for encoded bytes + * @param dstOffset offset into destination array + * @return amount of bytes written + */ + public static int encodeDesc(String value, byte[] dst, int dstOffset) { + return encode(value, dst, dstOffset, -1); + } + + /** + * @param xorMask 0 for normal encoding, -1 for descending encoding + */ + private static int encode(String value, byte[] dst, int dstOffset, int xorMask) { + if (value == null) { + dst[dstOffset] = (byte)(NULL_BYTE_HIGH ^ xorMask); + return 1; + } + + final int originalOffset = dstOffset; + + // All characters have an offset of 2 added, in order to reserve bytes + // 0 and 1 for encoding nulls and terminators. This means the ASCII + // string "HelloWorld" is actually encoded as "JgnnqYqtnf". This also + // means that the ASCII '~' and del characters are encoded in two bytes. + + int length = value.length(); + for (int i = 0; i < length; i++) { + int c = value.charAt(i) + 2; + if (c <= 0x7f) { + // 0xxxxxxx + dst[dstOffset++] = (byte)(c ^ xorMask); + } else if (c <= 12415) { + // 10xxxxxx xxxxxxxx + + // Second byte cannot have the values 0, 1, 254, or 255 because + // they clash with null and terminator bytes. Divide by 192 and + // store in first 6 bits. The remainder, with 32 added, goes + // into the second byte. Note that (192 * 63 + 191) + 128 == 12415. + // 63 is the maximum value that can be represented in 6 bits. + + c -= 128; // c will always be at least 128, so normalize. + + // approximate value / 192 + int a = (c * 21845) >> 22; + + // approximate value % 192 + // Note: the value 192 was chosen as a divisor because a multiply by + // 192 can be replaced with two summed shifts. + c = c - ((a << 7) + (a << 6)); + if (c == 192) { + // Fix error. + a++; + c = 0; + } + + dst[dstOffset++] = (byte)((0x80 | a) ^ xorMask); + dst[dstOffset++] = (byte)((c + 32) ^ xorMask); + } else { + // 110xxxxx xxxxxxxx xxxxxxxx + + if ((c - 2) >= 0xd800 && (c - 2) <= 0xdbff) { + // Found a high surrogate. Verify that surrogate pair is + // well-formed. Low surrogate must follow high surrogate. + if (i + 1 < length) { + int c2 = value.charAt(i + 1); + if (c2 >= 0xdc00 && c2 <= 0xdfff) { + c = ((((c - 2) & 0x3ff) << 10) | (c2 & 0x3ff)) + 0x10002; + i++; + } + } + } + + // Second and third bytes cannot have the values 0, 1, 254, or + // 255 because they clash with null and terminator + // bytes. Divide by 192 twice, storing the first and second + // remainders in the third and second bytes, respectively. + // Note that largest unicode value supported is 2^20 + 65535 == + // 1114111. When divided by 192 twice, the value is 30, which + // just barely fits in the 5 available bits of the first byte. + + c -= 12416; // c will always be at least 12416, so normalize. + + int a = (int)((c * 21845L) >> 22); + c = c - ((a << 7) + (a << 6)); + if (c == 192) { + a++; + c = 0; + } + + dst[dstOffset + 2] = (byte)((c + 32) ^ xorMask); + + c = (a * 21845) >> 22; + a = a - ((c << 7) + (c << 6)); + if (a == 192) { + c++; + a = 0; + } + + dst[dstOffset++] = (byte)((0xc0 | c) ^ xorMask); + dst[dstOffset++] = (byte)((a + 32) ^ xorMask); + dstOffset++; + } + } + + // Append terminator. + dst[dstOffset++] = (byte)(TERMINATOR ^ xorMask); + + return dstOffset - originalOffset; + } + + /** + * Returns the amount of bytes required to encode the given String. + * + * @param value String to encode, may be null + */ + public static int calculateEncodedStringLength(String value) { + int encodedLen = 1; + if (value != null) { + int valueLength = value.length(); + for (int i = 0; i < valueLength; i++) { + int c = value.charAt(i); + if (c <= (0x7f - 2)) { + encodedLen++; + } else if (c <= (12415 - 2)) { + encodedLen += 2; + } else { + if (c >= 0xd800 && c <= 0xdbff) { + // Found a high surrogate. Verify that surrogate pair is + // well-formed. Low surrogate must follow high surrogate. + if (i + 1 < valueLength) { + int c2 = value.charAt(i + 1); + if (c2 >= 0xdc00 && c2 <= 0xdfff) { + i++; + } + } + } + encodedLen += 3; + } + } + } + return encodedLen; + } + + /** + * Encodes the given byte array for use when there is only a single + * required property, descending order, whose type is a byte array. The + * original byte array is returned if the length is zero. + */ + public static byte[] encodeSingleDesc(byte[] value) { + return encodeSingleDesc(value, 0, 0); + } + + /** + * Encodes the given byte array for use when there is only a single + * required property, descending order, whose type is a byte array. The + * original byte array is returned if the length and padding lengths are + * zero. + * + * @param prefixPadding amount of extra bytes to allocate at start of encoded byte array + * @param suffixPadding amount of extra bytes to allocate at end of encoded byte array + */ + public static byte[] encodeSingleDesc(byte[] value, int prefixPadding, int suffixPadding) { + int length = value.length; + if (prefixPadding <= 0 && suffixPadding <= 0 && length == 0) { + return value; + } + byte[] dst = new byte[prefixPadding + length + suffixPadding]; + while (--length >= 0) { + dst[prefixPadding + length] = (byte) (~value[length]); + } + return dst; + } + + /** + * Encodes the given byte array for use when there is only a single + * nullable property, descending order, whose type is a byte array. + */ + public static byte[] encodeSingleNullableDesc(byte[] value) { + return encodeSingleNullableDesc(value, 0, 0); + } + + /** + * Encodes the given byte array for use when there is only a single + * nullable property, descending order, whose type is a byte array. + * + * @param prefixPadding amount of extra bytes to allocate at start of encoded byte array + * @param suffixPadding amount of extra bytes to allocate at end of encoded byte array + */ + public static byte[] encodeSingleNullableDesc(byte[] value, + int prefixPadding, int suffixPadding) { + if (prefixPadding <= 0 && suffixPadding <= 0) { + if (value == null) { + return NULL_BYTE_ARRAY_LOW; + } + + int length = value.length; + if (length == 0) { + return NOT_NULL_BYTE_ARRAY_LOW; + } + + byte[] dst = new byte[1 + length]; + dst[0] = NOT_NULL_BYTE_LOW; + while (--length >= 0) { + dst[1 + length] = (byte) (~value[length]); + } + return dst; + } + + if (value == null) { + byte[] dst = new byte[prefixPadding + 1 + suffixPadding]; + dst[prefixPadding] = NULL_BYTE_LOW; + return dst; + } + + int length = value.length; + byte[] dst = new byte[prefixPadding + 1 + length + suffixPadding]; + dst[prefixPadding] = NOT_NULL_BYTE_LOW; + while (--length >= 0) { + dst[prefixPadding + 1 + length] = (byte) (~value[length]); + } + return dst; + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/LayoutPropertyInfo.java b/src/main/java/com/amazon/carbonado/spi/raw/LayoutPropertyInfo.java new file mode 100644 index 0000000..362c17c --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/LayoutPropertyInfo.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.spi.raw; + +import java.lang.reflect.Method; + +import org.cojen.classfile.TypeDesc; + +import com.amazon.carbonado.layout.LayoutProperty; +import com.amazon.carbonado.lob.Lob; + +/** + * + * + * @author Brian S O'Neill + */ +public class LayoutPropertyInfo implements GenericPropertyInfo { + private final LayoutProperty mProp; + private final TypeDesc mPropertyType; + private final TypeDesc mStorageType; + private final Method mFromStorage; + private final Method mToStorage; + + LayoutPropertyInfo(LayoutProperty property) { + this(property, null, null, null); + } + + LayoutPropertyInfo(LayoutProperty property, + Class storageType, Method fromStorage, Method toStorage) + { + mProp = property; + mPropertyType = TypeDesc.forDescriptor(property.getPropertyTypeDescriptor()); + if (storageType == null) { + mStorageType = mPropertyType; + } else { + mStorageType = TypeDesc.forClass(storageType); + } + mFromStorage = fromStorage; + mToStorage = toStorage; + } + + public String getPropertyName() { + return mProp.getPropertyName(); + } + + public TypeDesc getPropertyType() { + return mPropertyType; + } + + public TypeDesc getStorageType() { + return mStorageType; + } + + public boolean isNullable() { + return mProp.isNullable(); + } + + public boolean isLob() { + Class clazz = mPropertyType.toClass(); + return clazz != null && Lob.class.isAssignableFrom(clazz); + } + + public Method getFromStorageAdapter() { + return mFromStorage; + } + + public Method getToStorageAdapter() { + return mToStorage; + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/RawCursor.java b/src/main/java/com/amazon/carbonado/spi/raw/RawCursor.java new file mode 100644 index 0000000..b665191 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/RawCursor.java @@ -0,0 +1,743 @@ +/* + * 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.spi.raw; + +import java.util.NoSuchElementException; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.cursor.AbstractCursor; + +/** + * Abstract Cursor implementation for a repository that manipulates raw bytes. + * + * @author Brian S O'Neill + */ +public abstract class RawCursor extends AbstractCursor { + // States for mState. + private static final byte + UNINITIALIZED = 0, + CLOSED = 1, + TRY_NEXT = 2, + HAS_NEXT = 3; + + /** Lock object, as passed into the constructor */ + protected final Lock mLock; + + private final byte[] mStartBound; + private final boolean mInclusiveStart; + private final byte[] mEndBound; + private final boolean mInclusiveEnd; + private final int mPrefixLength; + private final boolean mReverse; + + private byte mState; + + /** + * @param lock operations lock on this object + * @param startBound specify the starting key for the cursor, or null if first + * @param inclusiveStart true if start bound is inclusive + * @param endBound specify the ending key for the cursor, or null if last + * @param inclusiveEnd true if end bound is inclusive + * @param maxPrefix maximum expected common initial bytes in start and end bound + * @param reverse when true, iteration is reversed + * @throws IllegalArgumentException if any bound is null but is not inclusive + */ + protected RawCursor(Lock lock, + byte[] startBound, boolean inclusiveStart, + byte[] endBound, boolean inclusiveEnd, + int maxPrefix, + boolean reverse) { + mLock = lock == null ? new ReentrantLock() : lock; + + if ((startBound == null && !inclusiveStart) || (endBound == null && !inclusiveEnd)) { + throw new IllegalArgumentException(); + } + + mStartBound = startBound; + mInclusiveStart = inclusiveStart; + mEndBound = endBound; + mInclusiveEnd = inclusiveEnd; + mReverse = reverse; + + // Determine common prefix for start and end bound. + if (maxPrefix <= 0 || startBound == null && endBound == null) { + mPrefixLength = 0; + } else { + int len = Math.min(maxPrefix, Math.min(startBound.length, endBound.length)); + int i; + for (i=0; i= amount) { + return actual; + } + mState = TRY_NEXT; + // Since state was HAS_NEXT and is forced into TRY_NEXT, actual + // amount skipped is effectively one more. + actual++; + } + + // Reached the end naturally, so close. + close(); + + return actual; + } finally { + mLock.unlock(); + } + } + + /** + * Release any internal resources, called when closed. + */ + protected abstract void release() throws FetchException; + + /** + * Returns the contents of the current key being referenced, or null + * otherwise. Caller is responsible for making a copy of the key. The array + * must not be modified concurrently. + * + *

If cursor is not opened, null must be returned. + * + * @return currently referenced key bytes or null if no current + * @throws IllegalStateException if key is disabled + */ + protected abstract byte[] getCurrentKey() throws FetchException; + + /** + * Returns the contents of the current value being referenced, or null + * otherwise. Caller is responsible for making a copy of the value. The + * array must not be modified concurrently. + * + *

If cursor is not opened, null must be returned. + * + * @return currently referenced value bytes or null if no current + * @throws IllegalStateException if value is disabled + */ + protected abstract byte[] getCurrentValue() throws FetchException; + + /** + * An optimization hint which disables key and value acquisition. The + * default implementation of this method does nothing. + */ + protected void disableKeyAndValue() { + } + + /** + * An optimization hint which disables just value acquisition. The default + * implementation of this method does nothing. + */ + protected void disableValue() { + } + + /** + * Enable key and value acquisition again, after they have been + * disabled. Calling this method forces the key and value to be + * re-acquired, if they had been disabled. Key and value acquisition must + * be enabled by default. The default implementation of this method does + * nothing. + */ + protected void enableKeyAndValue() throws FetchException { + } + + /** + * Returns a new Storable instance for the currently referenced entry. + * + * @return new Storable instance, never null + * @throws IllegalStateException if no current entry to instantiate + */ + protected abstract S instantiateCurrent() throws FetchException; + + /** + * Move the cursor to the first available entry. If false is returned, the + * cursor must be positioned before the first available entry. + * + * @return true if first entry exists and is now current + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toFirst() throws FetchException; + + /** + * Move the cursor to the first available entry at or after the given + * key. If false is returned, the cursor must be positioned before the + * first available entry. Caller is responsible for preserving contents of + * array. + * + * @param key key to search for + * @return true if first entry exists and is now current + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toFirst(byte[] key) throws FetchException; + + /** + * Move the cursor to the last available entry. If false is returned, the + * cursor must be positioned after the last available entry. + * + * @return true if last entry exists and is now current + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toLast() throws FetchException; + + /** + * Move the cursor to the last available entry at or before the given + * key. If false is returned, the cursor must be positioned after the last + * available entry. Caller is responsible for preserving contents of array. + * + * @param key key to search for + * @return true if last entry exists and is now current + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toLast(byte[] key) throws FetchException; + + /** + * Move the cursor to the next available entry, returning false if none. If + * false is returned, the cursor must be positioned after the last + * available entry. + * + * @return true if moved to next entry + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toNext() throws FetchException; + + /** + * Move the cursor to the next available entry, incrementing by the amount + * given. The actual amount incremented is returned. If the amount is less + * then requested, the cursor must be positioned after the last available + * entry. Subclasses may wish to override this method with a faster + * implementation. + * + *

Calling to toNext(1) is equivalent to calling toNext(). + * + * @param amount positive amount to advance + * @return actual amount advanced + * @throws IllegalStateException if cursor is not opened + */ + protected int toNext(int amount) throws FetchException { + if (amount <= 1) { + return (amount <= 0) ? 0 : (toNext() ? 1 : 0); + } + + int count = 0; + + disableKeyAndValue(); + try { + while (amount > 0) { + if (toNext()) { + count++; + amount--; + } else { + break; + } + } + } finally { + enableKeyAndValue(); + } + + return count; + } + + /** + * Move the cursor to the next unique key, returning false if none. If + * false is returned, the cursor must be positioned after the last + * available entry. Subclasses may wish to override this method with a + * faster implementation. + * + * @return true if moved to next unique key + * @throws IllegalStateException if cursor is not opened + */ + protected boolean toNextKey() throws FetchException { + byte[] initialKey = getCurrentKey(); + if (initialKey == null) { + return false; + } + + disableValue(); + try { + while (true) { + if (toNext()) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + if (compareKeysPartially(currentKey, initialKey) > 0) { + break; + } + } else { + return false; + } + } + } finally { + enableKeyAndValue(); + } + + return true; + } + + /** + * Move the cursor to the previous available entry, returning false if + * none. If false is returned, the cursor must be positioned before the + * first available entry. + * + * @return true if moved to previous entry + * @throws IllegalStateException if cursor is not opened + */ + protected abstract boolean toPrevious() throws FetchException; + + /** + * Move the cursor to the previous available entry, decrementing by the + * amount given. The actual amount decremented is returned. If the amount + * is less then requested, the cursor must be positioned before the first + * available entry. Subclasses may wish to override this method with a + * faster implementation. + * + *

Calling to toPrevious(1) is equivalent to calling toPrevious(). + * + * @param amount positive amount to retreat + * @return actual amount retreated + * @throws IllegalStateException if cursor is not opened + */ + protected int toPrevious(int amount) throws FetchException { + if (amount <= 1) { + return (amount <= 0) ? 0 : (toPrevious() ? 1 : 0); + } + + int count = 0; + + disableKeyAndValue(); + try { + while (amount > 0) { + if (toPrevious()) { + count++; + amount--; + } else { + break; + } + } + } finally { + enableKeyAndValue(); + } + + return count; + } + + /** + * Move the cursor to the previous unique key, returning false if none. If + * false is returned, the cursor must be positioned before the first + * available entry. Subclasses may wish to override this method with a + * faster implementation. + * + * @return true if moved to previous unique key + * @throws IllegalStateException if cursor is not opened + */ + protected boolean toPreviousKey() throws FetchException { + byte[] initialKey = getCurrentKey(); + if (initialKey == null) { + return false; + } + + disableValue(); + try { + while (true) { + if (toPrevious()) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + if (compareKeysPartially(getCurrentKey(), initialKey) < 0) { + break; + } + } else { + return false; + } + } + } finally { + enableKeyAndValue(); + } + + return true; + } + + /** + * Returns <0 if key1 is less, 0 if equal (at least partially), >0 + * if key1 is greater. + */ + protected int compareKeysPartially(byte[] key1, byte[] key2) { + int length = Math.min(key1.length, key2.length); + for (int i=0; i 0) { + byte[] prefix = mStartBound; + byte[] key = getCurrentKey(); + if (key == null) { + return false; + } + for (int i=0; i= 0) { + if (result > 0 || !mInclusiveEnd) { + return false; + } + } + } + + return prefixMatches(); + } + + // Calls toLast, but considers start and end bounds. Caller is responsible + // for preserving key. + private boolean toBoundedLast() throws FetchException { + if (mEndBound == null) { + if (!toLast()) { + return false; + } + } else { + if (!toLast(mEndBound.clone())) { + return false; + } + if (!mInclusiveEnd) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + if (compareKeysPartially(mEndBound, currentKey) == 0) { + if (!toPreviousKey()) { + return false; + } + } + } + } + + if (mStartBound != null) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + int result = compareKeysPartially(currentKey, mStartBound); + if (result <= 0) { + if (result < 0 || !mInclusiveStart) { + return false; + } + } + } + + return prefixMatches(); + } + + // Calls toNext, but considers end bound. + private boolean toBoundedNext() throws FetchException { + if (!toNext()) { + return false; + } + + if (mEndBound != null) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + int result = compareKeysPartially(currentKey, mEndBound); + if (result >= 0) { + if (result > 0 || !mInclusiveEnd) { + return false; + } + } + } + + return prefixMatches(); + } + + // Calls toNext, but considers end bound. + private int toBoundedNext(int amount) throws FetchException { + if (mEndBound == null) { + return toNext(amount); + } + + int count = 0; + + disableValue(); + try { + while (amount > 0) { + if (!toNext()) { + break; + } + + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + break; + } + + int result = compareKeysPartially(currentKey, mEndBound); + if (result >= 0) { + if (result > 0 || !mInclusiveEnd) { + break; + } + } + + if (!prefixMatches()) { + break; + } + + count++; + amount--; + } + } finally { + enableKeyAndValue(); + } + + return count; + } + + // Calls toPrevious, but considers start bound. + private boolean toBoundedPrevious() throws FetchException { + if (!toPrevious()) { + return false; + } + + if (mStartBound != null) { + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + return false; + } + int result = compareKeysPartially(currentKey, mStartBound); + if (result <= 0) { + if (result < 0 || !mInclusiveStart) { + // Too far now, reset to first. + toBoundedFirst(); + return false; + } + } + } + + return prefixMatches(); + } + + // Calls toPrevious, but considers start bound. + private int toBoundedPrevious(int amount) throws FetchException { + if (mStartBound == null) { + return toPrevious(amount); + } + + int count = 0; + + disableValue(); + try { + while (amount > 0) { + if (!toPrevious()) { + break; + } + + byte[] currentKey = getCurrentKey(); + if (currentKey == null) { + break; + } + + int result = compareKeysPartially(currentKey, mStartBound); + if (result <= 0) { + if (result < 0 || !mInclusiveStart) { + // Too far now, reset to first. + toBoundedFirst(); + break; + } + } + + if (!prefixMatches()) { + break; + } + + count++; + amount--; + } + } finally { + enableKeyAndValue(); + } + + return count; + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/RawStorableGenerator.java b/src/main/java/com/amazon/carbonado/spi/raw/RawStorableGenerator.java new file mode 100644 index 0000000..6d6fbe5 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/RawStorableGenerator.java @@ -0,0 +1,355 @@ +/* + * 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.spi.raw; + +import java.lang.ref.Reference; +import java.lang.ref.SoftReference; +import java.util.EnumSet; +import java.util.Map; + +import org.cojen.classfile.ClassFile; +import org.cojen.classfile.CodeBuilder; +import org.cojen.classfile.Label; +import org.cojen.classfile.LocalVariable; +import org.cojen.classfile.MethodInfo; +import org.cojen.classfile.Modifiers; +import org.cojen.classfile.TypeDesc; +import org.cojen.util.ClassInjector; +import org.cojen.util.WeakIdentityMap; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.spi.MasterFeature; +import com.amazon.carbonado.spi.MasterStorableGenerator; +import com.amazon.carbonado.spi.MasterSupport; +import com.amazon.carbonado.spi.StorableGenerator; +import com.amazon.carbonado.spi.TriggerSupport; + +import static com.amazon.carbonado.spi.CommonMethodNames.*; + +/** + * Generates and caches abstract implementations of {@link Storable} types + * which are encoded and decoded in a raw format. The generated abstract + * classes extend those created by {@link MasterStorableGenerator}. + * + * @author Brian S O'Neill + * @see GenericStorableCodec + * @see RawSupport + */ +public class RawStorableGenerator { + // Note: All generated fields/methods have a "$" character in them to + // prevent name collisions with any inherited fields/methods. User storable + // properties are defined as fields which exactly match the property + // name. We don't want collisions with those either. Legal bean properties + // cannot have "$" in them, so there's nothing to worry about. + + /** Name of protected abstract method in generated storable */ + public static final String + ENCODE_KEY_METHOD_NAME = "encodeKey$", + DECODE_KEY_METHOD_NAME = "decodeKey$", + ENCODE_DATA_METHOD_NAME = "encodeData$", + DECODE_DATA_METHOD_NAME = "decodeData$"; + + @SuppressWarnings("unchecked") + private static Map> cCache = new WeakIdentityMap(); + + /** + * Collection of different abstract class flavors. + */ + static class Flavors { + private Reference> mMasterFlavor; + + private Reference> mNonMasterFlavor; + + /** + * May return null. + */ + Class getClass(boolean isMaster) { + Reference> ref; + if (isMaster) { + ref = mMasterFlavor; + } else { + ref = mNonMasterFlavor; + } + return (ref != null) ? ref.get() : null; + } + + @SuppressWarnings("unchecked") + void setClass(Class clazz, boolean isMaster) { + Reference> ref = new SoftReference(clazz); + if (isMaster) { + mMasterFlavor = ref; + } else { + mNonMasterFlavor = ref; + } + } + } + + // Can't be instantiated or extended + private RawStorableGenerator() { + } + + /** + * Returns an abstract implementation of the given Storable type, which is + * fully thread-safe. The Storable type itself may be an interface or a + * class. If it is a class, then it must not be final, and it must have a + * public, no-arg constructor. Two constructors are defined for the + * abstract implementation: + * + *

+     * public <init>(RawSupport);
+
+     * public <init>(RawSupport, byte[] key, byte[] value);
+     * 
+ * + *

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

+     * // Encode the primary key of this storable.
+     * protected abstract byte[] encodeKey();
+     *
+     * // Encode all properties of this storable excluding the primary key.
+     * protected abstract byte[] encodeData();
+     *
+     * // Decode the primary key into properties of this storable.
+     * // Note: this method is also invoked by the four argument constructor.
+     * protected abstract void decodeKey(byte[]);
+     *
+     * // Decode the data into properties of this storable.
+     * // Note: this method is also invoked by the four argument constructor.
+     * protected abstract void decodeData(byte[]);
+     * 
+ * + * @param isMaster when true, version properties, sequences, and triggers are managed + * @throws IllegalArgumentException if type is null + */ + @SuppressWarnings("unchecked") + public static Class + getAbstractClass(Class type, boolean isMaster) + throws SupportException, IllegalArgumentException + { + synchronized (cCache) { + Class abstractClass; + + Flavors flavors = (Flavors) cCache.get(type); + + if (flavors == null) { + flavors = new Flavors(); + cCache.put(type, flavors); + } else if ((abstractClass = flavors.getClass(isMaster)) != null) { + return abstractClass; + } + + abstractClass = generateAbstractClass(type, isMaster); + flavors.setClass(abstractClass, isMaster); + + return abstractClass; + } + } + + @SuppressWarnings("unchecked") + private static Class + generateAbstractClass(Class storableClass, boolean isMaster) + throws SupportException + { + EnumSet features; + if (isMaster) { + features = EnumSet.of(MasterFeature.VERSIONING, + MasterFeature.UPDATE_FULL, + MasterFeature.INSERT_SEQUENCES, + MasterFeature.INSERT_CHECK_REQUIRED); + } else { + features = EnumSet.of(MasterFeature.UPDATE_FULL); + } + + final Class abstractClass = + MasterStorableGenerator.getAbstractClass(storableClass, features); + + ClassInjector ci = ClassInjector.create + (storableClass.getName(), abstractClass.getClassLoader()); + + ClassFile cf = new ClassFile(ci.getClassName(), abstractClass); + cf.setModifiers(cf.getModifiers().toAbstract(true)); + cf.markSynthetic(); + cf.setSourceFile(RawStorableGenerator.class.getName()); + cf.setTarget("1.5"); + + // Declare some types. + final TypeDesc storableType = TypeDesc.forClass(Storable.class); + final TypeDesc storageType = TypeDesc.forClass(Storage.class); + final TypeDesc triggerSupportType = TypeDesc.forClass(TriggerSupport.class); + final TypeDesc masterSupportType = TypeDesc.forClass(MasterSupport.class); + final TypeDesc rawSupportType = TypeDesc.forClass(RawSupport.class); + final TypeDesc byteArrayType = TypeDesc.forClass(byte[].class); + + // Add constructor that accepts a RawSupport. + { + TypeDesc[] params = {rawSupportType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeSuperConstructor(new TypeDesc[] {masterSupportType}); + b.returnVoid(); + } + + // Add constructor that accepts a RawSupport, an encoded key, and an + // encoded data. + { + TypeDesc[] params = {rawSupportType, byteArrayType, byteArrayType}; + MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, params); + CodeBuilder b = new CodeBuilder(mi); + b.loadThis(); + b.loadLocal(b.getParameter(0)); + b.invokeSuperConstructor(new TypeDesc[] {masterSupportType}); + + params = new TypeDesc[] {byteArrayType}; + + b.loadThis(); + b.loadLocal(b.getParameter(1)); + b.invokeVirtual(DECODE_KEY_METHOD_NAME, null, params); + + b.loadThis(); + b.loadLocal(b.getParameter(2)); + b.invokeVirtual(DECODE_DATA_METHOD_NAME, null, params); + + // Indicate that object is clean by calling markAllPropertiesClean. + b.loadThis(); + b.invokeVirtual(MARK_ALL_PROPERTIES_CLEAN, null, null); + + b.returnVoid(); + } + + // Declare protected abstract methods. + { + cf.addMethod(Modifiers.PROTECTED.toAbstract(true), + ENCODE_KEY_METHOD_NAME, byteArrayType, null); + cf.addMethod(Modifiers.PROTECTED.toAbstract(true), + DECODE_KEY_METHOD_NAME, null, new TypeDesc[]{byteArrayType}); + cf.addMethod(Modifiers.PROTECTED.toAbstract(true), + ENCODE_DATA_METHOD_NAME, byteArrayType, null); + cf.addMethod(Modifiers.PROTECTED.toAbstract(true), + DECODE_DATA_METHOD_NAME, null, new TypeDesc[]{byteArrayType}); + } + + // Add required protected doTryLoad_master method, which delegates to RawSupport. + { + MethodInfo mi = cf.addMethod + (Modifiers.PROTECTED.toFinal(true), + MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(FetchException.class)); + CodeBuilder b = new CodeBuilder(mi); + + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(rawSupportType); + b.loadThis(); + b.invokeVirtual(ENCODE_KEY_METHOD_NAME, byteArrayType, null); + TypeDesc[] params = {byteArrayType}; + b.invokeInterface(rawSupportType, "tryLoad", byteArrayType, params); + LocalVariable encodedDataVar = b.createLocalVariable(null, byteArrayType); + b.storeLocal(encodedDataVar); + b.loadLocal(encodedDataVar); + Label notNull = b.createLabel(); + b.ifNullBranch(notNull, false); + b.loadConstant(false); + b.returnValue(TypeDesc.BOOLEAN); + notNull.setLocation(); + b.loadThis(); + b.loadLocal(encodedDataVar); + params = new TypeDesc[] {byteArrayType}; + b.invokeVirtual(DECODE_DATA_METHOD_NAME, null, params); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Add required protected doTryInsert_master method, which delegates to RawSupport. + { + MethodInfo mi = cf.addMethod + (Modifiers.PROTECTED.toFinal(true), + MasterStorableGenerator.DO_TRY_INSERT_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(PersistException.class)); + CodeBuilder b = new CodeBuilder(mi); + + // return rawSupport.tryInsert(this, this.encodeKey$(), this.encodeData$()); + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(rawSupportType); + b.loadThis(); // pass this to tryInsert method + b.loadThis(); + b.invokeVirtual(ENCODE_KEY_METHOD_NAME, byteArrayType, null); + b.loadThis(); + b.invokeVirtual(ENCODE_DATA_METHOD_NAME, byteArrayType, null); + TypeDesc[] params = {storableType, byteArrayType, byteArrayType}; + b.invokeInterface(rawSupportType, "tryInsert", TypeDesc.BOOLEAN, params); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Add required protected doTryUpdate_master method, which delegates to RawSupport. + { + MethodInfo mi = cf.addMethod + (Modifiers.PROTECTED.toFinal(true), + MasterStorableGenerator.DO_TRY_UPDATE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(PersistException.class)); + CodeBuilder b = new CodeBuilder(mi); + + // rawSupport.store(this, this.encodeKey$(), this.encodeData$()); + // return true; + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(rawSupportType); + b.loadThis(); // pass this to store method + b.loadThis(); + b.invokeVirtual(ENCODE_KEY_METHOD_NAME, byteArrayType, null); + b.loadThis(); + b.invokeVirtual(ENCODE_DATA_METHOD_NAME, byteArrayType, null); + TypeDesc[] params = {storableType, byteArrayType, byteArrayType}; + b.invokeInterface(rawSupportType, "store", null, params); + b.loadConstant(true); + b.returnValue(TypeDesc.BOOLEAN); + } + + // Add required protected doTryDelete_master method, which delegates to RawSupport. + { + MethodInfo mi = cf.addMethod + (Modifiers.PROTECTED.toFinal(true), + MasterStorableGenerator.DO_TRY_DELETE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null); + mi.addException(TypeDesc.forClass(PersistException.class)); + CodeBuilder b = new CodeBuilder(mi); + + // return rawSupport.tryDelete(this.encodeKey$()); + b.loadThis(); + b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, triggerSupportType); + b.checkCast(rawSupportType); + b.loadThis(); + b.invokeVirtual(ENCODE_KEY_METHOD_NAME, byteArrayType, null); + + TypeDesc[] params = {byteArrayType}; + b.invokeInterface(rawSupportType, "tryDelete", TypeDesc.BOOLEAN, params); + b.returnValue(TypeDesc.BOOLEAN); + } + + return ci.defineClass(cf); + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/RawSupport.java b/src/main/java/com/amazon/carbonado/spi/raw/RawSupport.java new file mode 100644 index 0000000..895089d --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/RawSupport.java @@ -0,0 +1,97 @@ +/* + * 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.spi.raw; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.PersistException; +import com.amazon.carbonado.Storable; + +import com.amazon.carbonado.lob.Blob; +import com.amazon.carbonado.lob.Clob; + +import com.amazon.carbonado.spi.MasterSupport; + +/** + * Provides runtime support for Storable classes generated by {@link RawStorableGenerator}. + * + * @author Brian S O'Neill + */ +public interface RawSupport extends MasterSupport { + /** + * Try to load the entry referenced by the given key, but return null + * if not found. + * + * @param key non-null key to search for + * @return non-null value that was found, or null if not found + */ + byte[] tryLoad(byte[] key) throws FetchException; + + /** + * Try to insert the entry referenced by the given key with the given + * value. + * + * @param storable storable object that key and value were derived from + * @param key non-null key to insert + * @param value non-null value to insert + * @return false if unique constraint prevents insert + */ + boolean tryInsert(S storable, byte[] key, byte[] value) throws PersistException; + + /** + * Try to store the entry referenced by the given key with the given + * value. If the entry does not exist, insert it. Otherwise, update it. + * + * @param storable storable object that key and value were derived from + * @param key non-null key to store + * @param value non-null value to store + */ + void store(S storable, byte[] key, byte[] value) throws PersistException; + + /** + * Try to delete the entry referenced by the given key. + * + * @param key non-null key to delete + * @return true if entry existed and is now deleted + */ + boolean tryDelete(byte[] key) throws PersistException; + + /** + * Returns the Blob for the given locator, returning null if not found. + */ + Blob getBlob(long locator) throws FetchException; + + /** + * Returns the locator for the given Blob, returning zero if null. + * + * @throws PersistException if blob is unrecognized + */ + long getLocator(Blob blob) throws PersistException; + + /** + * Returns the Clob for the given locator, returning null if not found. + */ + Clob getClob(long locator) throws FetchException; + + /** + * Returns the locator for the given Clob, returning zero if null. + * + * @throws PersistException if blob is unrecognized + */ + long getLocator(Clob clob) throws PersistException; +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/RawUtil.java b/src/main/java/com/amazon/carbonado/spi/raw/RawUtil.java new file mode 100644 index 0000000..5224b6c --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/RawUtil.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.spi.raw; + +/** + * Utilities for manipulating binary data. + * + * @author Brian S O'Neill + */ +public class RawUtil { + /** + * Adds one to an unsigned integer, represented as a byte array. If + * overflowed, value in byte array is 0x00, 0x00, 0x00... + * + * @param value unsigned integer to increment + * @return false if overflowed + */ + public static boolean increment(byte[] value) { + for (int i=value.length; --i>=0; ) { + byte newValue = (byte) ((value[i] & 0xff) + 1); + value[i] = newValue; + if (newValue != 0) { + // No carry bit, so done adding. + return true; + } + } + // This point is reached upon overflow. + return false; + } + + /** + * Subtracts one from an unsigned integer, represented as a byte array. If + * overflowed, value in byte array is 0xff, 0xff, 0xff... + * + * @param value unsigned integer to decrement + * @return false if overflowed + */ + public static boolean decrement(byte[] value) { + for (int i=value.length; --i>=0; ) { + byte newValue = (byte) ((value[i] & 0xff) + -1); + value[i] = newValue; + if (newValue != -1) { + // No borrow bit, so done subtracting. + return true; + } + } + // This point is reached upon overflow. + return false; + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/StorableCodec.java b/src/main/java/com/amazon/carbonado/spi/raw/StorableCodec.java new file mode 100644 index 0000000..307fe7e --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/StorableCodec.java @@ -0,0 +1,118 @@ +/* + * 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.spi.raw; + +import com.amazon.carbonado.FetchException; +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.Storage; + +import com.amazon.carbonado.info.StorableIndex; + +/** + * Supports encoding and decoding of storables. + * + * @author Brian S O'Neill + * @see StorableCodecFactory + */ +public interface StorableCodec { + /** + * Returns the type of Storable produced by this codec. + */ + Class getStorableType(); + + /** + * Instantiate a Storable with no key or value defined yet. + * + * @param support binds generated storable with a storage layer + */ + S instantiate(RawSupport support); + + /** + * Instantiate a Storable with a specific key and value. + * + * @param support binds generated storable with a storage layer + */ + S instantiate(RawSupport support, byte[] key, byte[] value) + throws FetchException; + + /** + * Returns the sequence and directions of properties that make up the + * primary key. + */ + StorableIndex getPrimaryKeyIndex(); + + /** + * Returns the number of prefix bytes in the primary key, which may be + * zero. + */ + int getPrimaryKeyPrefixLength(); + + /** + * Encode a key by extracting all the primary key properties from the given + * storable. + * + * @param storable extract primary key properties from this instance + * @return raw search key + */ + byte[] encodePrimaryKey(S storable); + + /** + * Encode a key by extracting all the primary key properties from the given + * storable. + * + * @param storable extract primary key properties from this instance + * @param rangeStart index of first property to use. Its value must be less + * than the count of primary key properties. + * @param rangeEnd index of last property to use, exlusive. Its value must + * be less than or equal to the count of primary key properties. + * @return raw search key + */ + byte[] encodePrimaryKey(S storable, int rangeStart, int rangeEnd); + + /** + * Encode a key by extracting all the primary key properties from the given + * storable. + * + * @param values values to build into a key. It must be long enough to + * accommodate all primary key properties. + * @return raw search key + */ + byte[] encodePrimaryKey(Object[] values); + + /** + * Encode a key by extracting all the primary key properties from the given + * storable. + * + * @param values values to build into a key. The length may be less than + * the amount of primary key properties used by this factory. It must not + * be less than the difference between rangeStart and rangeEnd. + * @param rangeStart index of first property to use. Its value must be less + * than the count of primary key properties. + * @param rangeEnd index of last property to use, exlusive. Its value must + * be less than or equal to the count of primary key properties. + * @return raw search key + */ + byte[] encodePrimaryKey(Object[] values, int rangeStart, int rangeEnd); + + /** + * Encode the primary key for when there are no values, but there may be a + * prefix. Returned value may be null if no prefix is defined. + */ + byte[] encodePrimaryKeyPrefix(); +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/StorableCodecFactory.java b/src/main/java/com/amazon/carbonado/spi/raw/StorableCodecFactory.java new file mode 100644 index 0000000..26e3858 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/StorableCodecFactory.java @@ -0,0 +1,54 @@ +/* + * 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.spi.raw; + +import com.amazon.carbonado.Storable; +import com.amazon.carbonado.SupportException; + +import com.amazon.carbonado.info.StorableIndex; +import com.amazon.carbonado.layout.Layout; + +/** + * Factory for creating instances of {@link StorableCodec}. + * + * @author Brian S O'Neill + */ +public interface StorableCodecFactory { + /** + * Returns the preferred storage/database name for the given type. Return + * null to let repository decide. + * + * @throws SupportException if type is not supported + */ + String getStorageName(Class type) throws SupportException; + + /** + * @param type type of storable to create codec for + * @param pkIndex suggested index for primary key (optional) + * @param isMaster when true, version properties and sequences are managed + * @param layout when non-null, attempt to encode a storable layout + * generation value in each storable + * @throws SupportException if type is not supported + */ + StorableCodec createCodec(Class type, + StorableIndex pkIndex, + boolean isMaster, + Layout layout) + throws SupportException; +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/StorablePropertyInfo.java b/src/main/java/com/amazon/carbonado/spi/raw/StorablePropertyInfo.java new file mode 100644 index 0000000..f13a56c --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/StorablePropertyInfo.java @@ -0,0 +1,132 @@ +/* + * 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.spi.raw; + +import java.lang.reflect.Method; + +import org.cojen.classfile.CodeAssembler; +import org.cojen.classfile.TypeDesc; + +import com.amazon.carbonado.info.StorableProperty; +import com.amazon.carbonado.lob.Lob; + +/** + * + * + * @author Brian S O'Neill + */ +public class StorablePropertyInfo implements GenericPropertyInfo { + private final StorableProperty mProp; + private final TypeDesc mPropertyType; + private final TypeDesc mStorageType; + private final Method mFromStorage; + private final Method mToStorage; + + StorablePropertyInfo(StorableProperty property) { + this(property, null, null, null); + } + + StorablePropertyInfo(StorableProperty property, + Class storageType, Method fromStorage, Method toStorage) { + mProp = property; + mPropertyType = TypeDesc.forClass(property.getType()); + if (storageType == null) { + mStorageType = mPropertyType; + } else { + mStorageType = TypeDesc.forClass(storageType); + } + mFromStorage = fromStorage; + mToStorage = toStorage; + } + + public String getPropertyName() { + return mProp.getName(); + } + + public TypeDesc getPropertyType() { + return mPropertyType; + } + + public TypeDesc getStorageType() { + return mStorageType; + } + + public boolean isNullable() { + return mProp.isNullable(); + } + + public boolean isLob() { + Class clazz = mPropertyType.toClass(); + return clazz != null && Lob.class.isAssignableFrom(clazz); + } + + public Method getFromStorageAdapter() { + return mFromStorage; + } + + public Method getToStorageAdapter() { + return mToStorage; + } + + public String getReadMethodName() { + return mProp.getReadMethodName(); + } + + public void addInvokeReadMethod(CodeAssembler a) { + a.invoke(mProp.getReadMethod()); + } + + public void addInvokeReadMethod(CodeAssembler a, TypeDesc instanceType) { + Class clazz = instanceType.toClass(); + if (clazz == null) { + // Can't know if instance should be invoked as an interface or as a + // virtual method. + throw new IllegalArgumentException("Instance type has no known class"); + } + if (clazz.isInterface()) { + a.invokeInterface(instanceType, getReadMethodName(), getPropertyType(), null); + } else { + a.invokeVirtual(instanceType, getReadMethodName(), getPropertyType(), null); + } + } + + public String getWriteMethodName() { + return mProp.getWriteMethodName(); + } + + public void addInvokeWriteMethod(CodeAssembler a) { + a.invoke(mProp.getWriteMethod()); + } + + public void addInvokeWriteMethod(CodeAssembler a, TypeDesc instanceType) { + Class clazz = instanceType.toClass(); + if (clazz == null) { + // Can't know if instance should be invoked as an interface or as a + // virtual method. + throw new IllegalArgumentException("Instance type has no known class"); + } + if (clazz.isInterface()) { + a.invokeInterface(instanceType, + getWriteMethodName(), null, new TypeDesc[] {getPropertyType()}); + } else { + a.invokeVirtual(instanceType, + getWriteMethodName(), null, new TypeDesc[] {getPropertyType()}); + } + } +} diff --git a/src/main/java/com/amazon/carbonado/spi/raw/package-info.java b/src/main/java/com/amazon/carbonado/spi/raw/package-info.java new file mode 100644 index 0000000..8d47419 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/raw/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * Provides support for repositories that encode/decode storables in a raw + * binary format. + */ +package com.amazon.carbonado.spi.raw; -- cgit v1.2.3