From 5a2aeb3ab59f286a6d2a5d8b7d62f4b17132b2b7 Mon Sep 17 00:00:00 2001 From: "Brian S. O'Neill" Date: Wed, 30 Aug 2006 02:24:36 +0000 Subject: Add core repository implementations --- .../repo/indexed/IndexEntryAccessCapability.java | 40 + .../carbonado/repo/indexed/IndexEntryAccessor.java | 73 + .../repo/indexed/IndexEntryGenerator.java | 173 ++ .../carbonado/repo/indexed/IndexedCursor.java | 183 ++ .../carbonado/repo/indexed/IndexedRepository.java | 184 ++ .../repo/indexed/IndexedRepositoryBuilder.java | 114 ++ .../carbonado/repo/indexed/IndexedStorage.java | 408 +++++ .../carbonado/repo/indexed/IndexesTrigger.java | 102 ++ .../carbonado/repo/indexed/ManagedIndex.java | 440 +++++ .../carbonado/repo/indexed/StoredIndexInfo.java | 84 + .../amazon/carbonado/repo/indexed/Unindexed.java | 27 + .../carbonado/repo/indexed/package-info.java | 26 + .../com/amazon/carbonado/repo/jdbc/JDBCBlob.java | 233 +++ .../amazon/carbonado/repo/jdbc/JDBCBlobLoader.java | 33 + .../com/amazon/carbonado/repo/jdbc/JDBCClob.java | 236 +++ .../amazon/carbonado/repo/jdbc/JDBCClobLoader.java | 33 + .../repo/jdbc/JDBCConnectionCapability.java | 76 + .../com/amazon/carbonado/repo/jdbc/JDBCCursor.java | 132 ++ .../repo/jdbc/JDBCExceptionTransformer.java | 110 ++ .../com/amazon/carbonado/repo/jdbc/JDBCLob.java | 28 + .../amazon/carbonado/repo/jdbc/JDBCRepository.java | 665 +++++++ .../carbonado/repo/jdbc/JDBCRepositoryBuilder.java | 255 +++ .../repo/jdbc/JDBCSequenceValueProducer.java | 68 + .../carbonado/repo/jdbc/JDBCStorableGenerator.java | 1892 ++++++++++++++++++++ .../carbonado/repo/jdbc/JDBCStorableInfo.java | 75 + .../repo/jdbc/JDBCStorableIntrospector.java | 1365 ++++++++++++++ .../carbonado/repo/jdbc/JDBCStorableProperty.java | 129 ++ .../amazon/carbonado/repo/jdbc/JDBCStorage.java | 1129 ++++++++++++ .../amazon/carbonado/repo/jdbc/JDBCSupport.java | 74 + .../carbonado/repo/jdbc/JDBCSupportStrategy.java | 233 +++ .../carbonado/repo/jdbc/JDBCTransaction.java | 122 ++ .../repo/jdbc/JDBCTransactionManager.java | 94 + .../repo/jdbc/LoggingCallableStatement.java | 392 ++++ .../carbonado/repo/jdbc/LoggingConnection.java | 227 +++ .../carbonado/repo/jdbc/LoggingDataSource.java | 93 + .../repo/jdbc/LoggingPreparedStatement.java | 255 +++ .../carbonado/repo/jdbc/LoggingStatement.java | 200 +++ .../carbonado/repo/jdbc/SimpleDataSource.java | 89 + .../amazon/carbonado/repo/jdbc/package-info.java | 28 + .../amazon/carbonado/repo/logging/CommonsLog.java | 44 + .../com/amazon/carbonado/repo/logging/Log.java | 30 + .../repo/logging/LogAccessCapability.java | 30 + .../carbonado/repo/logging/LoggingQuery.java | 98 + .../carbonado/repo/logging/LoggingRepository.java | 114 ++ .../repo/logging/LoggingRepositoryBuilder.java | 150 ++ .../carbonado/repo/logging/LoggingStorage.java | 138 ++ .../carbonado/repo/logging/LoggingTransaction.java | 69 + .../carbonado/repo/logging/package-info.java | 25 + .../repo/replicated/ReplicatedRepository.java | 583 ++++++ .../replicated/ReplicatedRepositoryBuilder.java | 161 ++ .../repo/replicated/ReplicatedStorage.java | 121 ++ .../repo/replicated/ReplicationTrigger.java | 398 ++++ .../carbonado/repo/replicated/package-info.java | 27 + 53 files changed, 12108 insertions(+) create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessCapability.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryAccessor.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexEntryGenerator.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexedCursor.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepository.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexedRepositoryBuilder.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexedStorage.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/IndexesTrigger.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/ManagedIndex.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/StoredIndexInfo.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/Unindexed.java create mode 100644 src/main/java/com/amazon/carbonado/repo/indexed/package-info.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCBlob.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCBlobLoader.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCClob.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCClobLoader.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCConnectionCapability.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCCursor.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCExceptionTransformer.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCLob.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCRepository.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCRepositoryBuilder.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSequenceValueProducer.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableGenerator.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableInfo.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableIntrospector.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorableProperty.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCStorage.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSupport.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCSupportStrategy.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCTransaction.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/JDBCTransactionManager.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/LoggingCallableStatement.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/LoggingConnection.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/LoggingDataSource.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/LoggingPreparedStatement.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/LoggingStatement.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/SimpleDataSource.java create mode 100644 src/main/java/com/amazon/carbonado/repo/jdbc/package-info.java create mode 100644 src/main/java/com/amazon/carbonado/repo/logging/CommonsLog.java create mode 100644 src/main/java/com/amazon/carbonado/repo/logging/Log.java create mode 100644 src/main/java/com/amazon/carbonado/repo/logging/LogAccessCapability.java create mode 100644 src/main/java/com/amazon/carbonado/repo/logging/LoggingQuery.java create mode 100644 src/main/java/com/amazon/carbonado/repo/logging/LoggingRepository.java create mode 100644 src/main/java/com/amazon/carbonado/repo/logging/LoggingRepositoryBuilder.java create mode 100644 src/main/java/com/amazon/carbonado/repo/logging/LoggingStorage.java create mode 100644 src/main/java/com/amazon/carbonado/repo/logging/LoggingTransaction.java create mode 100644 src/main/java/com/amazon/carbonado/repo/logging/package-info.java create mode 100644 src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepository.java create mode 100644 src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedRepositoryBuilder.java create mode 100644 src/main/java/com/amazon/carbonado/repo/replicated/ReplicatedStorage.java create mode 100644 src/main/java/com/amazon/carbonado/repo/replicated/ReplicationTrigger.java create mode 100644 src/main/java/com/amazon/carbonado/repo/replicated/package-info.java (limited to 'src/main/java/com/amazon') 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. + */ + IndexEntryAccessor[] getIndexEntryAccessors(Class 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 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 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 { + + // cache for generators + private static Map> cCache = + new WeakHashMap>(); + + + /** + * 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. + * + *

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: + * + *

+     * 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);
+     * 
+ * + * The above code can be fixed by saving a local reference to the generator: + * + *
+     * StorableIndex index = ...
+     * IndexEntryGenerator generator = IndexEntryGenerator.getInstance(index);
+     * Class indexEntryClass = generator.getIndexEntryClass();
+     * ...
+     * Storable indexEntry = instance of indexEntryClass
+     * generator.setAllProperties(indexEntry, source);
+     * 
+ * + * @throws SupportException if any non-primary key property doesn't have a + * public read method. + */ + public static IndexEntryGenerator + getInstance(StorableIndex index) throws SupportException + { + synchronized(cCache) { + IndexEntryGenerator generator; + Reference ref = cCache.get(index); + if (ref != null) { + generator = ref.get(); + if (generator != null) { + return generator; + } + } + generator = new IndexEntryGenerator(index); + cCache.put(index, new SoftReference(generator)); + return generator; + } + } + + private SyntheticStorableReferenceBuilder mBuilder; + + /** + * Convenience class for gluing new "builder" style synthetics to the traditional + * generator style. + * @param index Generator style index specification + */ + public IndexEntryGenerator(StorableIndex 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 type = index.getProperty(0).getEnclosingType(); + + mBuilder = new SyntheticStorableReferenceBuilder(type, index.isUnique()); + + for (int i=0; i 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 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 extends AbstractCursor { + private final Cursor mCursor; + private final IndexedStorage mStorage; + private final IndexEntryGenerator mGenerator; + + private S mNext; + + IndexedCursor(Cursor indexEntryCursor, + IndexedStorage storage, + IndexEntryGenerator 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, IndexedStorage> mStorages; + + IndexedRepository(String name, Repository repository) { + mRepository = repository; + mName = name; + mStorages = new IdentityHashMap, 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 Storage storageFor(Class type) + throws MalformedTypeException, SupportException, RepositoryException + { + synchronized (mStorages) { + IndexedStorage storage = (IndexedStorage) mStorages.get(type); + if (storage == null) { + Storage 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(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 getCapability(Class capabilityType) { + if (capabilityType.isInstance(this)) { + return (C) this; + } + return mRepository.getCapability(capabilityType); + } + + public IndexInfo[] getIndexInfo(Class storableType) + throws RepositoryException + { + return ((IndexedStorage) storageFor(storableType)).getIndexInfo(); + } + + public IndexEntryAccessor[] + getIndexEntryAccessors(Class storableType) + throws RepositoryException + { + return ((IndexedStorage) storageFor(storableType)).getIndexEntryAccessors(); + } + + public String[] getUserStorableTypeNames() throws RepositoryException { + StorableInfoCapability cap = mRepository.getCapability(StorableInfoCapability.class); + if (cap == null) { + return new String[0]; + } + ArrayList names = + new ArrayList(Arrays.asList(cap.getUserStorableTypeNames())); + + // Exclude our own metadata types as well as indexes. + + names.remove(StoredIndexInfo.class.getName()); + + Cursor 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 type) { + StorableInfoCapability cap = mRepository.getCapability(StorableInfoCapability.class); + return (cap == null) ? null : cap.isSupported(type); + } + + public boolean isPropertySupported(Class 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 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. + *

+ * In addition to supporting the capabilities of the wrapped repository, the + * following extra capabilities are supported: + *

    + *
  • {@link com.amazon.carbonado.capability.IndexInfoCapability IndexInfoCapability} + *
  • {@link com.amazon.carbonado.capability.StorableInfoCapability StorableInfoCapability} + *
  • {@link IndexEntryAccessCapability IndexEntryAccessCapability} + *
+ * + * @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 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 implements Storage { + static StorableIndexSet gatherRequiredIndexes(StorableInfo info) { + StorableIndexSet indexSet = new StorableIndexSet(); + indexSet.addIndexes(info); + indexSet.addAlternateKeys(info); + return indexSet; + } + + final IndexedRepository mRepository; + final Storage mMasterStorage; + + private final Map, IndexInfo> mIndexInfoMap; + + private final QueryEngine mQueryEngine; + + @SuppressWarnings("unchecked") + IndexedStorage(IndexedRepository repository, Storage masterStorage) + throws RepositoryException + { + mRepository = repository; + mMasterStorage = masterStorage; + mIndexInfoMap = new IdentityHashMap, IndexInfo>(); + + StorableInfo info = StorableIntrospector.examine(masterStorage.getStorableType()); + + // Determine what the set of indexes should be. + StorableIndexSet 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[] freeIndexes = new StorableIndex[infos.length]; + for (int i=0; i(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 currentIndexSet = new StorableIndexSet(); + // Gather indexes to remove. + StorableIndexSet indexesToRemove = new StorableIndexSet(); + + Query 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 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 index : indexesToRemove) { + removeIndex(index); + } + } + + currentIndexSet = newIndexSet; + + // Open all the indexes. + List> managedIndexList = new ArrayList>(); + for (StorableIndex index : currentIndexSet) { + IndexEntryGenerator builder = IndexEntryGenerator.getInstance(index); + Class indexEntryClass = builder.getIndexEntryClass(); + Storage indexEntryStorage = repository.getIndexEntryStorageFor(indexEntryClass); + ManagedIndex managedIndex = new ManagedIndex(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[] managedIndexes = + managedIndexList.toArray(new ManagedIndex[managedIndexList.size()]); + + if (!addTrigger(new IndexesTrigger(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(info, repository, this, currentIndexSet); + } + + public Class getStorableType() { + return mMasterStorage.getStorableType(); + } + + public S prepare() { + return mMasterStorage.prepare(); + } + + public Query query() throws FetchException { + return mQueryEngine.getCompiledQuery(); + } + + public Query query(String filter) throws FetchException { + return mQueryEngine.getCompiledQuery(filter); + } + + public Query query(Filter filter) throws FetchException { + return mQueryEngine.getCompiledQuery(filter); + } + + public boolean addTrigger(Trigger trigger) { + return mMasterStorage.addTrigger(trigger); + } + + public boolean removeTrigger(Trigger trigger) { + return mMasterStorage.removeTrigger(trigger); + } + + public IndexInfo[] getIndexInfo() { + IndexInfo[] infos = new IndexInfo[mIndexInfoMap.size()]; + return mIndexInfoMap.values().toArray(infos); + } + + @SuppressWarnings("unchecked") + public IndexEntryAccessor[] getIndexEntryAccessors() { + List> accessors = + new ArrayList>(mIndexInfoMap.size()); + for (IndexInfo info : mIndexInfoMap.values()) { + if (info instanceof IndexEntryAccessor) { + accessors.add((IndexEntryAccessor) info); + } + } + return accessors.toArray(new IndexEntryAccessor[accessors.size()]); + } + + Storage getStorageFor(StorableIndex 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 getManagedIndex(StorableIndex index) { + return (ManagedIndex) mIndexInfoMap.get(index); + } + + private void registerIndex(ManagedIndex 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 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 extends BaseQueryEngine { + + QueryEngine(StorableInfo info, + Repository repo, + IndexedStorage storage, + StorableIndexSet indexSet) { + super(info, repo, storage, null, indexSet); + } + + @Override + protected Storage getStorageFor(StorableIndex index) { + return storage().getStorageFor(index); + } + + protected Cursor openCursor(StorableIndex 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 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 indexEntryCursor = query.fetch(); + + return new IndexedCursor + (indexEntryCursor, storage(), indexInfo.getIndexEntryClassBuilder()); + } + + private IndexedStorage storage() { + return (IndexedStorage) 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 extends Trigger { + private final ManagedIndex[] mManagedIndexes; + + /** + * @param managedIndexes all the indexes that need to be updated. + */ + IndexesTrigger(ManagedIndex[] managedIndexes) { + mManagedIndexes = managedIndexes; + } + + @Override + public void afterInsert(S storable, Object state) throws PersistException { + for (ManagedIndex 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 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 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 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 implements IndexEntryAccessor { + private static final int POPULATE_BATCH_SIZE = 256; + + private final StorableIndex mIndex; + private final IndexEntryGenerator mGenerator; + private final Storage mIndexEntryStorage; + + private final Query[] mQueryCache; + + ManagedIndex(StorableIndex index, + IndexEntryGenerator 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 getComparator() { + return mGenerator.getComparator(); + } + + public IndexEntryGenerator 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 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 masterStorage) throws RepositoryException { + Cursor 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. + * + *

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. + * + *

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. + * + *

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. + * + *

+ * 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();
+ * }
+ * 
+ * + * @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 extends AbstractCursor { + private final JDBCStorage mStorage; + private Connection mConnection; + private PreparedStatement mStatement; + private ResultSet mResultSet; + + private boolean mHasNext; + + JDBCCursor(JDBCStorage 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, JDBCStorage> mStorages; + + // Track all open connections so that they can be closed when this + // repository is closed. + private Map mOpenConnections; + + private final ThreadLocal mCurrentTxnMgr; + + // Weakly tracks all JDBCTransactionManager instances for shutdown. + private final Map 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, JDBCStorage>(); + mOpenConnections = new IdentityHashMap(); + mCurrentTxnMgr = new ThreadLocal(); + 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 Storage storageFor(Class type) throws RepositoryException { + // Lock on mAllTxnMgrs to prevent databases from being opened during shutdown. + synchronized (mAllTxnMgrs) { + JDBCStorage storage = (JDBCStorage) mStorages.get(type); + if (storage == null) { + // Examine and throw exception early if there is a problem. + JDBCStorableInfo info = examineStorable(type); + + if (!info.isSupported()) { + throw new UnsupportedTypeException(type); + } + + storage = new JDBCStorage(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 JDBCStorableInfo examineStorable(Class type) + throws RepositoryException, SupportException + { + try { + return JDBCStorableIntrospector.examine(type, mDataSource, mCatalog, mSchema); + } catch (SQLException e) { + throw toRepositoryException(e); + } + } + + @SuppressWarnings("unchecked") + public C getCapability(Class capabilityType) { + if (capabilityType.isInstance(this)) { + return (C) this; + } + return null; + } + + public IndexInfo[] getIndexInfo(Class 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 type) { + if (type == null) { + return false; + } + try { + examineStorable(type); + return true; + } catch (RepositoryException e) { + return false; + } + } + + public boolean isPropertySupported(Class 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 + */ + JDBCStorableProperty + getJDBCStorableProperty(StorableProperty property) + throws RepositoryException, SupportException + { + JDBCStorableInfo info = examineStorable(property.getEnclosingType()); + JDBCStorableProperty 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. + * + *

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. + * + *

+ * The following extra capabilities are supported: + *

    + *
  • {@link com.amazon.carbonado.capability.IndexInfoCapability IndexInfoCapability} + *
  • {@link com.amazon.carbonado.capability.StorableInfoCapability StorableInfoCapability} + *
  • {@link com.amazon.carbonado.capability.ShutdownCapability ShutdownCapability} + *
  • {@link JDBCConnectionCapability JDBCConnectionCapability} + *
+ * + * @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 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 { + // 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> cCache; + + static { + cCache = new SoftValuedHashMap(); + } + + static Class getGeneratedClass(JDBCStorableInfo info) + throws SupportException + { + Class type = info.getStorableType(); + synchronized (cCache) { + Class generatedClass = (Class) cCache.get(type); + if (generatedClass != null) { + return generatedClass; + } + generatedClass = new JDBCStorableGenerator(info).generateAndInjectClass(); + cCache.put(type, generatedClass); + return generatedClass; + } + } + + private final Class mStorableType; + private final JDBCStorableInfo mInfo; + private final Map> mAllProperties; + + private final ClassLoader mParentClassLoader; + private final ClassInjector mClassInjector; + private final ClassFile mClassFile; + + private JDBCStorableGenerator(JDBCStorableInfo info) throws SupportException { + mStorableType = info.getStorableType(); + mInfo = info; + mAllProperties = mInfo.getAllProperties(); + + EnumSet features = EnumSet + .of(MasterFeature.INSERT_SEQUENCES, + MasterFeature.INSERT_TXN, MasterFeature.UPDATE_TXN); + + final Class 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 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, 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 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 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 versionProperty = null; + + // Gather all Lob properties to track if a post-insert update is required. + Map, 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 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> whereProperties = + mInfo.getPrimaryKeyProperties().values(); + + JDBCStorableProperty versionProperty = mInfo.getVersionProperty(); + if (versionProperty != null && versionProperty.isSupported()) { + // Include version property in WHERE clause to support optimistic locking. + List> list = + new ArrayList>(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 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, 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 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 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, Integer>findLobs() { + Map, Integer> lobIndexMap = + new IdentityHashMap, Integer>(); + + int lobIndex = 0; + + for (JDBCStorableProperty 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> properties = + mInfo.getPrimaryKeyProperties().values(); + + sqlBuilder.append(" WHERE "); + + List nullableProperties = new ArrayList(); + 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, 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, 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> properties, + Map, Class> lobLoaderMap) + { + LocalVariable offsetVar = null; + int lobIndex = 0; + + for (JDBCStorableProperty 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 0) { + b.append(','); + } + b.append('?'); + } + + b.append(')'); + + return versionPropNumber; + } + + private Map, Class> generateLobLoaders() { + Map, Class> lobLoaderMap = + new IdentityHashMap, Class>(); + + for (JDBCStorableProperty 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 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 extends StorableInfo { + /** + * 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> getAllProperties(); + + Map> getPrimaryKeyProperties(); + + Map> getDataProperties(); + + JDBCStorableProperty 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> 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 JDBCStorableInfo examine + (Class type, DataSource ds, String catalog, String schema) + throws SQLException, SupportException + { + Object key = KeyFactory.createKey(new Object[] {type, ds, catalog, schema}); + + synchronized (cCache) { + JDBCStorableInfo jInfo = (JDBCStorableInfo) cCache.get(key); + if (jInfo != null) { + return jInfo; + } + + // Call superclass for most info. + StorableInfo 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 jProperty : jInfo.getAllProperties().values()) { + ((JProperty) jProperty).fillInternalJoinElements(ds, catalog, schema); + ((JProperty) 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 JDBCStorableInfo examine + (StorableInfo 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 fitnessMap = new HashMap(); + 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 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 columnMap = + new TreeMap(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> mainProperties = mainInfo.getAllProperties(); + Map columnToProperty = new HashMap(); + Map> jProperties = + new LinkedHashMap>(mainProperties.size()); + + ArrayList errorMessages = new ArrayList(); + + for (StorableProperty mainProperty : mainProperties.values()) { + if (mainProperty.isJoin() || tableName == null) { + jProperties.put(mainProperty.getName(), new JProperty(mainProperty)); + continue; + } + + String[] columnAliases; + if (mainProperty.getAliasCount() > 0) { + columnAliases = mainProperty.getAliases(); + } else { + columnAliases = generateAliases(mainProperty.getName()); + } + + JDBCStorableProperty jProperty = null; + boolean addedError = false; + + findName: for (int i=0; i 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(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(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 infoList = new ArrayList(); + + try { + String indexName = null; + boolean unique = false; + boolean clustered = false; + List indexProperties = new ArrayList(); + List directions = new ArrayList(); + + 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 altKey = mainInfo.getAlternateKey(i); + Set> 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 orderedProp : altKeyProps) { + StorableProperty 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 + (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 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 aliases = new ArrayList(4); + + StringBuilder buf = new StringBuilder(); + + int i; + for (i=0; i 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 implements JDBCStorableInfo { + private final StorableInfo mMainInfo; + private final String mCatalogName; + private final String mSchemaName; + private final String mTableName; + private final String mQualifiedTableName; + private final IndexInfo[] mIndexInfo; + private final Map> mAllProperties; + + private transient Map> mPrimaryKeyProperties; + private transient Map> mDataProperties; + private transient JDBCStorableProperty mVersionProperty; + + JInfo(StorableInfo mainInfo, + String catalogName, String schemaName, String tableName, String qualifiedTableName, + IndexInfo[] indexInfo, + Map> 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 getStorableType() { + return mMainInfo.getStorableType(); + } + + public StorableKey getPrimaryKey() { + return mMainInfo.getPrimaryKey(); + } + + public int getAlternateKeyCount() { + return mMainInfo.getAlternateKeyCount(); + } + + public StorableKey getAlternateKey(int index) { + return mMainInfo.getAlternateKey(index); + } + + public StorableKey[] 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 getIndex(int index) { + return mMainInfo.getIndex(index); + } + + public StorableIndex[] 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> getAllProperties() { + return mAllProperties; + } + + public Map> getPrimaryKeyProperties() { + if (mPrimaryKeyProperties == null) { + Map> pkProps = + new LinkedHashMap>(mAllProperties.size()); + for (Map.Entry> entry : mAllProperties.entrySet()){ + JDBCStorableProperty property = entry.getValue(); + if (property.isPrimaryKeyMember()) { + pkProps.put(entry.getKey(), property); + } + } + mPrimaryKeyProperties = Collections.unmodifiableMap(pkProps); + } + return mPrimaryKeyProperties; + } + + public Map> getDataProperties() { + if (mDataProperties == null) { + Map> dataProps = + new LinkedHashMap>(mAllProperties.size()); + for (Map.Entry> entry : mAllProperties.entrySet()){ + JDBCStorableProperty property = entry.getValue(); + if (!property.isPrimaryKeyMember() && !property.isJoin()) { + dataProps.put(entry.getKey(), property); + } + } + mDataProperties = Collections.unmodifiableMap(dataProps); + } + return mDataProperties; + } + + public JDBCStorableProperty getVersionProperty() { + if (mVersionProperty == null) { + for (JDBCStorableProperty 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 implements JDBCStorableProperty { + private final StorableProperty 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[] mInternal; + private JDBCStorableProperty[] mExternal; + + /** + * Join properties need to be filled in later. + */ + JProperty(StorableProperty 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 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 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 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 getInternalJoinElement(int index) { + if (mInternal == null) { + throw new IndexOutOfBoundsException(); + } + return mInternal[index]; + } + + @SuppressWarnings("unchecked") + public JDBCStorableProperty[] 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[] mainInternal = mMainProperty.getInternalJoinElements(); + if (mainInternal.length == 0) { + mInternal = null; + return; + } + + JDBCStorableInfo info = examine(getEnclosingType(), ds, catalog, schema); + + JDBCStorableProperty[] 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 extends StorableProperty { + /** + * 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 getInternalJoinElement(int index); + + JDBCStorableProperty[] 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 extends BaseQueryCompiler + implements Storage, JDBCSupport +{ + private static final String TABLE_ALIAS_PREFIX = "T"; + private static final int FIRST_RESULT_INDEX = 1; + + final JDBCRepository mRepository; + final JDBCSupportStrategy mSupportStrategy; + final JDBCStorableInfo mInfo; + final InstanceFactory mInstanceFactory; + + final TriggerManager mTriggerManager; + + JDBCStorage(JDBCRepository repository, JDBCStorableInfo info) + throws SupportException + { + super(info); + mRepository = repository; + mSupportStrategy = repository.getSupportStrategy(); + mInfo = info; + + Class generatedStorableClass = JDBCStorableGenerator.getGeneratedClass(info); + mInstanceFactory = QuickConstructorGenerator + .getInstance(generatedStorableClass, InstanceFactory.class); + + mTriggerManager = new TriggerManager(); + } + + public Class getStorableType() { + return mInfo.getStorableType(); + } + + public S prepare() { + return (S) mInstanceFactory.instantiate(this); + } + + public Query query() throws FetchException { + return getCompiledQuery(); + } + + public Query query(String filter) throws FetchException { + return getCompiledQuery(filter); + } + + public Query query(Filter filter) throws FetchException { + return getCompiledQuery(filter); + } + + public JDBCRepository getJDBCRepository() { + return mRepository; + } + + public Repository getRootRepository() { + return mRepository.getRootRepository(); + } + + public boolean isPropertySupported(String propertyName) { + JDBCStorableProperty property = mInfo.getAllProperties().get(propertyName); + return property != null && property.isSupported(); + } + + public boolean addTrigger(Trigger trigger) { + return mTriggerManager.addTrigger(trigger); + } + + public boolean removeTrigger(Trigger trigger) { + return mTriggerManager.removeTrigger(trigger); + } + + public IndexInfo[] getIndexInfo() { + return mInfo.getIndexInfo(); + } + + public SequenceValueProducer getSequenceValueProducer(String name) throws PersistException { + return mSupportStrategy.getSequenceValueProducer(name); + } + + public Trigger getInsertTrigger() { + return mTriggerManager.getInsertTrigger(); + } + + public Trigger getUpdateTrigger() { + return mTriggerManager.getUpdateTrigger(); + } + + public Trigger 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 getStorableInfo() { + return mInfo; + } + + protected Query compileQuery(FilterValues values, OrderedProperty[] 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> properties = getStorableInfo().getAllProperties(); + int ordinal = 0; + for (JDBCStorableProperty 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[] 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 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 mSelectStatement; + private final int mMaxSelectStatementLength; + private final Statement mFromWhereStatement; + private final int mMaxFromWhereStatementLength; + + // The following arrays all have the same length, or they may all be null. + + private final PropertyFilter[] 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 selectStatement, + Statement fromWhereStatement, + PropertyFilter[] 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[] filters) + throws RepositoryException + { + for (int i=0; i filter = filters[i]; + ChainedProperty 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 openCursor(FilterValues filterValues, boolean forUpdate) + throws FetchException + { + Connection con = mRepository.getConnection(); + try { + PreparedStatement ps = + con.prepareStatement(prepareSelect(filterValues, forUpdate)); + + setParameters(ps, filterValues); + return new JDBCCursor(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 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 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 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 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 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 filterValues) + throws Exception + { + PropertyFilter[] 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 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 { + private final CursorFactory mCursorFactory; + + JDBCQuery(CursorFactory factory, + FilterValues values, + OrderedProperty[] orderings) + { + super(mRepository, JDBCStorage.this, values, orderings); + mCursorFactory = factory; + } + + JDBCQuery(CursorFactory factory, + FilterValues values, + String[] orderings) + { + super(mRepository, JDBCStorage.this, values, orderings); + mCursorFactory = factory; + } + + public Query orderBy(String property) + throws FetchException, UnsupportedOperationException + { + return JDBCStorage.this.getOrderedQuery(getFilterValues(), property); + } + + public Query orderBy(String... properties) + throws FetchException, UnsupportedOperationException + { + return JDBCStorage.this.getOrderedQuery(getFilterValues(), properties); + } + + public Cursor 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 newInstance(FilterValues values) { + return new JDBCQuery(mCursorFactory, values, getOrderings()); + } + + protected BaseQuery cachedInstance(Filter filter) throws FetchException { + return (BaseQuery) 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 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(); + } + + /** + * 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 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 { + 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 filter, Object param) { + try { + visit(filter); + return null; + } catch (RepositoryException e) { + throw new UndeclaredThrowableException(e); + } + } + + private void visit(PropertyFilter filter) throws RepositoryException { + ChainedProperty chained = filter.getChainedProperty(); + mAliasCounter = mRootJoinNode.addJoin(chained, mAliasCounter); + } + } + + /** + * Simple DOM representing a SQL statement. + */ + private static abstract class Statement { + 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 filterValues) { + StringBuilder b = new StringBuilder(initialCapacity); + this.appendTo(b, filterValues); + return b.toString(); + } + + public abstract void appendTo(StringBuilder b, FilterValues filterValues); + + /** + * Just used for debugging. + */ + public String toString() { + StringBuilder b = new StringBuilder(); + appendTo(b, null); + return b.toString(); + } + } + + private static class LiteralStatement extends Statement { + private final String mStr; + + LiteralStatement(String str) { + mStr = str; + } + + public int maxLength() { + return mStr.length(); + } + + public String buildStatement(int initialCapacity, FilterValues filterValues) { + return mStr; + } + + public void appendTo(StringBuilder b, FilterValues filterValues) { + b.append(mStr); + } + + /** + * Returns the literal value. + */ + public String toString() { + return mStr; + } + } + + private static class NullablePropertyStatement extends Statement { + private final PropertyFilter mFilter; + private final boolean mIsNullOp; + + NullablePropertyStatement(PropertyFilter 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 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 extends Statement { + private final Statement[] mStatements; + + @SuppressWarnings("unchecked") + CompositeStatement(List> statements) { + mStatements = statements.toArray(new Statement[statements.size()]); + } + + public int maxLength() { + int max = 0; + for (Statement statement : mStatements) { + max += statement.maxLength(); + } + return max; + } + + public void appendTo(StringBuilder b, FilterValues filterValues) { + for (Statement statement : mStatements) { + statement.appendTo(b, filterValues); + } + } + } + + private class StatementBuilder { + private List> mStatements; + private StringBuilder mLiteralBuilder; + + StatementBuilder() { + mStatements = new ArrayList>(); + mLiteralBuilder = new StringBuilder(); + } + + public Statement build() { + if (mStatements.size() == 0 || mLiteralBuilder.length() > 0) { + mStatements.add(new LiteralStatement(mLiteralBuilder.toString())); + mLiteralBuilder.setLength(0); + } + if (mStatements.size() == 1) { + return mStatements.get(0); + } else { + return new CompositeStatement(mStatements); + } + } + + public void append(char c) { + mLiteralBuilder.append(c); + } + + public void append(String str) { + mLiteralBuilder.append(str); + } + + public void append(LiteralStatement statement) { + append(statement.toString()); + } + + public void append(Statement statement) { + if (statement instanceof LiteralStatement) { + append((LiteralStatement) statement); + } else { + mStatements.add(new LiteralStatement(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 { + private final StatementBuilder mStatementBuilder; + private final JoinNode mJoinNode; + + private List> mPropertyFilters; + private List mPropertyFilterNullable; + + WhereBuilder(StatementBuilder statementBuilder, JoinNode jn) { + mStatementBuilder = statementBuilder; + mJoinNode = jn; + mPropertyFilters = new ArrayList>(); + mPropertyFilterNullable = new ArrayList(); + } + + @SuppressWarnings("unchecked") + public PropertyFilter[] getPropertyFilters() { + return mPropertyFilters.toArray(new PropertyFilter[mPropertyFilters.size()]); + } + + public boolean[] getPropertyFilterNullable() { + boolean[] array = new boolean[mPropertyFilterNullable.size()]; + for (int i=0; i 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 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 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(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 extends MasterSupport { + 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 clazz = + (Class) 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 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(); + } + 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 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(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 { + // Weakly reference repository because thread locals are not cleaned up + // very quickly. + private final WeakReference mRepositoryRef; + + JDBCTransactionManager(JDBCRepository repository) { + super(repository.getExceptionTransformer()); + mRepositoryRef = new WeakReference(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> 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> 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> getTypeMap() throws SQLException { + return mCon.getTypeMap(); + } + + public void setTypeMap(java.util.Map> 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 extends WrappedQuery { + private final LoggingStorage mStorage; + + LoggingQuery(LoggingStorage storage, Query query) { + super(query); + mStorage = storage; + } + + public Cursor 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 newInstance(Query query) { + return new LoggingQuery(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, LoggingStorage> mStorages; + + LoggingRepository(RepositoryReference rootRef, Repository actual, Log log) { + mRootRef = rootRef; + mRepo = actual; + mLog = log; + + mStorages = new IdentityHashMap, LoggingStorage>(); + } + + public String getName() { + return mRepo.getName(); + } + + public Storage storageFor(Class 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 getCapability(Class 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. + * + *

+ * The following extra capabilities are supported: + *

    + *
  • {@link LogAccessCapability} + *
+ * + * Example: + * + *
+ * LoggingRepositoryBuilder loggingBuilder = new LoggingRepositoryBuilder();
+ * loggingBuilder.setActualRepositoryBuilder(...);
+ * Repository repo = loggingBuilder.build();
+ * 
+ * + * @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 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 extends WrappedStorage { + final LoggingRepository mRepo; + + LoggingStorage(LoggingRepository repo, Storage storage) { + super(storage); + mRepo = repo; + } + + protected S wrap(S storable) { + return super.wrap(storable); + } + + protected Query wrap(Query query) { + return new LoggingQuery(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 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 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(pkSet); + + int i; + for (i=0; i 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, ReplicatedStorage> mStorages; + + ReplicatedRepository(String aName, + Repository aReplicaRepository, + Repository aMasterRepository) { + mName = aName; + mReplicaRepository = aReplicaRepository; + mMasterRepository = aMasterRepository; + + mStorages = new IdentityHashMap, ReplicatedStorage>(); + } + + public String getName() { + return mName; + } + + Repository getReplicaRepository() { + return mReplicaRepository; + } + + Repository getMasterRepository() { + return mMasterRepository; + } + + @SuppressWarnings("unchecked") + public Storage storageFor(Class 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 ReplicatedStorage createStorage(Class type) + throws SupportException, RepositoryException + { + return new ReplicatedStorage(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 getCapability(Class 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 names = new LinkedHashSet(); + 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 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 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 void resync(Class type, + double desiredSpeed, + String filter, + Object... filterValues) + throws RepositoryException + { + Storage replicaStorage, masterStorage; + replicaStorage = mReplicaRepository.storageFor(type); + masterStorage = mMasterRepository.storageFor(type); + + Query 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 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 replicaCursor = replicaQuery.fetch(); + try { + Cursor masterCursor = masterQuery.fetch(); + try { + resync(((ReplicatedStorage) storageFor(type)).getTrigger(), + replicaCursor, + masterCursor, + throttle, desiredSpeed, + bc); + } finally { + masterCursor.close(); + } + } finally { + replicaCursor.close(); + } + } + + @SuppressWarnings("unchecked") + private void resync(ReplicationTrigger trigger, + Cursor replicaCursor, + Cursor 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 resyncQueue = + new ArrayBlockingQueue(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 mQueue; + + private int mState = STATE_RUNNING; + + ResyncThread(BlockingQueue 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(); + } + } + } + + void addResyncTask(final ReplicationTrigger 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); + } + + + 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. + *

+ * The following extra capabilities are supported: + *

    + *
  • {@link com.amazon.carbonado.capability.ResyncCapability ResyncCapability} + *
+ * + * @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 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 implements Storage { + final Storage mReplicaStorage; + final ReplicationTrigger mTrigger; + + public ReplicatedStorage(ReplicatedRepository aRepository, Class 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 creator = new BelatedStorageCreator + (log, aRepository.getMasterRepository(), aType, + ReplicatedRepositoryBuilder.DEFAULT_RETRY_MILLIS); + + Storage 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(aRepository, mReplicaStorage, masterStorage); + addTrigger(mTrigger); + } + + /** + * For testing only. + */ + ReplicatedStorage(Repository aRepository, + Storage replicaStorage, + Storage masterStorage) + { + mReplicaStorage = replicaStorage; + mTrigger = new ReplicationTrigger(aRepository, mReplicaStorage, masterStorage); + addTrigger(mTrigger); + } + + public Class getStorableType() { + return mReplicaStorage.getStorableType(); + } + + public S prepare() { + return mReplicaStorage.prepare(); + } + + public Query query() throws FetchException { + return mReplicaStorage.query(); + } + + public Query query(String filter) throws FetchException { + return mReplicaStorage.query(filter); + } + + public Query query(Filter filter) throws FetchException { + return mReplicaStorage.query(filter); + } + + public boolean addTrigger(Trigger trigger) { + return mReplicaStorage.addTrigger(trigger); + } + + public boolean removeTrigger(Trigger trigger) { + return mReplicaStorage.removeTrigger(trigger); + } + + ReplicationTrigger 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 extends Trigger { + private final Repository mRepository; + private final Storage mReplicaStorage; + private final Storage mMasterStorage; + + private final ThreadLocal mDisabled = new ThreadLocal(); + + ReplicationTrigger(Repository repository, + Storage replicaStorage, + Storage 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; -- cgit v1.2.3