diff options
Diffstat (limited to 'src')
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 <0 if these results are better, 0 if equal, or >0 if other is better
 +         */
 +        public int compareTo(IndexAnalysis<?> otherAnalysis) {
 +            if (noBestIndex()) {
 +                if (otherAnalysis.noBestIndex()) {
 +                    return 0;
 +                }
 +                return 1;
 +            } else if (otherAnalysis.noBestIndex()) {
 +                return -1;
 +            } else {
 +                return IndexSelector.listCompare(mResults, otherAnalysis.mResults);
 +            }
 +        }
 +
 +        /**
 +         * If more than one result returned, then a union must be performed.
 +         * This is because there exist 'or' operators in the full filter.
 +         */
 +        List<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 <0 if "a" is nearest, 0 if both are equally good, >0 if "b" is
 +     * nearest.
 +     */
 +    public int compare(Class toType_a, Class toType_b) {
 +        TypeDesc from = mFrom;
 +        TypeDesc a = TypeDesc.forClass(toType_a);
 +        TypeDesc b = TypeDesc.forClass(toType_b);
 +
 +        if (from == a) {
 +            if (from == b) {
 +                return 0;
 +            }
 +            return -1;
 +        } else if (from == b) {
 +            return 1;
 +        }
 +
 +        int result = compare(from, a, b);
 +        if (result != 0) {
 +            return result;
 +        }
 +
 +        if (from.isPrimitive()) {
 +            // Try boxing.
 +            if (from.toObjectType() != null) {
 +                from = from.toObjectType();
 +                return compare(from, a, b);
 +            }
 +        } else {
 +            // Try unboxing.
 +            if (from.toPrimitiveType() != null) {
 +                from = from.toPrimitiveType();
 +                result = compare(from, a, b);
 +                if (result != 0) {
 +                    return result;
 +                }
 +                // Try boxing back up. Test by unboxing 'to' types.
 +                if (!toType_a.isPrimitive() && a.toPrimitiveType() != null) {
 +                    a = a.toPrimitiveType();
 +                }
 +                if (!toType_b.isPrimitive() && b.toPrimitiveType() != null) {
 +                    b = b.toPrimitiveType();
 +                }
 +                return compare(from, a, b);
 +            }
 +        }
 +
 +        return 0;
 +    }
 +
 +    private static int compare(TypeDesc from, TypeDesc a, TypeDesc b) {
 +        if (isConversionPossible(from, a)) {
 +            if (isConversionPossible(from, b)) {
 +                if (from.isPrimitive()) {
 +                    if (a.isPrimitive()) {
 +                        if (b.isPrimitive()) {
 +                            // Choose the one with the least amount of widening.
 +                            return primitiveWidth(a) - primitiveWidth(b);
 +                        } else {
 +                            return -1;
 +                        }
 +                    } else if (b.isPrimitive()) {
 +                        return 1;
 +                    }
 +                } else {
 +                    // Choose the one with the shortest distance up the class
 +                    // hierarchy.
 +                    Class fromClass = from.toClass();
 +                    if (!fromClass.isInterface()) {
 +                        if (a.toClass().isInterface()) {
 +                            if (!b.toClass().isInterface()) {
 +                                return -1;
 +                            }
 +                        } else if (b.toClass().isInterface()) {
 +                            return 1;
 +                        } else {
 +                            return distance(fromClass, a.toClass())
 +                                - distance(fromClass, b.toClass());
 +                        }
 +                    }
 +                }
 +            } else {
 +                return -1;
 +            }
 +        } else if (isConversionPossible(from, b)) {
 +            return 1;
 +        }
 +
 +        return 0;
 +    }
 +
 +    // 1 = boolean, 2 = byte, 3 = short, 4 = char, 5 = int, 6 = float, 7 = long, 8 = double
 +    private static int primitiveWidth(TypeDesc type) {
 +        switch (type.getTypeCode()) {
 +        default:
 +            return 0;
 +        case TypeDesc.BOOLEAN_CODE:
 +            return 1;
 +        case TypeDesc.BYTE_CODE:
 +            return 2;
 +        case TypeDesc.SHORT_CODE:
 +            return 3;
 +        case TypeDesc.CHAR_CODE:
 +            return 4;
 +        case TypeDesc.INT_CODE:
 +            return 5;
 +        case TypeDesc.FLOAT_CODE:
 +            return 6;
 +        case TypeDesc.LONG_CODE:
 +            return 7;
 +        case TypeDesc.DOUBLE_CODE:
 +            return 8;
 +        }
 +    }
 +
 +    private static int distance(Class from, Class to) {
 +        int distance = 0;
 +        while (from != to) {
 +            from = from.getSuperclass();
 +            if (from == null) {
 +                return Integer.MAX_VALUE;
 +            }
 +            distance++;
 +        }
 +        return distance;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/spi/ExceptionTransformer.java b/src/main/java/com/amazon/carbonado/spi/ExceptionTransformer.java new file mode 100644 index 0000000..2064e68 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/ExceptionTransformer.java @@ -0,0 +1,189 @@ +/*
 + * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
 + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
 + * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
 + *
 + * Licensed under the Apache License, Version 2.0 (the "License");
 + * you may not use this file except in compliance with the License.
 + * You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +
 +package com.amazon.carbonado.spi;
 +
 +import java.io.InterruptedIOException;
 +import java.nio.channels.ClosedByInterruptException;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.FetchInterruptedException;
 +import com.amazon.carbonado.PersistException;
 +import com.amazon.carbonado.RepositoryException;
 +
 +/**
 + * Supports transforming arbitrary exceptions into appropriate repository
 + * exceptions. Repositories will likely extend this class, providing custom
 + * transformation rules.
 + *
 + * @author Brian S O'Neill
 + */
 +public class ExceptionTransformer {
 +    private static ExceptionTransformer cInstance;
 +
 +    /**
 +     * Returns a generic instance.
 +     */
 +    public static ExceptionTransformer getInstance() {
 +        if (cInstance == null) {
 +            cInstance = new ExceptionTransformer();
 +        }
 +        return cInstance;
 +    }
 +
 +    public ExceptionTransformer() {
 +    }
 +
 +    /**
 +     * Transforms the given throwable into an appropriate fetch exception. If
 +     * it already is a fetch exception, it is simply casted.
 +     *
 +     * @param e required exception to transform
 +     * @return FetchException, never null
 +     */
 +    public FetchException toFetchException(Throwable e) {
 +        FetchException fe = transformIntoFetchException(e);
 +        if (fe != null) {
 +            return fe;
 +        }
 +
 +        Throwable cause = e.getCause();
 +        if (cause != null) {
 +            fe = transformIntoFetchException(cause);
 +            if (fe != null) {
 +                return fe;
 +            }
 +        } else {
 +            cause = e;
 +        }
 +
 +        return new FetchException(cause);
 +    }
 +
 +    /**
 +     * Transforms the given throwable into an appropriate persist exception. If
 +     * it already is a persist exception, it is simply casted.
 +     *
 +     * @param e required exception to transform
 +     * @return PersistException, never null
 +     */
 +    public PersistException toPersistException(Throwable e) {
 +        PersistException pe = transformIntoPersistException(e);
 +        if (pe != null) {
 +            return pe;
 +        }
 +
 +        Throwable cause = e.getCause();
 +        if (cause != null) {
 +            pe = transformIntoPersistException(cause);
 +            if (pe != null) {
 +                return pe;
 +            }
 +        } else {
 +            cause = e;
 +        }
 +
 +        return new PersistException(cause);
 +    }
 +
 +    /**
 +     * Transforms the given throwable into an appropriate repository
 +     * exception. If it already is a repository exception, it is simply casted.
 +     *
 +     * @param e required exception to transform
 +     * @return RepositoryException, never null
 +     */
 +    public RepositoryException toRepositoryException(Throwable e) {
 +        RepositoryException re = transformIntoRepositoryException(e);
 +        if (re != null) {
 +            return re;
 +        }
 +
 +        Throwable cause = e.getCause();
 +        if (cause != null) {
 +            re = transformIntoRepositoryException(cause);
 +            if (re != null) {
 +                return re;
 +            }
 +        } else {
 +            cause = e;
 +        }
 +
 +        return new RepositoryException(cause);
 +    }
 +
 +    /**
 +     * Override to support custom transformations, returning null if none is
 +     * applicable. Be sure to call super first. If it returns non-null, return
 +     * that result.
 +     *
 +     * @param e required exception to transform
 +     * @return FetchException, or null if no applicable transform
 +     */
 +    protected FetchException transformIntoFetchException(Throwable e) {
 +        if (e instanceof FetchException) {
 +            return (FetchException) e;
 +        }
 +        if (e instanceof InterruptedIOException ||
 +            e instanceof ClosedByInterruptException) {
 +            return new FetchInterruptedException(e);
 +        }
 +        return null;
 +    }
 +
 +    /**
 +     * Override to support custom transformations, returning null if none is
 +     * applicable. Be sure to call super first. If it returns non-null, return
 +     * that result.
 +     *
 +     * @param e required exception to transform
 +     * @return PersistException, or null if no applicable transform
 +     */
 +    protected PersistException transformIntoPersistException(Throwable e) {
 +        if (e instanceof PersistException) {
 +            return (PersistException) e;
 +        }
 +        if (e instanceof FetchException) {
 +            return ((FetchException) e).toPersistException();
 +        }
 +        return null;
 +    }
 +
 +    /**
 +     * Override to support custom transformations, returning null if none is
 +     * applicable. Be sure to call super first. If it returns non-null, return
 +     * that result.
 +     *
 +     * @param e required exception to transform
 +     * @return RepositoryException, or null if no applicable transform
 +     */
 +    protected RepositoryException transformIntoRepositoryException(Throwable e) {
 +        if (e instanceof RepositoryException) {
 +            return (RepositoryException) e;
 +        }
 +        RepositoryException re = transformIntoFetchException(e);
 +        if (re != null) {
 +            return re;
 +        }
 +        re = transformIntoPersistException(e);
 +        if (re != null) {
 +            return re;
 +        }
 +        return null;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/spi/IndexInfoImpl.java b/src/main/java/com/amazon/carbonado/spi/IndexInfoImpl.java new file mode 100644 index 0000000..de16f75 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/IndexInfoImpl.java @@ -0,0 +1,117 @@ +/*
 + * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
 + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
 + * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
 + *
 + * Licensed under the Apache License, Version 2.0 (the "License");
 + * you may not use this file except in compliance with the License.
 + * You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +
 +package com.amazon.carbonado.spi;
 +
 +import java.util.Arrays;
 +
 +import com.amazon.carbonado.capability.IndexInfo;
 +
 +import com.amazon.carbonado.info.Direction;
 +
 +/**
 + * Basic implementation of an {@link IndexInfo}.
 + *
 + * @author Brian S O'Neill
 + */
 +public class IndexInfoImpl implements IndexInfo {
 +    private final String mName;
 +    private final boolean mUnique;
 +    private final boolean mClustered;
 +    private final String[] mPropertyNames;
 +    private final Direction[] mPropertyDirections;
 +
 +    /**
 +     * @param name optional name for index
 +     * @param unique true if index requires unique values
 +     * @param propertyNames required list of property names, must have at least
 +     * one name
 +     * @param propertyDirections optional property directions, may be null or
 +     * same length as property names array
 +     * @throws IllegalArgumentException
 +     */
 +    public IndexInfoImpl(String name, boolean unique, boolean clustered,
 +                         String[] propertyNames, Direction[] propertyDirections) {
 +        mName = name;
 +        mUnique = unique;
 +        mClustered = clustered;
 +
 +        if (propertyNames == null || propertyNames.length == 0) {
 +            throw new IllegalArgumentException();
 +        }
 +
 +        for (int i=propertyNames.length; --i>=0; ) {
 +            if (propertyNames[i] == null) {
 +                throw new IllegalArgumentException();
 +            }
 +        }
 +
 +        propertyNames = propertyNames.clone();
 +
 +        if (propertyDirections == null) {
 +            propertyDirections = new Direction[propertyNames.length];
 +        } else {
 +            if (propertyNames.length != propertyDirections.length) {
 +                throw new IllegalArgumentException();
 +            }
 +            propertyDirections = propertyDirections.clone();
 +        }
 +        for (int i=propertyDirections.length; --i>=0; ) {
 +            if (propertyDirections[i] == null) {
 +                propertyDirections[i] = Direction.UNSPECIFIED;
 +            }
 +        }
 +
 +        mPropertyNames = propertyNames;
 +        mPropertyDirections = propertyDirections;
 +    }
 +
 +    public String getName() {
 +        return mName;
 +    }
 +
 +    public boolean isUnique() {
 +        return mUnique;
 +    }
 +
 +    public boolean isClustered() {
 +        return mClustered;
 +    }
 +
 +    public String[] getPropertyNames() {
 +        return mPropertyNames.clone();
 +    }
 +
 +    public Direction[] getPropertyDirections() {
 +        return mPropertyDirections.clone();
 +    }
 +
 +    public String toString() {
 +        StringBuilder b = new StringBuilder();
 +        b.append("IndexInfo {name=");
 +        b.append(mName);
 +        b.append(", unique=");
 +        b.append(mUnique);
 +        b.append(", propertyNames=");
 +        b.append(Arrays.toString(mPropertyNames));
 +        b.append(", propertyDirections=");
 +        b.append(Arrays.toString(mPropertyDirections));
 +        b.append('}');
 +        return b.toString();
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/spi/IndexSelector.java b/src/main/java/com/amazon/carbonado/spi/IndexSelector.java new file mode 100644 index 0000000..660cfdb --- /dev/null +++ b/src/main/java/com/amazon/carbonado/spi/IndexSelector.java @@ -0,0 +1,1204 @@ +/*
 + * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
 + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
 + * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
 + *
 + * Licensed under the Apache License, Version 2.0 (the "License");
 + * you may not use this file except in compliance with the License.
 + * You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +
 +package com.amazon.carbonado.spi;
 +
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.Comparator;
 +import java.util.HashSet;
 +import java.util.Iterator;
 +import java.util.LinkedList;
 +import java.util.LinkedHashMap;
 +import java.util.LinkedHashSet;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.Set;
 +
 +import com.amazon.carbonado.Storable;
 +
 +import com.amazon.carbonado.filter.Filter;
 +import com.amazon.carbonado.filter.OrFilter;
 +import com.amazon.carbonado.filter.PropertyFilter;
 +import com.amazon.carbonado.filter.RelOp;
 +import com.amazon.carbonado.filter.Visitor;
 +
 +import com.amazon.carbonado.info.ChainedProperty;
 +import com.amazon.carbonado.info.Direction;
 +import com.amazon.carbonado.info.OrderedProperty;
 +import com.amazon.carbonado.info.StorableIndex;
 +import com.amazon.carbonado.info.StorableProperty;
 +
 +/**
 + * Tries to find the best index to use for a query. When used to sort a list of
 + * indexes, the first in the list (the lowest) is the best index.
 + *
 + * @author Brian S O'Neill
 + */
 +public class IndexSelector<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 <0 if the current index is better than the candidate index, 0
 +     * if they are equally good, or >0 if the candidate index is
 +     * better.
 +     * <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 <0 if this score is better, 0 if equal, or >0 if other is better
 +         */
 +        public int compareTo(IndexFitness<?> otherFitness) {
 +            return mIndexScore.compareTo(otherFitness.mIndexScore);
 +        }
 +
 +        /**
 +         * Returns true if given fitness result uses the same index, and in the
 +         * same way.
 +         */
 +        public boolean canUnion(IndexFitness fitness) {
 +            if (this == fitness) {
 +                return true;
 +            }
 +
 +            return mIndex.equals(fitness.mIndex) &&
 +                (mExactFilter == null ?
 +                 fitness.mExactFilter == null :
 +                 (mExactFilter.equals(fitness.mExactFilter))) &&
 +                Arrays.equals(mInclusiveRangeStartFilters,
 +                              fitness.mInclusiveRangeStartFilters) &&
 +                Arrays.equals(mExclusiveRangeStartFilters,
 +                              fitness.mExclusiveRangeStartFilters) &&
 +                Arrays.equals(mInclusiveRangeEndFilters,
 +                              fitness.mInclusiveRangeEndFilters) &&
 +                Arrays.equals(mExclusiveRangeEndFilters,
 +                              fitness.mExclusiveRangeEndFilters) &&
 +                mShouldReverseOrder == fitness.mShouldReverseOrder &&
 +                mShouldReverseRange == fitness.mShouldReverseRange &&
 +                (mHandledOrderings == null ?
 +                 fitness.mHandledOrderings == null :
 +                 (Arrays.equals(mHandledOrderings, fitness.mHandledOrderings)));
 +        }
 +
 +        /**
 +         * If the given fitness can union with this one, return a new unioned
 +         * one. If union not possible, null is returned.
 +         */
 +        public IndexFitness union(IndexFitness fitness) {
 +            if (this == fitness) {
 +                return this;
 +            }
 +
 +            if (!canUnion(fitness)) {
 +                return null;
 +            }
 +
 +            // Union the remainder filter and orderings.
 +
 +            Filter<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 <init>(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
 +     *  */
 +     * public <init>(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
 +     *  */
 +     * public <init>(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 <init>(Storage, RawSupport);
 +     *
 +     * public <init>(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 <0 if key1 is less, 0 if equal (at least partially), >0
 +     * if key1 is greater.
 +     */
 +    protected int compareKeysPartially(byte[] key1, byte[] key2) {
 +        int length = Math.min(key1.length, key2.length);
 +        for (int i=0; i<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 <init>(RawSupport);
 +
 +     * public <init>(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;
  | 
