summaryrefslogtreecommitdiff
path: root/src/main/java/com/amazon/carbonado/repo/jdbc
diff options
context:
space:
mode:
authorBrian S. O'Neill <bronee@gmail.com>2006-08-30 02:24:36 +0000
committerBrian S. O'Neill <bronee@gmail.com>2006-08-30 02:24:36 +0000
commit5a2aeb3ab59f286a6d2a5d8b7d62f4b17132b2b7 (patch)
tree6901a95bdd325cabfc3613fb11dbcc6afbccddac /src/main/java/com/amazon/carbonado/repo/jdbc
parent8f88478b4be9c3165d678c43640052c8fc7d8943 (diff)
Add core repository implementations
Diffstat (limited to 'src/main/java/com/amazon/carbonado/repo/jdbc')
-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
27 files changed, 8266 insertions, 0 deletions
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;