All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opensaml.storage.AbstractMapBackedStorageService Maven / Gradle / Ivy

There is a newer version: 4.0.1
Show newest version
/*
 * 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;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import net.shibboleth.utilities.java.support.annotation.constraint.Live;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;


/**
 * Partial implementation of {@link StorageService} that stores data in-memory with no persistence
 * using a simple map.
 * 
 * 

Abstract methods supply the map of data to manipulate and the lock to use, which allows * optimizations in cases where locking isn't required or data isn't shared.

*/ public abstract class AbstractMapBackedStorageService extends AbstractStorageService { /** Class logger. */ @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractMapBackedStorageService.class); /** Constructor. */ public AbstractMapBackedStorageService() { setContextSize(Integer.MAX_VALUE); setKeySize(Integer.MAX_VALUE); setValueSize(Integer.MAX_VALUE); } /** {@inheritDoc} */ @Override public boolean create(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key, @Nonnull @NotEmpty final String value, @Nullable final Long expiration) throws IOException { final Lock writeLock = getLock().writeLock(); try { writeLock.lock(); final Map> contextMap; try { contextMap = getContextMap(); } catch (final Exception e) { throw new IOException(e); } // Create new context if necessary. Map dataMap = contextMap.get(context); if (dataMap == null) { dataMap = new HashMap(); contextMap.put(context, dataMap); } // Check for a duplicate. final StorageRecord record = dataMap.get(key); if (record != null) { // Not yet expired? final Long exp = record.getExpiration(); if (exp == null || System.currentTimeMillis() < exp) { return false; } // It's dead, so we can just remove it now and create the new record. } dataMap.put(key, new MutableStorageRecord(value, expiration)); log.trace("Inserted record '{}' in context '{}' with expiration '{}'", new Object[] { key, context, expiration }); setDirty(); return true; } finally { writeLock.unlock(); } } /** {@inheritDoc} */ @Override @Nullable public StorageRecord read(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key) throws IOException { return readImpl(context, key, null).getSecond(); } /** {@inheritDoc} */ @Override @Nonnull public Pair read(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key, final long version) throws IOException { return readImpl(context, key, version); } /** {@inheritDoc} */ @Override public boolean update(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key, @Nonnull @NotEmpty final String value, @Nullable final Long expiration) throws IOException { try { return updateImpl(null, context, key, value, expiration) != null; } catch (final VersionMismatchException e) { throw new IOException("Unexpected exception thrown by update.", e); } } /** {@inheritDoc} */ @Override @Nullable public Long updateWithVersion(final long version, @Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key, @Nonnull @NotEmpty final String value, @Nullable 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 final Long expiration) throws IOException { try { return updateImpl(null, context, key, null, expiration) != null; } catch (final VersionMismatchException e) { throw new IOException("Unexpected exception thrown by update.", e); } } /** {@inheritDoc} */ @Override public boolean deleteWithVersion(final long version, final String context, 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 IOException("Unexpected exception thrown by delete.", e); } } /** {@inheritDoc} */ @Override public void updateContextExpiration(@Nonnull @NotEmpty final String context, @Nullable final Long expiration) throws IOException { final Lock writeLock = getLock().writeLock(); try { writeLock.lock(); final Map> contextMap; try { contextMap = getContextMap(); } catch (final Exception e) { throw new IOException(e); } final Map dataMap = contextMap.get(context); if (dataMap != null) { setDirty(); final Long now = System.currentTimeMillis(); for (final MutableStorageRecord record : dataMap.values()) { final Long exp = record.getExpiration(); if (exp == null || now < exp) { record.setExpiration(expiration); } } log.debug("Updated expiration of valid records in context '{}' to '{}'", context, expiration); } } finally { writeLock.unlock(); } } /** {@inheritDoc} */ @Override public void deleteContext(@Nonnull @NotEmpty final String context) throws IOException { final Lock writeLock = getLock().writeLock(); try { writeLock.lock(); setDirty(); try { getContextMap().remove(context); } catch (final Exception e) { throw new IOException(e); } } finally { writeLock.unlock(); } log.debug("Deleted context '{}'", context); } /** {@inheritDoc} */ @Override public void reap(@Nonnull @NotEmpty final String context) throws IOException { final Lock writeLock = getLock().writeLock(); try { writeLock.lock(); final Map> contextMap; try { contextMap = getContextMap(); } catch (final Exception e) { throw new IOException(e); } final Map dataMap = contextMap.get(context); if (dataMap != null) { if (reapWithLock(dataMap, System.currentTimeMillis())) { setDirty(); if (dataMap.isEmpty()) { contextMap.remove(context); } } } } finally { writeLock.unlock(); } } /** * Get the shared lock to synchronize access. * * @return shared lock */ @Nonnull protected abstract ReadWriteLock getLock(); /** * Get the map of contexts to manipulate during operations. * *

This method is guaranteed to be called under cover the lock returned by {{@link #getLock()}.

* * TODO: this method needs to be able to throw IOException to deal with unexpected scenarios without * raising unchecked exceptions. * * @return map of contexts to manipulate */ @Nonnull @NonnullElements @Live protected abstract Map> getContextMap(); /** * A callback to indicate that data has been modified. * *

This method is guaranteed to be called under cover the lock returned by {{@link #getLock()}.

*/ protected void setDirty() { } /** * Internal method to implement read functions. * * @param context a storage context label * @param key a key unique to context * @param version only return record if newer than optionally supplied version * * @return a pair consisting of the version of the record read back, if any, and the record itself * @throws IOException if errors occur in the read process */ @Nonnull protected Pair readImpl(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key, @Nullable final Long version) throws IOException { final Lock readLock = getLock().readLock(); try { readLock.lock(); final Map> contextMap; try { contextMap = getContextMap(); } catch (final Exception e) { throw new IOException(e); } final Map dataMap = contextMap.get(context); if (dataMap == null) { log.debug("Read failed, context '{}' not found", context); return new Pair(); } final StorageRecord record = dataMap.get(key); if (record == null) { log.debug("Read failed, key '{}' not found in context '{}'", key, context); return new Pair(); } else { final Long exp = record.getExpiration(); if (exp != null && System.currentTimeMillis() >= exp) { log.debug("Read failed, key '{}' expired in context '{}'", key, context); return new Pair(); } } if (version != null && record.getVersion() == version) { // Nothing's changed, so just echo back the version. return new Pair(version, null); } return new Pair(record.getVersion(), record); } finally { readLock.unlock(); } } /** * Internal method to implement update functions. * * @param version only update if the current version matches this value * @param context a storage context label * @param key a key unique to context * @param value updated value * @param expiration expiration for record. or null * * @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 has already been updated to a newer version */ @Nullable protected Long updateImpl(@Nullable final Long version, @Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key, @Nullable final String value, @Nullable final Long expiration) throws IOException, VersionMismatchException { final Lock writeLock = getLock().writeLock(); try { writeLock.lock(); final Map> contextMap; try { contextMap = getContextMap(); } catch (final Exception e) { throw new IOException(e); } final Map dataMap = contextMap.get(context); if (dataMap == null) { log.debug("Update failed, context '{}' not found", context); return null; } final MutableStorageRecord record = dataMap.get(key); if (record == null) { log.debug("Update failed, key '{}' not found in context '{}'", key, context); return null; } else { final Long exp = record.getExpiration(); if (exp != null && System.currentTimeMillis() >= exp) { log.debug("Update failed, key '{}' expired in context '{}'", key, context); return null; } } if (version != null && version != record.getVersion()) { // Caller is out of sync. throw new VersionMismatchException(); } setDirty(); if (value != null) { record.setValue(value); record.incrementVersion(); } record.setExpiration(expiration); log.trace("Updated record '{}' in context '{}' with expiration '{}'", new Object[] { key, context, expiration }); return record.getVersion(); } finally { writeLock.unlock(); } } /** * Internal method to implement delete functions. * * @param version only update if the current version matches this value * @param context a storage context label * @param key a key unique to context * * @return true iff the record existed and was deleted * @throws IOException if errors occur in the update process * @throws VersionMismatchException if the record has already been updated to a newer version */ protected boolean deleteImpl(@Nullable @Positive final Long version, @Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key) throws IOException, VersionMismatchException { final Lock writeLock = getLock().writeLock(); try { writeLock.lock(); final Map> contextMap; try { contextMap = getContextMap(); } catch (final Exception e) { throw new IOException(e); } final Map dataMap = contextMap.get(context); if (dataMap == null) { log.debug("Deleting record '{}' in context '{}'....context not found", key, context); return false; } final MutableStorageRecord record = dataMap.get(key); if (record == null) { log.debug("Deleting record '{}' in context '{}'....key not found", key, context); return false; } else if (version != null && record.getVersion() != version) { throw new VersionMismatchException(); } else { setDirty(); dataMap.remove(key); log.trace("Deleted record '{}' in context '{}'", key, context); if (dataMap.isEmpty()) { contextMap.remove(context); } return true; } } finally { writeLock.unlock(); } } /** * Locates and removes expired records from the input map. * *

This method MUST be called while holding a write lock, if locking is required.

* * @param dataMap the map to reap * @param expiration time at which to consider records expired * * @return true iff anything was purged */ protected boolean reapWithLock(@Nonnull @NonnullElements final Map dataMap, final long expiration) { return Iterables.removeIf(dataMap.entrySet(), new Predicate>() { public boolean apply(@Nullable final Entry entry) { final Long exp = entry.getValue().getExpiration(); return exp != null && exp <= expiration; } } ); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy