diff options
| author | Brian S. O'Neill <bronee@gmail.com> | 2006-08-30 02:24:36 +0000 | 
|---|---|---|
| committer | Brian S. O'Neill <bronee@gmail.com> | 2006-08-30 02:24:36 +0000 | 
| commit | 5a2aeb3ab59f286a6d2a5d8b7d62f4b17132b2b7 (patch) | |
| tree | 6901a95bdd325cabfc3613fb11dbcc6afbccddac /src/main/java/com/amazon | |
| parent | 8f88478b4be9c3165d678c43640052c8fc7d8943 (diff) | |
Add core repository implementations
Diffstat (limited to 'src/main/java/com/amazon')
53 files changed, 12108 insertions, 0 deletions
| diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessCapability.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessCapability.java new file mode 100644 index 0000000..82f9d1f --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessCapability.java @@ -0,0 +1,40 @@ +/*
 + * 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.repo.indexed;
 +
 +import com.amazon.carbonado.RepositoryException;
 +import com.amazon.carbonado.Storable;
 +
 +import com.amazon.carbonado.capability.Capability;
 +
 +/**
 + * Capability for gaining low-level access to index data, which can be used for
 + * manual inspection and repair.
 + *
 + * @author Brian S O'Neill
 + */
 +public interface IndexEntryAccessCapability extends Capability {
 +    /**
 +     * Returns index entry accessors for the known indexes of the given
 +     * storable type. The array might be empty, but it is never null. The array
 +     * is a copy, and so it may be safely modified.
 +     */
 +    <S extends Storable> IndexEntryAccessor<S>[] getIndexEntryAccessors(Class<S> storableType)
 +        throws RepositoryException;
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java new file mode 100644 index 0000000..16a980d --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java @@ -0,0 +1,73 @@ +/*
 + * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
 + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
 + * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
 + *
 + * Licensed under the Apache License, Version 2.0 (the "License");
 + * you may not use this file except in compliance with the License.
 + * You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +
 +package com.amazon.carbonado.repo.indexed;
 +
 +import java.util.Comparator;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.Storable;
 +import com.amazon.carbonado.Storage;
 +
 +import com.amazon.carbonado.capability.IndexInfo;
 +
 +/**
 + * Provides low-level access to index data, which can be used for manual
 + * inspection and repair.
 + *
 + * @author Brian S O'Neill
 + */
 +public interface IndexEntryAccessor<S extends Storable> extends IndexInfo {
 +    /**
 +     * Returns the index entry storage. Index entry properties can only be
 +     * accessed via reflection.
 +     */
 +    Storage<?> getIndexEntryStorage();
 +
 +    /**
 +     * Loads the master object referenced by the given index entry.
 +     *
 +     * @param indexEntry index entry which points to master
 +     * @return master or null if missing
 +     */
 +    S loadMaster(Storable indexEntry) throws FetchException;
 +
 +    /**
 +     * Sets all the properties of the given index entry, using the applicable
 +     * properties of the given master.
 +     *
 +     * @param indexEntry index entry whose properties will be set
 +     * @param master source of property values
 +     */
 +    void setAllProperties(Storable indexEntry, S master);
 +
 +    /**
 +     * Returns true if the properties of the given index entry match those
 +     * contained in the master, exluding any version property. This will always
 +     * return true after a call to setAllProperties.
 +     *
 +     * @param indexEntry index entry whose properties will be tested
 +     * @param master source of property values
 +     */
 +    boolean isConsistent(Storable indexEntry, S master);
 +
 +    /**
 +     * Returns a comparator for ordering index entries.
 +     */
 +    Comparator<? extends Storable> getComparator();
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryGenerator.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryGenerator.java new file mode 100644 index 0000000..3da1a5b --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryGenerator.java @@ -0,0 +1,173 @@ +/*
 + * 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.repo.indexed;
 +
 +import java.util.Comparator;
 +import java.util.Map;
 +import java.util.WeakHashMap;
 +import java.lang.ref.Reference;
 +import java.lang.ref.SoftReference;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.Storable;
 +import com.amazon.carbonado.SupportException;
 +import com.amazon.carbonado.info.StorableIndex;
 +import com.amazon.carbonado.info.StorableProperty;
 +import com.amazon.carbonado.synthetic.SyntheticStorableReferenceBuilder;
 +
 +/**
 + * IndexEntryGenerator creates new kinds of Storables suitable for indexing a
 + * master Storable.
 + *
 + * @author Brian S O'Neill
 + * @author Don Schneider
 + *
 + */
 +class IndexEntryGenerator <S extends Storable> {
 +
 +    // cache for generators
 +    private static Map<StorableIndex, Reference<IndexEntryGenerator>> cCache =
 +            new WeakHashMap<StorableIndex, Reference<IndexEntryGenerator>>();
 +
 +
 +    /**
 +     * Returns a new or cached generator instance. The caching of generators is
 +     * soft, so if no references remain to a given instance it may be garbage
 +     * collected. A subsequent call will return a newly created instance.
 +     *
 +     * <p>In addition to generating an index entry storable, this class
 +     * contains methods to operate on it. Care must be taken to ensure that the
 +     * index entry instances are of the same type that the generator expects.
 +     * Since the generator may be garbage collected freely of the generated
 +     * index entry class, it is possible for index entries to be passed to a
 +     * generator instance that does not understand it. For example:
 +     *
 +     * <pre>
 +     * StorableIndex index = ...
 +     * Class indexEntryClass = IndexEntryGenerator.getInstance(index).getIndexEntryClass();
 +     * ...
 +     * garbage collection
 +     * ...
 +     * Storable indexEntry = instance of indexEntryClass
 +     * // Might fail because generator instance is new
 +     * IndexEntryGenerator.getInstance(index).setAllProperties(indexEntry, source);
 +     * </pre>
 +     *
 +     * The above code can be fixed by saving a local reference to the generator:
 +     *
 +     * <pre>
 +     * StorableIndex index = ...
 +     * IndexEntryGenerator generator = IndexEntryGenerator.getInstance(index);
 +     * Class indexEntryClass = generator.getIndexEntryClass();
 +     * ...
 +     * Storable indexEntry = instance of indexEntryClass
 +     * generator.setAllProperties(indexEntry, source);
 +     * </pre>
 +     *
 +     * @throws SupportException if any non-primary key property doesn't have a
 +     * public read method.
 +     */
 +    public static <S extends Storable> IndexEntryGenerator<S>
 +            getInstance(StorableIndex<S> index) throws SupportException
 +    {
 +        synchronized(cCache) {
 +            IndexEntryGenerator<S> generator;
 +            Reference<IndexEntryGenerator> ref = cCache.get(index);
 +            if (ref != null) {
 +                generator = ref.get();
 +                if (generator != null) {
 +                    return generator;
 +                }
 +            }
 +            generator = new IndexEntryGenerator<S>(index);
 +            cCache.put(index, new SoftReference<IndexEntryGenerator>(generator));
 +            return generator;
 +        }
 +    }
 +
 +    private SyntheticStorableReferenceBuilder<S> mBuilder;
 +
 +    /**
 +     * Convenience class for gluing new "builder" style synthetics to the traditional
 +     * generator style.
 +     * @param index Generator style index specification
 +     */
 +    public IndexEntryGenerator(StorableIndex<S> index) throws SupportException {
 +        // Need to try to find the base type.  This is an awkward way to do it,
 +        // but we have nothing better available to us
 +        Class<S> type = index.getProperty(0).getEnclosingType();
 +
 +        mBuilder = new SyntheticStorableReferenceBuilder<S>(type, index.isUnique());
 +
 +        for (int i=0; i<index.getPropertyCount();  i++) {
 +            StorableProperty source = index.getProperty(i);
 +            mBuilder.addKeyProperty(source.getName(), index.getPropertyDirection(i));
 +        }
 +        mBuilder.build();
 +    }
 +
 +    /**
 +     * Returns generated index entry class, which is abstract.
 +     *
 +     * @return class of index entry, which is a custom Storable
 +     */
 +    public Class<? extends Storable> getIndexEntryClass() {
 +        return mBuilder.getStorableClass();
 +    }
 +
 +    /**
 +     * Loads the master object referenced by the given index entry.
 +     *
 +     * @param indexEntry index entry which points to master
 +     * @return master or null if missing
 +     */
 +    public S loadMaster(Storable ref)  throws FetchException {
 +        return mBuilder.loadMaster(ref);
 +    }
 +
 +    /**
 +     * Sets all the properties of the given index entry, using the applicable
 +     * properties of the given master.
 +     *
 +     * @param indexEntry index entry whose properties will be set
 +     * @param master source of property values
 +     */
 +    public void setAllProperties(Storable indexEntry, S master) {
 +        mBuilder.setAllProperties(indexEntry, master);
 +    }
 +
 +    /**
 +     * Returns true if the properties of the given index entry match those
 +     * contained in the master. This will always return true after a call to
 +     * setAllProperties.
 +     *
 +     * @param indexEntry index entry whose properties will be tested
 +     * @param master source of property values
 +     */
 +    public boolean isConsistent(Storable indexEntry, S master) {
 +        return mBuilder.isConsistent(indexEntry, master);
 +    }
 +
 +    /**
 +     * Returns a comparator for ordering index entries.
 +     */
 +    public Comparator<? extends Storable> getComparator() {
 +        return mBuilder.getComparator();
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedCursor.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedCursor.java new file mode 100644 index 0000000..a6df5e0 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedCursor.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.repo.indexed;
 +
 +import java.util.NoSuchElementException;
 +
 +import org.apache.commons.logging.LogFactory;
 +
 +import com.amazon.carbonado.Cursor;
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.PersistException;
 +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.cursor.AbstractCursor;
 +
 +import com.amazon.carbonado.spi.RepairExecutor;
 +
 +/**
 + * Wraps another cursor which contains index entries and extracts master
 + * objects from them.
 + *
 + * @author Brian S O'Neill
 + */
 +class IndexedCursor<S extends Storable> extends AbstractCursor<S> {
 +    private final Cursor<? extends Storable> mCursor;
 +    private final IndexedStorage<S> mStorage;
 +    private final IndexEntryGenerator<S> mGenerator;
 +
 +    private S mNext;
 +
 +    IndexedCursor(Cursor<? extends Storable> indexEntryCursor,
 +                  IndexedStorage<S> storage,
 +                  IndexEntryGenerator<S> indexEntryGenerator) {
 +        mCursor = indexEntryCursor;
 +        mStorage = storage;
 +        mGenerator = indexEntryGenerator;
 +    }
 +
 +    public void close() throws FetchException {
 +        mCursor.close();
 +    }
 +
 +    public boolean hasNext() throws FetchException {
 +        synchronized (mCursor) {
 +            if (mNext != null) {
 +                return true;
 +            }
 +            while (mCursor.hasNext()) {
 +                final Storable indexEntry = mCursor.next();
 +                S master = mGenerator.loadMaster(indexEntry);
 +                if (master == null) {
 +                    LogFactory.getLog(getClass()).warn
 +                        ("Master is missing for index entry: " + indexEntry);
 +                } else {
 +                    if (mGenerator.isConsistent(indexEntry, master)) {
 +                        mNext = master;
 +                        return true;
 +                    }
 +
 +                    // This index entry is stale. Repair is needed.
 +
 +                    // Insert a correct index entry, just to be sure.
 +                    try {
 +                        final IndexedRepository repo = mStorage.mRepository;
 +                        final Storage<?> indexEntryStorage =
 +                            repo.getIndexEntryStorageFor(mGenerator.getIndexEntryClass());
 +                        Storable newIndexEntry = indexEntryStorage.prepare();
 +                        mGenerator.setAllProperties(newIndexEntry, master);
 +
 +                        if (newIndexEntry.tryLoad()) {
 +                            // Good, the correct index entry exists. We'll see
 +                            // the master record eventually, so skip.
 +                        } else {
 +                            // We have no choice but to return the master, at
 +                            // the risk of seeing it multiple times. This is
 +                            // better than seeing it never.
 +                            LogFactory.getLog(getClass()).warn
 +                                ("Inconsistent index entry: " + indexEntry);
 +                            mNext = master;
 +                        }
 +
 +                        // Repair the stale index entry.
 +                        RepairExecutor.execute(new Runnable() {
 +                            public void run() {
 +                                Transaction txn = repo.enterTransaction();
 +                                try {
 +                                    // Reload master and verify inconsistency.
 +                                    S master = mGenerator.loadMaster(indexEntry);
 +                                    if (mGenerator.isConsistent(indexEntry, master)) {
 +                                        return;
 +                                    }
 +
 +                                    Storable newIndexEntry = indexEntryStorage.prepare();
 +                                    mGenerator.setAllProperties(newIndexEntry, master);
 +
 +                                    newIndexEntry.tryInsert();
 +
 +                                    indexEntry.tryDelete();
 +                                    txn.commit();
 +                                } catch (FetchException fe) {
 +                                    LogFactory.getLog(IndexedCursor.class).warn
 +                                        ("Unable to check if repair required for " +
 +                                         "inconsistent index entry " +
 +                                         indexEntry, fe);
 +                                } catch (PersistException pe) {
 +                                    LogFactory.getLog(IndexedCursor.class).error
 +                                        ("Unable to repair inconsistent index entry " +
 +                                         indexEntry, pe);
 +                                } finally {
 +                                    try {
 +                                        txn.exit();
 +                                    } catch (PersistException pe) {
 +                                        LogFactory.getLog(IndexedCursor.class).error
 +                                            ("Unable to repair inconsistent index entry " +
 +                                             indexEntry, pe);
 +                                    }
 +                                }
 +                            }
 +                        });
 +                    } catch (RepositoryException re) {
 +                        LogFactory.getLog(getClass()).error
 +                            ("Unable to inspect inconsistent index entry " +
 +                             indexEntry, re);
 +                    }
 +
 +                    if (mNext != null) {
 +                        return true;
 +                    }
 +                }
 +            }
 +            return false;
 +        }
 +    }
 +
 +    public S next() throws FetchException {
 +        synchronized (mCursor) {
 +            if (hasNext()) {
 +                S next = mNext;
 +                mNext = null;
 +                return next;
 +            }
 +        }
 +        throw new NoSuchElementException();
 +    }
 +
 +    public int skipNext(int amount) throws FetchException {
 +        synchronized (mCursor) {
 +            if (mNext == null) {
 +                return mCursor.skipNext(amount);
 +            }
 +
 +            if (amount <= 0) {
 +                if (amount < 0) {
 +                    throw new IllegalArgumentException("Cannot skip negative amount: " + amount);
 +                }
 +                return 0;
 +            }
 +
 +            mNext = null;
 +            return 1 + mCursor.skipNext(amount - 1);
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java new file mode 100644 index 0000000..714e938 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java @@ -0,0 +1,184 @@ +/*
 + * 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.repo.indexed;
 +
 +import java.util.ArrayList;
 +import java.util.Arrays;
 +import java.util.Map;
 +import java.util.IdentityHashMap;
 +
 +import com.amazon.carbonado.Cursor;
 +import com.amazon.carbonado.IsolationLevel;
 +import com.amazon.carbonado.MalformedTypeException;
 +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.Transaction;
 +
 +import com.amazon.carbonado.capability.Capability;
 +import com.amazon.carbonado.capability.IndexInfo;
 +import com.amazon.carbonado.capability.IndexInfoCapability;
 +import com.amazon.carbonado.capability.StorableInfoCapability;
 +
 +import com.amazon.carbonado.info.StorableIntrospector;
 +
 +/**
 + * Wraps another repository in order to make it support indexes. The wrapped
 + * repository must support creation of new types.
 + *
 + * @author Brian S O'Neill
 + */
 +class IndexedRepository implements Repository,
 +                                   IndexInfoCapability,
 +                                   StorableInfoCapability,
 +                                   IndexEntryAccessCapability
 +{
 +    private final Repository mRepository;
 +    private final String mName;
 +    private final Map<Class<?>, IndexedStorage<?>> mStorages;
 +
 +    IndexedRepository(String name, Repository repository) {
 +        mRepository = repository;
 +        mName = name;
 +        mStorages = new IdentityHashMap<Class<?>, IndexedStorage<?>>();
 +        if (repository.getCapability(IndexInfoCapability.class) == null) {
 +            throw new UnsupportedOperationException
 +                ("Wrapped repository doesn't support being indexed");
 +        }
 +    }
 +
 +    public String getName() {
 +        return mName;
 +    }
 +
 +    @SuppressWarnings("unchecked")
 +    public <S extends Storable> Storage<S> storageFor(Class<S> type)
 +        throws MalformedTypeException, SupportException, RepositoryException
 +    {
 +        synchronized (mStorages) {
 +            IndexedStorage<S> storage = (IndexedStorage<S>) mStorages.get(type);
 +            if (storage == null) {
 +                Storage<S> masterStorage = mRepository.storageFor(type);
 +
 +                if (Unindexed.class.isAssignableFrom(type)) {
 +                    // Verify no indexes.
 +                    int indexCount = IndexedStorage
 +                        .gatherRequiredIndexes(StorableIntrospector.examine(type)).size();
 +                    if (indexCount > 0) {
 +                        throw new MalformedTypeException
 +                            (type, "Storable cannot have any indexes: " + type +
 +                             ", " + indexCount);
 +                    }
 +                    return mRepository.storageFor(type);
 +                }
 +
 +                storage = new IndexedStorage<S>(this, masterStorage);
 +                mStorages.put(type, storage);
 +            }
 +            return storage;
 +        }
 +    }
 +
 +    public Transaction enterTransaction() {
 +        return mRepository.enterTransaction();
 +    }
 +
 +    public Transaction enterTransaction(IsolationLevel level) {
 +        return mRepository.enterTransaction(level);
 +    }
 +
 +    public Transaction enterTopTransaction(IsolationLevel level) {
 +        return mRepository.enterTopTransaction(level);
 +    }
 +
 +    public IsolationLevel getTransactionIsolationLevel() {
 +        return mRepository.getTransactionIsolationLevel();
 +    }
 +
 +    @SuppressWarnings("unchecked")
 +    public <C extends Capability> C getCapability(Class<C> capabilityType) {
 +        if (capabilityType.isInstance(this)) {
 +            return (C) this;
 +        }
 +        return mRepository.getCapability(capabilityType);
 +    }
 +
 +    public <S extends Storable> IndexInfo[] getIndexInfo(Class<S> storableType)
 +        throws RepositoryException
 +    {
 +        return ((IndexedStorage) storageFor(storableType)).getIndexInfo();
 +    }
 +
 +    public <S extends Storable> IndexEntryAccessor<S>[]
 +        getIndexEntryAccessors(Class<S> storableType)
 +        throws RepositoryException
 +    {
 +        return ((IndexedStorage<S>) storageFor(storableType)).getIndexEntryAccessors();
 +    }
 +
 +    public String[] getUserStorableTypeNames() throws RepositoryException {
 +        StorableInfoCapability cap = mRepository.getCapability(StorableInfoCapability.class);
 +        if (cap == null) {
 +            return new String[0];
 +        }
 +        ArrayList<String> names =
 +            new ArrayList<String>(Arrays.asList(cap.getUserStorableTypeNames()));
 +
 +        // Exclude our own metadata types as well as indexes.
 +
 +        names.remove(StoredIndexInfo.class.getName());
 +
 +        Cursor<StoredIndexInfo> cursor =
 +            mRepository.storageFor(StoredIndexInfo.class)
 +            .query().fetch();
 +
 +        while (cursor.hasNext()) {
 +            StoredIndexInfo info = cursor.next();
 +            names.remove(info.getIndexName());
 +        }
 +
 +        return names.toArray(new String[names.size()]);
 +    }
 +
 +    public boolean isSupported(Class<Storable> type) {
 +        StorableInfoCapability cap = mRepository.getCapability(StorableInfoCapability.class);
 +        return (cap == null) ? null : cap.isSupported(type);
 +    }
 +
 +    public boolean isPropertySupported(Class<Storable> type, String name) {
 +        StorableInfoCapability cap = mRepository.getCapability(StorableInfoCapability.class);
 +        return (cap == null) ? null : cap.isPropertySupported(type, name);
 +    }
 +
 +    public void close() {
 +        mRepository.close();
 +    }
 +
 +    Storage<?> getIndexEntryStorageFor(Class<? extends Storable> indexEntryClass)
 +        throws RepositoryException
 +    {
 +        return mRepository.storageFor(indexEntryClass);
 +    }
 +
 +    Repository getWrappedRepository() {
 +        return mRepository;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepositoryBuilder.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepositoryBuilder.java new file mode 100644 index 0000000..cfbe128 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepositoryBuilder.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.repo.indexed;
 +
 +import java.util.Collection;
 +
 +import com.amazon.carbonado.ConfigurationException;
 +import com.amazon.carbonado.Repository;
 +import com.amazon.carbonado.RepositoryBuilder;
 +import com.amazon.carbonado.RepositoryException;
 +
 +import com.amazon.carbonado.spi.AbstractRepositoryBuilder;
 +
 +/**
 + * Repository builder for the indexed repository.
 + * <p>
 + * In addition to supporting the capabilities of the wrapped repository, the
 + * following extra capabilities are supported:
 + * <ul>
 + * <li>{@link com.amazon.carbonado.capability.IndexInfoCapability IndexInfoCapability}
 + * <li>{@link com.amazon.carbonado.capability.StorableInfoCapability StorableInfoCapability}
 + * <li>{@link IndexEntryAccessCapability IndexEntryAccessCapability}
 + * </ul>
 + *
 + * @author Brian S O'Neill
 + */
 +public class IndexedRepositoryBuilder extends AbstractRepositoryBuilder {
 +    private String mName;
 +    private boolean mIsMaster = true;
 +    private RepositoryBuilder mRepoBuilder;
 +
 +    public IndexedRepositoryBuilder() {
 +    }
 +
 +    public Repository build(RepositoryReference rootRef) throws RepositoryException {
 +        assertReady();
 +
 +        Repository wrapped;
 +
 +        boolean originalOption = mRepoBuilder.isMaster();
 +        try {
 +            mRepoBuilder.setMaster(mIsMaster);
 +            wrapped = mRepoBuilder.build(rootRef);
 +        } finally {
 +            mRepoBuilder.setMaster(originalOption);
 +        }
 +
 +        if (wrapped instanceof IndexedRepository) {
 +            return wrapped;
 +        }
 +
 +        Repository repo = new IndexedRepository(getName(), wrapped);
 +        rootRef.set(repo);
 +        return repo;
 +    }
 +
 +    public String getName() {
 +        String name = mName;
 +        if (name == null && mRepoBuilder != null) {
 +            name = mRepoBuilder.getName();
 +        }
 +        return name;
 +    }
 +
 +    public void setName(String name) {
 +        mName = name;
 +    }
 +
 +    public boolean isMaster() {
 +        return mIsMaster;
 +    }
 +
 +    public void setMaster(boolean b) {
 +        mIsMaster = b;
 +    }
 +
 +    /**
 +     * @return wrapped respository
 +     */
 +    public RepositoryBuilder getWrappedRepository() {
 +        return mRepoBuilder;
 +    }
 +
 +    /**
 +     * Set the required wrapped respository, which must support the
 +     * {@link com.amazon.carbonado.capability.IndexInfoCapability IndexInfoCapability}.
 +     */
 +    public void setWrappedRepository(RepositoryBuilder repoBuilder) {
 +        mRepoBuilder = repoBuilder;
 +    }
 +
 +    public void errorCheck(Collection<String> messages) throws ConfigurationException {
 +        super.errorCheck(messages);
 +        if (null == getWrappedRepository()) {
 +            messages.add("wrapped repository missing");
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java new file mode 100644 index 0000000..dd8b2b5 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java @@ -0,0 +1,408 @@ +/*
 + * 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.repo.indexed;
 +
 +import java.util.ArrayList;
 +import java.util.Comparator;
 +import java.util.IdentityHashMap;
 +import java.util.List;
 +import java.util.Map;
 +
 +import org.apache.commons.logging.Log;
 +import org.apache.commons.logging.LogFactory;
 +
 +import com.amazon.carbonado.Cursor;
 +import com.amazon.carbonado.FetchException;
 +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.SupportException;
 +import com.amazon.carbonado.Transaction;
 +import com.amazon.carbonado.Trigger;
 +import com.amazon.carbonado.UniqueConstraintException;
 +
 +import com.amazon.carbonado.capability.IndexInfo;
 +import com.amazon.carbonado.capability.IndexInfoCapability;
 +
 +import com.amazon.carbonado.filter.Filter;
 +
 +import com.amazon.carbonado.info.Direction;
 +import com.amazon.carbonado.info.StorableInfo;
 +import com.amazon.carbonado.info.StorableIntrospector;
 +import com.amazon.carbonado.info.StorableIndex;
 +
 +import com.amazon.carbonado.cursor.MergeSortBuffer;
 +
 +import com.amazon.carbonado.spi.BaseQueryEngine;
 +import com.amazon.carbonado.spi.BoundaryType;
 +import com.amazon.carbonado.spi.RepairExecutor;
 +import com.amazon.carbonado.spi.StorableIndexSet;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +class IndexedStorage<S extends Storable> implements Storage<S> {
 +    static <S extends Storable> StorableIndexSet<S> gatherRequiredIndexes(StorableInfo<S> info) {
 +        StorableIndexSet<S> indexSet = new StorableIndexSet<S>();
 +        indexSet.addIndexes(info);
 +        indexSet.addAlternateKeys(info);
 +        return indexSet;
 +    }
 +
 +    final IndexedRepository mRepository;
 +    final Storage<S> mMasterStorage;
 +
 +    private final Map<StorableIndex<S>, IndexInfo> mIndexInfoMap;
 +
 +    private final QueryEngine<S> mQueryEngine;
 +
 +    @SuppressWarnings("unchecked")
 +    IndexedStorage(IndexedRepository repository, Storage<S> masterStorage)
 +        throws RepositoryException
 +    {
 +        mRepository = repository;
 +        mMasterStorage = masterStorage;
 +        mIndexInfoMap = new IdentityHashMap<StorableIndex<S>, IndexInfo>();
 +
 +        StorableInfo<S> info = StorableIntrospector.examine(masterStorage.getStorableType());
 +
 +        // Determine what the set of indexes should be.
 +        StorableIndexSet<S> newIndexSet = gatherRequiredIndexes(info);
 +
 +        // Mix in the indexes we get for free, but remove after reduce. A free
 +        // index is one that the underlying storage is providing for us. We
 +        // don't want to create redundant indexes.
 +        IndexInfo[] infos = repository.getWrappedRepository()
 +            .getCapability(IndexInfoCapability.class)
 +            .getIndexInfo(masterStorage.getStorableType());
 +
 +        StorableIndex<S>[] freeIndexes = new StorableIndex[infos.length];
 +        for (int i=0; i<infos.length; i++) {
 +            try {
 +                freeIndexes[i] = new StorableIndex<S>(masterStorage.getStorableType(), infos[i]);
 +                newIndexSet.add(freeIndexes[i]);
 +                mIndexInfoMap.put(freeIndexes[i], infos[i]);
 +            } catch (IllegalArgumentException e) {
 +                // Assume index is bogus, so ignore it.
 +            }
 +        }
 +
 +        newIndexSet.reduce(Direction.ASCENDING);
 +
 +        // Gather current indexes.
 +        StorableIndexSet<S> currentIndexSet = new StorableIndexSet<S>();
 +        // Gather indexes to remove.
 +        StorableIndexSet<S> indexesToRemove = new StorableIndexSet<S>();
 +
 +        Query<StoredIndexInfo> query = repository.getWrappedRepository()
 +            .storageFor(StoredIndexInfo.class)
 +            // Primary key of StoredIndexInfo is an index descriptor, which
 +            // starts with the storable type name. This emulates a "wildcard at
 +            // the end" search.
 +            .query("indexName >= ? & indexName < ?")
 +            .with(getStorableType().getName() + '~')
 +            .with(getStorableType().getName() + '~' + '\uffff');
 +
 +        for (StoredIndexInfo indexInfo : query.fetch().toList()) {
 +            String name = indexInfo.getIndexName();
 +            StorableIndex index;
 +            try {
 +                index = StorableIndex.parseNameDescriptor(name, info);
 +            } catch (IllegalArgumentException e) {
 +                // Remove unrecognized descriptors.
 +                unregisterIndex(name);
 +                continue;
 +            }
 +            if (index.getTypeDescriptor().equals(indexInfo.getIndexTypeDescriptor())) {
 +                currentIndexSet.add(index);
 +            } else {
 +                indexesToRemove.add(index);
 +            }
 +        }
 +
 +        nonUniqueSearch: {
 +            // If any current indexes are non-unique, then indexes are for an
 +            // older version. For compatibility, don't uniquify the
 +            // indexes. Otherwise, these indexes would need to be rebuilt.
 +            for (StorableIndex<S> index : currentIndexSet) {
 +                if (!index.isUnique()) {
 +                    break nonUniqueSearch;
 +                }
 +            }
 +
 +            // The index implementation includes all primary key properties
 +            // anyhow, so adding them here allows query analyzer to see these
 +            // properties. As a side-effect of uniquify, all indexes are
 +            // unique, and thus have 'U' in the descriptor. Each time
 +            // nonUniqueSearch is run, it will not find any non-unique indexes.
 +            newIndexSet.uniquify(info);
 +        }
 +
 +        // Remove any old indexes.
 +        {
 +            indexesToRemove.addAll(currentIndexSet);
 +
 +            // Remove "free" indexes, since they don't need to be built.
 +            for (int i=0; i<freeIndexes.length; i++) {
 +                newIndexSet.remove(freeIndexes[i]);
 +            }
 +
 +            indexesToRemove.removeAll(newIndexSet);
 +
 +            for (StorableIndex<S> index : indexesToRemove) {
 +                removeIndex(index);
 +            }
 +        }
 +
 +        currentIndexSet = newIndexSet;
 +
 +        // Open all the indexes.
 +        List<ManagedIndex<S>> managedIndexList = new ArrayList<ManagedIndex<S>>();
 +        for (StorableIndex<S> index : currentIndexSet) {
 +            IndexEntryGenerator<S> builder = IndexEntryGenerator.getInstance(index);
 +            Class<? extends Storable> indexEntryClass = builder.getIndexEntryClass();
 +            Storage<?> indexEntryStorage = repository.getIndexEntryStorageFor(indexEntryClass);
 +            ManagedIndex managedIndex = new ManagedIndex<S>(index, builder, indexEntryStorage);
 +
 +            registerIndex(managedIndex);
 +
 +            mIndexInfoMap.put(index, managedIndex);
 +            managedIndexList.add(managedIndex);
 +        }
 +
 +        if (managedIndexList.size() > 0) {
 +            // Add trigger to keep indexes up-to-date.
 +            ManagedIndex<S>[] managedIndexes =
 +                managedIndexList.toArray(new ManagedIndex[managedIndexList.size()]);
 +
 +            if (!addTrigger(new IndexesTrigger<S>(managedIndexes))) {
 +                throw new RepositoryException("Unable to add trigger for managing indexes");
 +            }
 +        }
 +
 +        // Add "free" indexes back, in order for query engine to consider them.
 +        for (int i=0; i<freeIndexes.length; i++) {
 +            currentIndexSet.add(freeIndexes[i]);
 +        }
 +
 +        mQueryEngine = new QueryEngine<S>(info, repository, this, currentIndexSet);
 +    }
 +
 +    public Class<S> getStorableType() {
 +        return mMasterStorage.getStorableType();
 +    }
 +
 +    public S prepare() {
 +        return mMasterStorage.prepare();
 +    }
 +
 +    public Query<S> query() throws FetchException {
 +        return mQueryEngine.getCompiledQuery();
 +    }
 +
 +    public Query<S> query(String filter) throws FetchException {
 +        return mQueryEngine.getCompiledQuery(filter);
 +    }
 +
 +    public Query<S> query(Filter<S> filter) throws FetchException {
 +        return mQueryEngine.getCompiledQuery(filter);
 +    }
 +
 +    public boolean addTrigger(Trigger<? super S> trigger) {
 +        return mMasterStorage.addTrigger(trigger);
 +    }
 +
 +    public boolean removeTrigger(Trigger<? super S> trigger) {
 +        return mMasterStorage.removeTrigger(trigger);
 +    }
 +
 +    public IndexInfo[] getIndexInfo() {
 +        IndexInfo[] infos = new IndexInfo[mIndexInfoMap.size()];
 +        return mIndexInfoMap.values().toArray(infos);
 +    }
 +
 +    @SuppressWarnings("unchecked")
 +    public IndexEntryAccessor<S>[] getIndexEntryAccessors() {
 +        List<IndexEntryAccessor<S>> accessors =
 +            new ArrayList<IndexEntryAccessor<S>>(mIndexInfoMap.size());
 +        for (IndexInfo info : mIndexInfoMap.values()) {
 +            if (info instanceof IndexEntryAccessor) {
 +                accessors.add((IndexEntryAccessor<S>) info);
 +            }
 +        }
 +        return accessors.toArray(new IndexEntryAccessor[accessors.size()]);
 +    }
 +
 +    Storage<S> getStorageFor(StorableIndex<S> index) {
 +        if (mIndexInfoMap.get(index) instanceof ManagedIndex) {
 +            // Index is managed by this storage, which is typical.
 +            return this;
 +        }
 +        // Index is managed by master storage, most likely a primary key index.
 +        return mMasterStorage;
 +    }
 +
 +    ManagedIndex<S> getManagedIndex(StorableIndex<S> index) {
 +        return (ManagedIndex<S>) mIndexInfoMap.get(index);
 +    }
 +
 +    private void registerIndex(ManagedIndex<S> managedIndex)
 +        throws RepositoryException
 +    {
 +        StorableIndex index = managedIndex.getIndex();
 +
 +        if (StoredIndexInfo.class.isAssignableFrom(getStorableType())) {
 +            throw new IllegalStateException("StoredIndexInfo cannot have indexes");
 +        }
 +        StoredIndexInfo info = mRepository.getWrappedRepository()
 +            .storageFor(StoredIndexInfo.class).prepare();
 +        info.setIndexName(index.getNameDescriptor());
 +
 +        if (info.tryLoad()) {
 +            // Index already exists and is registered.
 +            return;
 +        }
 +
 +        // New index, so populate it.
 +        managedIndex.populateIndex(mRepository, mMasterStorage);
 +
 +        Transaction txn = mRepository.getWrappedRepository().enterTransaction();
 +        try {
 +            if (!info.tryLoad()) {
 +                info.setIndexTypeDescriptor(index.getTypeDescriptor());
 +                info.setCreationTimestamp(System.currentTimeMillis());
 +                info.setVersionNumber(0);
 +                info.insert();
 +                txn.commit();
 +            }
 +        } finally {
 +            txn.exit();
 +        }
 +    }
 +
 +    private void unregisterIndex(StorableIndex index) throws RepositoryException {
 +        if (StoredIndexInfo.class.isAssignableFrom(getStorableType())) {
 +            // Can't unregister when register wasn't allowed.
 +            return;
 +        }
 +        unregisterIndex(index.getNameDescriptor());
 +    }
 +
 +    private void unregisterIndex(String indexName) throws RepositoryException {
 +        StoredIndexInfo info = mRepository.getWrappedRepository()
 +            .storageFor(StoredIndexInfo.class).prepare();
 +        info.setIndexName(indexName);
 +        info.delete();
 +    }
 +
 +    @SuppressWarnings("unchecked")
 +    private void removeIndex(StorableIndex index) throws RepositoryException {
 +        Log log = LogFactory.getLog(IndexedStorage.class);
 +        if (log.isInfoEnabled()) {
 +            StringBuilder b = new StringBuilder();
 +            b.append("Removing index on ");
 +            b.append(getStorableType().getName());
 +            b.append(": ");
 +            try {
 +                index.appendTo(b);
 +            } catch (java.io.IOException e) {
 +                // Not gonna happen.
 +            }
 +            log.info(b.toString());
 +        }
 +
 +        Class<? extends Storable> indexEntryClass =
 +            IndexEntryGenerator.getInstance(index).getIndexEntryClass();
 +
 +        Storage<?> indexEntryStorage;
 +        try {
 +            indexEntryStorage = mRepository.getIndexEntryStorageFor(indexEntryClass);
 +        } catch (Exception e) {
 +            // Assume it doesn't exist.
 +            unregisterIndex(index);
 +            return;
 +        }
 +
 +        // Doesn't completely remove the index, but it should free up space.
 +        // TODO: when truncate method exists, call that instead
 +        indexEntryStorage.query().deleteAll();
 +        unregisterIndex(index);
 +    }
 +
 +    private static class QueryEngine<S extends Storable> extends BaseQueryEngine<S> {
 +
 +        QueryEngine(StorableInfo<S> info,
 +                    Repository repo,
 +                    IndexedStorage<S> storage,
 +                    StorableIndexSet<S> indexSet) {
 +            super(info, repo, storage, null, indexSet);
 +        }
 +
 +        @Override
 +        protected Storage<S> getStorageFor(StorableIndex<S> index) {
 +            return storage().getStorageFor(index);
 +        }
 +
 +        protected Cursor<S> openCursor(StorableIndex<S> index,
 +                                       Object[] exactValues,
 +                                       BoundaryType rangeStartBoundary,
 +                                       Object rangeStartValue,
 +                                       BoundaryType rangeEndBoundary,
 +                                       Object rangeEndValue,
 +                                       boolean reverseRange,
 +                                       boolean reverseOrder)
 +            throws FetchException
 +        {
 +            // Note: this code ignores the reverseRange parameter to avoid
 +            // double reversal. Only the lowest storage layer should examine
 +            // this parameter.
 +
 +            ManagedIndex<S> indexInfo = storage().getManagedIndex(index);
 +            Query<?> query = indexInfo.getIndexEntryQueryFor
 +                (exactValues == null ? 0 : exactValues.length,
 +                 rangeStartBoundary, rangeEndBoundary, reverseOrder);
 +
 +            if (exactValues != null) {
 +                query = query.withValues(exactValues);
 +            }
 +
 +            if (rangeStartBoundary != BoundaryType.OPEN) {
 +                query = query.with(rangeStartValue);
 +            }
 +            if (rangeEndBoundary != BoundaryType.OPEN) {
 +                query = query.with(rangeEndValue);
 +            }
 +
 +            Cursor<? extends Storable> indexEntryCursor = query.fetch();
 +
 +            return new IndexedCursor<S>
 +                (indexEntryCursor, storage(), indexInfo.getIndexEntryClassBuilder());
 +        }
 +
 +        private IndexedStorage<S> storage() {
 +            return (IndexedStorage<S>) super.getStorage();
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/IndexesTrigger.java b/src/main/java/com/amazon/carbonado/repo/indexed/IndexesTrigger.java new file mode 100644 index 0000000..ab718e4 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/IndexesTrigger.java @@ -0,0 +1,102 @@ +/*
 + * 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.repo.indexed;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.PersistException;
 +import com.amazon.carbonado.Storable;
 +import com.amazon.carbonado.Trigger;
 +import com.amazon.carbonado.UniqueConstraintException;
 +
 +/**
 + * Handles index updates.
 + *
 + * @author Brian S O'Neill
 + */
 +class IndexesTrigger<S extends Storable> extends Trigger<S> {
 +    private final ManagedIndex<S>[] mManagedIndexes;
 +
 +    /**
 +     * @param managedIndexes all the indexes that need to be updated.
 +     */
 +    IndexesTrigger(ManagedIndex<S>[] managedIndexes) {
 +        mManagedIndexes = managedIndexes;
 +    }
 +
 +    @Override
 +    public void afterInsert(S storable, Object state) throws PersistException {
 +        for (ManagedIndex<S> managed : mManagedIndexes) {
 +            if (!managed.insertIndexEntry(storable)) {
 +                throw new UniqueConstraintException
 +                    ("Alternate key constraint: " + storable.toString() +
 +                     ", " + managed);
 +            }
 +        }
 +    }
 +
 +    @Override
 +    public void afterTryInsert(S storable, Object state) throws PersistException {
 +        for (ManagedIndex<S> managed : mManagedIndexes) {
 +            if (!managed.insertIndexEntry(storable)) {
 +                throw abortTry();
 +            }
 +        }
 +    }
 +
 +    @Override
 +    public Object beforeUpdate(S storable) throws PersistException {
 +        // Return old storable for afterUpdate.
 +        S copy = (S) storable.copy();
 +        try {
 +            if (copy.tryLoad()) {
 +                return copy;
 +            }
 +        } catch (FetchException e) {
 +            throw e.toPersistException();
 +        }
 +        // If this point is reached, then afterUpdate is not called because
 +        // update will fail.
 +        return null;
 +    }
 +
 +    @Override
 +    public void afterUpdate(S storable, Object state) throws PersistException {
 +        // Cast old storable as provided by beforeUpdate.
 +        S oldStorable = (S) state;
 +        for (ManagedIndex<S> managed : mManagedIndexes) {
 +            managed.updateIndexEntry(storable, oldStorable);
 +        }
 +    }
 +
 +    @Override
 +    public Object beforeDelete(S storable) throws PersistException {
 +        // Delete index entries referenced by existing storable.
 +        S copy = (S) storable.copy();
 +        try {
 +            if (copy.tryLoad()) {
 +                for (ManagedIndex<S> managed : mManagedIndexes) {
 +                    managed.deleteIndexEntry(copy);
 +                }
 +            }
 +        } catch (FetchException e) {
 +            throw e.toPersistException();
 +        }
 +        return null;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java b/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java new file mode 100644 index 0000000..107067c --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java @@ -0,0 +1,440 @@ +/*
 + * 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.repo.indexed;
 +
 +import java.util.Comparator;
 +
 +import org.apache.commons.logging.Log;
 +import org.apache.commons.logging.LogFactory;
 +
 +import com.amazon.carbonado.Cursor;
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.PersistException;
 +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.Transaction;
 +import com.amazon.carbonado.UniqueConstraintException;
 +
 +import com.amazon.carbonado.info.Direction;
 +import com.amazon.carbonado.info.StorableIndex;
 +
 +import com.amazon.carbonado.cursor.MergeSortBuffer;
 +
 +import com.amazon.carbonado.spi.RepairExecutor;
 +
 +import com.amazon.carbonado.qe.BoundaryType;
 +
 +/**
 + * Encapsulates info and operations for a single index.
 + *
 + * @author Brian S O'Neill
 + */
 +class ManagedIndex<S extends Storable> implements IndexEntryAccessor<S> {
 +    private static final int POPULATE_BATCH_SIZE = 256;
 +
 +    private final StorableIndex mIndex;
 +    private final IndexEntryGenerator<S> mGenerator;
 +    private final Storage<?> mIndexEntryStorage;
 +
 +    private final Query<?>[] mQueryCache;
 +
 +    ManagedIndex(StorableIndex<S> index,
 +                 IndexEntryGenerator<S> generator,
 +                 Storage<?> indexEntryStorage)
 +        throws SupportException
 +    {
 +        mIndex = index;
 +        mGenerator = generator;
 +        mIndexEntryStorage = indexEntryStorage;
 +
 +        // Cache keys are encoded as follows:
 +        // bits 1..0: range end boundary
 +        //            0=open boundary, 1=inclusive boundary, 2=exlusive boundary, 3=not used
 +        // bits 3..2: range start boundary
 +        //            0=open boundary, 1=inclusive boundary, 2=exlusive boundary, 3=not used
 +        // bit     4: 0=forward order, 1=reverse
 +        // bits n..5: exact property match count
 +
 +        // The size of the cache is dependent on the number of possible
 +        // exactly matching properties, which is the property count of the
 +        // index. If the index contained a huge number of properties, say
 +        // 31, the cache size would be 1024.
 +
 +        int cacheSize = Integer.highestOneBit(index.getPropertyCount()) << (1 + 5);
 +
 +        mQueryCache = new Query[cacheSize];
 +    }
 +
 +    public String getName() {
 +        return mIndex.getNameDescriptor();
 +    }
 +
 +    public String[] getPropertyNames() {
 +        int i = mIndex.getPropertyCount();
 +        String[] names = new String[i];
 +        while (--i >= 0) {
 +            names[i] = mIndex.getProperty(i).getName();
 +        }
 +        return names;
 +    }
 +
 +    public Direction[] getPropertyDirections() {
 +        int i = mIndex.getPropertyCount();
 +        Direction[] directions = new Direction[i];
 +        while (--i >= 0) {
 +            directions[i] = mIndex.getPropertyDirection(i);
 +        }
 +        return directions;
 +    }
 +
 +    public boolean isUnique() {
 +        return mIndex.isUnique();
 +    }
 +
 +    public boolean isClustered() {
 +        return false;
 +    }
 +
 +    public StorableIndex getIndex() {
 +        return mIndex;
 +    }
 +
 +    // Required by IndexEntryAccessor interface.
 +    public Storage<?> getIndexEntryStorage() {
 +        return mIndexEntryStorage;
 +    }
 +
 +    // Required by IndexEntryAccessor interface.
 +    public S loadMaster(Storable indexEntry) throws FetchException {
 +        return mGenerator.loadMaster(indexEntry);
 +    }
 +
 +    // Required by IndexEntryAccessor interface.
 +    public void setAllProperties(Storable indexEntry, S master) {
 +        mGenerator.setAllProperties(indexEntry, master);
 +    }
 +
 +    // Required by IndexEntryAccessor interface.
 +    public boolean isConsistent(Storable indexEntry, S master) {
 +        return mGenerator.isConsistent(indexEntry, master);
 +    }
 +
 +    // Required by IndexEntryAccessor interface.
 +    public Comparator<? extends Storable> getComparator() {
 +        return mGenerator.getComparator();
 +    }
 +
 +    public IndexEntryGenerator<S> getIndexEntryClassBuilder() {
 +        return mGenerator;
 +    }
 +
 +    public Query<?> getIndexEntryQueryFor(int exactMatchCount,
 +                                          BoundaryType rangeStartBoundary,
 +                                          BoundaryType rangeEndBoundary,
 +                                          boolean reverse)
 +        throws FetchException
 +    {
 +        int key = exactMatchCount << 5;
 +        if (rangeEndBoundary != BoundaryType.OPEN) {
 +            if (rangeEndBoundary == BoundaryType.INCLUSIVE) {
 +                key |= 0x01;
 +            } else {
 +                key |= 0x02;
 +            }
 +        }
 +        if (rangeStartBoundary != BoundaryType.OPEN) {
 +            if (rangeStartBoundary == BoundaryType.INCLUSIVE) {
 +                key |= 0x04;
 +            } else {
 +                key |= 0x08;
 +            }
 +        }
 +
 +        if (reverse) {
 +            key |= 0x10;
 +        }
 +
 +        Query<?> query = mQueryCache[key];
 +        if (query == null) {
 +            StorableIndex index = mIndex;
 +
 +            StringBuilder filter = new StringBuilder();
 +
 +            int i;
 +            for (i=0; i<exactMatchCount; i++) {
 +                if (i > 0) {
 +                    filter.append(" & ");
 +                }
 +                filter.append(index.getProperty(i).getName());
 +                filter.append(" = ?");
 +            }
 +
 +            boolean addOrderBy = false;
 +
 +            if (rangeStartBoundary != BoundaryType.OPEN) {
 +                addOrderBy = true;
 +                if (filter.length() > 0) {
 +                    filter.append(" & ");
 +                }
 +                filter.append(index.getProperty(i).getName());
 +                if (rangeStartBoundary == BoundaryType.INCLUSIVE) {
 +                    filter.append(" >= ?");
 +                } else {
 +                    filter.append(" > ?");
 +                }
 +            }
 +
 +            if (rangeEndBoundary != BoundaryType.OPEN) {
 +                addOrderBy = true;
 +                if (filter.length() > 0) {
 +                    filter.append(" & ");
 +                }
 +                filter.append(index.getProperty(i).getName());
 +                if (rangeEndBoundary == BoundaryType.INCLUSIVE) {
 +                    filter.append(" <= ?");
 +                } else {
 +                    filter.append(" < ?");
 +                }
 +            }
 +
 +            if (filter.length() == 0) {
 +                query = mIndexEntryStorage.query();
 +            } else {
 +                query = mIndexEntryStorage.query(filter.toString());
 +            }
 +
 +            if (addOrderBy || reverse) {
 +                // Enforce ordering of properties for range searches and
 +                // reverse ordering to work properly. Underlying repository
 +                // should have ordered index properly, so this shouldn't
 +                // cause a sort.
 +                // TODO: should somehow warn if a sort is triggered
 +                String[] orderProperties = new String[exactMatchCount + 1];
 +                for (i=0; i<orderProperties.length; i++) {
 +                    Direction dir = index.getPropertyDirection(i);
 +                    if (reverse) {
 +                        dir = dir.reverse();
 +                    }
 +                    orderProperties[i] = dir.toCharacter() + index.getProperty(i).getName();
 +                }
 +                query = query.orderBy(orderProperties);
 +            }
 +
 +            mQueryCache[key] = query;
 +        }
 +
 +        return query;
 +    }
 +
 +    public String toString() {
 +        StringBuilder b = new StringBuilder();
 +        b.append("IndexInfo ");
 +        try {
 +            mIndex.appendTo(b);
 +        } catch (java.io.IOException e) {
 +            // Not gonna happen.
 +        }
 +        return b.toString();
 +    }
 +
 +    /** Assumes caller is in a transaction */
 +    boolean deleteIndexEntry(S userStorable) throws PersistException {
 +        return makeIndexEntry(userStorable).tryDelete();
 +    }
 +
 +    /** Assumes caller is in a transaction */
 +    boolean insertIndexEntry(S userStorable) throws PersistException {
 +        return insertIndexEntry(userStorable, makeIndexEntry(userStorable));
 +    }
 +
 +    /** Assumes caller is in a transaction */
 +    boolean updateIndexEntry(S userStorable, S oldUserStorable) throws PersistException {
 +        Storable newIndexEntry = makeIndexEntry(userStorable);
 +
 +        if (oldUserStorable != null) {
 +            Storable oldIndexEntry = makeIndexEntry(oldUserStorable);
 +            if (oldIndexEntry.equalPrimaryKeys(newIndexEntry)) {
 +                // Index entry didn't change, so nothing to do. If the
 +                // index entry has a version, it will lag behind the
 +                // master's version until the index entry changes, at which
 +                // point the version will again match the master.
 +                return true;
 +            }
 +
 +            oldIndexEntry.tryDelete();
 +        }
 +
 +        return insertIndexEntry(userStorable, newIndexEntry);
 +    }
 +
 +    /**
 +     * Populates the entire index, repairing as it goes.
 +     *
 +     * @param repo used to enter transactions
 +     */
 +    void populateIndex(Repository repo, Storage<S> masterStorage) throws RepositoryException {
 +        Cursor<S> cursor = masterStorage.query().fetch();
 +        if (!cursor.hasNext()) {
 +            // Nothing exists in master, so nothing to populate.
 +            cursor.close();
 +            return;
 +        }
 +
 +        Log log = LogFactory.getLog(IndexedStorage.class);
 +        if (log.isInfoEnabled()) {
 +            StringBuilder b = new StringBuilder();
 +            b.append("Populating index on ");
 +            b.append(masterStorage.getStorableType().getName());
 +            b.append(": ");
 +            try {
 +                mIndex.appendTo(b);
 +            } catch (java.io.IOException e) {
 +                // Not gonna happen.
 +            }
 +            log.info(b.toString());
 +        }
 +
 +        // Preload and sort all index entries for improved performance.
 +
 +        MergeSortBuffer buffer = new MergeSortBuffer(mIndexEntryStorage);
 +        Comparator c = mGenerator.getComparator();
 +        buffer.prepare(c);
 +
 +        while (cursor.hasNext()) {
 +            buffer.add(makeIndexEntry(cursor.next()));
 +        }
 +
 +        buffer.sort();
 +
 +        if (isUnique()) {
 +            // If index is unique, scan buffer and check for duplicates
 +            // _before_ inserting index entries. If there are duplicates,
 +            // fail, since unique index cannot be built.
 +
 +            Object last = null;
 +            for (Object obj : buffer) {
 +                if (last != null) {
 +                    if (c.compare(last, obj) == 0) {
 +                        buffer.close();
 +                        throw new UniqueConstraintException
 +                            ("Cannot build unique index because duplicates exist: "
 +                             + this);
 +                    }
 +                }
 +                last = obj;
 +            }
 +        }
 +
 +        Transaction txn = repo.enterTransaction();
 +        try {
 +            int totalInserted = 0;
 +            for (Object obj : buffer) {
 +                Storable indexEntry = (Storable) obj;
 +                if (!indexEntry.tryInsert()) {
 +                    // repair
 +                    indexEntry.tryDelete();
 +                    indexEntry.tryInsert();
 +                }
 +                totalInserted++;
 +                if (totalInserted % POPULATE_BATCH_SIZE == 0) {
 +                    txn.commit();
 +                    txn.exit();
 +                    txn = repo.enterTransaction();
 +                }
 +            }
 +            txn.commit();
 +        } finally {
 +            txn.exit();
 +            buffer.close();
 +        }
 +    }
 +
 +    private Storable makeIndexEntry(S userStorable) {
 +        Storable indexEntry = mIndexEntryStorage.prepare();
 +        mGenerator.setAllProperties(indexEntry, userStorable);
 +        return indexEntry;
 +    }
 +
 +    /** Assumes caller is in a transaction */
 +    private boolean insertIndexEntry(final S userStorable, final Storable indexEntry)
 +        throws PersistException
 +    {
 +        if (indexEntry.tryInsert()) {
 +            return true;
 +        }
 +
 +        // If index entry already exists, then index might be corrupt.
 +        {
 +            Storable freshEntry = mIndexEntryStorage.prepare();
 +            mGenerator.setAllProperties(freshEntry, userStorable);
 +            indexEntry.copyVersionProperty(freshEntry);
 +            if (freshEntry.equals(indexEntry)) {
 +                // Existing entry is exactly what we expect. Return false
 +                // exception if alternate key constraint, since this is
 +                // user error.
 +                return !isUnique();
 +            }
 +        }
 +
 +        // Run the repair outside a transaction.
 +
 +        RepairExecutor.execute(new Runnable() {
 +            public void run() {
 +                try {
 +                    // Blow it away entry and re-insert. Don't simply update
 +                    // the entry, since record version number may prevent
 +                    // update.
 +
 +                    // Since we may be running outside transaction now, user
 +                    // storable may have changed. Reload to get latest data.
 +
 +                    S freshUserStorable = (S) userStorable.copy();
 +                    if (!freshUserStorable.tryLoad()) {
 +                        // Gone now, nothing we can do. Assume index entry
 +                        // was properly deleted.
 +                        return;
 +                    }
 +
 +                    Storable freshEntry = mIndexEntryStorage.prepare();
 +                    mGenerator.setAllProperties(freshEntry, freshUserStorable);
 +
 +                    // Blow it away entry and re-insert. Don't simply update
 +                    // the entry, since record version number may prevent
 +                    // update.
 +                    freshEntry.tryDelete();
 +                    freshEntry.tryInsert();
 +                } catch (FetchException fe) {
 +                    LogFactory.getLog(IndexedStorage.class).warn
 +                        ("Unable to check if repair is required: " +
 +                         userStorable.toStringKeyOnly(), fe);
 +                } catch (PersistException pe) {
 +                    LogFactory.getLog(IndexedStorage.class).error
 +                        ("Unable to repair index entry for " +
 +                         userStorable.toStringKeyOnly(), pe);
 +                }
 +            }
 +        });
 +
 +        return true;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/StoredIndexInfo.java b/src/main/java/com/amazon/carbonado/repo/indexed/StoredIndexInfo.java new file mode 100644 index 0000000..915f90b --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/StoredIndexInfo.java @@ -0,0 +1,84 @@ +/*
 + * 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.repo.indexed;
 +
 +import com.amazon.carbonado.Nullable;
 +import com.amazon.carbonado.PrimaryKey;
 +import com.amazon.carbonado.Storable;
 +import com.amazon.carbonado.Version;
 +
 +import com.amazon.carbonado.layout.Unevolvable;
 +
 +/**
 + * Stores basic information about the indexes managed by IndexedRepository.
 + *
 + * <p>Note: This storable cannot have indexes defined, since it is used to
 + * discover information about indexes. It would create a cyclic dependency.
 + *
 + * @author Brian S O'Neill
 + */
 +@PrimaryKey("indexName")
 +public interface StoredIndexInfo extends Storable, Unevolvable, Unindexed {
 +    /**
 +     * Returns the index name, which is also a valid index name
 +     * descriptor. This descriptor is defined by {@link
 +     * com.amazon.carbonado.info.StorableIndex}. The name descriptor does not
 +     * contain type information.
 +     */
 +    String getIndexName();
 +
 +    void setIndexName(String name);
 +
 +    /**
 +     * Returns the types of the index properties. This descriptor is defined by
 +     * {@link com.amazon.carbonado.info.StorableIndex}.
 +     */
 +    @Nullable
 +    String getIndexTypeDescriptor();
 +
 +    void setIndexTypeDescriptor(String descriptor);
 +
 +    /**
 +     * Returns the milliseconds from 1970-01-01T00:00:00Z when this record was
 +     * created.
 +     */
 +    long getCreationTimestamp();
 +
 +    void setCreationTimestamp(long timestamp);
 +
 +    /**
 +     * Record version number for this StoredIndexInfo instance. Some encoding
 +     * strategies require a version number.
 +     */
 +    @Version
 +    int getVersionNumber();
 +
 +    void setVersionNumber(int version);
 +
 +    /**
 +     * Since this record cannot evolve, this property allows it to be extended
 +     * without conflicting with existing records. This record cannot evolve
 +     * because an evolution strategy likely depends on this interface remaining
 +     * stable, avoiding a cyclic dependency.
 +     */
 +    @Nullable
 +    byte[] getExtraData();
 +
 +    void setExtraData(byte[] data);
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/Unindexed.java b/src/main/java/com/amazon/carbonado/repo/indexed/Unindexed.java new file mode 100644 index 0000000..e5a4cff --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/Unindexed.java @@ -0,0 +1,27 @@ +/*
 + * 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.repo.indexed;
 +
 +/**
 + * Marker interface for storables that are not allowed to have indexes.
 + *
 + * @author Brian S O'Neill
 + */
 +public interface Unindexed {
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/indexed/package-info.java b/src/main/java/com/amazon/carbonado/repo/indexed/package-info.java new file mode 100644 index 0000000..9f01bb9 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/indexed/package-info.java @@ -0,0 +1,26 @@ +/*
 + * 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.
 + */
 +
 +/**
 + * Repository implementation that adds index support for repositories that have
 + * little or no index support. The wrapped repository must support creation of
 + * new types.
 + *
 + * @see com.amazon.carbonado.repo.indexed.IndexedRepositoryBuilder
 + */
 +package com.amazon.carbonado.repo.indexed;
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCBlob.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCBlob.java new file mode 100644 index 0000000..44612bd --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCBlob.java @@ -0,0 +1,233 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.io.InputStream;
 +import java.io.OutputStream;
 +import java.io.IOException;
 +
 +import java.sql.SQLException;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.PersistException;
 +
 +import com.amazon.carbonado.lob.AbstractBlob;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCBlob extends AbstractBlob implements JDBCLob {
 +    private static final int DEFAULT_BUFFER = 4000;
 +
 +    protected final JDBCRepository mRepo;
 +    private java.sql.Blob mBlob;
 +    private final JDBCBlobLoader mLoader;
 +
 +    JDBCBlob(JDBCRepository repo, java.sql.Blob blob, JDBCBlobLoader loader) {
 +        super(repo);
 +        mRepo = repo;
 +        mBlob = blob;
 +        mLoader = loader;
 +    }
 +
 +    // FIXME: I/O streams must have embedded transaction
 +
 +    public InputStream openInputStream() throws FetchException {
 +        try {
 +            return getInternalBlobForFetch().getBinaryStream();
 +        } catch (SQLException e) {
 +            throw mRepo.toFetchException(e);
 +        }
 +    }
 +
 +    public InputStream openInputStream(long pos) throws FetchException {
 +        try {
 +            if (pos == 0) {
 +                return getInternalBlobForFetch().getBinaryStream();
 +            }
 +            return new Input(getInternalBlobForFetch(), DEFAULT_BUFFER);
 +        } catch (SQLException e) {
 +            throw mRepo.toFetchException(e);
 +        }
 +    }
 +
 +    public InputStream openInputStream(long pos, int bufferSize) throws FetchException {
 +        try {
 +            if (pos == 0) {
 +                return getInternalBlobForFetch().getBinaryStream();
 +            }
 +            if (bufferSize <= 0) {
 +                bufferSize = DEFAULT_BUFFER;
 +            }
 +            return new Input(getInternalBlobForFetch(), bufferSize);
 +        } catch (SQLException e) {
 +            throw mRepo.toFetchException(e);
 +        }
 +    }
 +
 +    public long getLength() throws FetchException {
 +        try {
 +            return getInternalBlobForFetch().length();
 +        } catch (SQLException e) {
 +            throw mRepo.toFetchException(e);
 +        }
 +    }
 +
 +    public OutputStream openOutputStream() throws PersistException {
 +        return openOutputStream(0);
 +    }
 +
 +    public OutputStream openOutputStream(long pos) throws PersistException {
 +        try {
 +            return getInternalBlobForPersist().setBinaryStream(pos);
 +        } catch (SQLException e) {
 +            throw mRepo.toPersistException(e);
 +        }
 +    }
 +
 +    public OutputStream openOutputStream(long pos, int bufferSize) throws PersistException {
 +        return openOutputStream(pos);
 +    }
 +
 +    public void setLength(long length) throws PersistException {
 +        // FIXME: Add special code to support increasing length
 +        try {
 +            getInternalBlobForPersist().truncate(length);
 +        } catch (SQLException e) {
 +            throw mRepo.toPersistException(e);
 +        }
 +    }
 +
 +    public void close() {
 +        mBlob = null;
 +    }
 +
 +    java.sql.Blob getInternalBlobForFetch() throws FetchException {
 +        if (mBlob == null) {
 +            if ((mBlob = mLoader.load(mRepo)) == null) {
 +                throw new FetchException("Blob value is null");
 +            }
 +            try {
 +                JDBCTransaction txn = mRepo.openTransactionManager().getTxn();
 +                if (txn != null) {
 +                    txn.register(this);
 +                }
 +            } catch (Exception e) {
 +                throw mRepo.toFetchException(e);
 +            }
 +        }
 +        return mBlob;
 +    }
 +
 +    java.sql.Blob getInternalBlobForPersist() throws PersistException {
 +        if (mBlob == null) {
 +            try {
 +                if ((mBlob = mLoader.load(mRepo)) == null) {
 +                    throw new PersistException("Blob value is null");
 +                }
 +                JDBCTransaction txn = mRepo.openTransactionManager().getTxn();
 +                if (txn != null) {
 +                    txn.register(this);
 +                }
 +            } catch (Exception e) {
 +                throw mRepo.toPersistException(e);
 +            }
 +        }
 +        return mBlob;
 +    }
 +
 +    private static class Input extends InputStream {
 +        private final java.sql.Blob mBlob;
 +        private final int mBufferSize;
 +
 +        private long mPos;
 +        private byte[] mBuffer;
 +        private int mBufferPos;
 +
 +        Input(java.sql.Blob blob, int bufferSize) {
 +            mBlob = blob;
 +            mBufferSize = bufferSize;
 +        }
 +
 +        public int read() throws IOException {
 +            if (fillBuffer() <= 0) {
 +                return -1;
 +            }
 +            return mBuffer[mBufferPos++];
 +        }
 +
 +        public int read(byte[] b, int off, int len) throws IOException {
 +            int avail = fillBuffer();
 +            if (avail <= 0) {
 +                return -1;
 +            }
 +            if (len > avail) {
 +                len = avail;
 +            }
 +            System.arraycopy(mBuffer, mBufferPos, b, off, len);
 +            mBufferPos += len;
 +            return len;
 +        }
 +
 +        public long skip(long n) throws IOException {
 +            if (n <= 0) {
 +                return 0;
 +            }
 +            long newPos = mPos + n;
 +            long length;
 +            try {
 +                length = mBlob.length();
 +            } catch (SQLException e) {
 +                IOException ioe = new IOException();
 +                ioe.initCause(e);
 +                throw ioe;
 +            }
 +            if (newPos >= length) {
 +                newPos = length;
 +                n = newPos - mPos;
 +            }
 +            long newBufferPos = mBufferPos + n;
 +            if (mBuffer == null || newBufferPos >= mBuffer.length) {
 +                mBuffer = null;
 +                mBufferPos = 0;
 +            } else {
 +                mBufferPos = (int) newBufferPos;
 +            }
 +            mPos = newPos;
 +            return n;
 +        }
 +
 +        private int fillBuffer() throws IOException {
 +            try {
 +                if (mBuffer == null || mBufferPos >= mBuffer.length) {
 +                    mBuffer = mBlob.getBytes(mPos, mBufferSize);
 +                    mPos += mBuffer.length;
 +                    mBufferPos = 0;
 +                }
 +                return mBuffer.length - mBufferPos;
 +            } catch (SQLException e) {
 +                IOException ioe = new IOException();
 +                ioe.initCause(e);
 +                throw ioe;
 +            }
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCBlobLoader.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCBlobLoader.java new file mode 100644 index 0000000..cd818c0 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCBlobLoader.java @@ -0,0 +1,33 @@ +/*
 + * 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.repo.jdbc;
 +
 +import com.amazon.carbonado.FetchException;
 +
 +/**
 + * Callback for reloading Blobs outside original transaction.
 + *
 + * @author Brian S O'Neill
 + */
 +public interface JDBCBlobLoader {
 +    /**
 +     * @return Blob or null if missing
 +     */
 +    java.sql.Blob load(JDBCRepository jdbcRepo) throws FetchException;
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCClob.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCClob.java new file mode 100644 index 0000000..95c8966 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCClob.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.repo.jdbc;
 +
 +import java.io.Reader;
 +import java.io.Writer;
 +import java.io.IOException;
 +
 +import java.sql.SQLException;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.PersistException;
 +
 +import com.amazon.carbonado.lob.AbstractClob;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCClob extends AbstractClob implements JDBCLob {
 +    private static final int DEFAULT_BUFFER = 4000;
 +
 +    protected final JDBCRepository mRepo;
 +    private java.sql.Clob mClob;
 +    private final JDBCClobLoader mLoader;
 +
 +    JDBCClob(JDBCRepository repo, java.sql.Clob clob, JDBCClobLoader loader) {
 +        super(repo);
 +        mRepo = repo;
 +        mClob = clob;
 +        mLoader = loader;
 +    }
 +
 +    // FIXME: I/O streams must have embedded transaction
 +
 +    public Reader openReader() throws FetchException {
 +        try {
 +            return getInternalClobForFetch().getCharacterStream();
 +        } catch (SQLException e) {
 +            throw mRepo.toFetchException(e);
 +        }
 +    }
 +
 +    public Reader openReader(long pos) throws FetchException {
 +        try {
 +            if (pos == 0) {
 +                return getInternalClobForFetch().getCharacterStream();
 +            }
 +            return new Input(getInternalClobForFetch(), DEFAULT_BUFFER);
 +        } catch (SQLException e) {
 +            throw mRepo.toFetchException(e);
 +        }
 +    }
 +
 +    public Reader openReader(long pos, int bufferSize) throws FetchException {
 +        try {
 +            if (pos == 0) {
 +                return getInternalClobForFetch().getCharacterStream();
 +            }
 +            if (bufferSize <= 0) {
 +                bufferSize = DEFAULT_BUFFER;
 +            }
 +            return new Input(getInternalClobForFetch(), bufferSize);
 +        } catch (SQLException e) {
 +            throw mRepo.toFetchException(e);
 +        }
 +    }
 +
 +    public long getLength() throws FetchException {
 +        try {
 +            return getInternalClobForFetch().length();
 +        } catch (SQLException e) {
 +            throw mRepo.toFetchException(e);
 +        }
 +    }
 +
 +    public Writer openWriter() throws PersistException {
 +        return openWriter(0);
 +    }
 +
 +    public Writer openWriter(long pos) throws PersistException {
 +        try {
 +            return getInternalClobForPersist().setCharacterStream(pos);
 +        } catch (SQLException e) {
 +            throw mRepo.toPersistException(e);
 +        }
 +    }
 +
 +    public Writer openWriter(long pos, int bufferSize) throws PersistException {
 +        return openWriter(pos);
 +    }
 +
 +    public void setLength(long length) throws PersistException {
 +        // FIXME: Add special code to support increasing length
 +        try {
 +            getInternalClobForPersist().truncate(length);
 +        } catch (SQLException e) {
 +            throw mRepo.toPersistException(e);
 +        }
 +    }
 +
 +    public void close() {
 +        mClob = null;
 +    }
 +
 +    java.sql.Clob getInternalClobForFetch() throws FetchException {
 +        if (mClob == null) {
 +            if ((mClob = mLoader.load(mRepo)) == null) {
 +                throw new FetchException("Clob value is null");
 +            }
 +            try {
 +                JDBCTransaction txn = mRepo.openTransactionManager().getTxn();
 +                if (txn != null) {
 +                    txn.register(this);
 +                }
 +            } catch (Exception e) {
 +                throw mRepo.toFetchException(e);
 +            }
 +        }
 +        return mClob;
 +    }
 +
 +    java.sql.Clob getInternalClobForPersist() throws PersistException {
 +        if (mClob == null) {
 +            try {
 +                if ((mClob = mLoader.load(mRepo)) == null) {
 +                    throw new PersistException("Clob value is null");
 +                }
 +                JDBCTransaction txn = mRepo.openTransactionManager().getTxn();
 +                if (txn != null) {
 +                    txn.register(this);
 +                }
 +            } catch (Exception e) {
 +                throw mRepo.toPersistException(e);
 +            }
 +        }
 +        return mClob;
 +    }
 +
 +    private static class Input extends Reader {
 +        private final java.sql.Clob mClob;
 +        private final int mBufferSize;
 +
 +        private long mPos;
 +        private String mBuffer;
 +        private int mBufferPos;
 +
 +        Input(java.sql.Clob clob, int bufferSize) {
 +            mClob = clob;
 +            mBufferSize = bufferSize;
 +        }
 +
 +        public int read() throws IOException {
 +            if (fillBuffer() <= 0) {
 +                return -1;
 +            }
 +            return mBuffer.charAt(mBufferPos++);
 +        }
 +
 +        public int read(char[] c, int off, int len) throws IOException {
 +            int avail = fillBuffer();
 +            if (avail <= 0) {
 +                return -1;
 +            }
 +            if (len > avail) {
 +                len = avail;
 +            }
 +            mBuffer.getChars(mBufferPos, mBufferPos + len, c, off);
 +            mBufferPos += len;
 +            return len;
 +        }
 +
 +        public long skip(long n) throws IOException {
 +            if (n <= 0) {
 +                return 0;
 +            }
 +            long newPos = mPos + n;
 +            long length;
 +            try {
 +                length = mClob.length();
 +            } catch (SQLException e) {
 +                IOException ioe = new IOException();
 +                ioe.initCause(e);
 +                throw ioe;
 +            }
 +            if (newPos >= length) {
 +                newPos = length;
 +                n = newPos - mPos;
 +            }
 +            long newBufferPos = mBufferPos + n;
 +            if (mBuffer == null || newBufferPos >= mBuffer.length()) {
 +                mBuffer = null;
 +                mBufferPos = 0;
 +            } else {
 +                mBufferPos = (int) newBufferPos;
 +            }
 +            mPos = newPos;
 +            return n;
 +        }
 +
 +        public void close() {
 +        }
 +
 +        private int fillBuffer() throws IOException {
 +            try {
 +                if (mBuffer == null || mBufferPos >= mBuffer.length()) {
 +                    mBuffer = mClob.getSubString(mPos, mBufferSize);
 +                    mPos += mBuffer.length();
 +                    mBufferPos = 0;
 +                }
 +                return mBuffer.length() - mBufferPos;
 +            } catch (SQLException e) {
 +                IOException ioe = new IOException();
 +                ioe.initCause(e);
 +                throw ioe;
 +            }
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCClobLoader.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCClobLoader.java new file mode 100644 index 0000000..9e3470a --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCClobLoader.java @@ -0,0 +1,33 @@ +/*
 + * 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.repo.jdbc;
 +
 +import com.amazon.carbonado.FetchException;
 +
 +/**
 + * Callback for reloading Clobs outside original transaction.
 + *
 + * @author Brian S O'Neill
 + */
 +public interface JDBCClobLoader {
 +    /**
 +     * @return Clob or null if missing
 +     */
 +    java.sql.Clob load(JDBCRepository jdbcRepo) throws FetchException;
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCConnectionCapability.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCConnectionCapability.java new file mode 100644 index 0000000..dbde10e --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCConnectionCapability.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.repo.jdbc;
 +
 +import java.sql.Connection;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.capability.Capability;
 +
 +/**
 + * Capability to directly access the JDBC connection being used by the current
 + * transaction, which is thread-local. If no transaction is in progress, then
 + * the connection is in auto-commit mode.
 + *
 + * <p>All connections retrieved from this capability must be properly
 + * yielded. Do not close the connection directly, as this interferes with the
 + * transaction's ability to properly manage it.
 + *
 + * <p>It is perfectly okay for other Carbonado calls to be made while the
 + * connection is in use.  Also, it is okay to request more connections,
 + * although they will usually be the same instance. Failing to yield a
 + * connection has an undefined behavior.
 + *
 + * <pre>
 + * JDBCConnectionCapability cap = repo.getCapability(JDBCConnectionCapability.class);
 + * Transaction txn = repo.enterTransaction();
 + * try {
 + *     Connection con = cap.getConnection();
 + *     try {
 + *         ...
 + *     } finally {
 + *         cap.yieldConnection(con);
 + *     }
 + *     ...
 + *     txn.commit();
 + * } finally {
 + *     txn.exit();
 + * }
 + * </pre>
 + *
 + * @author Brian S O'Neill
 + */
 +public interface JDBCConnectionCapability extends Capability {
 +    /**
 +     * Any connection returned by this method must be closed by calling
 +     * yieldConnection.
 +     */
 +    Connection getConnection() throws FetchException;
 +
 +    /**
 +     * Gives up a connection returned from getConnection. Connection must be
 +     * yielded in same thread that retrieved it.
 +     */
 +    void yieldConnection(Connection con) throws FetchException;
 +
 +    /**
 +     * Returns the name of the database product connected to.
 +     */
 +    String getDatabaseProductName();
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCCursor.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCCursor.java new file mode 100644 index 0000000..7ef2510 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCCursor.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.repo.jdbc;
 +
 +import java.sql.Connection;
 +import java.sql.PreparedStatement;
 +import java.sql.ResultSet;
 +import java.sql.SQLException;
 +import java.util.NoSuchElementException;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.Storable;
 +import com.amazon.carbonado.cursor.AbstractCursor;
 +
 +/**
 + * Cursor implementation that queries a PreparedStatement.
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCCursor<S extends Storable> extends AbstractCursor<S> {
 +    private final JDBCStorage<S> mStorage;
 +    private Connection mConnection;
 +    private PreparedStatement mStatement;
 +    private ResultSet mResultSet;
 +
 +    private boolean mHasNext;
 +
 +    JDBCCursor(JDBCStorage<S> storage,
 +               Connection con,
 +               PreparedStatement statement)
 +        throws SQLException
 +    {
 +        mStorage = storage;
 +        mConnection = con;
 +        mStatement = statement;
 +        mResultSet = statement.executeQuery();
 +    }
 +
 +    public synchronized void close() throws FetchException {
 +        if (mResultSet != null) {
 +            try {
 +                mResultSet.close();
 +                mStatement.close();
 +                mStorage.mRepository.yieldConnection(mConnection);
 +            } catch (SQLException e) {
 +                throw mStorage.getJDBCRepository().toFetchException(e);
 +            } finally {
 +                mResultSet = null;
 +            }
 +        }
 +    }
 +
 +    public synchronized boolean hasNext() throws FetchException {
 +        ResultSet rs = mResultSet;
 +        if (rs == null) {
 +            return false;
 +        }
 +        if (!mHasNext) {
 +            try {
 +                mHasNext = rs.next();
 +            } catch (SQLException e) {
 +                throw mStorage.getJDBCRepository().toFetchException(e);
 +            }
 +            if (!mHasNext) {
 +                close();
 +            }
 +        }
 +        return mHasNext;
 +    }
 +
 +    public synchronized S next() throws FetchException, NoSuchElementException {
 +        if (!hasNext()) {
 +            throw new NoSuchElementException();
 +        }
 +        try {
 +            S obj = mStorage.instantiate(mResultSet);
 +            mHasNext = false;
 +            return obj;
 +        } catch (SQLException e) {
 +            throw mStorage.getJDBCRepository().toFetchException(e);
 +        }
 +    }
 +
 +    public synchronized int skipNext(int amount) throws FetchException {
 +        if (amount <= 0) {
 +            if (amount < 0) {
 +                throw new IllegalArgumentException("Cannot skip negative amount: " + amount);
 +            }
 +            return 0;
 +        }
 +
 +        ResultSet rs = mResultSet;
 +        if (rs == null) {
 +            return 0;
 +        }
 +
 +        mHasNext = true;
 +
 +        int actual = 0;
 +        while (amount > 0) {
 +            try {
 +                if (rs.next()) {
 +                    actual++;
 +                } else {
 +                    mHasNext = false;
 +                    close();
 +                    break;
 +                }
 +            } catch (SQLException e) {
 +                throw mStorage.getJDBCRepository().toFetchException(e);
 +            }
 +        }
 +
 +        return actual;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCExceptionTransformer.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCExceptionTransformer.java new file mode 100644 index 0000000..c40f08a --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCExceptionTransformer.java @@ -0,0 +1,110 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.sql.SQLException;
 +
 +import com.amazon.carbonado.ConstraintException;
 +import com.amazon.carbonado.PersistDeniedException;
 +import com.amazon.carbonado.PersistException;
 +import com.amazon.carbonado.UniqueConstraintException;
 +import com.amazon.carbonado.spi.ExceptionTransformer;
 +
 +/**
 + * Custom exception transform rules.
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCExceptionTransformer extends ExceptionTransformer {
 +    // Getting actual SQLSTATE codes is quite difficult, unless you shell out
 +    // cash for the proper manuals. SQLSTATE codes are five characters long,
 +    // where the first two indicate error class. Although the following links
 +    // are for DB2 SQLSTATE codes, the codes are fairly standard across all
 +    // major database implementations.
 +    //
 +    // ftp://ftp.software.ibm.com/ps/products/db2/info/vr6/htm/db2m0/db2state.htm
 +    // http://publib.boulder.ibm.com/infocenter/db2help/topic/com.ibm.db2.udb.doc/core/r0sttmsg.htm
 +
 +    /** Two digit SQLSTATE class prefix for all constraint violations */
 +    public static String SQLSTATE_CONSTRAINT_VIOLATION_CLASS_CODE = "23";
 +
 +    /**
 +     * Five digit SQLSTATE code for "A violation of the constraint imposed by a
 +     * unique index or a unique constraint occurred"
 +     */
 +    public static String SQLSTATE_UNIQUE_CONSTRAINT_VIOLATION = "23505";
 +
 +    /**
 +     * Examines the SQLSTATE code of the given SQL exception and determines if
 +     * it is a generic constaint violation.
 +     */
 +    public boolean isConstraintError(SQLException e) {
 +        if (e != null) {
 +            String sqlstate = e.getSQLState();
 +            if (sqlstate != null) {
 +                return sqlstate.startsWith(SQLSTATE_CONSTRAINT_VIOLATION_CLASS_CODE);
 +            }
 +        }
 +        return false;
 +    }
 +
 +    /**
 +     * Examines the SQLSTATE code of the given SQL exception and determines if
 +     * it is a unique constaint violation.
 +     */
 +    public boolean isUniqueConstraintError(SQLException e) {
 +        if (isConstraintError(e)) {
 +            String sqlstate = e.getSQLState();
 +            return SQLSTATE_UNIQUE_CONSTRAINT_VIOLATION.equals(sqlstate);
 +        }
 +        return false;
 +    }
 +
 +    /**
 +     * Examines the SQLSTATE code of the given SQL exception and determines if
 +     * it indicates insufficient privileges.
 +     */
 +    public boolean isInsufficientPrivilegesError(SQLException e) {
 +        return false;
 +    }
 +
 +    JDBCExceptionTransformer() {
 +    }
 +
 +    @Override
 +    protected PersistException transformIntoPersistException(Throwable e) {
 +        PersistException pe = super.transformIntoPersistException(e);
 +        if (pe != null) {
 +            return pe;
 +        }
 +        if (e instanceof SQLException) {
 +            SQLException se = (SQLException) e;
 +            if (isUniqueConstraintError(se)) {
 +                return new UniqueConstraintException(e);
 +            }
 +            if (isConstraintError(se)) {
 +                return new ConstraintException(e);
 +            }
 +            if (isInsufficientPrivilegesError(se)) {
 +                return new PersistDeniedException(e);
 +            }
 +        }
 +        return null;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCLob.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCLob.java new file mode 100644 index 0000000..a4f09b2 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCLob.java @@ -0,0 +1,28 @@ +/*
 + * 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.repo.jdbc;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +interface JDBCLob extends com.amazon.carbonado.lob.Lob {
 +    void close();
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCRepository.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCRepository.java new file mode 100644 index 0000000..6e53354 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCRepository.java @@ -0,0 +1,665 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.sql.Connection;
 +import java.sql.DatabaseMetaData;
 +import java.sql.SQLException;
 +import java.util.Map;
 +import java.util.IdentityHashMap;
 +
 +import javax.sql.DataSource;
 +
 +import org.apache.commons.logging.Log;
 +import org.apache.commons.logging.LogFactory;
 +
 +import org.cojen.util.WeakIdentityMap;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.IsolationLevel;
 +import com.amazon.carbonado.Storage;
 +import com.amazon.carbonado.Storable;
 +import com.amazon.carbonado.SupportException;
 +import com.amazon.carbonado.MalformedTypeException;
 +import com.amazon.carbonado.PersistException;
 +import com.amazon.carbonado.Repository;
 +import static com.amazon.carbonado.RepositoryBuilder.RepositoryReference;
 +import com.amazon.carbonado.RepositoryException;
 +import com.amazon.carbonado.Transaction;
 +import com.amazon.carbonado.UnsupportedTypeException;
 +
 +import com.amazon.carbonado.capability.Capability;
 +import com.amazon.carbonado.capability.IndexInfo;
 +import com.amazon.carbonado.capability.IndexInfoCapability;
 +import com.amazon.carbonado.capability.ShutdownCapability;
 +import com.amazon.carbonado.capability.StorableInfoCapability;
 +
 +import com.amazon.carbonado.info.StorableProperty;
 +
 +/**
 + * Repository implementation backed by a JDBC accessible database.
 + * JDBCRepository is not independent of the underlying database schema, and so
 + * it requires matching tables and columns in the database. It will not alter
 + * or create tables. Use the {@link com.amazon.carbonado.Alias Alias} annotation to
 + * control precisely which tables and columns must be matched up.
 + *
 + * @author Brian S O'Neill
 + * @see JDBCRepositoryBuilder
 + */
 +// Note: this class must be public because auto-generated code needs access to it
 +public class JDBCRepository
 +    implements Repository,
 +               IndexInfoCapability,
 +               ShutdownCapability,
 +               StorableInfoCapability,
 +               JDBCConnectionCapability
 +{
 +
 +    static IsolationLevel mapIsolationLevelFromJdbc(int jdbcLevel) {
 +        switch (jdbcLevel) {
 +        case Connection.TRANSACTION_READ_UNCOMMITTED: default:
 +            return IsolationLevel.READ_UNCOMMITTED;
 +        case Connection.TRANSACTION_READ_COMMITTED:
 +            return IsolationLevel.READ_COMMITTED;
 +        case Connection.TRANSACTION_REPEATABLE_READ:
 +            return IsolationLevel.REPEATABLE_READ;
 +        case Connection.TRANSACTION_SERIALIZABLE:
 +            return IsolationLevel.SERIALIZABLE;
 +        }
 +    }
 +
 +    static int mapIsolationLevelToJdbc(IsolationLevel level) {
 +        switch (level) {
 +        case READ_UNCOMMITTED: default:
 +            return Connection.TRANSACTION_READ_UNCOMMITTED;
 +        case READ_COMMITTED:
 +            return Connection.TRANSACTION_READ_COMMITTED;
 +        case REPEATABLE_READ:
 +            return Connection.TRANSACTION_REPEATABLE_READ;
 +        case SERIALIZABLE:
 +            return Connection.TRANSACTION_SERIALIZABLE;
 +        }
 +    }
 +
 +    /**
 +     * Returns the highest supported level for the given desired level.
 +     *
 +     * @return null if not supported
 +     */
 +    private static IsolationLevel selectIsolationLevel(DatabaseMetaData md,
 +                                                       IsolationLevel desiredLevel)
 +        throws SQLException, RepositoryException
 +    {
 +        while (!md.supportsTransactionIsolationLevel(mapIsolationLevelToJdbc(desiredLevel))) {
 +            switch (desiredLevel) {
 +            case READ_UNCOMMITTED:
 +                desiredLevel = IsolationLevel.READ_COMMITTED;
 +                break;
 +            case READ_COMMITTED:
 +                desiredLevel = IsolationLevel.REPEATABLE_READ;
 +                break;
 +            case REPEATABLE_READ:
 +                desiredLevel = IsolationLevel.SERIALIZABLE;
 +                break;
 +            case SERIALIZABLE: default:
 +                return null;
 +            }
 +        }
 +        return desiredLevel;
 +    }
 +
 +    private final Log mLog = LogFactory.getLog(getClass());
 +
 +    private final String mName;
 +    final boolean mIsMaster;
 +    private final RepositoryReference mRootRef;
 +    private final String mDatabaseProductName;
 +    private final DataSource mDataSource;
 +    private final String mCatalog;
 +    private final String mSchema;
 +    private final Map<Class<?>, JDBCStorage<?>> mStorages;
 +
 +    // Track all open connections so that they can be closed when this
 +    // repository is closed.
 +    private Map<Connection, Object> mOpenConnections;
 +
 +    private final ThreadLocal<JDBCTransactionManager> mCurrentTxnMgr;
 +
 +    // Weakly tracks all JDBCTransactionManager instances for shutdown.
 +    private final Map<JDBCTransactionManager, ?> mAllTxnMgrs;
 +
 +    private final boolean mSupportsSavepoints;
 +    private final boolean mSupportsSelectForUpdate;
 +
 +    private final IsolationLevel mDefaultIsolationLevel;
 +    private final int mJdbcDefaultIsolationLevel;
 +
 +    private final JDBCSupportStrategy mSupportStrategy;
 +    private JDBCExceptionTransformer mExceptionTransformer;
 +
 +    // Mappings from IsolationLevel to best matching supported level.
 +    final IsolationLevel mReadUncommittedLevel;
 +    final IsolationLevel mReadCommittedLevel;
 +    final IsolationLevel mRepeatableReadLevel;
 +    final IsolationLevel mSerializableLevel;
 +
 +    /**
 +     * @param name name to give repository instance
 +     * @param isMaster when true, storables in this repository must manage
 +     * version properties and sequence properties
 +     * @param dataSource provides JDBC database connections
 +     * @param catalog optional catalog to search for tables -- actual meaning
 +     * is database independent
 +     * @param schema optional schema to search for tables -- actual meaning is
 +     * database independent
 +     */
 +    @SuppressWarnings("unchecked")
 +    JDBCRepository(RepositoryReference rootRef,
 +                   String name, boolean isMaster,
 +                   DataSource dataSource, String catalog, String schema)
 +        throws RepositoryException
 +    {
 +        if (name == null || dataSource == null) {
 +            throw new IllegalArgumentException();
 +        }
 +        mName = name;
 +        mIsMaster = isMaster;
 +        mRootRef = rootRef;
 +        mDataSource = dataSource;
 +        mCatalog = catalog;
 +        mSchema = schema;
 +        mStorages = new IdentityHashMap<Class<?>, JDBCStorage<?>>();
 +        mOpenConnections = new IdentityHashMap<Connection, Object>();
 +        mCurrentTxnMgr = new ThreadLocal<JDBCTransactionManager>();
 +        mAllTxnMgrs = new WeakIdentityMap();
 +
 +        // Temporarily set to generic one, in case there's a problem during initialization.
 +        mExceptionTransformer = new JDBCExceptionTransformer();
 +
 +        // Test connectivity and get some info on transaction isolation levels.
 +        Connection con = getConnection();
 +        try {
 +            DatabaseMetaData md = con.getMetaData();
 +            if (md == null || !md.supportsTransactions()) {
 +                throw new RepositoryException("Database does not support transactions");
 +            }
 +
 +            mDatabaseProductName = md.getDatabaseProductName();
 +
 +            boolean supportsSavepoints;
 +            try {
 +                supportsSavepoints = md.supportsSavepoints();
 +            } catch (AbstractMethodError e) {
 +                supportsSavepoints = false;
 +            }
 +
 +            if (supportsSavepoints) {
 +                con.setAutoCommit(false);
 +                // Some JDBC drivers (HSQLDB) lie about their savepoint support.
 +                try {
 +                    con.setSavepoint();
 +                } catch (SQLException e) {
 +                    mLog.warn("JDBC driver for " + mDatabaseProductName +
 +                              " reports supporting savepoints, but it " +
 +                              "doesn't appear to work: " + e);
 +                    supportsSavepoints = false;
 +                } finally {
 +                    con.rollback();
 +                    con.setAutoCommit(true);
 +                }
 +            }
 +
 +            mSupportsSavepoints = supportsSavepoints;
 +            mSupportsSelectForUpdate = md.supportsSelectForUpdate();
 +
 +            mJdbcDefaultIsolationLevel = md.getDefaultTransactionIsolation();
 +            mDefaultIsolationLevel = mapIsolationLevelFromJdbc(mJdbcDefaultIsolationLevel);
 +
 +            mReadUncommittedLevel = selectIsolationLevel(md, IsolationLevel.READ_UNCOMMITTED);
 +            mReadCommittedLevel   = selectIsolationLevel(md, IsolationLevel.READ_COMMITTED);
 +            mRepeatableReadLevel  = selectIsolationLevel(md, IsolationLevel.REPEATABLE_READ);
 +            mSerializableLevel    = selectIsolationLevel(md, IsolationLevel.SERIALIZABLE);
 +
 +        } catch (SQLException e) {
 +            throw toRepositoryException(e);
 +        } finally {
 +            forceYieldConnection(con);
 +        }
 +
 +        mSupportStrategy = JDBCSupportStrategy.createStrategy(this);
 +        mExceptionTransformer = mSupportStrategy.createExceptionTransformer();
 +    }
 +
 +    public DataSource getDataSource() {
 +        return mDataSource;
 +    }
 +
 +    public String getName() {
 +        return mName;
 +    }
 +
 +    @SuppressWarnings("unchecked")
 +    public <S extends Storable> Storage<S> storageFor(Class<S> type) throws RepositoryException {
 +        // Lock on mAllTxnMgrs to prevent databases from being opened during shutdown.
 +        synchronized (mAllTxnMgrs) {
 +            JDBCStorage<S> storage = (JDBCStorage<S>) mStorages.get(type);
 +            if (storage == null) {
 +                // Examine and throw exception early if there is a problem.
 +                JDBCStorableInfo<S> info = examineStorable(type);
 +
 +                if (!info.isSupported()) {
 +                    throw new UnsupportedTypeException(type);
 +                }
 +
 +                storage = new JDBCStorage<S>(this, info);
 +                mStorages.put(type, storage);
 +            }
 +            return storage;
 +        }
 +    }
 +
 +    public Transaction enterTransaction() {
 +        return openTransactionManager().enter(null);
 +    }
 +
 +    public Transaction enterTransaction(IsolationLevel level) {
 +        return openTransactionManager().enter(level);
 +    }
 +
 +    public Transaction enterTopTransaction(IsolationLevel level) {
 +        return openTransactionManager().enterTop(level);
 +    }
 +
 +    public IsolationLevel getTransactionIsolationLevel() {
 +        return openTransactionManager().getIsolationLevel();
 +    }
 +
 +    /**
 +     * Returns true if a transaction is in progress and it is for update.
 +     */
 +    public boolean isTransactionForUpdate() {
 +        return openTransactionManager().isForUpdate();
 +    }
 +
 +    /**
 +     * Convenience method that calls into {@link JDBCStorableIntrospector}.
 +     *
 +     * @param type Storable type to examine
 +     * @throws MalformedTypeException if Storable type is not well-formed
 +     * @throws RepositoryException if there was a problem in accessing the database
 +     * @throws IllegalArgumentException if type is null
 +     */
 +    public <S extends Storable> JDBCStorableInfo<S> examineStorable(Class<S> type)
 +        throws RepositoryException, SupportException
 +    {
 +        try {
 +            return JDBCStorableIntrospector.examine(type, mDataSource, mCatalog, mSchema);
 +        } catch (SQLException e) {
 +            throw toRepositoryException(e);
 +        }
 +    }
 +
 +    @SuppressWarnings("unchecked")
 +    public <C extends Capability> C getCapability(Class<C> capabilityType) {
 +        if (capabilityType.isInstance(this)) {
 +            return (C) this;
 +        }
 +        return null;
 +    }
 +
 +    public <S extends Storable> IndexInfo[] getIndexInfo(Class<S> storableType)
 +        throws RepositoryException
 +    {
 +        return ((JDBCStorage) storageFor(storableType)).getIndexInfo();
 +    }
 +
 +    public String[] getUserStorableTypeNames() {
 +        // We don't register Storable types persistently, so just return what
 +        // we know right now.
 +        synchronized (mAllTxnMgrs) {
 +            String[] names = new String[mStorages.size()];
 +            int i = 0;
 +            for (Class<?> type : mStorages.keySet()) {
 +                names[i++] = type.getName();
 +            }
 +            return names;
 +        }
 +    }
 +
 +    public boolean isSupported(Class<Storable> type) {
 +        if (type == null) {
 +            return false;
 +        }
 +        try {
 +            examineStorable(type);
 +            return true;
 +        } catch (RepositoryException e) {
 +            return false;
 +        }
 +    }
 +
 +    public boolean isPropertySupported(Class<Storable> type, String name) {
 +        if (type == null || name == null) {
 +            return false;
 +        }
 +        try {
 +            JDBCStorableProperty<?> prop = examineStorable(type).getAllProperties().get(name);
 +            return prop == null ? false : prop.isSupported();
 +        } catch (RepositoryException e) {
 +            return false;
 +        }
 +    }
 +
 +    /**
 +     * Convenience method to convert a regular StorableProperty into a
 +     * JDBCStorableProperty.
 +     *
 +     * @throws UnsupportedOperationException if JDBCStorableProperty is not supported
 +     */
 +    <S extends Storable> JDBCStorableProperty<S>
 +        getJDBCStorableProperty(StorableProperty<S> property)
 +        throws RepositoryException, SupportException
 +    {
 +        JDBCStorableInfo<S> info = examineStorable(property.getEnclosingType());
 +        JDBCStorableProperty<S> jProperty = info.getAllProperties().get(property.getName());
 +        if (!jProperty.isSupported()) {
 +            throw new UnsupportedOperationException
 +                ("Property is not supported: " + property.getName());
 +        }
 +        return jProperty;
 +    }
 +
 +    /**
 +     * Returns the thread-local JDBCTransactionManager instance, creating it if
 +     * needed.
 +     */
 +    JDBCTransactionManager openTransactionManager() {
 +        JDBCTransactionManager txnMgr = mCurrentTxnMgr.get();
 +        if (txnMgr == null) {
 +            synchronized (mAllTxnMgrs) {
 +                txnMgr = new JDBCTransactionManager(this);
 +                mCurrentTxnMgr.set(txnMgr);
 +                mAllTxnMgrs.put(txnMgr, null);
 +            }
 +        }
 +        return txnMgr;
 +    }
 +
 +    public void close() {
 +        shutdown(false);
 +    }
 +
 +    public boolean isAutoShutdownEnabled() {
 +        return false;
 +    }
 +
 +    public void setAutoShutdownEnabled(boolean enabled) {
 +    }
 +
 +    public void shutdown() {
 +        shutdown(true);
 +    }
 +
 +    private void shutdown(boolean suspendThreads) {
 +        synchronized (mAllTxnMgrs) {
 +            // Close transactions and cursors.
 +            for (JDBCTransactionManager txnMgr : mAllTxnMgrs.keySet()) {
 +                if (suspendThreads) {
 +                    // Lock transaction manager but don't release it. This
 +                    // prevents other threads from beginning work during
 +                    // shutdown, which will likely fail along the way.
 +                    txnMgr.getLock().lock();
 +                }
 +                try {
 +                    txnMgr.close();
 +                } catch (Throwable e) {
 +                    getLog().error(null, e);
 +                }
 +            }
 +
 +            // Now close all open connections.
 +            if (mOpenConnections != null) {
 +                for (Connection con : mOpenConnections.keySet()) {
 +                    try {
 +                        con.close();
 +                    } catch (SQLException e) {
 +                        getLog().warn(null, e);
 +                    }
 +                }
 +                mOpenConnections = null;
 +            }
 +        }
 +    }
 +
 +    protected Log getLog() {
 +        return mLog;
 +    }
 +
 +    public String getDatabaseProductName() {
 +        return mDatabaseProductName;
 +    }
 +
 +    /**
 +     * Any connection returned by this method must be closed by calling
 +     * yieldConnection on this repository.
 +     */
 +    // Note: This method must be public for auto-generated code to access it.
 +    public Connection getConnection() throws FetchException {
 +        try {
 +            if (mOpenConnections == null) {
 +                throw new FetchException("Repository is closed");
 +            }
 +
 +            JDBCTransaction txn = openTransactionManager().getTxn();
 +            if (txn != null) {
 +                // Return the connection used by the current transaction.
 +                return txn.getConnection();
 +            }
 +
 +            // Get connection outside synchronized section since it may block.
 +            Connection con = mDataSource.getConnection();
 +            con.setAutoCommit(true);
 +
 +            synchronized (mAllTxnMgrs) {
 +                if (mOpenConnections == null) {
 +                    con.close();
 +                    throw new FetchException("Repository is closed");
 +                }
 +                mOpenConnections.put(con, null);
 +            }
 +
 +            return con;
 +        } catch (Exception e) {
 +            throw toFetchException(e);
 +        }
 +    }
 +
 +    /**
 +     * Called by JDBCTransactionManager.
 +     */
 +    Connection getConnectionForTxn(IsolationLevel level) throws FetchException {
 +        try {
 +            if (mOpenConnections == null) {
 +                throw new FetchException("Repository is closed");
 +            }
 +
 +            // Get connection outside synchronized section since it may block.
 +            Connection con = mDataSource.getConnection();
 +            con.setAutoCommit(false);
 +            if (level != mDefaultIsolationLevel) {
 +                con.setTransactionIsolation(mapIsolationLevelToJdbc(level));
 +            }
 +
 +            synchronized (mAllTxnMgrs) {
 +                if (mOpenConnections == null) {
 +                    con.close();
 +                    throw new FetchException("Repository is closed");
 +                }
 +                mOpenConnections.put(con, null);
 +            }
 +
 +            return con;
 +        } catch (Exception e) {
 +            throw toFetchException(e);
 +        }
 +    }
 +
 +    /**
 +     * Gives up a connection returned from getConnection. Connection must be
 +     * yielded in same thread that retrieved it.
 +     */
 +    // Note: This method must be public for auto-generated code to access it.
 +    public void yieldConnection(Connection con) throws FetchException {
 +        try {
 +            if (con.getAutoCommit()) {
 +                synchronized (mAllTxnMgrs) {
 +                    if (mOpenConnections != null) {
 +                        mOpenConnections.remove(con);
 +                    }
 +                }
 +                // Close connection outside synchronized section since it may block.
 +                if (con.getTransactionIsolation() != mJdbcDefaultIsolationLevel) {
 +                    con.setTransactionIsolation(mJdbcDefaultIsolationLevel);
 +                }
 +                con.close();
 +            }
 +
 +            // Connections which aren't auto-commit are in a transaction.
 +            // When transaction is finished, JDBCTransactionManager switches
 +            // connection back to auto-commit and calls yieldConnection.
 +        } catch (Exception e) {
 +            throw toFetchException(e);
 +        }
 +    }
 +
 +    /**
 +     * Yields connection without attempting to restore isolation level. Ignores
 +     * any exceptions too.
 +     */
 +    private void forceYieldConnection(Connection con) {
 +        synchronized (mAllTxnMgrs) {
 +            if (mOpenConnections != null) {
 +                mOpenConnections.remove(con);
 +            }
 +        }
 +        // Close connection outside synchronized section since it may block.
 +        try {
 +            con.close();
 +        } catch (SQLException e) {
 +            // Don't care.
 +        }
 +    }
 +
 +    boolean supportsSavepoints() {
 +        return mSupportsSavepoints;
 +    }
 +
 +    boolean supportsSelectForUpdate() {
 +        return mSupportsSelectForUpdate;
 +    }
 +
 +    /**
 +     * Returns the highest supported level for the given desired level.
 +     *
 +     * @return null if not supported
 +     */
 +    IsolationLevel selectIsolationLevel(Transaction parent, IsolationLevel desiredLevel) {
 +        if (desiredLevel == null) {
 +            if (parent == null) {
 +                desiredLevel = mDefaultIsolationLevel;
 +            } else {
 +                desiredLevel = parent.getIsolationLevel();
 +            }
 +        } else if (parent != null) {
 +            IsolationLevel parentLevel = parent.getIsolationLevel();
 +            // Can promote to higher level, but not lower.
 +            if (parentLevel.compareTo(desiredLevel) >= 0) {
 +                desiredLevel = parentLevel;
 +            } else {
 +                return null;
 +            }
 +        }
 +
 +        switch (desiredLevel) {
 +        case READ_UNCOMMITTED:
 +            return mReadUncommittedLevel;
 +        case READ_COMMITTED:
 +            return mReadCommittedLevel;
 +        case REPEATABLE_READ:
 +            return mRepeatableReadLevel;
 +        case SERIALIZABLE:
 +            return mSerializableLevel;
 +        }
 +
 +        return null;
 +    }
 +
 +    JDBCSupportStrategy getSupportStrategy() {
 +        return mSupportStrategy;
 +    }
 +
 +    Repository getRootRepository() {
 +        return mRootRef.get();
 +    }
 +
 +    /**
 +     * 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) {
 +        return mExceptionTransformer.toFetchException(e);
 +    }
 +
 +    /**
 +     * 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) {
 +        return mExceptionTransformer.toPersistException(e);
 +    }
 +
 +    /**
 +     * 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) {
 +        return mExceptionTransformer.toRepositoryException(e);
 +    }
 +
 +    /**
 +     * Examines the SQLSTATE code of the given SQL exception and determines if
 +     * it is a unique constaint violation.
 +     */
 +    public boolean isUniqueConstraintError(SQLException e) {
 +        return mExceptionTransformer.isUniqueConstraintError(e);
 +    }
 +
 +    JDBCExceptionTransformer getExceptionTransformer() {
 +        return mExceptionTransformer;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCRepositoryBuilder.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCRepositoryBuilder.java new file mode 100644 index 0000000..507d70a --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCRepositoryBuilder.java @@ -0,0 +1,255 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.sql.SQLException;
 +import java.util.Collection;
 +
 +import javax.sql.DataSource;
 +
 +import com.amazon.carbonado.ConfigurationException;
 +import com.amazon.carbonado.RepositoryException;
 +import com.amazon.carbonado.spi.AbstractRepositoryBuilder;
 +
 +/**
 + * Builds a repository instance backed by a JDBC accessible database.
 + * JDBCRepository is not independent of the underlying database schema, and so
 + * it requires matching tables and columns in the database. It will not alter
 + * or create tables. Use the {@link com.amazon.carbonado.Alias Alias}
 + * annotation to control precisely which tables and columns must be matched up.
 + *
 + * <p>Note: The current JDBC repository implementation makes certain
 + * assumptions about the database it is accessing. It must support transactions
 + * and multiple statements per connection. If it doesn't support savepoints,
 + * then nested transactions are faked -- rollback of inner transaction will
 + * appear to do nothing.
 + *
 + * <p>
 + * The following extra capabilities are supported:
 + * <ul>
 + * <li>{@link com.amazon.carbonado.capability.IndexInfoCapability IndexInfoCapability}
 + * <li>{@link com.amazon.carbonado.capability.StorableInfoCapability StorableInfoCapability}
 + * <li>{@link com.amazon.carbonado.capability.ShutdownCapability ShutdownCapability}
 + * <li>{@link JDBCConnectionCapability JDBCConnectionCapability}
 + * </ul>
 + *
 + * @author Brian S O'Neill
 + */
 +public class JDBCRepositoryBuilder extends AbstractRepositoryBuilder {
 +    private String mName;
 +    private boolean mIsMaster = true;
 +    private DataSource mDataSource;
 +    private boolean mDataSourceLogging;
 +    private String mCatalog;
 +    private String mSchema;
 +    private String mDriverClassName;
 +    private String mURL;
 +    private String mUsername;
 +    private String mPassword;
 +
 +    public JDBCRepositoryBuilder() {
 +    }
 +
 +    public JDBCRepository build(RepositoryReference rootRef) throws RepositoryException {
 +        assertReady();
 +        JDBCRepository repo = new JDBCRepository
 +            (rootRef, getName(), isMaster(), getDataSource(), mCatalog, mSchema);
 +        rootRef.set(repo);
 +        return repo;
 +    }
 +
 +    public String getName() {
 +        return mName;
 +    }
 +
 +    public void setName(String name) {
 +        mName = name;
 +    }
 +
 +    public boolean isMaster() {
 +        return mIsMaster;
 +    }
 +
 +    public void setMaster(boolean b) {
 +        mIsMaster = b;
 +    }
 +
 +    /**
 +     * Set the source of JDBC connections, overriding all other database
 +     * connectivity configuration in this object.
 +     */
 +    public void setDataSource(DataSource dataSource) {
 +        mDataSource = dataSource;
 +        mDriverClassName = null;
 +        mURL = null;
 +        mUsername = null;
 +        mPassword = null;
 +    }
 +
 +    /**
 +     * Returns the source of JDBC connections, which defaults to a non-pooling
 +     * source if driver class, driver URL, username, and password are all
 +     * supplied.
 +     *
 +     * @throws ConfigurationException if driver class wasn't found
 +     */
 +    public DataSource getDataSource() throws ConfigurationException {
 +        if (mDataSource == null) {
 +            if (mDriverClassName != null && mURL != null) {
 +                try {
 +                    mDataSource = new SimpleDataSource
 +                        (mDriverClassName, mURL, mUsername, mPassword);
 +                } catch (SQLException e) {
 +                    Throwable cause = e.getCause();
 +                    if (cause == null) {
 +                        cause = e;
 +                    }
 +                    throw new ConfigurationException(cause);
 +                }
 +            }
 +        }
 +
 +        DataSource ds = mDataSource;
 +        if (getDataSourceLogging() && !(ds instanceof LoggingDataSource)) {
 +            ds = LoggingDataSource.create(ds);
 +        }
 +
 +        return ds;
 +    }
 +
 +    /**
 +     * Pass true to enable debug logging. By default, it is false.
 +     *
 +     * @see LoggingDataSource
 +     */
 +    public void setDataSourceLogging(boolean b) {
 +        mDataSourceLogging = b;
 +    }
 +
 +    /**
 +     * Returns true if debug logging is enabled.
 +     *
 +     * @see LoggingDataSource
 +     */
 +    public boolean getDataSourceLogging() {
 +        return mDataSourceLogging;
 +    }
 +
 +    /**
 +     * Optionally set the catalog to search for metadata.
 +     */
 +    public void setCatalog(String catalog) {
 +        mCatalog = catalog;
 +    }
 +
 +    /**
 +     * Returns the optional catalog to search for metadata.
 +     */
 +    public String getCatalog() {
 +        return mCatalog;
 +    }
 +
 +    /**
 +     * Optionally set the schema to search for metadata.
 +     */
 +    public void setSchema(String schema) {
 +        mSchema = schema;
 +    }
 +
 +    /**
 +     * Returns the optional schema to search for metadata.
 +     */
 +    public String getSchema() {
 +        return mSchema;
 +    }
 +
 +    /**
 +     * Set the JDBC driver class name, which is required if a DataSource was not provided.
 +     */
 +    public void setDriverClassName(String driverClassName) {
 +        mDriverClassName = driverClassName;
 +    }
 +
 +    /**
 +     * Returns the driver class name, which may be null if a DataSource was provided.
 +     */
 +    public String getDriverClassName() {
 +        return mDriverClassName;
 +    }
 +
 +    /**
 +     * Set the JDBC connection URL, which is required if a DataSource was not
 +     * provided.
 +     */
 +    public void setDriverURL(String url) {
 +        mURL = url;
 +    }
 +
 +    /**
 +     * Returns the connection URL, which may be null if a DataSource was
 +     * provided.
 +     */
 +    public String getDriverURL() {
 +        return mURL;
 +    }
 +
 +    /**
 +     * Optionally set the username to use with DataSource.
 +     */
 +    public void setUserName(String username) {
 +        mUsername = username;
 +    }
 +
 +    /**
 +     * Returns the optional username to use with DataSource.
 +     */
 +    public String getUserName() {
 +        return mUsername;
 +    }
 +
 +    /**
 +     * Optionally set the password to use with DataSource.
 +     */
 +    public void setPassword(String password) {
 +        mPassword = password;
 +    }
 +
 +    /**
 +     * Returns the optional password to use with DataSource.
 +     */
 +    public String getPassword() {
 +        return mPassword;
 +    }
 +
 +    public void errorCheck(Collection<String> messages) throws ConfigurationException {
 +        super.errorCheck(messages);
 +        if (mDataSource == null) {
 +            if (mDriverClassName == null) {
 +                messages.add("driverClassName missing");
 +            }
 +            if (mURL == null) {
 +                messages.add("driverURL missing");
 +            }
 +            if (messages.size() == 0) {
 +                // Verify driver exists, only if no other errors.
 +                getDataSource();
 +            }
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSequenceValueProducer.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSequenceValueProducer.java new file mode 100644 index 0000000..f98bd1e --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSequenceValueProducer.java @@ -0,0 +1,68 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.sql.Connection;
 +import java.sql.ResultSet;
 +import java.sql.Statement;
 +
 +import com.amazon.carbonado.PersistException;
 +
 +import com.amazon.carbonado.spi.AbstractSequenceValueProducer;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCSequenceValueProducer extends AbstractSequenceValueProducer {
 +    private final JDBCRepository mRepo;
 +    private final String mQuery;
 +
 +    JDBCSequenceValueProducer(JDBCRepository repo, String sequenceQuery) {
 +        mRepo = repo;
 +        mQuery = sequenceQuery;
 +    }
 +
 +    public long nextLongValue() throws PersistException {
 +        try {
 +            Connection con = mRepo.getConnection();
 +            try {
 +                Statement st = con.createStatement();
 +                try {
 +                    ResultSet rs = st.executeQuery(mQuery);
 +                    try {
 +                        if (rs.next()) {
 +                            return rs.getLong(1);
 +                        }
 +                        throw new PersistException("No results from sequence query: " + mQuery);
 +                    } finally {
 +                        rs.close();
 +                    }
 +                } finally {
 +                    st.close();
 +                }
 +            } finally {
 +                mRepo.yieldConnection(con);
 +            }
 +        } catch (Exception e) {
 +            throw mRepo.toPersistException(e);
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java new file mode 100644 index 0000000..bb851ab --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java @@ -0,0 +1,1892 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.lang.reflect.Method;
 +import java.lang.reflect.UndeclaredThrowableException;
 +import java.sql.Connection;
 +import java.sql.PreparedStatement;
 +import java.sql.ResultSet;
 +import java.sql.SQLException;
 +import java.sql.Statement;
 +import java.util.ArrayList;
 +import java.util.Collection;
 +import java.util.EnumSet;
 +import java.util.IdentityHashMap;
 +import java.util.List;
 +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.SoftValuedHashMap;
 +
 +import com.amazon.carbonado.FetchException;
 +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.lob.Lob;
 +
 +import com.amazon.carbonado.info.StorablePropertyAdapter;
 +
 +import com.amazon.carbonado.spi.CodeBuilderUtil;
 +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 concrete implementations of {@link Storable} types for
 + * {@link JDBCRepository}.
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCStorableGenerator<S extends Storable> {
 +    // These method names end in "$" to prevent name collisions with any
 +    // inherited methods.
 +    private static final String EXTRACT_ALL_METHOD_NAME = "extractAll$";
 +    private static final String EXTRACT_DATA_METHOD_NAME = "extractData$";
 +    private static final String LOB_LOADER_FIELD_PREFIX = "lobLoader$";
 +
 +    // Initial StringBuilder capactity for update statement.
 +    private static final int INITIAL_UPDATE_BUFFER_SIZE = 100;
 +
 +    private static final Map<Class<?>, Class<? extends Storable>> cCache;
 +
 +    static {
 +        cCache = new SoftValuedHashMap();
 +    }
 +
 +    static <S extends Storable> Class<? extends S> getGeneratedClass(JDBCStorableInfo<S> info)
 +        throws SupportException
 +    {
 +        Class<S> type = info.getStorableType();
 +        synchronized (cCache) {
 +            Class<? extends S> generatedClass = (Class<? extends S>) cCache.get(type);
 +            if (generatedClass != null) {
 +                return generatedClass;
 +            }
 +            generatedClass = new JDBCStorableGenerator<S>(info).generateAndInjectClass();
 +            cCache.put(type, generatedClass);
 +            return generatedClass;
 +        }
 +    }
 +
 +    private final Class<S> mStorableType;
 +    private final JDBCStorableInfo<S> mInfo;
 +    private final Map<String, ? extends JDBCStorableProperty<S>> mAllProperties;
 +
 +    private final ClassLoader mParentClassLoader;
 +    private final ClassInjector mClassInjector;
 +    private final ClassFile mClassFile;
 +
 +    private JDBCStorableGenerator(JDBCStorableInfo<S> info) throws SupportException {
 +        mStorableType = info.getStorableType();
 +        mInfo = info;
 +        mAllProperties = mInfo.getAllProperties();
 +
 +        EnumSet<MasterFeature> features = EnumSet
 +            .of(MasterFeature.INSERT_SEQUENCES,
 +                MasterFeature.INSERT_TXN, MasterFeature.UPDATE_TXN);
 +
 +        final Class<? extends S> abstractClass =
 +            MasterStorableGenerator.getAbstractClass(mStorableType, features);
 +
 +        mParentClassLoader = abstractClass.getClassLoader();
 +        mClassInjector = ClassInjector.create(mStorableType.getName(), mParentClassLoader);
 +
 +        mClassFile = new ClassFile(mClassInjector.getClassName(), abstractClass);
 +        mClassFile.markSynthetic();
 +        mClassFile.setSourceFile(JDBCStorableGenerator.class.getName());
 +        mClassFile.setTarget("1.5");
 +    }
 +
 +    private Class<? extends S> generateAndInjectClass() {
 +        // We'll need these "inner classes" which serve as Lob loading
 +        // callbacks. Lob's need to be reloaded if the original transaction has
 +        // been committed.
 +        final Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap = generateLobLoaders();
 +
 +        // Declare some types.
 +        final TypeDesc storageType = TypeDesc.forClass(Storage.class);
 +        final TypeDesc jdbcRepoType = TypeDesc.forClass(JDBCRepository.class);
 +        final TypeDesc jdbcSupportType = TypeDesc.forClass(JDBCSupport.class);
 +        final TypeDesc resultSetType = TypeDesc.forClass(ResultSet.class);
 +        final TypeDesc connectionType = TypeDesc.forClass(Connection.class);
 +        final TypeDesc preparedStatementType = TypeDesc.forClass(PreparedStatement.class);
 +        final TypeDesc lobArrayType = TypeDesc.forClass(Lob.class).toArrayType();
 +        final TypeDesc masterSupportType = TypeDesc.forClass(MasterSupport.class);
 +        final TypeDesc classType = TypeDesc.forClass(Class.class);
 +
 +        if (lobLoaderMap.size() > 0) {
 +            // Add static initializer to save references to Lob
 +            // loaders. Otherwise, classes might get unloaded before they are
 +            // used for the first time.
 +
 +            MethodInfo mi = mClassFile.addInitializer();
 +            CodeBuilder b = new CodeBuilder(mi);
 +
 +            int i = 0;
 +            for (Class<?> loaderClass : lobLoaderMap.values()) {
 +                String fieldName = LOB_LOADER_FIELD_PREFIX + i;
 +                mClassFile.addField
 +                    (Modifiers.PRIVATE.toStatic(true).toFinal(true), fieldName, classType);
 +                b.loadConstant(TypeDesc.forClass(loaderClass));
 +                b.storeStaticField(fieldName, classType);
 +                i++;
 +            }
 +
 +            b.returnVoid();
 +        }
 +
 +        // Add constructor that accepts a JDBCSupport.
 +        {
 +            TypeDesc[] params = {jdbcSupportType};
 +            MethodInfo mi = mClassFile.addConstructor(Modifiers.PUBLIC, params);
 +            CodeBuilder b = new CodeBuilder(mi);
 +            b.loadThis();
 +            b.loadLocal(b.getParameter(0));
 +            b.checkCast(masterSupportType);
 +            b.invokeSuperConstructor(new TypeDesc[] {masterSupportType});
 +            b.returnVoid();
 +        }
 +
 +        // Add constructor that accepts a JDBCSupport and a ResultSet row.
 +        {
 +            TypeDesc[] params = {jdbcSupportType, resultSetType, TypeDesc.INT};
 +            MethodInfo mi = mClassFile.addConstructor(Modifiers.PUBLIC, params);
 +            CodeBuilder b = new CodeBuilder(mi);
 +            b.loadThis();
 +            b.loadLocal(b.getParameter(0));
 +            b.checkCast(masterSupportType);
 +            b.invokeSuperConstructor(new TypeDesc[] {masterSupportType});
 +
 +            // Call extractAll method to fill in properties.
 +            b.loadThis();
 +            b.loadLocal(b.getParameter(1));
 +            b.loadLocal(b.getParameter(2));
 +            b.invokePrivate(EXTRACT_ALL_METHOD_NAME, null,
 +                            new TypeDesc[] {resultSetType, TypeDesc.INT});
 +
 +            // Indicate that object is clean by calling markAllPropertiesClean.
 +            b.loadThis();
 +            b.invokeVirtual(MARK_ALL_PROPERTIES_CLEAN, null, null);
 +
 +            b.returnVoid();
 +        }
 +
 +        // Add private method to extract all properties from a ResultSet row.
 +        defineExtractAllMethod(lobLoaderMap);
 +        // Add private method to extract non-pk properties from a ResultSet row.
 +        defineExtractDataMethod(lobLoaderMap);
 +
 +        // For all unsupported properties, override get/set method to throw
 +        // UnsupportedOperationException.
 +        {
 +            for (JDBCStorableProperty<S> property : mAllProperties.values()) {
 +                if (property.isJoin() || property.isSupported()) {
 +                    continue;
 +                }
 +                String message = "Independent property \"" + property.getName() +
 +                    "\" is not supported by the SQL schema: ";
 +                message += mInfo.getTableName();
 +                CodeBuilder b = new CodeBuilder(mClassFile.addMethod(property.getReadMethod()));
 +                CodeBuilderUtil.throwException(b, UnsupportedOperationException.class, message);
 +                b = new CodeBuilder(mClassFile.addMethod(property.getWriteMethod()));
 +                CodeBuilderUtil.throwException(b, UnsupportedOperationException.class, message);
 +            }
 +        }
 +
 +        // Add required protected doTryLoad method.
 +        {
 +            MethodInfo mi = mClassFile.addMethod
 +                (Modifiers.PROTECTED,
 +                 MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null);
 +            mi.addException(TypeDesc.forClass(FetchException.class));
 +            CodeBuilder b = new CodeBuilder(mi);
 +
 +            LocalVariable repoVar = getJDBCRepository(b);
 +            Label tryBeforeCon = b.createLabel().setLocation();
 +            LocalVariable conVar = getConnection(b, repoVar);
 +            Label tryAfterCon = b.createLabel().setLocation();
 +
 +            b.loadThis();
 +            b.loadLocal(repoVar);
 +            b.loadLocal(conVar);
 +            b.loadNull(); // No Lobs to update
 +            b.invokeVirtual(MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME,
 +                            TypeDesc.BOOLEAN,
 +                            new TypeDesc[] {jdbcRepoType, connectionType, lobArrayType});
 +            LocalVariable resultVar = b.createLocalVariable("result", TypeDesc.BOOLEAN);
 +            b.storeLocal(resultVar);
 +
 +            yieldConAndHandleException(b, repoVar, tryBeforeCon, conVar, tryAfterCon, false);
 +
 +            b.loadLocal(resultVar);
 +            b.returnValue(TypeDesc.BOOLEAN);
 +        }
 +
 +        // Now define doTryLoad(JDBCRepositry, Connection, Lob[]). The Lob array argument
 +        // is optional, and it indicates which (large) Lobs should be updated upon load.
 +        {
 +            MethodInfo mi = mClassFile.addMethod
 +                (Modifiers.PROTECTED,
 +                 MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME, TypeDesc.BOOLEAN,
 +                 new TypeDesc[] {jdbcRepoType, connectionType, lobArrayType});
 +            mi.addException(TypeDesc.forClass(Exception.class));
 +            CodeBuilder b = new CodeBuilder(mi);
 +
 +            StringBuilder selectBuilder = null;
 +            for (JDBCStorableProperty<S> property : mAllProperties.values()) {
 +                // Along with unsupported properties and joins, primary keys are not loaded.
 +                // This is because they are included in the where clause.
 +                if (!property.isSelectable() || property.isPrimaryKeyMember()) {
 +                    continue;
 +                }
 +                if (selectBuilder == null) {
 +                    selectBuilder = new StringBuilder();
 +                    selectBuilder.append("SELECT ");
 +                } else {
 +                    selectBuilder.append(',');
 +                }
 +                selectBuilder.append(property.getColumnName());
 +            }
 +
 +            if (selectBuilder == null) {
 +                // All properties are pks. A select still needs to be
 +                // performed, but just discard the results. The select needs to
 +                // be performed in order to verify that a record exists, since
 +                // we need to return true or false.
 +                selectBuilder = new StringBuilder();
 +                selectBuilder.append("SELECT ");
 +                selectBuilder.append
 +                    (mInfo.getPrimaryKeyProperties().values().iterator().next().getColumnName());
 +            }
 +
 +            selectBuilder.append(" FROM ");
 +            selectBuilder.append(mInfo.getQualifiedTableName());
 +
 +            LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
 +
 +            Label tryAfterPs = buildWhereClauseAndPreparedStatement
 +                (b, selectBuilder, b.getParameter(1), psVar, b.getParameter(0), null);
 +
 +            b.loadLocal(psVar);
 +            b.invokeInterface(preparedStatementType, "executeQuery", resultSetType, null);
 +            LocalVariable rsVar = b.createLocalVariable("rs", resultSetType);
 +            b.storeLocal(rsVar);
 +            Label tryAfterRs = b.createLabel().setLocation();
 +
 +            // If no results, then return false. Otherwise, there must be
 +            // exactly one result.
 +
 +            LocalVariable resultVar = b.createLocalVariable("result", TypeDesc.BOOLEAN);
 +            b.loadLocal(rsVar);
 +            b.invokeInterface(resultSetType, "next", TypeDesc.BOOLEAN, null);
 +            b.storeLocal(resultVar);
 +            b.loadLocal(resultVar);
 +            Label noResults = b.createLabel();
 +            b.ifZeroComparisonBranch(noResults, "==");
 +
 +            b.loadThis();
 +            b.loadLocal(rsVar);
 +            b.loadConstant(1);
 +            b.loadLocal(b.getParameter(2)); // Pass Lobs to update
 +            b.invokePrivate(EXTRACT_DATA_METHOD_NAME, null,
 +                            new TypeDesc[] {resultSetType, TypeDesc.INT, lobArrayType});
 +
 +            noResults.setLocation();
 +
 +            closeResultSet(b, rsVar, tryAfterRs);
 +            closeStatement(b, psVar, tryAfterPs);
 +
 +            b.loadLocal(resultVar);
 +            b.returnValue(TypeDesc.BOOLEAN);
 +        }
 +
 +        // Unlike the other methods, doTryInsert is allowed to throw an
 +        // SQLException. Override insert and tryInsert to catch SQLException.
 +        // The tryInsert method must also decide if it is a unique constraint
 +        // exception and returns false instead. This design allows the original
 +        // SQLException to be passed with the UniqueConstraintException,
 +        // providing more context.
 +
 +        // Override insert method.
 +        {
 +            MethodInfo mi = mClassFile.addMethod
 +                (Modifiers.PUBLIC, INSERT_METHOD_NAME, null, null);
 +            mi.addException(TypeDesc.forClass(PersistException.class));
 +            CodeBuilder b = new CodeBuilder(mi);
 +
 +            Label tryStart = b.createLabel().setLocation();
 +            b.loadThis();
 +            b.invokeSuper(mClassFile.getSuperClassName(), INSERT_METHOD_NAME, null, null);
 +            Label tryEnd = b.createLabel().setLocation();
 +            b.returnVoid();
 +
 +            b.exceptionHandler(tryStart, tryEnd, Exception.class.getName());
 +            pushJDBCRepository(b);
 +            // Swap exception object and JDBCRepository instance.
 +            b.swap();
 +            TypeDesc[] params = {TypeDesc.forClass(Throwable.class)};
 +            b.invokeVirtual(jdbcRepoType, "toPersistException",
 +                            TypeDesc.forClass(PersistException.class), params);
 +            b.throwObject();
 +        }
 +
 +        // Override tryInsert method.
 +        {
 +            MethodInfo mi = mClassFile.addMethod
 +                (Modifiers.PUBLIC, TRY_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, null);
 +            mi.addException(TypeDesc.forClass(PersistException.class));
 +            CodeBuilder b = new CodeBuilder(mi);
 +
 +            Label tryStart = b.createLabel().setLocation();
 +            b.loadThis();
 +            b.invokeSuper(mClassFile.getSuperClassName(),
 +                          TRY_INSERT_METHOD_NAME, TypeDesc.BOOLEAN, null);
 +            Label innerTryEnd = b.createLabel().setLocation();
 +            b.returnValue(TypeDesc.BOOLEAN);
 +
 +            b.exceptionHandler(tryStart, innerTryEnd, SQLException.class.getName());
 +            b.dup(); // dup the SQLException
 +            pushJDBCRepository(b);
 +            b.swap(); // swap the dup'ed SQLException to pass to method
 +            b.invokeVirtual(jdbcRepoType, "isUniqueConstraintError",
 +                            TypeDesc.BOOLEAN,
 +                            new TypeDesc[] {TypeDesc.forClass(SQLException.class)});
 +            Label notConstraint = b.createLabel();
 +            b.ifZeroComparisonBranch(notConstraint, "==");
 +            // Return false to indicate unique constraint violation.
 +            b.loadConstant(false);
 +            b.returnValue(TypeDesc.BOOLEAN);
 +
 +            notConstraint.setLocation();
 +            // Re-throw SQLException, since it is not a unique constraint violation.
 +            b.throwObject();
 +
 +            Label outerTryEnd = b.createLabel().setLocation();
 +
 +            b.exceptionHandler(tryStart, outerTryEnd, Exception.class.getName());
 +            pushJDBCRepository(b);
 +            // Swap exception object and JDBCRepository instance.
 +            b.swap();
 +            TypeDesc[] params = {TypeDesc.forClass(Throwable.class)};
 +            b.invokeVirtual(jdbcRepoType, "toPersistException",
 +                            TypeDesc.forClass(PersistException.class), params);
 +            b.throwObject();
 +        }
 +
 +        // Add required protected doTryInsert method.
 +        {
 +            MethodInfo mi = mClassFile.addMethod
 +                (Modifiers.PROTECTED,
 +                 MasterStorableGenerator.DO_TRY_INSERT_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null);
 +            mi.addException(TypeDesc.forClass(PersistException.class));
 +            CodeBuilder b = new CodeBuilder(mi);
 +
 +            LocalVariable repoVar = getJDBCRepository(b);
 +            LocalVariable conVar = getConnection(b, repoVar);
 +            Label tryAfterCon = b.createLabel().setLocation();
 +
 +            // Push connection in preparation for preparing a statement.
 +            b.loadLocal(conVar);
 +
 +            // Only insert version property if DIRTY. Create two insert
 +            // statements, with and without the version property.
 +            StringBuilder sb = new StringBuilder();
 +            createInsertStatement(sb, false);
 +            String noVersion = sb.toString();
 +
 +            sb.setLength(0);
 +            int versionPropNumber = createInsertStatement(sb, true);
 +
 +            LocalVariable includeVersion = null;
 +
 +            if (versionPropNumber < 0) {
 +                // No version property at all, so no need to determine which
 +                // statement to execute.
 +                b.loadConstant(noVersion);
 +            } else {
 +                includeVersion = b.createLocalVariable(null, TypeDesc.BOOLEAN);
 +
 +                Label isDirty = b.createLabel();
 +                branchIfDirty(b, versionPropNumber, isDirty, true);
 +
 +                // Version not dirty, so don't insert it. Assume database
 +                // creates an initial version instead.
 +                b.loadConstant(false);
 +                b.storeLocal(includeVersion);
 +                b.loadConstant(noVersion);
 +                Label cont = b.createLabel();
 +                b.branch(cont);
 +
 +                isDirty.setLocation();
 +                // Including version property in statement.
 +                b.loadConstant(true);
 +                b.storeLocal(includeVersion);
 +                b.loadConstant(sb.toString());
 +
 +                cont.setLocation();
 +            }
 +
 +            // At this point, the stack contains a connection and a SQL
 +            // statement String.
 +
 +            LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
 +            b.invokeInterface(connectionType, "prepareStatement", preparedStatementType,
 +                              new TypeDesc[] {TypeDesc.STRING});
 +            b.storeLocal(psVar);
 +
 +            Label tryAfterPs = b.createLabel().setLocation();
 +
 +            // Now fill in parameters with property values.
 +
 +            JDBCStorableProperty<S> versionProperty = null;
 +
 +            // Gather all Lob properties to track if a post-insert update is required.
 +            Map<JDBCStorableProperty<S>, Integer> lobIndexMap = findLobs();
 +            LocalVariable lobArrayVar = null;
 +            if (lobIndexMap.size() != 0) {
 +                // Create array to track which Lobs are too large and need extra work.
 +                lobArrayVar = b.createLocalVariable(null, lobArrayType);
 +                b.loadConstant(lobIndexMap.size());
 +                b.newObject(lobArrayType);
 +                b.storeLocal(lobArrayVar);
 +            }
 +
 +            int ordinal = 0;
 +            for (JDBCStorableProperty<S> property : mAllProperties.values()) {
 +                if (!property.isSelectable()) {
 +                    continue;
 +                }
 +                if (property.isVersion()) {
 +                    if (includeVersion != null) {
 +                        // Fill in version later, but check against boolean
 +                        // local variable to decide if it is was dirty.
 +                        versionProperty = property;
 +                    }
 +                    continue;
 +                }
 +
 +                b.loadLocal(psVar);
 +                b.loadConstant(++ordinal);
 +
 +                setPreparedStatementValue
 +                    (b, property, repoVar, null, lobArrayVar, lobIndexMap.get(property));
 +            }
 +
 +            if (versionProperty != null) {
 +                // Fill in version property now, but only if was dirty.
 +                b.loadLocal(includeVersion);
 +                Label skipVersion = b.createLabel();
 +                b.ifZeroComparisonBranch(skipVersion, "==");
 +
 +                b.loadLocal(psVar);
 +                b.loadConstant(++ordinal);
 +                setPreparedStatementValue(b, versionProperty, repoVar, null, null, null);
 +
 +                skipVersion.setLocation();
 +            }
 +
 +            // Execute the statement.
 +            b.loadLocal(psVar);
 +            b.invokeInterface(preparedStatementType, "executeUpdate", TypeDesc.INT, null);
 +            b.pop();
 +            closeStatement(b, psVar, tryAfterPs);
 +
 +            // Immediately reload object, to ensure that any database supplied
 +            // default values are properly retrieved. Since INSERT_TXN is
 +            // enabled, superclass ensures that transaction is still in
 +            // progress at this point.
 +
 +            b.loadThis();
 +            b.loadLocal(repoVar);
 +            b.loadLocal(conVar);
 +            if (lobArrayVar == null) {
 +                b.loadNull();
 +            } else {
 +                b.loadLocal(lobArrayVar);
 +            }
 +            b.invokeVirtual(MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME,
 +                            TypeDesc.BOOLEAN,
 +                            new TypeDesc[] {jdbcRepoType, connectionType, lobArrayType});
 +            b.pop();
 +
 +            // Note: yieldConAndHandleException is not called, allowing any
 +            // SQLException to be thrown. The insert or tryInsert methods must handle it.
 +            yieldCon(b, repoVar, conVar, tryAfterCon);
 +
 +            b.loadConstant(true);
 +            b.returnValue(TypeDesc.BOOLEAN);
 +        }
 +
 +        // Add required protected doTryUpdate method.
 +        {
 +            MethodInfo mi = mClassFile.addMethod
 +                (Modifiers.PROTECTED,
 +                 MasterStorableGenerator.DO_TRY_UPDATE_MASTER_METHOD_NAME,
 +                 TypeDesc.BOOLEAN, null);
 +            mi.addException(TypeDesc.forClass(PersistException.class));
 +
 +            CodeBuilder b = new CodeBuilder(mi);
 +
 +            // Only update properties with state DIRTY. Therefore, update
 +            // statement is always dynamic.
 +
 +            LocalVariable repoVar = getJDBCRepository(b);
 +            Label tryBeforeCon = b.createLabel().setLocation();
 +            LocalVariable conVar = getConnection(b, repoVar);
 +            Label tryAfterCon = b.createLabel().setLocation();
 +
 +            // Load connection in preparation for creating statement.
 +            b.loadLocal(conVar);
 +
 +            TypeDesc stringBuilderType = TypeDesc.forClass(StringBuilder.class);
 +            b.newObject(stringBuilderType);
 +            b.dup();
 +            b.loadConstant(INITIAL_UPDATE_BUFFER_SIZE);
 +            b.invokeConstructor(stringBuilderType, new TypeDesc[] {TypeDesc.INT});
 +
 +            // Methods on StringBuilder.
 +            final Method appendStringMethod;
 +            final Method appendCharMethod;
 +            final Method toStringMethod;
 +            try {
 +                appendStringMethod = StringBuilder.class.getMethod("append", String.class);
 +                appendCharMethod = StringBuilder.class.getMethod("append", char.class);
 +                toStringMethod = StringBuilder.class.getMethod("toString", (Class[]) null);
 +            } catch (NoSuchMethodException e) {
 +                throw new UndeclaredThrowableException(e);
 +            }
 +
 +            {
 +                StringBuilder sqlBuilder = new StringBuilder();
 +                sqlBuilder.append("UPDATE ");
 +                sqlBuilder.append(mInfo.getQualifiedTableName());
 +                sqlBuilder.append(" SET ");
 +
 +                b.loadConstant(sqlBuilder.toString());
 +                b.invoke(appendStringMethod); // method leaves StringBuilder on stack
 +            }
 +
 +            // Iterate over the properties, appending a set parameter for each
 +            // that is dirty.
 +
 +            LocalVariable countVar = b.createLocalVariable("count", TypeDesc.INT);
 +            b.loadConstant(0);
 +            b.storeLocal(countVar);
 +
 +            int propNumber = -1;
 +            for (JDBCStorableProperty property : mAllProperties.values()) {
 +                propNumber++;
 +
 +                if (property.isSelectable() && !property.isPrimaryKeyMember()) {
 +                    if (property.isVersion()) {
 +                        // TODO: Support option where version property is
 +                        // updated on the Carbonado side rather than relying on
 +                        // SQL trigger.
 +                        continue;
 +                    }
 +
 +                    Label isNotDirty = b.createLabel();
 +                    branchIfDirty(b, propNumber, isNotDirty, false);
 +
 +                    b.loadLocal(countVar);
 +                    Label isZero = b.createLabel();
 +                    b.ifZeroComparisonBranch(isZero, "==");
 +                    b.loadConstant(',');
 +                    b.invoke(appendCharMethod);
 +
 +                    isZero.setLocation();
 +                    b.loadConstant(property.getColumnName());
 +                    b.invoke(appendStringMethod);
 +                    b.loadConstant("=?");
 +                    b.invoke(appendStringMethod);
 +
 +                    b.integerIncrement(countVar, 1);
 +
 +                    isNotDirty.setLocation();
 +                }
 +            }
 +
 +            Collection<JDBCStorableProperty<S>> whereProperties =
 +                mInfo.getPrimaryKeyProperties().values();
 +
 +            JDBCStorableProperty<S> versionProperty = mInfo.getVersionProperty();
 +            if (versionProperty != null && versionProperty.isSupported()) {
 +                // Include version property in WHERE clause to support optimistic locking.
 +                List<JDBCStorableProperty<S>> list =
 +                    new ArrayList<JDBCStorableProperty<S>>(whereProperties);
 +                list.add(versionProperty);
 +                whereProperties = list;
 +            }
 +
 +            // If no dirty properties, a valid update statement must still be
 +            // created. Just update the first "where" property to itself.
 +            {
 +                b.loadLocal(countVar);
 +                Label notZero = b.createLabel();
 +                b.ifZeroComparisonBranch(notZero, "!=");
 +
 +                b.loadConstant(whereProperties.iterator().next().getColumnName());
 +                b.invoke(appendStringMethod);
 +                b.loadConstant("=?");
 +                b.invoke(appendStringMethod);
 +
 +                notZero.setLocation();
 +            }
 +
 +            b.loadConstant(" WHERE ");
 +            b.invoke(appendStringMethod);
 +
 +            int ordinal = 0;
 +            for (JDBCStorableProperty<S> property : whereProperties) {
 +                if (ordinal > 0) {
 +                    b.loadConstant(" AND ");
 +                    b.invoke(appendStringMethod);
 +                }
 +                b.loadConstant(property.getColumnName());
 +                b.invoke(appendStringMethod);
 +                if (property.isNullable()) {
 +                    // FIXME
 +                    throw new UnsupportedOperationException();
 +                } else {
 +                    b.loadConstant("=?");
 +                    b.invoke(appendStringMethod);
 +                }
 +                ordinal++;
 +            }
 +
 +            // Convert StringBuilder value to a String.
 +            b.invoke(toStringMethod);
 +
 +            // At this point, the stack contains a connection and a SQL
 +            // statement String.
 +
 +            LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
 +            b.invokeInterface(connectionType, "prepareStatement", preparedStatementType,
 +                              new TypeDesc[] {TypeDesc.STRING});
 +            b.storeLocal(psVar);
 +            Label tryAfterPs = b.createLabel().setLocation();
 +
 +            // Walk through dirty properties again, setting values on statement.
 +
 +            LocalVariable indexVar = b.createLocalVariable("index", TypeDesc.INT);
 +            // First prepared statement index is always one, so says JDBC.
 +            b.loadConstant(1);
 +            b.storeLocal(indexVar);
 +
 +            // Gather all Lob properties to track if a post-update update is required.
 +            Map<JDBCStorableProperty<S>, Integer> lobIndexMap = findLobs();
 +            LocalVariable lobArrayVar = null;
 +            if (lobIndexMap.size() != 0) {
 +                // Create array to track which Lobs are too large and need extra work.
 +                lobArrayVar = b.createLocalVariable(null, lobArrayType);
 +                b.loadConstant(lobIndexMap.size());
 +                b.newObject(lobArrayType);
 +                b.storeLocal(lobArrayVar);
 +            }
 +
 +            // If no dirty properties, fill in extra property from before.
 +            {
 +                b.loadLocal(countVar);
 +                Label notZero = b.createLabel();
 +                b.ifZeroComparisonBranch(notZero, "!=");
 +
 +                JDBCStorableProperty property = whereProperties.iterator().next();
 +
 +                b.loadLocal(psVar);
 +                b.loadLocal(indexVar);
 +                setPreparedStatementValue
 +                    (b, property, repoVar, null, lobArrayVar, lobIndexMap.get(property));
 +
 +                b.integerIncrement(indexVar, 1);
 +
 +                notZero.setLocation();
 +            }
 +
 +            propNumber = -1;
 +            for (JDBCStorableProperty property : mAllProperties.values()) {
 +                propNumber++;
 +
 +                if (property.isSelectable() && !property.isPrimaryKeyMember()) {
 +                    if (property.isVersion()) {
 +                        // TODO: Support option where version property is
 +                        // updated on the Carbonado side rather than relying on
 +                        // SQL trigger. Just add one to the value.
 +                        continue;
 +                    }
 +
 +                    Label isNotDirty = b.createLabel();
 +                    branchIfDirty(b, propNumber, isNotDirty, false);
 +
 +                    b.loadLocal(psVar);
 +                    b.loadLocal(indexVar);
 +                    setPreparedStatementValue
 +                        (b, property, repoVar, null, lobArrayVar, lobIndexMap.get(property));
 +
 +                    b.integerIncrement(indexVar, 1);
 +
 +                    isNotDirty.setLocation();
 +                }
 +            }
 +
 +            // Walk through where clause properties again, setting values on
 +            // statement.
 +
 +            for (JDBCStorableProperty<S> property : whereProperties) {
 +                if (property.isNullable()) {
 +                    // FIXME
 +                    throw new UnsupportedOperationException();
 +                } else {
 +                    b.loadLocal(psVar);
 +                    b.loadLocal(indexVar);
 +                    setPreparedStatementValue(b, property, repoVar, null, null, null);
 +                }
 +
 +                b.integerIncrement(indexVar, 1);
 +            }
 +
 +            // Execute the update statement.
 +
 +            b.loadLocal(psVar);
 +            LocalVariable updateCount = b.createLocalVariable("updateCount", TypeDesc.INT);
 +            b.invokeInterface(preparedStatementType, "executeUpdate", TypeDesc.INT, null);
 +            b.storeLocal(updateCount);
 +
 +            closeStatement(b, psVar, tryAfterPs);
 +
 +            Label doReload = b.createLabel();
 +            Label skipReload = b.createLabel();
 +
 +            if (versionProperty == null) {
 +                b.loadLocal(updateCount);
 +                b.ifZeroComparisonBranch(skipReload, "==");
 +            } else {
 +                // If update count is zero, either the record was deleted or
 +                // the version doesn't match. To distinguish these two cases,
 +                // select record version. If not found, return
 +                // false. Otherwise, throw OptimisticLockException.
 +
 +                b.loadLocal(updateCount);
 +                b.ifZeroComparisonBranch(doReload, "!=");
 +
 +                StringBuilder selectBuilder = new StringBuilder();
 +                selectBuilder.append("SELECT ");
 +                selectBuilder.append(versionProperty.getColumnName());
 +                selectBuilder.append(" FROM ");
 +                selectBuilder.append(mInfo.getQualifiedTableName());
 +
 +                LocalVariable countPsVar = b.createLocalVariable("ps", preparedStatementType);
 +
 +                Label tryAfterCountPs = buildWhereClauseAndPreparedStatement
 +                    (b, selectBuilder, conVar, countPsVar, null, null);
 +
 +                b.loadLocal(countPsVar);
 +                b.invokeInterface(preparedStatementType, "executeQuery", resultSetType, null);
 +                LocalVariable rsVar = b.createLocalVariable("rs", resultSetType);
 +                b.storeLocal(rsVar);
 +                Label tryAfterRs = b.createLabel().setLocation();
 +
 +                b.loadLocal(rsVar);
 +                b.invokeInterface(resultSetType, "next", TypeDesc.BOOLEAN, null);
 +                // Record missing, return false.
 +                b.ifZeroComparisonBranch(skipReload, "==");
 +
 +                b.loadLocal(rsVar);
 +                b.loadConstant(1); // column 1
 +                b.invokeInterface(resultSetType, "getLong",
 +                                  TypeDesc.LONG, new TypeDesc[] {TypeDesc.INT});
 +                LocalVariable actualVersion = b.createLocalVariable(null, TypeDesc.LONG);
 +                b.storeLocal(actualVersion);
 +
 +                closeResultSet(b, rsVar, tryAfterRs);
 +                closeStatement(b, countPsVar, tryAfterCountPs);
 +
 +                // Throw exception.
 +                {
 +                    TypeDesc desc = TypeDesc.forClass(OptimisticLockException.class);
 +                    b.newObject(desc);
 +                    b.dup();
 +                    b.loadThis();
 +                    // Pass expected version number for exception message.
 +                    TypeDesc propertyType = TypeDesc.forClass(versionProperty.getType());
 +                    b.loadField(versionProperty.getName(), propertyType);
 +                    b.convert(propertyType, TypeDesc.LONG.toObjectType());
 +                    b.loadLocal(actualVersion);
 +                    b.convert(TypeDesc.LONG, TypeDesc.LONG.toObjectType());
 +                    b.invokeConstructor(desc, new TypeDesc[] {TypeDesc.OBJECT, TypeDesc.OBJECT});
 +                    b.throwObject();
 +                }
 +            }
 +
 +            // Immediately reload object, to ensure that any database supplied
 +            // default values are properly retrieved. Since UPDATE_TXN is
 +            // enabled, superclass ensures that transaction is still in
 +            // progress at this point.
 +
 +            doReload.setLocation();
 +            b.loadThis();
 +            b.loadLocal(repoVar);
 +            b.loadLocal(conVar);
 +            if (lobArrayVar == null) {
 +                b.loadNull();
 +            } else {
 +                b.loadLocal(lobArrayVar);
 +            }
 +            b.invokeVirtual(MasterStorableGenerator.DO_TRY_LOAD_MASTER_METHOD_NAME,
 +                            TypeDesc.BOOLEAN,
 +                            new TypeDesc[] {jdbcRepoType, connectionType, lobArrayType});
 +            // Even though a boolean is returned, the actual value for true and
 +            // false is an int, 1 or 0.
 +            b.storeLocal(updateCount);
 +
 +            skipReload.setLocation();
 +
 +            yieldConAndHandleException(b, repoVar, tryBeforeCon, conVar, tryAfterCon, true);
 +
 +            b.loadLocal(updateCount);
 +            b.returnValue(TypeDesc.BOOLEAN);
 +        }
 +
 +        // Add required protected doTryDelete method.
 +        {
 +            MethodInfo mi = mClassFile.addMethod
 +                (Modifiers.PROTECTED,
 +                 MasterStorableGenerator.DO_TRY_DELETE_MASTER_METHOD_NAME, TypeDesc.BOOLEAN, null);
 +            mi.addException(TypeDesc.forClass(PersistException.class));
 +            CodeBuilder b = new CodeBuilder(mi);
 +
 +            StringBuilder deleteBuilder = new StringBuilder();
 +            deleteBuilder.append("DELETE FROM ");
 +            deleteBuilder.append(mInfo.getQualifiedTableName());
 +
 +            LocalVariable repoVar = getJDBCRepository(b);
 +            Label tryBeforeCon = b.createLabel().setLocation();
 +            LocalVariable conVar = getConnection(b, repoVar);
 +            Label tryAfterCon = b.createLabel().setLocation();
 +
 +            LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
 +
 +            Label tryAfterPs = buildWhereClauseAndPreparedStatement
 +                (b, deleteBuilder, conVar, psVar, null, null);
 +
 +            b.loadLocal(psVar);
 +            b.invokeInterface(preparedStatementType, "executeUpdate", TypeDesc.INT, null);
 +
 +            // Return false if count is zero, true otherwise. Just return the
 +            // int as if it were boolean.
 +
 +            LocalVariable resultVar = b.createLocalVariable("result", TypeDesc.INT);
 +            b.storeLocal(resultVar);
 +
 +            closeStatement(b, psVar, tryAfterPs);
 +            yieldConAndHandleException(b, repoVar, tryBeforeCon, conVar, tryAfterCon, true);
 +
 +            b.loadLocal(resultVar);
 +            b.returnValue(TypeDesc.BOOLEAN);
 +        }
 +
 +        Class<? extends S> generatedClass = mClassInjector.defineClass(mClassFile);
 +
 +        // Touch lobLoaderMap to ensure reference to these classes are kept
 +        // until after storable class is generated. Otherwise, these classes
 +        // might get unloaded.
 +        lobLoaderMap.size();
 +
 +        return generatedClass;
 +    }
 +
 +    /**
 +     * Finds all Lob properties and maps them to a zero-based index. This
 +     * information is used to update large Lobs after an insert or update.
 +     */
 +    private Map<JDBCStorableProperty<S>, Integer>findLobs() {
 +        Map<JDBCStorableProperty<S>, Integer> lobIndexMap =
 +            new IdentityHashMap<JDBCStorableProperty<S>, Integer>();
 +
 +        int lobIndex = 0;
 +
 +        for (JDBCStorableProperty<S> property : mAllProperties.values()) {
 +            if (!property.isSelectable() || property.isVersion()) {
 +                continue;
 +            }
 +
 +            Class psClass = property.getPreparedStatementSetMethod().getParameterTypes()[1];
 +
 +            if (Lob.class.isAssignableFrom(property.getType()) ||
 +                java.sql.Blob.class.isAssignableFrom(psClass) ||
 +                java.sql.Clob.class.isAssignableFrom(psClass)) {
 +
 +                lobIndexMap.put(property, lobIndex++);
 +            }
 +        }
 +
 +        return lobIndexMap;
 +    }
 +
 +    /**
 +     * Generates code to get the JDBCRepository instance and store it in a
 +     * local variable.
 +     */
 +    private LocalVariable getJDBCRepository(CodeBuilder b) {
 +        pushJDBCRepository(b);
 +        LocalVariable repoVar =
 +            b.createLocalVariable("repo", TypeDesc.forClass(JDBCRepository.class));
 +        b.storeLocal(repoVar);
 +        return repoVar;
 +    }
 +
 +    /**
 +     * Generates code to push the JDBCRepository instance on the stack.
 +     */
 +    private void pushJDBCRepository(CodeBuilder b) {
 +        pushJDBCSupport(b);
 +        b.invokeInterface(TypeDesc.forClass(JDBCSupport.class), "getJDBCRepository",
 +                          TypeDesc.forClass(JDBCRepository.class), null);
 +    }
 +
 +    /**
 +     * Generates code to push the JDBCSupport instance on the stack.
 +     */
 +    private void pushJDBCSupport(CodeBuilder b) {
 +        b.loadThis();
 +        b.loadField(StorableGenerator.SUPPORT_FIELD_NAME, TypeDesc.forClass(TriggerSupport.class));
 +        b.checkCast(TypeDesc.forClass(JDBCSupport.class));
 +    }
 +
 +    /**
 +     * Generates code to get connection from JDBCRepository and store it in a local variable.
 +     *
 +     * @param repoVar reference to JDBCRepository
 +     */
 +    private LocalVariable getConnection(CodeBuilder b, LocalVariable repoVar) {
 +        b.loadLocal(repoVar);
 +        b.invokeVirtual(TypeDesc.forClass(JDBCRepository.class),
 +                        "getConnection", TypeDesc.forClass(Connection.class), null);
 +        LocalVariable conVar = b.createLocalVariable("con", TypeDesc.forClass(Connection.class));
 +        b.storeLocal(conVar);
 +        return conVar;
 +    }
 +
 +    /**
 +     * Generates code which emulates this:
 +     *
 +     *     // May throw FetchException
 +     *     JDBCRepository.yieldConnection(con);
 +     *
 +     * @param repoVar required reference to JDBCRepository
 +     * @param conVar optional connection to yield
 +     */
 +    private void yieldConnection(CodeBuilder b, LocalVariable repoVar, LocalVariable conVar) {
 +        if (conVar != null) {
 +            b.loadLocal(repoVar);
 +            b.loadLocal(conVar);
 +            b.invokeVirtual(TypeDesc.forClass(JDBCRepository.class),
 +                            "yieldConnection", null,
 +                            new TypeDesc[] {TypeDesc.forClass(Connection.class)});
 +        }
 +    }
 +
 +    /**
 +     * Generates code that finishes the given SQL statement by appending a
 +     * WHERE clause. Prepared statement is then created and all parameters are
 +     * filled in.
 +     *
 +     * @param sqlBuilder contains SQL statement right before the WHERE clause
 +     * @param conVar local variable referencing connection
 +     * @param psVar declared local variable which will receive PreparedStatement
 +     * @param jdbcRepoVar when non-null, check transaction if SELECT should be FOR UPDATE
 +     * @param instanceVar when null, assume properties are contained in
 +     * "this". Otherwise, invoke property access methods on storable referenced
 +     * in var.
 +     * @return label right after prepared statement was created, which is to be
 +     * used as the start of a try block that ensures the prepared statement is
 +     * closed.
 +     */
 +    private Label buildWhereClauseAndPreparedStatement
 +        (CodeBuilder b,
 +         StringBuilder sqlBuilder,
 +         LocalVariable conVar,
 +         LocalVariable psVar,
 +         LocalVariable jdbcRepoVar,
 +         LocalVariable instanceVar)
 +    {
 +        final TypeDesc superType = TypeDesc.forClass(mClassFile.getSuperClassName());
 +        final Iterable<? extends JDBCStorableProperty<?>> properties =
 +            mInfo.getPrimaryKeyProperties().values();
 +
 +        sqlBuilder.append(" WHERE ");
 +
 +        List<JDBCStorableProperty> nullableProperties = new ArrayList<JDBCStorableProperty>();
 +        int ordinal = 0;
 +        for (JDBCStorableProperty property : properties) {
 +            if (!property.isSelectable()) {
 +                continue;
 +            }
 +            if (property.isNullable()) {
 +                // Nullable properties need to alter the SQL where clause
 +                // syntax at runtime, taking the forms "=?" or "IS NULL".
 +                nullableProperties.add(property);
 +                continue;
 +            }
 +            if (ordinal > 0) {
 +                sqlBuilder.append(" AND ");
 +            }
 +            sqlBuilder.append(property.getColumnName());
 +            sqlBuilder.append("=?");
 +            ordinal++;
 +        }
 +
 +        // Push connection in preparation for preparing a statement.
 +        b.loadLocal(conVar);
 +
 +        if (nullableProperties.size() == 0) {
 +            b.loadConstant(sqlBuilder.toString());
 +
 +            // Determine at runtime if SELECT should be " FOR UPDATE".
 +            if (jdbcRepoVar != null) {
 +                b.loadLocal(jdbcRepoVar);
 +                b.invokeVirtual
 +                    (jdbcRepoVar.getType(), "isTransactionForUpdate", TypeDesc.BOOLEAN, null);
 +                Label notForUpdate = b.createLabel();
 +                b.ifZeroComparisonBranch(notForUpdate, "==");
 +
 +                b.loadConstant(" FOR UPDATE");
 +                b.invokeVirtual(TypeDesc.STRING, "concat",
 +                                TypeDesc.STRING, new TypeDesc[] {TypeDesc.STRING});
 +
 +                notForUpdate.setLocation();
 +            }
 +        } else {
 +            // Finish select statement at runtime, since we don't know if the
 +            // properties are null or not.
 +            if (ordinal > 0) {
 +                sqlBuilder.append(" AND ");
 +            }
 +
 +            // Make runtime buffer capacity large enough to hold all "IS NULL" phrases.
 +            int capacity = sqlBuilder.length() + 7 * nullableProperties.size();
 +            if (nullableProperties.size() > 1) {
 +                // Account for all the appended " AND " phrases.
 +                capacity += 5 * (nullableProperties.size() - 1);
 +            }
 +            for (JDBCStorableProperty property : nullableProperties) {
 +                // Account for property names.
 +                capacity += property.getColumnName().length();
 +            }
 +
 +            TypeDesc stringBuilderType = TypeDesc.forClass(StringBuilder.class);
 +            b.newObject(stringBuilderType);
 +            b.dup();
 +            b.loadConstant(capacity);
 +            b.invokeConstructor(stringBuilderType, new TypeDesc[] {TypeDesc.INT});
 +
 +            // Methods on StringBuilder.
 +            final Method appendStringMethod;
 +            final Method toStringMethod;
 +            try {
 +                appendStringMethod = StringBuilder.class.getMethod("append", String.class);
 +                toStringMethod = StringBuilder.class.getMethod("toString", (Class[]) null);
 +            } catch (NoSuchMethodException e) {
 +                throw new UndeclaredThrowableException(e);
 +            }
 +
 +            b.loadConstant(sqlBuilder.toString());
 +            b.invoke(appendStringMethod); // method leaves StringBuilder on stack
 +
 +            ordinal = 0;
 +            for (JDBCStorableProperty property : nullableProperties) {
 +                if (ordinal > 0) {
 +                    b.loadConstant(" AND ");
 +                    b.invoke(appendStringMethod);
 +                }
 +
 +                b.loadConstant(property.getColumnName());
 +                b.invoke(appendStringMethod);
 +
 +                b.loadThis();
 +
 +                final TypeDesc propertyType = TypeDesc.forClass(property.getType());
 +                b.loadField(superType, property.getName(), propertyType);
 +
 +                Label notNull = b.createLabel();
 +                b.ifNullBranch(notNull, false);
 +                b.loadConstant("IS NULL");
 +                b.invoke(appendStringMethod);
 +                Label next = b.createLabel();
 +                b.branch(next);
 +
 +                notNull.setLocation();
 +                b.loadConstant("=?");
 +                b.invoke(appendStringMethod);
 +
 +                next.setLocation();
 +                ordinal++;
 +            }
 +
 +            // Determine at runtime if SELECT should be " FOR UPDATE".
 +            if (jdbcRepoVar != null) {
 +                b.loadLocal(jdbcRepoVar);
 +                b.invokeVirtual
 +                    (jdbcRepoVar.getType(), "isTransactionForUpdate", TypeDesc.BOOLEAN, null);
 +                Label notForUpdate = b.createLabel();
 +                b.ifZeroComparisonBranch(notForUpdate, "==");
 +
 +                b.loadConstant(" FOR UPDATE");
 +                b.invoke(appendStringMethod);
 +
 +                notForUpdate.setLocation();
 +            }
 +
 +            // Convert StringBuilder to String.
 +            b.invoke(toStringMethod);
 +        }
 +
 +        // At this point, the stack contains a connection and a SQL statement String.
 +
 +        final TypeDesc connectionType = TypeDesc.forClass(Connection.class);
 +        final TypeDesc preparedStatementType = TypeDesc.forClass(PreparedStatement.class);
 +
 +        b.invokeInterface(connectionType, "prepareStatement", preparedStatementType,
 +                          new TypeDesc[] {TypeDesc.STRING});
 +        b.storeLocal(psVar);
 +        Label tryAfterPs = b.createLabel().setLocation();
 +
 +        // Now set where clause parameters.
 +        ordinal = 0;
 +        for (JDBCStorableProperty property : properties) {
 +            if (!property.isSelectable()) {
 +                continue;
 +            }
 +
 +            Label skipProperty = b.createLabel();
 +
 +            final TypeDesc propertyType = TypeDesc.forClass(property.getType());
 +
 +            if (!property.isNullable()) {
 +                b.loadLocal(psVar);
 +                b.loadConstant(++ordinal);
 +            } else {
 +                // Nullable properties are dynamically added to where clause,
 +                // and are at the end of the prepared statement. If value is
 +                // null, then skip to the next property, since the statement
 +                // was appended earlier with "IS NULL".
 +                b.loadThis();
 +                b.loadField(superType, property.getName(), propertyType);
 +                b.ifNullBranch(skipProperty, true);
 +            }
 +
 +            setPreparedStatementValue(b, property, null, instanceVar, null, null);
 +
 +            skipProperty.setLocation();
 +        }
 +
 +        return tryAfterPs;
 +    }
 +
 +    /**
 +     * Generates code to call a PreparedStatement.setXxx(int, Xxx) method, with
 +     * the value of the given property. Assumes that PreparedStatement and int
 +     * index are on the stack.
 +     *
 +     * If the property is a Lob, then pass in the optional lobTooLargeVar to
 +     * track if it was too large to insert/update. The type of lobTooLargeVar
 +     * must be the carbonado lob type. At runtime, if the variable's value is
 +     * not null, then lob was too large to insert. The value of the variable is
 +     * the original lob. An update statement needs to be issued after the load
 +     * to insert/update the large value.
 +     *
 +     * @param instanceVar when null, assume properties are contained in
 +     * "this". Otherwise, invoke property access methods on storable referenced
 +     * in var.
 +     * @param lobArrayVar optional, used for lob properties
 +     * @param lobIndex optional, used for lob properties
 +     */
 +    private void setPreparedStatementValue
 +        (CodeBuilder b, JDBCStorableProperty<?> property, LocalVariable repoVar,
 +         LocalVariable instanceVar,
 +         LocalVariable lobArrayVar, Integer lobIndex)
 +    {
 +        if (instanceVar == null) {
 +            b.loadThis();
 +        } else {
 +            b.loadLocal(instanceVar);
 +        }
 +
 +        Class psClass = property.getPreparedStatementSetMethod().getParameterTypes()[1];
 +        TypeDesc psType = TypeDesc.forClass(psClass);
 +        TypeDesc propertyType = TypeDesc.forClass(property.getType());
 +
 +        StorablePropertyAdapter adapter = property.getAppliedAdapter();
 +        TypeDesc fromType;
 +        if (adapter == null) {
 +            // Get protected field directly, since no adapter.
 +            if (instanceVar == null) {
 +                b.loadField(property.getName(), propertyType);
 +            } else {
 +                b.loadField(instanceVar.getType(), property.getName(), propertyType);
 +            }
 +            fromType = propertyType;
 +        } else {
 +            Class toClass = psClass;
 +            if (java.sql.Blob.class.isAssignableFrom(toClass)) {
 +                toClass = com.amazon.carbonado.lob.Blob.class;
 +            } else if (java.sql.Clob.class.isAssignableFrom(toClass)) {
 +                toClass = com.amazon.carbonado.lob.Clob.class;
 +            }
 +            Method adaptMethod = adapter.findAdaptMethod(property.getType(), toClass);
 +            TypeDesc adaptType = TypeDesc.forClass(adaptMethod.getReturnType());
 +            // Invoke special inherited protected method that gets the field
 +            // and invokes the adapter. Method was generated by
 +            // StorableGenerator.
 +            String methodName = property.getReadMethodName() + '$';
 +            if (instanceVar == null) {
 +                b.invokeVirtual(methodName, adaptType, null);
 +            } else {
 +                b.invokeVirtual (instanceVar.getType(), methodName, adaptType, null);
 +            }
 +            fromType = adaptType;
 +        }
 +
 +        Label done = b.createLabel();
 +
 +        if (!fromType.isPrimitive()) {
 +            // Handle case where property value is null.
 +            b.dup();
 +            Label notNull = b.createLabel();
 +            b.ifNullBranch(notNull, false);
 +            // Invoke setNull method instead.
 +            b.pop(); // discard duplicate null.
 +            b.loadConstant(property.getDataType());
 +            b.invokeInterface(TypeDesc.forClass(PreparedStatement.class), "setNull",
 +                              null, new TypeDesc[] {TypeDesc.INT, TypeDesc.INT});
 +            b.branch(done);
 +            notNull.setLocation();
 +        }
 +
 +        if (Lob.class.isAssignableFrom(fromType.toClass())) {
 +            // Run special conversion.
 +
 +            LocalVariable lobVar = b.createLocalVariable(null, fromType);
 +            b.storeLocal(lobVar);
 +            LocalVariable columnVar = b.createLocalVariable(null, TypeDesc.INT);
 +            b.storeLocal(columnVar);
 +            LocalVariable psVar = b.createLocalVariable
 +                ("ps", TypeDesc.forClass(PreparedStatement.class));
 +            b.storeLocal(psVar);
 +
 +            if (lobArrayVar != null && lobIndex != null) {
 +                // Prepare for update result. If too large, then array entry is not null.
 +                b.loadLocal(lobArrayVar);
 +                b.loadConstant(lobIndex);
 +            }
 +
 +            pushJDBCSupport(b);
 +            b.loadLocal(psVar);
 +            b.loadLocal(columnVar);
 +            b.loadLocal(lobVar);
 +
 +            // Stack looks like this: JDBCSupport, PreparedStatement, int (column), Lob
 +
 +            Method setValueMethod;
 +            try {
 +                String name = fromType.toClass().getName();
 +                name = "set" + name.substring(name.lastIndexOf('.') + 1) + "Value";
 +                setValueMethod = JDBCSupport.class.getMethod
 +                    (name, PreparedStatement.class, int.class, fromType.toClass());
 +            } catch (NoSuchMethodException e) {
 +                throw new UndeclaredThrowableException(e);
 +            }
 +
 +            b.invoke(setValueMethod);
 +
 +            if (lobArrayVar == null || lobIndex == null) {
 +                b.pop();
 +            } else {
 +                b.storeToArray(TypeDesc.OBJECT);
 +            }
 +        } else {
 +            b.convert(fromType, psType);
 +            b.invoke(property.getPreparedStatementSetMethod());
 +        }
 +
 +        done.setLocation();
 +    }
 +
 +    /**
 +     * Generates code which emulates this:
 +     *
 +     * ...
 +     * } finally {
 +     *     JDBCRepository.yieldConnection(con);
 +     * }
 +     *
 +     * @param repoVar required reference to JDBCRepository
 +     * @param conVar optional connection variable
 +     * @param tryAfterCon label right after connection acquisition
 +     */
 +    private void yieldCon
 +        (CodeBuilder b,
 +         LocalVariable repoVar,
 +         LocalVariable conVar,
 +         Label tryAfterCon)
 +    {
 +        Label endFinallyLabel = b.createLabel().setLocation();
 +        Label contLabel = b.createLabel();
 +
 +        yieldConnection(b, repoVar, conVar);
 +        b.branch(contLabel);
 +
 +        b.exceptionHandler(tryAfterCon, endFinallyLabel, null);
 +        yieldConnection(b, repoVar, conVar);
 +        b.throwObject();
 +
 +        contLabel.setLocation();
 +    }
 +
 +    /**
 +     * Generates code which emulates this:
 +     *
 +     * ...
 +     *     } finally {
 +     *         JDBCRepository.yieldConnection(con);
 +     *     }
 +     * } catch (Exception e) {
 +     *     throw JDBCRepository.toFetchException(e);
 +     * }
 +     *
 +     * @param repoVar required reference to JDBCRepository
 +     * @param txnVar optional transaction variable to commit/exit
 +     * @param tryBeforeCon label right before connection acquisition
 +     * @param conVar optional connection variable
 +     * @param tryAfterCon label right after connection acquisition
 +     */
 +    private void yieldConAndHandleException
 +        (CodeBuilder b,
 +         LocalVariable repoVar,
 +         Label tryBeforeCon, LocalVariable conVar, Label tryAfterCon,
 +         boolean forPersist)
 +    {
 +        Label endFinallyLabel = b.createLabel().setLocation();
 +        Label contLabel = b.createLabel();
 +
 +        yieldConnection(b, repoVar, conVar);
 +        b.branch(contLabel);
 +
 +        b.exceptionHandler(tryAfterCon, endFinallyLabel, null);
 +        yieldConnection(b, repoVar, conVar);
 +        b.throwObject();
 +
 +        b.exceptionHandler
 +            (tryBeforeCon, b.createLabel().setLocation(), Exception.class.getName());
 +        b.loadLocal(repoVar);
 +        // Swap exception object and JDBCRepository instance.
 +        b.swap();
 +        TypeDesc[] params = {TypeDesc.forClass(Throwable.class)};
 +        if (forPersist) {
 +            b.invokeVirtual(TypeDesc.forClass(JDBCRepository.class), "toPersistException",
 +                            TypeDesc.forClass(PersistException.class), params);
 +        } else {
 +            b.invokeVirtual(TypeDesc.forClass(JDBCRepository.class), "toFetchException",
 +                            TypeDesc.forClass(FetchException.class), params);
 +        }
 +        b.throwObject();
 +
 +        contLabel.setLocation();
 +    }
 +
 +    /**
 +     * Generates code which emulates this:
 +     *
 +     * ...
 +     * } finally {
 +     *     statement.close();
 +     * }
 +     *
 +     * @param statementVar Statement variable
 +     * @param tryAfterStatement label right after Statement acquisition
 +     */
 +    private void closeStatement
 +        (CodeBuilder b, LocalVariable statementVar, Label tryAfterStatement)
 +    {
 +        Label contLabel = b.createLabel();
 +        Label endFinallyLabel = b.createLabel().setLocation();
 +
 +        b.loadLocal(statementVar);
 +        b.invokeInterface(TypeDesc.forClass(Statement.class), "close", null, null);
 +        b.branch(contLabel);
 +
 +        b.exceptionHandler(tryAfterStatement, endFinallyLabel, null);
 +        b.loadLocal(statementVar);
 +        b.invokeInterface(TypeDesc.forClass(Statement.class), "close", null, null);
 +        b.throwObject();
 +
 +        contLabel.setLocation();
 +    }
 +
 +    /**
 +     * Generates code which emulates this:
 +     *
 +     * ...
 +     * } finally {
 +     *     rs.close();
 +     * }
 +     *
 +     * @param rsVar ResultSet variable
 +     * @param tryAfterRs label right after ResultSet acquisition
 +     */
 +    private void closeResultSet
 +        (CodeBuilder b, LocalVariable rsVar, Label tryAfterRs)
 +    {
 +        Label contLabel = b.createLabel();
 +        Label endFinallyLabel = b.createLabel().setLocation();
 +
 +        b.loadLocal(rsVar);
 +        b.invokeInterface(TypeDesc.forClass(ResultSet.class), "close", null, null);
 +        b.branch(contLabel);
 +
 +        b.exceptionHandler(tryAfterRs, endFinallyLabel, null);
 +        b.loadLocal(rsVar);
 +        b.invokeInterface(TypeDesc.forClass(ResultSet.class), "close", null, null);
 +        b.throwObject();
 +
 +        contLabel.setLocation();
 +    }
 +
 +    /**
 +     * Generates code to branch if a property is dirty.
 +     *
 +     * @param propNumber property number from all properties map
 +     * @param target branch target
 +     * @param when true, branch if dirty; when false, branch when not dirty
 +     */
 +    private void branchIfDirty(CodeBuilder b, int propNumber,
 +                               Label target, boolean branchIfDirty)
 +    {
 +        String stateFieldName = StorableGenerator.PROPERTY_STATE_FIELD_NAME + (propNumber >> 4);
 +        b.loadThis();
 +        b.loadField(stateFieldName, TypeDesc.INT);
 +
 +        int shift = (propNumber & 0xf) * 2;
 +        b.loadConstant(StorableGenerator.PROPERTY_STATE_MASK << shift);
 +        b.math(Opcode.IAND);
 +        b.loadConstant(StorableGenerator.PROPERTY_STATE_DIRTY << shift);
 +
 +        b.ifComparisonBranch(target, branchIfDirty ? "==" : "!=");
 +    }
 +
 +    private void defineExtractAllMethod(Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap) {
 +        MethodInfo mi = mClassFile.addMethod
 +            (Modifiers.PRIVATE, EXTRACT_ALL_METHOD_NAME, null,
 +             new TypeDesc[] {TypeDesc.forClass(ResultSet.class), TypeDesc.INT});
 +        CodeBuilder b = new CodeBuilder(mi);
 +
 +        defineExtract(b, b.getParameter(0), b.getParameter(1), null,
 +                      mInfo.getPrimaryKeyProperties().values(), lobLoaderMap);
 +
 +        // Invoke extract data method to do the rest.
 +        b.loadThis();
 +        // Load the ResultSet var.
 +        b.loadLocal(b.getParameter(0));
 +        // The offset variable has already been incremented by code generated
 +        // by defineExtract, except for the last property.
 +        b.loadLocal(b.getParameter(1));
 +        b.loadConstant(1);
 +        b.math(Opcode.IADD);
 +        b.loadNull(); // No Lobs to update
 +        b.invokePrivate(EXTRACT_DATA_METHOD_NAME, null,
 +                        new TypeDesc[] {TypeDesc.forClass(ResultSet.class), TypeDesc.INT,
 +                                        TypeDesc.forClass(Lob.class).toArrayType()});
 +
 +        b.returnVoid();
 +    }
 +
 +    private void defineExtractDataMethod(Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap) {
 +        MethodInfo mi = mClassFile.addMethod
 +            (Modifiers.PRIVATE, EXTRACT_DATA_METHOD_NAME, null,
 +             new TypeDesc[] {TypeDesc.forClass(ResultSet.class), TypeDesc.INT,
 +                             TypeDesc.forClass(Lob.class).toArrayType()});
 +        CodeBuilder b = new CodeBuilder(mi);
 +        defineExtract(b, b.getParameter(0), b.getParameter(1), b.getParameter(2),
 +                      mInfo.getDataProperties().values(), lobLoaderMap);
 +        b.returnVoid();
 +    }
 +
 +    private void defineExtract
 +        (CodeBuilder b,
 +         LocalVariable rsVar, LocalVariable initialOffsetVar, LocalVariable lobArrayVar,
 +         Iterable<JDBCStorableProperty<S>> properties,
 +         Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap)
 +    {
 +        LocalVariable offsetVar = null;
 +        int lobIndex = 0;
 +
 +        for (JDBCStorableProperty<S> property : properties) {
 +            if (!property.isSelectable()) {
 +                continue;
 +            }
 +
 +            // Push this in preparation for calling setXxx method.
 +            b.loadThis();
 +
 +            b.loadLocal(rsVar);
 +            if (offsetVar == null) {
 +                offsetVar = initialOffsetVar;
 +            } else {
 +                b.integerIncrement(offsetVar, 1);
 +            }
 +            b.loadLocal(offsetVar);
 +            Method resultSetGetMethod = property.getResultSetGetMethod();
 +            b.invoke(resultSetGetMethod);
 +
 +            TypeDesc resultSetType = TypeDesc.forClass(resultSetGetMethod.getReturnType());
 +
 +            Label wasNull = b.createLabel();
 +            if (resultSetType.isPrimitive() && property.isNullable()) {
 +                b.loadLocal(rsVar);
 +                b.invokeInterface
 +                    (TypeDesc.forClass(ResultSet.class), "wasNull", TypeDesc.BOOLEAN, null);
 +                Label wasNotNull = b.createLabel();
 +                // boolean value is false (==0) when was not null.
 +                b.ifZeroComparisonBranch(wasNotNull, "==");
 +
 +                // Discard result and replace with null.
 +                if (resultSetType.isDoubleWord()) {
 +                    b.pop2();
 +                } else {
 +                    b.pop();
 +                }
 +                b.loadNull();
 +                b.branch(wasNull);
 +
 +                wasNotNull.setLocation();
 +            }
 +
 +            if (Lob.class.isAssignableFrom(property.getType()) ||
 +                java.sql.Blob.class.isAssignableFrom(resultSetType.toClass()) ||
 +                java.sql.Clob.class.isAssignableFrom(resultSetType.toClass())) {
 +
 +                // Run special conversion and then lie about the result set type.
 +
 +                boolean isClob =
 +                    com.amazon.carbonado.lob.Clob.class.isAssignableFrom(property.getType()) ||
 +                    java.sql.Clob.class.isAssignableFrom(resultSetType.toClass());
 +
 +                String lobTypeName = isClob ? "Clob" : "Blob";
 +
 +                Method convertMethod;
 +                try {
 +                    String loaderName =
 +                        "com.amazon.carbonado.repo.jdbc.JDBC" + lobTypeName + "Loader";
 +                    convertMethod = JDBCSupport.class.getMethod
 +                        ("convert".concat(lobTypeName),
 +                         resultSetType.toClass(), Class.forName(loaderName));
 +                } catch (ClassNotFoundException e) {
 +                    throw new UndeclaredThrowableException(e);
 +                } catch (NoSuchMethodException e) {
 +                    throw new UndeclaredThrowableException(e);
 +                }
 +
 +                pushJDBCSupport(b);
 +                b.swap();
 +
 +                // Instantiate loader, which may be used later to reload the
 +                // lob. Loader is passed to convert method, where it is saved
 +                // inside the converted lob for future use.
 +                TypeDesc lobLoaderType = TypeDesc.forClass(lobLoaderMap.get(property));
 +                b.newObject(lobLoaderType);
 +                b.dup();
 +                b.loadThis();
 +                b.invokeConstructor(lobLoaderType, new TypeDesc[] {mClassFile.getType()});
 +
 +                b.invoke(convertMethod);
 +                resultSetType = TypeDesc.forClass(convertMethod.getReturnType());
 +
 +                if (lobArrayVar != null) {
 +                    // Add code to check if Lob needs to be updated.
 +                    b.loadLocal(lobArrayVar);
 +                    Label noUpdateLob = b.createLabel();
 +                    b.ifNullBranch(noUpdateLob, true);
 +
 +                    b.loadLocal(lobArrayVar);
 +                    b.loadConstant(lobIndex);
 +                    b.loadFromArray(TypeDesc.OBJECT);
 +                    b.ifNullBranch(noUpdateLob, true);
 +
 +                    // The Lob in the array represents the new value. What is
 +                    // currently on the stack (as converted above) is the old
 +                    // value currently in the database. Call the JDBCRepository
 +                    // updateXlob method, which stuffs the new blob contents
 +                    // into the old blob, thus updating it.
 +
 +                    TypeDesc lobType = TypeDesc.forClass(convertMethod.getReturnType());
 +                    LocalVariable lob = b.createLocalVariable(null, lobType);
 +                    b.storeLocal(lob);
 +
 +                    pushJDBCSupport(b);
 +                    b.loadLocal(lob);
 +
 +                    b.loadLocal(lobArrayVar);
 +                    b.loadConstant(lobIndex);
 +                    b.loadFromArray(TypeDesc.OBJECT);
 +                    b.checkCast(lobType);
 +
 +                    TypeDesc[] params = {lobType, lobType};
 +                    b.invokeInterface(TypeDesc.forClass(JDBCSupport.class),
 +                                      "update".concat(lobTypeName), null, params);
 +
 +                    // Lob content now updated.
 +                    b.loadLocal(lob);
 +
 +                    noUpdateLob.setLocation();
 +
 +                    lobIndex++;
 +                }
 +            }
 +
 +            TypeDesc superType = TypeDesc.forClass(mClassFile.getSuperClassName());
 +
 +            StorablePropertyAdapter adapter = property.getAppliedAdapter();
 +            if (adapter == null) {
 +                TypeDesc propertyType = TypeDesc.forClass(property.getType());
 +                b.convert(resultSetType, propertyType);
 +                wasNull.setLocation();
 +                // Set protected field directly, since no adapter.
 +                b.storeField(superType, property.getName(), propertyType);
 +            } else {
 +                Method adaptMethod = adapter.findAdaptMethod
 +                    (resultSetType.toClass(), property.getType());
 +                TypeDesc adaptType = TypeDesc.forClass(adaptMethod.getParameterTypes()[0]);
 +                b.convert(resultSetType, adaptType);
 +                wasNull.setLocation();
 +                // Invoke special inherited protected method that invokes the
 +                // adapter and sets the field. Method was generated by StorableGenerator.
 +                b.invokeVirtual(superType,
 +                                property.getWriteMethodName() + '$',
 +                                null, new TypeDesc[] {adaptType});
 +            }
 +        }
 +    }
 +
 +    /**
 +     * @param b builder to receive statement
 +     * @param withVersion when false, ignore any version property
 +     * @return version property number, or -1 if none
 +     */
 +    private int createInsertStatement(StringBuilder b, boolean withVersion) {
 +        b.append("INSERT INTO ");
 +        b.append(mInfo.getQualifiedTableName());
 +        b.append(" (");
 +
 +        JDBCStorableProperty<?> versionProperty = null;
 +        int versionPropNumber = -1;
 +
 +        int ordinal = 0;
 +        int propNumber = -1;
 +        for (JDBCStorableProperty<?> property : mInfo.getAllProperties().values()) {
 +            propNumber++;
 +            if (!property.isSelectable()) {
 +                continue;
 +            }
 +            if (property.isVersion()) {
 +                if (withVersion) {
 +                    versionProperty = property;
 +                    versionPropNumber = propNumber;
 +                }
 +                continue;
 +            }
 +            if (ordinal > 0) {
 +                b.append(',');
 +            }
 +            b.append(property.getColumnName());
 +            ordinal++;
 +        }
 +
 +        // Insert version property at end, to make things easier for when the
 +        // proper insert statement is selected.
 +        if (versionProperty != null) {
 +            if (ordinal > 0) {
 +                b.append(',');
 +            }
 +            b.append(versionProperty.getColumnName());
 +            ordinal++;
 +        }
 +
 +        b.append(") VALUES (");
 +
 +        for (int i=0; i<ordinal; i++) {
 +            if (i > 0) {
 +                b.append(',');
 +            }
 +            b.append('?');
 +        }
 +
 +        b.append(')');
 +
 +        return versionPropNumber;
 +    }
 +
 +    private Map<JDBCStorableProperty<S>, Class<?>> generateLobLoaders() {
 +        Map<JDBCStorableProperty<S>, Class<?>> lobLoaderMap =
 +            new IdentityHashMap<JDBCStorableProperty<S>, Class<?>>();
 +
 +        for (JDBCStorableProperty<S> property : mAllProperties.values()) {
 +            if (!property.isSelectable() || property.isVersion()) {
 +                continue;
 +            }
 +
 +            Class psClass = property.getPreparedStatementSetMethod().getParameterTypes()[1];
 +
 +            Class<?> lobLoader;
 +
 +            if (com.amazon.carbonado.lob.Blob.class.isAssignableFrom(property.getType()) ||
 +                java.sql.Blob.class.isAssignableFrom(psClass)) {
 +
 +                lobLoader = generateLobLoader(property, JDBCBlobLoader.class);
 +            } else if (com.amazon.carbonado.lob.Clob.class.isAssignableFrom(property.getType()) ||
 +                       java.sql.Clob.class.isAssignableFrom(psClass)) {
 +
 +                lobLoader = generateLobLoader(property, JDBCClobLoader.class);
 +            } else {
 +                continue;
 +            }
 +
 +            lobLoaderMap.put(property, lobLoader);
 +        }
 +
 +        return lobLoaderMap;
 +    }
 +
 +    /**
 +     * Generates an inner class conforming to JDBCBlobLoader or JDBCClobLoader.
 +     *
 +     * @param loaderType either JDBCBlobLoader or JDBCClobLoader
 +     */
 +    private Class<?> generateLobLoader(JDBCStorableProperty<S> property, Class<?> loaderType) {
 +        ClassInjector ci = ClassInjector.create
 +            (property.getEnclosingType().getName(), mParentClassLoader);
 +
 +        ClassFile cf = new ClassFile(ci.getClassName());
 +        cf.markSynthetic();
 +        cf.setSourceFile(JDBCStorableGenerator.class.getName());
 +        cf.setTarget("1.5");
 +        cf.addInterface(loaderType);
 +
 +        boolean isClob = loaderType == JDBCClobLoader.class;
 +
 +        final TypeDesc jdbcRepoType = TypeDesc.forClass(JDBCRepository.class);
 +        final TypeDesc resultSetType = TypeDesc.forClass(ResultSet.class);
 +        final TypeDesc preparedStatementType = TypeDesc.forClass(PreparedStatement.class);
 +        final TypeDesc sqlLobType = TypeDesc.forClass
 +            (isClob ? java.sql.Clob.class : java.sql.Blob.class);
 +
 +        final String enclosingFieldName = "enclosing";
 +        final TypeDesc enclosingType = mClassFile.getType();
 +
 +        cf.addField(Modifiers.PRIVATE, enclosingFieldName, enclosingType);
 +
 +        // Add constructor that accepts reference to enclosing storable.
 +        {
 +            MethodInfo mi = cf.addConstructor(Modifiers.PUBLIC, new TypeDesc[] {enclosingType});
 +            CodeBuilder b = new CodeBuilder(mi);
 +            b.loadThis();
 +            b.invokeSuperConstructor(null);
 +            b.loadThis();
 +            b.loadLocal(b.getParameter(0));
 +            b.storeField(enclosingFieldName, enclosingType);
 +            b.returnVoid();
 +        }
 +
 +        MethodInfo mi = cf.addMethod
 +            (Modifiers.PUBLIC, "load", sqlLobType, new TypeDesc[] {jdbcRepoType});
 +        mi.addException(TypeDesc.forClass(FetchException.class));
 +        CodeBuilder b = new CodeBuilder(mi);
 +
 +        LocalVariable repoVar = b.getParameter(0);
 +
 +        Label tryBeforeCon = b.createLabel().setLocation();
 +        LocalVariable conVar = getConnection(b, repoVar);
 +        Label tryAfterCon = b.createLabel().setLocation();
 +
 +        StringBuilder selectBuilder = new StringBuilder();
 +        selectBuilder.append("SELECT ");
 +        selectBuilder.append(property.getColumnName());
 +        selectBuilder.append(" FROM ");
 +        selectBuilder.append(mInfo.getQualifiedTableName());
 +
 +        LocalVariable psVar = b.createLocalVariable("ps", preparedStatementType);
 +
 +        LocalVariable instanceVar = b.createLocalVariable(null, enclosingType);
 +        b.loadThis();
 +        b.loadField(enclosingFieldName, enclosingType);
 +        b.storeLocal(instanceVar);
 +
 +        Label tryAfterPs = buildWhereClauseAndPreparedStatement
 +            (b, selectBuilder, conVar, psVar, repoVar, instanceVar);
 +
 +        b.loadLocal(psVar);
 +        b.invokeInterface(preparedStatementType, "executeQuery", resultSetType, null);
 +        LocalVariable rsVar = b.createLocalVariable("rs", resultSetType);
 +        b.storeLocal(rsVar);
 +        Label tryAfterRs = b.createLabel().setLocation();
 +
 +        // If no results, then return null. Otherwise, there must be exactly
 +        // one result.
 +
 +        LocalVariable resultVar = b.createLocalVariable(null, sqlLobType);
 +        b.loadNull();
 +        b.storeLocal(resultVar);
 +
 +        b.loadLocal(rsVar);
 +        b.invokeInterface(resultSetType, "next", TypeDesc.BOOLEAN, null);
 +        Label noResults = b.createLabel();
 +        b.ifZeroComparisonBranch(noResults, "==");
 +
 +        b.loadLocal(rsVar);
 +        b.loadConstant(1);
 +        b.invokeInterface(resultSetType, isClob ? "getClob" : "getBlob",
 +                          sqlLobType, new TypeDesc[] {TypeDesc.INT});
 +        b.storeLocal(resultVar);
 +
 +        noResults.setLocation();
 +
 +        closeResultSet(b, rsVar, tryAfterRs);
 +        closeStatement(b, psVar, tryAfterPs);
 +        yieldConAndHandleException(b, repoVar, tryBeforeCon, conVar, tryAfterCon, false);
 +
 +        b.loadLocal(resultVar);
 +        b.returnValue(sqlLobType);
 +
 +        return ci.defineClass(cf);
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableInfo.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableInfo.java new file mode 100644 index 0000000..6275c09 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableInfo.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.repo.jdbc;
 +
 +import java.util.Map;
 +
 +import com.amazon.carbonado.capability.IndexInfo;
 +import com.amazon.carbonado.Storable;
 +
 +import com.amazon.carbonado.info.StorableInfo;
 +
 +/**
 + * Contains all the metadata describing a specific {@link Storable} type as
 + * needed by JDBCRepository. It extends the regular {@link StorableInfo} with
 + * information gathered from the database.
 + *
 + * @author Brian S O'Neill
 + * @see JDBCStorableIntrospector
 + */
 +public interface JDBCStorableInfo<S extends Storable> extends StorableInfo<S> {
 +    /**
 +     * Returns false only if storable type is {@link com.amazon.carbonado.Independent independent}
 +     * and no matching table was found.
 +     */
 +    boolean isSupported();
 +
 +    /**
 +     * Returns the optional catalog name for the Storable. Some databases use a
 +     * catalog name to fully qualify the table name.
 +     */
 +    String getCatalogName();
 +
 +    /**
 +     * Returns the optional schema name for the Storable. Some databases use a
 +     * schema name to fully qualify the table name.
 +     */
 +    String getSchemaName();
 +
 +    /**
 +     * Returns the table name for the Storable or null if unsupported.
 +     */
 +    String getTableName();
 +
 +    /**
 +     * Returns the qualified table name for the Storable or null if
 +     * unsupported. Is used by SQL statements.
 +     */
 +    String getQualifiedTableName();
 +
 +    IndexInfo[] getIndexInfo();
 +
 +    Map<String, JDBCStorableProperty<S>> getAllProperties();
 +
 +    Map<String, JDBCStorableProperty<S>> getPrimaryKeyProperties();
 +
 +    Map<String, JDBCStorableProperty<S>> getDataProperties();
 +
 +    JDBCStorableProperty<S> getVersionProperty();
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java new file mode 100644 index 0000000..7ea8ef4 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java @@ -0,0 +1,1365 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.io.IOException;
 +import java.lang.reflect.Method;
 +import java.lang.reflect.UndeclaredThrowableException;
 +import java.util.ArrayList;
 +import java.util.Collections;
 +import java.util.Date;
 +import java.util.LinkedHashMap;
 +import java.util.List;
 +import java.util.HashMap;
 +import java.util.Map;
 +import java.util.Set;
 +import java.util.TreeMap;
 +
 +import java.math.BigDecimal;
 +
 +import java.sql.Connection;
 +import java.sql.DatabaseMetaData;
 +import java.sql.PreparedStatement;
 +import java.sql.ResultSet;
 +import java.sql.SQLException;
 +import static java.sql.Types.*;
 +
 +import javax.sql.DataSource;
 +
 +import org.apache.commons.logging.Log;
 +import org.apache.commons.logging.LogFactory;
 +
 +import org.cojen.classfile.TypeDesc;
 +import org.cojen.util.KeyFactory;
 +import org.cojen.util.SoftValuedHashMap;
 +
 +import com.amazon.carbonado.capability.IndexInfo;
 +import com.amazon.carbonado.MalformedTypeException;
 +import com.amazon.carbonado.MismatchException;
 +import com.amazon.carbonado.RepositoryException;
 +import com.amazon.carbonado.Storable;
 +import com.amazon.carbonado.SupportException;
 +
 +import com.amazon.carbonado.info.Direction;
 +import com.amazon.carbonado.info.OrderedProperty;
 +import com.amazon.carbonado.info.StorableInfo;
 +import com.amazon.carbonado.info.StorableIntrospector;
 +import com.amazon.carbonado.info.StorableIndex;
 +import com.amazon.carbonado.info.StorableKey;
 +import com.amazon.carbonado.info.StorableProperty;
 +import com.amazon.carbonado.info.StorablePropertyAdapter;
 +import com.amazon.carbonado.info.StorablePropertyConstraint;
 +
 +import com.amazon.carbonado.spi.IndexInfoImpl;
 +
 +/**
 + * Provides additional metadata for a {@link Storable} type needed by
 + * JDBCRepository. The storable type must match to a table in an external
 + * database. All examined data is cached, so repeat examinations are fast,
 + * unless the examination failed.
 + *
 + * @author Brian S O'Neill
 + */
 +public class JDBCStorableIntrospector extends StorableIntrospector {
 +    // Maps compound keys to softly referenced JDBCStorableInfo objects.
 +    @SuppressWarnings("unchecked")
 +    private static Map<Object, JDBCStorableInfo<?>> cCache = new SoftValuedHashMap();
 +
 +    /**
 +     * Examines the given class and returns a JDBCStorableInfo describing it. A
 +     * MalformedTypeException is thrown for a variety of reasons if the given
 +     * class is not a well-defined Storable type or if it can't match up with
 +     * an entity in the external database.
 +     *
 +     * @param type Storable type to examine
 +     * @param ds source of JDBC connections to use for matching to a table
 +     * @param catalog optional catalog to search
 +     * @param schema optional schema to search
 +     * @throws MalformedTypeException if Storable type is not well-formed
 +     * @throws RepositoryException if there was a problem in accessing the database
 +     * @throws IllegalArgumentException if type is null
 +     */
 +    @SuppressWarnings("unchecked")
 +    public static <S extends Storable> JDBCStorableInfo<S> examine
 +        (Class<S> type, DataSource ds, String catalog, String schema)
 +        throws SQLException, SupportException
 +    {
 +        Object key = KeyFactory.createKey(new Object[] {type, ds, catalog, schema});
 +
 +        synchronized (cCache) {
 +            JDBCStorableInfo<S> jInfo = (JDBCStorableInfo<S>) cCache.get(key);
 +            if (jInfo != null) {
 +                return jInfo;
 +            }
 +
 +            // Call superclass for most info.
 +            StorableInfo<S> mainInfo = examine(type);
 +            Connection con = ds.getConnection();
 +            try {
 +                jInfo = examine(mainInfo, con, catalog, schema);
 +            } finally {
 +                try {
 +                    con.close();
 +                } catch (SQLException e) {
 +                    // Don't care.
 +                }
 +            }
 +
 +            cCache.put(key, jInfo);
 +
 +            // Finish resolving join properties, after properties have been
 +            // added to cache. This makes it possible for joins to (directly or
 +            // indirectly) reference their own enclosing type.
 +            for (JDBCStorableProperty<S> jProperty : jInfo.getAllProperties().values()) {
 +                ((JProperty<S>) jProperty).fillInternalJoinElements(ds, catalog, schema);
 +                ((JProperty<S>) jProperty).fillExternalJoinElements(ds, catalog, schema);
 +            }
 +
 +            return jInfo;
 +        }
 +    }
 +
 +    /**
 +     * Uses the given database connection to query database metadata. This is
 +     * used to bind storables to tables, and properties to columns. Other
 +     * checks are performed to ensure that storable type matches well with the
 +     * definition in the database.
 +     */
 +    private static <S extends Storable> JDBCStorableInfo<S> examine
 +        (StorableInfo<S> mainInfo, Connection con,
 +         final String searchCatalog, final String searchSchema)
 +        throws SQLException, SupportException
 +    {
 +        DatabaseMetaData meta = con.getMetaData();
 +
 +        final String databaseProductName = meta.getDatabaseProductName();
 +        final String userName = meta.getUserName();
 +
 +        String[] tableAliases;
 +        if (mainInfo.getAliasCount() > 0) {
 +            tableAliases = mainInfo.getAliases();
 +        } else {
 +            String name = mainInfo.getStorableType().getName();
 +            int index = name.lastIndexOf('.');
 +            if (index >= 0) {
 +                name = name.substring(index + 1);
 +            }
 +            tableAliases = generateAliases(name);
 +        }
 +
 +        // Try to find matching table from aliases.
 +        String catalog = null, schema = null, tableName = null, tableType = null;
 +        findName: {
 +            // The call to getTables may return several matching tables. This
 +            // map defines the "best" table type we'd like to use. The higher
 +            // the number the better.
 +            Map<String, Integer> fitnessMap = new HashMap<String, Integer>();
 +            fitnessMap.put("LOCAL TEMPORARY", 1);
 +            fitnessMap.put("GLOBAL TEMPORARY", 2);
 +            fitnessMap.put("VIEW", 3);
 +            fitnessMap.put("SYSTEM TABLE", 4);
 +            fitnessMap.put("TABLE", 5);
 +            fitnessMap.put("ALIAS", 6);
 +            fitnessMap.put("SYNONYM", 7);
 +
 +            for (int i=0; i<tableAliases.length; i++) {
 +                ResultSet rs = meta.getTables(searchCatalog, searchSchema, tableAliases[i], null);
 +                try {
 +                    int bestFitness = 0;
 +                    while (rs.next()) {
 +                        String type = rs.getString("TABLE_TYPE");
 +                        Integer fitness = fitnessMap.get(type);
 +                        if (fitness != null) {
 +                            String rsSchema = rs.getString("TABLE_SCHEM");
 +
 +                            if (searchSchema == null) {
 +                                if (userName != null && userName.equalsIgnoreCase(rsSchema)) {
 +                                    // Favor entities whose schema name matches
 +                                    // the user name.
 +                                    fitness += 7;
 +                                }
 +                            }
 +
 +                            if (fitness > bestFitness) {
 +                                bestFitness = fitness;
 +                                catalog = rs.getString("TABLE_CAT");
 +                                schema = rsSchema;
 +                                tableName = rs.getString("TABLE_NAME");
 +                                tableType = type;
 +                            }
 +                        }
 +                    }
 +                } finally {
 +                    rs.close();
 +                }
 +            }
 +        }
 +
 +        if (tableName == null && !mainInfo.isIndependent()) {
 +            StringBuilder buf = new StringBuilder();
 +            buf.append("Unable to find matching table name for type \"");
 +            buf.append(mainInfo.getStorableType().getName());
 +            buf.append("\" by looking for ");
 +            appendToSentence(buf, tableAliases);
 +            buf.append(" with catalog " + searchCatalog + " and schema " + searchSchema);
 +            throw new MismatchException(buf.toString());
 +        }
 +
 +        String qualifiedTableName = tableName;
 +        String resolvedTableName = tableName;
 +
 +        // Oracle specific stuff...
 +        // TODO: Migrate this to OracleSupportStrategy.
 +        if (tableName != null && databaseProductName.toUpperCase().contains("ORACLE")) {
 +            if ("TABLE".equals(tableType) && searchSchema != null) {
 +                // Qualified table name references the schema. Used by SQL statements.
 +                qualifiedTableName = searchSchema + '.' + tableName;
 +            } else if ("SYNONYM".equals(tableType)) {
 +                // Try to get the real schema. This call is Oracle specific, however.
 +                String select = "SELECT TABLE_OWNER,TABLE_NAME " +
 +                    "FROM ALL_SYNONYMS " +
 +                    "WHERE OWNER=? AND SYNONYM_NAME=?";
 +                PreparedStatement ps = con.prepareStatement(select);
 +                ps.setString(1, schema); // in Oracle, schema is the owner
 +                ps.setString(2, tableName);
 +                try {
 +                    ResultSet rs = ps.executeQuery();
 +                    try {
 +                        if (rs.next()) {
 +                            schema = rs.getString("TABLE_OWNER");
 +                            resolvedTableName = rs.getString("TABLE_NAME");
 +                        }
 +                    } finally {
 +                        rs.close();
 +                    }
 +                } finally {
 +                    ps.close();
 +                }
 +            }
 +        }
 +
 +        // Gather information on all columns such that metadata only needs to
 +        // be retrieved once.
 +        Map<String, ColumnInfo> columnMap =
 +            new TreeMap<String, ColumnInfo>(String.CASE_INSENSITIVE_ORDER);
 +
 +        if (resolvedTableName != null) {
 +            ResultSet rs = meta.getColumns(catalog, schema, resolvedTableName, null);
 +            try {
 +                while (rs.next()) {
 +                    ColumnInfo info = new ColumnInfo(rs);
 +                    columnMap.put(info.columnName, info);
 +                }
 +            } finally {
 +                rs.close();
 +            }
 +        }
 +
 +        // Make sure that all properties have a corresponding column.
 +        Map<String, ? extends StorableProperty<S>> mainProperties = mainInfo.getAllProperties();
 +        Map<String, String> columnToProperty = new HashMap<String, String>();
 +        Map<String, JDBCStorableProperty<S>> jProperties =
 +            new LinkedHashMap<String, JDBCStorableProperty<S>>(mainProperties.size());
 +
 +        ArrayList<String> errorMessages = new ArrayList<String>();
 +
 +        for (StorableProperty<S> mainProperty : mainProperties.values()) {
 +            if (mainProperty.isJoin() || tableName == null) {
 +                jProperties.put(mainProperty.getName(), new JProperty<S>(mainProperty));
 +                continue;
 +            }
 +
 +            String[] columnAliases;
 +            if (mainProperty.getAliasCount() > 0) {
 +                columnAliases = mainProperty.getAliases();
 +            } else {
 +                columnAliases = generateAliases(mainProperty.getName());
 +            }
 +
 +            JDBCStorableProperty<S> jProperty = null;
 +            boolean addedError = false;
 +
 +            findName: for (int i=0; i<columnAliases.length; i++) {
 +                ColumnInfo columnInfo = columnMap.get(columnAliases[i]);
 +                if (columnInfo != null) {
 +                    AccessInfo accessInfo = getAccessInfo
 +                        (mainProperty,
 +                         columnInfo.dataType, columnInfo.dataTypeName,
 +                         columnInfo.columnSize, columnInfo.decimalDigits);
 +
 +                    if (accessInfo == null) {
 +                        TypeDesc propertyType = TypeDesc.forClass(mainProperty.getType());
 +                        String message =
 +                            "Property \"" + mainProperty.getName() +
 +                            "\" has type \"" + propertyType.getFullName() +
 +                            "\" which is incompatible with database type \"" +
 +                            columnInfo.dataTypeName + '"';
 +
 +                        if (columnInfo.decimalDigits > 0) {
 +                            message += " (decimal digits = " + columnInfo.decimalDigits + ')';
 +                        }
 +
 +                        errorMessages.add(message);
 +                        addedError = true;
 +                        break findName;
 +                    }
 +
 +                    if (columnInfo.nullable) {
 +                        if (!mainProperty.isNullable()) {
 +                            errorMessages.add
 +                                ("Property \"" + mainProperty.getName() +
 +                                 "\" must have a Nullable annotation");
 +                        }
 +                    } else {
 +                        if (mainProperty.isNullable()) {
 +                            errorMessages.add
 +                                ("Property \"" + mainProperty.getName() +
 +                                 "\" must not have a Nullable annotation");
 +                        }
 +                    }
 +
 +                    jProperty = new JProperty<S>(mainProperty, columnInfo,
 +                                                 accessInfo.mResultSetGet,
 +                                                 accessInfo.mPreparedStatementSet,
 +                                                 accessInfo.getAdapter());
 +
 +                    break findName;
 +                }
 +            }
 +
 +            if (jProperty != null) {
 +                jProperties.put(mainProperty.getName(), jProperty);
 +                columnToProperty.put(jProperty.getColumnName(), jProperty.getName());
 +            } else {
 +                if (mainProperty.isIndependent()) {
 +                    jProperties.put(mainProperty.getName(), new JProperty<S>(mainProperty));
 +                } else if (!addedError) {
 +                    StringBuilder buf = new StringBuilder();
 +                    buf.append("Unable to find matching database column for property \"");
 +                    buf.append(mainProperty.getName());
 +                    buf.append("\" by looking for ");
 +                    appendToSentence(buf, columnAliases);
 +                    errorMessages.add(buf.toString());
 +                }
 +            }
 +        }
 +
 +        if (errorMessages.size() > 0) {
 +            throw new MismatchException(errorMessages);
 +        }
 +
 +        // Gather index info...
 +        IndexInfo[] indexInfo;
 +        boolean hasIndexInfo = false;
 +
 +        gatherIndexInfo: {
 +            if (resolvedTableName == null) {
 +                indexInfo = new IndexInfo[0];
 +                break gatherIndexInfo;
 +            }
 +
 +            ResultSet rs;
 +            try {
 +                rs = meta.getIndexInfo(catalog, schema, resolvedTableName, false, true);
 +            } catch (SQLException e) {
 +                getLog().info
 +                    ("Unable to get index info for table \"" + resolvedTableName +
 +                     "\" with catalog " + catalog + " and schema " + schema + ": " + e);
 +                indexInfo = new IndexInfo[0];
 +                break gatherIndexInfo;
 +            }
 +
 +            List<IndexInfo> infoList = new ArrayList<IndexInfo>();
 +
 +            try {
 +                String indexName = null;
 +                boolean unique = false;
 +                boolean clustered = false;
 +                List<String> indexProperties = new ArrayList<String>();
 +                List<Direction> directions = new ArrayList<Direction>();
 +
 +                while (rs.next()) {
 +                    if (rs.getInt("TYPE") == DatabaseMetaData.tableIndexStatistic) {
 +                        // Ignore this type.
 +                        continue;
 +                    }
 +
 +                    String propertyName = columnToProperty.get(rs.getString("COLUMN_NAME"));
 +                    if (propertyName == null) {
 +                        // Ignore indexes on unknown columns.
 +                        continue;
 +                    }
 +
 +                    String nextName = rs.getString("INDEX_NAME");
 +
 +                    if (indexName != null && !indexName.equals(nextName)) {
 +                        infoList.add(new IndexInfoImpl(indexName, unique, clustered,
 +                                                       indexProperties.toArray(new String[0]),
 +                                                       directions.toArray(new Direction[0])));
 +                        indexProperties.clear();
 +                        directions.clear();
 +                    }
 +
 +                    indexName = nextName;
 +                    unique = !rs.getBoolean("NON_UNIQUE");
 +                    clustered = rs.getInt("TYPE") == DatabaseMetaData.tableIndexClustered;
 +
 +                    String ascOrDesc = rs.getString("ASC_OR_DESC");
 +                    Direction direction = Direction.UNSPECIFIED;
 +                    if ("A".equals(ascOrDesc)) {
 +                        direction = Direction.ASCENDING;
 +                    } else if ("D".equals(ascOrDesc)) {
 +                        direction = Direction.DESCENDING;
 +                    }
 +
 +                    indexProperties.add(propertyName);
 +                    directions.add(direction);
 +                }
 +
 +                if (indexProperties.size() > 0) {
 +                    infoList.add(new IndexInfoImpl(indexName, unique, clustered,
 +                                                   indexProperties.toArray(new String[0]),
 +                                                   directions.toArray(new Direction[0])));
 +                }
 +            } finally {
 +                rs.close();
 +            }
 +
 +            indexInfo = infoList.toArray(new IndexInfo[0]);
 +            hasIndexInfo = true;
 +        }
 +
 +        // Now verify that primary keys match.
 +
 +        // As primary keys are found, remove from columnToProperty map.
 +
 +        if (resolvedTableName != null) checkPrimaryKey: {
 +            ResultSet rs;
 +            try {
 +                rs = meta.getPrimaryKeys(catalog, schema, resolvedTableName);
 +            } catch (SQLException e) {
 +                getLog().info
 +                    ("Unable to get primary keys for table \"" + resolvedTableName +
 +                     "\" with catalog " + catalog + " and schema " + schema + ": " + e);
 +                break checkPrimaryKey;
 +            }
 +
 +            try {
 +                while (rs.next()) {
 +                    String columnName = rs.getString("COLUMN_NAME");
 +                    String propertyName = columnToProperty.remove(columnName);
 +                    StorableProperty mainProperty = mainProperties.get(propertyName);
 +
 +                    if (!mainProperty.isPrimaryKeyMember()) {
 +                        errorMessages.add
 +                            ("Property \"" + propertyName +
 +                             "\" must have a PrimaryKey annotation");
 +                    }
 +                }
 +            } finally {
 +                rs.close();
 +            }
 +
 +            // All remaining properties must not have a primary key annotation.
 +            for (String propertyName : columnToProperty.values()) {
 +                StorableProperty mainProperty = mainProperties.get(propertyName);
 +
 +                if (mainProperty.isPrimaryKeyMember()) {
 +                    errorMessages.add
 +                        ("Property \"" + propertyName + "\" cannot have a PrimaryKey annotation");
 +                }
 +            }
 +        }
 +
 +        // Verify AlternateKey annotations are backed by unique indexes. Unlike
 +        // for primary keys, there is no requirement that unique indexes must
 +        // have an AlternateKey annotation.
 +        if (hasIndexInfo) {
 +            // Note the deep nesting of loops. I hope the index sets are small.
 +
 +            int altKeyCount = mainInfo.getAlternateKeyCount();
 +            altKeyScan:
 +            for (int i=altKeyCount; --i>=0; ) {
 +                StorableKey<S> altKey = mainInfo.getAlternateKey(i);
 +                Set<? extends OrderedProperty<S>> altKeyProps = altKey.getProperties();
 +                indexMatch:
 +                for (int j=indexInfo.length; --j>=0; ) {
 +                    IndexInfo ii = indexInfo[j];
 +                    if (ii.isUnique()) {
 +                        String[] indexPropNames = ii.getPropertyNames();
 +                        if (indexPropNames.length == altKeyProps.size()) {
 +                            propertyMatch:
 +                            for (OrderedProperty<S> orderedProp : altKeyProps) {
 +                                StorableProperty<S> altKeyProp =
 +                                    orderedProp.getChainedProperty().getPrimeProperty();
 +                                String keyPropName = altKeyProp.getName();
 +                                for (int k=indexPropNames.length; --k>=0; ) {
 +                                    if (indexPropNames[k].equals(keyPropName)) {
 +                                        // This property matched...
 +                                        continue propertyMatch;
 +                                    }
 +                                }
 +                                // Didn't match a property, so move on to next index.
 +                                continue indexMatch;
 +                            }
 +                            // Fully matched an index, move on to next alt key.
 +                            continue altKeyScan;
 +                        }
 +                    }
 +                }
 +                // No indexes match, so error.
 +                StringBuilder buf = new StringBuilder();
 +                buf.append("No matching unique index for alternate key: ");
 +                try {
 +                    altKey.appendTo(buf);
 +                } catch (IOException e) {
 +                    // Not gonna happen.
 +                }
 +                errorMessages.add(buf.toString());
 +            }
 +        }
 +
 +        if (errorMessages.size() > 0) {
 +            throw new MismatchException(errorMessages);
 +        }
 +
 +        return new JInfo<S>
 +            (mainInfo, catalog, schema, tableName, qualifiedTableName, indexInfo, jProperties);
 +    }
 +
 +    private static Log getLog() {
 +        return LogFactory.getLog(JDBCStorableIntrospector.class);
 +    }
 +
 +    /**
 +     * Figures out how to best access the given property, or returns null if
 +     * not supported. An adapter may be applied.
 +     *
 +     * @return null if not supported
 +     */
 +    private static AccessInfo getAccessInfo
 +        (StorableProperty property,
 +         int dataType, String dataTypeName, int columnSize, int decimalDigits)
 +    {
 +        AccessInfo info = getAccessInfo
 +            (property.getType(), dataType, dataTypeName, columnSize, decimalDigits);
 +        if (info != null) {
 +            return info;
 +        }
 +
 +        // See if an appropriate adapter exists.
 +        StorablePropertyAdapter adapter = property.getAdapter();
 +        if (adapter != null) {
 +            Method[] toMethods = adapter.findAdaptMethodsTo(property.getType());
 +            for (Method toMethod : toMethods) {
 +                Class fromType = toMethod.getParameterTypes()[0];
 +                // Verify that reverse adapt method exists as well...
 +                if (adapter.findAdaptMethod(property.getType(), fromType) != null) {
 +                    // ...and try to get access info for fromType.
 +                    info = getAccessInfo
 +                        (fromType, dataType, dataTypeName, columnSize, decimalDigits);
 +                    if (info != null) {
 +                        info.setAdapter(adapter);
 +                        return info;
 +                    }
 +                }
 +            }
 +        }
 +
 +        return null;
 +    }
 +
 +    /**
 +     * Figures out how to best access the given property, or returns null if
 +     * not supported. An adapter is not be applied.
 +     *
 +     * @return null if not supported
 +     */
 +    private static AccessInfo getAccessInfo
 +        (Class desiredClass,
 +         int dataType, String dataTypeName, int columnSize, int decimalDigits)
 +    {
 +        if (!desiredClass.isPrimitive()) {
 +            TypeDesc desiredType = TypeDesc.forClass(desiredClass);
 +            if (desiredType.toPrimitiveType() != null) {
 +                desiredType = desiredType.toPrimitiveType();
 +                desiredClass = desiredType.toClass();
 +            }
 +        }
 +
 +        Class actualClass;
 +        String suffix;
 +
 +        switch (dataType) {
 +        default:
 +            return null;
 +
 +        case BIT:
 +        case BOOLEAN:
 +            if (desiredClass == boolean.class) {
 +                actualClass = boolean.class;
 +                suffix = "Boolean";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case TINYINT:
 +            if (desiredClass == byte.class) {
 +                actualClass = byte.class;
 +                suffix = "Byte";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case SMALLINT:
 +            if (desiredClass == short.class) {
 +                actualClass = short.class;
 +                suffix = "Short";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case INTEGER:
 +            if (desiredClass == int.class) {
 +                actualClass = int.class;
 +                suffix = "Int";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case BIGINT:
 +            if (desiredClass == long.class) {
 +                actualClass = long.class;
 +                suffix = "Long";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case FLOAT:
 +            if (desiredClass == float.class) {
 +                actualClass = float.class;
 +                suffix = "Float";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case DOUBLE:
 +        case REAL:
 +            if (desiredClass == double.class) {
 +                actualClass = double.class;
 +                suffix = "Double";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case NUMERIC:
 +        case DECIMAL:
 +            if (desiredClass == int.class) {
 +                if (decimalDigits == 0) {
 +                    actualClass = int.class;
 +                    suffix = "Int";
 +                } else {
 +                    return null;
 +                }
 +            } else if (desiredClass == long.class) {
 +                if (decimalDigits == 0) {
 +                    actualClass = long.class;
 +                    suffix = "Long";
 +                } else {
 +                    return null;
 +                }
 +            } else if (desiredClass == double.class) {
 +                actualClass = double.class;
 +                suffix = "Double";
 +            } else if (desiredClass == BigDecimal.class) {
 +                actualClass = BigDecimal.class;
 +                suffix = "BigDecimal";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case CHAR:
 +        case VARCHAR:
 +        case LONGVARCHAR:
 +            if (desiredClass == String.class) {
 +                actualClass = String.class;
 +                suffix = "String";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case DATE:
 +            // Treat Date as a Timestamp since some databases make no
 +            // distinction.  The DateTimeAdapter can be used to provide
 +            // more control over the desired precision.
 +            if (desiredClass == Date.class || desiredClass == java.sql.Date.class) {
 +                actualClass = java.sql.Timestamp.class;
 +                suffix = "Timestamp";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case TIME:
 +            if (desiredClass == Date.class || desiredClass == java.sql.Time.class) {
 +                actualClass = java.sql.Time.class;
 +                suffix = "Time";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case TIMESTAMP:
 +            if (desiredClass == Date.class || desiredClass == java.sql.Timestamp.class) {
 +                actualClass = java.sql.Timestamp.class;
 +                suffix = "Timestamp";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case BINARY:
 +        case VARBINARY:
 +        case LONGVARBINARY:
 +            if (desiredClass == byte[].class) {
 +                actualClass = byte[].class;
 +                suffix = "Bytes";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case BLOB:
 +            if (desiredClass == com.amazon.carbonado.lob.Blob.class) {
 +                actualClass = java.sql.Blob.class;
 +                suffix = "Blob";
 +            } else {
 +                return null;
 +            }
 +            break;
 +
 +        case CLOB:
 +            if (desiredClass == com.amazon.carbonado.lob.Clob.class) {
 +                actualClass = java.sql.Clob.class;
 +                suffix = "Clob";
 +            } else {
 +                return null;
 +            }
 +            break;
 +        }
 +
 +        return new AccessInfo(suffix, actualClass);
 +    }
 +
 +    /**
 +     * Appends words to a sentence as an "or" list.
 +     */
 +    private static void appendToSentence(StringBuilder buf, String[] names) {
 +        for (int i=0; i<names.length; i++) {
 +            if (i > 0) {
 +                if (i + 1 >= names.length) {
 +                    buf.append(" or ");
 +                } else {
 +                    buf.append(", ");
 +                }
 +            }
 +            buf.append('"');
 +            buf.append(names[i]);
 +            buf.append('"');
 +        }
 +    }
 +
 +    /**
 +     * Generates aliases for the given name, converting camel case form into
 +     * various underscore forms.
 +     */
 +    static String[] generateAliases(String base) {
 +        int length = base.length();
 +        if (length <= 1) {
 +            return new String[]{base.toUpperCase(), base.toLowerCase()};
 +        }
 +
 +        ArrayList<String> aliases = new ArrayList<String>(4);
 +
 +        StringBuilder buf = new StringBuilder();
 +
 +        int i;
 +        for (i=0; i<length; ) {
 +            char c = base.charAt(i++);
 +            if (c == '_' || !Character.isJavaIdentifierPart(c)) {
 +                // Keep scanning for first letter.
 +                buf.append(c);
 +            } else {
 +                buf.append(Character.toUpperCase(c));
 +                break;
 +            }
 +        }
 +
 +        boolean canSeparate = false;
 +        boolean appendedIdentifierPart = false;
 +
 +        for (; i<length; i++) {
 +            char c = base.charAt(i);
 +            if (c == '_' || !Character.isJavaIdentifierPart(c)) {
 +                canSeparate = false;
 +                appendedIdentifierPart = false;
 +            } else if (Character.isLowerCase(c)) {
 +                canSeparate = true;
 +                appendedIdentifierPart = true;
 +            } else {
 +                if (appendedIdentifierPart &&
 +                    i + 1 < length && Character.isLowerCase(base.charAt(i + 1))) {
 +                    canSeparate = true;
 +                }
 +                if (canSeparate) {
 +                    buf.append('_');
 +                }
 +                canSeparate = false;
 +                appendedIdentifierPart = true;
 +            }
 +            buf.append(c);
 +        }
 +
 +        String derived = buf.toString();
 +
 +        addToSet(aliases, derived.toUpperCase());
 +        addToSet(aliases, derived.toLowerCase());
 +        addToSet(aliases, derived);
 +        addToSet(aliases, base);
 +
 +        return aliases.toArray(new String[aliases.size()]);
 +    }
 +
 +    private static void addToSet(ArrayList<String> list, String value) {
 +        if (!list.contains(value)) {
 +            list.add(value);
 +        }
 +    }
 +
 +    static String intern(String str) {
 +        return str == null ? null : str.intern();
 +    }
 +
 +    private static class ColumnInfo {
 +        final String columnName;
 +        final int dataType;
 +        final String dataTypeName;
 +        final int columnSize;
 +        final int decimalDigits;
 +        final boolean nullable;
 +        final int charOctetLength;
 +        final int ordinalPosition;
 +
 +        ColumnInfo(ResultSet rs) throws SQLException {
 +            columnName = intern(rs.getString("COLUMN_NAME"));
 +            dataTypeName = intern(rs.getString("TYPE_NAME"));
 +            columnSize = rs.getInt("COLUMN_SIZE");
 +            decimalDigits = rs.getInt("DECIMAL_DIGITS");
 +            nullable = rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable;
 +            charOctetLength = rs.getInt("CHAR_OCTET_LENGTH");
 +            ordinalPosition = rs.getInt("ORDINAL_POSITION");
 +
 +            int dt = rs.getInt("DATA_TYPE");
 +            if (dt == OTHER) {
 +                if ("BLOB".equalsIgnoreCase(dataTypeName)) {
 +                    dt = BLOB;
 +                } else if ("CLOB".equalsIgnoreCase(dataTypeName)) {
 +                    dt = CLOB;
 +                } else if ("FLOAT".equalsIgnoreCase(dataTypeName)) {
 +                    dt = FLOAT;
 +                } else if ("TIMESTAMP".equalsIgnoreCase(dataTypeName)) {
 +                    dt = TIMESTAMP;
 +                } else if (dataTypeName.toUpperCase().contains("TIMESTAMP")) {
 +                    dt = TIMESTAMP;
 +                }
 +            }
 +
 +            dataType = dt;
 +        }
 +    }
 +
 +    private static class AccessInfo {
 +        // ResultSet get method, never null.
 +        final Method mResultSetGet;
 +
 +        // PreparedStatement set method, never null.
 +        final Method mPreparedStatementSet;
 +
 +        // Is null if no adapter needed.
 +        private StorablePropertyAdapter mAdapter;
 +
 +        AccessInfo(String suffix, Class actualClass) {
 +            try {
 +                mResultSetGet = ResultSet.class.getMethod("get" + suffix, int.class);
 +                mPreparedStatementSet = PreparedStatement.class.getMethod
 +                    ("set" + suffix, int.class, actualClass);
 +            } catch (NoSuchMethodException e) {
 +                throw new UndeclaredThrowableException(e);
 +            }
 +        }
 +
 +        StorablePropertyAdapter getAdapter() {
 +            return mAdapter;
 +        }
 +
 +        void setAdapter(StorablePropertyAdapter adapter) {
 +            mAdapter = adapter;
 +        }
 +    }
 +
 +    /**
 +     * Implementation of JDBCStorableInfo. The 'J' prefix is just a shorthand
 +     * to disambiguate the class name.
 +     */
 +    private static class JInfo<S extends Storable> implements JDBCStorableInfo<S> {
 +        private final StorableInfo<S> mMainInfo;
 +        private final String mCatalogName;
 +        private final String mSchemaName;
 +        private final String mTableName;
 +        private final String mQualifiedTableName;
 +        private final IndexInfo[] mIndexInfo;
 +        private final Map<String, JDBCStorableProperty<S>> mAllProperties;
 +
 +        private transient Map<String, JDBCStorableProperty<S>> mPrimaryKeyProperties;
 +        private transient Map<String, JDBCStorableProperty<S>> mDataProperties;
 +        private transient JDBCStorableProperty<S> mVersionProperty;
 +
 +        JInfo(StorableInfo<S> mainInfo,
 +              String catalogName, String schemaName, String tableName, String qualifiedTableName,
 +              IndexInfo[] indexInfo,
 +              Map<String, JDBCStorableProperty<S>> allProperties)
 +        {
 +            mMainInfo = mainInfo;
 +            mCatalogName = intern(catalogName);
 +            mSchemaName = intern(schemaName);
 +            mTableName = intern(tableName);
 +            mQualifiedTableName = intern(qualifiedTableName);
 +            mIndexInfo = indexInfo;
 +            mAllProperties = Collections.unmodifiableMap(allProperties);
 +        }
 +
 +        public String getName() {
 +            return mMainInfo.getName();
 +        }
 +
 +        public Class<S> getStorableType() {
 +            return mMainInfo.getStorableType();
 +        }
 +
 +        public StorableKey<S> getPrimaryKey() {
 +            return mMainInfo.getPrimaryKey();
 +        }
 +
 +        public int getAlternateKeyCount() {
 +            return mMainInfo.getAlternateKeyCount();
 +        }
 +
 +        public StorableKey<S> getAlternateKey(int index) {
 +            return mMainInfo.getAlternateKey(index);
 +        }
 +
 +        public StorableKey<S>[] getAlternateKeys() {
 +            return mMainInfo.getAlternateKeys();
 +        }
 +
 +        public int getAliasCount() {
 +            return mMainInfo.getAliasCount();
 +        }
 +
 +        public String getAlias(int index) {
 +            return mMainInfo.getAlias(index);
 +        }
 +
 +        public String[] getAliases() {
 +            return mMainInfo.getAliases();
 +        }
 +
 +        public int getIndexCount() {
 +            return mMainInfo.getIndexCount();
 +        }
 +
 +        public StorableIndex<S> getIndex(int index) {
 +            return mMainInfo.getIndex(index);
 +        }
 +
 +        public StorableIndex<S>[] getIndexes() {
 +            return mMainInfo.getIndexes();
 +        }
 +
 +        public boolean isIndependent() {
 +            return mMainInfo.isIndependent();
 +        }
 +
 +        public boolean isSupported() {
 +            return mTableName != null;
 +        }
 +
 +        public String getCatalogName() {
 +            return mCatalogName;
 +        }
 +
 +        public String getSchemaName() {
 +            return mSchemaName;
 +        }
 +
 +        public String getTableName() {
 +            return mTableName;
 +        }
 +
 +        public String getQualifiedTableName() {
 +            return mQualifiedTableName;
 +        }
 +
 +        public IndexInfo[] getIndexInfo() {
 +            return mIndexInfo.clone();
 +        }
 +
 +        public Map<String, JDBCStorableProperty<S>> getAllProperties() {
 +            return mAllProperties;
 +        }
 +
 +        public Map<String, JDBCStorableProperty<S>> getPrimaryKeyProperties() {
 +            if (mPrimaryKeyProperties == null) {
 +                Map<String, JDBCStorableProperty<S>> pkProps =
 +                    new LinkedHashMap<String, JDBCStorableProperty<S>>(mAllProperties.size());
 +                for (Map.Entry<String, JDBCStorableProperty<S>> entry : mAllProperties.entrySet()){
 +                    JDBCStorableProperty<S> property = entry.getValue();
 +                    if (property.isPrimaryKeyMember()) {
 +                        pkProps.put(entry.getKey(), property);
 +                    }
 +                }
 +                mPrimaryKeyProperties = Collections.unmodifiableMap(pkProps);
 +            }
 +            return mPrimaryKeyProperties;
 +        }
 +
 +        public Map<String, JDBCStorableProperty<S>> getDataProperties() {
 +            if (mDataProperties == null) {
 +                Map<String, JDBCStorableProperty<S>> dataProps =
 +                    new LinkedHashMap<String, JDBCStorableProperty<S>>(mAllProperties.size());
 +                for (Map.Entry<String, JDBCStorableProperty<S>> entry : mAllProperties.entrySet()){
 +                    JDBCStorableProperty<S> property = entry.getValue();
 +                    if (!property.isPrimaryKeyMember() && !property.isJoin()) {
 +                        dataProps.put(entry.getKey(), property);
 +                    }
 +                }
 +                mDataProperties = Collections.unmodifiableMap(dataProps);
 +            }
 +            return mDataProperties;
 +        }
 +
 +        public JDBCStorableProperty<S> getVersionProperty() {
 +            if (mVersionProperty == null) {
 +                for (JDBCStorableProperty<S> property : mAllProperties.values()) {
 +                    if (property.isVersion()) {
 +                        mVersionProperty = property;
 +                        break;
 +                    }
 +                }
 +            }
 +            return mVersionProperty;
 +        }
 +    }
 +
 +    /**
 +     * Implementation of JDBCStorableProperty. The 'J' prefix is just a
 +     * shorthand to disambiguate the class name.
 +     */
 +    private static class JProperty<S extends Storable> implements JDBCStorableProperty<S> {
 +        private final StorableProperty<S> mMainProperty;
 +        private final String mColumnName;
 +        private final Integer mDataType;
 +        private final String mDataTypeName;
 +        private final Method mResultSetGet;
 +        private final Method mPreparedStatementSet;
 +        private final StorablePropertyAdapter mAdapter;
 +        private final Integer mColumnSize;
 +        private final Integer mDecimalDigits;
 +        private final Integer mCharOctetLength;
 +        private final Integer mOrdinalPosition;
 +
 +        private JDBCStorableProperty<S>[] mInternal;
 +        private JDBCStorableProperty<?>[] mExternal;
 +
 +        /**
 +         * Join properties need to be filled in later.
 +         */
 +        JProperty(StorableProperty<S> mainProperty, ColumnInfo columnInfo,
 +                  Method resultSetGet, Method preparedStatementSet,
 +                  StorablePropertyAdapter adapter) {
 +            mMainProperty = mainProperty;
 +            mColumnName = columnInfo.columnName;
 +            mDataType = columnInfo.dataType;
 +            mDataTypeName = columnInfo.dataTypeName;
 +            mResultSetGet = resultSetGet;
 +            mPreparedStatementSet = preparedStatementSet;
 +            mAdapter = adapter;
 +            mColumnSize = columnInfo.columnSize;
 +            mDecimalDigits = columnInfo.decimalDigits;
 +            mCharOctetLength = columnInfo.charOctetLength;
 +            mOrdinalPosition = columnInfo.ordinalPosition;
 +        }
 +
 +        JProperty(StorableProperty<S> mainProperty) {
 +            mMainProperty = mainProperty;
 +            mColumnName = null;
 +            mDataType = null;
 +            mDataTypeName = null;
 +            mResultSetGet = null;
 +            mPreparedStatementSet = null;
 +            mAdapter = null;
 +            mColumnSize = null;
 +            mDecimalDigits = null;
 +            mCharOctetLength = null;
 +            mOrdinalPosition = null;
 +        }
 +
 +        public String getName() {
 +            return mMainProperty.getName();
 +        }
 +
 +        public Class<?> getType() {
 +            return mMainProperty.getType();
 +        }
 +
 +        public Class<S> getEnclosingType() {
 +            return mMainProperty.getEnclosingType();
 +        }
 +
 +        public Method getReadMethod() {
 +            return mMainProperty.getReadMethod();
 +        }
 +
 +        public String getReadMethodName() {
 +            return mMainProperty.getReadMethodName();
 +        }
 +
 +        public Method getWriteMethod() {
 +            return mMainProperty.getWriteMethod();
 +        }
 +
 +        public String getWriteMethodName() {
 +            return mMainProperty.getWriteMethodName();
 +        }
 +
 +        public boolean isNullable() {
 +            return mMainProperty.isNullable();
 +        }
 +
 +        public boolean isPrimaryKeyMember() {
 +            return mMainProperty.isPrimaryKeyMember();
 +        }
 +
 +        public boolean isAlternateKeyMember() {
 +            return mMainProperty.isAlternateKeyMember();
 +        }
 +
 +        public int getAliasCount() {
 +            return mMainProperty.getAliasCount();
 +        }
 +
 +        public String getAlias(int index) {
 +            return mMainProperty.getAlias(index);
 +        }
 +
 +        public String[] getAliases() {
 +            return mMainProperty.getAliases();
 +        }
 +
 +        public boolean isJoin() {
 +            return mMainProperty.isJoin();
 +        }
 +
 +        public Class<? extends Storable> getJoinedType() {
 +            return mMainProperty.getJoinedType();
 +        }
 +
 +        public int getJoinElementCount() {
 +            return mMainProperty.getJoinElementCount();
 +        }
 +
 +        public boolean isQuery() {
 +            return mMainProperty.isQuery();
 +        }
 +
 +        public int getConstraintCount() {
 +            return mMainProperty.getConstraintCount();
 +        }
 +
 +        public StorablePropertyConstraint getConstraint(int index) {
 +            return mMainProperty.getConstraint(index);
 +        }
 +
 +        public StorablePropertyConstraint[] getConstraints() {
 +            return mMainProperty.getConstraints();
 +        }
 +
 +        public StorablePropertyAdapter getAdapter() {
 +            return mMainProperty.getAdapter();
 +        }
 +
 +        public String getSequenceName() {
 +            return mMainProperty.getSequenceName();
 +        }
 +
 +        public boolean isVersion() {
 +            return mMainProperty.isVersion();
 +        }
 +
 +        public boolean isIndependent() {
 +            return mMainProperty.isIndependent();
 +        }
 +
 +        public boolean isSupported() {
 +            if (isJoin()) {
 +                // TODO: Check if joined type is supported
 +                return true;
 +            } else {
 +                return mColumnName != null;
 +            }
 +        }
 +
 +        public boolean isSelectable() {
 +            return mColumnName != null && !isJoin();
 +        }
 +
 +        public String getColumnName() {
 +            return mColumnName;
 +        }
 +
 +        public Integer getDataType() {
 +            return mDataType;
 +        }
 +
 +        public String getDataTypeName() {
 +            return mDataTypeName;
 +        }
 +
 +        public Method getResultSetGetMethod() {
 +            return mResultSetGet;
 +        }
 +
 +        public Method getPreparedStatementSetMethod() {
 +            return mPreparedStatementSet;
 +        }
 +
 +        public StorablePropertyAdapter getAppliedAdapter() {
 +            return mAdapter;
 +        }
 +
 +        public Integer getColumnSize() {
 +            return mColumnSize;
 +        }
 +
 +        public Integer getDecimalDigits() {
 +            return mDecimalDigits;
 +        }
 +
 +        public Integer getCharOctetLength() {
 +            return mCharOctetLength;
 +        }
 +
 +        public Integer getOrdinalPosition() {
 +            return mOrdinalPosition;
 +        }
 +
 +        public JDBCStorableProperty<S> getInternalJoinElement(int index) {
 +            if (mInternal == null) {
 +                throw new IndexOutOfBoundsException();
 +            }
 +            return mInternal[index];
 +        }
 +
 +        @SuppressWarnings("unchecked")
 +        public JDBCStorableProperty<S>[] getInternalJoinElements() {
 +            if (mInternal == null) {
 +                return new JDBCStorableProperty[0];
 +            }
 +            return mInternal.clone();
 +        }
 +
 +        public JDBCStorableProperty<?> getExternalJoinElement(int index) {
 +            if (mExternal == null) {
 +                throw new IndexOutOfBoundsException();
 +            }
 +            return mExternal[index];
 +        }
 +
 +        public JDBCStorableProperty<?>[] getExternalJoinElements() {
 +            if (mExternal == null) {
 +                return new JDBCStorableProperty[0];
 +            }
 +            return mExternal.clone();
 +        }
 +
 +        public String toString() {
 +            return mMainProperty.toString();
 +        }
 +
 +        public void appendTo(Appendable app) throws IOException {
 +            mMainProperty.appendTo(app);
 +        }
 +
 +        @SuppressWarnings("unchecked")
 +        void fillInternalJoinElements(DataSource ds, String catalog, String schema)
 +            throws SQLException, SupportException
 +        {
 +            StorableProperty<S>[] mainInternal = mMainProperty.getInternalJoinElements();
 +            if (mainInternal.length == 0) {
 +                mInternal = null;
 +                return;
 +            }
 +
 +            JDBCStorableInfo<S> info = examine(getEnclosingType(), ds, catalog, schema);
 +
 +            JDBCStorableProperty<S>[] internal = new JDBCStorableProperty[mainInternal.length];
 +            for (int i=mainInternal.length; --i>=0; ) {
 +                internal[i] = info.getAllProperties().get(mainInternal[i].getName());
 +            }
 +            mInternal = internal;
 +        }
 +
 +        void fillExternalJoinElements(DataSource ds, String catalog, String schema)
 +            throws SQLException, SupportException
 +        {
 +            StorableProperty<?>[] mainExternal = mMainProperty.getExternalJoinElements();
 +            if (mainExternal.length == 0) {
 +                mExternal = null;
 +                return;
 +            }
 +
 +            JDBCStorableInfo<?> info = examine(getJoinedType(), ds, catalog, schema);
 +
 +            JDBCStorableProperty<?>[] external = new JDBCStorableProperty[mainExternal.length];
 +            for (int i=mainExternal.length; --i>=0; ) {
 +                external[i] = info.getAllProperties().get(mainExternal[i].getName());
 +            }
 +            mExternal = external;
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableProperty.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableProperty.java new file mode 100644 index 0000000..b900e69 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableProperty.java @@ -0,0 +1,129 @@ +/*
 + * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
 + * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
 + * of Amazon Technologies, Inc. or its affiliates.  All rights reserved.
 + *
 + * Licensed under the Apache License, Version 2.0 (the "License");
 + * you may not use this file except in compliance with the License.
 + * You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +
 +package com.amazon.carbonado.repo.jdbc;
 +
 +import java.lang.reflect.Method;
 +
 +import com.amazon.carbonado.Storable;
 +
 +import com.amazon.carbonado.info.StorableProperty;
 +import com.amazon.carbonado.info.StorablePropertyAdapter;
 +
 +/**
 + * Contains all the metadata describing a property of a specific {@link
 + * Storable} type as needed by JDBCRepository.
 + *
 + * @author Brian S O'Neill
 + * @see JDBCStorableIntrospector
 + */
 +public interface JDBCStorableProperty<S extends Storable> extends StorableProperty<S> {
 +    /**
 +     * Returns false only if property is independent and no matching column was
 +     * found.
 +     */
 +    boolean isSupported();
 +
 +    /**
 +     * Returns true if property is both supported and not a join. Simply put,
 +     * it can appear in a select statement.
 +     */
 +    boolean isSelectable();
 +
 +    /**
 +     * Returns the table column for this property.
 +     *
 +     * @return null if property is unsupported
 +     */
 +    String getColumnName();
 +
 +    /**
 +     * Returns the data type as defined by {@link java.sql.Types}.
 +     *
 +     * @return null if property is unsupported
 +     */
 +    Integer getDataType();
 +
 +    /**
 +     * Returns the data type name.
 +     *
 +     * @return null if property is unsupported
 +     */
 +    String getDataTypeName();
 +
 +    /**
 +     * Returns the method to use to access this property (by index) from a
 +     * ResultSet.
 +     *
 +     * @return null if property is unsupported
 +     */
 +    Method getResultSetGetMethod();
 +
 +    /**
 +     * Returns the method to use to set this property (by index) into a
 +     * PreparedStatement.
 +     *
 +     * @return null if property is unsupported
 +     */
 +    Method getPreparedStatementSetMethod();
 +
 +    /**
 +     * Returns the adapter that needs to be applied to properties returned from
 +     * ResultSets and set into PreparedStatements. Is null if not needed.
 +     *
 +     * @return null if property is unsupported or if not needed.
 +     */
 +    StorablePropertyAdapter getAppliedAdapter();
 +
 +    /**
 +     * The column size is either the maximum number of characters or the
 +     * numeric precision.
 +     *
 +     * @return null if property is unsupported
 +     */
 +    Integer getColumnSize();
 +
 +    /**
 +     * Returns the amount of fractional decimal digits.
 +     *
 +     * @return null if property is unsupported
 +     */
 +    Integer getDecimalDigits();
 +
 +    /**
 +     * Returns the maximum amount of bytes for property value.
 +     *
 +     * @return null if property is unsupported
 +     */
 +    Integer getCharOctetLength();
 +
 +    /**
 +     * Returns the one-based index of the column in the table.
 +     *
 +     * @return null if property is unsupported
 +     */
 +    Integer getOrdinalPosition();
 +
 +    JDBCStorableProperty<S> getInternalJoinElement(int index);
 +
 +    JDBCStorableProperty<S>[] getInternalJoinElements();
 +
 +    JDBCStorableProperty<?> getExternalJoinElement(int index);
 +
 +    JDBCStorableProperty<?>[] getExternalJoinElements();
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java new file mode 100644 index 0000000..99c4bea --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java @@ -0,0 +1,1129 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.sql.Connection;
 +import java.sql.PreparedStatement;
 +import java.sql.ResultSet;
 +import java.sql.SQLException;
 +
 +import java.io.IOException;
 +
 +import java.lang.reflect.Method;
 +import java.lang.reflect.UndeclaredThrowableException;
 +
 +import java.util.ArrayList;
 +import java.util.List;
 +import java.util.Map;
 +import java.util.LinkedHashMap;
 +
 +import org.apache.commons.logging.LogFactory;
 +
 +import com.amazon.carbonado.Cursor;
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.PersistException;
 +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.capability.IndexInfo;
 +
 +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.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.StorableProperty;
 +import com.amazon.carbonado.info.StorablePropertyAdapter;
 +
 +import com.amazon.carbonado.spi.BaseQuery;
 +import com.amazon.carbonado.spi.BaseQueryCompiler;
 +import com.amazon.carbonado.spi.SequenceValueProducer;
 +import com.amazon.carbonado.spi.TriggerManager;
 +
 +import com.amazon.carbonado.util.QuickConstructorGenerator;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCStorage<S extends Storable> extends BaseQueryCompiler<S>
 +    implements Storage<S>, JDBCSupport<S>
 +{
 +    private static final String TABLE_ALIAS_PREFIX = "T";
 +    private static final int FIRST_RESULT_INDEX = 1;
 +
 +    final JDBCRepository mRepository;
 +    final JDBCSupportStrategy mSupportStrategy;
 +    final JDBCStorableInfo<S> mInfo;
 +    final InstanceFactory mInstanceFactory;
 +
 +    final TriggerManager<S> mTriggerManager;
 +
 +    JDBCStorage(JDBCRepository repository, JDBCStorableInfo<S> info)
 +        throws SupportException
 +    {
 +        super(info);
 +        mRepository = repository;
 +        mSupportStrategy = repository.getSupportStrategy();
 +        mInfo = info;
 +
 +        Class<? extends S> generatedStorableClass = JDBCStorableGenerator.getGeneratedClass(info);
 +        mInstanceFactory = QuickConstructorGenerator
 +            .getInstance(generatedStorableClass, InstanceFactory.class);
 +
 +        mTriggerManager = new TriggerManager<S>();
 +    }
 +
 +    public Class<S> getStorableType() {
 +        return mInfo.getStorableType();
 +    }
 +
 +    public S prepare() {
 +        return (S) mInstanceFactory.instantiate(this);
 +    }
 +
 +    public Query<S> query() throws FetchException {
 +        return getCompiledQuery();
 +    }
 +
 +    public Query<S> query(String filter) throws FetchException {
 +        return getCompiledQuery(filter);
 +    }
 +
 +    public Query<S> query(Filter<S> filter) throws FetchException {
 +        return getCompiledQuery(filter);
 +    }
 +
 +    public JDBCRepository getJDBCRepository() {
 +        return mRepository;
 +    }
 +
 +    public Repository getRootRepository() {
 +        return mRepository.getRootRepository();
 +    }
 +
 +    public boolean isPropertySupported(String propertyName) {
 +        JDBCStorableProperty<S> property = mInfo.getAllProperties().get(propertyName);
 +        return property != null && property.isSupported();
 +    }
 +
 +    public boolean addTrigger(Trigger<? super S> trigger) {
 +        return mTriggerManager.addTrigger(trigger);
 +    }
 +
 +    public boolean removeTrigger(Trigger<? super S> trigger) {
 +        return mTriggerManager.removeTrigger(trigger);
 +    }
 +
 +    public IndexInfo[] getIndexInfo() {
 +        return mInfo.getIndexInfo();
 +    }
 +
 +    public SequenceValueProducer getSequenceValueProducer(String name) throws PersistException {
 +        return mSupportStrategy.getSequenceValueProducer(name);
 +    }
 +
 +    public Trigger<? super S> getInsertTrigger() {
 +        return mTriggerManager.getInsertTrigger();
 +    }
 +
 +    public Trigger<? super S> getUpdateTrigger() {
 +        return mTriggerManager.getUpdateTrigger();
 +    }
 +
 +    public Trigger<? super S> getDeleteTrigger() {
 +        return mTriggerManager.getDeleteTrigger();
 +    }
 +
 +    /**
 +     * @param loader used to reload Blob outside original transaction
 +     */
 +    public com.amazon.carbonado.lob.Blob convertBlob(java.sql.Blob blob, JDBCBlobLoader loader)
 +        throws FetchException
 +    {
 +        JDBCBlob jblob = mSupportStrategy.convertBlob(blob, loader);
 +
 +        if (jblob != null) {
 +            try {
 +                JDBCTransaction txn = mRepository.openTransactionManager().getTxn();
 +                if (txn != null) {
 +                    txn.register(jblob);
 +                }
 +            } catch (Exception e) {
 +                throw mRepository.toFetchException(e);
 +            }
 +        }
 +
 +        return jblob;
 +    }
 +
 +    /**
 +     * @param loader used to reload Clob outside original transaction
 +     */
 +    public com.amazon.carbonado.lob.Clob convertClob(java.sql.Clob clob, JDBCClobLoader loader)
 +        throws FetchException
 +    {
 +        JDBCClob jclob = mSupportStrategy.convertClob(clob, loader);
 +
 +        if (jclob != null) {
 +            try {
 +                JDBCTransaction txn = mRepository.openTransactionManager().getTxn();
 +                if (txn != null) {
 +                    txn.register(jclob);
 +                }
 +            } catch (Exception e) {
 +                throw mRepository.toFetchException(e);
 +            }
 +        }
 +
 +        return jclob;
 +    }
 +
 +    /**
 +     * @return original blob if too large and post-insert update is required, null otherwise
 +     * @throws PersistException instead of FetchException since this code is
 +     * called during an insert operation
 +     */
 +    public com.amazon.carbonado.lob.Blob setBlobValue(PreparedStatement ps, int column,
 +                                                      com.amazon.carbonado.lob.Blob blob)
 +        throws PersistException
 +    {
 +        return mSupportStrategy.setBlobValue(ps, column, blob);
 +    }
 +
 +    /**
 +     * @return original clob if too large and post-insert update is required, null otherwise
 +     * @throws PersistException instead of FetchException since this code is
 +     * called during an insert operation
 +     */
 +    public com.amazon.carbonado.lob.Clob setClobValue(PreparedStatement ps, int column,
 +                                                      com.amazon.carbonado.lob.Clob clob)
 +        throws PersistException
 +    {
 +        return mSupportStrategy.setClobValue(ps, column, clob);
 +    }
 +
 +    public void updateBlob(com.amazon.carbonado.lob.Blob oldBlob,
 +                           com.amazon.carbonado.lob.Blob newBlob)
 +        throws PersistException
 +    {
 +        mSupportStrategy.updateBlob(oldBlob, newBlob);
 +    }
 +
 +    public void updateClob(com.amazon.carbonado.lob.Clob oldClob,
 +                           com.amazon.carbonado.lob.Clob newClob)
 +        throws PersistException
 +    {
 +        mSupportStrategy.updateClob(oldClob, newClob);
 +    }
 +
 +    protected JDBCStorableInfo<S> getStorableInfo() {
 +        return mInfo;
 +    }
 +
 +    protected Query<S> compileQuery(FilterValues<S> values, OrderedProperty<S>[] orderings)
 +        throws FetchException, UnsupportedOperationException
 +    {
 +        JoinNode jn;
 +        try {
 +            JoinNodeBuilder jnb = new JoinNodeBuilder();
 +            if (values == null) {
 +                jn = new JoinNode(getStorableInfo(), null);
 +            } else {
 +                values.getFilter().accept(jnb, null);
 +                jn = jnb.getRootJoinNode();
 +            }
 +            jnb.captureOrderings(orderings);
 +        } catch (UndeclaredThrowableException e) {
 +            throw mRepository.toFetchException(e);
 +        }
 +
 +        StatementBuilder selectBuilder = new StatementBuilder();
 +        selectBuilder.append("SELECT ");
 +
 +        // Don't bother using a table alias for one table. With just one table,
 +        // there's no need to disambiguate.
 +        String alias = jn.hasAnyJoins() ? jn.getAlias() : null;
 +
 +        Map<String, JDBCStorableProperty<S>> properties = getStorableInfo().getAllProperties();
 +        int ordinal = 0;
 +        for (JDBCStorableProperty<S> property : properties.values()) {
 +            if (!property.isSelectable()) {
 +                continue;
 +            }
 +            if (ordinal > 0) {
 +                selectBuilder.append(',');
 +            }
 +            if (alias != null) {
 +                selectBuilder.append(alias);
 +                selectBuilder.append('.');
 +            }
 +            selectBuilder.append(property.getColumnName());
 +            ordinal++;
 +        }
 +
 +        selectBuilder.append(" FROM");
 +
 +        StatementBuilder fromWhereBuilder = new StatementBuilder();
 +        fromWhereBuilder.append(" FROM");
 +
 +        if (alias == null) {
 +            // Don't bother defining a table alias for one table.
 +            jn.appendTableNameTo(selectBuilder);
 +            jn.appendTableNameTo(fromWhereBuilder);
 +        } else {
 +            jn.appendFullJoinTo(selectBuilder);
 +            jn.appendFullJoinTo(fromWhereBuilder);
 +        }
 +
 +        PropertyFilter<S>[] propertyFilters;
 +        boolean[] propertyFilterNullable;
 +
 +        if (values == null) {
 +            propertyFilters = null;
 +            propertyFilterNullable = null;
 +        } else {
 +            // Build the WHERE clause only if anything to filter on.
 +            selectBuilder.append(" WHERE ");
 +            fromWhereBuilder.append(" WHERE ");
 +
 +            WhereBuilder wb = new WhereBuilder(selectBuilder, alias == null ? null : jn);
 +            FetchException e = values.getFilter().accept(wb, null);
 +            if (e != null) {
 +                throw e;
 +            }
 +
 +            propertyFilters = wb.getPropertyFilters();
 +            propertyFilterNullable = wb.getPropertyFilterNullable();
 +
 +            wb = new WhereBuilder(fromWhereBuilder, alias == null ? null : jn);
 +            e = values.getFilter().accept(wb, null);
 +            if (e != null) {
 +                throw e;
 +            }
 +        }
 +
 +        // Append order-by clause.
 +        if (orderings != null && orderings.length != 0) {
 +            selectBuilder.append(" ORDER BY ");
 +            ordinal = 0;
 +            for (OrderedProperty<S> orderedProperty : orderings) {
 +                if (ordinal > 0) {
 +                    selectBuilder.append(',');
 +                }
 +                selectBuilder.appendColumn(alias == null ? null : jn,
 +                                           orderedProperty.getChainedProperty());
 +                if (orderedProperty.getDirection() == Direction.DESCENDING) {
 +                    selectBuilder.append(" DESC");
 +                }
 +                ordinal++;
 +            }
 +        }
 +
 +        try {
 +            CursorFactory factory = new CursorFactory(selectBuilder.build(),
 +                                                      fromWhereBuilder.build(),
 +                                                      propertyFilters,
 +                                                      propertyFilterNullable);
 +            return new JDBCQuery(factory, values, orderings);
 +        } catch (RepositoryException e) {
 +            throw mRepository.toFetchException(e);
 +        }
 +    }
 +
 +    public S instantiate(ResultSet rs) throws SQLException {
 +        return (S) mInstanceFactory.instantiate(this, rs, FIRST_RESULT_INDEX);
 +    }
 +
 +    public static interface InstanceFactory {
 +        Storable instantiate(JDBCSupport storage);
 +
 +        Storable instantiate(JDBCSupport storage, ResultSet rs, int offset) throws SQLException;
 +    }
 +
 +    private class CursorFactory {
 +        private final Statement<S> mSelectStatement;
 +        private final int mMaxSelectStatementLength;
 +        private final Statement<S> mFromWhereStatement;
 +        private final int mMaxFromWhereStatementLength;
 +
 +        // The following arrays all have the same length, or they may all be null.
 +
 +        private final PropertyFilter<S>[] mPropertyFilters;
 +        private final boolean[] mPropertyFilterNullable;
 +
 +        private final Method[] mPreparedStatementSetMethods;
 +
 +        // Some entries may be null if no adapter required.
 +        private final Method[] mAdapterMethods;
 +
 +        // Some entries may be null if no adapter required.
 +        private final Object[] mAdapterInstances;
 +
 +        CursorFactory(Statement<S> selectStatement,
 +                      Statement<S> fromWhereStatement,
 +                      PropertyFilter<S>[] propertyFilters,
 +                      boolean[] propertyFilterNullable)
 +            throws RepositoryException
 +        {
 +            mSelectStatement = selectStatement;
 +            mMaxSelectStatementLength = selectStatement.maxLength();
 +            mFromWhereStatement = fromWhereStatement;
 +            mMaxFromWhereStatementLength = fromWhereStatement.maxLength();
 +
 +            if (propertyFilters == null) {
 +                mPropertyFilters = null;
 +                mPropertyFilterNullable = null;
 +                mPreparedStatementSetMethods = null;
 +                mAdapterMethods = null;
 +                mAdapterInstances = null;
 +            } else {
 +                mPropertyFilters = propertyFilters;
 +                mPropertyFilterNullable = propertyFilterNullable;
 +
 +                int length = propertyFilters.length;
 +
 +                mPreparedStatementSetMethods = new Method[length];
 +                mAdapterMethods = new Method[length];
 +                mAdapterInstances = new Object[length];
 +
 +                gatherAdapterMethods(propertyFilters);
 +            }
 +        }
 +
 +        private void gatherAdapterMethods(PropertyFilter<S>[] filters)
 +            throws RepositoryException
 +        {
 +            for (int i=0; i<filters.length; i++) {
 +                PropertyFilter<S> filter = filters[i];
 +                ChainedProperty<S> chained = filter.getChainedProperty();
 +                StorableProperty<?> property = chained.getLastProperty();
 +                JDBCStorableProperty<?> jProperty =
 +                    mRepository.getJDBCStorableProperty(property);
 +
 +                Method psSetMethod = jProperty.getPreparedStatementSetMethod();
 +                mPreparedStatementSetMethods[i] = psSetMethod;
 +
 +                StorablePropertyAdapter adapter = jProperty.getAppliedAdapter();
 +                if (adapter != null) {
 +                    Class toType = psSetMethod.getParameterTypes()[1];
 +                    mAdapterMethods[i] = adapter.findAdaptMethod(jProperty.getType(), toType);
 +                    mAdapterInstances[i] = adapter.getAdapterInstance();
 +                }
 +            }
 +        }
 +
 +        JDBCCursor<S> openCursor(FilterValues<S> filterValues, boolean forUpdate)
 +            throws FetchException
 +        {
 +            Connection con = mRepository.getConnection();
 +            try {
 +                PreparedStatement ps =
 +                    con.prepareStatement(prepareSelect(filterValues, forUpdate));
 +
 +                setParameters(ps, filterValues);
 +                return new JDBCCursor<S>(JDBCStorage.this, con, ps);
 +            } catch (Exception e) {
 +                throw mRepository.toFetchException(e);
 +            }
 +        }
 +
 +        /**
 +         * Delete operation is included in cursor factory for ease of implementation.
 +         */
 +        int executeDelete(FilterValues<S> filterValues) throws PersistException {
 +            Connection con;
 +            try {
 +                con = mRepository.getConnection();
 +            } catch (FetchException e) {
 +                throw e.toPersistException();
 +            }
 +            try {
 +                PreparedStatement ps = con.prepareStatement(prepareDelete(filterValues));
 +                setParameters(ps, filterValues);
 +                return ps.executeUpdate();
 +            } catch (Exception e) {
 +                throw mRepository.toPersistException(e);
 +            } finally {
 +                try {
 +                    mRepository.yieldConnection(con);
 +                } catch (FetchException e) {
 +                    throw e.toPersistException();
 +                }
 +            }
 +        }
 +
 +        /**
 +         * Count operation is included in cursor factory for ease of implementation.
 +         */
 +        long executeCount(FilterValues<S> filterValues) throws FetchException {
 +            Connection con = mRepository.getConnection();
 +            try {
 +                PreparedStatement ps = con.prepareStatement(prepareCount(filterValues));
 +                setParameters(ps, filterValues);
 +                ResultSet rs = ps.executeQuery();
 +                try {
 +                    rs.next();
 +                    return rs.getLong(1);
 +                } finally {
 +                    rs.close();
 +                }
 +            } catch (Exception e) {
 +                throw mRepository.toFetchException(e);
 +            } finally {
 +                mRepository.yieldConnection(con);
 +            }
 +        }
 +
 +        String prepareSelect(FilterValues<S> filterValues, boolean forUpdate) {
 +            if (!forUpdate) {
 +                return mSelectStatement.buildStatement(mMaxSelectStatementLength, filterValues);
 +            }
 +
 +            // Allocate with extra room for " FOR UPDATE"
 +            StringBuilder b = new StringBuilder(mMaxSelectStatementLength + 11);
 +            mSelectStatement.appendTo(b, filterValues);
 +            b.append(" FOR UPDATE");
 +            return b.toString();
 +        }
 +
 +        String prepareDelete(FilterValues<S> filterValues) {
 +            // Allocate with extra room for "DELETE"
 +            StringBuilder b = new StringBuilder(6 + mMaxFromWhereStatementLength);
 +            b.append("DELETE");
 +            mFromWhereStatement.appendTo(b, filterValues);
 +            return b.toString();
 +        }
 +
 +        String prepareCount(FilterValues<S> filterValues) {
 +            // Allocate with extra room for "SELECT COUNT(*)"
 +            StringBuilder b = new StringBuilder(15 + mMaxFromWhereStatementLength);
 +            b.append("SELECT COUNT(*)");
 +            mFromWhereStatement.appendTo(b, filterValues);
 +            return b.toString();
 +        }
 +
 +        private void setParameters(PreparedStatement ps, FilterValues<S> filterValues)
 +            throws Exception
 +        {
 +            PropertyFilter<S>[] propertyFilters = mPropertyFilters;
 +
 +            if (propertyFilters == null) {
 +                return;
 +            }
 +
 +            boolean[] propertyFilterNullable = mPropertyFilterNullable;
 +            Method[] psSetMethods = mPreparedStatementSetMethods;
 +            Method[] adapterMethods = mAdapterMethods;
 +            Object[] adapterInstances = mAdapterInstances;
 +
 +            int ordinal = 0;
 +            int psOrdinal = 1; // Start at one since JDBC ordinals are one-based.
 +            for (PropertyFilter<S> filter : propertyFilters) {
 +                setValue: {
 +                    Object value = filterValues.getAssignedValue(filter);
 +
 +                    if (value == null && propertyFilterNullable[ordinal]) {
 +                        // No '?' parameter to fill since value "IS NULL" or "IS NOT NULL"
 +                        break setValue;
 +                    }
 +
 +                    Method adapter = adapterMethods[ordinal];
 +                    if (adapter != null) {
 +                        value = adapter.invoke(adapterInstances[ordinal], value);
 +                    }
 +
 +                    psSetMethods[ordinal].invoke(ps, psOrdinal, value);
 +                    psOrdinal++;
 +                }
 +
 +                ordinal++;
 +            }
 +        }
 +    }
 +
 +    private class JDBCQuery extends BaseQuery<S> {
 +        private final CursorFactory mCursorFactory;
 +
 +        JDBCQuery(CursorFactory factory,
 +                  FilterValues<S> values,
 +                  OrderedProperty<S>[] orderings)
 +        {
 +            super(mRepository, JDBCStorage.this, values, orderings);
 +            mCursorFactory = factory;
 +        }
 +
 +        JDBCQuery(CursorFactory factory,
 +                  FilterValues<S> values,
 +                  String[] orderings)
 +        {
 +            super(mRepository, JDBCStorage.this, values, orderings);
 +            mCursorFactory = factory;
 +        }
 +
 +        public Query<S> orderBy(String property)
 +            throws FetchException, UnsupportedOperationException
 +        {
 +            return JDBCStorage.this.getOrderedQuery(getFilterValues(), property);
 +        }
 +
 +        public Query<S> orderBy(String... properties)
 +            throws FetchException, UnsupportedOperationException
 +        {
 +            return JDBCStorage.this.getOrderedQuery(getFilterValues(), properties);
 +        }
 +
 +        public Cursor<S> fetch() throws FetchException {
 +            boolean forUpdate = mRepository.openTransactionManager().isForUpdate();
 +            return mCursorFactory.openCursor(getFilterValues(), forUpdate);
 +        }
 +
 +        public void deleteAll() throws PersistException {
 +            if (mTriggerManager.getDeleteTrigger() != null) {
 +                // Super implementation loads one at time and calls
 +                // delete. This allows delete trigger to be invoked on each.
 +                super.deleteAll();
 +            } else {
 +                mCursorFactory.executeDelete(getFilterValues());
 +            }
 +        }
 +
 +        public long count() throws FetchException {
 +            return mCursorFactory.executeCount(getFilterValues());
 +        }
 +
 +        public boolean printNative(Appendable app, int indentLevel) throws IOException {
 +            indent(app, indentLevel);
 +            boolean forUpdate = mRepository.openTransactionManager().isForUpdate();
 +            app.append(mCursorFactory.prepareSelect(getFilterValues(), forUpdate));
 +            app.append('\n');
 +            return true;
 +        }
 +
 +        public boolean printPlan(Appendable app, int indentLevel) throws IOException {
 +            try {
 +                boolean forUpdate = mRepository.openTransactionManager().isForUpdate();
 +                String statement = mCursorFactory.prepareSelect(getFilterValues(), forUpdate);
 +                return mRepository.getSupportStrategy().printPlan(app, indentLevel, statement);
 +            } catch (FetchException e) {
 +                LogFactory.getLog(JDBCStorage.class).error(null, e);
 +                return false;
 +            }
 +        }
 +
 +        protected BaseQuery<S> newInstance(FilterValues<S> values) {
 +            return new JDBCQuery(mCursorFactory, values, getOrderings());
 +        }
 +
 +        protected BaseQuery<S> cachedInstance(Filter<S> filter) throws FetchException {
 +            return (BaseQuery<S>) JDBCStorage.this.getCompiledQuery(filter);
 +        }
 +    }
 +
 +    /**
 +     * Node in a tree structure describing how tables are joined together.
 +     */
 +    private class JoinNode {
 +        // Joined property which led to this node. For root node, it is null.
 +        private final JDBCStorableProperty<?> mProperty;
 +
 +        private final JDBCStorableInfo<?> mInfo;
 +        private final String mAlias;
 +
 +        private final Map<String, JoinNode> mSubNodes;
 +
 +        /**
 +         * @param alias table alias in SQL statement, i.e. "T1"
 +         */
 +        JoinNode(JDBCStorableInfo<?> info, String alias) {
 +            this(null, info, alias);
 +        }
 +
 +        private JoinNode(JDBCStorableProperty<?> property, JDBCStorableInfo<?> info, String alias)
 +        {
 +            mProperty = property;
 +            mInfo = info;
 +            mAlias = alias;
 +            mSubNodes = new LinkedHashMap<String, JoinNode>();
 +        }
 +
 +        /**
 +         * Returns the table alias to use in SQL statement, i.e. "T1"
 +         */
 +        public String getAlias() {
 +            return mAlias;
 +        }
 +
 +        public String findAliasFor(ChainedProperty<?> chained) {
 +            return findAliasFor(chained, 0);
 +        }
 +
 +        private String findAliasFor(ChainedProperty<?> chained, int offset) {
 +            if ((chained.getChainCount() - offset) <= 0) {
 +                // At this point in the chain, there are no more joins.
 +                return mAlias;
 +            }
 +            StorableProperty<?> property;
 +            if (offset == 0) {
 +                property = chained.getPrimeProperty();
 +            } else {
 +                property = chained.getChainedProperty(offset - 1);
 +            }
 +            String name = property.getName();
 +            JoinNode subNode = mSubNodes.get(name);
 +            if (subNode != null) {
 +                return subNode.findAliasFor(chained, offset + 1);
 +            }
 +            return null;
 +        }
 +
 +        public boolean hasAnyJoins() {
 +            return mSubNodes.size() > 0;
 +        }
 +
 +        /**
 +         * Appends table name to the given FROM clause builder.
 +         */
 +        public void appendTableNameTo(StatementBuilder fromClause) {
 +            fromClause.append(' ');
 +            fromClause.append(mInfo.getQualifiedTableName());
 +        }
 +
 +        /**
 +         * Appends table names, aliases, and joins to the given FROM clause
 +         * builder.
 +         */
 +        public void appendFullJoinTo(StatementBuilder fromClause) {
 +            appendTableNameTo(fromClause);
 +            fromClause.append(' ');
 +            fromClause.append(mAlias);
 +            for (JoinNode jn : mSubNodes.values()) {
 +                // TODO: By default, joins are all inner. A join could become
 +                // LEFT OUTER JOIN if the query filter has a term like this:
 +                // "address = ? | address.state = ?", and the runtime value of
 +                // address is null. Because of DNF transformation and lack of
 +                // short-circuit ops, this syntax might be difficult to parse.
 +                // This might be a better way of expressing an outer join:
 +                // "address(.)state = ?".
 +
 +                fromClause.append(" INNER JOIN");
 +                jn.appendFullJoinTo(fromClause);
 +                fromClause.append(" ON ");
 +                int count = jn.mProperty.getJoinElementCount();
 +                for (int i=0; i<count; i++) {
 +                    if (i > 0) {
 +                        fromClause.append(" AND ");
 +                    }
 +                    fromClause.append(mAlias);
 +                    fromClause.append('.');
 +                    fromClause.append(jn.mProperty.getInternalJoinElement(i).getColumnName());
 +                    fromClause.append('=');
 +                    fromClause.append(jn.mAlias);
 +                    fromClause.append('.');
 +                    fromClause.append(jn.mProperty.getExternalJoinElement(i).getColumnName());
 +                }
 +            }
 +        }
 +
 +        /**
 +         * @return new value for aliasCounter
 +         */
 +        public int addJoin(ChainedProperty<?> chained, int aliasCounter)
 +            throws RepositoryException
 +        {
 +            return addJoin(chained, aliasCounter, 0);
 +        }
 +
 +        private int addJoin(ChainedProperty<?> chained, int aliasCounter, int offset)
 +            throws RepositoryException
 +        {
 +            if ((chained.getChainCount() - offset) <= 0) {
 +                // At this point in the chain, there are no more joins.
 +                return aliasCounter;
 +            }
 +            StorableProperty<?> property;
 +            if (offset == 0) {
 +                property = chained.getPrimeProperty();
 +            } else {
 +                property = chained.getChainedProperty(offset - 1);
 +            }
 +            String name = property.getName();
 +            JoinNode subNode = mSubNodes.get(name);
 +            if (subNode == null) {
 +                JDBCStorableInfo<?> info = mRepository.examineStorable(property.getJoinedType());
 +                JDBCStorableProperty<?> jProperty = mRepository.getJDBCStorableProperty(property);
 +                subNode = new JoinNode(jProperty, info, TABLE_ALIAS_PREFIX + (++aliasCounter));
 +                mSubNodes.put(name, subNode);
 +            }
 +            return subNode.addJoin(chained, aliasCounter, offset + 1);
 +        }
 +
 +        public String toString() {
 +            StringBuilder b = new StringBuilder();
 +            b.append("{table=");
 +            b.append(mInfo.getQualifiedTableName());
 +            b.append(", alias=");
 +            b.append(mAlias);
 +            if (mSubNodes.size() > 0) {
 +                b.append(", subNodes=");
 +                b.append(mSubNodes);
 +            }
 +            b.append('}');
 +            return b.toString();
 +        }
 +    }
 +
 +    /**
 +     * Filter visitor that constructs a JoinNode tree.
 +     */
 +    private class JoinNodeBuilder extends Visitor<S, Object, Object> {
 +        private JoinNode mRootJoinNode;
 +        private int mAliasCounter;
 +
 +        JoinNodeBuilder() {
 +            mAliasCounter = 1;
 +            mRootJoinNode = new JoinNode(getStorableInfo(), TABLE_ALIAS_PREFIX + mAliasCounter);
 +        }
 +
 +        public JoinNode getRootJoinNode() {
 +            return mRootJoinNode;
 +        }
 +
 +        /**
 +         * Processes the given property orderings and ensures that they are
 +         * part of the JoinNode tree.
 +         *
 +         * @throws UndeclaredThrowableException wraps a RepositoryException
 +         */
 +        public void captureOrderings(OrderedProperty<?>[] orderings) {
 +            try {
 +                if (orderings != null) {
 +                    for (OrderedProperty<?> orderedProperty : orderings) {
 +                        ChainedProperty<?> chained = orderedProperty.getChainedProperty();
 +                        mAliasCounter = mRootJoinNode.addJoin(chained, mAliasCounter);
 +                    }
 +                }
 +            } catch (RepositoryException e) {
 +                throw new UndeclaredThrowableException(e);
 +            }
 +        }
 +
 +        /**
 +         * @throws UndeclaredThrowableException wraps a RepositoryException
 +         * since RepositoryException cannot be thrown directly
 +         */
 +        public Object visit(PropertyFilter<S> filter, Object param) {
 +            try {
 +                visit(filter);
 +                return null;
 +            } catch (RepositoryException e) {
 +                throw new UndeclaredThrowableException(e);
 +            }
 +        }
 +
 +        private void visit(PropertyFilter<S> filter) throws RepositoryException {
 +            ChainedProperty<S> chained = filter.getChainedProperty();
 +            mAliasCounter = mRootJoinNode.addJoin(chained, mAliasCounter);
 +        }
 +    }
 +
 +    /**
 +     * Simple DOM representing a SQL statement.
 +     */
 +    private static abstract class Statement<S extends Storable> {
 +        public abstract int maxLength();
 +
 +        /**
 +         * Builds a statement string from the given values.
 +         *
 +         * @param initialCapacity expected size of finished string
 +         * length. Should be value returned from maxLength.
 +         * @param filterValues values may be needed to build complete statement
 +         */
 +        public String buildStatement(int initialCapacity, FilterValues<S> filterValues) {
 +            StringBuilder b = new StringBuilder(initialCapacity);
 +            this.appendTo(b, filterValues);
 +            return b.toString();
 +        }
 +
 +        public abstract void appendTo(StringBuilder b, FilterValues<S> filterValues);
 +
 +        /**
 +         * Just used for debugging.
 +         */
 +        public String toString() {
 +            StringBuilder b = new StringBuilder();
 +            appendTo(b, null);
 +            return b.toString();
 +        }
 +    }
 +
 +    private static class LiteralStatement<S extends Storable> extends Statement<S> {
 +        private final String mStr;
 +
 +        LiteralStatement(String str) {
 +            mStr = str;
 +        }
 +
 +        public int maxLength() {
 +            return mStr.length();
 +        }
 +
 +        public String buildStatement(int initialCapacity, FilterValues<S> filterValues) {
 +            return mStr;
 +        }
 +
 +        public void appendTo(StringBuilder b, FilterValues<S> filterValues) {
 +            b.append(mStr);
 +        }
 +
 +        /**
 +         * Returns the literal value.
 +         */
 +        public String toString() {
 +            return mStr;
 +        }
 +    }
 +
 +    private static class NullablePropertyStatement<S extends Storable> extends Statement<S> {
 +        private final PropertyFilter<S> mFilter;
 +        private final boolean mIsNullOp;
 +
 +        NullablePropertyStatement(PropertyFilter<S> filter, boolean isNullOp) {
 +            mFilter = filter;
 +            mIsNullOp = isNullOp;
 +        }
 +
 +        public int maxLength() {
 +            return mIsNullOp ? 8 : 12; // for " IS NULL" or " IS NOT NULL"
 +        }
 +
 +        public void appendTo(StringBuilder b, FilterValues<S> filterValues) {
 +            if (filterValues != null
 +                && filterValues.getValue(mFilter) == null
 +                && filterValues.isAssigned(mFilter))
 +            {
 +                if (mIsNullOp) {
 +                    b.append(" IS NULL");
 +                } else {
 +                    b.append(" IS NOT NULL");
 +                }
 +            } else {
 +                if (mIsNullOp) {
 +                    b.append("=?");
 +                } else {
 +                    b.append("<>?");
 +                }
 +            }
 +        }
 +    }
 +
 +    private static class CompositeStatement<S extends Storable> extends Statement<S> {
 +        private final Statement<S>[] mStatements;
 +
 +        @SuppressWarnings("unchecked")
 +        CompositeStatement(List<Statement<S>> statements) {
 +            mStatements = statements.toArray(new Statement[statements.size()]);
 +        }
 +
 +        public int maxLength() {
 +            int max = 0;
 +            for (Statement<S> statement : mStatements) {
 +                max += statement.maxLength();
 +            }
 +            return max;
 +        }
 +
 +        public void appendTo(StringBuilder b, FilterValues<S> filterValues) {
 +            for (Statement<S> statement : mStatements) {
 +                statement.appendTo(b, filterValues);
 +            }
 +        }
 +    }
 +
 +    private class StatementBuilder {
 +        private List<Statement<S>> mStatements;
 +        private StringBuilder mLiteralBuilder;
 +
 +        StatementBuilder() {
 +            mStatements = new ArrayList<Statement<S>>();
 +            mLiteralBuilder = new StringBuilder();
 +        }
 +
 +        public Statement<S> build() {
 +            if (mStatements.size() == 0 || mLiteralBuilder.length() > 0) {
 +                mStatements.add(new LiteralStatement<S>(mLiteralBuilder.toString()));
 +                mLiteralBuilder.setLength(0);
 +            }
 +            if (mStatements.size() == 1) {
 +                return mStatements.get(0);
 +            } else {
 +                return new CompositeStatement<S>(mStatements);
 +            }
 +        }
 +
 +        public void append(char c) {
 +            mLiteralBuilder.append(c);
 +        }
 +
 +        public void append(String str) {
 +            mLiteralBuilder.append(str);
 +        }
 +
 +        public void append(LiteralStatement<S> statement) {
 +            append(statement.toString());
 +        }
 +
 +        public void append(Statement<S> statement) {
 +            if (statement instanceof LiteralStatement) {
 +                append((LiteralStatement<S>) statement);
 +            } else {
 +                mStatements.add(new LiteralStatement<S>(mLiteralBuilder.toString()));
 +                mLiteralBuilder.setLength(0);
 +                mStatements.add(statement);
 +            }
 +        }
 +
 +        public void appendColumn(JoinNode jn, ChainedProperty<?> chained)
 +            throws FetchException
 +        {
 +            String alias;
 +            if (jn == null) {
 +                alias = null;
 +            } else {
 +                alias = jn.findAliasFor(chained);
 +            }
 +            if (alias != null) {
 +                mLiteralBuilder.append(alias);
 +                mLiteralBuilder.append('.');
 +            }
 +            StorableProperty<?> property = chained.getLastProperty();
 +            JDBCStorableProperty<?> jProperty;
 +            try {
 +                jProperty = mRepository.getJDBCStorableProperty(property);
 +            } catch (RepositoryException e) {
 +                throw mRepository.toFetchException(e);
 +            }
 +            if (jProperty.isJoin()) {
 +                throw new UnsupportedOperationException
 +                    ("Join property doesn't have a corresponding column: " + chained);
 +            }
 +            mLiteralBuilder.append(jProperty.getColumnName());
 +        }
 +    }
 +
 +    private class WhereBuilder extends Visitor<S, FetchException, Object> {
 +        private final StatementBuilder mStatementBuilder;
 +        private final JoinNode mJoinNode;
 +
 +        private List<PropertyFilter<S>> mPropertyFilters;
 +        private List<Boolean> mPropertyFilterNullable;
 +
 +        WhereBuilder(StatementBuilder statementBuilder, JoinNode jn) {
 +            mStatementBuilder = statementBuilder;
 +            mJoinNode = jn;
 +            mPropertyFilters = new ArrayList<PropertyFilter<S>>();
 +            mPropertyFilterNullable = new ArrayList<Boolean>();
 +        }
 +
 +        @SuppressWarnings("unchecked")
 +        public PropertyFilter<S>[] getPropertyFilters() {
 +            return mPropertyFilters.toArray(new PropertyFilter[mPropertyFilters.size()]);
 +        }
 +
 +        public boolean[] getPropertyFilterNullable() {
 +            boolean[] array = new boolean[mPropertyFilterNullable.size()];
 +            for (int i=0; i<array.length; i++) {
 +                array[i] = mPropertyFilterNullable.get(i);
 +            }
 +            return array;
 +        }
 +
 +        public FetchException visit(OrFilter<S> filter, Object param) {
 +            FetchException e;
 +            mStatementBuilder.append('(');
 +            e = filter.getLeftFilter().accept(this, null);
 +            if (e != null) {
 +                return e;
 +            }
 +            mStatementBuilder.append(" OR ");
 +            e = filter.getRightFilter().accept(this, null);
 +            if (e != null) {
 +                return e;
 +            }
 +            mStatementBuilder.append(')');
 +            return null;
 +        }
 +
 +        public FetchException visit(AndFilter<S> filter, Object param) {
 +            FetchException e;
 +            mStatementBuilder.append('(');
 +            e = filter.getLeftFilter().accept(this, null);
 +            if (e != null) {
 +                return e;
 +            }
 +            mStatementBuilder.append(" AND ");
 +            e = filter.getRightFilter().accept(this, null);
 +            if (e != null) {
 +                return e;
 +            }
 +            mStatementBuilder.append(')');
 +            return null;
 +        }
 +
 +        public FetchException visit(PropertyFilter<S> filter, Object param) {
 +            try {
 +                mStatementBuilder.appendColumn(mJoinNode, filter.getChainedProperty());
 +            } catch (FetchException e) {
 +                return e;
 +            }
 +
 +            mPropertyFilters.add(filter);
 +
 +            RelOp op = filter.getOperator();
 +            StorableProperty<?> property = filter.getChainedProperty().getLastProperty();
 +
 +            if (property.isNullable() && (op == RelOp.EQ || op == RelOp.NE)) {
 +                mPropertyFilterNullable.add(true);
 +                mStatementBuilder.append(new NullablePropertyStatement<S>(filter, op == RelOp.EQ));
 +            } else {
 +                mPropertyFilterNullable.add(false);
 +                if (op == RelOp.NE) {
 +                    mStatementBuilder.append("<>");
 +                } else {
 +                    mStatementBuilder.append(op.toString());
 +                }
 +                mStatementBuilder.append('?');
 +            }
 +
 +            return null;
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSupport.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSupport.java new file mode 100644 index 0000000..f178b7d --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSupport.java @@ -0,0 +1,74 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.sql.PreparedStatement;
 +
 +import com.amazon.carbonado.Storable;
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.PersistException;
 +
 +import com.amazon.carbonado.spi.MasterSupport;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +public interface JDBCSupport<S extends Storable> extends MasterSupport<S> {
 +    public JDBCRepository getJDBCRepository();
 +
 +    /**
 +     * @param loader used to reload Blob outside original transaction
 +     */
 +    public com.amazon.carbonado.lob.Blob convertBlob(java.sql.Blob blob, JDBCBlobLoader loader)
 +        throws FetchException;
 +
 +    /**
 +     * @param loader used to reload Clob outside original transaction
 +     */
 +    public com.amazon.carbonado.lob.Clob convertClob(java.sql.Clob clob, JDBCClobLoader loader)
 +        throws FetchException;
 +
 +    /**
 +     * @return original blob if too large and post-insert update is required, null otherwise
 +     * @throws PersistException instead of FetchException since this code is
 +     * called during an insert operation
 +     */
 +    public com.amazon.carbonado.lob.Blob setBlobValue(PreparedStatement ps, int column,
 +                                                      com.amazon.carbonado.lob.Blob blob)
 +        throws PersistException;
 +
 +    /**
 +     * @return original clob if too large and post-insert update is required, null otherwise
 +     * @throws PersistException instead of FetchException since this code is
 +     * called during an insert operation
 +     */
 +    public com.amazon.carbonado.lob.Clob setClobValue(PreparedStatement ps, int column,
 +                                                      com.amazon.carbonado.lob.Clob clob)
 +        throws PersistException;
 +
 +    public void updateBlob(com.amazon.carbonado.lob.Blob oldBlob,
 +                           com.amazon.carbonado.lob.Blob newBlob)
 +        throws PersistException;
 +
 +    public void updateClob(com.amazon.carbonado.lob.Clob oldClob,
 +                           com.amazon.carbonado.lob.Clob newClob)
 +        throws PersistException;
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSupportStrategy.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSupportStrategy.java new file mode 100644 index 0000000..b5d900a --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSupportStrategy.java @@ -0,0 +1,233 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.io.InputStream;
 +import java.io.IOException;
 +import java.io.OutputStream;
 +import java.io.Reader;
 +import java.io.Writer;
 +
 +import java.sql.PreparedStatement;
 +import java.sql.SQLException;
 +
 +import java.util.HashMap;
 +import java.util.Map;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.PersistException;
 +
 +import com.amazon.carbonado.util.ThrowUnchecked;
 +
 +import com.amazon.carbonado.spi.SequenceValueProducer;
 +
 +/**
 + * Allows database product specific features to be abstracted.
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCSupportStrategy {
 +    private static final int BLOB_BUFFER_SIZE = 4000;
 +    private static final int CLOB_BUFFER_SIZE = 2000;
 +
 +    static JDBCSupportStrategy createStrategy(JDBCRepository repo) {
 +        String databaseProductName = repo.getDatabaseProductName();
 +        if (databaseProductName != null && databaseProductName.length() > 0) {
 +            String className =
 +                "com.amazon.carbonado.repo.jdbc." +
 +                Character.toUpperCase(databaseProductName.charAt(0)) +
 +                databaseProductName.substring(1).toLowerCase() +
 +                "SupportStrategy";
 +            try {
 +                Class<JDBCSupportStrategy> clazz =
 +                    (Class<JDBCSupportStrategy>) Class.forName(className);
 +                return clazz.getDeclaredConstructor(JDBCRepository.class).newInstance(repo);
 +            } catch (ClassNotFoundException e) {
 +                // just use default strategy
 +            } catch (Exception e) {
 +                ThrowUnchecked.fireFirstDeclaredCause(e);
 +            }
 +        }
 +
 +        return new JDBCSupportStrategy(repo);
 +    }
 +
 +    protected final JDBCRepository mRepo;
 +
 +    private Map<String, SequenceValueProducer> mSequences;
 +
 +    protected JDBCSupportStrategy(JDBCRepository repo) {
 +        mRepo = repo;
 +    }
 +
 +    JDBCExceptionTransformer createExceptionTransformer() {
 +        return new JDBCExceptionTransformer();
 +    }
 +
 +    /**
 +     * Utility method used by generated storables to get sequence values during
 +     * an insert operation.
 +     *
 +     * @param sequenceName name of sequence
 +     * @throws PersistException instead of FetchException since this code is
 +     * called during an insert operation
 +     */
 +    synchronized SequenceValueProducer getSequenceValueProducer(String sequenceName)
 +        throws PersistException
 +    {
 +        SequenceValueProducer sequence = mSequences == null ? null : mSequences.get(sequenceName);
 +
 +        if (sequence == null) {
 +            String sequenceQuery = createSequenceQuery(sequenceName);
 +            sequence = new JDBCSequenceValueProducer(mRepo, sequenceQuery);
 +            if (mSequences == null) {
 +                mSequences = new HashMap<String, SequenceValueProducer>();
 +            }
 +            mSequences.put(sequenceName, sequence);
 +        }
 +
 +        return sequence;
 +    }
 +
 +    String createSequenceQuery(String sequenceName) {
 +        throw new UnsupportedOperationException
 +            ("Sequences are not supported by default JDBC support strategy. " +
 +             "If \"" + mRepo.getDatabaseProductName() + "\" actually does support sequences, " +
 +             "then a custom support strategy might be available in a separate jar. " +
 +             "If so, simply add it to your classpath.");
 +    }
 +
 +    /**
 +     * @param loader used to reload Blob outside original transaction
 +     */
 +    JDBCBlob convertBlob(java.sql.Blob blob, JDBCBlobLoader loader) {
 +        return blob == null ? null: new JDBCBlob(mRepo, blob, loader);
 +    }
 +
 +    /**
 +     * @param loader used to reload Clob outside original transaction
 +     */
 +    JDBCClob convertClob(java.sql.Clob clob, JDBCClobLoader loader) {
 +        return clob == null ? null : new JDBCClob(mRepo, clob, loader);
 +    }
 +
 +    /**
 +     * @return original blob if too large and post-insert update is required, null otherwise
 +     */
 +    com.amazon.carbonado.lob.Blob setBlobValue(PreparedStatement ps, int column,
 +                                               com.amazon.carbonado.lob.Blob blob)
 +        throws PersistException
 +    {
 +        try {
 +            if (blob instanceof JDBCBlob) {
 +                ps.setBlob(column, ((JDBCBlob) blob).getInternalBlobForPersist());
 +                return null;
 +            }
 +
 +            long length = blob.getLength();
 +
 +            if (((long) ((int) length)) != length) {
 +                throw new PersistException("BLOB length is too long: " + length);
 +            }
 +
 +            ps.setBinaryStream(column, blob.openInputStream(), (int) length);
 +            return null;
 +        } catch (SQLException e) {
 +            throw mRepo.toPersistException(e);
 +        } catch (FetchException e) {
 +            throw e.toPersistException();
 +        }
 +    }
 +
 +    /**
 +     * @return original clob if too large and post-insert update is required, null otherwise
 +     */
 +    com.amazon.carbonado.lob.Clob setClobValue(PreparedStatement ps, int column,
 +                                               com.amazon.carbonado.lob.Clob clob)
 +        throws PersistException
 +    {
 +        try {
 +            if (clob instanceof JDBCClob) {
 +                ps.setClob(column, ((JDBCClob) clob).getInternalClobForPersist());
 +                return null;
 +            }
 +
 +            long length = clob.getLength();
 +
 +            if (((long) ((int) length)) != length) {
 +                throw new PersistException("CLOB length is too long: " + length);
 +            }
 +
 +            ps.setCharacterStream(column, clob.openReader(), (int) length);
 +            return null;
 +        } catch (SQLException e) {
 +            throw mRepo.toPersistException(e);
 +        } catch (FetchException e) {
 +            throw e.toPersistException();
 +        }
 +    }
 +
 +    void updateBlob(com.amazon.carbonado.lob.Blob oldBlob, com.amazon.carbonado.lob.Blob newBlob)
 +        throws PersistException
 +    {
 +        try {
 +            OutputStream out = oldBlob.openOutputStream();
 +            InputStream in = newBlob.openInputStream();
 +            byte[] buf = new byte[BLOB_BUFFER_SIZE];
 +            int amt;
 +            while ((amt = in.read(buf)) > 0) {
 +                out.write(buf, 0, amt);
 +            }
 +            in.close();
 +            out.close();
 +            oldBlob.setLength(newBlob.getLength());
 +        } catch (FetchException e) {
 +            throw e.toPersistException();
 +        } catch (IOException e) {
 +            throw mRepo.toPersistException(e);
 +        }
 +    }
 +
 +    void updateClob(com.amazon.carbonado.lob.Clob oldClob, com.amazon.carbonado.lob.Clob newClob)
 +        throws PersistException
 +    {
 +        try {
 +            Writer out = oldClob.openWriter();
 +            Reader in = newClob.openReader();
 +            char[] buf = new char[CLOB_BUFFER_SIZE];
 +            int amt;
 +            while ((amt = in.read(buf)) > 0) {
 +                out.write(buf, 0, amt);
 +            }
 +            in.close();
 +            out.close();
 +            oldClob.setLength(newClob.getLength());
 +        } catch (FetchException e) {
 +            throw e.toPersistException();
 +        } catch (IOException e) {
 +            throw mRepo.toPersistException(e);
 +        }
 +    }
 +
 +    boolean printPlan(Appendable app, int indentLevel, String statement)
 +        throws FetchException, IOException
 +    {
 +        return false;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCTransaction.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCTransaction.java new file mode 100644 index 0000000..d92228d --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCTransaction.java @@ -0,0 +1,122 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.util.ArrayList;
 +import java.util.List;
 +
 +import java.sql.Connection;
 +import java.sql.Savepoint;
 +import java.sql.SQLException;
 +
 +import com.amazon.carbonado.IsolationLevel;
 +
 +/**
 + * JDBCTransaction is just a wrapper around a connection and (optionally) a
 + * savepoint.
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCTransaction {
 +    private final Connection mConnection;
 +    // Use TRANSACTION_NONE as a magic value to indicate that the isolation
 +    // level need not be changed when the transaction ends. This is a little
 +    // optimization to avoid a round trip call to the remote database.
 +    private final int mOriginalLevel;
 +    private Savepoint mSavepoint;
 +
 +    private List<JDBCLob> mRegisteredLobs;
 +
 +    JDBCTransaction(Connection con) {
 +        mConnection = con;
 +        // Don't change level upon abort.
 +        mOriginalLevel = Connection.TRANSACTION_NONE;
 +    }
 +
 +    /**
 +     * Construct a nested transaction.
 +     */
 +    JDBCTransaction(JDBCTransaction parent, IsolationLevel level) throws SQLException {
 +        mConnection = parent.mConnection;
 +
 +        if (level == null) {
 +            // Don't change level upon abort.
 +            mOriginalLevel = Connection.TRANSACTION_NONE;
 +        } else {
 +            int newLevel = JDBCRepository.mapIsolationLevelToJdbc(level);
 +            int originalLevel = mConnection.getTransactionIsolation();
 +            if (newLevel == originalLevel) {
 +                // Don't change level upon abort.
 +                mOriginalLevel = Connection.TRANSACTION_NONE;
 +            } else {
 +                // Don't change level upon abort.
 +                mOriginalLevel = originalLevel;
 +                mConnection.setTransactionIsolation(newLevel);
 +            }
 +        }
 +
 +        mSavepoint = mConnection.setSavepoint();
 +    }
 +
 +    Connection getConnection() {
 +        return mConnection;
 +    }
 +
 +    void commit() throws SQLException {
 +        if (mSavepoint == null) {
 +            mConnection.commit();
 +        } else {
 +            // Don't commit, make a new savepoint. Root transaction has no
 +            // savepoint, and so it will do the real commit.
 +            mSavepoint = mConnection.setSavepoint();
 +        }
 +    }
 +
 +    /**
 +     * @return connection to close, or null if not ready to because this was a
 +     * nested transaction
 +     */
 +    Connection abort() throws SQLException {
 +        if (mRegisteredLobs != null) {
 +            for (JDBCLob lob : mRegisteredLobs) {
 +                lob.close();
 +            }
 +            mRegisteredLobs = null;
 +        }
 +        if (mSavepoint == null) {
 +            mConnection.rollback();
 +            mConnection.setAutoCommit(true);
 +            return mConnection;
 +        } else {
 +            mConnection.rollback(mSavepoint);
 +            if (mOriginalLevel != Connection.TRANSACTION_NONE) {
 +                mConnection.setTransactionIsolation(mOriginalLevel);
 +            }
 +            mSavepoint = null;
 +            return null;
 +        }
 +    }
 +
 +    void register(JDBCLob lob) {
 +        if (mRegisteredLobs == null) {
 +            mRegisteredLobs = new ArrayList<JDBCLob>(4);
 +        }
 +        mRegisteredLobs.add(lob);
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCTransactionManager.java b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCTransactionManager.java new file mode 100644 index 0000000..7fa3fcc --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/JDBCTransactionManager.java @@ -0,0 +1,94 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.lang.ref.WeakReference;
 +import java.sql.Connection;
 +import java.sql.SQLException;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.IsolationLevel;
 +import com.amazon.carbonado.Transaction;
 +import com.amazon.carbonado.spi.TransactionManager;
 +
 +/**
 + * Manages transactions for JDBCRepository. Only one instance is allocated per
 + * thread.
 + *
 + * @author Brian S O'Neill
 + */
 +class JDBCTransactionManager extends TransactionManager<JDBCTransaction> {
 +    // Weakly reference repository because thread locals are not cleaned up
 +    // very quickly.
 +    private final WeakReference<JDBCRepository> mRepositoryRef;
 +
 +    JDBCTransactionManager(JDBCRepository repository) {
 +        super(repository.getExceptionTransformer());
 +        mRepositoryRef = new WeakReference<JDBCRepository>(repository);
 +    }
 +
 +    @Override
 +    public boolean isForUpdate() {
 +        return super.isForUpdate() && mRepositoryRef.get().supportsSelectForUpdate();
 +    }
 +
 +    protected IsolationLevel selectIsolationLevel(Transaction parent, IsolationLevel level) {
 +        JDBCRepository repo = mRepositoryRef.get();
 +        if (repo == null) {
 +            throw new IllegalStateException("Repository closed");
 +        }
 +        return repo.selectIsolationLevel(parent, level);
 +    }
 +
 +    protected JDBCTransaction createTxn(JDBCTransaction parent, IsolationLevel level)
 +        throws SQLException, FetchException
 +    {
 +        JDBCRepository repo = mRepositoryRef.get();
 +        if (repo == null) {
 +            throw new IllegalStateException("Repository closed");
 +        }
 +
 +        if (parent != null) {
 +            if (!repo.supportsSavepoints()) {
 +                // No support for nested transactions, so fake it.
 +                return parent;
 +            }
 +            return new JDBCTransaction(parent, level);
 +        }
 +
 +        return new JDBCTransaction(repo.getConnectionForTxn(level));
 +    }
 +
 +    protected boolean commitTxn(JDBCTransaction txn) throws SQLException {
 +        txn.commit();
 +        return true;
 +    }
 +
 +    protected void abortTxn(JDBCTransaction txn) throws SQLException, FetchException {
 +        Connection con;
 +        if ((con = txn.abort()) != null) {
 +            JDBCRepository repo = mRepositoryRef.get();
 +            if (repo == null) {
 +                con.close();
 +            } else {
 +                repo.yieldConnection(con);
 +            }
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingCallableStatement.java b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingCallableStatement.java new file mode 100644 index 0000000..86ac2e0 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingCallableStatement.java @@ -0,0 +1,392 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.math.BigDecimal;
 +import java.util.Calendar;
 +import java.sql.*;
 +
 +import org.apache.commons.logging.Log;
 +
 +/**
 + * CallableStatement returned by LoggingConnection;
 + *
 + * @author Brian S O'Neill
 + */
 +class LoggingCallableStatement extends LoggingPreparedStatement implements CallableStatement {
 +    LoggingCallableStatement(Log log, Connection con, CallableStatement ps, String sql) {
 +        super(log, con, ps, sql);
 +    }
 +
 +    public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException {
 +        cs().registerOutParameter(parameterIndex, sqlType);
 +    }
 +
 +    public void registerOutParameter(int parameterIndex, int sqlType, int scale)
 +        throws SQLException
 +    {
 +        cs().registerOutParameter(parameterIndex, sqlType, scale);
 +    }
 +
 +    public boolean wasNull() throws SQLException {
 +        return cs().wasNull();
 +    }
 +
 +    public String getString(int parameterIndex) throws SQLException {
 +        return cs().getString(parameterIndex);
 +    }
 +
 +    public boolean getBoolean(int parameterIndex) throws SQLException {
 +        return cs().getBoolean(parameterIndex);
 +    }
 +
 +    public byte getByte(int parameterIndex) throws SQLException {
 +        return cs().getByte(parameterIndex);
 +    }
 +
 +    public short getShort(int parameterIndex) throws SQLException {
 +        return cs().getShort(parameterIndex);
 +    }
 +
 +    public int getInt(int parameterIndex) throws SQLException {
 +        return cs().getInt(parameterIndex);
 +    }
 +
 +    public long getLong(int parameterIndex) throws SQLException {
 +        return cs().getLong(parameterIndex);
 +    }
 +
 +    public float getFloat(int parameterIndex) throws SQLException {
 +        return cs().getFloat(parameterIndex);
 +    }
 +
 +    public double getDouble(int parameterIndex) throws SQLException {
 +        return cs().getDouble(parameterIndex);
 +    }
 +
 +    @Deprecated
 +    public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException {
 +        return cs().getBigDecimal(parameterIndex, scale);
 +    }
 +
 +    public byte[] getBytes(int parameterIndex) throws SQLException {
 +        return cs().getBytes(parameterIndex);
 +    }
 +
 +    public java.sql.Date getDate(int parameterIndex) throws SQLException {
 +        return cs().getDate(parameterIndex);
 +    }
 +
 +    public java.sql.Time getTime(int parameterIndex) throws SQLException {
 +        return cs().getTime(parameterIndex);
 +    }
 +
 +    public java.sql.Timestamp getTimestamp(int parameterIndex)
 +        throws SQLException
 +    {
 +        return cs().getTimestamp(parameterIndex);
 +    }
 +
 +    public Object getObject(int parameterIndex) throws SQLException {
 +        return cs().getObject(parameterIndex);
 +    }
 +
 +    public BigDecimal getBigDecimal(int parameterIndex) throws SQLException {
 +        return cs().getBigDecimal(parameterIndex);
 +    }
 +
 +    public Object getObject(int i, java.util.Map<String,Class<?>> map) throws SQLException {
 +        return cs().getObject(i, map);
 +    }
 +
 +    public Ref getRef(int i) throws SQLException {
 +        return cs().getRef(i);
 +    }
 +
 +    public Blob getBlob(int i) throws SQLException {
 +        return cs().getBlob(i);
 +    }
 +
 +    public Clob getClob(int i) throws SQLException {
 +        return cs().getClob(i);
 +    }
 +
 +    public Array getArray(int i) throws SQLException {
 +        return cs().getArray(i);
 +    }
 +
 +    public java.sql.Date getDate(int parameterIndex, Calendar cal) throws SQLException {
 +        return cs().getDate(parameterIndex, cal);
 +    }
 +
 +    public java.sql.Time getTime(int parameterIndex, Calendar cal) throws SQLException {
 +        return cs().getTime(parameterIndex, cal);
 +    }
 +
 +    public java.sql.Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException {
 +        return cs().getTimestamp(parameterIndex, cal);
 +    }
 +
 +    public void registerOutParameter(int paramIndex, int sqlType, String typeName)
 +        throws SQLException
 +    {
 +        cs().registerOutParameter(paramIndex, sqlType, typeName);
 +    }
 +
 +    public void registerOutParameter(String parameterName, int sqlType) throws SQLException {
 +        cs().registerOutParameter(parameterName, sqlType);
 +    }
 +
 +    public void registerOutParameter(String parameterName, int sqlType, int scale)
 +        throws SQLException
 +    {
 +        cs().registerOutParameter(parameterName, sqlType, scale);
 +    }
 +
 +    public void registerOutParameter(String parameterName, int sqlType, String typeName)
 +        throws SQLException
 +    {
 +        cs().registerOutParameter(parameterName, sqlType, typeName);
 +    }
 +
 +    public java.net.URL getURL(int parameterIndex) throws SQLException {
 +        return cs().getURL(parameterIndex);
 +    }
 +
 +    public void setURL(String parameterName, java.net.URL val) throws SQLException {
 +        cs().setURL(parameterName, val);
 +    }
 +
 +    public void setNull(String parameterName, int sqlType) throws SQLException {
 +        cs().setNull(parameterName, sqlType);
 +    }
 +
 +    public void setBoolean(String parameterName, boolean x) throws SQLException {
 +        cs().setBoolean(parameterName, x);
 +    }
 +
 +    public void setByte(String parameterName, byte x) throws SQLException {
 +        cs().setByte(parameterName, x);
 +    }
 +
 +    public void setShort(String parameterName, short x) throws SQLException {
 +        cs().setShort(parameterName, x);
 +    }
 +
 +    public void setInt(String parameterName, int x) throws SQLException {
 +        cs().setInt(parameterName, x);
 +    }
 +
 +    public void setLong(String parameterName, long x) throws SQLException {
 +        cs().setLong(parameterName, x);
 +    }
 +
 +    public void setFloat(String parameterName, float x) throws SQLException {
 +        cs().setFloat(parameterName, x);
 +    }
 +
 +    public void setDouble(String parameterName, double x) throws SQLException {
 +        cs().setDouble(parameterName, x);
 +    }
 +
 +    public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException {
 +        cs().setBigDecimal(parameterName, x);
 +    }
 +
 +    public void setString(String parameterName, String x) throws SQLException {
 +        cs().setString(parameterName, x);
 +    }
 +
 +    public void setBytes(String parameterName, byte x[]) throws SQLException {
 +        cs().setBytes(parameterName, x);
 +    }
 +
 +    public void setDate(String parameterName, java.sql.Date x) throws SQLException {
 +        cs().setDate(parameterName, x);
 +    }
 +
 +    public void setTime(String parameterName, java.sql.Time x) throws SQLException {
 +        cs().setTime(parameterName, x);
 +    }
 +
 +    public void setTimestamp(String parameterName, java.sql.Timestamp x) throws SQLException {
 +        cs().setTimestamp(parameterName, x);
 +    }
 +
 +    public void setAsciiStream(String parameterName, java.io.InputStream x, int length)
 +        throws SQLException
 +    {
 +        cs().setAsciiStream(parameterName, x, length);
 +    }
 +
 +    public void setBinaryStream(String parameterName, java.io.InputStream x,
 +                                int length)
 +        throws SQLException
 +    {
 +        cs().setBinaryStream(parameterName, x, length);
 +    }
 +
 +    public void setObject(String parameterName, Object x, int targetSqlType, int scale)
 +        throws SQLException
 +    {
 +        cs().setObject(parameterName, x, targetSqlType, scale);
 +    }
 +
 +    public void setObject(String parameterName, Object x, int targetSqlType)
 +        throws SQLException
 +    {
 +        cs().setObject(parameterName, x, targetSqlType);
 +    }
 +
 +    public void setObject(String parameterName, Object x) throws SQLException {
 +        cs().setObject(parameterName, x);
 +    }
 +
 +    public void setCharacterStream(String parameterName,
 +                                   java.io.Reader reader,
 +                                   int length)
 +        throws SQLException
 +    {
 +        cs().setCharacterStream(parameterName, reader, length);
 +    }
 +
 +    public void setDate(String parameterName, java.sql.Date x, Calendar cal)
 +        throws SQLException
 +    {
 +        cs().setDate(parameterName, x, cal);
 +    }
 +
 +    public void setTime(String parameterName, java.sql.Time x, Calendar cal)
 +        throws SQLException
 +    {
 +        cs().setTime(parameterName, x, cal);
 +    }
 +
 +    public void setTimestamp(String parameterName, java.sql.Timestamp x, Calendar cal)
 +        throws SQLException
 +    {
 +        cs().setTimestamp(parameterName, x, cal);
 +    }
 +
 +    public void setNull(String parameterName, int sqlType, String typeName)
 +        throws SQLException
 +    {
 +        cs().setNull(parameterName, sqlType, typeName);
 +    }
 +
 +    public String getString(String parameterName) throws SQLException {
 +        return cs().getString(parameterName);
 +    }
 +
 +    public boolean getBoolean(String parameterName) throws SQLException {
 +        return cs().getBoolean(parameterName);
 +    }
 +
 +    public byte getByte(String parameterName) throws SQLException {
 +        return cs().getByte(parameterName);
 +    }
 +
 +    public short getShort(String parameterName) throws SQLException {
 +        return cs().getShort(parameterName);
 +    }
 +
 +    public int getInt(String parameterName) throws SQLException {
 +        return cs().getInt(parameterName);
 +    }
 +
 +    public long getLong(String parameterName) throws SQLException {
 +        return cs().getLong(parameterName);
 +    }
 +
 +    public float getFloat(String parameterName) throws SQLException {
 +        return cs().getFloat(parameterName);
 +    }
 +
 +    public double getDouble(String parameterName) throws SQLException {
 +        return cs().getDouble(parameterName);
 +    }
 +
 +    public byte[] getBytes(String parameterName) throws SQLException {
 +        return cs().getBytes(parameterName);
 +    }
 +
 +    public java.sql.Date getDate(String parameterName) throws SQLException {
 +        return cs().getDate(parameterName);
 +    }
 +
 +    public java.sql.Time getTime(String parameterName) throws SQLException {
 +        return cs().getTime(parameterName);
 +    }
 +
 +    public java.sql.Timestamp getTimestamp(String parameterName) throws SQLException {
 +        return cs().getTimestamp(parameterName);
 +    }
 +
 +    public Object getObject(String parameterName) throws SQLException {
 +        return cs().getObject(parameterName);
 +    }
 +
 +    public BigDecimal getBigDecimal(String parameterName) throws SQLException {
 +        return cs().getBigDecimal(parameterName);
 +    }
 +
 +    public Object getObject(String parameterName, java.util.Map<String,Class<?>> map)
 +        throws SQLException
 +    {
 +        return cs().getObject(parameterName, map);
 +    }
 +
 +    public Ref getRef(String parameterName) throws SQLException {
 +        return cs().getRef(parameterName);
 +    }
 +
 +    public Blob getBlob(String parameterName) throws SQLException {
 +        return cs().getBlob(parameterName);
 +    }
 +
 +    public Clob getClob(String parameterName) throws SQLException {
 +        return cs().getClob(parameterName);
 +    }
 +
 +    public Array getArray(String parameterName) throws SQLException {
 +        return cs().getArray(parameterName);
 +    }
 +
 +    public java.sql.Date getDate(String parameterName, Calendar cal) throws SQLException {
 +        return cs().getDate(parameterName, cal);
 +    }
 +
 +    public java.sql.Time getTime(String parameterName, Calendar cal) throws SQLException {
 +        return cs().getTime(parameterName, cal);
 +    }
 +
 +    public java.sql.Timestamp getTimestamp(String parameterName, Calendar cal)
 +        throws SQLException
 +    {
 +        return cs().getTimestamp(parameterName, cal);
 +    }
 +
 +    public java.net.URL getURL(String parameterName) throws SQLException {
 +        return cs().getURL(parameterName);
 +    }
 +
 +    private CallableStatement cs() {
 +        return (CallableStatement) mStatement;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingConnection.java b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingConnection.java new file mode 100644 index 0000000..5d3327e --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingConnection.java @@ -0,0 +1,227 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.sql.*;
 +
 +import org.apache.commons.logging.Log;
 +
 +/**
 + * Connection returned by LoggingDataSource.
 + *
 + * @author Brian S O'Neill
 + */
 +class LoggingConnection implements Connection {
 +    private final Log mLog;
 +    private final Connection mCon;
 +
 +    LoggingConnection(Log log, Connection con) {
 +        mLog = log;
 +        mCon = con;
 +    }
 +
 +    public Statement createStatement() throws SQLException {
 +        return new LoggingStatement(mLog, this, mCon.createStatement());
 +    }
 +
 +    public Statement createStatement(int resultSetType, int resultSetConcurrency)
 +        throws SQLException
 +    {
 +        return new LoggingStatement
 +            (mLog, this, mCon.createStatement(resultSetType, resultSetConcurrency));
 +    }
 +
 +    public Statement createStatement(int resultSetType, int resultSetConcurrency,
 +                                     int resultSetHoldability)
 +        throws SQLException
 +    {
 +        return new LoggingStatement(mLog, this,
 +                                    mCon.createStatement(resultSetType, resultSetConcurrency,
 +                                                         resultSetHoldability));
 +    }
 +
 +    public PreparedStatement prepareStatement(String sql) throws SQLException {
 +        return new LoggingPreparedStatement
 +            (mLog, this, mCon.prepareStatement(sql), sql);
 +    }
 +
 +    public PreparedStatement prepareStatement(String sql, int resultSetType,
 +                                              int resultSetConcurrency)
 +        throws SQLException
 +    {
 +        return new LoggingPreparedStatement
 +            (mLog, this, mCon.prepareStatement(sql, resultSetType, resultSetConcurrency), sql);
 +    }
 +
 +    public PreparedStatement prepareStatement(String sql, int resultSetType,
 +                                              int resultSetConcurrency, int resultSetHoldability)
 +        throws SQLException
 +    {
 +        return new LoggingPreparedStatement
 +            (mLog, this, mCon.prepareStatement(sql, resultSetType,
 +                                               resultSetConcurrency, resultSetHoldability), sql);
 +    }
 +
 +    public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys)
 +        throws SQLException
 +    {
 +        return new LoggingPreparedStatement
 +            (mLog, this, mCon.prepareStatement(sql, autoGeneratedKeys), sql);
 +    }
 +
 +    public PreparedStatement prepareStatement(String sql, int columnIndexes[])
 +        throws SQLException
 +    {
 +        return new LoggingPreparedStatement
 +            (mLog, this, mCon.prepareStatement(sql, columnIndexes), sql);
 +    }
 +
 +    public PreparedStatement prepareStatement(String sql, String columnNames[])
 +        throws SQLException
 +    {
 +        return new LoggingPreparedStatement
 +            (mLog, this, mCon.prepareStatement(sql, columnNames), sql);
 +    }
 +
 +    public CallableStatement prepareCall(String sql) throws SQLException {
 +        return new LoggingCallableStatement(mLog, this, mCon.prepareCall(sql), sql);
 +    }
 +
 +    public CallableStatement prepareCall(String sql, int resultSetType,
 +                                         int resultSetConcurrency)
 +        throws SQLException
 +    {
 +        return new LoggingCallableStatement
 +            (mLog, this, mCon.prepareCall(sql, resultSetType, resultSetConcurrency), sql);
 +    }
 +
 +    public CallableStatement prepareCall(String sql, int resultSetType,
 +                                         int resultSetConcurrency,
 +                                         int resultSetHoldability)
 +        throws SQLException
 +    {
 +        return new LoggingCallableStatement
 +            (mLog, this, mCon.prepareCall(sql, resultSetType,
 +                                          resultSetConcurrency, resultSetHoldability), sql);
 +    }
 +
 +    public String nativeSQL(String sql) throws SQLException {
 +        return mCon.nativeSQL(sql);
 +    }
 +
 +    public void setAutoCommit(boolean autoCommit) throws SQLException {
 +        mCon.setAutoCommit(autoCommit);
 +    }
 +
 +    public boolean getAutoCommit() throws SQLException {
 +        return mCon.getAutoCommit();
 +    }
 +
 +    public void commit() throws SQLException {
 +        mLog.debug("Connection.commit()");
 +        mCon.commit();
 +    }
 +
 +    public void rollback() throws SQLException {
 +        mLog.debug("Connection.rollback()");
 +        mCon.rollback();
 +    }
 +
 +    public void close() throws SQLException {
 +        mLog.debug("Connection.close()");
 +        mCon.close();
 +    }
 +
 +    public boolean isClosed() throws SQLException {
 +        return mCon.isClosed();
 +    }
 +
 +    public DatabaseMetaData getMetaData() throws SQLException {
 +        mLog.debug("Connection.getMetaData()");
 +        return mCon.getMetaData();
 +    }
 +
 +    public void setReadOnly(boolean readOnly) throws SQLException {
 +        mCon.setReadOnly(readOnly);
 +    }
 +
 +    public boolean isReadOnly() throws SQLException {
 +        return mCon.isReadOnly();
 +    }
 +
 +    public void setCatalog(String catalog) throws SQLException {
 +        mCon.setCatalog(catalog);
 +    }
 +
 +    public String getCatalog() throws SQLException {
 +        return mCon.getCatalog();
 +    }
 +
 +    public void setTransactionIsolation(int level) throws SQLException {
 +        mCon.setTransactionIsolation(level);
 +    }
 +
 +    public int getTransactionIsolation() throws SQLException {
 +        return mCon.getTransactionIsolation();
 +    }
 +
 +    public SQLWarning getWarnings() throws SQLException {
 +        return mCon.getWarnings();
 +    }
 +
 +    public void clearWarnings() throws SQLException {
 +        mCon.clearWarnings();
 +    }
 +
 +    public java.util.Map<String,Class<?>> getTypeMap() throws SQLException {
 +        return mCon.getTypeMap();
 +    }
 +
 +    public void setTypeMap(java.util.Map<String,Class<?>> map) throws SQLException {
 +        mCon.setTypeMap(map);
 +    }
 +
 +    public void setHoldability(int holdability) throws SQLException {
 +        mCon.setHoldability(holdability);
 +    }
 +
 +    public int getHoldability() throws SQLException {
 +        return mCon.getHoldability();
 +    }
 +
 +    public Savepoint setSavepoint() throws SQLException {
 +        mLog.debug("Connection.setSavepoint()");
 +        return mCon.setSavepoint();
 +    }
 +
 +    public Savepoint setSavepoint(String name) throws SQLException {
 +        mLog.debug("Connection.setSavepoint(name)");
 +        return mCon.setSavepoint(name);
 +    }
 +
 +    public void rollback(Savepoint savepoint) throws SQLException {
 +        mLog.debug("Connection.rollback(savepoint)");
 +        mCon.rollback(savepoint);
 +    }
 +
 +    public void releaseSavepoint(Savepoint savepoint) throws SQLException {
 +        mLog.debug("Connection.releaseSavepoint(savepoint)");
 +        mCon.releaseSavepoint(savepoint);
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingDataSource.java b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingDataSource.java new file mode 100644 index 0000000..f612956 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingDataSource.java @@ -0,0 +1,93 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.sql.Connection;
 +import java.sql.SQLException;
 +import java.io.PrintWriter;
 +import javax.sql.DataSource;
 +
 +import org.apache.commons.logging.Log;
 +import org.apache.commons.logging.LogFactory;
 +
 +/**
 + * Wraps another DataSource such that all SQL statements are logged as debug.
 + *
 + * @author Brian S O'Neill
 + */
 +public class LoggingDataSource implements DataSource {
 +    /**
 +     * Wraps the given DataSource which logs to the default log. If debug
 +     * logging is disabled, the original DataSource is returned.
 +     */
 +    public static DataSource create(DataSource ds) {
 +        return create(ds, null);
 +    }
 +
 +    /**
 +     * Wraps the given DataSource which logs to the given log. If debug logging
 +     * is disabled, the original DataSource is returned.
 +     */
 +    public static DataSource create(DataSource ds, Log log) {
 +        if (ds == null) {
 +            throw new IllegalArgumentException();
 +        }
 +        if (log == null) {
 +            log = LogFactory.getLog(LoggingDataSource.class);
 +        }
 +        if (!log.isDebugEnabled()) {
 +            return ds;
 +        }
 +        return new LoggingDataSource(log, ds);
 +    }
 +
 +    private final Log mLog;
 +    private final DataSource mDataSource;
 +
 +    private LoggingDataSource(Log log, DataSource ds) {
 +        mLog = log;
 +        mDataSource = ds;
 +    }
 +
 +    public Connection getConnection() throws SQLException {
 +        mLog.debug("DataSource.getConnection()");
 +        return new LoggingConnection(mLog, mDataSource.getConnection());
 +    }
 +
 +    public Connection getConnection(String username, String password) throws SQLException {
 +        mLog.debug("DataSource.getConnection(username, password)");
 +        return new LoggingConnection(mLog, mDataSource.getConnection(username, password));
 +    }
 +
 +    public PrintWriter getLogWriter() throws SQLException {
 +        return mDataSource.getLogWriter();
 +    }
 +
 +    public void setLogWriter(PrintWriter writer) throws SQLException {
 +        mDataSource.setLogWriter(writer);
 +    }
 +
 +    public void setLoginTimeout(int seconds) throws SQLException {
 +        mDataSource.setLoginTimeout(seconds);
 +    }
 +
 +    public int getLoginTimeout() throws SQLException {
 +        return mDataSource.getLoginTimeout();
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingPreparedStatement.java b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingPreparedStatement.java new file mode 100644 index 0000000..be2c48c --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingPreparedStatement.java @@ -0,0 +1,255 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.math.BigDecimal;
 +import java.util.Calendar;
 +import java.sql.*;
 +
 +import org.apache.commons.logging.Log;
 +
 +/**
 + * PreparedStatement returned by LoggingConnection;
 + *
 + * @author Brian S O'Neill
 + */
 +class LoggingPreparedStatement extends LoggingStatement implements PreparedStatement {
 +    protected final String mSQL;
 +
 +    LoggingPreparedStatement(Log log, Connection con, PreparedStatement ps, String sql) {
 +        super(log, con, ps);
 +        mSQL = sql;
 +    }
 +
 +    public ResultSet executeQuery() throws SQLException {
 +        mLog.debug(mSQL);
 +        return ps().executeQuery();
 +    }
 +
 +    public int executeUpdate() throws SQLException {
 +        mLog.debug(mSQL);
 +        return ps().executeUpdate();
 +    }
 +
 +    public boolean execute() throws SQLException {
 +        mLog.debug(mSQL);
 +        return ps().execute();
 +    }
 +
 +    public void addBatch() throws SQLException {
 +        // TODO: add local batch
 +        ps().addBatch();
 +    }
 +
 +    public void clearParameters() throws SQLException {
 +        // TODO: clear locally
 +        ps().clearParameters();
 +    }
 +
 +    public void setNull(int parameterIndex, int sqlType) throws SQLException {
 +        // TODO: set locally
 +        ps().setNull(parameterIndex, sqlType);
 +    }
 +
 +    public void setBoolean(int parameterIndex, boolean x) throws SQLException {
 +        // TODO: set locally
 +        ps().setBoolean(parameterIndex, x);
 +    }
 +
 +    public void setByte(int parameterIndex, byte x) throws SQLException {
 +        // TODO: set locally
 +        ps().setByte(parameterIndex, x);
 +    }
 +
 +    public void setShort(int parameterIndex, short x) throws SQLException {
 +        // TODO: set locally
 +        ps().setShort(parameterIndex, x);
 +    }
 +
 +    public void setInt(int parameterIndex, int x) throws SQLException {
 +        // TODO: set locally
 +        ps().setInt(parameterIndex, x);
 +    }
 +
 +    public void setLong(int parameterIndex, long x) throws SQLException {
 +        // TODO: set locally
 +        ps().setLong(parameterIndex, x);
 +    }
 +
 +    public void setFloat(int parameterIndex, float x) throws SQLException {
 +        // TODO: set locally
 +        ps().setFloat(parameterIndex, x);
 +    }
 +
 +    public void setDouble(int parameterIndex, double x) throws SQLException {
 +        // TODO: set locally
 +        ps().setDouble(parameterIndex, x);
 +    }
 +
 +    public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
 +        // TODO: set locally
 +        ps().setBigDecimal(parameterIndex, x);
 +    }
 +
 +    public void setString(int parameterIndex, String x) throws SQLException {
 +        // TODO: set locally
 +        ps().setString(parameterIndex, x);
 +    }
 +
 +    public void setBytes(int parameterIndex, byte x[]) throws SQLException {
 +        // TODO: set locally
 +        ps().setBytes(parameterIndex, x);
 +    }
 +
 +    public void setDate(int parameterIndex, java.sql.Date x)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setDate(parameterIndex, x);
 +    }
 +
 +    public void setTime(int parameterIndex, java.sql.Time x)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setTime(parameterIndex, x);
 +    }
 +
 +    public void setTimestamp(int parameterIndex, java.sql.Timestamp x)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setTimestamp(parameterIndex, x);
 +    }
 +
 +    public void setAsciiStream(int parameterIndex, java.io.InputStream x, int length)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setAsciiStream(parameterIndex, x, length);
 +    }
 +
 +    @Deprecated
 +    public void setUnicodeStream(int parameterIndex, java.io.InputStream x, int length)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setUnicodeStream(parameterIndex, x, length);
 +    }
 +
 +    public void setBinaryStream(int parameterIndex, java.io.InputStream x, int length)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setBinaryStream(parameterIndex, x, length);
 +    }
 +
 +    public void setObject(int parameterIndex, Object x, int targetSqlType, int scale)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setObject(parameterIndex, x, targetSqlType, scale);
 +    }
 +
 +    public void setObject(int parameterIndex, Object x, int targetSqlType)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setObject(parameterIndex, x, targetSqlType);
 +    }
 +
 +    public void setObject(int parameterIndex, Object x) throws SQLException {
 +        // TODO: set locally
 +        ps().setObject(parameterIndex, x);
 +    }
 +
 +    public void setCharacterStream(int parameterIndex,
 +                                   java.io.Reader reader,
 +                                   int length)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setCharacterStream(parameterIndex, reader, length);
 +    }
 +
 +    public void setRef(int i, Ref x) throws SQLException {
 +        // TODO: set locally
 +        ps().setRef(i, x);
 +    }
 +
 +    public void setBlob(int i, Blob x) throws SQLException {
 +        // TODO: set locally
 +        ps().setBlob(i, x);
 +    }
 +
 +    public void setClob(int i, Clob x) throws SQLException {
 +        // TODO: set locally
 +        ps().setClob(i, x);
 +    }
 +
 +    public void setArray(int i, Array x) throws SQLException {
 +        // TODO: set locally
 +        ps().setArray(i, x);
 +    }
 +
 +    public void setDate(int parameterIndex, java.sql.Date x, Calendar cal)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setDate(parameterIndex, x, cal);
 +    }
 +
 +    public void setTime(int parameterIndex, java.sql.Time x, Calendar cal)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setTime(parameterIndex, x, cal);
 +    }
 +
 +    public void setTimestamp(int parameterIndex, java.sql.Timestamp x, Calendar cal)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setTimestamp(parameterIndex, x, cal);
 +    }
 +
 +    public void setNull(int paramIndex, int sqlType, String typeName)
 +        throws SQLException
 +    {
 +        // TODO: set locally
 +        ps().setNull(paramIndex, sqlType, typeName);
 +    }
 +
 +    public void setURL(int parameterIndex, java.net.URL x) throws SQLException {
 +        // TODO: set locally
 +        ps().setURL(parameterIndex, x);
 +    }
 +
 +    public ResultSetMetaData getMetaData() throws SQLException {
 +        return ps().getMetaData();
 +    }
 +
 +    public ParameterMetaData getParameterMetaData() throws SQLException {
 +        return ps().getParameterMetaData();
 +    }
 +
 +    private PreparedStatement ps() {
 +        return (PreparedStatement) mStatement;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingStatement.java b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingStatement.java new file mode 100644 index 0000000..790778f --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/LoggingStatement.java @@ -0,0 +1,200 @@ +/*
 + * 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.repo.jdbc;
 +
 +import java.sql.*;
 +
 +import org.apache.commons.logging.Log;
 +
 +/**
 + * Statement returned by LoggingConnection;
 + *
 + * @author Brian S O'Neill
 + */
 +class LoggingStatement implements Statement {
 +    private final Connection mCon;
 +    protected final Log mLog;
 +    protected final Statement mStatement;
 +
 +    LoggingStatement(Log log, Connection con, Statement statement) {
 +        mLog = log;
 +        mCon = con;
 +        mStatement = statement;
 +    }
 +
 +    public ResultSet executeQuery(String sql) throws SQLException {
 +        mLog.debug(sql);
 +        return mStatement.executeQuery(sql);
 +    }
 +
 +    public int executeUpdate(String sql) throws SQLException {
 +        mLog.debug(sql);
 +        return mStatement.executeUpdate(sql);
 +    }
 +
 +    public boolean execute(String sql) throws SQLException {
 +        mLog.debug(sql);
 +        return mStatement.execute(sql);
 +    }
 +
 +    public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
 +        mLog.debug(sql);
 +        return mStatement.executeUpdate(sql, autoGeneratedKeys);
 +    }
 +
 +    public int executeUpdate(String sql, int columnIndexes[]) throws SQLException {
 +        mLog.debug(sql);
 +        return mStatement.executeUpdate(sql, columnIndexes);
 +    }
 +
 +    public int executeUpdate(String sql, String columnNames[]) throws SQLException {
 +        mLog.debug(sql);
 +        return mStatement.executeUpdate(sql, columnNames);
 +    }
 +
 +    public boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
 +        mLog.debug(sql);
 +        return mStatement.execute(sql, autoGeneratedKeys);
 +    }
 +
 +    public boolean execute(String sql, int columnIndexes[]) throws SQLException {
 +        mLog.debug(sql);
 +        return mStatement.execute(sql, columnIndexes);
 +    }
 +
 +    public boolean execute(String sql, String columnNames[]) throws SQLException {
 +        mLog.debug(sql);
 +        return mStatement.execute(sql, columnNames);
 +    }
 +
 +    public void addBatch(String sql) throws SQLException {
 +        // TODO: save locally
 +        mStatement.addBatch(sql);
 +    }
 +
 +    public void clearBatch() throws SQLException {
 +        // TODO: clear locally
 +        mStatement.clearBatch();
 +    }
 +
 +    public int[] executeBatch() throws SQLException {
 +        // TODO: log locally recorded batch
 +        return mStatement.executeBatch();
 +    }
 +
 +    public void close() throws SQLException {
 +        mStatement.close();
 +    }
 +
 +    public int getMaxFieldSize() throws SQLException {
 +        return mStatement.getMaxFieldSize();
 +    }
 +
 +    public void setMaxFieldSize(int max) throws SQLException {
 +        mStatement.setMaxFieldSize(max);
 +    }
 +
 +    public int getMaxRows() throws SQLException {
 +        return mStatement.getMaxRows();
 +    }
 +
 +    public void setMaxRows(int max) throws SQLException {
 +        mStatement.setMaxRows(max);
 +    }
 +
 +    public void setEscapeProcessing(boolean enable) throws SQLException {
 +        mStatement.setEscapeProcessing(enable);
 +    }
 +
 +    public int getQueryTimeout() throws SQLException {
 +        return mStatement.getQueryTimeout();
 +    }
 +
 +    public void setQueryTimeout(int seconds) throws SQLException {
 +        mStatement.setQueryTimeout(seconds);
 +    }
 +
 +    public void cancel() throws SQLException {
 +        mStatement.cancel();
 +    }
 +
 +    public SQLWarning getWarnings() throws SQLException {
 +        return mStatement.getWarnings();
 +    }
 +
 +    public void clearWarnings() throws SQLException {
 +        mStatement.clearWarnings();
 +    }
 +
 +    public void setCursorName(String name) throws SQLException {
 +        mStatement.setCursorName(name);
 +    }
 +
 +    public ResultSet getResultSet() throws SQLException {
 +        return mStatement.getResultSet();
 +    }
 +
 +    public int getUpdateCount() throws SQLException {
 +        return mStatement.getUpdateCount();
 +    }
 +
 +    public boolean getMoreResults() throws SQLException {
 +        return mStatement.getMoreResults();
 +    }
 +
 +    public void setFetchDirection(int direction) throws SQLException {
 +        mStatement.setFetchDirection(direction);
 +    }
 +
 +    public int getFetchDirection() throws SQLException {
 +        return mStatement.getFetchDirection();
 +    }
 +
 +    public void setFetchSize(int rows) throws SQLException {
 +        mStatement.setFetchSize(rows);
 +    }
 +
 +    public int getFetchSize() throws SQLException {
 +        return mStatement.getFetchSize();
 +    }
 +
 +    public int getResultSetConcurrency() throws SQLException {
 +        return mStatement.getResultSetConcurrency();
 +    }
 +
 +    public int getResultSetType()  throws SQLException {
 +        return mStatement.getResultSetType();
 +    }
 +
 +    public Connection getConnection() {
 +        return mCon;
 +    }
 +
 +    public boolean getMoreResults(int current) throws SQLException {
 +        return mStatement.getMoreResults(current);
 +    }
 +
 +    public ResultSet getGeneratedKeys() throws SQLException {
 +        return mStatement.getGeneratedKeys();
 +    }
 +
 +    public int getResultSetHoldability() throws SQLException {
 +        return mStatement.getResultSetHoldability();
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/SimpleDataSource.java b/src/main/java/com/amazon/carbonado/repo/jdbc/SimpleDataSource.java new file mode 100644 index 0000000..9c93fec --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/SimpleDataSource.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.repo.jdbc;
 +
 +import java.sql.Connection;
 +import java.sql.DriverManager;
 +import java.sql.SQLException;
 +import java.io.PrintWriter;
 +import java.util.Properties;
 +
 +import javax.sql.DataSource;
 +
 +/**
 + * SimpleDataSource does not implement any connection pooling.
 + *
 + * @author Brian S O'Neill
 + */
 +public class SimpleDataSource implements DataSource {
 +    private final String mURL;
 +    private final Properties mProperties;
 +
 +    private PrintWriter mLogWriter;
 +
 +    public SimpleDataSource(String driverClass, String driverURL, String username, String password)
 +        throws SQLException
 +    {
 +        try {
 +            Class.forName(driverClass);
 +        } catch (ClassNotFoundException e) {
 +            SQLException e2 = new SQLException();
 +            e2.initCause(e);
 +            throw e2;
 +        }
 +        mURL = driverURL;
 +        mProperties = new Properties();
 +        if (username != null) {
 +            mProperties.put("user", username);
 +        }
 +        if (password != null) {
 +            mProperties.put("password", password);
 +        }
 +    }
 +
 +    public Connection getConnection() throws SQLException {
 +        return DriverManager.getConnection(mURL, mProperties);
 +    }
 +
 +    public Connection getConnection(String username, String password) throws SQLException {
 +        Properties props = new Properties();
 +        if (username != null) {
 +            props.put("user", username);
 +        }
 +        if (password != null) {
 +            props.put("password", password);
 +        }
 +        return DriverManager.getConnection(mURL, props);
 +    }
 +
 +    public PrintWriter getLogWriter() throws SQLException {
 +        return mLogWriter;
 +    }
 +
 +    public void setLogWriter(PrintWriter writer) throws SQLException {
 +        mLogWriter = writer;
 +    }
 +
 +    public void setLoginTimeout(int seconds) throws SQLException {
 +    }
 +
 +    public int getLoginTimeout() throws SQLException {
 +        return 0;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/jdbc/package-info.java b/src/main/java/com/amazon/carbonado/repo/jdbc/package-info.java new file mode 100644 index 0000000..e7300a5 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/jdbc/package-info.java @@ -0,0 +1,28 @@ +/*
 + * 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.
 + */
 +
 +/**
 + * Repository implementation that connects to an external SQL database via
 + * JDBC. JDBC repository is not independent of the underlying database schema,
 + * and so it requires matching tables and columns in the database. It will not
 + * alter or create tables. Use the {@link com.amazon.carbonado.Alias Alias}
 + * annotation to control precisely which tables and columns must be matched up.
 + *
 + * @see com.amazon.carbonado.repo.jdbc.JDBCRepositoryBuilder
 + */
 +package com.amazon.carbonado.repo.jdbc;
 diff --git a/src/main/java/com/amazon/carbonado/repo/logging/CommonsLog.java b/src/main/java/com/amazon/carbonado/repo/logging/CommonsLog.java new file mode 100644 index 0000000..f3fca4b --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/logging/CommonsLog.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.repo.logging;
 +
 +/**
 + * Log implementation that uses Jakarta Commons Logging at debug level.
 + *
 + * @author Brian S O'Neill
 + */
 +public class CommonsLog implements Log {
 +    private final org.apache.commons.logging.Log mLog;
 +
 +    public CommonsLog(org.apache.commons.logging.Log log) {
 +        mLog = log;
 +    }
 +
 +    public CommonsLog(Class clazz) {
 +        mLog = org.apache.commons.logging.LogFactory.getLog(clazz);
 +    }
 +
 +    public boolean isEnabled() {
 +        return mLog.isDebugEnabled();
 +    }
 +
 +    public void write(String message) {
 +        mLog.debug(message);
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/logging/Log.java b/src/main/java/com/amazon/carbonado/repo/logging/Log.java new file mode 100644 index 0000000..d5ac74e --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/logging/Log.java @@ -0,0 +1,30 @@ +/*
 + * 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.repo.logging;
 +
 +/**
 + * Very simple Log interface.
 + *
 + * @author Brian S O'Neill
 + */
 +public interface Log {
 +    boolean isEnabled();
 +
 +    void write(String message);
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/logging/LogAccessCapability.java b/src/main/java/com/amazon/carbonado/repo/logging/LogAccessCapability.java new file mode 100644 index 0000000..fe3a59a --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/logging/LogAccessCapability.java @@ -0,0 +1,30 @@ +/*
 + * 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.repo.logging;
 +
 +import com.amazon.carbonado.capability.Capability;
 +
 +/**
 + * Provides access to the Log.
 + *
 + * @author Brian S O'Neill
 + */
 +public interface LogAccessCapability extends Capability {
 +    Log getLog();
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/logging/LoggingQuery.java b/src/main/java/com/amazon/carbonado/repo/logging/LoggingQuery.java new file mode 100644 index 0000000..2535c74 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/logging/LoggingQuery.java @@ -0,0 +1,98 @@ +/*
 + * 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.repo.logging;
 +
 +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.Storage;
 +
 +import com.amazon.carbonado.spi.WrappedQuery;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +class LoggingQuery<S extends Storable> extends WrappedQuery<S> {
 +    private final LoggingStorage<S> mStorage;
 +
 +    LoggingQuery(LoggingStorage<S> storage, Query<S> query) {
 +        super(query);
 +        mStorage = storage;
 +    }
 +
 +    public Cursor<S> fetch() throws FetchException {
 +        Log log = mStorage.mRepo.getLog();
 +        if (log.isEnabled()) {
 +            log.write("Query.fetch() on " + this);
 +        }
 +        return super.fetch();
 +    }
 +
 +    public S loadOne() throws FetchException {
 +        Log log = mStorage.mRepo.getLog();
 +        if (log.isEnabled()) {
 +            log.write("Query.loadOne() on " + this);
 +        }
 +        return super.loadOne();
 +    }
 +
 +    public S tryLoadOne() throws FetchException {
 +        Log log = mStorage.mRepo.getLog();
 +        if (log.isEnabled()) {
 +            log.write("Query.tryLoadOne() on " + this);
 +        }
 +        return super.tryLoadOne();
 +    }
 +
 +    public void deleteOne() throws PersistException {
 +        Log log = mStorage.mRepo.getLog();
 +        if (log.isEnabled()) {
 +            log.write("Query.deleteOne() on " + this);
 +        }
 +        super.deleteOne();
 +    }
 +
 +    public boolean tryDeleteOne() throws PersistException {
 +        Log log = mStorage.mRepo.getLog();
 +        if (log.isEnabled()) {
 +            log.write("Query.tryDeleteOne() on " + this);
 +        }
 +        return super.tryDeleteOne();
 +    }
 +
 +    public void deleteAll() throws PersistException {
 +        Log log = mStorage.mRepo.getLog();
 +        if (log.isEnabled()) {
 +            log.write("Query.deleteAll() on " + this);
 +        }
 +        super.deleteAll();
 +    }
 +
 +    protected S wrap(S storable) {
 +        return mStorage.wrap(storable);
 +    }
 +
 +    protected WrappedQuery<S> newInstance(Query<S> query) {
 +        return new LoggingQuery<S>(mStorage, query);
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/logging/LoggingRepository.java b/src/main/java/com/amazon/carbonado/repo/logging/LoggingRepository.java new file mode 100644 index 0000000..e44e3b7 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/logging/LoggingRepository.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.repo.logging;
 +
 +import java.util.IdentityHashMap;
 +import java.util.Map;
 +
 +import com.amazon.carbonado.IsolationLevel;
 +import com.amazon.carbonado.Repository;
 +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;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +class LoggingRepository implements Repository, LogAccessCapability {
 +    private final RepositoryReference mRootRef;
 +    private final Repository mRepo;
 +    private final Log mLog;
 +
 +    // Map of storages by storable class
 +    private final Map<Class<?>, LoggingStorage<?>> mStorages;
 +
 +    LoggingRepository(RepositoryReference rootRef, Repository actual, Log log) {
 +        mRootRef = rootRef;
 +        mRepo = actual;
 +        mLog = log;
 +
 +        mStorages = new IdentityHashMap<Class<?>, LoggingStorage<?>>();
 +    }
 +
 +    public String getName() {
 +        return mRepo.getName();
 +    }
 +
 +    public <S extends Storable> Storage<S> storageFor(Class<S> type)
 +        throws SupportException, RepositoryException
 +    {
 +        synchronized (mStorages) {
 +            LoggingStorage storage = mStorages.get(type);
 +            if (storage == null) {
 +                storage = new LoggingStorage(this, mRepo.storageFor(type));
 +                mStorages.put(type, storage);
 +            }
 +            return storage;
 +        }
 +    }
 +
 +    public Transaction enterTransaction() {
 +        mLog.write("Repository.enterTransaction()");
 +        return new LoggingTransaction(mLog, mRepo.enterTransaction());
 +    }
 +
 +    public Transaction enterTransaction(IsolationLevel level) {
 +        if (mLog.isEnabled()) {
 +            mLog.write("Repository.enterTransaction(" + level + ')');
 +        }
 +        return new LoggingTransaction(mLog, mRepo.enterTransaction(level));
 +    }
 +
 +    public Transaction enterTopTransaction(IsolationLevel level) {
 +        if (mLog.isEnabled()) {
 +            mLog.write("Repository.enterTopTransaction(" + level + ')');
 +        }
 +        return new LoggingTransaction(mLog, mRepo.enterTopTransaction(level));
 +    }
 +
 +    public IsolationLevel getTransactionIsolationLevel() {
 +        return mRepo.getTransactionIsolationLevel();
 +    }
 +
 +    public <C extends Capability> C getCapability(Class<C> capabilityType) {
 +        if (capabilityType.isInstance(this)) {
 +            return (C) this;
 +        }
 +        return mRepo.getCapability(capabilityType);
 +    }
 +
 +    public void close() {
 +        mRepo.close();
 +    }
 +
 +    public Log getLog() {
 +        return mLog;
 +    }
 +
 +    Repository getRootRepository() {
 +        return mRootRef.get();
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/logging/LoggingRepositoryBuilder.java b/src/main/java/com/amazon/carbonado/repo/logging/LoggingRepositoryBuilder.java new file mode 100644 index 0000000..5f06cd5 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/logging/LoggingRepositoryBuilder.java @@ -0,0 +1,150 @@ +/*
 + * 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.repo.logging;
 +
 +import java.util.Collection;
 +
 +import com.amazon.carbonado.ConfigurationException;
 +import com.amazon.carbonado.Repository;
 +import com.amazon.carbonado.RepositoryBuilder;
 +import com.amazon.carbonado.RepositoryException;
 +
 +import com.amazon.carbonado.spi.AbstractRepositoryBuilder;
 +
 +/**
 + * Repository implementation which logs activity against it. By default, all
 + * logged messages are at the debug level.
 + *
 + * <p>
 + * The following extra capabilities are supported:
 + * <ul>
 + * <li>{@link LogAccessCapability}
 + * </ul>
 + *
 + * Example:
 + *
 + * <pre>
 + * LoggingRepositoryBuilder loggingBuilder = new LoggingRepositoryBuilder();
 + * loggingBuilder.setActualRepositoryBuilder(...);
 + * Repository repo = loggingBuilder.build();
 + * </pre>
 + *
 + * @author Brian S O'Neill
 + */
 +public class LoggingRepositoryBuilder extends AbstractRepositoryBuilder {
 +    private String mName;
 +    private Boolean mMaster;
 +    private Log mLog;
 +    private RepositoryBuilder mRepoBuilder;
 +
 +    public LoggingRepositoryBuilder() {
 +    }
 +
 +    public Repository build(RepositoryReference rootRef) throws RepositoryException {
 +        if (mName == null) {
 +            if (mRepoBuilder != null) {
 +                mName = mRepoBuilder.getName();
 +            }
 +        }
 +
 +        assertReady();
 +
 +        if (mLog == null) {
 +            mLog = new CommonsLog(LoggingRepository.class);
 +        }
 +
 +        String originalName = mRepoBuilder.getName();
 +        boolean originalIsMaster = mRepoBuilder.isMaster();
 +
 +        boolean enabled = mLog.isEnabled();
 +        boolean master = mMaster != null ? mMaster : originalIsMaster;
 +
 +        Repository actual;
 +        try {
 +            if (enabled) {
 +                mRepoBuilder.setName("Logging " + mName);
 +            }
 +            mRepoBuilder.setMaster(master);
 +            actual = mRepoBuilder.build(rootRef);
 +        } finally {
 +            mRepoBuilder.setName(originalName);
 +            mRepoBuilder.setMaster(originalIsMaster);
 +        }
 +
 +        if (!enabled) {
 +            return actual;
 +        }
 +
 +        Repository repo = new LoggingRepository(rootRef, actual, mLog);
 +        rootRef.set(repo);
 +        return repo;
 +    }
 +
 +    public void setName(String name) {
 +        mName = name;
 +    }
 +
 +    public String getName() {
 +        return mName;
 +    }
 +
 +    public void setMaster(boolean master) {
 +        mMaster = master;
 +    }
 +
 +    public boolean isMaster() {
 +        return mMaster;
 +    }
 +
 +    /**
 +     * Set the Log to use. If null, use default. Log must be enabled when build
 +     * is called, or else no logging is ever performed.
 +     */
 +    public void setLog(Log log) {
 +        mLog = log;
 +    }
 +
 +    /**
 +     * Return the Log to use. If null, use default.
 +     */
 +    public Log getLog() {
 +        return mLog;
 +    }
 +
 +    /**
 +     * Set the Repository to wrap all calls to.
 +     */
 +    public void setActualRepositoryBuilder(RepositoryBuilder builder) {
 +        mRepoBuilder = builder;
 +    }
 +
 +    /**
 +     * Returns the Repository that all calls are wrapped to.
 +     */
 +    public RepositoryBuilder getActualRepositoryBuilder() {
 +        return mRepoBuilder;
 +    }
 +
 +    public void errorCheck(Collection<String> messages) throws ConfigurationException {
 +        super.errorCheck(messages);
 +        if (mRepoBuilder == null) {
 +            messages.add("Actual repository builder must be set");
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/logging/LoggingStorage.java b/src/main/java/com/amazon/carbonado/repo/logging/LoggingStorage.java new file mode 100644 index 0000000..93b5a33 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/logging/LoggingStorage.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.repo.logging;
 +
 +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.spi.WrappedStorage;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +class LoggingStorage<S extends Storable> extends WrappedStorage<S> {
 +    final LoggingRepository mRepo;
 +
 +    LoggingStorage(LoggingRepository repo, Storage<S> storage) {
 +        super(storage);
 +        mRepo = repo;
 +    }
 +
 +    protected S wrap(S storable) {
 +        return super.wrap(storable);
 +    }
 +
 +    protected Query<S> wrap(Query<S> query) {
 +        return new LoggingQuery<S>(this, query);
 +    }
 +
 +    protected Support createSupport(S storable) {
 +        return new Handler(storable);
 +    }
 +
 +    private class Handler extends Support {
 +        private final S mStorable;
 +
 +        Handler(S storable) {
 +            mStorable = storable;
 +        }
 +
 +        public Repository getRootRepository() {
 +            return mRepo.getRootRepository();
 +        }
 +
 +        public boolean isPropertySupported(String propertyName) {
 +            return mStorable.isPropertySupported(propertyName);
 +        }
 +
 +        public void load() throws FetchException {
 +            Log log = mRepo.getLog();
 +            if (log.isEnabled()) {
 +                log.write("Storable.load() on " + mStorable.toStringKeyOnly());
 +            }
 +            mStorable.load();
 +        }
 +
 +        public boolean tryLoad() throws FetchException {
 +            Log log = mRepo.getLog();
 +            if (log.isEnabled()) {
 +                log.write("Storable.tryLoad() on " + mStorable.toStringKeyOnly());
 +            }
 +            return mStorable.tryLoad();
 +        }
 +
 +        public void insert() throws PersistException {
 +            Log log = mRepo.getLog();
 +            if (log.isEnabled()) {
 +                log.write("Storable.insert() on " + mStorable.toString());
 +            }
 +            mStorable.insert();
 +        }
 +
 +        public boolean tryInsert() throws PersistException {
 +            Log log = mRepo.getLog();
 +            if (log.isEnabled()) {
 +                log.write("Storable.tryInsert() on " + mStorable.toString());
 +            }
 +            return mStorable.tryInsert();
 +        }
 +
 +        public void update() throws PersistException {
 +            Log log = mRepo.getLog();
 +            if (log.isEnabled()) {
 +                log.write("Storable.update() on " + mStorable.toString());
 +            }
 +            mStorable.update();
 +        }
 +
 +        public boolean tryUpdate() throws PersistException {
 +            Log log = mRepo.getLog();
 +            if (log.isEnabled()) {
 +                log.write("Storable.tryUpdate() on " + mStorable.toString());
 +            }
 +            return mStorable.tryUpdate();
 +        }
 +
 +        public void delete() throws PersistException {
 +            Log log = mRepo.getLog();
 +            if (log.isEnabled()) {
 +                log.write("Storable.delete() on " + mStorable.toStringKeyOnly());
 +            }
 +            mStorable.delete();
 +        }
 +
 +        public boolean tryDelete() throws PersistException {
 +            Log log = mRepo.getLog();
 +            if (log.isEnabled()) {
 +                log.write("Storable.tryDelete() on " + mStorable.toStringKeyOnly());
 +            }
 +            return mStorable.tryDelete();
 +        }
 +
 +        public Support createSupport(S storable) {
 +            return new Handler(storable);
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/logging/LoggingTransaction.java b/src/main/java/com/amazon/carbonado/repo/logging/LoggingTransaction.java new file mode 100644 index 0000000..7deac92 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/logging/LoggingTransaction.java @@ -0,0 +1,69 @@ +/*
 + * 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.repo.logging;
 +
 +import java.util.concurrent.TimeUnit;
 +
 +import com.amazon.carbonado.IsolationLevel;
 +import com.amazon.carbonado.PersistException;
 +import com.amazon.carbonado.Transaction;
 +
 +/**
 + *
 + *
 + * @author Brian S O'Neill
 + */
 +class LoggingTransaction implements Transaction {
 +    private final Log mLog;
 +    private final Transaction mTxn;
 +
 +    LoggingTransaction(Log log, Transaction txn) {
 +        mLog = log;
 +        mTxn = txn;
 +    }
 +
 +    public void commit() throws PersistException {
 +        mLog.write("Transaction.commit()");
 +        mTxn.commit();
 +    }
 +
 +    public void exit() throws PersistException {
 +        mLog.write("Transaction.exit()");
 +        mTxn.exit();
 +    }
 +
 +    public void setForUpdate(boolean forUpdate) {
 +        if (mLog.isEnabled()) {
 +            mLog.write("Transaction.setForUpdate(" + forUpdate + ')');
 +        }
 +        mTxn.setForUpdate(forUpdate);
 +    }
 +
 +    public boolean isForUpdate() {
 +        return mTxn.isForUpdate();
 +    }
 +
 +    public void setDesiredLockTimeout(int timeout, TimeUnit unit) {
 +        mTxn.setDesiredLockTimeout(timeout, unit);
 +    }
 +
 +    public IsolationLevel getIsolationLevel() {
 +        return mTxn.getIsolationLevel();
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/logging/package-info.java b/src/main/java/com/amazon/carbonado/repo/logging/package-info.java new file mode 100644 index 0000000..860a9ae --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/logging/package-info.java @@ -0,0 +1,25 @@ +/*
 + * 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.
 + */
 +
 +/**
 + * Repository implementation which logs activity against it. By default, all
 + * logged messages are at the debug level.
 + *
 + * @see com.amazon.carbonado.repo.logging.LoggingRepositoryBuilder
 + */
 +package com.amazon.carbonado.repo.logging;
 diff --git a/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepository.java b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepository.java new file mode 100644 index 0000000..5003053 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepository.java @@ -0,0 +1,583 @@ +/*
 + * 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.repo.replicated;
 +
 +import java.util.Comparator;
 +import java.util.IdentityHashMap;
 +import java.util.LinkedHashSet;
 +import java.util.Map;
 +import java.util.Set;
 +
 +import java.util.concurrent.ArrayBlockingQueue;
 +import java.util.concurrent.BlockingQueue;
 +import java.util.concurrent.TimeUnit;
 +
 +import org.apache.commons.logging.Log;
 +import org.apache.commons.logging.LogFactory;
 +
 +import org.cojen.util.BeanComparator;
 +
 +import com.amazon.carbonado.Cursor;
 +import com.amazon.carbonado.FetchInterruptedException;
 +import com.amazon.carbonado.IsolationLevel;
 +import com.amazon.carbonado.MalformedTypeException;
 +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.Transaction;
 +
 +import com.amazon.carbonado.capability.Capability;
 +import com.amazon.carbonado.capability.IndexInfo;
 +import com.amazon.carbonado.capability.IndexInfoCapability;
 +import com.amazon.carbonado.capability.ResyncCapability;
 +import com.amazon.carbonado.capability.ShutdownCapability;
 +import com.amazon.carbonado.capability.StorableInfoCapability;
 +
 +import com.amazon.carbonado.info.Direction;
 +import com.amazon.carbonado.info.StorableIntrospector;
 +
 +import com.amazon.carbonado.spi.TransactionPair;
 +
 +import com.amazon.carbonado.util.Throttle;
 +
 +/**
 + * A ReplicatedRepository binds two repositories together.  One will be used
 + * for reading (the replica), and the other will be used for writing; changes
 + * to the master repository will be copied to the replica.
 + *
 + * @author Don Schneider
 + */
 +class ReplicatedRepository
 +    implements Repository,
 +               ResyncCapability,
 +               ShutdownCapability,
 +               StorableInfoCapability
 +{
 +    // Constants used by resync method.
 +    private static final int RESYNC_QUEUE_SIZE = 1000;
 +    private static final long RESYNC_QUEUE_TIMEOUT_MS = 30000;
 +
 +    /**
 +     * Utility method to select the natural ordering of a storage, by looking
 +     * for a clustered index on the primary key. Returns null if no clustered
 +     * index was found.
 +     *
 +     * TODO: Try to incorporate this into standard storage interface somehow.
 +     */
 +    private static String[] selectNaturalOrder(Repository repo, Class<? extends Storable> type)
 +        throws RepositoryException
 +    {
 +        IndexInfoCapability capability = repo.getCapability(IndexInfoCapability.class);
 +        if (capability == null) {
 +            return null;
 +        }
 +        IndexInfo info = null;
 +        for (IndexInfo candidate : capability.getIndexInfo(type)) {
 +            if (candidate.isClustered()) {
 +                info = candidate;
 +                break;
 +            }
 +        }
 +        if (info == null) {
 +            return null;
 +        }
 +
 +        // Verify index is part of primary key.
 +        Set<String> pkSet = StorableIntrospector.examine(type).getPrimaryKeyProperties().keySet();
 +
 +        String[] propNames = info.getPropertyNames();
 +        for (String prop : propNames) {
 +            if (!pkSet.contains(prop)) {
 +                return null;
 +            }
 +        }
 +
 +        String[] orderBy = new String[pkSet.size()];
 +
 +        Direction[] directions = info.getPropertyDirections();
 +
 +        // Clone to remove elements.
 +        pkSet = new LinkedHashSet<String>(pkSet);
 +
 +        int i;
 +        for (i=0; i<propNames.length; i++) {
 +            orderBy[i] = ((directions[i] == Direction.DESCENDING) ? "-" : "+") + propNames[i];
 +            pkSet.remove(propNames[i]);
 +        }
 +
 +        // Append any remaining pk properties, to ensure complete ordering.
 +        if (pkSet.size() > 0) {
 +            for (String prop : pkSet) {
 +                orderBy[i++] = prop;
 +            }
 +        }
 +
 +        return orderBy;
 +    }
 +
 +    private String mName;
 +    private Repository mReplicaRepository;
 +    private Repository mMasterRepository;
 +
 +    // Map of storages by storable class
 +    private final Map<Class<?>, ReplicatedStorage<?>> mStorages;
 +
 +    ReplicatedRepository(String aName,
 +                         Repository aReplicaRepository,
 +                         Repository aMasterRepository) {
 +        mName = aName;
 +        mReplicaRepository = aReplicaRepository;
 +        mMasterRepository = aMasterRepository;
 +
 +        mStorages = new IdentityHashMap<Class<?>, ReplicatedStorage<?>>();
 +    }
 +
 +    public String getName() {
 +        return mName;
 +    }
 +
 +    Repository getReplicaRepository() {
 +        return mReplicaRepository;
 +    }
 +
 +    Repository getMasterRepository() {
 +        return mMasterRepository;
 +    }
 +
 +    @SuppressWarnings("unchecked")
 +    public <S extends Storable> Storage<S> storageFor(Class<S> type)
 +        throws MalformedTypeException, SupportException, RepositoryException
 +    {
 +        synchronized (mStorages) {
 +            ReplicatedStorage storage = mStorages.get(type);
 +            if (storage == null) {
 +                // Examine and throw exception if there is a problem.
 +                StorableIntrospector.examine(type);
 +
 +                storage = createStorage(type);
 +                mStorages.put(type, storage);
 +            }
 +            return storage;
 +        }
 +    }
 +
 +    private <S extends Storable> ReplicatedStorage<S> createStorage(Class<S> type)
 +        throws SupportException, RepositoryException
 +    {
 +        return new ReplicatedStorage<S>(this, type);
 +    }
 +
 +    public Transaction enterTransaction() {
 +        return new TransactionPair(mMasterRepository.enterTransaction(),
 +                                   mReplicaRepository.enterTransaction());
 +    }
 +
 +    public Transaction enterTransaction(IsolationLevel level) {
 +        return new TransactionPair(mMasterRepository.enterTransaction(level),
 +                                   mReplicaRepository.enterTransaction(level));
 +    }
 +
 +    public Transaction enterTopTransaction(IsolationLevel level) {
 +        return new TransactionPair(mMasterRepository.enterTopTransaction(level),
 +                                   mReplicaRepository.enterTopTransaction(level));
 +    }
 +
 +    public IsolationLevel getTransactionIsolationLevel() {
 +        IsolationLevel replicaLevel = mReplicaRepository.getTransactionIsolationLevel();
 +        if (replicaLevel == null) {
 +            return null;
 +        }
 +        IsolationLevel masterLevel = mMasterRepository.getTransactionIsolationLevel();
 +        if (masterLevel == null) {
 +            return null;
 +        }
 +        return replicaLevel.lowestCommon(masterLevel);
 +    }
 +
 +    @SuppressWarnings("unchecked")
 +    public <C extends Capability> C getCapability(Class<C> capabilityType) {
 +        if (capabilityType.isInstance(this)) {
 +            if (ShutdownCapability.class.isAssignableFrom(capabilityType)) {
 +                if (mReplicaRepository.getCapability(capabilityType) == null &&
 +                    mMasterRepository.getCapability(capabilityType) == null) {
 +
 +                    return null;
 +                }
 +            }
 +            return (C) this;
 +        }
 +
 +        C cap = mMasterRepository.getCapability(capabilityType);
 +        if (cap == null) {
 +            cap = mReplicaRepository.getCapability(capabilityType);
 +        }
 +
 +        return cap;
 +    }
 +
 +    public void close() {
 +        mReplicaRepository.close();
 +        mMasterRepository.close();
 +    }
 +
 +    public String[] getUserStorableTypeNames() throws RepositoryException {
 +        StorableInfoCapability replicaCap =
 +            mReplicaRepository.getCapability(StorableInfoCapability.class);
 +        StorableInfoCapability masterCap =
 +            mMasterRepository.getCapability(StorableInfoCapability.class);
 +
 +        if (replicaCap == null) {
 +            if (masterCap == null) {
 +                return new String[0];
 +            }
 +            return masterCap.getUserStorableTypeNames();
 +        } else if (masterCap == null) {
 +            return replicaCap.getUserStorableTypeNames();
 +        }
 +
 +        // Merge the two sets together.
 +        Set<String> names = new LinkedHashSet<String>();
 +        for (String name : replicaCap.getUserStorableTypeNames()) {
 +            names.add(name);
 +        }
 +        for (String name : masterCap.getUserStorableTypeNames()) {
 +            names.add(name);
 +        }
 +
 +        return names.toArray(new String[names.size()]);
 +    }
 +
 +    public boolean isSupported(Class<Storable> type) {
 +        StorableInfoCapability replicaCap =
 +            mReplicaRepository.getCapability(StorableInfoCapability.class);
 +        StorableInfoCapability masterCap =
 +            mMasterRepository.getCapability(StorableInfoCapability.class);
 +
 +        return (masterCap == null || masterCap.isSupported(type))
 +            && (replicaCap == null || replicaCap.isSupported(type));
 +    }
 +
 +    public boolean isPropertySupported(Class<Storable> type, String name) {
 +        StorableInfoCapability replicaCap =
 +            mReplicaRepository.getCapability(StorableInfoCapability.class);
 +        StorableInfoCapability masterCap =
 +            mMasterRepository.getCapability(StorableInfoCapability.class);
 +
 +        return (masterCap == null || masterCap.isPropertySupported(type, name))
 +            && (replicaCap == null || replicaCap.isPropertySupported(type, name));
 +    }
 +
 +    public boolean isAutoShutdownEnabled() {
 +        ShutdownCapability cap = mReplicaRepository.getCapability(ShutdownCapability.class);
 +        if (cap != null && cap.isAutoShutdownEnabled()) {
 +            return true;
 +        }
 +        cap = mMasterRepository.getCapability(ShutdownCapability.class);
 +        if (cap != null && cap.isAutoShutdownEnabled()) {
 +            return true;
 +        }
 +        return false;
 +    }
 +
 +    public void setAutoShutdownEnabled(boolean enabled) {
 +        ShutdownCapability cap = mReplicaRepository.getCapability(ShutdownCapability.class);
 +        if (cap != null) {
 +            cap.setAutoShutdownEnabled(enabled);
 +        }
 +        cap = mMasterRepository.getCapability(ShutdownCapability.class);
 +        if (cap != null) {
 +            cap.setAutoShutdownEnabled(enabled);
 +        }
 +    }
 +
 +    public void shutdown() {
 +        ShutdownCapability cap = mReplicaRepository.getCapability(ShutdownCapability.class);
 +        if (cap != null) {
 +            cap.shutdown();
 +        } else {
 +            mReplicaRepository.close();
 +        }
 +        cap = mMasterRepository.getCapability(ShutdownCapability.class);
 +        if (cap != null) {
 +            cap.shutdown();
 +        } else {
 +            mMasterRepository.close();
 +        }
 +    }
 +
 +    /**
 +     * Repairs replicated storables by synchronizing the replica repository
 +     * against the master repository.
 +     *
 +     * @param type type of storable to re-sync
 +     * @param desiredSpeed throttling parameter - 1.0 = full speed, 0.5 = half
 +     * speed, 0.1 = one-tenth speed, etc
 +     * @param filter optional query filter to limit which objects get re-sync'ed
 +     * @param filterValues filter values for optional filter
 +     */
 +    public <S extends Storable> void resync(Class<S> type,
 +                                            double desiredSpeed,
 +                                            String filter,
 +                                            Object... filterValues)
 +        throws RepositoryException
 +    {
 +        Storage<S> replicaStorage, masterStorage;
 +        replicaStorage = mReplicaRepository.storageFor(type);
 +        masterStorage = mMasterRepository.storageFor(type);
 +
 +        Query<S> replicaQuery, masterQuery;
 +        if (filter == null) {
 +            replicaQuery = replicaStorage.query();
 +            masterQuery = masterStorage.query();
 +        } else {
 +            replicaQuery = replicaStorage.query(filter).withValues(filterValues);
 +            masterQuery = masterStorage.query(filter).withValues(filterValues);
 +        }
 +
 +        // Order both queries the same so that they can be run in parallel.
 +        String[] orderBy = selectNaturalOrder(mMasterRepository, type);
 +        if (orderBy == null) {
 +            orderBy = selectNaturalOrder(mReplicaRepository, type);
 +            if (orderBy == null) {
 +                Set<String> pkSet =
 +                    StorableIntrospector.examine(type).getPrimaryKeyProperties().keySet();
 +                orderBy = pkSet.toArray(new String[0]);
 +            }
 +        }
 +
 +        BeanComparator bc = BeanComparator.forClass(type);
 +        for (String order : orderBy) {
 +            bc = bc.orderBy(order);
 +            bc = bc.caseSensitive();
 +        }
 +
 +        replicaQuery = replicaQuery.orderBy(orderBy);
 +        masterQuery = masterQuery.orderBy(orderBy);
 +
 +        Throttle throttle;
 +        if (desiredSpeed >= 1.0) {
 +            throttle = null;
 +        } else {
 +            if (desiredSpeed < 0.0) {
 +                desiredSpeed = 0.0;
 +            }
 +            // 50 samples
 +            throttle = new Throttle(50);
 +        }
 +
 +        Cursor<S> replicaCursor = replicaQuery.fetch();
 +        try {
 +            Cursor<S> masterCursor = masterQuery.fetch();
 +            try {
 +                resync(((ReplicatedStorage) storageFor(type)).getTrigger(),
 +                       replicaCursor,
 +                       masterCursor,
 +                       throttle, desiredSpeed,
 +                       bc);
 +            } finally {
 +                masterCursor.close();
 +            }
 +        } finally {
 +            replicaCursor.close();
 +        }
 +    }
 +
 +    @SuppressWarnings("unchecked")
 +    private <S extends Storable> void resync(ReplicationTrigger<S> trigger,
 +                                             Cursor<S> replicaCursor,
 +                                             Cursor<S> masterCursor,
 +                                             Throttle throttle, double desiredSpeed,
 +                                             Comparator comparator)
 +        throws RepositoryException
 +    {
 +        // Enqueue resyncs to a separate thread since open cursors hold locks
 +        // on currently referenced entries.
 +        BlockingQueue<Runnable> resyncQueue =
 +            new ArrayBlockingQueue<Runnable>(RESYNC_QUEUE_SIZE, true);
 +        ResyncThread resyncThread = new ResyncThread(resyncQueue);
 +        resyncThread.start();
 +
 +        try {
 +            S replicaEntry = null;
 +            S masterEntry = null;
 +
 +            while (true) {
 +                if (throttle != null) {
 +                    try {
 +                        // 100 millisecond clock precision
 +                        throttle.throttle(desiredSpeed, 100);
 +                    } catch (InterruptedException e) {
 +                        throw new FetchInterruptedException(e);
 +                    }
 +                }
 +
 +                if (replicaEntry == null && replicaCursor.hasNext()) {
 +                    replicaEntry = replicaCursor.next();
 +                }
 +
 +                if (masterEntry == null && masterCursor.hasNext()) {
 +                    masterEntry = masterCursor.next();
 +                }
 +
 +                // Comparator should treat null as high.
 +                int compare = comparator.compare(replicaEntry, masterEntry);
 +
 +                if (compare < 0) {
 +                    // Bogus exists only in replica so delete it.
 +                    resyncThread.addResyncTask(trigger, replicaEntry, null);
 +                    // Allow replica to advance.
 +                    replicaEntry = null;
 +                } else if (compare > 0) {
 +                    // Replica cursor is missing an entry so copy it.
 +                    resyncThread.addResyncTask(trigger, null, masterEntry);
 +                    // Allow master to advance.
 +                    masterEntry = null;
 +                } else {
 +                    if (replicaEntry == null && masterEntry == null) {
 +                        // Both cursors exhausted -- resync is complete.
 +                        break;
 +                    }
 +
 +                    if (!replicaEntry.equalProperties(masterEntry)) {
 +                        // Replica is stale.
 +                        resyncThread.addResyncTask(trigger, replicaEntry, masterEntry);
 +                    }
 +
 +                    // Entries are synchronized so allow both cursors to advance.
 +                    replicaEntry = null;
 +                    masterEntry = null;
 +                }
 +            }
 +        } finally {
 +            resyncThread.waitUntilDone();
 +        }
 +    }
 +
 +    // TODO: Use TaskQueueThread
 +
 +    private static class ResyncThread extends Thread {
 +        private static final int
 +            STATE_RUNNING = 0,
 +            STATE_SHOULD_STOP = 1,
 +            STATE_STOPPED = 2;
 +
 +        private static final Runnable STOP_TASK = new Runnable() {public void run() {}};
 +
 +        private final BlockingQueue<Runnable> mQueue;
 +
 +        private int mState = STATE_RUNNING;
 +
 +        ResyncThread(BlockingQueue<Runnable> queue) {
 +            super("ReplicatedRepository Resync");
 +            mQueue = queue;
 +        }
 +
 +        public void run() {
 +            try {
 +                while (true) {
 +                    boolean isStopping;
 +                    synchronized (this) {
 +                        isStopping = mState != STATE_RUNNING;
 +                    }
 +
 +                    Runnable task;
 +                    if (isStopping) {
 +                        // Poll the queue so this thread doesn't block when it
 +                        // should be stopping.
 +                        task = mQueue.poll();
 +                    } else {
 +                        try {
 +                            task = mQueue.take();
 +                        } catch (InterruptedException e) {
 +                            break;
 +                        }
 +                    }
 +
 +                    if (task == null || task == STOP_TASK) {
 +                        // Marker to indicate we should stop.
 +                        break;
 +                    }
 +
 +                    task.run();
 +                }
 +            } finally {
 +                synchronized (this) {
 +                    mState = STATE_STOPPED;
 +                    notifyAll();
 +                }
 +            }
 +        }
 +
 +        <S extends Storable> void addResyncTask(final ReplicationTrigger<S> trigger,
 +                                                final S replicaEntry,
 +                                                final S masterEntry)
 +            throws RepositoryException
 +        {
 +            if (replicaEntry == null && masterEntry == null) {
 +                // If both are null, then there's nothing to do, is there?
 +                // Note: Caller shouldn't have passed double nulls to
 +                // addResyncTask in the first place.
 +                return;
 +            }
 +
 +            Runnable task = new Runnable() {
 +                public void run() {
 +                    try {
 +                        trigger.resyncEntries(replicaEntry, masterEntry);
 +                    } catch (Exception e) {
 +                        LogFactory.getLog(ReplicatedRepository.class).error(null, e);
 +                    }
 +                }
 +            };
 +
 +            addResyncTask(task);
 +        }
 +
 +
 +        <S extends Storable> void addResyncTask(Runnable task)
 +            throws RepositoryException
 +        {
 +            try {
 +                if (!mQueue.offer(task, RESYNC_QUEUE_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
 +                    throw new RepositoryException("Unable to enqueue resync task");
 +                }
 +            } catch (InterruptedException e) {
 +                throw new RepositoryException(e);
 +            }
 +        }
 +
 +        synchronized void waitUntilDone() throws RepositoryException {
 +            if (mState == STATE_STOPPED) {
 +                return;
 +            }
 +            mState = STATE_SHOULD_STOP;
 +            try {
 +                // Inject stop task into the queue so it knows to stop.
 +                mQueue.offer(STOP_TASK, RESYNC_QUEUE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
 +                while (mState != STATE_STOPPED) {
 +                    wait();
 +                }
 +            } catch (InterruptedException e) {
 +                throw new RepositoryException(e);
 +            }
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepositoryBuilder.java b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepositoryBuilder.java new file mode 100644 index 0000000..8d5cc75 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepositoryBuilder.java @@ -0,0 +1,161 @@ +/*
 + * 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.repo.replicated;
 +
 +import java.util.Collection;
 +
 +import org.apache.commons.logging.Log;
 +import org.apache.commons.logging.LogFactory;
 +
 +import com.amazon.carbonado.ConfigurationException;
 +import com.amazon.carbonado.Repository;
 +import com.amazon.carbonado.RepositoryBuilder;
 +import com.amazon.carbonado.RepositoryException;
 +
 +import com.amazon.carbonado.spi.AbstractRepositoryBuilder;
 +import com.amazon.carbonado.spi.BelatedRepositoryCreator;
 +
 +/**
 + * Repository builder for the replicated repository.
 + * <p>
 + * The following extra capabilities are supported:
 + * <ul>
 + * <li>{@link com.amazon.carbonado.capability.ResyncCapability ResyncCapability}
 + * </ul>
 + *
 + * @author Don Schneider
 + * @author Brian S O'Neill
 + */
 +public class ReplicatedRepositoryBuilder extends AbstractRepositoryBuilder {
 +    static final int DEFAULT_MASTER_TIMEOUT_MILLIS = 15000;
 +    static final int DEFAULT_RETRY_MILLIS = 30000;
 +
 +    private String mName;
 +    private boolean mIsMaster = true;
 +    private RepositoryBuilder mReplicaRepositoryBuilder;
 +    private RepositoryBuilder mMasterRepositoryBuilder;
 +
 +    public ReplicatedRepositoryBuilder() {
 +    }
 +
 +    public Repository build(RepositoryReference rootRef) throws RepositoryException {
 +        assertReady();
 +
 +        Repository replica, master;
 +
 +        {
 +            boolean originalOption = mReplicaRepositoryBuilder.isMaster();
 +            try {
 +                mReplicaRepositoryBuilder.setMaster(false);
 +                replica = mReplicaRepositoryBuilder.build(rootRef);
 +            } finally {
 +                mReplicaRepositoryBuilder.setMaster(originalOption);
 +            }
 +        }
 +
 +        {
 +            // Create master using BelatedRepositoryCreator such that we can
 +            // start up and read from replica even if master is down.
 +
 +            final boolean originalOption = mMasterRepositoryBuilder.isMaster();
 +            mMasterRepositoryBuilder.setMaster(mIsMaster);
 +
 +            Log log = LogFactory.getLog(ReplicatedRepositoryBuilder.class);
 +            BelatedRepositoryCreator creator = new BelatedRepositoryCreator
 +                (log, mMasterRepositoryBuilder, rootRef, DEFAULT_RETRY_MILLIS) {
 +
 +                protected void createdNotification(Repository repo) {
 +                    // Don't need builder any more so restore it.
 +                    mMasterRepositoryBuilder.setMaster(originalOption);
 +                }
 +            };
 +
 +            master = creator.get(DEFAULT_MASTER_TIMEOUT_MILLIS);
 +        }
 +
 +        Repository repo = new ReplicatedRepository(getName(), replica, master);
 +        rootRef.set(repo);
 +        return repo;
 +    }
 +
 +    public String getName() {
 +        String name = mName;
 +        if (name == null) {
 +            if (mReplicaRepositoryBuilder != null && mReplicaRepositoryBuilder.getName() != null) {
 +                name = mReplicaRepositoryBuilder.getName();
 +            } else if (mMasterRepositoryBuilder != null) {
 +                name = mMasterRepositoryBuilder.getName();
 +            }
 +        }
 +        return name;
 +    }
 +
 +    public void setName(String name) {
 +        mName = name;
 +    }
 +
 +    public boolean isMaster() {
 +        return mIsMaster;
 +    }
 +
 +    public void setMaster(boolean b) {
 +        mIsMaster = b;
 +    }
 +
 +    /**
 +     * @return "replica" respository to replicate to.
 +     */
 +    public RepositoryBuilder getReplicaRepositoryBuilder() {
 +        return mReplicaRepositoryBuilder;
 +    }
 +
 +    /**
 +     * Set "replica" respository to replicate to, which is required. This builder
 +     * automatically sets the master option of the given repository builder to
 +     * false.
 +     */
 +    public void setReplicaRepositoryBuilder(RepositoryBuilder replicaRepositoryBuilder) {
 +        mReplicaRepositoryBuilder = replicaRepositoryBuilder;
 +    }
 +
 +    /**
 +     * @return "master" respository to replicate from.
 +     */
 +    public RepositoryBuilder getMasterRepositoryBuilder() {
 +        return mMasterRepositoryBuilder;
 +    }
 +
 +    /**
 +     * Set "master" respository to replicate from, which is required. This
 +     * builder automatically sets the master option of the given repository to
 +     * true.
 +     */
 +    public void setMasterRepositoryBuilder(RepositoryBuilder masterRepositoryBuilder) {
 +        mMasterRepositoryBuilder = masterRepositoryBuilder;
 +    }
 +
 +    public void errorCheck(Collection<String> messages) throws ConfigurationException {
 +        super.errorCheck(messages);
 +        if (null == getReplicaRepositoryBuilder()) {
 +            messages.add("replicaRepositoryBuilder missing");
 +        }
 +        if (null == getMasterRepositoryBuilder()) {
 +            messages.add("masterRepositoryBuilder missing");
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedStorage.java b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedStorage.java new file mode 100644 index 0000000..cf30e5d --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedStorage.java @@ -0,0 +1,121 @@ +/*
 + * 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.repo.replicated;
 +
 +import org.apache.commons.logging.Log;
 +import org.apache.commons.logging.LogFactory;
 +
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.OptimisticLockException;
 +import com.amazon.carbonado.PersistException;
 +import com.amazon.carbonado.PersistNoneException;
 +import com.amazon.carbonado.Query;
 +import com.amazon.carbonado.Storage;
 +import com.amazon.carbonado.Storable;
 +import com.amazon.carbonado.Repository;
 +import com.amazon.carbonado.SupportException;
 +import com.amazon.carbonado.RepositoryException;
 +import com.amazon.carbonado.Transaction;
 +import com.amazon.carbonado.Trigger;
 +import com.amazon.carbonado.UniqueConstraintException;
 +import com.amazon.carbonado.UnsupportedTypeException;
 +
 +import com.amazon.carbonado.filter.Filter;
 +
 +import com.amazon.carbonado.spi.BelatedStorageCreator;
 +import com.amazon.carbonado.spi.RepairExecutor;
 +
 +/**
 + * ReplicatedStorage
 + *
 + * @author Don Schneider
 + * @author Brian S O'Neill
 + */
 +class ReplicatedStorage<S extends Storable> implements Storage<S> {
 +    final Storage<S> mReplicaStorage;
 +    final ReplicationTrigger<S> mTrigger;
 +
 +    public ReplicatedStorage(ReplicatedRepository aRepository, Class<S> aType)
 +        throws SupportException, RepositoryException
 +    {
 +        mReplicaStorage = aRepository.getReplicaRepository().storageFor(aType);
 +
 +        // Create master using BelatedStorageCreator such that we can start up
 +        // and read from replica even if master is down.
 +
 +        Log log = LogFactory.getLog(getClass());
 +        BelatedStorageCreator<S> creator = new BelatedStorageCreator<S>
 +            (log, aRepository.getMasterRepository(), aType,
 +             ReplicatedRepositoryBuilder.DEFAULT_RETRY_MILLIS);
 +
 +        Storage<S> masterStorage;
 +        try {
 +            masterStorage = creator.get(ReplicatedRepositoryBuilder.DEFAULT_MASTER_TIMEOUT_MILLIS);
 +        } catch (UnsupportedTypeException e) {
 +            // Master doesn't support Storable, but it is marked as Independent.
 +            masterStorage = null;
 +        }
 +
 +        mTrigger = new ReplicationTrigger<S>(aRepository, mReplicaStorage, masterStorage);
 +        addTrigger(mTrigger);
 +    }
 +
 +    /**
 +     * For testing only.
 +     */
 +    ReplicatedStorage(Repository aRepository,
 +                      Storage<S> replicaStorage,
 +                      Storage<S> masterStorage)
 +    {
 +        mReplicaStorage = replicaStorage;
 +        mTrigger = new ReplicationTrigger<S>(aRepository, mReplicaStorage, masterStorage);
 +        addTrigger(mTrigger);
 +    }
 +
 +    public Class<S> getStorableType() {
 +        return mReplicaStorage.getStorableType();
 +    }
 +
 +    public S prepare() {
 +        return mReplicaStorage.prepare();
 +    }
 +
 +    public Query<S> query() throws FetchException {
 +        return mReplicaStorage.query();
 +    }
 +
 +    public Query<S> query(String filter) throws FetchException {
 +        return mReplicaStorage.query(filter);
 +    }
 +
 +    public Query<S> query(Filter<S> filter) throws FetchException {
 +        return mReplicaStorage.query(filter);
 +    }
 +
 +    public boolean addTrigger(Trigger<? super S> trigger) {
 +        return mReplicaStorage.addTrigger(trigger);
 +    }
 +
 +    public boolean removeTrigger(Trigger<? super S> trigger) {
 +        return mReplicaStorage.removeTrigger(trigger);
 +    }
 +
 +    ReplicationTrigger<S> getTrigger() {
 +        return mTrigger;
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/replicated/ReplicationTrigger.java b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicationTrigger.java new file mode 100644 index 0000000..19915b7 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/replicated/ReplicationTrigger.java @@ -0,0 +1,398 @@ +/*
 + * 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.repo.replicated;
 +
 +import java.util.concurrent.atomic.AtomicInteger;
 +
 +import org.apache.commons.logging.Log;
 +import org.apache.commons.logging.LogFactory;
 +
 +import com.amazon.carbonado.FetchDeadlockException;
 +import com.amazon.carbonado.FetchException;
 +import com.amazon.carbonado.FetchNoneException;
 +import com.amazon.carbonado.IsolationLevel;
 +import com.amazon.carbonado.OptimisticLockException;
 +import com.amazon.carbonado.PersistDeadlockException;
 +import com.amazon.carbonado.PersistException;
 +import com.amazon.carbonado.PersistNoneException;
 +import com.amazon.carbonado.Repository;
 +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.spi.RepairExecutor;
 +
 +/**
 + * All inserts/updates/deletes are first committed to the master storage, then
 + * duplicated and committed to the replica.
 + *
 + * @author Don Schneider
 + * @author Brian S O'Neill
 + */
 +class ReplicationTrigger<S extends Storable> extends Trigger<S> {
 +    private final Repository mRepository;
 +    private final Storage<S> mReplicaStorage;
 +    private final Storage<S> mMasterStorage;
 +
 +    private final ThreadLocal<AtomicInteger> mDisabled = new ThreadLocal<AtomicInteger>();
 +
 +    ReplicationTrigger(Repository repository,
 +                       Storage<S> replicaStorage,
 +                       Storage<S> masterStorage)
 +    {
 +        mRepository = repository;
 +        mReplicaStorage = replicaStorage;
 +        mMasterStorage = masterStorage;
 +    }
 +
 +    @Override
 +    public Object beforeInsert(S replica) throws PersistException {
 +        return beforeInsert(replica, false);
 +    }
 +
 +    @Override
 +    public Object beforeTryInsert(S replica) throws PersistException {
 +        return beforeInsert(replica, true);
 +    }
 +
 +    private Object beforeInsert(S replica, boolean forTry) throws PersistException {
 +        if (isReplicationDisabled()) {
 +            return null;
 +        }
 +
 +        final S master = mMasterStorage.prepare();
 +        replica.copyAllProperties(master);
 +
 +        try {
 +            if (forTry) {
 +                if (!master.tryInsert()) {
 +                    throw abortTry();
 +                }
 +            } else {
 +                master.insert();
 +            }
 +        } catch (UniqueConstraintException e) {
 +            // This may be caused by an inconsistency between replica and
 +            // master. Here's one scenerio: user called tryLoad and saw the
 +            // entry does not exist. So instead of calling update, he/she calls
 +            // insert. If the master entry exists, then there is an
 +            // inconsistency. The code below checks for this specific kind of
 +            // error and repairs it by inserting a record in the replica.
 +
 +            // Here's another scenerio: Unique constraint was caused by an
 +            // inconsistency with the values of the alternate keys. User
 +            // expected alternate keys to have unique values, as indicated by
 +            // replica.
 +
 +            repair(replica);
 +
 +            // Throw exception since we don't know what the user's intentions
 +            // really are.
 +            throw e;
 +        }
 +
 +        // Master may have applied sequences to unitialized primary keys, so
 +        // copy primary keys to replica. Mark properties as dirty to allow
 +        // primary key to be changed.
 +        replica.markPropertiesDirty();
 +
 +        // Copy all properties in order to trigger constraints that
 +        // master should have resolved.
 +        master.copyAllProperties(replica);
 +
 +        return null;
 +    }
 +
 +    @Override
 +    public Object beforeUpdate(S replica) throws PersistException {
 +        return beforeUpdate(replica, false);
 +    }
 +
 +    @Override
 +    public Object beforeTryUpdate(S replica) throws PersistException {
 +        return beforeUpdate(replica, true);
 +    }
 +
 +    private Object beforeUpdate(S replica, boolean forTry) throws PersistException {
 +        if (isReplicationDisabled()) {
 +            return null;
 +        }
 +
 +        final S master = mMasterStorage.prepare();
 +        replica.copyPrimaryKeyProperties(master);
 +
 +        if (!replica.hasDirtyProperties()) {
 +            // Nothing to update, but must load from master anyhow, since
 +            // update must always perform a fresh load as a side-effect. We
 +            // cannot simply call update on the master, since it may need a
 +            // version property to be set. Setting the version has the
 +            // side-effect of making the storable look dirty, so the master
 +            // will perform an update. This in turn causes the version to
 +            // increase for no reason.
 +            try {
 +                if (forTry) {
 +                    if (!master.tryLoad()) {
 +                        // Master record does not exist. To ensure consistency,
 +                        // delete record from replica.
 +                        deleteReplica(replica);
 +                        throw abortTry();
 +                    }
 +                } else {
 +                    try {
 +                        master.load();
 +                    } catch (FetchNoneException e) {
 +                        // Master record does not exist. To ensure consistency,
 +                        // delete record from replica.
 +                        deleteReplica(replica);
 +                        throw e;
 +                    }
 +                }
 +            } catch (FetchException e) {
 +                throw e.toPersistException
 +                    ("Could not load master object for update: " + master.toStringKeyOnly());
 +            }
 +        } else {
 +            replica.copyVersionProperty(master);
 +            replica.copyDirtyProperties(master);
 +
 +            try {
 +                if (forTry) {
 +                    if (!master.tryUpdate()) {
 +                        // Master record does not exist. To ensure consistency,
 +                        // delete record from replica.
 +                        deleteReplica(replica);
 +                        throw abortTry();
 +                    }
 +                } else {
 +                    try {
 +                        master.update();
 +                    } catch (PersistNoneException e) {
 +                        // Master record does not exist. To ensure consistency,
 +                        // delete record from replica.
 +                        deleteReplica(replica);
 +                        throw e;
 +                    }
 +                }
 +            } catch (OptimisticLockException e) {
 +                // This may be caused by an inconsistency between replica and
 +                // master.
 +
 +                repair(replica);
 +
 +                // Throw original exception since we don't know what the user's
 +                // intentions really are.
 +                throw e;
 +            }
 +        }
 +
 +        // Copy master properties back, since its repository may have
 +        // altered property values as a side effect.
 +        master.copyUnequalProperties(replica);
 +
 +        return null;
 +    }
 +
 +    @Override
 +    public Object beforeDelete(S replica) throws PersistException {
 +        if (isReplicationDisabled()) {
 +            return null;
 +        }
 +
 +        S master = mMasterStorage.prepare();
 +        replica.copyPrimaryKeyProperties(master);
 +
 +        // If this fails to delete anything, don't care. Any delete failure
 +        // will be detected when the replica is deleted. If there was an
 +        // inconsistency, it is resolved after the replica is deleted.
 +        master.tryDelete();
 +
 +        return null;
 +    }
 +
 +    /**
 +     * Re-sync the replica to the master. The primary keys of both entries are
 +     * assumed to match.
 +     *
 +     * @param replicaEntry current replica entry, or null if none
 +     * @param masterEntry current master entry, or null if none
 +     */
 +    void resyncEntries(S replicaEntry, S masterEntry) throws FetchException, PersistException {
 +        if (replicaEntry == null && masterEntry == null) {
 +            return;
 +        }
 +
 +        Log log = LogFactory.getLog(ReplicatedRepository.class);
 +
 +        setReplicationDisabled(true);
 +        try {
 +            Transaction txn = mRepository.enterTransaction();
 +            try {
 +                if (replicaEntry != null) {
 +                    if (masterEntry == null) {
 +                        log.info("Deleting bogus entry: " + replicaEntry);
 +                    }
 +                    replicaEntry.tryDelete();
 +                }
 +                if (masterEntry != null) {
 +                    Storable newReplicaEntry = mReplicaStorage.prepare();
 +                    if (replicaEntry == null) {
 +                        masterEntry.copyAllProperties(newReplicaEntry);
 +                        log.info("Adding missing entry: " + newReplicaEntry);
 +                    } else {
 +                        if (replicaEntry.equalProperties(masterEntry)) {
 +                            return;
 +                        }
 +                        // First copy from old replica to preserve values of
 +                        // any independent properties. Be sure not to copy
 +                        // nulls from old replica to new replica, in case new
 +                        // non-nullable properties have been added. This is why
 +                        // copyUnequalProperties is called instead of
 +                        // copyAllProperties.
 +                        replicaEntry.copyUnequalProperties(newReplicaEntry);
 +                        // Calling copyAllProperties will skip unsupported
 +                        // independent properties in master, thus preserving
 +                        // old independent property values.
 +                        masterEntry.copyAllProperties(newReplicaEntry);
 +                        log.info("Replacing stale entry with: " + newReplicaEntry);
 +                    }
 +                    if (!newReplicaEntry.tryInsert()) {
 +                        // Try to correct bizarre corruption.
 +                        newReplicaEntry.tryDelete();
 +                        newReplicaEntry.tryInsert();
 +                    }
 +                }
 +                txn.commit();
 +            } finally {
 +                txn.exit();
 +            }
 +        } finally {
 +            setReplicationDisabled(false);
 +        }
 +    }
 +
 +    /**
 +     * Runs a repair in a background thread. This is done for two reasons: It
 +     * allows repair to not be hindered by locks acquired by transactions and
 +     * repairs don't get rolled back when culprit exception is thrown. Culprit
 +     * may be UniqueConstraintException or OptimisticLockException.
 +     */
 +    private void repair(S replica) throws PersistException {
 +        replica = (S) replica.copy();
 +        S master = mMasterStorage.prepare();
 +        replica.copyPrimaryKeyProperties(master);
 +
 +        try {
 +            if (replica.tryLoad()) {
 +                if (master.tryLoad()) {
 +                    if (replica.equalProperties(master)) {
 +                        // Both are equal -- no repair needed.
 +                        return;
 +                    }
 +                }
 +            } else if (!master.tryLoad()) {
 +                // Both are missing -- no repair needed.
 +                return;
 +            }
 +        } catch (FetchException e) {
 +            throw e.toPersistException();
 +        }
 +
 +        final S finalReplica = replica;
 +        final S finalMaster = master;
 +
 +        RepairExecutor.execute(new Runnable() {
 +            public void run() {
 +                try {
 +                    Transaction txn = mRepository.enterTransaction();
 +                    try {
 +                        txn.setForUpdate(true);
 +                        if (finalReplica.tryLoad()) {
 +                            if (finalMaster.tryLoad()) {
 +                                resyncEntries(finalReplica, finalMaster);
 +                            } else {
 +                                resyncEntries(finalReplica, null);
 +                            }
 +                        } else if (finalMaster.tryLoad()) {
 +                            resyncEntries(null, finalMaster);
 +                        }
 +                        txn.commit();
 +                    } finally {
 +                        txn.exit();
 +                    }
 +                } catch (FetchException fe) {
 +                    Log log = LogFactory.getLog(ReplicatedRepository.class);
 +                    log.warn("Unable to check if repair is required for " +
 +                             finalReplica.toStringKeyOnly(), fe);
 +                } catch (PersistException pe) {
 +                    Log log = LogFactory.getLog(ReplicatedRepository.class);
 +                    log.error("Unable to repair entry " +
 +                              finalReplica.toStringKeyOnly(), pe);
 +                }
 +            }
 +        });
 +    }
 +
 +    /**
 +     * Deletes the replica entry with replication disabled.
 +     */
 +    private void deleteReplica(S replica) throws PersistException {
 +        // Disable replication to prevent trigger from being invoked by
 +        // deleting replica.
 +        setReplicationDisabled(true);
 +        try {
 +            replica.tryDelete();
 +        } finally {
 +            setReplicationDisabled(false);
 +        }
 +    }
 +
 +    /**
 +     * Returns true if replication is disabled for the current thread.
 +     */
 +    private boolean isReplicationDisabled() {
 +        // Count indicates how many times disabled (nested)
 +        AtomicInteger i = mDisabled.get();
 +        return i != null && i.get() > 0;
 +    }
 +
 +    /**
 +     * By default, replication is enabled for the current thread. Pass true to
 +     * disable during re-sync operations.
 +     */
 +    private void setReplicationDisabled(boolean disabled) {
 +        // Using a count allows this method call to be nested. Based on the
 +        // current implementation, it should never be nested, so this extra
 +        // work is just a safeguard.
 +        AtomicInteger i = mDisabled.get();
 +        if (disabled) {
 +            if (i == null) {
 +                i = new AtomicInteger(1);
 +                mDisabled.set(i);
 +            } else {
 +                i.incrementAndGet();
 +            }
 +        } else {
 +            if (i != null) {
 +                i.decrementAndGet();
 +            }
 +        }
 +    }
 +}
 diff --git a/src/main/java/com/amazon/carbonado/repo/replicated/package-info.java b/src/main/java/com/amazon/carbonado/repo/replicated/package-info.java new file mode 100644 index 0000000..44d8124 --- /dev/null +++ b/src/main/java/com/amazon/carbonado/repo/replicated/package-info.java @@ -0,0 +1,27 @@ +/*
 + * 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.
 + */
 +
 +/**
 + * Repository implementation that supports replication between two
 + * repositories. One repository is the replica, and the other is the
 + * master. Read operations are served by the replica, and the master is
 + * consulted when writing. Changes to the master are copied to the replica.
 + *
 + * @see com.amazon.carbonado.repo.replicated.ReplicatedRepositoryBuilder
 + */
 +package com.amazon.carbonado.repo.replicated;
 | 
