[java-plugin-storage-jdbc] 05/05: OSJ-342 Investigate Strategies to end of life our use of Hibernate in V5
Rod Widdowson
rdw at steadingsoftware.com
Fri May 20 14:00:58 UTC 2022
This is an automated email from the git hooks/post-receive script.
rdw pushed a commit to branch main
in repository java-plugin-storage-jdbc.
View the commit online:
http://git.shibboleth.net/view/?p=java-plugin-storage-jdbc.git;a=commit;h=7b8a5b8577c6a8197f944d06b1ec8465ee66b9ef
commit 7b8a5b8577c6a8197f944d06b1ec8465ee66b9ef
Author: Rod Widdowson <rdw at steadingsoftware.com>
AuthorDate: Fri May 20 14:17:13 2022 +0100
OSJ-342 Investigate Strategies to end of life our use of Hibernate in V5
https://shibboleth.atlassian.net/browse/OSJ-342
Initial populate and tests.
---
jdbc-storage-impl/pom.xml | 36 +-
.../storage/jdbc/impl/JDBCStorageRecord.java | 48 +
.../storage/jdbc/impl/JDBCStorageService.java | 1047 ++++++++++++++++++++
.../storage/jdbc/impl/JDBCStorageServiceTest.java | 328 ++++++
.../jdbc/impl/JDCBJPAMixedStorageServiceTest.java | 316 ++++++
.../src/test/resources/logback-test.xml | 21 +
pom.xml | 8 +-
7 files changed, 1797 insertions(+), 7 deletions(-)
diff --git a/jdbc-storage-impl/pom.xml b/jdbc-storage-impl/pom.xml
index 9492b36..7ad2940 100644
--- a/jdbc-storage-impl/pom.xml
+++ b/jdbc-storage-impl/pom.xml
@@ -21,8 +21,8 @@
<dependencies>
<!-- compile time intra project dependencies -->
<dependency>
- <groupId>net.shibboleth.idp.plugin.storage.jdbc</groupId>
- <artifactId>idp-plugin-jdbc-storage-api</artifactId>
+ <groupId>net.shibboleth.plugin.storage.jdbc</groupId>
+ <artifactId>jdbc-storage-api</artifactId>
</dependency>
<!-- Service API and Plugin Description dependency -->
<dependency>
@@ -35,7 +35,37 @@
<artifactId>idp-admin-impl</artifactId>
<scope>provided</scope>
</dependency>
-
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>${opensaml.groupId}</groupId>
+ <artifactId>opensaml-storage-api</artifactId>
+ <version>${opensaml.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${opensaml.groupId}</groupId>
+ <artifactId>opensaml-storage-impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-dbcp2</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hsqldb</groupId>
+ <artifactId>hsqldb</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <!--
+ <dependency>
+ <groupId>com.microsoft.sqlserver</groupId>
+ <artifactId>mssql-jdbc</artifactId>
+ <version>10.2.0.jre11</version>
+ <scope>test</scope>
+ </dependency>
+ -->
</dependencies>
diff --git a/jdbc-storage-impl/src/main/java/net/shibboleth/plugin/storage/jdbc/impl/JDBCStorageRecord.java b/jdbc-storage-impl/src/main/java/net/shibboleth/plugin/storage/jdbc/impl/JDBCStorageRecord.java
new file mode 100644
index 0000000..ff5be43
--- /dev/null
+++ b/jdbc-storage-impl/src/main/java/net/shibboleth/plugin/storage/jdbc/impl/JDBCStorageRecord.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.plugin.storage.jdbc.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.storage.MutableStorageRecord;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/** Storage record used by {@link JDBCStorageService}.
+ * This is notable in that it allows creation with a specified version (from
+ * the database).
+ * @param <T> type of record
+ */
+class JDBCStorageRecord<T> extends MutableStorageRecord<T> {
+
+ /**
+ * Constructor.
+ *
+ * @param val The value to store
+ * @param exp The expiration
+ * @param version The version.
+ */
+ public JDBCStorageRecord(@Nonnull @NotEmpty final String val,
+ @Nullable final Long exp, @Nullable final Long version) {
+ super(val, exp);
+ if (version != null) {
+ setVersion(version);
+ }
+ }
+}
\ No newline at end of file
diff --git a/jdbc-storage-impl/src/main/java/net/shibboleth/plugin/storage/jdbc/impl/JDBCStorageService.java b/jdbc-storage-impl/src/main/java/net/shibboleth/plugin/storage/jdbc/impl/JDBCStorageService.java
new file mode 100644
index 0000000..aeefb1d
--- /dev/null
+++ b/jdbc-storage-impl/src/main/java/net/shibboleth/plugin/storage/jdbc/impl/JDBCStorageService.java
@@ -0,0 +1,1047 @@
+// Checkstyle: FileLength|Header OFF
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.plugin.storage.jdbc.impl;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.TimerTask;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.sql.DataSource;
+
+import org.opensaml.storage.AbstractStorageService;
+import org.opensaml.storage.MutableStorageRecord;
+import org.opensaml.storage.StorageCapabilitiesEx;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.VersionMismatchException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
+import net.shibboleth.utilities.java.support.collection.Pair;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+
+/**
+ * Implementation of {@link org.opensaml.storage.StorageService} that uses native JDBC to persist to a database.
+ */
+public final class JDBCStorageService extends AbstractStorageService implements StorageCapabilitiesEx {
+
+ /* Default SQL. Keep it here to keep it together and to allow isertion into javadoc */
+ /** The SQL to get all the contexts.*/
+ static final String DEFAULT_READ_CONTEXTS_SQL = "SELECT context FROM StorageRecords";
+
+ /** The SQL to get all the records.*/
+ static final String DEFAULT_READ_ALL_SQL = "SELECT context, id, expires, value, version FROM StorageRecords";
+
+ /** The SQL to get all the records for a specific context.*/
+ static final String DEFAULT_READ_ALL_BY_CONTEXT_SQL =
+ "SELECT id, expires, value, version FROM StorageRecords WHERE context = ?";
+
+ /** The SQL to get check whether this record already exists and is unexpired prior to a create.*/
+ static final String DEFAULT_PRE_CREATE_QUERY_SQL = "SELECT expires FROM StorageRecords WHERE context =? AND id=?";
+
+ /** The SQL to create a new record. */
+ static final String DEFAULT_CREATE_CREATE_RECORD_SQL = "INSERT INTO StorageRecords VALUES (?, ?, ?, ?, 1)";
+
+ /** The SQL to update an expired record as part of a create. */
+ static final String DEFAULT_CREATE_UPDATE_RECORD_SQL =
+ "UPDATE StorageRecords SET value=?, version=1, expires=? WHERE context=? AND id=?";
+
+ /** The SQL to read a single record.*/
+ static final String DEFAULT_READ_RECORD_SQL =
+ "SELECT version, expires, value FROM StorageRecords WHERE context =? AND id=?";
+
+ /** The SQL to check whether a record exists prior to updating it. */
+ static final String DEFAULT_PRE_UPDATE_QUERY_SQL =
+ "SELECT version, expires FROM StorageRecords WHERE context =? AND id=?";
+
+ /** The SQL to update a record. */
+ static final String DEFAULT_UPDATE_RECORD_SQL =
+ "UPDATE StorageRecords SET value=?, version=?, expires=? WHERE context=? AND id=?";
+
+ /** The SQL to check whether a record exists prior to deleting it.*/
+ static final String DEFAULT_PRE_DELETE_QUERY_SQL ="SELECT version FROM StorageRecords WHERE context =? AND id=?";
+
+ /** The SQL to delete a record. */
+ static final String DEFAULT_DELETE_RECORD_SQL = "DELETE FROM StorageRecords WHERE context=? AND id=?";
+
+ /** The SQL to delete all records by specified expiration. */
+ static final String DEFAULT_DELETE_BY_EXPIRED_SQL = "DELETE FROM StorageRecords WHERE expires < ? ";
+
+ /** The SQL to delete all records by specified context and expiration. */
+ static final String DEFAULT_DELETE_BY_CONTEXT_EXPIRED_SQL =
+ "DELETE FROM StorageRecords WHERE context = ? AND expires < ?";
+
+ /** The SQL to update the expiration for a given context. */
+ static final String DEFAULT_UPDATE_EXPIRES_BY_CONTEXT_SQL =
+ "UPDATE StorageRecords SET expires = ? WHERE context = ? AND expires > ? ";
+
+ /** The SQL to delete a given context. */
+ static final String DEFAULT_DELETE_BY_CONTEXT_SQL = "DELETE FROM StorageRecords WHERE context = ? ";
+
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(JDBCStorageService.class);
+
+ /** How many times do we try an operation before giving up? */
+ private int transactionRetry = 3;
+
+ /** Error messages that signal a transaction should be retried. */
+ @Nonnull @NonnullElements private Collection<String> retryableErrors = Collections.emptyList();
+
+ /** The Data Source. */
+ @NonnullAfterInit private DataSource dataSource;
+
+ /* SQL Statements */
+ /**
+ * The SQL to get all the contexts.
+ * Default: {@value #DEFAULT_READ_CONTEXTS_SQL}.
+ */
+ @Nonnull @NotEmpty private String readContextsSQL = DEFAULT_READ_CONTEXTS_SQL;
+
+ /**
+ * The SQL to get all the records.
+ * Default: {@value #DEFAULT_READ_ALL_SQL}.
+ */
+ @Nonnull @NotEmpty private String readAllSQL = DEFAULT_READ_ALL_SQL;
+
+ /**
+ * The SQL to get all the records for a specific context.
+ * Default: {@value #DEFAULT_READ_ALL_BY_CONTEXT_SQL}
+ */
+ @Nonnull @NotEmpty private String readAllByContextSQL = DEFAULT_READ_ALL_BY_CONTEXT_SQL;
+
+ /**
+ * The SQL to get check whether this record already exists and is unexpired prior to a create.
+ * Default: {@value #DEFAULT_PRE_CREATE_QUERY_SQL}
+ */
+ @Nonnull @NotEmpty private String preCreateQuerySQL = DEFAULT_PRE_CREATE_QUERY_SQL;
+
+ /**
+ * The SQL to create a new record (transactional with {@link #preCreateQuerySQL}.
+ * Default: {@value #DEFAULT_CREATE_CREATE_RECORD_SQL}
+ */
+ @Nonnull @NotEmpty private String createCreateRecordSQL = DEFAULT_CREATE_CREATE_RECORD_SQL;
+
+ /**
+ * The SQL to update an expired record as part of a create (transactional with {@link #preCreateQuerySQL}.
+ * Default: {@value #DEFAULT_CREATE_UPDATE_RECORD_SQL}
+ */
+ @Nonnull @NotEmpty private String createUpdateRecordSQL = DEFAULT_CREATE_UPDATE_RECORD_SQL;
+
+ /**
+ * The SQL to read a single record.
+ * Default: {@value #DEFAULT_READ_RECORD_SQL}
+ */
+ @Nonnull @NotEmpty private String readRecordSQL = DEFAULT_READ_RECORD_SQL;
+
+ /**
+ * The SQL to check whether a record exists prior to updating it.
+ * Default: {@value #DEFAULT_PRE_UPDATE_QUERY_SQL}
+ */
+ @Nonnull @NotEmpty private String preUpdateQuerySQL = DEFAULT_PRE_UPDATE_QUERY_SQL;
+
+ /** The SQL to update a record. Transactional with {@link #preUpdateQuerySQL}.
+ * Default: {@value #DEFAULT_UPDATE_RECORD_SQL}
+ */
+ @Nonnull @NotEmpty private String updateRecordSQL = DEFAULT_UPDATE_RECORD_SQL;
+
+ /**
+ * The SQL to check whether a record exists prior to deleting it.
+ * Default: {@value #DEFAULT_PRE_DELETE_QUERY_SQL}
+ */
+ @Nonnull @NotEmpty private String preDeleteQuerySQL = DEFAULT_PRE_DELETE_QUERY_SQL;
+
+ /**
+ * The SQL to delete a record. Transactional with {@link #preDeleteQuerySQL}
+ * Default: {@value #DEFAULT_DELETE_RECORD_SQL}
+ */
+ @Nonnull @NotEmpty private String deleteRecordSQL = DEFAULT_DELETE_RECORD_SQL;
+
+ /**
+ * The SQL to delete expired record. Used as part of the {@link #getCleanupTask()}.
+ * Default: {@value #DEFAULT_DELETE_BY_EXPIRED_SQL}
+ */
+ @Nonnull @NotEmpty private String deleteByExpiredSQL = DEFAULT_DELETE_BY_EXPIRED_SQL;
+
+ /**
+ * The SQL to delete expired record with specified context. Used as part of {@link #reap(String)}.
+ * Default: {@value #DEFAULT_DELETE_BY_CONTEXT_EXPIRED_SQL}
+ */
+ @Nonnull @NotEmpty private String deleteByContextExpiredSQL = DEFAULT_DELETE_BY_CONTEXT_EXPIRED_SQL;
+
+ /**
+ * The SQL to update the expiration of a given context.
+ * Default: {@value #DEFAULT_UPDATE_EXPIRES_BY_CONTEXT_SQL}
+ */
+ @Nonnull @NotEmpty private String updateExpiresByContextSQL = DEFAULT_UPDATE_EXPIRES_BY_CONTEXT_SQL;
+
+ /**
+ * The SQL to to delete a given context. .
+ * Default: {@value #DEFAULT_DELETE_BY_CONTEXT_SQL}
+ */
+ @Nonnull @NotEmpty private String deleteByContextSQL = DEFAULT_DELETE_BY_CONTEXT_SQL;
+
+ /* Bean Setters*/
+ /** set {@link #transactionRetry}.
+ * @param count how many time to try before we bail.
+ */
+ public void setTransactionRetry(@Positive final int count) {
+ transactionRetry = count;
+ if (count < 0) {
+ throw new ConstraintViolationException("transaction retry must be positive");
+ }
+ }
+
+ /** Set the {@link DataSource}.
+ * @param source what to set.
+ */
+ public void setDataSource(@Nonnull final DataSource source) {
+ dataSource = Constraint.isNotNull(source, "DataSource should be non null");
+ }
+
+ /** What errors do we retry?
+ * @param errors what to set.
+ */
+ public void setRetryableErrors(@Nonnull @NonnullElements final Collection<String> errors) {
+ retryableErrors = Constraint.isNotNull(errors, "errors must not be null");
+ Constraint.noNullItems(errors, "errors must not have null members");
+ }
+
+ /** SQL to read contexts.
+ * @param what the SQL to set.
+ */
+ public void setReadContextsSQL(@Nonnull @NotEmpty final String what) {
+ readContextsSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "Read Context SQL should be non-null and non empty");
+ }
+
+ /** SQL to read all contexts.
+ * @param what the SQL to set.
+ */
+ public void setReadAllByContextSQL(@Nonnull @NotEmpty final String what) {
+ readAllByContextSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "Read All By Context SQL should be non-null and non empty");
+ }
+
+ /** SQL to read all contexts.
+ * @param what the SQL to set.
+ */
+ public void setReadAllSQL(@Nonnull @NotEmpty final String what) {
+ readAllSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "Read All SQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to get check whether this record already exists and is unexpired prior to a create.
+ * This starts a transaction which will be completed after either the SQL in
+ * {@link #createCreateRecordSQL} or {@link #createUpdateRecordSQL}.
+ * @param what the SQL to set.
+ */
+ public void setPreCreateQuerySQL(@Nonnull @NotEmpty final String what) {
+ preCreateQuerySQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "PreCreateQuerySQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to create a new record (transactional with {@link #preCreateQuerySQL}.
+ * @param what the SQl to set.
+ */
+ public void setCreateCreateRecordSQL(@Nonnull @NotEmpty final String what) {
+ createCreateRecordSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "CreateCreateRecordSQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to update an expired record as part of a create (transactional with {@link #preCreateQuerySQL}.
+ * @param what the SQL to set.
+ */
+ public void setCreateUpdateRecordSQL(@Nonnull @NotEmpty final String what) {
+ createUpdateRecordSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "CreateUpdateRecordSQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to read a single record.
+ * @param what what to set.
+ */
+ public void setReadRecordSQL(@Nonnull @NotEmpty final String what) {
+ readRecordSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "ReadRecordSQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to get check whether this record already exists and is unexpired prior to an update.
+ * @param what The preUpdateQuerySQL to set.
+ */
+ public void setPreUpdateQuerySQL(@Nonnull @NotEmpty final String what) {
+ preUpdateQuerySQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "PreUpdateQuerySQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to update a record. Transactional with {@link #preUpdateQuerySQL}
+ * @param what The updateQuerySQL to set.
+ */
+ public void setUpdateRecordSQL(@Nonnull @NotEmpty final String what) {
+ updateRecordSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "UpdateRecordSQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to check whether a record exists prior to deleting it.
+ * @param what The SQL to set.
+ */
+ public void setPreDeleteQuerySQL(final String what) {
+ preDeleteQuerySQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "PreDeleteSQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to delete a record. Transactional with {@link #preDeleteQuerySQL}.
+ * @param what The SQL to set.
+ */
+ public void setDeleteRecordSQL(final String what) {
+ deleteRecordSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "DeleteRecordSQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to delete expired record.
+ * Used as part of the {@link #getCleanupTask()}.
+ * @param what The SQL to set.
+ */
+ public void setDeleteByExpiredSQL(final String what) {
+ deleteByExpiredSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "DeleteByExpiredSQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to delete expired record with specified context.
+ * Used as part of {@link #reap(String)}.
+ * @param what The SQL to set.
+ */
+ public void setDeleteByContextExpiredSQL(final String what) {
+ deleteByContextExpiredSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "DeleteByContextExpiredSQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to update the expiration of a record specified by context.
+ * @param what The SQL to set.
+ */
+ public void setUpdateExpiresByContextSQL(final String what) {
+ updateExpiresByContextSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "UpdateExpiresByContextSQL should be non-null and non empty");
+ }
+
+ /** Set the SQL to Delete a specified Context.
+ * @param what The SQL to set.
+ */
+ public void setDeleteByContextSQL(final String what) {
+ deleteByContextSQL = Constraint.isNotNull(StringSupport.trimOrNull(what),
+ "DeleteByContextSQL should be non-null and non empty");
+ }
+
+ /** {@inheritDoc} */
+ protected void doInitialize() throws ComponentInitializationException {
+ Constraint.isNotNull(dataSource, "data source must be specified and non-null");
+ super.doInitialize();
+ }
+
+ /**
+ * Returns all contexts from the store (for testing only).
+ *
+ * @return all contexts or an empty list
+ * @throws IOException if errors occur in the read process
+ */
+ @Nonnull @NonnullElements protected List<String> readContexts() throws IOException {
+ final List<String> result = new ArrayList<>();
+ try (final Connection connection = getConnection(true)) {
+ log.trace("ReadContexts:: ", readContextsSQL);
+ final PreparedStatement query = connection.prepareStatement(readContextsSQL);
+ final ResultSet results = query.executeQuery();
+ while (results.next()) {
+ final String context = results.getString(1);
+ log.trace("Context = '{}'", context);
+ result.add(context);
+ }
+ return result;
+
+ } catch (final SQLException e) {
+ log.error("ReadContexts failed", e);
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Returns all records from the store (for testing only).
+ *
+ * @return all records or an empty list
+ * @throws IOException if errors occur in the read process
+ */
+ @Nonnull @NonnullElements protected List<?> readAll() throws IOException {
+ final List<JDBCStorageRecord<?>> result = new ArrayList<>();
+ try (final Connection connection = getConnection(true)) {
+ log.trace("ReadAll:: '{}' ", readAllSQL);
+ final PreparedStatement query = connection.prepareStatement(readAllSQL);
+ final ResultSet results = query.executeQuery();
+ while (results.next()) {
+ final String context = results.getString(1);
+ final String id = results.getString(2);
+ final Long expires = getExpires(results, 3);
+ final String value = results.getString(4);
+ final Long version = results.getLong(5);
+ log.trace("Record: Context = '{}', Id = '{}', value = '{}', verion = '{}', expires = '{}'",
+ context, id, value, version, expires == null ? "<never>": expires);
+ result.add(new JDBCStorageRecord<>(value, expires, version));
+ }
+ return result;
+
+ } catch (final SQLException e) {
+ log.error("ReadAll failed", e);
+ throw new IOException(e);
+ }
+ }
+
+ /**
+ * Returns all records from the store for the supplied context (for testing only).
+ *
+ * @param context a storage context label
+ *
+ * @return all records in the context or an empty list
+ * @throws IOException if errors occur in the read process
+ */
+ @Nonnull @NonnullElements protected List<?> readAll(@Nonnull @NotEmpty final String context)
+ throws IOException {
+ final List<JDBCStorageRecord<?>> result = new ArrayList<>();
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "ReadAll(String): context must not be null"),
+ "ReadAll(String): context must not be empty");
+ try (final Connection connection = getConnection(true)) {
+ log.trace("ReadAll:: '{}' 1: '{}' ", readAllByContextSQL, context);
+ final PreparedStatement query = connection.prepareStatement(readAllByContextSQL);
+ query.setString(1, context);
+ final ResultSet results = query.executeQuery();
+ while (results.next()) {
+ final String id = results.getString(1);
+ final Long expires = getExpires(results, 2);
+ final String value = results.getString(3);
+ final Long version = results.getLong(4);
+ log.trace("Record: Id = '{}', value = '{}', verion = '{}', expires = '{}'",
+ id, value, version, expires == null ? "<never>": expires);
+ result.add(new JDBCStorageRecord<>(value, expires, version));
+ }
+ return result;
+
+ } catch (final SQLException e) {
+ log.error("ReadAll()", e);
+ throw new IOException(e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ // Checkstyle: CyclomaticComplexity OFF
+ public boolean create(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key,
+ @Nonnull @NotEmpty final String value, @Nullable @Positive final Long expiration) throws IOException {
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "create: context must not be null"),
+ "create: context must not be empty");
+ Constraint.isNotEmpty(Constraint.isNotNull(value, "create: value must not be null"),
+ "create: value must not be empty");
+ int retries = transactionRetry;
+ while(true) {
+ try (final Connection connection = getConnection(false)) {
+ log.trace("Create [Query]:: '{}':: 1: '{}' ; 2: '{}'", preCreateQuerySQL, context, key);
+ // Does it exist?
+ // If not insert
+ // If so check expiration
+ // If not expired complain
+ // otherwise update
+ final PreparedStatement query = connection.prepareStatement(preCreateQuerySQL);
+ query.setString(1, context);
+ query.setString(2, key);
+ final ResultSet resultSet = query.executeQuery();
+ if (!resultSet.next()) {
+ log.trace("Create [Insert]:: '{}' 1: '{}' ; 2: '{}' ; 3 '{}' ; 4 '{}'", createCreateRecordSQL,
+ context, key, expiration, value);
+ final PreparedStatement insert = connection.prepareStatement(createCreateRecordSQL);
+ insert.setString(1, context);
+ insert.setString(2, key);
+ setExpires(insert, 3, expiration);
+ insert.setString(4,value);
+ insert.executeUpdate();
+ connection.commit();
+ return true;
+ }
+ final Long returnedExpiration = getExpires(resultSet, 1);
+ if (returnedExpiration == null || System.currentTimeMillis() < returnedExpiration) {
+ log.debug("Duplicate record '{}' in context '{}'", key, context);
+ return false;
+ }
+ final PreparedStatement update = connection.prepareStatement(createUpdateRecordSQL);
+ log.trace("Create [Update]:: '{}' 1: '{}' ; 2: '{}' ; 3 '{}' ; 4 '{}'", createUpdateRecordSQL,
+ value, expiration, context, key);
+ update.setString(1, value);
+ setExpires(update, 2, expiration);
+ update.setString(3,context);
+ update.setString(4,key);
+ update.executeUpdate();
+ connection.commit();
+ return true;
+ } catch (final SQLException e) {
+ boolean retry = false;
+ for (final String msg : retryableErrors) {
+ if (e.getSQLState() != null && e.getSQLState().contains(msg)) {
+ log.warn("Caught retryable SQL exception", e);
+ retry = true;
+ break;
+ }
+ }
+ if (retry) {
+ if (--retries < 0) {
+ log.warn("Error retryable, but retry limit exceeded");
+ throw new IOException(e);
+ }
+ log.info("Retrying JDBC Create operation");
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+ }
+ // Checkstyle: CyclomaticComplexity ON
+
+ /** {@inheritDoc} */
+ @Override @Nullable public <T> StorageRecord<T> read(@Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key) throws IOException {
+ return this.<T>readImpl(context, key, null).getSecond();
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nonnull public <T> Pair<Long, StorageRecord<T>> read(@Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key, @Positive final long version) throws IOException {
+ return readImpl(context, key, version);
+ }
+
+ /**
+ * Reads the record matching the supplied parameters. Returns an empty pair if the record cannot be found or is
+ * expired.
+ *
+ * @param <T> type of object
+ * @param context to search for
+ * @param key to search for
+ * @param version to match
+ *
+ * @return pair of version and storage record
+ * @throws IOException if errors occur in the read process
+ */
+ // Checkstyle: CyclomaticComplexity OFF
+ @Nonnull protected <T> Pair<Long, StorageRecord<T>> readImpl(@Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key, @Positive @Nullable final Long version) throws IOException {
+
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "read: context must not be null"),
+ "read: context must not be empty");
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "read: key must not be null"),
+ "read: key must not be empty");
+
+ int retries = transactionRetry;
+ while(true) {
+ try (final Connection connection = getConnection(true)) {
+ final PreparedStatement stmnt = connection.prepareStatement(readRecordSQL);
+ log.trace("Read:: '{}' 1: '{}' ; 2: '{}'", readRecordSQL, context, key);
+ stmnt.setString(1, context);
+ stmnt.setString(2, key);
+ final ResultSet resultSet = stmnt.executeQuery();
+ if (!resultSet.next()) {
+ log.debug("Nothing returned");
+ return new Pair<>();
+ }
+ final Long returnedVersion = resultSet.getLong(1);
+ final Long returnedExpires = getExpires(resultSet, 2);
+ final String returnedValue = resultSet.getString(3);
+ log.trace("Considering Version '{}', Expires '{}', Value '{}'",
+ returnedVersion, returnedValue, returnedExpires);
+ if (returnedExpires != null && System.currentTimeMillis() >= returnedExpires) {
+ log.debug("Read failed, key '{}' expired in context '{}'", key, context);
+ return new Pair<>();
+ }
+ if (version != null && returnedVersion == version) {
+ // Nothing's changed, so just echo back the version.
+ return new Pair<>(version, null);
+ }
+ if (resultSet.next()) {
+ log.error("Multiple values returned?");
+ }
+ final MutableStorageRecord<T> result =
+ new JDBCStorageRecord<>(returnedValue, returnedExpires, returnedVersion);
+ return new Pair<>(version, result);
+ } catch (final SQLException e) {
+ boolean retry = false;
+ for (final String msg : retryableErrors) {
+ if (e.getSQLState() != null && e.getSQLState().contains(msg)) {
+ log.warn("Caught retryable SQL exception", e);
+ retry = true;
+ break;
+ }
+ }
+
+ if (retry) {
+ if (--retries < 0) {
+ log.warn("Error retryable, but retry limit exceeded");
+ throw new IOException(e);
+ }
+ log.info("Retrying JDBC Read operation");
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+ }
+ // Checkstyle: CyclomaticComplexity ON
+
+ /** {@inheritDoc} */
+ @Override public boolean update(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key,
+ @Nonnull @NotEmpty final String value, @Nullable @Positive final Long expiration) throws IOException {
+ try {
+ return updateImpl(null, context, key, value, expiration) != null;
+ } catch (final VersionMismatchException e) {
+ throw new IllegalStateException("Unexpected exception thrown by update.", e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable public Long updateWithVersion(@Positive final long version,
+ @Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key,
+ @Nonnull @NotEmpty final String value, @Nullable @Positive final Long expiration) throws IOException,
+ VersionMismatchException {
+ return updateImpl(version, context, key, value, expiration);
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean updateExpiration(@Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key, @Nullable @Positive final Long expiration) throws IOException {
+ try {
+ return updateImpl(null, context, key, null, expiration) != null;
+ } catch (final VersionMismatchException e) {
+ throw new IllegalStateException("Unexpected exception thrown by updateExpiration.", e);
+ }
+ }
+
+ /**
+ * Updates the record matching the supplied parameters. Returns null if the record cannot be found or is expired.
+ *
+ * @param version to check
+ * @param context to search for
+ * @param key to search for
+ * @param value to update
+ * @param expires to update
+ *
+ * @return the version of the record after update, null if no record exists
+ * @throws IOException if errors occur in the update process
+ * @throws VersionMismatchException if the record found contains a version that does not match the parameter
+ */
+ // Checkstyle: CyclomaticComplexity OFF
+ @Nullable protected Long updateImpl(@Nullable final Long version, @Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key, @Nonnull @NotEmpty final String value,
+ @Nullable @Positive final Long expires) throws IOException, VersionMismatchException {
+
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "update: context must not be null"),
+ "update: context must not be empty");
+ Constraint.isNotEmpty(Constraint.isNotNull(key, "update: key must not be null"),
+ "update: key must not be empty");
+ Constraint.isNotEmpty(Constraint.isNotNull(value, "update: value must not be null"),
+ "update: value must not be empty");
+ int retries = transactionRetry;
+ while (true) {
+ try (Connection connection = getConnection(false)) {
+ final PreparedStatement selectStmnt = connection.prepareStatement(preUpdateQuerySQL);
+ log.trace("Update [Query]:: '{}' 1: '{}' ; 2: '{}'", preUpdateQuerySQL, context, key);
+
+ selectStmnt.setString(1, context);
+ selectStmnt.setString(2, key);
+
+ final ResultSet resultSet = selectStmnt.executeQuery();
+ if (!resultSet.next()) {
+ log.debug("Nothing returned");
+ return null;
+ }
+ final Long returnedExpires = getExpires(resultSet, 2);
+ final Long returnedVersion = resultSet.getLong(1);
+ if (returnedExpires != null && System.currentTimeMillis() >= returnedExpires) {
+ log.debug("Update failed, key '{}' expired in context '{}'", key, context);
+ return null;
+ }
+
+ if (version != null && returnedVersion != version) {
+ // Caller is out of sync.
+ throw new VersionMismatchException();
+ }
+ final PreparedStatement updateStmnt = connection.prepareStatement(updateRecordSQL);
+ final Long newVersion = Long.valueOf(returnedVersion + 1);
+ log.trace("Update [Update]:: '{}': 1: '{}' ; 2: '{}' ; 3: '{}' ; 4: '{}' ; 5: '{}'", updateRecordSQL,
+ value, newVersion, expires, context, key);
+ updateStmnt.setString(1, value);
+ updateStmnt.setLong(2, newVersion);
+ setExpires(updateStmnt, 3, expires);
+ updateStmnt.setString(4, context);
+ updateStmnt.setString(5, key);
+ updateStmnt.executeUpdate();
+ connection.commit();
+ return newVersion;
+ } catch (final SQLException e) {
+ boolean retry = false;
+ for (final String msg : retryableErrors) {
+ if (e.getSQLState() != null && e.getSQLState().contains(msg)) {
+ log.warn("Caught retryable SQL exception", e);
+ retry = true;
+ break;
+ }
+ }
+
+ if (retry) {
+ if (--retries < 0) {
+ log.warn("Error retryable, but retry limit exceeded");
+ throw new IOException(e);
+ }
+ log.info("Retrying JDBC Update Operation");
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+ }
+ // Checkstyle: CyclomaticComplexity ON
+
+ /** {@inheritDoc} */
+ @Override public boolean deleteWithVersion(@Positive final long version, @Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key) throws IOException, VersionMismatchException {
+ return deleteImpl(version, context, key);
+ }
+
+ /** {@inheritDoc} */
+ @Override public boolean delete(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key)
+ throws IOException {
+ try {
+ return deleteImpl(null, context, key);
+ } catch (final VersionMismatchException e) {
+ throw new IllegalStateException("Unexpected exception thrown by delete.", e);
+ }
+ }
+
+ /**
+ * Deletes the record matching the supplied parameters.
+ *
+ * @param version to check
+ * @param context to search for
+ * @param key to search for
+ *
+ * @return whether the record was deleted
+ * @throws IOException if errors occur in the delete process
+ * @throws VersionMismatchException if the record found contains a version that does not match the parameter
+ */
+ // Checkstyle: CyclomaticComplexity OFF
+ protected boolean deleteImpl(@Nullable @Positive final Long version, @Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key) throws IOException, VersionMismatchException {
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "delete: context must not be null"),
+ "delete: context must not be empty");
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "delete: key must not be null"),
+ "delete: key must not be empty");
+ Constraint.isTrue(version == null || version > 0, "delete: version should be null of > 0");
+ int retries = transactionRetry;
+ while (true) {
+ try (Connection connection = getConnection(false)) {
+ final PreparedStatement selectStmnt = connection.prepareStatement(preDeleteQuerySQL);
+ selectStmnt.setString(1, context);
+ selectStmnt.setString(2, key);
+ log.trace("Delete [Query]:: '{}': 1: '{}' ; 2: '{}'", preDeleteQuerySQL, context, key);
+ final ResultSet resultSet = selectStmnt.executeQuery();
+ if (!resultSet.next()) {
+ log.debug("Nothing returned");
+ return false;
+ }
+ final Long returnedVersion = resultSet.getLong(1);
+ if (version != null && returnedVersion != version) {
+ throw new VersionMismatchException();
+ }
+ final PreparedStatement deleteStmnt = connection.prepareStatement(deleteRecordSQL);
+ log.trace("Delete [Delete]:: '{}': 1: '{}' ; 2: '{}'", deleteRecordSQL, context, key);
+ deleteStmnt.setString(1, context);
+ deleteStmnt.setString(2, key);
+ deleteStmnt.execute();
+ connection.commit();
+ return true;
+ } catch (final SQLException e) {
+ boolean retry = false;
+ for (final String msg : retryableErrors) {
+ if (e.getSQLState() != null && e.getSQLState().contains(msg)) {
+ log.warn("Caught retryable SQL exception", e);
+ retry = true;
+ break;
+ }
+ }
+
+ if (retry) {
+ if (--retries < 0) {
+ log.warn("Error retryable, but retry limit exceeded");
+ throw new IOException(e);
+ }
+ log.info("Retrying JDBC Delete Operation");
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+ }
+ // Checkstyle: CyclomaticComplexity ON
+
+ /**
+ * Deletes every record with an expiration before the supplied expiration.
+ *
+ * @param expiration of records to delete
+ *
+ * @throws IOException if errors occur in the cleanup process
+ */
+ protected void deleteImpl(@Nonnull final Long expiration) throws IOException {
+ Constraint.isNotNull(expiration, "expiration: context must not be null");
+ int retries = transactionRetry;
+ while (true) {
+ try (Connection connection = getConnection(false)) {
+ final PreparedStatement updateStmnt = connection.prepareStatement(deleteByExpiredSQL);
+ updateStmnt.setLong(1, expiration);
+ log.trace("DeleteByExpired:: '{}': 1: '{}' ;", deleteByExpiredSQL, expiration);
+ updateStmnt.execute();
+ connection.commit();
+ return;
+ } catch (final SQLException e) {
+ boolean retry = false;
+ for (final String msg : retryableErrors) {
+ if (e.getSQLState() != null && e.getSQLState().contains(msg)) {
+ log.warn("Caught retryable SQL exception", e);
+ retry = true;
+ break;
+ }
+ }
+
+ if (retry) {
+ if (--retries < 0) {
+ log.warn("Error retryable, but retry limit exceeded");
+ throw new IOException(e);
+ }
+ log.info("Retrying JDBC DeletebyExpiration Operation");
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void reap(@Nonnull @NotEmpty final String context) throws IOException {
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "reap: context must not be null"),
+ "reap: context must not be empty");
+ int retries = transactionRetry;
+ while (true) {
+ try (Connection connection = getConnection(true)) {
+ final PreparedStatement updateStmnt = connection.prepareStatement(deleteByContextExpiredSQL);
+ updateStmnt.setString(1, context);
+ final Long expires = System.currentTimeMillis();
+ setExpires(updateStmnt, 2, expires);
+ log.trace("Reap:: '{}': 1: '{}' ; 2: '{}' ;", deleteByContextExpiredSQL, context, expires);
+ updateStmnt.execute();
+ connection.commit();
+ return;
+ } catch (final SQLException e) {
+ boolean retry = false;
+ for (final String msg : retryableErrors) {
+ if (e.getSQLState() != null && e.getSQLState().contains(msg)) {
+ log.warn("Caught retryable SQL exception", e);
+ retry = true;
+ break;
+ }
+ }
+ if (retry) {
+ if (--retries < 0) {
+ log.warn("Error retryable, but retry limit exceeded");
+ throw new IOException(e);
+ }
+ log.info("Retrying JDBC DeleteByContext Operation");
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void updateContextExpiration(@Nonnull @NotEmpty final String context, @Nullable final Long expires)
+ throws IOException {
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "updateContextExpiration: context must not be null"),
+ "updateContextExpiration: context must not be empty");
+ int retries = transactionRetry;
+ while (true) {
+ try (Connection connection = getConnection(true)) {
+ final PreparedStatement updateStmnt = connection.prepareStatement(updateExpiresByContextSQL);
+ setExpires(updateStmnt, 1, expires);
+ updateStmnt.setString(2, context);
+ final Long newExpires = System.currentTimeMillis();
+ setExpires(updateStmnt, 3, newExpires);
+ log.trace("UpdateContextExpiration:: '{}': 1: '{}' ; 2: '{}' ; 3: '{}' ;",
+ updateExpiresByContextSQL, expires, context, newExpires);
+ updateStmnt.execute();
+ connection.commit();
+ return;
+ } catch (final SQLException e) {
+ boolean retry = false;
+ for (final String msg : retryableErrors) {
+ if (e.getSQLState() != null && e.getSQLState().contains(msg)) {
+ log.warn("Caught retryable SQL exception", e);
+ retry = true;
+ break;
+ }
+ }
+ if (retry) {
+ if (--retries < 0) {
+ log.warn("Error retryable, but retry limit exceeded");
+ throw new IOException(e);
+ }
+ log.info("Retrying JDBC DeleteByContext Operation");
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+ }
+
+ /** {@inheritDoc} */
+ public void deleteContext(@Nonnull @NotEmpty final String context) throws IOException {
+ Constraint.isNotEmpty(Constraint.isNotNull(context, "deleteContext: context must not be null"),
+ "deleteContext: context must not be empty");
+ int retries = transactionRetry;
+ while (true) {
+ try (Connection connection = getConnection(true)) {
+ final PreparedStatement updateStmnt = connection.prepareStatement(deleteByContextSQL);
+ updateStmnt.setString(1, context);
+ log.trace("UpdateContextExpiration:: '{}': 1: '{}'", deleteByContextSQL, context);
+
+ updateStmnt.execute();
+ connection.commit();
+ return;
+ } catch (final SQLException e) {
+ boolean retry = false;
+ for (final String msg : retryableErrors) {
+ if (e.getSQLState() != null && e.getSQLState().contains(msg)) {
+ log.warn("Caught retryable SQL exception", e);
+ retry = true;
+ break;
+ }
+ }
+
+ if (retry) {
+ if (--retries < 0) {
+ log.warn("Error retryable, but retry limit exceeded");
+ throw new IOException(e);
+ }
+ log.info("Retrying JDBC DeleteByContext Operation");
+ } else {
+ throw new IOException(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Obtain a connection from the data source.
+ *
+ * <p>The caller must close the connection.</p>
+ *
+ * @param autoCommit auto-commit setting to apply to the connection
+ *
+ * @return a fresh connection
+ * @throws SQLException if an error occurs
+ */
+ @Nonnull private Connection getConnection(final boolean autoCommit) throws SQLException {
+ final Connection conn = dataSource.getConnection();
+ conn.setAutoCommit(autoCommit);
+ conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
+ return conn;
+ }
+
+ /** Return the value of expires in the supplied column of the supplied {@link ResultSet}.
+ * @param results the results whose current row we want to inspect
+ * @param columm the column
+ * @return the expiration (converting an SQL null into a null)
+ * @throws SQLException if the results interrogation fails
+ */
+ @Nullable private static Long getExpires(@Nonnull final ResultSet results, final int columm) throws SQLException {
+ final long value = results.getLong(columm);
+ if (results.wasNull()) {
+ return null;
+ }
+ return value;
+ }
+
+ /** Set the value of expiration into the prepared statement at the suppiled column
+ * converting java nulls into SQL nulls.
+ *
+ * @param stmnt where to put it
+ * @param column which column to put it in
+ * @param expires
+ * @throws SQLException
+ */
+ private static void setExpires(@Nonnull final PreparedStatement stmnt,
+ final int column, final @Nullable Long expires) throws SQLException {
+ if (expires == null) {
+ stmnt.setNull(column, Types.BIGINT);
+ } else {
+ stmnt.setLong(column, expires);
+ }
+ }
+
+ /** {@inheritDoc} */
+ public boolean isServerSide() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isClustered() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override @Nullable protected TimerTask getCleanupTask() {
+ return new TimerTask() {
+
+ /** {@inheritDoc} */
+ @Override public void run() {
+ final Long now = System.currentTimeMillis();
+ log.debug("Running cleanup task at {}", now);
+ try {
+ deleteImpl(now);
+ } catch (final IOException e) {
+ log.error("Error running cleanup task for {}", now, e);
+ }
+ log.debug("Finished cleanup task for {}", now);
+ }
+ };
+ }
+}
diff --git a/jdbc-storage-impl/src/test/java/net/shibboleth/plugin/storage/jdbc/impl/JDBCStorageServiceTest.java b/jdbc-storage-impl/src/test/java/net/shibboleth/plugin/storage/jdbc/impl/JDBCStorageServiceTest.java
new file mode 100644
index 0000000..8f15084
--- /dev/null
+++ b/jdbc-storage-impl/src/test/java/net/shibboleth/plugin/storage/jdbc/impl/JDBCStorageServiceTest.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.plugin.storage.jdbc.impl;
+
+import static org.testng.Assert.assertTrue;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.time.Duration;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+
+import org.apache.commons.dbcp2.BasicDataSource;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.opensaml.storage.impl.JPAStorageService;
+import org.opensaml.storage.testing.StorageServiceTest;
+import org.testng.Assert;
+import org.testng.TestException;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+/**
+ * Test of {@link JPAStorageService} implementation.
+ */
+//@Test(enabled = false)
+ at SuppressWarnings("javadoc")
+public class JDBCStorageServiceTest extends StorageServiceTest {
+
+ private JDBCStorageService storageService;
+
+ private final boolean USE_SQLSERVER = false;
+
+ private static final String INIT_SQL_SQLSERVER="CREATE TABLE StorageRecords (\r\n"
+ + " context varchar(255) COLLATE Latin1_General_100_CS_AS NOT NULL,\n"
+ + " id varchar(255) COLLATE Latin1_General_100_CS_AS NOT NULL,\n"
+ + " expires bigint DEFAULT NULL,\n"
+ + " value varchar(255) NOT NULL,\n"
+ + " version bigint NOT NULL,\n"
+ + " PRIMARY KEY (context,id)\n"
+ + ")";
+
+ private static final String INIT_SQL_HSQLDB="CREATE TABLE StorageRecords (\r\n"
+ + " context varchar(255) NOT NULL,\n"
+ + " id varchar(255) NOT NULL,\n"
+ + " expires bigint DEFAULT NULL,\n"
+ + " value varchar(255) NOT NULL,\n"
+ + " version bigint NOT NULL,\n"
+ + " PRIMARY KEY (context,id)\n"
+ + ")";
+
+ private static final String CLEANUP_SQL = "DROP TABLE StorageRecords;";
+
+ /** Contexts used for testing. */
+ private Object[][] contexts;
+
+ private BasicDataSource dataSource;
+
+ public JDBCStorageServiceTest() {
+ final SecureRandom random1 = new SecureRandom();
+ contexts = new Object[10][1];
+ for (int i = 0; i < 10; i++) {
+ contexts[i] = new Object[] {Long.toString(random1.nextLong()), };
+ }
+ }
+
+ private void setupHSSQLDB() throws ClassNotFoundException, SQLException {
+ dataSource.setUrl("jdbc:hsqldb:mem:JPAStorageService;hsqldb.sqllog=3");
+ dataSource.setUsername("sa");
+ dataSource.setPassword("");
+ try (final Connection dbConn = dataSource.getConnection()) {
+ final Statement statement = dbConn.createStatement();
+ statement.executeUpdate(INIT_SQL_HSQLDB);
+ }
+ }
+
+ private void setupSQLServer() throws ClassNotFoundException, SQLException {
+ Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
+ dataSource.setUrl("jdbc:sqlserver://10.0.0.125:1433;encrypt=false");
+ dataSource.setUsername("sa");
+ dataSource.setPassword("");
+ try (final Connection dbConn = dataSource.getConnection()) {
+ final Statement statement = dbConn.createStatement();
+ try {
+ statement.executeUpdate(CLEANUP_SQL);
+ } catch (final SQLException e) {
+ System.out.println(e);
+ }
+ statement.executeUpdate(INIT_SQL_SQLSERVER);
+ }
+ }
+
+ /**
+ * Creates the shared instance of the entity manager factory.
+ */
+ @BeforeClass public void setUp() throws ComponentInitializationException {
+ try {
+ dataSource = new BasicDataSource();
+ if (USE_SQLSERVER) {
+ setupSQLServer();
+ } else {
+ setupHSSQLDB();
+ }
+
+ storageService = new JDBCStorageService();
+ storageService.setId("test");
+ storageService.setDataSource(dataSource);
+ storageService.setCleanupInterval(Duration.ofSeconds(5));
+ storageService.setTransactionRetry(12);
+ storageService.setRetryableErrors(List.of("40001"));
+ } catch (final SQLException | ClassNotFoundException e) {
+ throw new ComponentInitializationException(e);
+ }
+ super.setUp();
+ }
+
+ @AfterClass
+ protected void tearDown() {
+ try {
+ List<String> contexts1 = storageService.readContexts();
+ for (String ctx : contexts1) {
+ storageService.deleteContext(ctx);
+ }
+ List<?> recs = storageService.readAll();
+ Assert.assertEquals(recs.size(), 0);
+ } catch (IOException e){
+ throw new RuntimeException(e);
+ }
+ super.tearDown();
+ try {
+ Statement statement = dataSource.getConnection().createStatement();
+ statement.executeUpdate(CLEANUP_SQL);
+ dataSource.close();
+ } catch (SQLException e) {
+ throw new TestException(e);
+ }
+ }
+
+ @Nonnull protected StorageService getStorageService() {
+ return storageService;
+ }
+ /*
+ @Test(enabled = false)
+ public void strings() throws IOException {
+ }*/
+
+ @Test
+ public void cleanup() throws ComponentInitializationException, IOException {
+ String context = Long.toString(random.nextLong());
+ for (int i = 1; i <= 100; i++) {
+ storageService.create(context, Integer.toString(i), Integer.toString(i + 1), System.currentTimeMillis() + 100);
+ }
+ try {
+ Thread.sleep(7500);
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ }
+ List<?> recs = storageService.readAll(context);
+ Assert.assertEquals(recs.size(), 0);
+ }
+
+ @DataProvider(name = "contexts")
+ public Object[][] contexts() throws Exception {
+ return contexts;
+ }
+
+ @Test(dataProvider = "contexts", singleThreaded = false, threadPoolSize = 25, invocationCount = 100, enabled = true)
+ public void multithread(final String context) throws IOException {
+ shared.create(context, "mt", "bar", System.currentTimeMillis() + 300000);
+ StorageRecord<?> rec = shared.read(context, "mt");
+ Assert.assertNotNull(rec);
+ shared.update(context, "mt", "baz", System.currentTimeMillis() + 300000);
+ rec = shared.read(context, "mt");
+ Assert.assertNotNull(rec);
+ boolean result = shared.create(context, "mt", "qux", null);
+ Assert.assertFalse(result, "createString should have failed");
+ }
+
+ @Test(singleThreaded = false, threadPoolSize = 25, invocationCount = 100, enabled = true)
+ public void multithreadCaseSensitiveKey() throws IOException {
+ shared.create("unit_test", "foo", "bar", null);
+ shared.create("unit_test", "FOO", "bar", null);
+ StorageRecord<?> rec1 = shared.read("unit_test", "foo");
+ StorageRecord<?> rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+ }
+
+ @Test
+ public void keyCollision() throws IOException {
+ shared.create("unit_test", "dlo1", "value", null);
+ shared.create("unit_test", "dn11", "value", null);
+ StorageRecord<?> rec1 = shared.read("unit_test", "dlo1");
+ StorageRecord<?> rec2 = shared.read("unit_test", "dn11");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ shared.update("unit_test", "dlo1", "value2", null);
+ shared.update("unit_test", "dn11", "value2", null);
+ rec1 = shared.read("unit_test", "dlo1");
+ rec2 = shared.read("unit_test", "dn11");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ Assert.assertEquals(2, storageService.readAll().size());
+ Assert.assertEquals(2, storageService.readAll("unit_test").size());
+
+ shared.delete("unit_test", "dlo1");
+ rec1 = shared.read("unit_test", "dlo1");
+ rec2 = shared.read("unit_test", "dn11");
+ Assert.assertNull(rec1);
+ Assert.assertNotNull(rec2);
+ shared.delete("unit_test", "dn11");
+ rec1 = shared.read("unit_test", "dlo1");
+ rec2 = shared.read("unit_test", "dn11");
+ Assert.assertNull(rec1);
+ Assert.assertNull(rec2);
+ }
+
+ @Test
+ public void caseSensitiveContext() throws IOException {
+ assertTrue(shared.create("foo", "bar", "value", null));
+ assertTrue(shared.create("FOO", "bar", "value", null));
+ StorageRecord<?> rec1 = shared.read("foo", "bar");
+ StorageRecord<?> rec2 = shared.read("FOO", "bar");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+ Assert.assertEquals(2, storageService.readAll().size());
+
+ shared.update("foo", "bar", "value2", null);
+ shared.update("FOO", "bar", "value2", null);
+ rec1 = shared.read("foo", "bar");
+ rec2 = shared.read("FOO", "bar");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ Assert.assertEquals(storageService.readAll().size(), 2);
+ Assert.assertEquals(storageService.readAll("foo").size(), 1);
+ Assert.assertEquals(storageService.readAll("FOO").size(), 1);
+
+ shared.delete("foo", "bar");
+ rec1 = shared.read("foo", "bar");
+ rec2 = shared.read("FOO", "bar");
+ Assert.assertNull(rec1);
+ Assert.assertNotNull(rec2);
+ shared.delete("FOO", "bar");
+ rec1 = shared.read("foo", "bar");
+ rec2 = shared.read("FOO", "bar");
+ Assert.assertNull(rec1);
+ Assert.assertNull(rec2);
+ }
+
+ @Test
+ public void caseSensitiveKey() throws IOException {
+ shared.create("unit_test", "foo", "value", null);
+ shared.create("unit_test", "FOO", "value", null);
+ StorageRecord<?> rec1 = shared.read("unit_test", "foo");
+ StorageRecord<?> rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ shared.update("unit_test", "foo", "value2", null);
+ shared.update("unit_test", "FOO", "value2", null);
+ rec1 = shared.read("unit_test", "foo");
+ rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ Assert.assertEquals(2, storageService.readAll().size());
+ Assert.assertEquals(2, storageService.readAll("unit_test").size());
+
+ shared.delete("unit_test", "foo");
+ rec1 = shared.read("unit_test", "foo");
+ rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNull(rec1);
+ Assert.assertNotNull(rec2);
+ shared.delete("unit_test", "FOO");
+ rec1 = shared.read("unit_test", "foo");
+ rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNull(rec1);
+ Assert.assertNull(rec2);
+ }
+
+ @Test(enabled = false)
+ public void largeValue() throws IOException {
+ // hsqldb defaults LOB length to 255 chars; disabled for now
+ StringBuilder sb = new StringBuilder(1000 * 36);
+ for (int i = 0; i < 1000; i++) {
+ sb.append(UUID.randomUUID());
+ }
+ shared.create("unit_test", "large", sb.toString(), System.currentTimeMillis() + 300000);
+ StorageRecord<?> rec = shared.read("unit_test", "large");
+ Assert.assertNotNull(rec);
+ Assert.assertEquals(sb.toString(), rec.getValue());
+ }
+
+}
diff --git a/jdbc-storage-impl/src/test/java/net/shibboleth/plugin/storage/jdbc/impl/JDCBJPAMixedStorageServiceTest.java b/jdbc-storage-impl/src/test/java/net/shibboleth/plugin/storage/jdbc/impl/JDCBJPAMixedStorageServiceTest.java
new file mode 100644
index 0000000..f055baf
--- /dev/null
+++ b/jdbc-storage-impl/src/test/java/net/shibboleth/plugin/storage/jdbc/impl/JDCBJPAMixedStorageServiceTest.java
@@ -0,0 +1,316 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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 net.shibboleth.plugin.storage.jdbc.impl;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.util.List;
+import java.util.UUID;
+
+import javax.annotation.Nonnull;
+import javax.persistence.EntityManagerFactory;
+
+import net.shibboleth.ext.spring.util.ApplicationContextBuilder;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+
+import org.apache.commons.dbcp2.BasicDataSource;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.opensaml.storage.impl.JPAStorageService;
+import org.opensaml.storage.testing.StorageServiceTest;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Test of {@link JPAStorageService} implementation.
+ */
+ at SuppressWarnings("javadoc")
+public class JDCBJPAMixedStorageServiceTest extends StorageServiceTest {
+
+ /** Storage service. */
+ private JPAStorageService storageService;
+
+ private JDBCStorageService jdbmsService;
+
+ /** Contexts used for testing. */
+ private Object[][] contexts;
+
+ public JDCBJPAMixedStorageServiceTest() {
+ final SecureRandom random1 = new SecureRandom();
+ contexts = new Object[10][1];
+ for (int i = 0; i < 10; i++) {
+ contexts[i] = new Object[] {Long.toString(random1.nextLong()), };
+ }
+ }
+
+ /**
+ * Creates the shared instance of the entity manager factory.
+ */
+ @BeforeClass public void setUp() throws ComponentInitializationException {
+ storageService = new JPAStorageService(createEntityManagerFactory());
+ storageService.setId("test");
+ storageService.setCleanupInterval(Duration.ofSeconds(5));
+ storageService.setTransactionRetry(2);
+ super.setUp();
+ }
+
+ /**
+ * Creates an entity manager factory instance.
+ *
+ * @return an entity manager factory instance
+ *
+ * @throws ComponentInitializationException ...
+ */
+ private EntityManagerFactory createEntityManagerFactory() throws ComponentInitializationException
+ {
+ final Resource resource = new ClassPathResource("/org/opensaml/storage/impl/jpa-spring-context.xml");
+ final GenericApplicationContext context =
+ new ApplicationContextBuilder()
+ .setName("JPAStorageService")
+ .setServiceConfiguration(resource)
+ .build();
+ final FactoryBean<EntityManagerFactory> factoryBean =
+ context.getBean(org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.class);
+ final BasicDataSource source = context.getBean("hibernateDataSource", BasicDataSource.class);
+ jdbmsService = new JDBCStorageService();
+ jdbmsService.setDataSource(source);
+ jdbmsService.setId("TEstJDBCS");
+ jdbmsService.initialize();
+
+ try {
+ return factoryBean.getObject();
+ } catch (Exception e) {
+ throw new ComponentInitializationException(e);
+ }
+ }
+
+ @AfterClass
+ protected void tearDown() {
+ try {
+ List<String> contexts1 = storageService.readContexts();
+ for (String ctx : contexts1) {
+ storageService.deleteContext(ctx);
+ }
+ List<?> recs = storageService.readAll();
+ Assert.assertEquals(recs.size(), 0);
+ } catch (IOException e){
+ throw new RuntimeException(e);
+ }
+ super.tearDown();
+ }
+
+ @Nonnull protected StorageService getStorageService() {
+ return storageService;
+ }
+
+ @Test
+ public void cleanup() throws ComponentInitializationException, IOException {
+ String context = Long.toString(random.nextLong());
+ for (int i = 1; i <= 100; i++) {
+ storageService.create(context, Integer.toString(i), Integer.toString(i + 1), System.currentTimeMillis() + 100);
+ }
+ try {
+ Thread.sleep(7500);
+ } catch (InterruptedException e) {
+ throw new IOException(e);
+ }
+ List<?> recs = storageService.readAll(context);
+ Assert.assertEquals(recs.size(), 0);
+ }
+
+ @DataProvider(name = "contexts")
+ public Object[][] contexts() throws Exception {
+ return contexts;
+ }
+
+ @Test(dataProvider = "contexts", singleThreaded = false, threadPoolSize = 25, invocationCount = 100)
+ public void multithread(final String context) throws IOException {
+ shared.create(context, "mt", "bar", System.currentTimeMillis() + 300000);
+ StorageRecord<?> rec = shared.read(context, "mt");
+ Assert.assertNotNull(rec);
+ shared.update(context, "mt", "baz", System.currentTimeMillis() + 300000);
+ rec = shared.read(context, "mt");
+ Assert.assertNotNull(rec);
+ boolean result = shared.create(context, "mt", "qux", null);
+ Assert.assertFalse(result, "createString should have failed");
+ }
+
+ @Test(singleThreaded = false, threadPoolSize = 25, invocationCount = 100)
+ public void multithreadCaseSensitiveKey() throws IOException {
+ shared.create("unit_test", "foo", "bar", null);
+ shared.create("unit_test", "FOO", "bar", null);
+ StorageRecord<?> rec1 = shared.read("unit_test", "foo");
+ StorageRecord<?> rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+ }
+
+ @Test
+ public void keyCollision() throws IOException {
+ shared.create("unit_test", "dlo1", "value", null);
+ shared.create("unit_test", "dn11", "value", null);
+ StorageRecord<?> rec1 = shared.read("unit_test", "dlo1");
+ StorageRecord<?> rec2 = shared.read("unit_test", "dn11");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ shared.update("unit_test", "dlo1", "value2", null);
+ shared.update("unit_test", "dn11", "value2", null);
+ rec1 = shared.read("unit_test", "dlo1");
+ rec2 = shared.read("unit_test", "dn11");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ Assert.assertEquals(2, storageService.readAll().size());
+ Assert.assertEquals(2, storageService.readAll("unit_test").size());
+
+ shared.delete("unit_test", "dlo1");
+ rec1 = shared.read("unit_test", "dlo1");
+ rec2 = shared.read("unit_test", "dn11");
+ Assert.assertNull(rec1);
+ Assert.assertNotNull(rec2);
+ shared.delete("unit_test", "dn11");
+ rec1 = shared.read("unit_test", "dlo1");
+ rec2 = shared.read("unit_test", "dn11");
+ Assert.assertNull(rec1);
+ Assert.assertNull(rec2);
+ }
+
+ @Test
+ public void caseSensitiveContext() throws IOException {
+ shared.create("foo", "bar", "value", null);
+ shared.create("FOO", "bar", "value", null);
+ StorageRecord<?> rec1 = shared.read("foo", "bar");
+ StorageRecord<?> rec2 = shared.read("FOO", "bar");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ shared.update("foo", "bar", "value2", null);
+ shared.update("FOO", "bar", "value2", null);
+ rec1 = shared.read("foo", "bar");
+ rec2 = shared.read("FOO", "bar");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ Assert.assertEquals(2, storageService.readAll().size());
+ Assert.assertEquals(1, storageService.readAll("foo").size());
+ Assert.assertEquals(1, storageService.readAll("FOO").size());
+
+ shared.delete("foo", "bar");
+ rec1 = shared.read("foo", "bar");
+ rec2 = shared.read("FOO", "bar");
+ Assert.assertNull(rec1);
+ Assert.assertNotNull(rec2);
+ shared.delete("FOO", "bar");
+ rec1 = shared.read("foo", "bar");
+ rec2 = shared.read("FOO", "bar");
+ Assert.assertNull(rec1);
+ Assert.assertNull(rec2);
+ }
+
+ @Test
+ public void caseSensitiveKey() throws IOException {
+ shared.create("unit_test", "foo", "value", null);
+ shared.create("unit_test", "FOO", "value", null);
+ StorageRecord<?> rec1 = shared.read("unit_test", "foo");
+ StorageRecord<?> rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ shared.update("unit_test", "foo", "value2", null);
+ shared.update("unit_test", "FOO", "value2", null);
+ rec1 = shared.read("unit_test", "foo");
+ rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNotNull(rec1);
+ Assert.assertNotNull(rec2);
+ Assert.assertNotEquals(rec1, rec2);
+
+ Assert.assertEquals(2, storageService.readAll().size());
+ Assert.assertEquals(2, storageService.readAll("unit_test").size());
+
+ shared.delete("unit_test", "foo");
+ rec1 = shared.read("unit_test", "foo");
+ rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNull(rec1);
+ Assert.assertNotNull(rec2);
+ shared.delete("unit_test", "FOO");
+ rec1 = shared.read("unit_test", "foo");
+ rec2 = shared.read("unit_test", "FOO");
+ Assert.assertNull(rec1);
+ Assert.assertNull(rec2);
+ }
+
+ @Test(enabled = false)
+ public void largeValue() throws IOException {
+ // hsqldb defaults LOB length to 255 chars; disabled for now
+ StringBuilder sb = new StringBuilder(1000 * 36);
+ for (int i = 0; i < 1000; i++) {
+ sb.append(UUID.randomUUID());
+ }
+ shared.create("unit_test", "large", sb.toString(), System.currentTimeMillis() + 300000);
+ StorageRecord<?> rec = shared.read("unit_test", "large");
+ Assert.assertNotNull(rec);
+ Assert.assertEquals(sb.toString(), rec.getValue());
+ }
+
+ @Test
+ public void jpaWriteRDBMSRead() throws IOException {
+ StringBuilder sb = new StringBuilder(255);
+ for (int i = 0; i < 255/36; i++) {
+ sb.append(UUID.randomUUID());
+ }
+ shared.create("mixed1", "large", sb.toString(), System.currentTimeMillis() + 300000);
+ final StorageRecord<?> jpa = shared.read("mixed1", "large");
+ Assert.assertNotNull(jpa);
+ Assert.assertEquals(sb.toString(), jpa.getValue());
+
+ final StorageRecord<?> rdbms = jdbmsService.read("mixed1", "large");
+ Assert.assertNotNull(rdbms );
+ Assert.assertEquals(sb.toString(), rdbms .getValue());
+ }
+ @Test
+ public void jpaReadDBMSWrite() throws IOException {
+ StringBuilder sb = new StringBuilder(255);
+ for (int i = 0; i < 255/36; i++) {
+ sb.append(UUID.randomUUID());
+ }
+ Assert.assertTrue(jdbmsService.create("mixed1", "rrrwd", sb.toString(), System.currentTimeMillis() + 300000));
+ final StorageRecord<?> rdbms = jdbmsService.read("mixed1", "rrrwd");
+ Assert.assertNotNull(rdbms );
+ Assert.assertEquals(sb.toString(), rdbms .getValue());
+ final StorageRecord<?> jpa = shared.read("mixed1", "rrrwd");
+ Assert.assertNotNull(jpa);
+ Assert.assertEquals(sb.toString(), jpa.getValue());
+
+ Assert.assertFalse(jdbmsService.create("mixed1", "rrrwd", sb.toString(), System.currentTimeMillis() + 300000));
+ }
+}
diff --git a/jdbc-storage-impl/src/test/resources/logback-test.xml b/jdbc-storage-impl/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..c21c4ab
--- /dev/null
+++ b/jdbc-storage-impl/src/test/resources/logback-test.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+
+ <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>%level [%logger:%line] - %msg%n%ex{short}</pattern>
+ <charset>UTF-8</charset>
+ </encoder>
+ </appender>
+
+ <root>
+ <level value="warn" />
+ <appender-ref ref="STDOUT" />
+ </root>
+
+ <logger name="net.shibboleth.plugin.storage.jdbc.impl" level="TRACE"/>
+ <logger name="org.opensaml.storage" level="DEBUG"/>
+ <logger name="net.shibboleth.utilities.java.support.security.DataSealer" level="TRACE"/>
+
+</configuration>
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 9a7a2ed..82287be 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,13 +67,13 @@
<!-- jdbc-storage project dependencies -->
<dependencies>
<dependency>
- <groupId>net.shibboleth.idp.plugin.storage.jdbc</groupId>
- <artifactId>idp-plugin-jdbc-storage-api</artifactId>
+ <groupId>net.shibboleth.plugin.storage.jdbc</groupId>
+ <artifactId>jdbc-storage-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>net.shibboleth.idp.plugin.storage.jdbc</groupId>
- <artifactId>idp-plugin-jdbc-storage-impl</artifactId>
+ <groupId>net.shibboleth.plugin.storage.jdbc</groupId>
+ <artifactId>jdbc-storage-impl</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Shibboleth IdP BOM for importing IdP dependencies -->
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list