[java-opensaml] 01/05: osj-342

Rod Widdowson rdw at steadingsoftware.com
Thu May 12 10:12:57 UTC 2022


This is an automated email from the git hooks/post-receive script.

rdw pushed a commit to branch dev/OSJ-342
in repository java-opensaml.

View the commit online:
http://git.shibboleth.net/view/?p=java-opensaml.git;a=commit;h=c533bd11f376b8aa7e929e64ea19421df26b7125

commit c533bd11f376b8aa7e929e64ea19421df26b7125
Author: Rod Widdowson <rdw at steadingsoftware.com>
AuthorDate: Mon May 9 13:41:03 2022 +0100

    osj-342
---
 .../opensaml/storage/impl/JDBCStorageService.java  | 707 +++++++++++++++++++++
 .../storage/impl/JDBCStorageServiceTest.java       | 282 ++++++++
 .../impl/JDCBJPAMixedStorageServiceTest.java       | 315 +++++++++
 .../org/opensaml/storage/impl/RdbmsStore.sql       |   8 +
 4 files changed, 1312 insertions(+)

diff --git a/opensaml-storage-impl/src/main/java/org/opensaml/storage/impl/JDBCStorageService.java b/opensaml-storage-impl/src/main/java/org/opensaml/storage/impl/JDBCStorageService.java
new file mode 100644
index 000000000..2525243cb
--- /dev/null
+++ b/opensaml-storage-impl/src/main/java/org/opensaml/storage/impl/JDBCStorageService.java
@@ -0,0 +1,707 @@
+/*
+ * 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 org.opensaml.storage.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;
+
+
+/**
+ *
+ */
+public final class JDBCStorageService extends AbstractStorageService implements StorageCapabilitiesEx {
+    
+    /** 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;
+    
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        Constraint.isNotNull(dataSource, "data source must be specified and nonnul");
+        super.doInitialize();
+    }
+    
+    /** 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 = source;
+    }
+
+    /** 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");
+    }
+    
+    /**
+     * 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<>();
+        log.trace("Getting Context");
+        try (final Connection connection = getConnection(true)) {
+            final PreparedStatement query = connection.prepareStatement("SELECT context FROM StorageRecords");
+            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("ReadAll()", 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<MyStorageRecord<?>> result = new ArrayList<>();
+        log.trace("Getting all Records");
+        try (final Connection connection = getConnection(true)) {
+            final PreparedStatement query = connection.prepareStatement("SELECT context, id, expires, value, version FROM StorageRecords");
+            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 MyStorageRecord<>(value, expires, version));
+            }
+            return result;
+            
+        } catch (final SQLException e) {
+            log.error("ReadAll()", 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<MyStorageRecord<?>> result = new ArrayList<>();
+        log.trace("Getting all Records for context {}", context);
+        try (final Connection connection = getConnection(true)) {
+            final PreparedStatement query = connection.prepareStatement("SELECT id, expires, value, version FROM StorageRecords WHERE context = ?");
+            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 MyStorageRecord<>(value, expires, version));
+            }
+            return result;
+            
+        } catch (final SQLException e) {
+            log.error("ReadAll()", e);
+            throw new IOException(e);
+        }
+    }
+
+
+    /** {@inheritDoc} */
+    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 {
+        //
+        // Constraints, Logging
+        //
+        int retries = transactionRetry;
+        while(true) {
+            try (final Connection connection = getConnection(false)) {
+                // Does it exist?
+                // If not insert
+                // If so check expiration
+                // If not expired complain
+                // otherwise update
+                final PreparedStatement query = connection.prepareStatement("SELECT expires FROM StorageRecords WHERE context =? AND id=?");
+                query.setString(1, context);
+                query.setString(2, key);
+                log.debug("Querying {}", query);
+                final ResultSet resultSet = query.executeQuery();
+                if (!resultSet.next()) {
+                    final PreparedStatement insert = connection.prepareStatement("INSERT INTO StorageRecords VALUES (?, ?, ?, ?, 1)");
+                    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("UPDATE StorageRecords SET value=?, version=0, expires=? WHERE context=? AND id=?");
+                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);
+                }
+            }
+        }
+    }
+
+    /** {@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
+     */
+    @Nonnull protected <T> Pair<Long, StorageRecord<T>> readImpl(@Nonnull @NotEmpty final String context,
+            @Nonnull @NotEmpty final String key, @Positive final Long version) throws IOException {
+        //
+        // Constraints, Logging
+        //
+        int retries = transactionRetry;
+        while(true) {
+            try (final Connection connection = getConnection(true)) {
+                final PreparedStatement stmnt = connection.prepareStatement("SELECT version, expires, value FROM StorageRecords WHERE context =? AND id=?");
+                stmnt.setString(1, context);
+                stmnt.setString(2, key);
+                log.debug("Querying {}", stmnt);
+                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.debug("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 MyStorageRecord<>(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);
+                }
+            }
+        }
+    }
+
+    /** {@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
+     */
+    @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 {
+        
+        //
+        // Constraints, Logging
+        //        
+        int retries = transactionRetry;
+        while (true) {
+            try (Connection connection = getConnection(false)) {
+                final PreparedStatement selectStmnt = connection.prepareStatement("SELECT version, expires FROM StorageRecords WHERE context =? AND id=?");
+                selectStmnt.setString(1, context);
+                selectStmnt.setString(2, key);
+                log.debug("Querying {}", selectStmnt);
+                final String s = selectStmnt.toString();
+                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("UPDATE StorageRecords SET value=?, version=?, expires=? WHERE context=? AND id=?");
+                updateStmnt.setString(1, value);
+                final Long newVersion = Long.valueOf(returnedVersion + 1);
+                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);
+                }
+            }
+        }
+    }
+
+    /** {@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
+     */
+    protected boolean deleteImpl(@Nullable @Positive final Long version, @Nonnull @NotEmpty final String context,
+            @Nonnull @NotEmpty final String key) throws IOException, VersionMismatchException {
+        //
+        // Constraints, Logging
+        //        
+        int retries = transactionRetry;
+        while (true) {
+            try (Connection connection = getConnection(false)) {
+                final PreparedStatement selectStmnt = connection.prepareStatement("SELECT version FROM StorageRecords WHERE context =? AND id=?");
+                selectStmnt.setString(1, context);
+                selectStmnt.setString(2, key);
+                log.debug("Querying {}", selectStmnt);
+                final String s = selectStmnt.toString();
+                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("DELETE FROM StorageRecords WHERE context=? AND id=?");
+                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);
+                }
+            }
+        }
+    }
+    
+    /**
+     * 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 {
+        //
+        // Constraints, Logging
+        //        
+        int retries = transactionRetry;
+        while (true) {
+            try (Connection connection = getConnection(false)) {
+                final PreparedStatement updateStmnt = connection.prepareStatement("DELETE FROM StorageRecords WHERE expires < ? ");
+                updateStmnt.setLong(1, 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(String context) throws IOException {
+        throw new IOException("Not implemented");
+    }
+
+    /** {@inheritDoc} */
+    public void updateContextExpiration(String context, Long expires) throws IOException {
+        throw new IOException("Not implemented");
+    }
+
+    /** {@inheritDoc} */
+    public void deleteContext(String context) throws IOException {
+        //
+        // Constraints, Logging
+        //        
+        int retries = transactionRetry;
+        while (true) {
+            try (Connection connection = getConnection(true)) {
+                final PreparedStatement updateStmnt = connection.prepareStatement("DELETE FROM StorageRecords WHERE context = ? ");
+                updateStmnt.setString(1, 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);
+            }
+        };
+    }
+
+
+    private static class MyStorageRecord<T> extends MutableStorageRecord<T> {
+
+        /**
+         * Constructor.
+         *
+         * @param val
+         * @param exp
+         */
+        public MyStorageRecord(String val, Long exp, Long version) {
+            super(val, exp);
+            if (version != null) {
+                setVersion(version);
+            }
+        }
+        
+    }
+
+    
+}
diff --git a/opensaml-storage-impl/src/test/java/org/opensaml/storage/impl/JDBCStorageServiceTest.java b/opensaml-storage-impl/src/test/java/org/opensaml/storage/impl/JDBCStorageServiceTest.java
new file mode 100644
index 000000000..be6adcb6c
--- /dev/null
+++ b/opensaml-storage-impl/src/test/java/org/opensaml/storage/impl/JDBCStorageServiceTest.java
@@ -0,0 +1,282 @@
+/*
+ * 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 org.opensaml.storage.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+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.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 com.google.common.io.CharStreams;
+
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * Test of {@link JPAStorageService} implementation.
+ */
+ at SuppressWarnings("javadoc")
+public class JDBCStorageServiceTest extends StorageServiceTest {
+
+    private JDBCStorageService storageService;
+
+    private static final String INIT_FILE = "/org/opensaml/storage/impl/RdbmsStore.sql";
+
+
+    /** 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()), };
+        }
+    }
+
+    /**
+     * Creates the shared instance of the entity manager factory.
+     */
+    @BeforeClass public void setUp() throws ComponentInitializationException {
+        try {
+            dataSource = new BasicDataSource();
+            dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
+            dataSource.setUrl("jdbc:hsqldb:mem:JPAStorageService");
+            dataSource.setUsername("SA");
+            dataSource.setPassword("");
+            Connection dbConn;
+            dbConn = dataSource.getConnection();
+            Statement statement = dbConn.createStatement();
+            final InputStream is = getClass().getResourceAsStream(INIT_FILE);
+            final String sql = StringSupport.trimOrNull(CharStreams.toString(new InputStreamReader(is)));
+            statement.executeUpdate(sql);
+            
+            storageService = new JDBCStorageService();
+            storageService.setId("test");
+            storageService.setDataSource(dataSource);
+            storageService.setCleanupInterval(Duration.ofSeconds(5));
+            storageService.setTransactionRetry(6);
+            storageService.setRetryableErrors(List.of("40001"));
+        } catch (final SQLException | IOException 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 {
+            dataSource.close();
+        } catch (SQLException e) {
+            throw new TestException(e);
+        }
+    }
+
+    @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, enabled = false)
+    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 = false)
+    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());
+    }
+    
+}
diff --git a/opensaml-storage-impl/src/test/java/org/opensaml/storage/impl/JDCBJPAMixedStorageServiceTest.java b/opensaml-storage-impl/src/test/java/org/opensaml/storage/impl/JDCBJPAMixedStorageServiceTest.java
new file mode 100644
index 000000000..934fe73e1
--- /dev/null
+++ b/opensaml-storage-impl/src/test/java/org/opensaml/storage/impl/JDCBJPAMixedStorageServiceTest.java
@@ -0,0 +1,315 @@
+/*
+ * 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 org.opensaml.storage.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.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/opensaml-storage-impl/src/test/resources/org/opensaml/storage/impl/RdbmsStore.sql b/opensaml-storage-impl/src/test/resources/org/opensaml/storage/impl/RdbmsStore.sql
new file mode 100644
index 000000000..7ac38fce5
--- /dev/null
+++ b/opensaml-storage-impl/src/test/resources/org/opensaml/storage/impl/RdbmsStore.sql
@@ -0,0 +1,8 @@
+CREATE TABLE StorageRecords (
+  context varchar(255) NOT NULL,
+  id varchar(255) NOT NULL,
+  expires bigint DEFAULT NULL,
+  value varchar(255) NOT NULL,
+  version bigint NOT NULL,
+  PRIMARY KEY (context,id)
+)

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list