summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/amazon/carbonado/spi/AbstractRepositoryBuilder.java81
-rw-r--r--src/main/java/com/amazon/carbonado/spi/AbstractSequenceValueProducer.java79
-rw-r--r--src/main/java/com/amazon/carbonado/spi/BaseQuery.java378
-rw-r--r--src/main/java/com/amazon/carbonado/spi/BaseQueryCompiler.java248
-rw-r--r--src/main/java/com/amazon/carbonado/spi/BaseQueryEngine.java1412
-rw-r--r--src/main/java/com/amazon/carbonado/spi/BelatedRepositoryCreator.java149
-rw-r--r--src/main/java/com/amazon/carbonado/spi/BelatedStorageCreator.java138
-rw-r--r--src/main/java/com/amazon/carbonado/spi/BlobProperty.java55
-rw-r--r--src/main/java/com/amazon/carbonado/spi/ClobProperty.java55
-rw-r--r--src/main/java/com/amazon/carbonado/spi/CodeBuilderUtil.java400
-rw-r--r--src/main/java/com/amazon/carbonado/spi/CommonMethodNames.java87
-rw-r--r--src/main/java/com/amazon/carbonado/spi/ConversionComparator.java212
-rw-r--r--src/main/java/com/amazon/carbonado/spi/ExceptionTransformer.java189
-rw-r--r--src/main/java/com/amazon/carbonado/spi/IndexInfoImpl.java117
-rw-r--r--src/main/java/com/amazon/carbonado/spi/IndexSelector.java1204
-rw-r--r--src/main/java/com/amazon/carbonado/spi/LobEngine.java1059
-rw-r--r--src/main/java/com/amazon/carbonado/spi/LobEngineTrigger.java181
-rw-r--r--src/main/java/com/amazon/carbonado/spi/LobProperty.java44
-rw-r--r--src/main/java/com/amazon/carbonado/spi/MasterFeature.java56
-rw-r--r--src/main/java/com/amazon/carbonado/spi/MasterStorableGenerator.java767
-rw-r--r--src/main/java/com/amazon/carbonado/spi/MasterSupport.java38
-rw-r--r--src/main/java/com/amazon/carbonado/spi/RAFInputStream.java62
-rw-r--r--src/main/java/com/amazon/carbonado/spi/RAFOutputStream.java55
-rw-r--r--src/main/java/com/amazon/carbonado/spi/RepairExecutor.java183
-rw-r--r--src/main/java/com/amazon/carbonado/spi/RunnableTransaction.java114
-rw-r--r--src/main/java/com/amazon/carbonado/spi/SequenceValueGenerator.java288
-rw-r--r--src/main/java/com/amazon/carbonado/spi/SequenceValueProducer.java87
-rw-r--r--src/main/java/com/amazon/carbonado/spi/StorableGenerator.java3534
-rw-r--r--src/main/java/com/amazon/carbonado/spi/StorableIndexSet.java512
-rw-r--r--src/main/java/com/amazon/carbonado/spi/StorableSerializer.java337
-rw-r--r--src/main/java/com/amazon/carbonado/spi/StorableSupport.java41
-rw-r--r--src/main/java/com/amazon/carbonado/spi/StoredLob.java96
-rw-r--r--src/main/java/com/amazon/carbonado/spi/StoredSequence.java49
-rw-r--r--src/main/java/com/amazon/carbonado/spi/TransactionManager.java642
-rw-r--r--src/main/java/com/amazon/carbonado/spi/TransactionPair.java89
-rw-r--r--src/main/java/com/amazon/carbonado/spi/TriggerManager.java691
-rw-r--r--src/main/java/com/amazon/carbonado/spi/TriggerSupport.java50
-rw-r--r--src/main/java/com/amazon/carbonado/spi/WrappedQuery.java236
-rw-r--r--src/main/java/com/amazon/carbonado/spi/WrappedStorage.java228
-rw-r--r--src/main/java/com/amazon/carbonado/spi/WrappedSupport.java75
-rw-r--r--src/main/java/com/amazon/carbonado/spi/package-info.java24
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodec.java337
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/CustomStorableCodecFactory.java70
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/DataDecoder.java567
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/DataEncoder.java595
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/GenericEncodingStrategy.java1963
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/GenericInstanceFactory.java36
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/GenericPropertyInfo.java60
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodec.java813
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/GenericStorableCodecFactory.java76
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/KeyDecoder.java646
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/KeyEncoder.java741
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/LayoutPropertyInfo.java86
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/RawCursor.java743
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/RawStorableGenerator.java355
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/RawSupport.java97
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/RawUtil.java66
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/StorableCodec.java118
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/StorableCodecFactory.java54
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/StorablePropertyInfo.java132
-rw-r--r--src/main/java/com/amazon/carbonado/spi/raw/package-info.java23
61 files changed, 21920 insertions, 0 deletions
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<String> messages = new ArrayList<String>();
+ 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<size; i++) {
+ if (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<String> 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<S extends Storable> extends AbstractQuery<S> 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<indentLevel; i++) {
+ app.append(' ');
+ }
+ }
+
+ private final Repository mRepository;
+ private final Storage<S> mStorage;
+ // Values for this query.
+ private final FilterValues<S> 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<S> storage,
+ FilterValues<S> values,
+ OrderedProperty<S>[] 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<S> storage,
+ FilterValues<S> 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<S> getStorableType() {
+ return mStorage.getStorableType();
+ }
+
+ public Filter<S> getFilter() {
+ FilterValues<S> values = mValues;
+ if (values != null) {
+ return values.getFilter();
+ }
+ return Filter.getOpenFilter(mStorage.getStorableType());
+ }
+
+ public FilterValues<S> getFilterValues() {
+ return mValues;
+ }
+
+ public int getBlankParameterCount() {
+ return mValues == null ? 0 : mValues.getBlankParameterCount();
+ }
+
+ public Query<S> with(int value) {
+ return newInstance(requireValues().with(value));
+ }
+
+ public Query<S> with(long value) {
+ return newInstance(requireValues().with(value));
+ }
+
+ public Query<S> with(float value) {
+ return newInstance(requireValues().with(value));
+ }
+
+ public Query<S> with(double value) {
+ return newInstance(requireValues().with(value));
+ }
+
+ public Query<S> with(boolean value) {
+ return newInstance(requireValues().with(value));
+ }
+
+ public Query<S> with(char value) {
+ return newInstance(requireValues().with(value));
+ }
+
+ public Query<S> with(byte value) {
+ return newInstance(requireValues().with(value));
+ }
+
+ public Query<S> with(short value) {
+ return newInstance(requireValues().with(value));
+ }
+
+ public Query<S> with(Object value) {
+ return newInstance(requireValues().with(value));
+ }
+
+ public Query<S> withValues(Object... values) {
+ if (values == null || values.length == 0) {
+ return this;
+ }
+ return newInstance(requireValues().withValues(values));
+ }
+
+ public Query<S> and(Filter<S> filter) throws FetchException {
+ FilterValues<S> values = getFilterValues();
+ Query<S> 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<S> or(Filter<S> filter) throws FetchException {
+ FilterValues<S> values = getFilterValues();
+ if (values == null) {
+ throw new IllegalStateException("Query is already guaranteed to fetch everything");
+ }
+ Query<S> newQuery = mStorage.query(values.getFilter().or(filter));
+ newQuery = newQuery.withValues(values.getValues());
+ return mOrderings.length == 0 ? newQuery : newQuery.orderBy(mOrderings);
+ }
+
+ public Query<S> not() throws FetchException {
+ FilterValues<S> values = getFilterValues();
+ if (values == null) {
+ return new EmptyQuery<S>(mStorage, mOrderings);
+ }
+ Query<S> newQuery = mStorage.query(values.getFilter().not());
+ newQuery = newQuery.withValues(values.getSuppliedValues());
+ return mOrderings.length == 0 ? newQuery : newQuery.orderBy(mOrderings);
+ }
+
+ public Cursor<S> fetchAfter(S start) throws FetchException {
+ String[] orderings;
+ if (start == null || (orderings = mOrderings).length == 0) {
+ return fetch();
+ }
+
+ Class<S> storableType = mStorage.getStorableType();
+ Filter<S> orderFilter = Filter.getClosedFilter(storableType);
+ Filter<S> 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<S> newQuery = this.and(orderFilter);
+
+ for (int i=0; i<values.length; i++) {
+ for (int j=0; j<=i; j++) {
+ newQuery = newQuery.with(values[j]);
+ }
+ }
+
+ return newQuery.fetch();
+ }
+
+ public boolean tryDeleteOne() throws PersistException {
+ Transaction txn = mRepository.enterTransaction(IsolationLevel.READ_COMMITTED);
+ try {
+ Cursor<S> 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<S> 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<S> 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<S> 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<mOrderings.length; i++) {
+ if (i > 0) {
+ app.append(", ");
+ }
+ app.append(mOrderings[i]);
+ }
+ app.append(']');
+ }
+
+ app.append('}');
+ }
+
+ private FilterValues<S> requireValues() {
+ FilterValues<S> 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.
+ *
+ * <p>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<S> newInstance(FilterValues<S> 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<S extends Storable> {
+ private final StorableInfo<S> mInfo;
+ private final Map<String, Query<S>> mStringToQuery;
+ private final Map<Filter<S>, Queries<S>> 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<S> 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<S> 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<S> getCompiledQuery(String filter) throws FetchException {
+ if (filter == null) {
+ throw new IllegalArgumentException("Query filter must not be null");
+ }
+ Query<S> 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<S> getCompiledQuery(Filter<S> filter) throws FetchException {
+ if (filter == null) {
+ throw new IllegalArgumentException("Filter is null");
+ }
+ Queries<S> queries = mFilterToQueries.get(filter);
+ if (queries == null) {
+ Query<S> query;
+ FilterValues<S> values = filter.initialFilterValues();
+ if (values != null) {
+ // FilterValues applies to bound filter. Use that instead.
+ Filter<S> 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<S>(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<S> getOrderedQuery(FilterValues<S> values, String... propertyNames)
+ throws FetchException, IllegalArgumentException, UnsupportedOperationException
+ {
+ final Filter<S> filter =
+ values == null ? Filter.getOpenFilter(mInfo.getStorableType()) : values.getFilter();
+
+ final Queries<S> 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<S> 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<length; i++) {
+ String propertyName = propertyNames[i];
+ if (propertyName == null) {
+ throw new IllegalArgumentException("Order by property [" + i + "] is null");
+ }
+ if (!propertyName.startsWith("+") && !propertyName.startsWith("-")) {
+ if (!propertyNamesChanged) {
+ propertyNames = propertyNames.clone();
+ propertyNamesChanged = true;
+ }
+ propertyNames[i] = "+".concat(propertyName);
+ }
+ }
+
+ if (propertyNamesChanged) {
+ return getOrderedQuery(values, propertyNames);
+ }
+
+ // If this point is reached, propertyNames is guaranteed to have no
+ // null elements, and all have an explicit direction.
+
+ OrderedProperty<S>[] orderings = new OrderedProperty[length];
+
+ for (int i=0; i<length; i++) {
+ orderings[i] = OrderedProperty.parse(mInfo, propertyNames[i]);
+ }
+
+ FilterValues<S> 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<S> 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<S> compileQuery(FilterValues<S> values,
+ OrderedProperty<S>[] orderings)
+ throws FetchException, UnsupportedOperationException;
+
+ private static class Queries<S extends Storable> {
+ final Query<S> mPlainQuery;
+
+ final Map<Object, Query<S>> mOrderingsToQuery;
+
+ @SuppressWarnings("unchecked")
+ Queries(Query<S> 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<S extends Storable> extends BaseQueryCompiler<S> {
+ 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<S> mStorage;
+ private final StorableIndex<S> mPrimaryKeyIndex;
+ private final StorableIndexSet<S> 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<S> info,
+ Repository repo,
+ Storage<S> storage,
+ StorableIndex<S> primaryKeyIndex,
+ StorableIndexSet<S> 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<S>(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<S> compileQuery(final FilterValues<S> values,
+ final OrderedProperty<S>[] orderings)
+ throws FetchException, UnsupportedOperationException
+ {
+ if (values == null) {
+ // Perform requested full scan.
+ return fullScan(values, orderings);
+ }
+
+ final Filter<S> originalFilter = values.getFilter();
+ final Filter<S> dnfFilter = originalFilter.disjunctiveNormalForm();
+
+ // Analyze the disjunctive normal form, breaking down the query into
+ // separate queries that can be unioned together.
+
+ IndexAnalysis<S> analysis = new IndexAnalysis<S>(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<S>[] 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<S>[] effectiveOrderings = null;
+ totalOrderCheck:
+ if (orderings == null || orderings.length == 0) {
+ for (IndexSelector.IndexFitness<S> result : analysis.getResults()) {
+ StorableIndex<S> 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<StorableIndex<S>, Integer> counts = new LinkedHashMap<StorableIndex<S>, Integer>();
+
+ for (IndexSelector.IndexFitness<S> result : analysis.getResults()) {
+ StorableIndex<S> 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<S> unique = mPrimaryKeyIndex;
+ int uniqueCount = 0;
+ for (Map.Entry<StorableIndex<S>, 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<S> 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<S> best = null;
+ int bestCount = 0;
+ for (IndexSelector.IndexFitness<S> 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<S> 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<orderings.length; i++) {
+ totalOrderings[j++] = orderings[i];
+ }
+ }
+ if (best != null) {
+ for (int i=0; i<best.getPropertyCount(); i++) {
+ totalOrderings[j++] = OrderedProperty.get
+ (best.getProperty(i), best.getPropertyDirection(i));
+ }
+ }
+ for (int i=0; i<unique.getPropertyCount(); i++) {
+ totalOrderings[j++] = OrderedProperty.get
+ (unique.getProperty(i), unique.getPropertyDirection(i));
+ }
+ }
+
+ // Augmented total orderings may contain redundancies, which are
+ // removed by index selector. Running the analysis again may be
+ // produce the exact same results as before. No harm done.
+
+ analysis = new IndexAnalysis<S>(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<CursorFactory<S>> subFactories = new ArrayList<CursorFactory<S>>();
+
+ for (IndexSelector.IndexFitness<S> result : analysis.getResults()) {
+ CursorFactory<S> subFactory;
+
+ // Determine if KeyCursorFactory should be used instead.
+ boolean isKeyFilter = result.isKeyFilter();
+ if (isKeyFilter) {
+ subFactory = new KeyCursorFactory<S>
+ (this, result.getIndex(), result.getExactFilter());
+ } else {
+ subFactory = new IndexCursorFactory<S>
+ (this, result.getIndex(),
+ result.shouldReverseOrder(), result.shouldReverseRange(),
+ result.getExactFilter(),
+ result.getInclusiveRangeStartFilters(),
+ result.getExclusiveRangeStartFilters(),
+ result.getInclusiveRangeEndFilters(),
+ result.getExclusiveRangeEndFilters());
+ }
+
+ Filter<S> remainderFilter = result.getRemainderFilter();
+ if (remainderFilter != null) {
+ subFactory = new FilteredCursorFactory<S>(this, subFactory, remainderFilter);
+ }
+
+ if (!isKeyFilter) {
+ OrderedProperty<S>[] remainderOrderings = result.getRemainderOrderings();
+ if (remainderOrderings != null && remainderOrderings.length > 0) {
+ subFactory = new SortedCursorFactory<S>
+ (this, subFactory, result.getHandledOrderings(), remainderOrderings);
+ }
+ }
+
+ subFactories.add(subFactory);
+ }
+
+ CursorFactory<S> factory = UnionedCursorFactory
+ .createUnion(this, subFactories, totalOrderings);
+
+ return CompiledQuery.create(mRepository, mStorage, values, orderings, this, factory);
+ }
+
+ private Query<S> fullScan(FilterValues<S> values, OrderedProperty<S>[] orderings)
+ throws FetchException
+ {
+ // Try to select index that has best ordering.
+ IndexSelector<S> selector = new IndexSelector<S>(null, orderings);
+ StorableIndex<S> best = mPrimaryKeyIndex;
+
+ if (mIndexSet != null) {
+ for (StorableIndex<S> candidate : mIndexSet) {
+ int cmpResult = selector.compare(best, candidate);
+ if (cmpResult > 0) {
+ best = candidate;
+ }
+ }
+ }
+
+ IndexSelector.IndexFitness<S> result = selector.examine(best);
+
+ CursorFactory<S> factory;
+ if (result == null || result.isUseless()) {
+ factory = new FullScanCursorFactory<S>(this, mPrimaryKeyIndex);
+ if (values != null) {
+ factory = new FilteredCursorFactory<S>(this, factory, values.getFilter());
+ }
+ if (orderings != null && orderings.length > 0) {
+ factory = new SortedCursorFactory<S>(this, factory, null, orderings);
+ }
+ } else {
+ factory = new IndexCursorFactory<S>
+ (this, result.getIndex(),
+ result.shouldReverseOrder(), result.shouldReverseRange(),
+ result.getExactFilter(),
+ result.getInclusiveRangeStartFilters(),
+ result.getExclusiveRangeStartFilters(),
+ result.getInclusiveRangeEndFilters(),
+ result.getExclusiveRangeEndFilters());
+
+ Filter<S> remainderFilter = result.getRemainderFilter();
+ if (remainderFilter != null) {
+ factory = new FilteredCursorFactory<S>(this, factory, remainderFilter);
+ }
+
+ OrderedProperty<S>[] remainderOrderings = result.getRemainderOrderings();
+ if (remainderOrderings != null && remainderOrderings.length > 0) {
+ factory = new SortedCursorFactory<S>
+ (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<S> 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<S> getStorageFor(StorableIndex<S> 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<S> openCursor(StorableIndex<S> 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.
+ * <p>
+ * 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<S> openKeyCursor(StorableIndex<S> index,
+ Object[] exactValues)
+ throws FetchException
+ {
+ return openCursor(index, exactValues,
+ BoundaryType.OPEN, null,
+ BoundaryType.OPEN, null,
+ false,
+ false);
+ }
+
+ @SuppressWarnings("unchecked")
+ Comparator<S> makeComparator(OrderedProperty<S>[] orderings) {
+ if (orderings == null) {
+ return null;
+ }
+
+ BeanComparator bc = BeanComparator.forClass(getStorableInfo().getStorableType());
+
+ for (OrderedProperty<S> 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<S extends Storable> extends BaseQuery<S> {
+ private final BaseQueryEngine<S> mEngine;
+ private final CursorFactory<S> mFactory;
+
+ static <S extends Storable> Query<S> create(Repository repo,
+ Storage<S> storage,
+ FilterValues<S> values,
+ OrderedProperty<S>[] orderings,
+ BaseQueryEngine<S> engine,
+ CursorFactory<S> factory)
+ throws FetchException
+ {
+ if (factory == null) {
+ throw new IllegalArgumentException();
+ }
+ factory = factory.getActualFactory();
+ return new CompiledQuery<S>(repo, storage, values, orderings, engine, factory);
+ }
+
+ private CompiledQuery(Repository repo,
+ Storage<S> storage,
+ FilterValues<S> values,
+ OrderedProperty<S>[] orderings,
+ BaseQueryEngine<S> engine,
+ CursorFactory<S> factory)
+ throws FetchException
+ {
+ super(repo, storage, values, orderings);
+ mEngine = engine;
+ mFactory = factory;
+ }
+
+ private CompiledQuery(Repository repo,
+ Storage<S> storage,
+ FilterValues<S> values,
+ String[] orderings,
+ BaseQueryEngine<S> engine,
+ CursorFactory<S> factory)
+ {
+ super(repo, storage, values, orderings);
+ mEngine = engine;
+ mFactory = factory;
+ }
+
+ public Query<S> orderBy(String property)
+ throws FetchException, UnsupportedOperationException
+ {
+ return mEngine.getOrderedQuery(getFilterValues(), property);
+ }
+
+ public Query<S> orderBy(String... properties)
+ throws FetchException, UnsupportedOperationException
+ {
+ return mEngine.getOrderedQuery(getFilterValues(), properties);
+ }
+
+ public Cursor<S> 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<S> newInstance(FilterValues<S> values) {
+ return new CompiledQuery<S>
+ (getRepository(), getStorage(), values, getOrderings(), mEngine, mFactory);
+ }
+ }
+
+ private static interface CursorFactory<S extends Storable> {
+ Cursor<S> openCursor(FilterValues<S> values) throws FetchException;
+
+ long count(FilterValues<S> values) throws FetchException;
+
+ /**
+ * Append filter rules to the given filter.
+ *
+ * @param filter initial filter, might be null.
+ */
+ Filter<S> buildFilter(Filter<S> filter);
+
+ /**
+ * Applies an ordering to the given query in a new query.
+ */
+ Query<S> applyOrderBy(Query<S> 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<S> getActualStorage();
+
+ /**
+ * Returns another instance of this factory that uses the proper
+ * storage.
+ */
+ CursorFactory<S> getActualFactory() throws FetchException;
+
+ /**
+ * @param values optional
+ */
+ boolean printNative(Appendable app, int indentLevel, FilterValues<S> values)
+ throws IOException;
+
+ /**
+ * @param values optional
+ */
+ boolean printPlan(Appendable app, int indentLevel, FilterValues<S> values)
+ throws IOException;
+ }
+
+ private abstract static class AbstractCursorFactory<S extends Storable>
+ implements CursorFactory<S>
+ {
+ protected final BaseQueryEngine<S> mEngine;
+
+ AbstractCursorFactory(BaseQueryEngine<S> engine) {
+ mEngine = engine;
+ }
+
+ public long count(FilterValues<S> values) throws FetchException {
+ Cursor<S> 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<S> getActualFactory() throws FetchException {
+ Storage<S> storage = getActualStorage();
+ if (storage == mEngine.getStorage()) {
+ return this;
+ }
+ return new QueryCursorFactory<S>(this, storage);
+ }
+
+ public boolean printNative(Appendable app, int indentLevel, FilterValues<S> values)
+ throws IOException
+ {
+ return false;
+ }
+
+ void indent(Appendable app, int indentLevel) throws IOException {
+ for (int i=0; i<indentLevel; i++) {
+ app.append(' ');
+ }
+ }
+ }
+
+ private static class IndexCursorFactory<S extends Storable>
+ extends AbstractCursorFactory<S>
+ {
+ protected final StorableIndex<S> mIndex;
+
+ private final boolean mReverseOrder;
+ private final boolean mReverseRange;
+ private final Filter<S> mExactFilter;
+ private final PropertyFilter<S>[] mInclusiveRangeStartFilters;
+ private final PropertyFilter<S>[] mExclusiveRangeStartFilters;
+ private final PropertyFilter<S>[] mInclusiveRangeEndFilters;
+ private final PropertyFilter<S>[] mExclusiveRangeEndFilters;
+
+ IndexCursorFactory(BaseQueryEngine<S> engine,
+ StorableIndex<S> index,
+ boolean reverseOrder,
+ boolean reverseRange,
+ Filter<S> exactFilter,
+ PropertyFilter<S>[] inclusiveRangeStartFilters,
+ PropertyFilter<S>[] exclusiveRangeStartFilters,
+ PropertyFilter<S>[] inclusiveRangeEndFilters,
+ PropertyFilter<S>[] exclusiveRangeEndFilters)
+ {
+ super(engine);
+ mIndex = index;
+ mExactFilter = exactFilter;
+ mReverseOrder = reverseOrder;
+ mReverseRange = reverseRange;
+ mInclusiveRangeStartFilters = inclusiveRangeStartFilters;
+ mExclusiveRangeStartFilters = exclusiveRangeStartFilters;
+ mInclusiveRangeEndFilters = inclusiveRangeEndFilters;
+ mExclusiveRangeEndFilters = exclusiveRangeEndFilters;
+ }
+
+ public Cursor<S> openCursor(FilterValues<S> 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<S> p : mExclusiveRangeStartFilters) {
+ Object value = values.getValue(p);
+ if (rangeStartBoundary == BoundaryType.OPEN ||
+ compareWithNullHigh(value, rangeStartValue) > 0)
+ {
+ rangeStartValue = value;
+ rangeStartBoundary = BoundaryType.EXCLUSIVE;
+ }
+ }
+
+ for (PropertyFilter<S> p : mInclusiveRangeStartFilters) {
+ Object value = values.getValue(p);
+ if (rangeStartBoundary == BoundaryType.OPEN ||
+ compareWithNullHigh(value, rangeStartValue) > 0)
+ {
+ rangeStartValue = value;
+ rangeStartBoundary = BoundaryType.INCLUSIVE;
+ }
+ }
+
+ for (PropertyFilter<S> p : mExclusiveRangeEndFilters) {
+ Object value = values.getValue(p);
+ if (rangeEndBoundary == BoundaryType.OPEN ||
+ compareWithNullHigh(value, rangeEndValue) < 0)
+ {
+ rangeEndValue = value;
+ rangeEndBoundary = BoundaryType.EXCLUSIVE;
+ }
+ }
+
+ for (PropertyFilter<S> 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<S> buildFilter(Filter<S> filter) {
+ if (mExactFilter != null) {
+ filter = filter == null ? mExactFilter : filter.and(mExactFilter);
+ }
+ for (PropertyFilter<S> p : mInclusiveRangeStartFilters) {
+ filter = filter == null ? p : filter.and(p);
+ }
+ for (PropertyFilter<S> p : mExclusiveRangeStartFilters) {
+ filter = filter == null ? p : filter.and(p);
+ }
+ for (PropertyFilter<S> p : mInclusiveRangeEndFilters) {
+ filter = filter == null ? p : filter.and(p);
+ }
+ for (PropertyFilter<S> p : mExclusiveRangeEndFilters) {
+ filter = filter == null ? p : filter.and(p);
+ }
+ return filter;
+ }
+
+ public Query<S> applyOrderBy(Query<S> 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<count; i++) {
+ String propName = mIndex.getProperty(i).getName();
+ Direction dir = mIndex.getPropertyDirection(i);
+ if (mReverseOrder) {
+ dir = dir.reverse();
+ }
+ if (dir == Direction.ASCENDING) {
+ propName = "+".concat(propName);
+ } else if (dir == Direction.DESCENDING) {
+ propName = "-".concat(propName);
+ }
+ orderBy[i] = propName;
+ }
+
+ return query.orderBy(orderBy);
+ }
+
+ public Storage<S> getActualStorage() {
+ return mEngine.getStorageFor(mIndex);
+ }
+
+ public boolean printPlan(Appendable app, int indentLevel, FilterValues<S> 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<S> p : mExclusiveRangeStartFilters) {
+ if (count++ > 0) {
+ app.append(" & ");
+ }
+ p.appendTo(app, values);
+ }
+ for (PropertyFilter<S> p : mInclusiveRangeStartFilters) {
+ if (count++ > 0) {
+ app.append(" & ");
+ }
+ p.appendTo(app, values);
+ }
+ for (PropertyFilter<S> p : mExclusiveRangeEndFilters) {
+ if (count++ > 0) {
+ app.append(" & ");
+ }
+ p.appendTo(app, values);
+ }
+ for (PropertyFilter<S> p : mInclusiveRangeEndFilters) {
+ if (count++ > 0) {
+ app.append(" & ");
+ }
+ p.appendTo(app, values);
+ }
+ app.append('\n');
+ }
+ return true;
+ }
+ }
+
+ private static class FullScanCursorFactory<S extends Storable> extends IndexCursorFactory<S> {
+ FullScanCursorFactory(BaseQueryEngine<S> engine, StorableIndex<S> index) {
+ super(engine, index, false, false,
+ null, NO_FILTERS, NO_FILTERS, NO_FILTERS, NO_FILTERS);
+ }
+
+ @Override
+ public Filter<S> buildFilter(Filter<S> filter) {
+ // Full scan doesn't filter anything.
+ return filter;
+ }
+
+ @Override
+ public boolean printPlan(Appendable app, int indentLevel, FilterValues<S> 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<S extends Storable> extends AbstractCursorFactory<S> {
+ private final StorableIndex<S> mIndex;
+ private final Filter<S> mExactFilter;
+
+ KeyCursorFactory(BaseQueryEngine<S> engine,
+ StorableIndex<S> index, Filter<S> exactFilter) {
+ super(engine);
+ mIndex = index;
+ mExactFilter = exactFilter;
+ }
+
+ public Cursor<S> openCursor(FilterValues<S> values) throws FetchException {
+ return mEngine.openKeyCursor(mIndex, values.getValuesFor(mExactFilter));
+ }
+
+ public Filter<S> buildFilter(Filter<S> filter) {
+ if (mExactFilter != null) {
+ filter = filter == null ? mExactFilter : filter.and(mExactFilter);
+ }
+ return filter;
+ }
+
+ public Query<S> applyOrderBy(Query<S> query) {
+ return query;
+ }
+
+ public Storage<S> getActualStorage() {
+ return mEngine.getStorageFor(mIndex);
+ }
+
+ public boolean printPlan(Appendable app, int indentLevel, FilterValues<S> 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<S extends Storable>
+ extends AbstractCursorFactory<S>
+ {
+ private final CursorFactory<S> mFactory;
+ private final Filter<S> mFilter;
+
+ FilteredCursorFactory(BaseQueryEngine<S> engine,
+ CursorFactory<S> factory, Filter<S> filter) {
+ super(engine);
+ mFactory = factory;
+ mFilter = filter;
+ }
+
+ public Cursor<S> openCursor(FilterValues<S> values) throws FetchException {
+ return FilteredCursor.applyFilter(mFilter,
+ values,
+ mFactory.openCursor(values));
+ }
+
+ public Filter<S> buildFilter(Filter<S> filter) {
+ filter = mFactory.buildFilter(filter);
+ filter = filter == null ? mFilter : filter.and(mFilter);
+ return filter;
+ }
+
+ public Query<S> applyOrderBy(Query<S> query) throws FetchException {
+ return mFactory.applyOrderBy(query);
+ }
+
+ public Storage<S> getActualStorage() {
+ return mFactory.getActualStorage();
+ }
+
+ public boolean printPlan(Appendable app, int indentLevel, FilterValues<S> 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<S extends Storable> extends AbstractCursorFactory<S> {
+ private final CursorFactory<S> mFactory;
+ private final OrderedProperty<S>[] mHandledOrderings;
+ private final OrderedProperty<S>[] mRemainderOrderings;
+
+ private final Comparator<S> mHandledComparator;
+ private final Comparator<S> mFinisherComparator;
+
+ SortedCursorFactory(BaseQueryEngine<S> engine,
+ CursorFactory<S> factory,
+ OrderedProperty<S>[] handledOrderings,
+ OrderedProperty<S>[] 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<S> openCursor(FilterValues<S> values) throws FetchException {
+ Cursor<S> cursor = mFactory.openCursor(values);
+
+ SortBuffer<S> buffer = new MergeSortBuffer<S>
+ (getActualStorage(), mEngine.mMergeSortTempDir);
+
+ return new SortedCursor<S>(cursor, buffer, mHandledComparator, mFinisherComparator);
+ }
+
+ @Override
+ public long count(FilterValues<S> values) throws FetchException {
+ return mFactory.count(values);
+ }
+
+
+ public Filter<S> buildFilter(Filter<S> filter) {
+ return mFactory.buildFilter(filter);
+ }
+
+ public Query<S> applyOrderBy(Query<S> 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<handledLength; i++) {
+ orderBy[pos++] = mHandledOrderings[i].toString();
+ }
+ for (int i=0; i<remainderLength; i++) {
+ orderBy[pos++] = mRemainderOrderings[i].toString();
+ }
+ return query.orderBy(orderBy);
+ }
+
+ public Storage<S> getActualStorage() {
+ return mFactory.getActualStorage();
+ }
+
+ public boolean printPlan(Appendable app, int indentLevel, FilterValues<S> 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<S extends Storable>
+ extends AbstractCursorFactory<S>
+ {
+ static <S extends Storable> CursorFactory<S> createUnion
+ (BaseQueryEngine<S> engine,
+ List<CursorFactory<S>> factories,
+ OrderedProperty<S>[] totalOrderings)
+ {
+ Comparator<S> orderComparator = engine.makeComparator(totalOrderings);
+ return createUnion(engine, factories, totalOrderings, orderComparator);
+ }
+
+ @SuppressWarnings("unchecked")
+ static <S extends Storable> CursorFactory<S> createUnion
+ (BaseQueryEngine<S> engine,
+ List<CursorFactory<S>> factories,
+ OrderedProperty<S>[] totalOrderings,
+ Comparator<S> orderComparator)
+ {
+ if (factories.size() > 1) {
+ CursorFactory<S>[] array = new CursorFactory[factories.size()];
+ factories.toArray(array);
+ return new UnionedCursorFactory<S>(engine, array, totalOrderings, orderComparator);
+ }
+ return factories.get(0);
+ }
+
+ private final CursorFactory<S>[] mFactories;
+ private final OrderedProperty<S>[] mTotalOrderings;
+ private final Comparator<S> mOrderComparator;
+
+ private UnionedCursorFactory(BaseQueryEngine<S> engine,
+ CursorFactory<S>[] factories,
+ OrderedProperty<S>[] totalOrderings,
+ Comparator<S> orderComparator) {
+ super(engine);
+ mFactories = factories;
+ mTotalOrderings = totalOrderings;
+ mOrderComparator = orderComparator;
+ }
+
+ public Cursor<S> openCursor(FilterValues<S> values) throws FetchException {
+ Cursor<S> cursor = null;
+ for (CursorFactory<S> factory : mFactories) {
+ Cursor<S> subCursor = factory.openCursor(values);
+ cursor = (cursor == null) ? subCursor
+ : new UnionCursor<S>(cursor, subCursor, mOrderComparator);
+ }
+ return cursor;
+ }
+
+ public Filter<S> buildFilter(Filter<S> filter) {
+ for (CursorFactory<S> factory : mFactories) {
+ Filter<S> subFilter = factory.buildFilter(null);
+ filter = filter == null ? subFilter : filter.or(subFilter);
+ }
+ return filter;
+ }
+
+ public Query<S> applyOrderBy(Query<S> 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<S> getActualStorage() {
+ Storage<S> storage = null;
+ for (CursorFactory<S> factory : mFactories) {
+ Storage<S> subStorage = factory.getActualStorage();
+ if (storage == null) {
+ storage = subStorage;
+ } else if (storage != subStorage) {
+ return null;
+ }
+ }
+ return storage;
+ }
+
+ @Override
+ public CursorFactory<S> getActualFactory() throws FetchException {
+ Storage<S> 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<S>(this, requiredStorage);
+ }
+
+ // Group factories by required storage instance, and then create a
+ // union of unions.
+
+ Comparator<CursorFactory<S>> comparator = new Comparator<CursorFactory<S>>() {
+ public int compare(CursorFactory<S> a, CursorFactory<S> b) {
+ Storage<S> aStorage = a.getActualStorage();
+ Storage<S> bStorage = b.getActualStorage();
+ if (aStorage == bStorage) {
+ return 0;
+ }
+ Storage<S> 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<CursorFactory<S>> masterList = new ArrayList<CursorFactory<S>>();
+
+ List<CursorFactory<S>> subList = new ArrayList<CursorFactory<S>>();
+ Storage<S> group = null;
+ for (CursorFactory<S> factory : mFactories) {
+ Storage<S> storage = factory.getActualStorage();
+ if (group != storage) {
+ if (subList.size() > 0) {
+ masterList.add(createUnion
+ (mEngine, subList, mTotalOrderings, mOrderComparator));
+ subList.clear();
+ }
+ group = storage;
+ }
+ CursorFactory<S> subFactory = new QueryCursorFactory<S>(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<S> values)
+ throws IOException
+ {
+ indent(app, indentLevel);
+ app.append("union");
+ app.append('\n');
+ for (CursorFactory<S> factory : mFactories) {
+ factory.printPlan(app, indentLevel + 2, values);
+ }
+ return true;
+ }
+ }
+
+ /**
+ * CursorFactory implementation that reconstructs and calls an external
+ * Query.
+ */
+ private static class QueryCursorFactory<S extends Storable> implements CursorFactory<S> {
+ private final CursorFactory<S> mFactory;
+ private final Storage<S> mStorage;
+ private final Query<S> mQuery;
+
+ /**
+ * @param factory factory to derive this factory from
+ * @param storage actual storage to query against
+ */
+ QueryCursorFactory(CursorFactory<S> factory, Storage<S> storage) throws FetchException {
+ mFactory = factory;
+ mStorage = storage;
+
+ Filter<S> filter = factory.buildFilter(null);
+
+ Query<S> query;
+ if (filter == null) {
+ query = storage.query();
+ } else {
+ query = storage.query(filter);
+ }
+
+ mQuery = factory.applyOrderBy(query);
+ }
+
+ public Cursor<S> openCursor(FilterValues<S> values) throws FetchException {
+ return applyFilterValues(values).fetch();
+ }
+
+ public long count(FilterValues<S> values) throws FetchException {
+ return applyFilterValues(values).count();
+ }
+
+ public Filter<S> buildFilter(Filter<S> filter) {
+ return mFactory.buildFilter(filter);
+ }
+
+ public Query<S> applyOrderBy(Query<S> query) throws FetchException {
+ return mFactory.applyOrderBy(query);
+ }
+
+ public Storage<S> getActualStorage() {
+ return mStorage;
+ }
+
+ public CursorFactory<S> getActualFactory() {
+ return this;
+ }
+
+ public boolean printNative(Appendable app, int indentLevel, FilterValues<S> values)
+ throws IOException
+ {
+ return applyFilterValues(values).printNative(app, indentLevel);
+ }
+
+ public boolean printPlan(Appendable app, int indentLevel, FilterValues<S> values)
+ throws IOException
+ {
+ Query<S> query;
+ try {
+ query = applyFilterValues(values);
+ } catch (IllegalStateException e) {
+ query = mQuery;
+ }
+ return query.printPlan(app, indentLevel);
+ }
+
+ private Query<S> applyFilterValues(FilterValues<S> values) {
+ // FIXME: figure out how to transfer values directly to query.
+
+ Query<S> query = mQuery;
+ Filter<S> 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<S extends Storable> extends Visitor<S, Object, Object>
+ implements Comparable<IndexAnalysis<?>>
+ {
+ private final StorableIndex<S> mPrimaryKeyIndex;
+ private final StorableIndexSet<S> mIndexSet;
+ private final OrderedProperty<S>[] mOrderings;
+
+ private List<IndexSelector.IndexFitness<S>> mResults;
+
+ IndexAnalysis(StorableIndex<S> primaryKeyIndex,
+ StorableIndexSet<S> indexSet,
+ OrderedProperty<S>[] orderings)
+ {
+ mPrimaryKeyIndex = primaryKeyIndex;
+ mIndexSet = indexSet;
+ mOrderings = orderings;
+ mResults = new ArrayList<IndexSelector.IndexFitness<S>>();
+ }
+
+ public Object visit(OrFilter<S> filter, Object param) {
+ Filter<S> left = filter.getLeftFilter();
+ if (!(left instanceof OrFilter)) {
+ selectIndex(left);
+ } else {
+ left.accept(this, param);
+ }
+ Filter<S> 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<S> filter, Object param) {
+ selectIndex(filter);
+ return null;
+ }
+
+ // This method should only be called if root filter has no logical operators.
+ public Object visit(PropertyFilter<S> 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 &lt;0 if these results are better, 0 if equal, or &gt;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<IndexSelector.IndexFitness<S>> 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<IndexSelector.IndexFitness<S>> reduced =
+ new ArrayList<IndexSelector.IndexFitness<S>>(mResults.size());
+
+ gather:
+ for (int i=0; i<mResults.size(); i++) {
+ IndexSelector.IndexFitness fitness = mResults.get(i);
+ for (int j=0; j<reduced.size(); j++) {
+ IndexSelector.IndexFitness unioned = fitness.union(reduced.get(j));
+ if (unioned != null) {
+ reduced.set(j, unioned);
+ continue gather;
+ }
+ }
+ // Couldn't union with another use of index, so add it to reduced list.
+ reduced.add(fitness);
+ }
+
+ mResults = reduced;
+ }
+
+ boolean noBestIndex() {
+ // Must be an index for each property filter. No point in unioning
+ // an index scan with a full scan. Just do a full scan.
+ for (IndexSelector.IndexFitness<S> result : mResults) {
+ if (result.isUseless()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void selectIndex(Filter<S> filter) {
+ IndexSelector<S> selector = new IndexSelector<S>(filter, mOrderings);
+
+ StorableIndex<S> best = mPrimaryKeyIndex;
+ if (mIndexSet != null) {
+ for (StorableIndex<S> 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<Repository, SupportException> {
+ 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 <S extends Storable> Storage<S> storageFor(Class<S> 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 extends Capability> C getCapability(Class<C> 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<S extends Storable>
+ extends BelatedCreator<Storage<S>, SupportException>
+{
+ final Log mLog;
+ final Repository mRepo;
+ final Class<S> 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<S> storableType,
+ int minRetryDelayMillis) {
+ // Nice double cast hack, eh?
+ super((Class<Storage<S>>) ((Class) Storage.class), minRetryDelayMillis);
+ mLog = log;
+ mRepo = repo;
+ mStorableType = storableType;
+ }
+
+ protected Storage<S> 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<S> 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<S> {
+ public Class<S> getStorableType() {
+ return mStorableType;
+ }
+
+ public S prepare() {
+ throw error();
+ }
+
+ public Query<S> query() {
+ throw error();
+ }
+
+ public Query<S> query(String filter) {
+ throw error();
+ }
+
+ public Query<S> query(Filter<S> filter) {
+ throw error();
+ }
+
+ public Repository getRepository() {
+ return mRepo;
+ }
+
+ public boolean addTrigger(Trigger<? super S> trigger) {
+ throw error();
+ }
+
+ public boolean removeTrigger(Trigger<? super S> 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<Blob> {
+ 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<Clob> {
+ 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<foo>, all
+ * classes and implemented interfaces for every superclass between foo (the leaf) and
+ * Object (the base).
+ * <P>A copy must be coercible into any of these types, and copy bridge methods must be
+ * provided to do so.
+ *
+ * <P>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<Class> gatherAllBridgeTypes(Set<Class> 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<Class>(), 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<String, Method> gatherAllDeclaredMethods(Class clazz) {
+ Map<String, Method> methods = new HashMap<String, Method>();
+ gatherAllDeclaredMethods(methods, clazz);
+ return methods;
+ }
+
+ private static void gatherAllDeclaredMethods(Map<String, Method> 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:
+ * <ul>
+ * <li>implements Storable</li>
+ * <li>implements Cloneable
+ * <li>abstract if appropriate
+ * <li>marked synthetic
+ * <li>targetted for java version 1.5
+ * </ul>
+ * @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 <S extends Storable> ClassFile createStorableClassFile(
+ ClassInjector ci, Class<S> 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.
+ *
+ * <P>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<Class> {
+ 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 &lt;0 if "a" is nearest, 0 if both are equally good, &gt;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<S extends Storable> implements Comparator<StorableIndex<S>> {
+ 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 <E extends Comparable> int listCompare(List<? extends E> a,
+ List<? extends E> b) {
+ int size = Math.min(a.size(), b.size());
+ for (int i=0; i<size; i++) {
+ int result = a.get(i).compareTo(b.get(i));
+ if (result != 0) {
+ return result;
+ }
+ }
+ if (a.size() < size) {
+ return -1;
+ }
+ if (a.size() > size) {
+ return 1;
+ }
+ return 0;
+ }
+
+ // Original filter passed into constructor
+ private final Filter<S> 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<S>[] mFilters;
+
+ final OrderedProperty<S>[] mOrderings;
+
+ /**
+ * @param filter filter which cannot contain any logical 'or' operations.
+ * @throws IllegalArgumentException if filter not supported
+ */
+ public IndexSelector(Filter<S> filter) {
+ this(filter, (OrderedProperty<S>[]) 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<S> filter, OrderedProperty<S>... orderings) {
+ mFilter = filter;
+
+ // Copy property filters.
+ final List<PropertyFilter<S>> filterList = new ArrayList<PropertyFilter<S>>();
+
+ if (filter != null) {
+ filter.accept(new Visitor<S, Object, Object>() {
+ public Object visit(OrFilter<S> filter, Object param) {
+ throw new IllegalArgumentException("Logical 'or' not allowed");
+ }
+
+ public Object visit(PropertyFilter<S> 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<S>());
+
+ if (orderings == null || orderings.length == 0) {
+ mOrderings = null;
+ } else {
+ // Copy ordering properties, but don't duplicate properties.
+ int length = orderings.length;
+ Map<ChainedProperty<S>, OrderedProperty<S>> orderingMap =
+ new LinkedHashMap<ChainedProperty<S>, OrderedProperty<S>>(length);
+ for (int i=0; i<length; i++) {
+ OrderedProperty<S> ordering = orderings[i];
+ if (ordering != null) {
+ ChainedProperty<S> 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<S> propFilter : filterList) {
+ if (propFilter.getOperator() == RelOp.EQ) {
+ orderingMap.remove(propFilter.getChainedProperty());
+ }
+ }
+
+ mOrderings = orderingMap.values().toArray(new OrderedProperty[orderingMap.size()]);
+ }
+ }
+
+ /**
+ * Returns &lt;0 if the current index is better than the candidate index, 0
+ * if they are equally good, or &gt;0 if the candidate index is
+ * better.
+ * <p>
+ * 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<S> currentIndex, StorableIndex<S> candidateIndex) {
+ if (currentIndex == null) {
+ if (candidateIndex == null) {
+ return 0;
+ } else {
+ return 1;
+ }
+ } else if (candidateIndex == null) {
+ return -1;
+ }
+
+ IndexScore<S> currentScore = new IndexScore<S>(this, currentIndex);
+ IndexScore<S> candidateScore = new IndexScore<S>(this, candidateIndex);
+
+ return currentScore.compareTo(candidateScore);
+ }
+
+ /**
+ * Examines the given index for overall fitness.
+ */
+ public IndexFitness<S> examine(StorableIndex<S> index) {
+ return new IndexFitness<S>(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<S extends Storable> implements Comparable<IndexFitness<?>> {
+ private final StorableIndex<S> mIndex;
+ private final IndexScore<S> mIndexScore;
+
+ private final Filter<S> mExactFilter;
+ private final PropertyFilter<S>[] mInclusiveRangeStartFilters;
+ private final PropertyFilter<S>[] mExclusiveRangeStartFilters;
+ private final PropertyFilter<S>[] mInclusiveRangeEndFilters;
+ private final PropertyFilter<S>[] mExclusiveRangeEndFilters;
+ private final Filter<S> mRemainderFilter;
+
+ private final OrderedProperty<S>[] mHandledOrderings;
+ private final OrderedProperty<S>[] mRemainderOrderings;
+
+ private final boolean mShouldReverseOrder;
+ private final boolean mShouldReverseRange;
+
+ @SuppressWarnings("unchecked")
+ IndexFitness(IndexSelector<S> selector, StorableIndex<S> index,
+ Filter<S> fullFilter, PropertyFilter<S>[] fullFilters,
+ OrderedProperty<S>[] fullOrderings)
+ {
+ mIndex = index;
+ mIndexScore = new IndexScore<S>(selector, index);
+
+ FilterScore filterScore = mIndexScore.getFilterScore();
+
+ Filter<S> exactFilter;
+ List<PropertyFilter<S>> inclusiveRangeStartFilters =
+ new ArrayList<PropertyFilter<S>>();
+ List<PropertyFilter<S>> exclusiveRangeStartFilters =
+ new ArrayList<PropertyFilter<S>>();
+ List<PropertyFilter<S>> inclusiveRangeEndFilters = new ArrayList<PropertyFilter<S>>();
+ List<PropertyFilter<S>> exclusiveRangeEndFilters = new ArrayList<PropertyFilter<S>>();
+ Filter<S> remainderFilter;
+
+ Direction rangeDirection = null;
+ buildFilters: {
+ if (fullFilter == null) {
+ exactFilter = null;
+ remainderFilter = fullFilter;
+ break buildFilters;
+ }
+
+ int exactMatches = filterScore.exactMatches();
+ int indexPos = 0;
+
+ LinkedList<PropertyFilter<S>> filterList =
+ new LinkedList<PropertyFilter<S>>(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<exactMatches; i++) {
+ StorableProperty<S> indexProp = index.getProperty(indexPos++);
+ Filter<S> 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<S> indexProp = index.getProperty(indexPos);
+ rangeDirection = index.getPropertyDirection(indexPos);
+
+ while (true) {
+ PropertyFilter<S> p = removeIndexProp(filterList, indexProp, RelOp.GE);
+ if (p == null) {
+ break;
+ }
+ inclusiveRangeStartFilters.add(p);
+ }
+
+ while (true) {
+ PropertyFilter<S> p = removeIndexProp(filterList, indexProp, RelOp.GT);
+ if (p == null) {
+ break;
+ }
+ exclusiveRangeStartFilters.add(p);
+ }
+
+ while (true) {
+ PropertyFilter<S> p = removeIndexProp(filterList, indexProp, RelOp.LE);
+ if (p == null) {
+ break;
+ }
+ inclusiveRangeEndFilters.add(p);
+ }
+
+ while (true) {
+ PropertyFilter<S> p = removeIndexProp(filterList, indexProp, RelOp.LT);
+ if (p == null) {
+ break;
+ }
+ exclusiveRangeEndFilters.add(p);
+ }
+ }
+
+ remainderFilter = null;
+ while (filterList.size() > 0) {
+ Filter<S> 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<S>[] handledOrderings;
+ OrderedProperty<S>[] 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<OrderedProperty<S>> handledSet = new LinkedHashSet<OrderedProperty<S>>();
+ Set<OrderedProperty<S>> remainderSet =
+ new LinkedHashSet<OrderedProperty<S>>(Arrays.asList(fullOrderings));
+
+ for (int i=0; i<totalMatches; i++) {
+ ChainedProperty<S> chainedProp =
+ ChainedProperty.get(index.getProperty(pos + i));
+ OrderedProperty<S> 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<S> index, IndexScore<S> indexScore,
+ Filter<S> exactFilter,
+ PropertyFilter<S>[] inclusiveRangeStartFilters,
+ PropertyFilter<S>[] exclusiveRangeStartFilters,
+ PropertyFilter<S>[] inclusiveRangeEndFilters,
+ PropertyFilter<S>[] exclusiveRangeEndFilters,
+ Filter<S> remainderFilter,
+ OrderedProperty<S>[] handledOrderings,
+ OrderedProperty<S>[] 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<S> removeIndexProp(List<PropertyFilter<S>> filterList,
+ StorableProperty<S> indexProp,
+ RelOp operator)
+ {
+ Iterator<PropertyFilter<S>> it = filterList.iterator();
+ while (it.hasNext()) {
+ PropertyFilter<S> filter = it.next();
+
+ if (operator != filter.getOperator()) {
+ continue;
+ }
+
+ ChainedProperty<S> chainedProp = filter.getChainedProperty();
+ if (chainedProp.getChainCount() == 0) {
+ StorableProperty<S> prime = chainedProp.getPrimeProperty();
+ if (indexProp.equals(prime)) {
+ it.remove();
+ return filter;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the index that this fitness object applies to.
+ */
+ public StorableIndex<S> 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<S> 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<StorableProperty<S>> properties;
+ {
+ int propertyCount = mIndex.getPropertyCount();
+ properties = new HashSet<StorableProperty<S>>(propertyCount);
+ for (int i=0; i<propertyCount; i++) {
+ properties.add(mIndex.getProperty(i));
+ }
+ }
+
+ mExactFilter.accept(new Visitor<S, Object, Object>() {
+ public Object visit(PropertyFilter<S> filter, Object param) {
+ ChainedProperty<S> 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<S>[] 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<S>[] 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<S>[] 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<S>[] 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<S> getRemainderFilter() {
+ return mRemainderFilter;
+ }
+
+ /**
+ * Returns the desired orderings handled by the applicable index,
+ * possibly when reversed.
+ */
+ public OrderedProperty<S>[] 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<S>[] 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<S>[] 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<S>[] getEffectiveOrderings() {
+ return getActualOrderings(true);
+ }
+
+ @SuppressWarnings("unchecked")
+ private OrderedProperty<S>[] getActualOrderings(boolean excludeExactMatches) {
+ int exactMatches = 0;
+ if (excludeExactMatches) {
+ exactMatches = mIndexScore.getFilterScore().exactMatches();
+ }
+
+ int count = mIndex.getPropertyCount();
+ OrderedProperty<S>[] orderings = new OrderedProperty[count - exactMatches];
+ for (int i=exactMatches; i<count; i++) {
+ StorableProperty<S> 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 &lt;0 if this score is better, 0 if equal, or &gt;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<S> 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<S>[] 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<S>(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<S extends Storable> implements Comparable<IndexScore<?>> {
+ private final IndexSelector<S> mSelector;
+ private final StorableIndex<S> mIndex;
+
+ private FilterScore<S> mFilterScore;
+ private OrderingScore mOrderingScore;
+
+ IndexScore(IndexSelector<S> selector, StorableIndex<S> 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<S> getFilterScore() {
+ if (mFilterScore != null) {
+ return mFilterScore;
+ }
+
+ mFilterScore = new FilterScore<S>();
+
+ int indexPropCount = mIndex.getPropertyCount();
+ PropertyFilter<S>[] filters = mSelector.mFilters;
+ int filterCount = filters.length;
+
+ for (int i=0; i<indexPropCount; i++) {
+ StorableProperty<S> prop = mIndex.getProperty(i);
+ int matchesBefore = mFilterScore.totalMatches();
+ for (int pos=0; pos<filterCount; pos++) {
+ mFilterScore.tally(prop, filters[pos], pos);
+ }
+ if (mFilterScore.totalMatches() <= matchesBefore) {
+ // Missed an index property and cannot have holes in index.
+ break;
+ }
+ }
+
+ return mFilterScore;
+ }
+
+ public OrderingScore getOrderingScore() {
+ if (mOrderingScore != null) {
+ return mOrderingScore;
+ }
+
+ OrderedProperty<S>[] 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<S> 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<orderings.length && ++pos<indexPropCount; i++) {
+ if (orderings[i].getChainedProperty().getChainCount() > 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.
+ * <P>A filter mentions properties, either as exact ("=") or inexact (">" "<", et al)
+ * <P>An index contains properties, in order.
+ * <P>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)
+ * <P>Then the index filter score is a function of the number of matches it contains,
+ * and how early in the filter they appear.
+ * <P>Any exact filter match beats an inexact filter.
+ * <P>More exact filter matches beats fewer.
+ * <P>Inexact will be selected if there are no exact matches
+ *
+ * <P>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).
+ *
+ * <P>For example:
+ * <pre>
+ * 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
+ * </pre>
+ * ...so the "c,d,b" index will win.
+ */
+ private static class FilterScore<S extends Storable> {
+ // Positions of exact matches
+ private List<Integer> mExactMatches = new ArrayList<Integer>();
+
+ // Properties which have been used for exact matching -- these should
+ // show up only once per filter set
+ private Set<StorableProperty> mExactMatchProps = new HashSet<StorableProperty>();
+
+ // Position of inexact match
+ private int mInexactMatchPos;
+
+ // Property used for inexact match
+ private StorableProperty<S> 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<S> prop, PropertyFilter<S> filter, int pos) {
+ ChainedProperty<S> 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<S> candidate) {
+ return -intCompare(mExactMatches.size(), candidate.mExactMatches.size());
+ }
+
+ int compareInexact(FilterScore<S> candidate) {
+ if (mInexactMatch == null && candidate.mInexactMatch != null) {
+ return 1;
+ } else if (mInexactMatch != null && candidate.mInexactMatch == null) {
+ return -1;
+ }
+ return 0;
+ }
+
+ int compareExactPositions(FilterScore<S> candidate) {
+ return listCompare(mExactMatches, candidate.mExactMatches);
+ }
+
+ int compareInexactPosition(FilterScore<S> 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<S extends Storable>
+ implements Comparator<PropertyFilter<S>>
+ {
+ public int compare(PropertyFilter<S> a, PropertyFilter<S> 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 <S extends Storable> boolean hasLobs(Class<S> type) {
+ StorableInfo<S> info = StorableIntrospector.examine(type);
+ for (StorableProperty<? extends S> 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<StoredLob> mLobStorage;
+ final Storage<StoredLob.Block> 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 <i>bytes</i>) 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 <i>bytes</i>) 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 <S extends Storable> Trigger<S>
+ getSupportTrigger(Class<S> type, int blockSize)
+ {
+ Object key = KeyFactory.createKey(new Object[] {type, blockSize});
+
+ Trigger<S> trigger = (mTriggers == null) ? null : (Trigger<S>) mTriggers.get(key);
+
+ if (trigger == null) {
+ StorableInfo<S> info = StorableIntrospector.examine(type);
+
+ List<LobProperty<?>> lobProperties = null;
+
+ for (StorableProperty<? extends S> prop : info.getAllProperties().values()) {
+ if (Blob.class.isAssignableFrom(prop.getType())) {
+ if (lobProperties == null) {
+ lobProperties = new ArrayList<LobProperty<?>>();
+ }
+ lobProperties.add(new BlobProperty(this, prop.getName()));
+ } else if (Clob.class.isAssignableFrom(prop.getType())) {
+ if (lobProperties == null) {
+ lobProperties = new ArrayList<LobProperty<?>>();
+ }
+ lobProperties.add(new ClobProperty(this, prop.getName()));
+ }
+ }
+
+ if (lobProperties != null) {
+ trigger = new LobEngineTrigger<S>(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<StoredLob.Block> 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<S extends Storable> extends Trigger<S> {
+ final LobEngine mEngine;
+ private final int mBlockSize;
+ private final BeanPropertyAccessor mAccessor;
+ private final LobProperty<Lob>[] mLobProperties;
+
+ LobEngineTrigger(LobEngine engine, Class<S> type, int blockSize,
+ List<LobProperty<?>> 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<length; i++) {
+ LobProperty<Lob> 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<length; i++) {
+ Object userLob = userLobs[i];
+ if (userLob != null) {
+ LobProperty<Lob> 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<length; i++) {
+ LobProperty<Lob> 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<Lob> 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<length; i++) {
+ Object userLob = userLobs[i];
+ if (userLob != null) {
+ mAccessor.setPropertyValue(storable, mLobProperties[i].mName, userLob);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/spi/LobProperty.java b/src/main/java/com/amazon/carbonado/spi/LobProperty.java
new file mode 100644
index 0000000..d21acfb
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/spi/LobProperty.java
@@ -0,0 +1,44 @@
+/*
+ * 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.lob.Lob;
+
+/**
+ *
+ *
+ * @author Brian S O'Neill
+ * @see LobEngine
+ * @see LobEngineTrigger
+ */
+abstract class LobProperty<L extends Lob> {
+ 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<S extends Storable> {
+ // 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<Object, Class<? extends Storable>> 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:
+ *
+ * <pre>
+ * public &lt;init&gt;(MasterSupport);
+ * </pre>
+ *
+ * Subclasses must implement the following abstract protected methods,
+ * whose exact names are defined by constants in this class:
+ *
+ * <pre>
+ * // 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;
+ * </pre>
+ *
+ * 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 <S extends Storable> Class<? extends S>
+ getAbstractClass(Class<S> type, EnumSet<MasterFeature> features)
+ throws SupportException, IllegalArgumentException
+ {
+ StorableInfo<S> info = StorableIntrospector.examine(type);
+
+ anySequences:
+ if (features.contains(MasterFeature.INSERT_SEQUENCES)) {
+ for (StorableProperty<S> 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<? extends S> abstractClass = (Class<? extends S>) cCache.get(key);
+ if (abstractClass != null) {
+ return abstractClass;
+ }
+ abstractClass =
+ new MasterStorableGenerator<S>(type, features).generateAndInjectClass();
+ cCache.put(key, abstractClass);
+ return abstractClass;
+ }
+ }
+
+ private final EnumSet<MasterFeature> mFeatures;
+ private final StorableInfo<S> mInfo;
+ private final Map<String, ? extends StorableProperty<S>> mAllProperties;
+
+ private final ClassInjector mClassInjector;
+ private final ClassFile mClassFile;
+
+ private MasterStorableGenerator(Class<S> storableType, EnumSet<MasterFeature> features) {
+ mFeatures = features;
+ mInfo = StorableIntrospector.examine(storableType);
+ mAllProperties = mInfo.getAllProperties();
+
+ final Class<? extends S> 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<? extends S> generateAndInjectClass() throws SupportException {
+ generateClass();
+ Class abstractClass = mClassInjector.defineClass(mClassFile);
+ return (Class<? extends S>) 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<S> 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<MasterFeature> 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<MasterFeature> 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<S extends Storable> extends TriggerSupport<S> {
+ /**
+ * Returns a sequence value producer by name, or throw PersistException if not found.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>The following system properties are supported:
+ *
+ * <ul>
+ * <li>com.amazon.carbonado.spi.RepairExecutor.keepAliveSeconds (default is 10)
+ * <li>com.amazon.carbonado.spi.RepairExecutor.queueSize (default is 10000)
+ * </ul>
+ *
+ * @author Brian S O'Neill
+ */
+public class RepairExecutor {
+ static final ThreadLocal<RepairExecutor> 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<RepairExecutor>() {
+ 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<Runnable> mQueue;
+ private Worker mWorker;
+ private boolean mIdle = true;
+
+ private RepairExecutor(int keepAliveSeconds, int queueSize) {
+ mKeepAliveSeconds = keepAliveSeconds;
+ mQueue = new LinkedBlockingQueue<Runnable>(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.
+ *
+ * <P>A typical use pattern would be:
+ *
+ * <pre>
+ * RunnableTransaction rt = new RunnableTransaction(repository) {
+ * public void body() throws PersistException {
+ * for (Storable s : someFieldContainingStorables) {
+ * s.insert();
+ * }
+ * };
+ * rt.run();
+ * </pre>
+ *
+ * @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 <S extends Storable> 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 <S extends Storable> 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 <S extends Storable> 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<StoredSequence> 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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<S extends Storable> {
+
+ // 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<Class, Reference<Class<? extends Storable>>> cAbstractCache;
+ // Cache of generated wrapped classes.
+ private static Map<Class, Reference<Class<? extends Storable>>> 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:
+ *
+ * <pre>
+ * /**
+ * * @param support Access to triggers
+ * *&#047;
+ * public &lt;init&gt;(TriggerSupport support);
+ * </pre>
+ *
+ * <p>Subclasses must implement the following abstract protected methods,
+ * whose exact names are defined by constants in this class:
+ *
+ * <pre>
+ * // 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;
+ * </pre>
+ *
+ * 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.
+ *
+ * <pre>
+ * // 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;
+ * </pre>
+ *
+ * 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <pre>
+ * // 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();
+ * </pre>
+ *
+ * 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 <S extends Storable> Class<? extends S> getAbstractClass(Class<S> type)
+ throws IllegalArgumentException
+ {
+ synchronized (cAbstractCache) {
+ Class<? extends S> abstractClass;
+ Reference<Class<? extends Storable>> ref = cAbstractCache.get(type);
+ if (ref != null) {
+ abstractClass = (Class<? extends S>) ref.get();
+ if (abstractClass != null) {
+ return abstractClass;
+ }
+ }
+ abstractClass = new StorableGenerator<S>(type, GEN_ABSTRACT).generateAndInjectClass();
+ cAbstractCache.put(type, new SoftReference<Class<? extends Storable>>(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:
+ *
+ * <pre>
+ * /**
+ * * @param support Custom implementation for Storable CRUD operations
+ * * @param storable Storable being wrapped
+ * *&#047;
+ * public &lt;init&gt;(WrappedSupport support, Storable storable);
+ * </pre>
+ *
+ * <p>Instances of the wrapped Storable delegate to the WrappedSupport for
+ * all CRUD operations:
+ *
+ * <ul>
+ * <li>load and tryLoad
+ * <li>insert and tryInsert
+ * <li>update and tryUpdate
+ * <li>delete and tryDelete
+ * </ul>
+ *
+ * <p>Methods which delegate to wrapped Storable:
+ *
+ * <ul>
+ * <li>all ordinary user-defined properties
+ * <li>copyAllProperties
+ * <li>copyPrimaryKeyProperties
+ * <li>copyVersionProperty
+ * <li>copyUnequalProperties
+ * <li>copyDirtyProperties
+ * <li>hasDirtyProperties
+ * <li>markPropertiesClean
+ * <li>markAllPropertiesClean
+ * <li>markPropertiesDirty
+ * <li>markAllPropertiesDirty
+ * <li>hashCode
+ * <li>equalPrimaryKeys
+ * <li>equalProperties
+ * <li>toString
+ * <li>toStringKeyOnly
+ * </ul>
+ *
+ * <p>Methods with special implementation:
+ *
+ * <ul>
+ * <li>all user-defined join properties (join properties query using wrapper's Storage)
+ * <li>storage (returns Storage used by wrapper)
+ * <li>storableType (returns literal class)
+ * <li>copy (delegates to wrapped storable, but bridge methods must be defined as well)
+ * <li>equals (compares Storage instance and properties)
+ * </ul>
+ *
+ * @throws com.amazon.carbonado.MalformedTypeException if Storable type is not well-formed
+ * @throws IllegalArgumentException if type is null
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> Class<? extends S> getWrappedClass(Class<S> type)
+ throws IllegalArgumentException
+ {
+ synchronized (cWrappedCache) {
+ Class<? extends S> wrappedClass;
+ Reference<Class<? extends Storable>> ref = cWrappedCache.get(type);
+ if (ref != null) {
+ wrappedClass = (Class<? extends S>) ref.get();
+ if (wrappedClass != null) {
+ return wrappedClass;
+ }
+ }
+ wrappedClass = new StorableGenerator<S>(type, GEN_WRAPPED).generateAndInjectClass();
+ cWrappedCache.put(type, new SoftReference<Class<? extends Storable>>(wrappedClass));
+ return wrappedClass;
+ }
+ }
+
+ private final Class<S> mStorableType;
+ private final int mGenMode;
+ private final TypeDesc mSupportType;
+ private final StorableInfo<S> mInfo;
+ private final Map<String, ? extends StorableProperty<S>> mAllProperties;
+ private final boolean mHasJoins;
+
+ private final ClassInjector mClassInjector;
+ private final ClassFile mClassFile;
+
+ private StorableGenerator(Class<S> 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<? extends S> generateAndInjectClass() {
+ generateClass();
+ Class abstractClass = mClassInjector.defineClass(mClassFile);
+ return (Class<? extends S>) 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 <user storable> 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<count; i++) {
+ StorablePropertyConstraint spc = property.getConstraint(i);
+ String fieldName = property.getName() + CONSTRAINT_FIELD_ELEMENT + i;
+ TypeDesc constraintType = TypeDesc.forClass
+ (spc.getConstraintConstructor().getDeclaringClass());
+ mClassFile.addField(fieldModifiers, fieldName, constraintType);
+
+ if (clinit == null) {
+ clinit = new CodeBuilder(mClassFile.addInitializer());
+ }
+
+ // Assign value to new field.
+ // admin$constraint$0 = new LengthConstraint.Constraint
+ // (UserInfo.class, "firstName", annotation);
+
+ clinit.newObject(constraintType);
+ clinit.dup();
+ clinit.loadConstant(TypeDesc.forClass(mStorableType));
+ clinit.loadConstant(property.getName());
+
+ // Generate code to load property annotation third parameter.
+ loadPropertyAnnotation(clinit, property, spc.getAnnotation());
+
+ clinit.invoke(spc.getConstraintConstructor());
+ clinit.storeStaticField(fieldName, constraintType);
+ }
+ }
+
+ if (clinit != null) {
+ // Must return else verifier complains.
+ clinit.returnVoid();
+ }
+ }
+
+ // Add property fields and methods.
+ // Also remember ordinal of optional version property for use later.
+ int versionOrdinal = -1;
+ {
+ int ordinal = -1;
+ int maxOrdinal = mAllProperties.size() - 1;
+ boolean requireStateField = false;
+
+ for (StorableProperty<S> 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<count; i++) {
+ StorableProperty internal = property.getInternalJoinElement(i);
+ StorableProperty external = property.getExternalJoinElement(i);
+ if (!internal.getType().isPrimitive() &&
+ external.getType().isPrimitive()) {
+ break nullPossible;
+ }
+ }
+ break buildShortCircuit;
+ }
+
+ for (int i=0; i<count; i++) {
+ StorableProperty internal = property.getInternalJoinElement(i);
+ StorableProperty external = property.getExternalJoinElement(i);
+ if (!internal.getType().isPrimitive() &&
+ external.getType().isPrimitive()) {
+
+ 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());
+ }
+
+ Label notNull = b.createLabel();
+ b.ifNullBranch(notNull, false);
+ b.loadNull();
+ b.storeLocal(join);
+ b.branch(shortCircuit);
+ notNull.setLocation();
+ }
+ }
+ }
+
+ // Get the storage for the join type.
+ loadStorageForFetch(b, TypeDesc.forClass(property.getJoinedType()));
+ TypeDesc storageType = TypeDesc.forClass(Storage.class);
+
+ // There are two ways that property can be loaded. The
+ // general form is to use a Query. Calling load on the
+ // property itself is preferred, but it is only
+ // possible if the join is against a key and all
+ // external properties have a write method.
+
+ boolean canUseDirectForm = !property.isQuery();
+
+ if (canUseDirectForm) {
+ int joinCount = property.getJoinElementCount();
+ for (int i=0; i<joinCount; i++) {
+ StorableProperty external = property.getExternalJoinElement(i);
+ if (external.getWriteMethod() == null) {
+ canUseDirectForm = false;
+ }
+ }
+ }
+
+ final TypeDesc storableDesc = TypeDesc.forClass(Storable.class);
+
+ if (canUseDirectForm) {
+ // Generate direct load form.
+
+ // Storage instance is already on the stack... replace it
+ // with an instance of the joined type.
+ b.invokeInterface
+ (storageType, PREPARE_METHOD_NAME, storableDesc, null);
+ b.checkCast(type);
+ b.storeLocal(join);
+
+ // Set the keys on the joined type.
+ int count = property.getJoinElementCount();
+ for (int i=0; i<count; i++) {
+ b.loadLocal(join);
+ StorableProperty internal = property.getInternalJoinElement(i);
+ StorableProperty external = property.getExternalJoinElement(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());
+ }
+ CodeBuilderUtil.convertValue
+ (b, internal.getType(), external.getType());
+ b.invoke(external.getWriteMethod());
+ }
+
+ // Now load the object.
+ b.loadLocal(join);
+ if (!property.isNullable()) {
+ b.invokeInterface(storableDesc, LOAD_METHOD_NAME, null, null);
+ } else {
+ b.invokeInterface
+ (storableDesc, TRY_LOAD_METHOD_NAME, TypeDesc.BOOLEAN, null);
+ Label wasLoaded = b.createLabel();
+ b.ifZeroComparisonBranch(wasLoaded, "!=");
+ // Not loaded, so replace joined object with null.
+ b.loadNull();
+ b.storeLocal(join);
+ wasLoaded.setLocation();
+ }
+ } else {
+ // Generate query load form.
+
+ // Storage instance is already on the stack... replace it
+ // with a Query. First, we need to define the query string.
+
+ StringBuilder queryBuilder = new StringBuilder();
+
+ // Set the keys on the joined type.
+ int count = property.getJoinElementCount();
+ for (int i=0; i<count; i++) {
+ if (i > 0) {
+ queryBuilder.append(" & ");
+ }
+ queryBuilder.append(property.getExternalJoinElement(i).getName());
+ queryBuilder.append(" = ?");
+ }
+
+ b.loadConstant(queryBuilder.toString());
+ TypeDesc queryType = TypeDesc.forClass(Query.class);
+ b.invokeInterface(storageType, QUERY_METHOD_NAME, queryType,
+ new TypeDesc[]{TypeDesc.STRING});
+
+ // Now fill in the parameters of the query.
+ for (int i=0; i<count; i++) {
+ StorableProperty<S> 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<count; i++) {
+ StorableProperty internal = property.getInternalJoinElement(i);
+ StorableProperty external = property.getExternalJoinElement(i);
+
+ b.loadLocal(b.getParameter(0));
+ b.invoke(external.getReadMethod());
+ CodeBuilderUtil.convertValue
+ (b, external.getType(), internal.getType());
+
+ LocalVariable newInternalPropVar =
+ b.createLocalVariable(null, TypeDesc.forClass(internal.getType()));
+ b.storeLocal(newInternalPropVar);
+
+ // Since join properties may be pre-loaded, they
+ // are set via the public write method. If internal
+ // property is clean and equal to new value, then
+ // don't set internal property. Doing so would mark
+ // it as dirty, which is not the right behavior
+ // when pre-loading join properties. The internal
+ // properties should remain clean.
+
+ Label setInternalProp = b.createLabel();
+
+ if (mGenMode == GEN_ABSTRACT) {
+ // Access state of internal property directly.
+ int ord = findPropertyOrdinal(internal);
+ b.loadThis();
+ b.loadField(PROPERTY_STATE_FIELD_NAME + (ord >> 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<mInfo.getAlternateKeyCount(); i++) {
+ b.loadThis();
+ b.invokeVirtual(IS_ALT_KEY_INITIALIZED_PREFIX + i, TypeDesc.BOOLEAN, null);
+ Label noAltKey = b.createLabel();
+ b.ifZeroComparisonBranch(noAltKey, "==");
+
+ StorableKey<S> altKey = mInfo.getAlternateKey(i);
+
+ // Form query filter.
+ StringBuilder queryBuilder = new StringBuilder();
+ for (OrderedProperty<S> 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<S> op : altKey.getProperties()) {
+ StorableProperty<S> 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<mInfo.getAlternateKeyCount(); i++) {
+ Map<String, StorableProperty<S>> altProps =
+ new HashMap<String, StorableProperty<S>>();
+
+ StorableKey<S> altKey = mInfo.getAlternateKey(i);
+
+ for (OrderedProperty<S> op : altKey.getProperties()) {
+ ChainedProperty<S> cp = op.getChainedProperty();
+ if (cp.getChainCount() > 0) {
+ // This should not be possible.
+ continue addAltKeyMethods;
+ }
+ StorableProperty<S> property = cp.getPrimeProperty();
+ altProps.put(property.getName(), property);
+ }
+
+ addIsInitializedMethod(IS_ALT_KEY_INITIALIZED_PREFIX + i, altProps);
+ }
+
+ // Define protected isRequiredDataInitialized method.
+ defineIsRequiredDataInitialized: {
+ Map<String, StorableProperty<S>> requiredProperties =
+ new HashMap<String, StorableProperty<S>>();
+
+ 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<count; j++) {
+ b.loadLocal(b.getParameter(j));
+ }
+
+ Method method = lookupMethod(mStorableType, mi);
+ b.invoke(method);
+ if (method.getReturnType() == void.class) {
+ b.returnVoid();
+ } else {
+ b.returnValue(TypeDesc.forClass(method.getReturnType()));
+ }
+ }
+
+ private static Method lookupMethod(Class type, MethodInfo mi) {
+ MethodDesc desc = mi.getMethodDescriptor();
+ TypeDesc[] params = desc.getParameterTypes();
+ Class[] args;
+
+ if (params == null || params.length == 0) {
+ args = null;
+ } else {
+ args = new Class[params.length];
+ for (int i=0; i<args.length; i++) {
+ args[i] = params[i].toClass();
+ }
+ }
+
+ return lookupMethod(type, mi.getName(), args);
+ }
+
+ private static Method lookupMethod(Class type, String name, Class[] args) {
+ try {
+ return type.getMethod(name, args);
+ } catch (NoSuchMethodException e) {
+ Error error = new NoSuchMethodError();
+ error.initCause(e);
+ throw error;
+ }
+ }
+
+ /**
+ * Generates a copy properties method with several options to control its
+ * behavior. Although eight combinations can be defined, only four are
+ * required by Storable interface. Uninitialized properties are never
+ * copied.
+ *
+ * @param pkProperties when true, copy primary key properties
+ * @param dataProperties when true, copy data properties
+ * @param unequalOnly when true, only copy unequal properties
+ * @param dirtyOnly when true, only copy dirty properties
+ */
+ private void addCopyPropertiesMethod
+ (String methodName,
+ boolean pkProperties,
+ boolean versionProperty,
+ boolean dataProperties,
+ boolean unequalOnly,
+ boolean dirtyOnly)
+ {
+ TypeDesc[] param = { TypeDesc.forClass(Storable.class) };
+ TypeDesc storableTypeDesc = TypeDesc.forClass(mStorableType);
+
+ MethodInfo mi= mClassFile.addMethod
+ (Modifiers.PUBLIC.toSynchronized(mGenMode == GEN_ABSTRACT),
+ methodName,
+ null,
+ param);
+
+ if (mGenMode == GEN_WRAPPED) {
+ callWrappedStorable(mi);
+ return;
+ }
+
+ CodeBuilder b = new CodeBuilder(mi);
+
+ LocalVariable target = CodeBuilderUtil.uneraseGenericParameter(b, storableTypeDesc, 0);
+
+ LocalVariable stateBits = null;
+ int ordinal = 0;
+ int mask = PROPERTY_STATE_DIRTY;
+
+ for (StorableProperty property : mAllProperties.values()) {
+ // Decide if property should be part of the copy.
+ boolean shouldCopy = !property.isJoin() &&
+ (property.isPrimaryKeyMember() && pkProperties ||
+ property.isVersion() && versionProperty ||
+ !property.isPrimaryKeyMember() && dataProperties);
+
+ if (shouldCopy) {
+ if (stateBits == null) {
+ // Load state bits into local for quick retrieval.
+ stateBits = b.createLocalVariable(null, TypeDesc.INT);
+ String stateFieldName =
+ StorableGenerator.PROPERTY_STATE_FIELD_NAME + (ordinal >> 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<String, ? extends StorableProperty<S>> 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<S> 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<caseCount; i++) {
+ cases[i] = i;
+ }
+
+ Label[] switchLabels = new Label[caseCount];
+ Label noMatch = b.createLabel();
+ List<StorableProperty<?>>[] caseMatches = caseMatches(caseCount);
+
+ for (int i=0; i<caseCount; i++) {
+ List<?> 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<StorableProperty<?>, Integer> ordinalMap = new HashMap<StorableProperty<?>, 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<caseCount; i++) {
+ List<StorableProperty<?>> matches = caseMatches[i];
+ if (matches == null || matches.size() == 0) {
+ continue;
+ }
+
+ switchLabels[i].setLocation();
+
+ int matchCount = matches.size();
+ for (int j=0; j<matchCount; j++) {
+ StorableProperty<?> 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<StorableProperty<?>>[] caseMatches(int caseCount) {
+ List<StorableProperty<?>>[] 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<StorableProperty<?>>();
+ }
+ 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<S extends Storable> extends TreeSet<StorableIndex<S>> {
+
+ private static final long serialVersionUID = -5840661016235340456L;
+
+ private static final Comparator<StorableIndex<?>> STORABLE_INDEX_COMPARATOR =
+ new StorableIndexComparator();
+
+ public StorableIndexSet() {
+ super(STORABLE_INDEX_COMPARATOR);
+ }
+
+ /**
+ * Copy constructor.
+ */
+ public StorableIndexSet(StorableIndexSet<S> 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<S> 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<S> 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<S> 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.
+ *
+ * <p>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<S> 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<S> key) {
+ if (key == null) {
+ throw new IllegalArgumentException();
+ }
+ add(new StorableIndex<S>(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<StorableIndex<S>> group = new ArrayList<StorableIndex<S>>();
+ Map<StorableIndex<S>, StorableIndex<S>> mergedReplacements =
+ new TreeMap<StorableIndex<S>, StorableIndex<S>>(STORABLE_INDEX_COMPARATOR);
+
+ Iterator<StorableIndex<S>> it = iterator();
+ while (it.hasNext()) {
+ StorableIndex<S> 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<S>, StorableIndex<S>> replacements = null;
+ for (StorableIndex<S> index : this) {
+ StorableIndex<S> replacement = index.setDefaultDirection(defaultDirection);
+ if (replacement != index) {
+ if (replacements == null) {
+ replacements = new HashMap<StorableIndex<S>, StorableIndex<S>>();
+ }
+ 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<S> 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<S> key) {
+ if (key == null) {
+ throw new IllegalArgumentException();
+ }
+
+
+ // Replace indexes which were are implied unique, even if they are not
+ // declared as such.
+ {
+ Map<StorableIndex<S>, StorableIndex<S>> replacements = null;
+ for (StorableIndex<S> index : this) {
+ if (!index.isUnique() && isUniqueImplied(index)) {
+ if (replacements == null) {
+ replacements = new HashMap<StorableIndex<S>, StorableIndex<S>>();
+ }
+ replacements.put(index, index.unique(true));
+ }
+ }
+ replaceEntries(replacements);
+ }
+
+ // Now augment with key properties.
+ {
+ Map<StorableIndex<S>, StorableIndex<S>> replacements = null;
+ for (StorableIndex<S> index : this) {
+ StorableIndex<S> replacement = index.uniquify(key);
+ if (replacement != index) {
+ if (replacements == null) {
+ replacements = new HashMap<StorableIndex<S>, StorableIndex<S>>();
+ }
+ 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<S> findPrimaryKeyIndex(StorableInfo<S> 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<S> findKeyIndex(StorableKey<S> key) {
+ if (key == null) {
+ throw new IllegalArgumentException();
+ }
+
+ Set<? extends OrderedProperty<S>> orderedProps = key.getProperties();
+
+ Set<StorableProperty<S>> keyProps = new HashSet<StorableProperty<S>>();
+ for (OrderedProperty<S> orderedProp : orderedProps) {
+ keyProps.add(orderedProp.getChainedProperty().getPrimeProperty());
+ }
+
+ search: for (StorableIndex<S> 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<S> candidate) {
+ if (candidate.isUnique()) {
+ return true;
+ }
+ if (this.size() <= 1) {
+ return false;
+ }
+
+ Set<StorableProperty<S>> candidateProps = new HashSet<StorableProperty<S>>();
+ for (int i=candidate.getPropertyCount(); --i>=0; ) {
+ candidateProps.add(candidate.getProperty(i));
+ }
+
+ search: for (StorableIndex<S> 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<S> groupLeader, StorableIndex<S> candidate) {
+ int count = candidate.getPropertyCount();
+ if (count > groupLeader.getPropertyCount()) {
+ return true;
+ }
+ for (int i=0; i<count; i++) {
+ StorableProperty aProp = groupLeader.getProperty(i);
+ StorableProperty bProp = candidate.getProperty(i);
+ if (aProp.getName().compareTo(bProp.getName()) != 0) {
+ return true;
+ }
+ }
+ return candidate.isUnique() && (count < groupLeader.getPropertyCount());
+ }
+
+ /**
+ * Returns true if candidate index is less qualified than an existing group
+ * member, or if it was merged with another group member. If it was merged,
+ * then an entry is placed in the merged map, and the given group list is
+ * updated.
+ */
+ private boolean isRedundant(List<StorableIndex<S>> group, StorableIndex<S> candidate,
+ Map<StorableIndex<S>, StorableIndex<S>> 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<StorableIndex<S>> it = group.listIterator();
+ groupScan:
+ while (it.hasNext()) {
+ StorableIndex<S> member = it.next();
+
+ boolean moreQualified = false;
+ boolean canReverse = true;
+ boolean reverse = false;
+
+ for (int i=0; i<count; i++) {
+ Direction candidateOrder = candidate.getPropertyDirection(i);
+ if (candidateOrder == Direction.UNSPECIFIED) {
+ // Property direction is unspecified, so no need to compare
+ // direction. Move on to next property.
+ continue;
+ }
+
+ Direction memberOrder = member.getPropertyDirection(i);
+ if (memberOrder == Direction.UNSPECIFIED) {
+ // Candidate index is more qualified because member
+ // property under examination hasn't specified a
+ // direction. Move on to next property to continue checking
+ // if a merge is possible.
+ moreQualified = true;
+ continue;
+ }
+
+ if (reverse) {
+ candidateOrder = candidateOrder.reverse();
+ }
+
+ if (candidateOrder == memberOrder) {
+ // Direction exactly matches, move on to next property.
+ canReverse = false;
+ continue;
+ }
+
+ // If this point is reached, then the direction would match if
+ // one was reversed. For an index to fully match, all
+ // properties must be reversed.
+
+ if (canReverse) {
+ // Switch to reverse mode and move on to next property.
+ reverse = true;
+ canReverse = false;
+ continue;
+ }
+
+ // Match failed and merge is not possible.
+ continue groupScan;
+ }
+
+ if (moreQualified) {
+ // Candidate is more qualified than all members compared to so
+ // far, but it can be merged. Once merged, it is redundant.
+ Direction[] directions = member.getPropertyDirections();
+ for (int i=0; i<count; i++) {
+ if (directions[i] == Direction.UNSPECIFIED) {
+ Direction direction = candidate.getPropertyDirection(i);
+ directions[i] = reverse ? direction.reverse() : direction;
+ }
+ }
+
+ StorableIndex<S> merged =
+ new StorableIndex<S>(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<S>, StorableIndex<S>> replacements) {
+ if (replacements != null) {
+ for (Map.Entry<StorableIndex<S>, StorableIndex<S>> 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<StorableIndex<?>> {
+ 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<count; i++) {
+ StorableProperty aProp = a.getProperty(i);
+ StorableProperty bProp = b.getProperty(i);
+ int result = aProp.getName().compareTo(bProp.getName());
+ if (aProp.getName().compareTo(bProp.getName()) != 0) {
+ return result;
+ }
+ }
+
+ // Index with more properties is first.
+ if (aCount > 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<count; i++) {
+ if (a.isUnique()) {
+ if (!b.isUnique()) {
+ return -1;
+ }
+ } else if (b.isUnique()) {
+ return 1;
+ }
+
+ Direction aDirection = a.getPropertyDirection(i);
+ Direction bDirection = b.getPropertyDirection(i);
+
+ if (aDirection == bDirection) {
+ continue;
+ }
+
+ // These order in which these tests are performed must not be
+ // altered without careful examination.
+
+ if (aDirection == Direction.ASCENDING) {
+ return -1;
+ }
+ if (bDirection == Direction.ASCENDING) {
+ return 1;
+ }
+ if (aDirection == Direction.DESCENDING) {
+ return -1;
+ }
+ if (bDirection == Direction.DESCENDING) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/spi/StorableSerializer.java b/src/main/java/com/amazon/carbonado/spi/StorableSerializer.java
new file mode 100644
index 0000000..1f6ab6e
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/spi/StorableSerializer.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.spi;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+
+import java.lang.reflect.UndeclaredThrowableException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.cojen.classfile.ClassFile;
+import org.cojen.classfile.CodeBuilder;
+import org.cojen.classfile.LocalVariable;
+import org.cojen.classfile.MethodInfo;
+import org.cojen.classfile.Modifiers;
+import org.cojen.classfile.TypeDesc;
+import org.cojen.util.ClassInjector;
+import org.cojen.util.WeakIdentityMap;
+
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.Storage;
+import com.amazon.carbonado.SupportException;
+
+import com.amazon.carbonado.info.StorableIntrospector;
+import com.amazon.carbonado.info.StorableProperty;
+
+import static com.amazon.carbonado.spi.CommonMethodNames.*;
+
+import com.amazon.carbonado.spi.raw.GenericEncodingStrategy;
+
+/**
+ * Support for general-purpose serialization of storables.
+ * <p>
+ * 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<S extends Storable> {
+ 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<Class, Reference<StorableSerializer<?>>> cCache = new WeakIdentityMap();
+
+ /**
+ * @param type type of storable to serialize
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> StorableSerializer<S> forType(Class<S> type)
+ throws SupportException
+ {
+ synchronized (cCache) {
+ StorableSerializer<S> serializer;
+ Reference<StorableSerializer<?>> ref = cCache.get(type);
+ if (ref != null) {
+ serializer = (StorableSerializer<S>) ref.get();
+ if (serializer != null) {
+ return serializer;
+ }
+ }
+ serializer = generateSerializer(type);
+ cCache.put(type, new SoftReference<StorableSerializer<?>>(serializer));
+ return serializer;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <S extends Storable> StorableSerializer<S> generateSerializer(Class<S> type)
+ throws SupportException
+ {
+ Class<? extends S> 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<String, ? extends StorableProperty<S>> propertyMap =
+ StorableIntrospector.examine(type).getAllProperties();
+
+ StorableProperty<S>[] properties;
+ {
+ // Exclude joins.
+ List<StorableProperty<S>> list =
+ new ArrayList<StorableProperty<S>>(propertyMap.size());
+ for (StorableProperty<S> property : propertyMap.values()) {
+ if (!property.isJoin()) {
+ list.add(property);
+ }
+ }
+ properties = new StorableProperty[list.size()];
+ list.toArray(properties);
+ }
+
+ GenericEncodingStrategy<S> ges = new GenericEncodingStrategy<S>(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<StorableSerializer> clazz = (Class<StorableSerializer>) 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<S> storage, DataInput in) throws IOException, EOFException;
+
+ public abstract S read(Storage<S> 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<S extends Storable> {
+ /**
+ * 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<StoredLob> {
+ @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<Block> 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<Block> {
+ 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<StoredSequence> {
+ 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<Txn> {
+
+ final Lock mLock;
+ final ExceptionTransformer mExTransformer;
+
+ TransactionImpl<Txn> mCurrent;
+
+ // Tracks all registered cursors by storage type.
+ private Map<Class<?>, CursorList<TransactionImpl<Txn>>> 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<Txn> 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<Txn>(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<Txn>(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 <S extends Storable> void register(Class<S> type, Cursor<S> cursor) {
+ mLock.lock();
+ try {
+ checkState();
+ if (mCursors == null) {
+ mCursors = new IdentityHashMap<Class<?>, CursorList<TransactionImpl<Txn>>>();
+ }
+
+ CursorList<TransactionImpl<Txn>> cursorList = mCursors.get(type);
+ if (cursorList == null) {
+ cursorList = new CursorList<TransactionImpl<Txn>>();
+ 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 <S extends Storable> void unregister(Class<S> type, Cursor<S> cursor) {
+ mLock.lock();
+ try {
+ if (mCursors != null) {
+ CursorList<TransactionImpl<Txn>> cursorList = mCursors.get(type);
+ if (cursorList != null) {
+ TransactionImpl<Txn> txnImpl = cursorList.unregister(cursor);
+ if (txnImpl != null) {
+ txnImpl.unregister(cursor);
+ }
+ }
+ }
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ /**
+ * Returns the count of registered cursors of a specific type.
+ */
+ public <S extends Storable> int getRegisteredCount(Class<S> type) {
+ mLock.lock();
+ try {
+ if (mCursors != null) {
+ CursorList<TransactionImpl<Txn>> 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 <S extends Storable> Cursor<S> getRegisteredCursor(Class<S> type, int index) {
+ mLock.lock();
+ try {
+ if (mCursors != null) {
+ CursorList<TransactionImpl<Txn>> cursorList = mCursors.get(type);
+ if (cursorList != null) {
+ if (index < cursorList.size()) {
+ return (Cursor<S>) 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<TransactionImpl<Txn>> 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.
+ *
+ * <p>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<Txn> implements Transaction {
+ private final TransactionManager<Txn> mTxnMgr;
+ private final TransactionImpl<Txn> mParent;
+ private final boolean mTop;
+ private final IsolationLevel mLevel;
+
+ private boolean mForUpdate;
+ private int mDesiredLockTimeout;
+ private TimeUnit mTimeoutUnit;
+
+ private TransactionImpl<Txn> mChild;
+ private boolean mExited;
+ private Txn mTxn;
+
+ // Tracks all registered cursors.
+ private CursorList<?> mCursorList;
+
+ TransactionImpl(TransactionManager<Txn> txnMgr,
+ TransactionImpl<Txn> 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<Txn> 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<Txn> 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;
+ }
+
+ <S extends Storable> void register(Cursor<S> cursor) {
+ if (mCursorList == null) {
+ mCursorList = new CursorList<Object>();
+ }
+ mCursorList.register(cursor, null);
+ }
+
+ <S extends Storable> void unregister(Cursor<S> cursor) {
+ if (mCursorList != null) {
+ mCursorList.unregister(cursor);
+ }
+ }
+
+ Txn getTxn() throws Exception {
+ TransactionManager<Txn> 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<V> {
+ 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<S extends Storable> {
+ // 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<S> mForInsert;
+ private volatile ForUpdate<S> mForUpdate;
+ private volatile ForDelete<S> mForDelete;
+
+ public TriggerManager() {
+ }
+
+ /**
+ * Returns consolidated trigger to call for insert operations, or null if
+ * none.
+ */
+ public Trigger<? super S> getInsertTrigger() {
+ return mForInsert;
+ }
+
+ /**
+ * Returns consolidated trigger to call for update operations, or null if
+ * none.
+ */
+ public Trigger<? super S> getUpdateTrigger() {
+ return mForUpdate;
+ }
+
+ /**
+ * Returns consolidated trigger to call for delete operations, or null if
+ * none.
+ */
+ public Trigger<? super S> getDeleteTrigger() {
+ return mForDelete;
+ }
+
+ public synchronized boolean addTrigger(Trigger<? super S> 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<S>();
+ }
+ retValue |= mForInsert.add(trigger);
+ }
+
+ if ((types & FOR_UPDATE) != 0) {
+ if (mForUpdate == null) {
+ mForUpdate = new ForUpdate<S>();
+ }
+ retValue |= mForUpdate.add(trigger);
+ }
+
+ if ((types & FOR_DELETE) != 0) {
+ if (mForDelete == null) {
+ mForDelete = new ForDelete<S>();
+ }
+ retValue |= mForDelete.add(trigger);
+ }
+
+ return retValue;
+ }
+
+ public synchronized boolean removeTrigger(Trigger<? super S> 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<? super S> trigger) {
+ Class<? extends Trigger> 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<? extends Trigger> triggerClass, Method method) {
+ try {
+ return !method.equals(triggerClass.getMethod(method.getName(),
+ method.getParameterTypes()));
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ private static class TriggerStates<S> {
+ final Trigger<? super S>[] mTriggers;
+ final Object[] mStates;
+
+ TriggerStates(Trigger<? super S>[] triggers) {
+ mTriggers = triggers;
+ mStates = new Object[triggers.length];
+ }
+ }
+
+ private static abstract class ForSomething<S> extends Trigger<S> {
+ private static Trigger[] NO_TRIGGERS = new Trigger[0];
+
+ protected volatile Trigger<? super S>[] mTriggers;
+
+ ForSomething() {
+ mTriggers = NO_TRIGGERS;
+ }
+
+ boolean add(Trigger<? super S> trigger) {
+ ArrayList<Trigger<? super S>> list =
+ new ArrayList<Trigger<? super S>>(Arrays.asList(mTriggers));
+ if (list.contains(trigger)) {
+ return false;
+ }
+ list.add(trigger);
+ mTriggers = list.toArray(new Trigger[list.size()]);
+ return true;
+ }
+
+ boolean remove(Trigger<? super S> trigger) {
+ ArrayList<Trigger<? super S>> list =
+ new ArrayList<Trigger<? super S>>(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<S> extends ForSomething<S> {
+ @Override
+ public Object beforeInsert(S storable) throws PersistException {
+ TriggerStates<S> triggerStates = null;
+ Trigger<? super S>[] triggers = mTriggers;
+
+ for (int i=triggers.length; --i>=0; ) {
+ Object state = triggers[i].beforeInsert(storable);
+ if (state != null) {
+ if (triggerStates == null) {
+ triggerStates = new TriggerStates<S>(triggers);
+ }
+ triggerStates.mStates[i] = state;
+ }
+ }
+
+ return triggerStates == null ? triggers : triggerStates;
+ }
+
+ @Override
+ public Object beforeTryInsert(S storable) throws PersistException {
+ TriggerStates<S> triggerStates = null;
+ Trigger<? super S>[] triggers = mTriggers;
+
+ for (int i=triggers.length; --i>=0; ) {
+ Object state = triggers[i].beforeTryInsert(storable);
+ if (state != null) {
+ if (triggerStates == null) {
+ triggerStates = new TriggerStates<S>(triggers);
+ }
+ triggerStates.mStates[i] = state;
+ }
+ }
+
+ return triggerStates == null ? triggers : triggerStates;
+ }
+
+ @Override
+ public void afterInsert(S storable, Object state) throws PersistException {
+ TriggerStates<S> triggerStates;
+ Trigger<? super S>[] triggers;
+
+ if (state == null) {
+ triggerStates = null;
+ triggers = mTriggers;
+ } else if (state instanceof TriggerStates) {
+ triggerStates = (TriggerStates<S>) state;
+ triggers = triggerStates.mTriggers;
+ } else {
+ triggerStates = null;
+ triggers = (Trigger<? super S>[]) state;
+ }
+
+ int length = triggers.length;
+
+ if (triggerStates == null) {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterInsert(storable, null);
+ }
+ } else {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterInsert(storable, triggerStates.mStates[i]);
+ }
+ }
+ }
+
+ @Override
+ public void afterTryInsert(S storable, Object state) throws PersistException {
+ TriggerStates<S> triggerStates;
+ Trigger<? super S>[] triggers;
+
+ if (state == null) {
+ triggerStates = null;
+ triggers = mTriggers;
+ } else if (state instanceof TriggerStates) {
+ triggerStates = (TriggerStates<S>) state;
+ triggers = triggerStates.mTriggers;
+ } else {
+ triggerStates = null;
+ triggers = (Trigger<? super S>[]) state;
+ }
+
+ int length = triggers.length;
+
+ if (triggerStates == null) {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterTryInsert(storable, null);
+ }
+ } else {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterTryInsert(storable, triggerStates.mStates[i]);
+ }
+ }
+ }
+
+ @Override
+ public void failedInsert(S storable, Object state) {
+ TriggerStates<S> triggerStates;
+ Trigger<? super S>[] triggers;
+
+ if (state == null) {
+ triggerStates = null;
+ triggers = mTriggers;
+ } else if (state instanceof TriggerStates) {
+ triggerStates = (TriggerStates<S>) state;
+ triggers = triggerStates.mTriggers;
+ } else {
+ triggerStates = null;
+ triggers = (Trigger<? super S>[]) state;
+ }
+
+ int length = triggers.length;
+
+ if (triggerStates == null) {
+ for (int i=0; i<length; i++) {
+ try {
+ triggers[i].failedInsert(storable, null);
+ } catch (Throwable e) {
+ Thread t = Thread.currentThread();
+ t.getUncaughtExceptionHandler().uncaughtException(t, e);
+ }
+ }
+ } else {
+ for (int i=0; i<length; i++) {
+ try {
+ triggers[i].failedInsert(storable, triggerStates.mStates[i]);
+ } catch (Throwable e) {
+ Thread t = Thread.currentThread();
+ t.getUncaughtExceptionHandler().uncaughtException(t, e);
+ }
+ }
+ }
+ }
+ }
+
+ private static class ForUpdate<S> extends ForSomething<S> {
+ @Override
+ public Object beforeUpdate(S storable) throws PersistException {
+ TriggerStates<S> triggerStates = null;
+ Trigger<? super S>[] triggers = mTriggers;
+
+ for (int i=triggers.length; --i>=0; ) {
+ Object state = triggers[i].beforeUpdate(storable);
+ if (state != null) {
+ if (triggerStates == null) {
+ triggerStates = new TriggerStates<S>(triggers);
+ }
+ triggerStates.mStates[i] = state;
+ }
+ }
+
+ return triggerStates == null ? triggers : triggerStates;
+ }
+
+ @Override
+ public Object beforeTryUpdate(S storable) throws PersistException {
+ TriggerStates<S> triggerStates = null;
+ Trigger<? super S>[] triggers = mTriggers;
+
+ for (int i=triggers.length; --i>=0; ) {
+ Object state = triggers[i].beforeTryUpdate(storable);
+ if (state != null) {
+ if (triggerStates == null) {
+ triggerStates = new TriggerStates<S>(triggers);
+ }
+ triggerStates.mStates[i] = state;
+ }
+ }
+
+ return triggerStates == null ? triggers : triggerStates;
+ }
+
+ @Override
+ public void afterUpdate(S storable, Object state) throws PersistException {
+ TriggerStates<S> triggerStates;
+ Trigger<? super S>[] triggers;
+
+ if (state == null) {
+ triggerStates = null;
+ triggers = mTriggers;
+ } else if (state instanceof TriggerStates) {
+ triggerStates = (TriggerStates<S>) state;
+ triggers = triggerStates.mTriggers;
+ } else {
+ triggerStates = null;
+ triggers = (Trigger<? super S>[]) state;
+ }
+
+ int length = triggers.length;
+
+ if (triggerStates == null) {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterUpdate(storable, null);
+ }
+ } else {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterUpdate(storable, triggerStates.mStates[i]);
+ }
+ }
+ }
+
+ @Override
+ public void afterTryUpdate(S storable, Object state) throws PersistException {
+ TriggerStates<S> triggerStates;
+ Trigger<? super S>[] triggers;
+
+ if (state == null) {
+ triggerStates = null;
+ triggers = mTriggers;
+ } else if (state instanceof TriggerStates) {
+ triggerStates = (TriggerStates<S>) state;
+ triggers = triggerStates.mTriggers;
+ } else {
+ triggerStates = null;
+ triggers = (Trigger<? super S>[]) state;
+ }
+
+ int length = triggers.length;
+
+ if (triggerStates == null) {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterTryUpdate(storable, null);
+ }
+ } else {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterTryUpdate(storable, triggerStates.mStates[i]);
+ }
+ }
+ }
+
+ @Override
+ public void failedUpdate(S storable, Object state) {
+ TriggerStates<S> triggerStates;
+ Trigger<? super S>[] triggers;
+
+ if (state == null) {
+ triggerStates = null;
+ triggers = mTriggers;
+ } else if (state instanceof TriggerStates) {
+ triggerStates = (TriggerStates<S>) state;
+ triggers = triggerStates.mTriggers;
+ } else {
+ triggerStates = null;
+ triggers = (Trigger<? super S>[]) state;
+ }
+
+ int length = triggers.length;
+
+ if (triggerStates == null) {
+ for (int i=0; i<length; i++) {
+ try {
+ triggers[i].failedUpdate(storable, null);
+ } catch (Throwable e) {
+ Thread t = Thread.currentThread();
+ t.getUncaughtExceptionHandler().uncaughtException(t, e);
+ }
+ }
+ } else {
+ for (int i=0; i<length; i++) {
+ try {
+ triggers[i].failedUpdate(storable, triggerStates.mStates[i]);
+ } catch (Throwable e) {
+ Thread t = Thread.currentThread();
+ t.getUncaughtExceptionHandler().uncaughtException(t, e);
+ }
+ }
+ }
+ }
+ }
+
+ private static class ForDelete<S> extends ForSomething<S> {
+ @Override
+ public Object beforeDelete(S storable) throws PersistException {
+ TriggerStates<S> triggerStates = null;
+ Trigger<? super S>[] triggers = mTriggers;
+
+ for (int i=triggers.length; --i>=0; ) {
+ Object state = triggers[i].beforeDelete(storable);
+ if (state != null) {
+ if (triggerStates == null) {
+ triggerStates = new TriggerStates<S>(triggers);
+ }
+ triggerStates.mStates[i] = state;
+ }
+ }
+
+ return triggerStates == null ? triggers : triggerStates;
+ }
+
+ @Override
+ public Object beforeTryDelete(S storable) throws PersistException {
+ TriggerStates<S> triggerStates = null;
+ Trigger<? super S>[] triggers = mTriggers;
+
+ for (int i=triggers.length; --i>=0; ) {
+ Object state = triggers[i].beforeTryDelete(storable);
+ if (state != null) {
+ if (triggerStates == null) {
+ triggerStates = new TriggerStates<S>(triggers);
+ }
+ triggerStates.mStates[i] = state;
+ }
+ }
+
+ return triggerStates == null ? triggers : triggerStates;
+ }
+
+ @Override
+ public void afterDelete(S storable, Object state) throws PersistException {
+ TriggerStates<S> triggerStates;
+ Trigger<? super S>[] triggers;
+
+ if (state == null) {
+ triggerStates = null;
+ triggers = mTriggers;
+ } else if (state instanceof TriggerStates) {
+ triggerStates = (TriggerStates<S>) state;
+ triggers = triggerStates.mTriggers;
+ } else {
+ triggerStates = null;
+ triggers = (Trigger<? super S>[]) state;
+ }
+
+ int length = triggers.length;
+
+ if (triggerStates == null) {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterDelete(storable, null);
+ }
+ } else {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterDelete(storable, triggerStates.mStates[i]);
+ }
+ }
+ }
+
+ @Override
+ public void afterTryDelete(S storable, Object state) throws PersistException {
+ TriggerStates<S> triggerStates;
+ Trigger<? super S>[] triggers;
+
+ if (state == null) {
+ triggerStates = null;
+ triggers = mTriggers;
+ } else if (state instanceof TriggerStates) {
+ triggerStates = (TriggerStates<S>) state;
+ triggers = triggerStates.mTriggers;
+ } else {
+ triggerStates = null;
+ triggers = (Trigger<? super S>[]) state;
+ }
+
+ int length = triggers.length;
+
+ if (triggerStates == null) {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterTryDelete(storable, null);
+ }
+ } else {
+ for (int i=0; i<length; i++) {
+ triggers[i].afterTryDelete(storable, triggerStates.mStates[i]);
+ }
+ }
+ }
+
+ @Override
+ public void failedDelete(S storable, Object state) {
+ TriggerStates<S> triggerStates;
+ Trigger<? super S>[] triggers;
+
+ if (state == null) {
+ triggerStates = null;
+ triggers = mTriggers;
+ } else if (state instanceof TriggerStates) {
+ triggerStates = (TriggerStates<S>) state;
+ triggers = triggerStates.mTriggers;
+ } else {
+ triggerStates = null;
+ triggers = (Trigger<? super S>[]) state;
+ }
+
+ int length = triggers.length;
+
+ if (triggerStates == null) {
+ for (int i=0; i<length; i++) {
+ try {
+ triggers[i].failedDelete(storable, null);
+ } catch (Throwable e) {
+ Thread t = Thread.currentThread();
+ t.getUncaughtExceptionHandler().uncaughtException(t, e);
+ }
+ }
+ } else {
+ for (int i=0; i<length; i++) {
+ try {
+ triggers[i].failedDelete(storable, triggerStates.mStates[i]);
+ } catch (Throwable e) {
+ Thread t = Thread.currentThread();
+ t.getUncaughtExceptionHandler().uncaughtException(t, e);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/spi/TriggerSupport.java b/src/main/java/com/amazon/carbonado/spi/TriggerSupport.java
new file mode 100644
index 0000000..0e40c38
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/spi/TriggerSupport.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.spi;
+
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.Trigger;
+
+/**
+ *
+ *
+ * @author Brian S O'Neill
+ */
+public interface TriggerSupport<S extends Storable> extends StorableSupport<S> {
+ /**
+ * Returns a trigger which must be run for all insert operations.
+ *
+ * @return null if no trigger
+ */
+ Trigger<? super S> getInsertTrigger();
+
+ /**
+ * Returns a trigger which must be run for all update operations.
+ *
+ * @return null if no trigger
+ */
+ Trigger<? super S> getUpdateTrigger();
+
+ /**
+ * Returns a trigger which must be run for all delete operations.
+ *
+ * @return null if no trigger
+ */
+ Trigger<? super S> 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<S extends Storable> implements Query<S> {
+
+ // The query to which this query will delegate
+ private final Query<S> mQuery;
+
+ /**
+ * @param query query to wrap
+ */
+ public WrappedQuery(Query<S> query) {
+ mQuery = query;
+ }
+
+ public Class<S> getStorableType() {
+ return mQuery.getStorableType();
+ }
+
+ public Filter<S> getFilter() {
+ return mQuery.getFilter();
+ }
+
+ public FilterValues<S> getFilterValues() {
+ return mQuery.getFilterValues();
+ }
+
+ public int getBlankParameterCount() {
+ return mQuery.getBlankParameterCount();
+ }
+
+ public Query<S> with(int value) {
+ return newInstance(mQuery.with(value));
+ }
+
+ public Query<S> with(long value) {
+ return newInstance(mQuery.with(value));
+ }
+
+ public Query<S> with(float value) {
+ return newInstance(mQuery.with(value));
+ }
+
+ public Query<S> with(double value) {
+ return newInstance(mQuery.with(value));
+ }
+
+ public Query<S> with(boolean value) {
+ return newInstance(mQuery.with(value));
+ }
+
+ public Query<S> with(char value) {
+ return newInstance(mQuery.with(value));
+ }
+
+ public Query<S> with(byte value) {
+ return newInstance(mQuery.with(value));
+ }
+
+ public Query<S> with(short value) {
+ return newInstance(mQuery.with(value));
+ }
+
+ public Query<S> with(Object value) {
+ return newInstance(mQuery.with(value));
+ }
+
+ public Query<S> withValues(Object... objects) {
+ return newInstance(mQuery.withValues(objects));
+ }
+
+ public Query<S> and(String filter) throws FetchException {
+ return newInstance(mQuery.and(filter));
+ }
+
+ public Query<S> and(Filter<S> filter) throws FetchException {
+ return newInstance(mQuery.and(filter));
+ }
+
+ public Query<S> or(String filter) throws FetchException {
+ return newInstance(mQuery.or(filter));
+ }
+
+ public Query<S> or(Filter<S> filter) throws FetchException {
+ return newInstance(mQuery.or(filter));
+ }
+
+ public Query<S> not() throws FetchException {
+ return newInstance(mQuery.not());
+ }
+
+ public Query<S> orderBy(String property) throws FetchException, UnsupportedOperationException {
+ return newInstance(mQuery.orderBy(property));
+ }
+
+ public Query<S> orderBy(String... strings)
+ throws FetchException, UnsupportedOperationException
+ {
+ return newInstance(mQuery.orderBy(strings));
+ }
+
+ public Cursor<S> fetch() throws FetchException {
+ return new WrappedCursor(mQuery.fetch());
+ }
+
+ public Cursor<S> 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<S> getWrappedQuery() {
+ return mQuery;
+ }
+
+ /**
+ * Called to wrap the given Storable.
+ */
+ protected abstract S wrap(S storable);
+
+ protected abstract WrappedQuery<S> newInstance(Query<S> query);
+
+ private class WrappedCursor extends AbstractCursor<S> {
+ private Cursor<S> mCursor;
+
+ public WrappedCursor(Cursor<S> 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<S extends Storable> implements Storage<S> {
+ private final Storage<S> mStorage;
+ private final WrappedStorableFactory<S> mFactory;
+ final TriggerManager<S> mTriggerManager;
+
+ /**
+ * @param storage storage to wrap
+ */
+ public WrappedStorage(Storage<S> storage) {
+ mStorage = storage;
+ Class<? extends S> wrappedClass = StorableGenerator
+ .getWrappedClass(storage.getStorableType());
+ mFactory = QuickConstructorGenerator
+ .getInstance(wrappedClass, WrappedStorableFactory.class);
+ mTriggerManager = new TriggerManager<S>();
+ }
+
+ public Class<S> getStorableType() {
+ return mStorage.getStorableType();
+ }
+
+ public S prepare() {
+ return wrap(mStorage.prepare());
+ }
+
+ public Query<S> query() throws FetchException {
+ return wrap(mStorage.query());
+ }
+
+ public Query<S> query(String filter) throws FetchException {
+ return wrap(mStorage.query(filter));
+ }
+
+ public Query<S> query(Filter<S> filter) throws FetchException {
+ return wrap(mStorage.query(filter));
+ }
+
+ public boolean addTrigger(Trigger<? super S> trigger) {
+ return mTriggerManager.addTrigger(trigger);
+ }
+
+ public boolean removeTrigger(Trigger<? super S> 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<S> wrap(Query<S> 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<S> 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<S> {
+ public Trigger<? super S> getInsertTrigger() {
+ return mTriggerManager.getInsertTrigger();
+ }
+
+ public Trigger<? super S> getUpdateTrigger() {
+ return mTriggerManager.getUpdateTrigger();
+ }
+
+ public Trigger<? super S> 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<S> {
+ QueryWrapper(Query<S> query) {
+ super(query);
+ }
+
+ protected S wrap(S storable) {
+ return WrappedStorage.this.wrap(storable);
+ }
+
+ protected WrappedQuery<S> newInstance(Query<S> query) {
+ return new QueryWrapper(query);
+ }
+ }
+
+ /**
+ * Used with QuickConstructorGenerator.
+ */
+ public static interface WrappedStorableFactory<S extends Storable> {
+ /**
+ * @param storable storable being wrapped
+ * @param support handler for persistence methods
+ */
+ S newWrappedStorable(WrappedSupport<S> 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<S extends Storable> extends TriggerSupport<S> {
+ /**
+ * @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<S> 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<S extends Storable> implements StorableCodec<S> {
+ // 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<Class, RawStorableGenerator.Flavors<? extends Storable>> cCache =
+ new WeakIdentityMap();
+
+ /**
+ * Returns a storable implementation that calls into CustomStorableCodec
+ * implementation for encoding and decoding.
+ */
+ @SuppressWarnings("unchecked")
+ static <S extends Storable> Class<? extends S>
+ getStorableClass(Class<S> type, boolean isMaster)
+ throws SupportException
+ {
+ synchronized (cCache) {
+ Class<? extends S> storableClass;
+
+ RawStorableGenerator.Flavors<S> flavors =
+ (RawStorableGenerator.Flavors<S>) cCache.get(type);
+
+ if (flavors == null) {
+ flavors = new RawStorableGenerator.Flavors<S>();
+ 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 <S extends Storable> Class<? extends S>
+ generateStorableClass(Class<S> type, boolean isMaster)
+ throws SupportException
+ {
+ final Class<? extends S> 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<S> 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<S> type, boolean isMaster) throws SupportException {
+ mType = type;
+ mPkPropertyCount = getPrimaryKeyIndex().getPropertyCount();
+ Class<? extends S> storableClass = getStorableClass(type, isMaster);
+ mInstanceFactory = QuickConstructorGenerator
+ .getInstance(storableClass, InstanceFactory.class);
+ }
+
+ public Class<S> getStorableType() {
+ return mType;
+ }
+
+ @SuppressWarnings("unchecked")
+ public S instantiate(RawSupport<S> support) {
+ return (S) mInstanceFactory.instantiate(support, this);
+ }
+
+ @SuppressWarnings("unchecked")
+ public S instantiate(RawSupport<S> 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<String, ? extends StorableProperty<S>> 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<S> buildPkIndex(String... propertyNames) {
+ Map<String, ? extends StorableProperty<S>> map = getAllProperties();
+ int length = propertyNames.length;
+ StorableProperty<S>[] properties = new StorableProperty[length];
+ Direction[] directions = new Direction[length];
+ for (int i=0; i<length; i++) {
+ String name = propertyNames[i];
+ char c = name.charAt(0);
+ Direction dir = Direction.fromCharacter(c);
+ if (dir != Direction.UNSPECIFIED || c == Direction.UNSPECIFIED.toCharacter()) {
+ name = name.substring(1);
+ } else {
+ // Default to ascending if not specified.
+ dir = Direction.ASCENDING;
+ }
+ if ((properties[i] = map.get(name)) == null) {
+ throw new IllegalArgumentException("Unknown property: " + name);
+ }
+ directions[i] = dir;
+ }
+ return new StorableIndex<S>(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<? extends Storable> 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 <S extends Storable> CustomStorableCodec<S> createCodec(Class<S> 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 <S extends Storable> CustomStorableCodec<S>
+ createCodec(Class<S> 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.
+ * <p>
+ * 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.
+ *
+ * <p>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<S extends Storable> {
+ private final Class<S> mType;
+ private final StorableIndex<S> 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<S> type, StorableIndex<S> 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<S> type, StorableIndex<S> 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<String, ? extends StorableProperty<S>> map =
+ StorableIntrospector.examine(mType).getPrimaryKeyProperties();
+
+ StorableProperty<S>[] properties = new StorableProperty[map.size()];
+ map.values().toArray(properties);
+
+ Direction[] directions = new Direction[map.size()];
+ Arrays.fill(directions, Direction.UNSPECIFIED);
+
+ pkIndex = new StorableIndex<S>(properties, directions, true);
+ }
+
+ mPkIndex = pkIndex;
+ }
+
+ /**
+ * Generates bytecode instructions to encode properties. The encoding is
+ * suitable for "key" encoding, which means it is correctly comparable.
+ *
+ * <p>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<S>[] 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<S>[] 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<S>[] 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<S>[] 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<S> 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<S> getPrimaryKeyIndex() {
+ return mPkIndex;
+ }
+
+ /**
+ * Returns all key properties as ordered properties, possibly with
+ * unspecified directions.
+ */
+ protected OrderedProperty<S>[] gatherAllKeyProperties() {
+ return mPkIndex.getOrderedProperties();
+ }
+
+ /**
+ * Returns all data properties for storable.
+ */
+ @SuppressWarnings("unchecked")
+ protected StorableProperty<S>[] gatherAllDataProperties() {
+ Map<String, ? extends StorableProperty<S>> map =
+ StorableIntrospector.examine(mType).getDataProperties();
+
+ StorableProperty<S>[] properties = new StorableProperty[map.size()];
+
+ int ordinal = 0;
+ for (StorableProperty<S> property : map.values()) {
+ properties[ordinal++] = property;
+ }
+
+ return properties;
+ }
+
+ protected StorablePropertyInfo checkSupport(StorableProperty<S> 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<S>[] properties)
+ throws SupportException
+ {
+ int length = properties.length;
+ StorablePropertyInfo[] infos = new StorablePropertyInfo[length];
+ for (int i=0; i<length; i++) {
+ infos[i] = checkSupport(properties[i]);
+ }
+ return infos;
+ }
+
+ private SupportException notSupported(StorableProperty<S> 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<S>[] ensureKeyProperties(OrderedProperty<S>[] properties) {
+ if (properties == null) {
+ properties = gatherAllKeyProperties();
+ } else {
+ for (Object prop : properties) {
+ if (prop == null) {
+ throw new IllegalArgumentException();
+ }
+ }
+ }
+ return properties;
+ }
+
+ @SuppressWarnings("unchecked")
+ private StorableProperty<S>[] extractProperties(OrderedProperty<S>[] ordered) {
+ StorableProperty<S>[] properties = new StorableProperty[ordered.length];
+ for (int i=0; i<ordered.length; i++) {
+ ChainedProperty chained = ordered[i].getChainedProperty();
+ if (chained.getChainCount() > 0) {
+ throw new IllegalArgumentException();
+ }
+ properties[i] = chained.getPrimeProperty();
+ }
+ return properties;
+ }
+
+ private Direction[] extractDirections(OrderedProperty<S>[] ordered) {
+ Direction[] directions = new Direction[ordered.length];
+ for (int i=0; i<ordered.length; i++) {
+ directions[i] = ordered[i].getDirection();
+ }
+ return directions;
+ }
+
+ private StorableProperty<S>[] ensureDataProperties(StorableProperty<S>[] 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<S>[] 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<S> 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<properties.length; i++) {
+ StorableProperty<S> 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<properties.length; i++) {
+ StorableProperty<S> 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<propertyCount; i++) {
+ cases[i] = i;
+ entryPoints[i] = a.createLabel();
+ }
+
+ // Now jump in!
+ Label errorLoc = a.createLabel();
+ a.loadLocal(partialStartVar);
+ a.switchBranch(cases, entryPoints, errorLoc);
+
+ errorLoc.setLocation();
+ TypeDesc errorType = TypeDesc.forClass(IllegalArgumentException.class);
+ a.newObject(errorType);
+ a.dup();
+ a.loadConstant("Illegal partial start offset");
+ a.invokeConstructor(errorType, new TypeDesc[] {TypeDesc.STRING});
+ a.throwObject();
+
+ return entryPoints;
+ }
+
+ /**
+ * Generates code that calls an encoding method in DataEncoder or
+ * KeyEncoder. 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
+ */
+ private int encodeProperty(CodeAssembler a, TypeDesc type,
+ boolean forKey, boolean descending) {
+ TypeDesc[] params = new TypeDesc[] {
+ type, TypeDesc.forClass(byte[].class), TypeDesc.INT
+ };
+
+ if (type.isPrimitive()) {
+ if (forKey && descending) {
+ a.invokeStatic(KeyEncoder.class.getName(), "encodeDesc", null, params);
+ } else {
+ a.invokeStatic(DataEncoder.class.getName(), "encode", null, params);
+ }
+
+ switch (type.getTypeCode()) {
+ case TypeDesc.BYTE_CODE:
+ case TypeDesc.BOOLEAN_CODE:
+ return 1;
+ case TypeDesc.SHORT_CODE:
+ case TypeDesc.CHAR_CODE:
+ return 2;
+ default:
+ case TypeDesc.INT_CODE:
+ case TypeDesc.FLOAT_CODE:
+ return 4;
+ case TypeDesc.LONG_CODE:
+ case TypeDesc.DOUBLE_CODE:
+ return 8;
+ }
+ } else if (type.toPrimitiveType() != null) {
+ // Type is a primitive wrapper.
+
+ int adjust;
+ TypeDesc retType;
+
+ switch (type.toPrimitiveType().getTypeCode()) {
+ case TypeDesc.BOOLEAN_CODE:
+ adjust = 1;
+ retType = null;
+ break;
+ case TypeDesc.FLOAT_CODE:
+ adjust = 4;
+ retType = null;
+ break;
+ case TypeDesc.DOUBLE_CODE:
+ adjust = 8;
+ retType = null;
+ break;
+ default:
+ adjust = 0;
+ retType = TypeDesc.INT;
+ }
+
+ if (forKey && descending) {
+ a.invokeStatic(KeyEncoder.class.getName(), "encodeDesc", retType, params);
+ } else {
+ a.invokeStatic(DataEncoder.class.getName(), "encode", retType, params);
+ }
+
+ return adjust;
+ } else {
+ // Type is a String or byte array.
+ if (forKey) {
+ if (descending) {
+ a.invokeStatic
+ (KeyEncoder.class.getName(), "encodeDesc", TypeDesc.INT, params);
+ } else {
+ a.invokeStatic(KeyEncoder.class.getName(), "encode", TypeDesc.INT, params);
+ }
+ } else {
+ a.invokeStatic(DataEncoder.class.getName(), "encode", TypeDesc.INT, params);
+ }
+ return 0;
+ }
+ }
+
+ /**
+ * Generates code that stores a one or four byte generation value into a
+ * byte array referenced by the local variable.
+ *
+ * @param generation if less than zero, no code is generated
+ */
+ private void encodeGeneration(CodeAssembler a, LocalVariable encodedVar,
+ int offset, int generation)
+ {
+ if (offset < 0) {
+ throw new IllegalArgumentException();
+ }
+ if (generation < 0) {
+ return;
+ }
+ if (generation < 128) {
+ a.loadLocal(encodedVar);
+ a.loadConstant(offset);
+ a.loadConstant((byte) generation);
+ a.storeToArray(TypeDesc.BYTE);
+ } else {
+ generation |= 0x80000000;
+ for (int i=0; i<4; i++) {
+ a.loadLocal(encodedVar);
+ a.loadConstant(offset + i);
+ a.loadConstant((byte) (generation >> (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<S>[] 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<S> 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<infos.length; i++) {
+ StorablePropertyInfo info = infos[i];
+
+ // Push to stack in preparation for storing a property.
+ pushDecodingInstanceVar(a, i, instanceVar);
+
+ TypeDesc storageType = info.getStorageType();
+
+ if (info.isLob()) {
+ // Need RawSupport instance for getting Lob from locator.
+ pushRawSupport(a, instanceVar);
+ // Locator is encoded as a long.
+ storageType = TypeDesc.LONG;
+ }
+
+ a.loadLocal(encodedVar);
+ if (offset == null) {
+ a.loadConstant(constantOffset);
+ } else {
+ a.loadLocal(offset);
+ }
+
+ boolean descending =
+ forKey && directions != null && directions[i] == Direction.DESCENDING;
+
+ int amt = decodeProperty(a, info, storageType, forKey, descending,
+ stringRef, byteArrayRef, valueRefRef);
+
+ if (info.isLob()) {
+ getLobFromLocator(a, info);
+ }
+
+ if (amt != 0) {
+ if (i + 1 < properties.length) {
+ // Only adjust offset if there are more properties.
+
+ if (amt > 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<String, BeanProperty> 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<S extends Storable> implements StorableCodec<S> {
+ 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 <S extends Storable> GenericStorableCodec<S> getInstance
+ (GenericStorableCodecFactory factory,
+ GenericEncodingStrategy<S> 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<S> codec = (GenericStorableCodec<S>) cCache.get(key);
+ if (codec == null) {
+ codec = new GenericStorableCodec<S>
+ (factory,
+ encodingStrategy.getType(),
+ generateStorable(encodingStrategy, isMaster, layout),
+ encodingStrategy,
+ layout);
+ cCache.put(key, codec);
+ }
+
+ return codec;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <S extends Storable> Class<? extends S> generateStorable
+ (GenericEncodingStrategy<S> encodingStrategy, boolean isMaster, Layout layout)
+ throws SupportException
+ {
+ final Class<S> storableClass = encodingStrategy.getType();
+ final Class<? extends S> 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<S> mType;
+
+ private final Class<? extends S> 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<GenericEncodingStrategy<S>> mEncodingStrategy;
+
+ private final GenericInstanceFactory mInstanceFactory;
+
+ private final SearchKeyFactory<S> 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<S> type, Class<? extends S> storableClass,
+ GenericEncodingStrategy<S> encodingStrategy,
+ Layout layout) {
+ mFactory = factory;
+ mType = type;
+ mStorableClass = storableClass;
+ mEncodingStrategy = new WeakReference<GenericEncodingStrategy<S>>(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<S> 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<S> 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<S> support, byte[] key, byte[] value)
+ throws FetchException
+ {
+ return (S) mInstanceFactory.instantiate(support, key, value);
+ }
+
+ public StorableIndex<S> 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:
+ *
+ * <pre>
+ * public &lt;init&gt;(Storage, RawSupport);
+ *
+ * public &lt;init&gt;(Storage, RawSupport, byte[] key, byte[] value);
+ * </pre>
+ *
+ * Convenience methods are provided in this class to instantiate the
+ * generated Storable.
+ */
+ public Class<? extends S> 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<S> getSearchKeyFactory(OrderedProperty<S>[] properties) {
+ // This KeyFactory makes arrays work as hashtable keys.
+ Object key = org.cojen.util.KeyFactory.createKey(properties);
+
+ synchronized (mSearchKeyFactories) {
+ SearchKeyFactory<S> factory = (SearchKeyFactory<S>) 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<S> getDecoder(int generation) throws FetchNoneException, FetchException {
+ try {
+ synchronized (mLayout) {
+ IntHashMap decoders = mDecoders;
+ if (decoders == null) {
+ mDecoders = decoders = new IntHashMap();
+ }
+ Decoder<S> decoder = (Decoder<S>) 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<S> 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<S> generateSearchKeyFactory(OrderedProperty<S>[] 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<? extends SearchKeyFactory> clazz = ci.defineClass(cf);
+ try {
+ return clazz.newInstance();
+ } catch (InstantiationException e) {
+ throw new UndeclaredThrowableException(e);
+ } catch (IllegalAccessException e) {
+ throw new UndeclaredThrowableException(e);
+ }
+ }
+
+ private Decoder<S> generateDecoder(int generation) throws FetchException {
+ // Create an encoding strategy against the reconstructed storable.
+ GenericEncodingStrategy<? extends Storable> altStrategy;
+ try {
+ Class<? extends Storable> 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<? extends Decoder> 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<S extends 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
+ * @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<S extends Storable> {
+ /**
+ * @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<? extends Storable> 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 <S extends Storable> GenericStorableCodec<S> createCodec(Class<S> 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 <S extends Storable> GenericEncodingStrategy<S> createStrategy
+ (Class<S> type, StorableIndex<S> pkIndex)
+ throws SupportException
+ {
+ return new GenericEncodingStrategy<S>(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<end; i++) {
+ if (accumBits <= 7) {
+ accumBits += 8;
+ accum = (accum << 8) | (value[i] & 0xff);
+ if (accumBits == 15) {
+ emitDigit(accum, dst, dstOffset, xorMask);
+ dstOffset += 2;
+ accum = 0;
+ accumBits = 0;
+ }
+ } else {
+ int supply = 15 - accumBits;
+ accum = (accum << supply) | ((value[i] & 0xff) >> (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.
+ * <p>
+ * 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.
+ * <p>
+ * 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<S> extends AbstractCursor<S> {
+ // 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<len; i++) {
+ if (startBound[i] != endBound[i]) {
+ break;
+ }
+ }
+ mPrefixLength = i;
+ }
+ }
+
+ public void close() throws FetchException {
+ mLock.lock();
+ try {
+ if (mState != CLOSED) {
+ release();
+ // Switch state to closed before committing transaction, to
+ // prevent infinite recursion that results when transaction
+ // exits. Exiting a transaction causes all cursors to close.
+ mState = CLOSED;
+ }
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ public boolean hasNext() throws FetchException {
+ mLock.lock();
+ try {
+ try {
+ switch (mState) {
+ case UNINITIALIZED:
+ if (mReverse ? toBoundedLast() : toBoundedFirst()) {
+ mState = HAS_NEXT;
+ return true;
+ } else {
+ mState = TRY_NEXT;
+ }
+ break;
+
+ case CLOSED: default:
+ return false;
+
+ case TRY_NEXT:
+ if (mReverse ? toBoundedPrevious() : toBoundedNext()) {
+ mState = HAS_NEXT;
+ return true;
+ }
+ break;
+
+ case HAS_NEXT:
+ return true;
+ }
+ } catch (FetchException e) {
+ // Auto-close in response to FetchException.
+ try {
+ close();
+ } catch (FetchException e2) {
+ // Ignore.
+ }
+ throw e;
+ }
+
+ // Reached the end naturally, so close.
+ close();
+ } finally {
+ mLock.unlock();
+ }
+
+ return false;
+ }
+
+ public S next() throws FetchException, NoSuchElementException {
+ mLock.lock();
+ try {
+ if (!hasNext()) {
+ handleNoSuchElement();
+ throw new NoSuchElementException();
+ }
+ try {
+ S obj = instantiateCurrent();
+ mState = TRY_NEXT;
+ return obj;
+ } catch (FetchException e) {
+ // Auto-close in response to FetchException.
+ try {
+ close();
+ } catch (FetchException e2) {
+ // Ignore.
+ }
+ throw e;
+ }
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ public int skipNext(int amount) throws FetchException {
+ if (amount <= 0) {
+ if (amount < 0) {
+ throw new IllegalArgumentException("Cannot skip negative amount: " + amount);
+ }
+ return 0;
+ }
+
+ mLock.lock();
+ try {
+ int actual = 0;
+
+ if (hasNext()) {
+ try {
+ actual += mReverse ? toBoundedPrevious(amount) : toBoundedNext(amount);
+ } catch (FetchException e) {
+ // Auto-close in response to FetchException.
+ try {
+ close();
+ } catch (FetchException e2) {
+ // Ignore.
+ }
+ throw e;
+ }
+
+ if (actual >= 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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 &lt;0 if key1 is less, 0 if equal (at least partially), &gt;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<length; i++) {
+ int a1 = key1[i];
+ int a2 = key2[i];
+ if (a1 != a2) {
+ return (a1 & 0xff) - (a2 & 0xff);
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Called right before throwing NoSuchElementException. Subclasses may
+ * override to do special checks or throw a different exception.
+ */
+ protected void handleNoSuchElement() throws FetchException {
+ }
+
+ private boolean prefixMatches() throws FetchException {
+ int prefixLen = mPrefixLength;
+ if (prefixLen > 0) {
+ byte[] prefix = mStartBound;
+ byte[] key = getCurrentKey();
+ if (key == null) {
+ return false;
+ }
+ for (int i=0; i<prefixLen; i++) {
+ if (prefix[i] != key[i]) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // Calls toFirst, but considers start and end bounds.
+ private boolean toBoundedFirst() throws FetchException {
+ if (mStartBound == null) {
+ if (!toFirst()) {
+ return false;
+ }
+ } else {
+ if (!toFirst(mStartBound.clone())) {
+ return false;
+ }
+ if (!mInclusiveStart) {
+ byte[] currentKey = getCurrentKey();
+ if (currentKey == null) {
+ return false;
+ }
+ if (compareKeysPartially(mStartBound, currentKey) == 0) {
+ if (!toNextKey()) {
+ 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 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<Class, Flavors<? extends Storable>> cCache = new WeakIdentityMap();
+
+ /**
+ * Collection of different abstract class flavors.
+ */
+ static class Flavors<S extends Storable> {
+ private Reference<Class<? extends S>> mMasterFlavor;
+
+ private Reference<Class<? extends S>> mNonMasterFlavor;
+
+ /**
+ * May return null.
+ */
+ Class<? extends S> getClass(boolean isMaster) {
+ Reference<Class<? extends S>> ref;
+ if (isMaster) {
+ ref = mMasterFlavor;
+ } else {
+ ref = mNonMasterFlavor;
+ }
+ return (ref != null) ? ref.get() : null;
+ }
+
+ @SuppressWarnings("unchecked")
+ void setClass(Class<? extends S> clazz, boolean isMaster) {
+ Reference<Class<? extends S>> 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:
+ *
+ * <pre>
+ * public &lt;init&gt;(RawSupport);
+
+ * public &lt;init&gt;(RawSupport, byte[] key, byte[] value);
+ * </pre>
+ *
+ * <p>Subclasses must implement the following abstract protected methods,
+ * whose exact names are defined by constants in this class:
+ *
+ * <pre>
+ * // 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[]);
+ * </pre>
+ *
+ * @param isMaster when true, version properties, sequences, and triggers are managed
+ * @throws IllegalArgumentException if type is null
+ */
+ @SuppressWarnings("unchecked")
+ public static <S extends Storable> Class<? extends S>
+ getAbstractClass(Class<S> type, boolean isMaster)
+ throws SupportException, IllegalArgumentException
+ {
+ synchronized (cCache) {
+ Class<? extends S> abstractClass;
+
+ Flavors<S> flavors = (Flavors<S>) cCache.get(type);
+
+ if (flavors == null) {
+ flavors = new Flavors<S>();
+ 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 <S extends Storable> Class<? extends S>
+ generateAbstractClass(Class<S> storableClass, boolean isMaster)
+ throws SupportException
+ {
+ EnumSet<MasterFeature> 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<? extends S> 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<S extends Storable> extends MasterSupport<S> {
+ /**
+ * 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<S extends Storable> {
+ /**
+ * Returns the type of Storable produced by this codec.
+ */
+ Class<S> getStorableType();
+
+ /**
+ * Instantiate a Storable with no key or value defined yet.
+ *
+ * @param support binds generated storable with a storage layer
+ */
+ S instantiate(RawSupport<S> support);
+
+ /**
+ * Instantiate a Storable with a specific key and value.
+ *
+ * @param support binds generated storable with a storage layer
+ */
+ S instantiate(RawSupport<S> support, byte[] key, byte[] value)
+ throws FetchException;
+
+ /**
+ * Returns the sequence and directions of properties that make up the
+ * primary key.
+ */
+ StorableIndex<S> 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<? extends Storable> 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
+ */
+ <S extends Storable> StorableCodec<S> createCodec(Class<S> 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;