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