[java-oidc-common] 05/05: Split metadata cache into a dynamic and batch version.

Phil Smart philip.smart at jisc.ac.uk
Thu Oct 14 16:48:24 UTC 2021


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

philsmart pushed a commit to branch main
in repository java-oidc-common.

View the commit online:
http://git.shibboleth.net/view/?p=java-oidc-common.git;a=commit;h=383b6d54047c106f955b7c42617c0016c52ce7ee

commit 383b6d54047c106f955b7c42617c0016c52ce7ee
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Thu Oct 14 17:37:11 2021 +0100

    Split metadata cache into a dynamic and batch version.
    
     - Also split backing store to dynamic and batch (similar to original)
     - Fix checkstyle and some javadoc
---
 .../AbstractEvaluableMetadataCriterion.java        |   3 +-
 .../net/shibboleth/oidc/metadata/BackingStore.java |  34 -
 .../oidc/metadata/BatchBackingStore.java           |  68 ++
 ...{BackingStore.java => DynamicBackingStore.java} |  46 +-
 .../oidc/metadata/MetadataManagementData.java      |   5 +-
 .../oidc/metadata/cache/CacheLoadingContext.java   |  69 ++
 .../oidc/metadata/cache/MetadataCache.java         |  35 +-
 .../metadata/cache/impl/AbstractMetadataCache.java | 555 +++++++++++++++
 .../metadata/cache/impl/BatchMetadataCache.java    | 306 +++++++++
 .../metadata/cache/impl/DefaultMetadataCache.java  | 743 ---------------------
 ...OIDCProviderMetadataExpirationTimeStrategy.java |  12 +-
 .../metadata/cache/impl/DynamicMetadataCache.java  | 434 ++++++++++++
 .../metadata/cache/impl/MetadataCacheBuilder.java  | 168 ++++-
 .../impl/OIDCProviderMetadataCacheFactoryBean.java |  67 +-
 .../oidc/metadata/impl/AbstractBackingStore.java   |  67 ++
 .../impl/AbstractDynamicHTTPFetchingStrategy.java  |  19 +-
 .../impl/AbstractDynamicOIDCMetadataResolver.java  |  26 +-
 .../impl/AbstractOIDCMetadataResolver.java         |  42 +-
 .../metadata/impl/DefaultBatchBackingStore.java    |  64 ++
 ...gStore.java => DefaultDynamicBackingStore.java} |  37 +-
 .../impl/DynamicOIDCProviderMetadataResolver.java  |   9 +-
 .../HTTPProviderConfigurationFetchingStrategy.java |  24 +-
 .../metadata/cache/impl/BatchMetadatCacheTest.java | 190 ++++++
 ...acheTest.java => DynamicMetadataCacheTest.java} | 205 +++---
 .../DynamicOIDCProviderMetadataResolverTest.java   |  76 ++-
 25 files changed, 2234 insertions(+), 1070 deletions(-)

diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/AbstractEvaluableMetadataCriterion.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/AbstractEvaluableMetadataCriterion.java
index 15c06d2..f7c071f 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/AbstractEvaluableMetadataCriterion.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/AbstractEvaluableMetadataCriterion.java
@@ -15,7 +15,7 @@ public abstract class AbstractEvaluableMetadataCriterion<T> implements Evaluable
     /** Object type. */
     @Nonnull private final Class<T> objectType;
     
-    /** What should the default return type be if the wrong type <T> is supplied.*/
+    /** What should the default return type be if the wrong type is supplied.*/
     @Nonnull private final boolean defaultResultOnWrongType;
     
     /**
@@ -23,6 +23,7 @@ public abstract class AbstractEvaluableMetadataCriterion<T> implements Evaluable
      * Constructor.
      *
      * @param claz the class this criterion accepts.
+     * @param defaultResult what should be returned if the criterion is not appropriate for the given type.
      */
     protected AbstractEvaluableMetadataCriterion(@Nonnull final Class<T> claz,
             @Nonnull  final boolean defaultResult) {
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java
index bd21547..1b6253d 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java
@@ -15,19 +15,13 @@
  * limitations under the License.
  */
 
-
 package net.shibboleth.oidc.metadata;
 
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import javax.annotation.Nonnull;
 
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
-import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
-import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
-
 /**
  * A backing store that holds cached objects.
  * 
@@ -50,35 +44,7 @@ public interface BackingStore<I, T> {
      */
     @Nonnull List<T> getOrderedValues();
     
-    /**
-     * Get the management data for the specified identifier. If the management data does not exist
-     * it should be created.
-     * 
-     * <p>Management data facilitates per-entity metadata locking and cache primitives e.g. next refresh time. </p>
-     * 
-     * <p>Should do so in a thread-safe way e.g. if two threads enter this method, only one should be allowed
-     * to create management data for the same identifier.</p>
-     * 
-     * @param identifier the identifier of the entity to find management data aboout
-     * 
-     * @return the corresponding management data
-     */
-    @Nonnull public MetadataManagementData<I> computeManagementDataIfAbsent(@Nonnull final I identifier);
-    
-    /**
-     * Remove the management data for the specified entityID.
-     * 
-     * @param identifier the input identifier
-     */
-    void removeManagementData(@Nonnull final I identifier);
     
-    /**
-     * Get the set of entityIDs which currently have management data.
-     *
-     * @return set of entityIDs, may be empty
-     */
-    @Nonnull @NonnullElements @Unmodifiable @NotLive
-    Set<I> getManagementDataIdentifiers();
     
     
 
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BatchBackingStore.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BatchBackingStore.java
new file mode 100644
index 0000000..ca4984e
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BatchBackingStore.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.oidc.metadata;
+
+import java.time.Instant;
+
+import javax.annotation.Nullable;
+
+/**
+ * A specialisation of a {@link BackingStore} that deals with batch metadata. Operations on batch metadata
+ * happen together e.g. a reload will evict all previous entries and all new entries are reloaded in one go. 
+ * Individual entries are not updated dynamically as an when needed. 
+ *
+ * @param <I> The metadata identifier type.
+ * @param <T> The metadata type.
+ */
+public interface BatchBackingStore<I,T> extends BackingStore<I,T> {
+    
+    
+    /**
+     * Get the time at which this batch backing store was successfully reloaded. That is, completely cleared and
+     * new entries added.
+     * 
+     * @return the last update time.
+     */
+    @Nullable Instant getLastUpdate();
+    
+    
+    /**
+     * Get the time the last refresh of this backing store was attempted.
+     * 
+     * @return the last attempted refresh time.
+     */
+    @Nullable Instant getLastRefresh();
+    
+    
+    /**
+     * Set the time at which this batch backing store was successfully reloaded/updated.
+     * 
+     * @param updatedAt the time it was last updated.
+     */
+    void setLastUpdate(@Nullable final Instant updatedAt);
+    
+    /**
+     * Set the time at which the last refresh was attempted.
+     * 
+     * @param refreshedAt the refreshed time. 
+     */
+    void setLastRefresh(@Nullable final Instant refreshedAt);
+    
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicBackingStore.java
similarity index 50%
copy from oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java
copy to oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicBackingStore.java
index bd21547..a9f5b57 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BackingStore.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicBackingStore.java
@@ -1,25 +1,5 @@
-/*
- * 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.oidc.metadata;
 
-import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 import javax.annotation.Nonnull;
@@ -28,27 +8,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElemen
 import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
 import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 
-/**
- * A backing store that holds cached objects.
- * 
- * @param <I> the identifier type
- * @param <T> the type of object stored, referenced by the key.
- */
-public interface BackingStore<I, T> {
-    
-    /**
-     * Get the index - mapping keys to values. 
-     * 
-     * @return the index.
-     */
-    @Nonnull Map<I, List<T>> getIndexedValues();
-    
-    /**
-     * Get the list of ordered values.
-     * 
-     * @return the list of ordered values.
-     */
-    @Nonnull List<T> getOrderedValues();
+public interface DynamicBackingStore<I, T> extends BackingStore<I, T> {
     
     /**
      * Get the management data for the specified identifier. If the management data does not exist
@@ -59,7 +19,7 @@ public interface BackingStore<I, T> {
      * <p>Should do so in a thread-safe way e.g. if two threads enter this method, only one should be allowed
      * to create management data for the same identifier.</p>
      * 
-     * @param identifier the identifier of the entity to find management data aboout
+     * @param identifier the identifier of the entity to find management data about
      * 
      * @return the corresponding management data
      */
@@ -79,7 +39,5 @@ public interface BackingStore<I, T> {
      */
     @Nonnull @NonnullElements @Unmodifiable @NotLive
     Set<I> getManagementDataIdentifiers();
-    
-    
 
 }
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java
index 6a94131..9923e84 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/MetadataManagementData.java
@@ -1,4 +1,3 @@
-package net.shibboleth.oidc.metadata;
 /*
  * Licensed to the University Corporation for Advanced Internet Development,
  * Inc. (UCAID) under one or more contributor license agreements.  See the
@@ -16,7 +15,8 @@ package net.shibboleth.oidc.metadata;
  * limitations under the License.
  */
 
-import java.time.Duration;
+package net.shibboleth.oidc.metadata;
+
 import java.time.Instant;
 import java.util.concurrent.locks.StampedLock;
 
@@ -56,7 +56,6 @@ public class MetadataManagementData<MetadataIdentifier> {
     /** Constructor. 
      * 
      * @param identifier the entity ID managed by this instance
-     * @param maxCacheDuration the maximum cache duration for metadata
      */
     public MetadataManagementData(@Nonnull final MetadataIdentifier identifier) {
         id = Constraint.isNotNull(identifier, "ID was null");
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/CacheLoadingContext.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/CacheLoadingContext.java
new file mode 100644
index 0000000..f7cd2a9
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/CacheLoadingContext.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.oidc.metadata.cache;
+
+import java.time.Instant;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Context to hold information pertaining to a cache loading event.
+ */
+ at Immutable
+public class CacheLoadingContext {
+    
+    /** Last time the metadata was updated. */
+    @Nullable private final Instant lastUpdate;
+
+    /** Last time a refresh cycle occurred. */
+    @Nullable private final Instant lastRefresh;
+
+    /**
+     * Constructor.
+     *
+     * @param lastUpdateTime the time of the last successful loading event.
+     * @param lastRefreshTime the time of the last refresh attempt.
+     */
+    public CacheLoadingContext(@Nullable final Instant lastUpdateTime, @Nullable final Instant lastRefreshTime) {
+        lastUpdate = lastUpdateTime;
+        lastRefresh = lastRefreshTime;
+    }
+
+    /**
+     * Get the last successful update time.
+     * 
+     * @return Returns the lastUpdate.
+     */
+    public final Instant getLastUpdate() {
+        return lastUpdate;
+    }
+
+    /**
+     * Get the last refresh time.
+     * 
+     * @return Returns the lastRefresh.
+     */
+    public final Instant getLastRefresh() {
+        return lastRefresh;
+    }
+    
+    
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCache.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCache.java
index 92b3e90..186f09e 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCache.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCache.java
@@ -1,7 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.oidc.metadata.cache;
 
 import java.util.List;
-import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 
@@ -10,28 +27,26 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
 /**
- * A cache for metadata. 
+ * A metadata cache. 
+ * 
+ * TODO: finish
  *
- * @param <T> The metadata identifier type.
  * @param <U> The metadata type.
  */
 public interface MetadataCache<U> {
     
     /**
-     * Get the list of metadata matching the given identifier. If not found, obtain it from the given fetching
-     * function. 
+     * Get the list of metadata matching the given identifier. 
      * 
-     * <p>The implementing method is required to be thread safe. The fetching function must be called within an
-     * appropriate lock.</p>
+     * <p>The implementing method is required to be thread safe.</p>
      * 
      * @param criteria the criteria to use when getting the metadata from the cache or the source. 
-     * @param fetchFunction a function to call to obtain the metadata if it does not exist in the cache.
      *  
      * @return a list of metadata matching the criteria.
      * 
      * @throws MetadataCacheException on error fetching metadata. 
      */
-    @Nonnull @NonnullElements public List<U> getOrFetchIfAbsent(@Nonnull @NotEmpty final CriteriaSet criteria,
-            @Nonnull final Function<CriteriaSet, U> fetchFunction) throws MetadataCacheException;
+    @Nonnull @NonnullElements public List<U> get(@Nonnull @NotEmpty final CriteriaSet criteria)
+            throws MetadataCacheException;
 
 }
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/AbstractMetadataCache.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/AbstractMetadataCache.java
new file mode 100644
index 0000000..e14237d
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/AbstractMetadataCache.java
@@ -0,0 +1,555 @@
+/*
+ * 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.oidc.metadata.cache.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+import net.shibboleth.oidc.metadata.BackingStore;
+import net.shibboleth.oidc.metadata.MetadataManagementData;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.oidc.metadata.filter.MetadataSource;
+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.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.TimerSupport;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+
+/**
+ * A base {@link MetadataCache} implementation. Supports the following:
+ * <ul>
+ * <li>A configurable backing store to store metadata.</li>
+ * <li>Read-write locking on individual metadata entries. Including optimistic reads.</li>
+ * <li>Synchronous metadata fetching for new metadata.</li>
+ * <li>Synchronous metadata fetching for stale (past refresh point) metadata.</li>
+ * <li>A background task to remove expired and idle metadata.</li>
+ * <li>Configuration of type specific functions via strategies.</li>
+ * </ul>
+ *
+ * @param <IdentifierType> the metadata identifier type
+ * @param <MetadataType> the metadata type
+ */
+//TODO do not set a maximum cache size? make eviction harder
+ at ThreadSafe
+public abstract class AbstractMetadataCache<IdentifierType, MetadataType> 
+                extends AbstractIdentifiableInitializableComponent implements MetadataCache<MetadataType> {
+    
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(AbstractMetadataCache.class);    
+    
+    /** Maximum cache duration. */
+    //@NonnullAfterInit private Duration maxCacheDuration;
+    
+    /** Cached log prefix. */
+    @Nullable private String logPrefix;
+    
+    /** Minimum cache duration. */
+    @NonnullAfterInit private Duration minCacheDuration;
+    
+    /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
+    @NonnullAfterInit @Positive private Float refreshDelayFactor;
+
+    /** Backing store for runtime metadata.*/
+    @Nonnull private final BackingStore<IdentifierType, MetadataType> backingStore; 
+    
+    /** 
+     * A hook that is executed just before a cache entry will been removed/invalidated/evicted.
+     * The metadata list could be null, the identifier is never null.
+     */
+    @Nullable private BiConsumer<List<MetadataType>, IdentifierType> metadataBeforeRemovalHook;
+    
+    /** Strategy used to extract an identifier from the given metadata.*/
+    @NonnullAfterInit private Function<MetadataType, IdentifierType> identifierExtractionStrategy;
+    
+    /** Strategy used to compute an expiration time. */
+    @NonnullAfterInit private BiFunction<MetadataType, Instant, Instant> metadataExpirationTimeStrategy;
+    
+    /** Map criteria to identifiers to use as keys to the backing store.*/
+    @NonnullAfterInit private Function<CriteriaSet, IdentifierType> criteriaToIdentifierStrategy;
+    
+    /** A strategy to filter metadata. */
+    @NonnullAfterInit private BiFunction<MetadataType, MetadataFilterContext , MetadataType> metadataFilterStrategy;
+    
+    /** A single threaded executor service for running background cache tasks.*/
+    @NonnullAfterInit private ScheduledExecutorService executorService;
+    
+    /** Are metrics enabled?.*/
+    //TODO not yet implemented
+    private boolean enableMetrics;
+    
+
+    
+    /** Whether we created our own schedular during object construction. */
+    private boolean createOwnSchedular;
+    
+    /** Package private constructor. For safe-construction through the factory only.*/
+    AbstractMetadataCache(@Nonnull final BackingStore<IdentifierType, MetadataType> store) {       
+        this(store, null);
+    }
+   
+    
+    /**
+     * 
+     * Package private constructor. For safe-construction through the factory only.
+     * 
+     * <p>Accepts an executor. Mostly used for testing.</p>
+     *
+     * @param store the backing store.
+     * @param executor the scheduled executor
+     */
+    AbstractMetadataCache(@Nonnull final BackingStore<IdentifierType, MetadataType> store, 
+            @Nullable final ScheduledExecutorService executor) {    
+        backingStore = Constraint.isNotNull(store, "A backingstore must be set");
+        if (executor != null) {
+            executorService = executor;
+            createOwnSchedular = false;
+        } else {
+            createOwnSchedular = true;
+        }
+        
+    }
+    
+    /**
+     * Return a prefix for logging messages for this component.
+     * 
+     * @return a string for insertion at the beginning of any log messages
+     */
+    @Nonnull @NotEmpty protected String getLogPrefix() {
+        if (logPrefix == null) {
+            logPrefix = "Metadata Cache " + (getId() != null ? getId() : "(unknown)") + ":";
+        }
+        return logPrefix;
+    }
+
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();    
+       
+        
+        if (identifierExtractionStrategy == null) {
+            throw new ComponentInitializationException("Identifier extraction strategy can not be null");
+        }
+        if (metadataExpirationTimeStrategy == null) {
+            throw new ComponentInitializationException("Metadata expiration strategy can not be null");
+        }
+        if (criteriaToIdentifierStrategy == null) {
+            throw new ComponentInitializationException("Criteria to identifier strategy can not be null");
+        }
+        if (metadataFilterStrategy == null) {
+            throw new ComponentInitializationException("Metadata filter strategy can not be null");
+        }
+        if (//maxCacheDuration == null || 
+                minCacheDuration == null || refreshDelayFactor == null ||               
+                backingStore == null) {
+            throw new ComponentInitializationException("Metadata cache not property initialized");
+        }
+        
+        // create a schedular for thread tasks.
+        if (createOwnSchedular) {
+            // Use thread builder to allow setting threads as deamon and a name.
+            // Set as deamon background threads. Do not prevent JVM exit.
+            executorService = Executors.newSingleThreadScheduledExecutor(
+                    new ThreadFactoryBuilder().setDaemon(true).setNameFormat(TimerSupport.getTimerName(this)+"-%d").build()); 
+        }                  
+    }
+    
+    @Override protected void doDestroy() {
+        log.info("Shutting down cache '{}'",getLogPrefix());
+        executorService.shutdown();
+        super.doDestroy();
+    }
+    
+    /**
+     * Get the background executor service.
+     * 
+     * @return the executor service.
+     */
+    @NonnullAfterInit protected ScheduledExecutorService getExecutorService() {
+        return executorService;
+    }
+    
+
+
+    /**
+     * Set the {@link CriteriaSet} to IdentifierType strategy.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setCriteriaToIdentifierStrategy(@Nonnull final Function<CriteriaSet, IdentifierType> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        criteriaToIdentifierStrategy =  Constraint.isNotNull(strategy,"Criteria to identifier strategy can not be null");
+    }
+    
+    /**
+     * Get the identifier from criteria set extraction strategy.
+     * 
+     * @return the identifier extraction strategy. 
+     */
+    @NonnullAfterInit protected Function<CriteriaSet, IdentifierType> getCriteriaToIdentifierStrategy() {
+        return criteriaToIdentifierStrategy;
+    }
+    
+    /**
+     * Set the MetadataType to IdentifierType extraction time strategy.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setIdentifierExtractionStrategy(@Nonnull final Function<MetadataType, IdentifierType> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        identifierExtractionStrategy = Constraint.isNotNull(strategy, "Identifier extraction strategy can not be null");
+    }
+    
+    /**
+     * Get the identifier extraction strategy.
+     * 
+     * @return the identifier extraction strategy.
+     */
+    @NonnullAfterInit protected Function<MetadataType, IdentifierType> getIdentifierExtractionStrategy() {
+        return identifierExtractionStrategy;
+    }
+    
+
+    /**
+     * Set the metadata expiration time strategy.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setMetadataExpirationTimeStrategy(
+            @Nonnull final BiFunction<MetadataType, Instant, Instant> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        metadataExpirationTimeStrategy = Constraint.isNotNull(strategy, "Metadata expiration strategy can not be null");
+    }
+    
+    /**
+     * Get the metadata expiration time strategy. 
+     * 
+     * @return the expiration time strategy.
+     */
+    @NonnullAfterInit protected BiFunction<MetadataType, Instant, Instant> getMetadataExpirationTimeStrategy() {
+        return metadataExpirationTimeStrategy;
+    }
+    
+    /**
+     * Set the metadata filtering strategy.
+     * 
+     * @param strategy the metadata filtering strategy.
+     */
+    public void setMetadataFilterStrategy(
+            @Nonnull final BiFunction<MetadataType, MetadataFilterContext, MetadataType> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        metadataFilterStrategy = Constraint.isNotNull(strategy, "Metadata filter strategy can not be null");;
+    }
+    
+    /**
+     * Get the metadata filtering strategy.
+     * 
+     * @return the filtering strategy.
+     */
+    @NonnullAfterInit protected BiFunction<MetadataType, MetadataFilterContext, MetadataType> getMetadataFilterStrategy() {
+        return metadataFilterStrategy;
+    }
+    
+    /**
+     *  Set the maximum cache duration for metadata.
+     *  
+     *  <p>Defaults to: 8 hours.</p>
+     *  
+     * @param duration the maximum cache duration
+     */
+//    public void setMaxCacheDuration(@Nonnull final Duration duration) {
+//        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+//        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+//
+//        Constraint.isNotNull(duration, "Duration cannot be null");
+//        Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
+//        
+//        maxCacheDuration = duration;
+//    }
+
+
+    /**
+     *  Set the minimum cache duration for metadata.
+     *  
+     *  <p>Defaults to: 10 minutes.</p>
+     *  
+     * @param duration the minimum cache duration
+     */
+    public void setMinCacheDuration(@Nonnull final Duration duration) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        Constraint.isNotNull(duration, "Duration cannot be null");
+        Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
+        
+        minCacheDuration = duration;
+    }
+    
+    /**
+     * Set a hook to run before a metadata cache entry is removed from the cache.
+     * <p>The hook is required to gracefully handle null metadata lists.</p>
+     * 
+     * @param hook the hook to run.
+     */
+    public void setMetadataBeforeRemovalHook(
+            @Nullable final BiConsumer<List<MetadataType>, IdentifierType> hook) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        metadataBeforeRemovalHook = hook;
+    }
+    
+
+    /**
+     * Sets the delay factor used to compute the next refresh time. The delay must be between 0.0 and 1.0, exclusive.
+     * 
+     * <p>Defaults to:  0.75.</p>
+     * 
+     * @param factor delay factor used to compute the next refresh time
+     */
+    public void setRefreshDelayFactor(@Nonnull final Float factor) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        if (factor <= 0 || factor >= 1) {
+            throw new IllegalArgumentException("Refresh delay factor must be a number between 0.0 and 1.0, exclusive");
+        }
+
+        refreshDelayFactor = factor;
+    }
+    
+    /**
+     * Get the refresh delay factor.
+     * 
+     * @return the refresh delay factor.
+     */
+    @NonnullAfterInit protected Float getRefreshDelayFactor() {
+        return refreshDelayFactor;
+    }
+    
+    /**
+     * Lookup the specified identifier from the index. The returned list will be a copy of what is stored in the backing
+     * index, and so the list is safe to be manipulated by callers.
+     * 
+     * @param identifier the entityID to lookup
+     * 
+     * @return list copy of indexed metadata, may be empty, will never be null
+     */
+    @Nonnull @NonnullElements protected List<MetadataType> lookupIndexedIdentifier(
+            @Nonnull @NotEmpty final IdentifierType identifier) {
+        final List<MetadataType> metadata = backingStore.getIndexedValues().get(identifier);
+        if (metadata != null) {
+            return new ArrayList<>(metadata);
+        }
+        return Collections.emptyList();
+    }
+    
+    /**
+     * Get the backing store.
+     * 
+     * @return the backing store.
+     */
+    @Nonnull protected BackingStore<IdentifierType, MetadataType> getBackingStore() {
+        return backingStore;
+    }
+    
+     
+    /**
+     * Clear the backing store and load each metadata entry one at a time.
+     * 
+     * @param metadataToStore the metadata to load into the cache.
+     */
+    protected void freshLoad(@Nonnull final List<MetadataType> metadataToStore) {
+        invalidateAll();
+        for (final MetadataType metadata : metadataToStore) {
+            final IdentifierType identifier = identifierExtractionStrategy.apply(metadata);
+            log.debug("{} Resolved criteria to identifier: {}",  getLogPrefix(), identifier); 
+            writeToBackingStore(metadata);
+        }
+        
+    }
+    
+    protected void writeToBackingStore(@Nonnull final MetadataType metadata) {
+        
+        final IdentifierType extractedIdentifier = identifierExtractionStrategy.apply(metadata);
+        
+        // will only effect the dynamic cache case, static loading should have cleared the backing store
+        // by this point
+        invalidate(extractedIdentifier);
+        
+        backingStore.getOrderedValues().add(metadata);
+        // add new metadata to index
+        List<MetadataType> existingMetadata = backingStore.getIndexedValues().get(extractedIdentifier);
+        if (existingMetadata == null) {
+            existingMetadata = new ArrayList<>();
+            backingStore.getIndexedValues().put(extractedIdentifier, existingMetadata);
+        } else if (!existingMetadata.isEmpty()) {
+            log.warn("{} Detected duplicate metadata for identifier: {}", getLogPrefix(), extractedIdentifier);
+        }
+        existingMetadata.add(metadata);
+    }    
+    
+
+    
+    
+    
+    /**
+     * Get a new instance of {@link MetadataFilterContext} to be used when filtering metadata.
+     *
+     * <p>
+     * This default implementation just returns an empty context with as metadatasource for passing to 
+     * the filter strategy. The strategy can then add to the context as appropriate.
+     * </p>
+     *
+     * @return the new filter context instance
+     */
+    @Nonnull protected MetadataFilterContext newFilterContext() {
+        
+        final MetadataSource source = new MetadataSource();
+        source.setSourceId(getId());
+
+        final MetadataFilterContext context = new MetadataFilterContext();
+        context.add(source);
+        
+        return context;
+    }
+    
+   
+    /**
+     * Determine whether should attempt to refresh the metadata, based on stored refresh trigger time.
+     * 
+     * @param mgmtData the entity'd management data
+     * @return true if should attempt refresh, false otherwise
+     */
+    protected boolean shouldAttemptRefresh(@Nonnull final MetadataManagementData<IdentifierType> mgmtData) {
+        return Instant.now().isAfter(mgmtData.getRefreshTriggerTime());
+        
+    }
+    
+    /**
+     * Remove/discard from the backing store all metadata for the entity with the given identifier.
+     * 
+     * @param identifier the ID of the metadata to remove
+     */
+    protected void invalidate(@Nonnull final IdentifierType identifier) {
+        final Map<IdentifierType, List<MetadataType>> indexedDescriptors = backingStore.getIndexedValues();
+        final List<MetadataType> descriptors = indexedDescriptors.get(identifier);
+        if (metadataBeforeRemovalHook != null) {
+            // descriptors is nullable.
+            metadataBeforeRemovalHook.accept(descriptors, identifier);
+        }
+        if (descriptors != null) {
+            backingStore.getOrderedValues().removeAll(descriptors);
+        }
+        indexedDescriptors.remove(identifier);
+    }
+    
+    /**
+     * Remove/discard all metadata for the backing store.
+     * 
+     * <p>Ensure thread-safety is observed if this is called from an unsafe
+     * call-site. </p>
+     * 
+     * TODO check lock. 
+     */
+    protected void invalidateAll() {
+        backingStore.getIndexedValues().clear();
+        backingStore.getOrderedValues().clear();
+    }
+    
+    /**
+     * Compute the refresh trigger time.
+     * 
+     * @param expirationTime the time at which the metadata effectively expires
+     * @param nowDateTime the current date time instant
+     * 
+     * @return the time after which refresh attempt(s) should be made
+     */
+    @Nonnull protected Instant computeRefreshTriggerTime(@Nullable final Instant expirationTime,
+            @Nonnull final Instant nowDateTime) {
+        
+        final long now = nowDateTime.toEpochMilli();
+
+        long expireInstant = 0;
+        if (expirationTime != null) {
+            expireInstant = expirationTime.toEpochMilli();
+        }
+        long refreshDelay = (long) ((expireInstant - now) * refreshDelayFactor);
+
+        // if the expiration time was null or the calculated refresh delay was less than the floor
+        // use the floor
+        if (refreshDelay < minCacheDuration.toMillis()) {
+            refreshDelay = minCacheDuration.toMillis();
+        }
+
+        return nowDateTime.plusMillis(refreshDelay);
+    }
+    
+    /**
+     * Create a wrapper for runnables that catches any throwable and logs it. Useful
+     * to prevent thread death from an uncaught exception.
+     * 
+     * @param action the runnable to wrap
+     * 
+     * @return the wrapped runnable.
+     */
+    @Nonnull protected Runnable errorHandlingWrapper(@Nonnull final Runnable action) {
+        return () -> {
+            try {
+                action.run();
+            } catch (final Throwable e) {
+                log.error("{} Error executing thread task", getLogPrefix(), e);
+            }
+        };
+    }
+    
+    
+}
+
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCache.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCache.java
new file mode 100644
index 0000000..ea8da6a
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCache.java
@@ -0,0 +1,306 @@
+/*
+ * 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.oidc.metadata.cache.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.oidc.metadata.BatchBackingStore;
+import net.shibboleth.oidc.metadata.cache.CacheLoadingContext;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+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.Positive;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * A metadata cache implementation that supports 'refresh-ahead' semantics for batch cache updates. 
+ * Does not support 'read-through' semantics if an entry does not exist in the cache.
+ *
+ * @param <IdentifierType> the metadata identifier type.
+ * @param <MetadataType> the metadata type.
+ */
+//TODO should be called reloadable or batch?
+public class BatchMetadataCache<IdentifierType, MetadataType> 
+                                extends AbstractMetadataCache<IdentifierType, MetadataType> {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(BatchMetadataCache.class);
+
+    /**
+     * Refresh interval used when metadata does not contain any validUntil or cacheDuration information. Default value:
+     * 4 hours
+     */
+    @NonnullAfterInit @Positive private Duration maxRefreshDelay;
+
+    /** Floor, in milliseconds, for the refresh interval. Default value: 5 minutes */
+    @NonnullAfterInit @Positive private Duration minRefreshDelay;
+    
+    /** The function to use to load metadata.*/
+    @Nonnull private final Function<CacheLoadingContext, byte[]> loadingStrategy;
+    
+    /** How to parse the loaded metadata from the loadingStrategy into a usable metadatatype.*/
+    @Nonnull private final Function<byte[], List<MetadataType>> parsingStrategy;
+   
+
+    /**
+     * 
+     * Package private constructor.
+     *
+     * @param store the backing store.
+     * @param metadataLoadingStrategy strategy used to load metadata.
+     * @param parseStrategy the strategy used to convert raw metadata in bytes to the given metadata type.
+     */
+    public BatchMetadataCache(@Nonnull final BatchBackingStore<IdentifierType, MetadataType> store,
+            @Nonnull final Function<CacheLoadingContext, byte[]> metadataLoadingStrategy,
+            @Nonnull final Function<byte[], List<MetadataType>> parseStrategy) {
+        this(store, metadataLoadingStrategy, parseStrategy, null);
+    }
+
+    /**
+     * 
+     * Package private constructor.
+     *
+     * @param store the backing store.
+     * @param metadataLoadingStrategy strategy used to load metadata.
+     * @param parseStrategy the strategy used to convert raw metadata in bytes to the given metadata type.
+     * @param executor the scheduled executor
+     */
+    BatchMetadataCache(@Nonnull final BatchBackingStore<IdentifierType, MetadataType> store,
+            @Nonnull final Function<CacheLoadingContext, byte[]> metadataLoadingStrategy,
+            @Nonnull final Function<byte[], List<MetadataType>> parseStrategy,
+            @Nullable final ScheduledExecutorService executor) {
+        super(store, executor);
+        loadingStrategy = 
+                Constraint.isNotNull(metadataLoadingStrategy, "Metadata loading strategy can not be null");
+        parsingStrategy = Constraint.isNotNull(parseStrategy, "Metadata Parsing strategy can not be null");
+    }
+
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (maxRefreshDelay == null || minRefreshDelay == null) {
+            throw new ComponentInitializationException("Refreshable metadata cache not property initialized");
+        }
+        
+        try {
+            loadCache();
+        } catch (final MetadataCacheException e) {
+            throw new ComponentInitializationException("Error loading metadata during init", e);
+        }      
+    }
+    
+    /**
+     * {@inheritDoc}
+     * 
+     * <p>Cast the backing store to the type used by this cache type.</p>
+     */
+    @Override @Nonnull protected BatchBackingStore<IdentifierType, MetadataType> getBackingStore() {
+        return (BatchBackingStore<IdentifierType, MetadataType>) super.getBackingStore();
+    }
+
+    /**
+     * Sets the minimum amount of time between refreshes.
+     * 
+     * @param delay minimum amount of time between refreshes
+     */
+    public void setMinRefreshDelay(@Positive @Nonnull final Duration delay) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        Constraint.isFalse(delay == null || delay.isNegative(), "Minimum refresh delay must be greater than 0");
+        minRefreshDelay = delay;
+    }
+
+    /**
+     * Sets the maximum amount of time between refresh intervals.
+     * 
+     * @param delay maximum amount of time, in milliseconds, between refresh intervals
+     */
+    public void setMaxRefreshDelay(@Positive @Nonnull final Duration delay) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        Constraint.isFalse(delay == null || delay.isNegative(), "Maximum refresh delay must be greater than 0");
+        maxRefreshDelay = delay;
+    }
+
+    //TODO we sure get does not need synchornization with loadingCache?
+    @Override @Nonnull @NonnullElements
+    public List<MetadataType> get(@Nonnull final CriteriaSet criteria) throws MetadataCacheException {
+
+        if (!isInitialized()) {
+            throw new MetadataCacheException("Metadata cache has not been initialized");
+        }
+
+        final IdentifierType identifier = getCriteriaToIdentifierStrategy().apply(criteria);
+        log.debug("{} Resolved criteria to identifier: {}", getLogPrefix(), identifier);
+
+        if (identifier != null) {
+
+            final List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
+            if (allMetadata.isEmpty()) {
+                log.debug("{} Metadata candidates for '{}' do not exist, returning empty result", 
+                        getLogPrefix(), identifier);
+                return Collections.emptyList();
+            } else {
+                log.debug("{} There are {} Metadata candidates for '{}' found in cache", 
+                        getLogPrefix(), allMetadata.size(), identifier);
+                return allMetadata;
+            }
+        } else {
+            // TODO: see SAML version, could resolve from criteria even if no identifier.
+            log.debug("{} Identifier not resolvable from criteria, can not fetch metadata", getLogPrefix());
+            return Collections.emptyList();
+        }
+    }
+    
+    private CacheLoadingContext createLoadingContext() {
+        return new CacheLoadingContext(getBackingStore().getLastUpdate(), getBackingStore().getLastRefresh());
+    }
+    
+    /**
+     * Reload the entire backing-store cache using the loading strategy.
+     * 
+     * <p>Use an intrinsic object lock when loading. Although it is possible a {@link #get(CriteriaSet)} will
+     * occur at the same time as this load.</p>
+     * 
+     * @throws MetadataCacheException on loading error.
+     */
+    //TODO lock?
+    private synchronized void loadCache() throws MetadataCacheException{
+        
+        log.debug("{} Populating cache from '{}'",getLogPrefix());
+        final Instant now = Instant.now();
+        Duration refreshDelay = null;
+        try {
+            // Any exception here is caught
+            final byte[] rawFetchedMetadata = loadingStrategy.apply(createLoadingContext());
+            if (rawFetchedMetadata != null) {
+                final List<MetadataType> parsedMetadata = parsingStrategy.apply(rawFetchedMetadata);
+                if (parsedMetadata != null) {
+                    log.info("{} Parsed {} metadata candidates, loading into cache", 
+                            getLogPrefix(), parsedMetadata.size());
+                    freshLoad(parsedMetadata);
+                } 
+            } else {
+                log.info("{} Metadata from '{}' has not changed since last refresh", getLogPrefix());
+            }
+        } catch (final Throwable t) {
+            log.error("{} Error loading or parsing metadata",getLogPrefix(), t);
+            refreshDelay = minRefreshDelay;
+            if (t instanceof Exception) {
+                throw new MetadataCacheException((Exception) t);
+            } else {
+                throw new MetadataCacheException(String.format("Saw an error of type '%s' with message '%s'", 
+                        t.getClass().getName(), t.getMessage()));
+            }
+        } finally {
+            scheduleNextRefresh(refreshDelay);
+            getBackingStore().setLastRefresh(now);
+        }
+        
+    }
+    
+    
+    /**
+     * Schedules the next refresh. If the given delay is 0 or null, then {@link #maxRefreshDelay} is used.
+     * 
+     * @param delay The delay before the next refresh.
+     */
+    private void scheduleNextRefresh(@Nullable final Duration delay) {
+        Duration refreshDelay = delay;
+        if (delay == null || delay.isZero()) {
+            refreshDelay = maxRefreshDelay;
+        }
+        final Instant nextRefresh = Instant.now().plus(refreshDelay);
+        final long nextRefreshDelay = nextRefresh.toEpochMilli() - System.currentTimeMillis();
+
+        getExecutorService().schedule(errorHandlingWrapper(
+                new AsynchronousRefreshAHeadTask()), nextRefreshDelay, TimeUnit.MILLISECONDS);
+        
+        log.info("{} Next refresh cycle for metadata provider '{}' will occur on '{}' ('{}' local time)",
+                getLogPrefix(), "ADD THIS ONE", nextRefresh, nextRefresh.atZone(ZoneId.systemDefault()));
+    }
+    
+    
+    /**
+     * Computes the delay until the next refresh time based on the current metadata's expiration time and the refresh
+     * interval floor.
+     * 
+     * @param expectedExpiration the time when the metadata is expected to expire and need refreshing
+     * 
+     * @return delay until the next refresh time
+     */
+    @Nonnull private Duration computeNextRefreshDelay(final Instant expectedExpiration) {
+        final long now = System.currentTimeMillis();
+
+        long expireInstant = 0;
+        if (expectedExpiration != null) {
+            expireInstant = expectedExpiration.toEpochMilli();
+        }
+        long refreshDelay = (long) ((expireInstant - now) * getRefreshDelayFactor());
+
+        // if the expiration time was null or the calculated refresh delay was less than the floor
+        // use the floor
+        if (refreshDelay < minRefreshDelay.toMillis()) {
+            refreshDelay = minRefreshDelay.toMillis();
+        }
+
+        return Duration.ofMillis(refreshDelay);
+    }
+
+    private class AsynchronousRefreshAHeadTask implements Runnable {
+
+        @Override
+        public void run() {
+
+            if (!isInitialized()) {
+                // just in case the metadata provider was destroyed before this task runs
+                return;
+            }
+
+            try {
+                loadCache();
+            } catch (final MetadataCacheException e) {
+                // nothing the thread can do.
+                return;
+            }
+
+        }
+        
+        
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCache.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCache.java
deleted file mode 100644
index 4bc59a9..0000000
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCache.java
+++ /dev/null
@@ -1,743 +0,0 @@
-/*
- * 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.oidc.metadata.cache.impl;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.StampedLock;
-import java.util.function.BiConsumer;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.util.concurrent.ThreadFactoryBuilder;
-
-import net.shibboleth.oidc.metadata.BackingStore;
-import net.shibboleth.oidc.metadata.MetadataManagementData;
-import net.shibboleth.oidc.metadata.cache.MetadataCache;
-import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
-import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
-import net.shibboleth.oidc.metadata.filter.MetadataSource;
-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.component.AbstractIdentifiableInitializableComponent;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.primitive.TimerSupport;
-import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
-
-
-/**
- * A default {@link MetadataCache} implementation. Supports the following:
- * <ul>
- * <li>A configurable backing store to store metadata.</li>
- * <li>Read-write locking on individual metadata entries. Including optimistic reads.</li>
- * <li>Synchronous metadata fetching for new metadata.</li>
- * <li>Synchronous metadata fetching for stale (past refresh point) metadata.</li>
- * <li>A background task to remove expired and idle metadata.</li>
- * <li>Configuration of type specific functions via strategies.</li>
- * </ul>
- *
- * @param <IdentifierType> the metadata identifier type
- * @param <MetadataType> the metadata type
- */
-//TODO add getId() to log messages?
- at ThreadSafe
-public final class DefaultMetadataCache<IdentifierType, MetadataType> 
-                extends AbstractIdentifiableInitializableComponent implements MetadataCache<MetadataType> {
-    
-    /** Class logger. */
-    private final Logger log = LoggerFactory.getLogger(DefaultMetadataCache.class);    
-    
-    /** Maximum cache duration. */
-    @NonnullAfterInit private Duration maxCacheDuration;
-    
-    /** Minimum cache duration. */
-    @NonnullAfterInit private Duration minCacheDuration;
-    
-    /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
-    @NonnullAfterInit @Positive private Float refreshDelayFactor;
-    
-    /** The maximum idle time for which the resolver will keep data for a given entityID, 
-     * before it is removed. */
-    @NonnullAfterInit private Duration maxIdleEntityData;
-    
-    /** Flag indicating whether idle entity data should be removed. */
-    private boolean removeIdleEntityData;
-    
-    /** The interval at which the cleanup task should run. */
-    @NonnullAfterInit private Duration cleanupTaskInterval;
-    
-    /** The initial cleanup task delay.*/
-    @NonnullAfterInit private Duration initialCleanupTaskDelay;
-    
-    /** Backing store for runtime metadata.*/
-    @Nonnull private final BackingStore<IdentifierType, MetadataType> backingStore; 
-    
-    /** 
-     * A hook that is executed just before a cache entry will been removed/invalidated/evicted.
-     * The metadata list could be null, the identifier is never null.
-     */
-    @Nullable private BiConsumer<List<MetadataType>, IdentifierType> metadataBeforeRemovalHook;
-    
-    //TODO move this into the method for get or fetch? Otherwise why not add the fetch strategy to here also?
-    /** Strategy used to extract an identifier from the given metadata.*/
-    @NonnullAfterInit private Function<MetadataType, IdentifierType> identifierExtractionStrategy;
-    
-    /** Strategy used to compute an expiration time. */
-    @NonnullAfterInit private BiFunction<MetadataType, Instant, Instant> metadataExpirationTimeStrategy;
-    
-    /** Map criteria to identifiers to use as keys to the backing store.*/
-    @NonnullAfterInit private Function<CriteriaSet, IdentifierType> criteriaToIdentifierStrategy;
-    
-    /** A strategy to filter metadata. */
-    @NonnullAfterInit private BiFunction<MetadataType, MetadataFilterContext , MetadataType> metadataFilterStrategy;
-    
-    /** A single threaded executor service for running background cache tasks.*/
-    @NonnullAfterInit private ScheduledExecutorService executorService;
-    
-    /** Whether we created our own schedular during object construction. */
-    private boolean createOwnSchedular;
-    
-    /** Package private constructor. For safe-construction through the factory only.*/
-    DefaultMetadataCache(@Nonnull final BackingStore<IdentifierType, MetadataType> store) {       
-        this(store, null);
-    }
-    
-    
-    /**
-     * 
-     * Package private constructor. For safe-construction through the factory only.
-     * 
-     * <p>Accepts an executor. Mostly used for testing.</p>
-     *
-     * @param executor
-     */
-    DefaultMetadataCache(@Nonnull final BackingStore<IdentifierType, MetadataType> store, 
-            @Nullable final ScheduledExecutorService executor) {    
-        backingStore = Constraint.isNotNull(store, "A backingstore must be set");
-        if (executor != null) {
-            executorService = executor;
-            createOwnSchedular = false;
-        } else {
-            createOwnSchedular = true;
-        }
-        
-    }
-    
-    
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();    
-       
-        
-        if (identifierExtractionStrategy == null) {
-            throw new ComponentInitializationException("Identifier extraction strategy can not be null");
-        }
-        if (metadataExpirationTimeStrategy == null) {
-            throw new ComponentInitializationException("Metadata expiration strategy can not be null");
-        }
-        if (criteriaToIdentifierStrategy == null) {
-            throw new ComponentInitializationException("Criteria to identifier strategy can not be null");
-        }
-        if (metadataFilterStrategy == null) {
-            throw new ComponentInitializationException("Metadata filter strategy can not be null");
-        }
-        if (maxCacheDuration == null || minCacheDuration == null || refreshDelayFactor == null ||
-                maxIdleEntityData == null  || cleanupTaskInterval == null || initialCleanupTaskDelay == null ||
-                backingStore == null) {
-            throw new ComponentInitializationException("Metadata cache not property initialized");
-        }
-        
-        if (createOwnSchedular) {
-            // Use thread builder to allow setting threads as deamon and a name.
-            // Set as deamon background threads. Do not prevent JVM exit.
-            executorService = Executors.newSingleThreadScheduledExecutor(
-                    new ThreadFactoryBuilder().setDaemon(true).setNameFormat(TimerSupport.getTimerName(this)+"-%d").build()); 
-        }
-        
-        executorService.scheduleAtFixedRate(
-                errorHandlingWrapper(new ExpiredAndIdleMetadataCleanupTask()), initialCleanupTaskDelay.toMillis(), 
-                cleanupTaskInterval.toMillis(), TimeUnit.MILLISECONDS);
-                   
-    }
-    
-    @Override protected void doDestroy() {
-        executorService.shutdown();
-        super.doDestroy();
-    }
-    
-    /**
-     * Set the initial cleanup task delay.
-     * 
-     * @param delay The initialCleanupTaskDelay to set.
-     */
-    public void setInitialCleanupTaskDelay(@Nonnull final Duration delay) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        Constraint.isNotNull(delay, "Cleanup task delay can not be null");
-        Constraint.isFalse(delay.isNegative() || delay.isZero(), "Cleanup task delay must be positive");
-        initialCleanupTaskDelay = delay;
-        
-    }
-    
-    /**
-     * Set the interval at which the cleanup task should run.
-     * 
-     * <p>Defaults to: 30 minutes.</p>
-     * 
-     * @param interval the interval to set
-     */
-    public void setCleanupTaskInterval(@Nonnull final Duration interval) {    
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        Constraint.isNotNull(interval, "Cleanup task interval may not be null");
-        Constraint.isFalse(interval.isNegative() || interval.isZero(), "Cleanup task interval must be positive");
-        
-        cleanupTaskInterval = interval;
-    }
-    
-    /**
-     * Set the {@link CriteriaSet} to IdentifierType strategy.
-     * 
-     * @param strategy the strategy.
-     */
-    public void setCriteriaToIdentifierStrategy(@Nonnull final Function<CriteriaSet, IdentifierType> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        criteriaToIdentifierStrategy =  Constraint.isNotNull(strategy,"Criteria to identifier strategy can not be null");
-    }
-    
-    /**
-     * Set the MetadataType to IdentifierType extraction time strategy.
-     * 
-     * @param strategy the strategy.
-     */
-    public void setIdentifierExtractionStrategy(@Nonnull final Function<MetadataType, IdentifierType> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        identifierExtractionStrategy = Constraint.isNotNull(strategy, "Identifier extraction strategy can not be null");
-    }
-
-    /**
-     * Set the metadata expiration time strategy.
-     * 
-     * @param strategy the strategy.
-     */
-    public void setMetadataExpirationTimeStrategy(
-            @Nonnull final BiFunction<MetadataType, Instant, Instant> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        metadataExpirationTimeStrategy = Constraint.isNotNull(strategy, "Metadata expiration strategy can not be null");
-    }
-    
-    /**
-     * Set the metadata filtering strategy.
-     * 
-     * @param strategy the metadata filtering strategy.
-     */
-    public void setMetadataFilterStrategy(
-            @Nonnull final BiFunction<MetadataType, MetadataFilterContext, MetadataType> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        metadataFilterStrategy = Constraint.isNotNull(strategy, "Metadata filter strategy can not be null");;
-    }
-    
-    /**
-     * Set the flag indicating whether idle entity data should be removed. 
-     * 
-     * @param flag true if idle entity data should be removed, false otherwise
-     */
-    public void setRemoveIdleEntityData(final boolean flag) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        removeIdleEntityData = flag;
-    }
-    
-    /**
-     * Set the maximum idle time for which the resolver will keep data for a given entityID, 
-     * before it is removed.
-     * 
-     * <p>Defaults to: 8 hours.</p>
-     * 
-     * @param max the maximum entity data idle time
-     */
-    protected void setMaxIdleEntityData(@Nonnull final Duration max) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
-        Constraint.isNotNull(max, "Max idle time cannot be null");
-        Constraint.isFalse(max.isNegative(), "Max idle time cannot be negative");
-
-        maxIdleEntityData = max;
-    }
-    
-
-    /**
-     *  Set the maximum cache duration for metadata.
-     *  
-     *  <p>Defaults to: 8 hours.</p>
-     *  
-     * @param duration the maximum cache duration
-     */
-    public void setMaxCacheDuration(@Nonnull final Duration duration) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
-        Constraint.isNotNull(duration, "Duration cannot be null");
-        Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
-        
-        maxCacheDuration = duration;
-    }
-
-
-    /**
-     *  Set the minimum cache duration for metadata.
-     *  
-     *  <p>Defaults to: 10 minutes.</p>
-     *  
-     * @param duration the minimum cache duration
-     */
-    public void setMinCacheDuration(@Nonnull final Duration duration) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
-        Constraint.isNotNull(duration, "Duration cannot be null");
-        Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
-        
-        minCacheDuration = duration;
-    }
-    
-    /**
-     * Set a hook to run before a metadata cache entry is removed from the cache.
-     * <p>The hook is required to gracefully handle null metadata lists.</p>
-     * 
-     * @param hook the hook to run.
-     */
-    public void setMetadataBeforeRemovalHook(
-            @Nullable final BiConsumer<List<MetadataType>, IdentifierType> hook) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        metadataBeforeRemovalHook = hook;
-    }
-    
-
-    /**
-     * Sets the delay factor used to compute the next refresh time. The delay must be between 0.0 and 1.0, exclusive.
-     * 
-     * <p>Defaults to:  0.75.</p>
-     * 
-     * @param factor delay factor used to compute the next refresh time
-     */
-    public void setRefreshDelayFactor(@Nonnull final Float factor) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
-        if (factor <= 0 || factor >= 1) {
-            throw new IllegalArgumentException("Refresh delay factor must be a number between 0.0 and 1.0, exclusive");
-        }
-
-        refreshDelayFactor = factor;
-    }
-    
-    /**
-     * Lookup the specified identifier from the index. The returned list will be a copy of what is stored in the backing
-     * index, and so the list is safe to be manipulated by callers.
-     * 
-     * @param identifier the entityID to lookup
-     * 
-     * @return list copy of indexed metadata, may be empty, will never be null
-     */
-    @Nonnull @NonnullElements private List<MetadataType> lookupIndexedIdentifier(
-            @Nonnull @NotEmpty final IdentifierType identifier) {
-        final List<MetadataType> metadata = backingStore.getIndexedValues().get(identifier);
-        if (metadata != null) {
-            return new ArrayList<>(metadata);
-        }
-        return Collections.emptyList();
-    }
-    
-    /**
-     * Get the backing store. Mainly used for accessing the store for tests.
-     * 
-     * @return the backing store.
-     */
-    @Nonnull public BackingStore<IdentifierType, MetadataType> getBackingStore() {
-        return backingStore;
-    }
-    
-    @Override
-    @Nonnull @NonnullElements public List<MetadataType> getOrFetchIfAbsent(
-            @Nonnull @NotEmpty final CriteriaSet criteria,
-            @Nonnull final Function<CriteriaSet, MetadataType> fetchFunction) throws MetadataCacheException {
-        
-        if (!isInitialized()) {
-            throw new MetadataCacheException("Metadata cache has not been initialized");
-        }
-        
-        final IdentifierType identifier = criteriaToIdentifierStrategy.apply(criteria);
-        log.debug("Resolved criteria to identifier: {}",  identifier);
-        
-        if (identifier != null) {        
-            final MetadataManagementData<IdentifierType> mgmtData = backingStore
-                    .computeManagementDataIfAbsent(identifier);
-            
-            // check metadata refresh is not needed before reading.
-            List<MetadataType> allMetadata = Collections.emptyList();
-            if (!shouldAttemptRefresh(mgmtData)) {
-                allMetadata = read(mgmtData, identifier);
-            }
-            if (allMetadata.isEmpty()) {
-                log.debug("Metadata for '{}' does not exist or is stale, attempting to fetch it", identifier);
-                fetch(mgmtData, identifier, fetchFunction, criteria);
-                return read(mgmtData, identifier);
-            } else {
-                log.debug("Metadata for '{}' found in cache", identifier);
-                return allMetadata;
-            }   
-        } else {
-            //TODO: see SAML version, could resolve from criteria even if no identifier.
-            log.debug("Identifier not resolvable from criteria, can not fetch metadata");
-            return Collections.emptyList();
-        }
-            
-    } 
-    
-    
-    /**
-     * Fetch metadata using the supplied fetch function, filter the metadata, then save it to the
-     * backing store - updating the management data at the same time.
-     * 
-     * @param mgmtData the metadata management data.
-     * @param identifier the identifier of the metadata to fetch.
-     * @param fetchFunction the function used to fetch the metadata.
-     * @param criteria the criteria used in determining how to fetch the metadata.
-     */
-    private void fetch(@Nonnull final MetadataManagementData<IdentifierType> mgmtData,
-            @Nonnull final IdentifierType identifier,
-            @Nonnull final Function<CriteriaSet, MetadataType> fetchFunction,
-            @Nonnull @NotEmpty final CriteriaSet criteria){
-        
-        final StampedLock sl = mgmtData.getStampLock();
-        long stamp = sl.writeLock();
-        
-        try {            
-            if (!shouldAttemptRefresh(mgmtData)){
-                // re-check another thread has not acquired this lock before hand - and therefore obtained
-                // the metadata.
-                List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
-                if (!allMetadata.isEmpty()) {
-                    log.debug("Metadata for '{}' was acquired while waiting for the write lock", identifier);
-                    return;                    
-                }
-            } else {
-                log.debug("Metadata for '{}' is stale and requires refreshing", identifier);
-            }
-            
-            final MetadataType resolvedMetadata = fetchFunction.apply(criteria);   
-            
-            if (resolvedMetadata != null) {
-
-                final MetadataType filteredMetadata = metadataFilterStrategy.apply(resolvedMetadata, newFilterContext());
-                
-                final IdentifierType extractedIdentifier = identifierExtractionStrategy.apply(filteredMetadata);
-                // equality method of the identifier is required to be implemented correctly.
-                if (!Objects.equals(identifier, extractedIdentifier)) {
-                    log.warn("New metadata's identifer '{}' does not match expected identifier '{}', will not process", 
-                            extractedIdentifier, identifier); 
-                    return;
-                }                   
-                
-                log.debug("Resolved metadata dynamically with identifier '{}'",extractedIdentifier);
-                invalidate(extractedIdentifier);
-                
-                backingStore.getOrderedValues().add(filteredMetadata);
-                // add new metadata to index
-                List<MetadataType> existingMetadata = backingStore.getIndexedValues().get(identifier);
-                if (existingMetadata == null) {
-                    existingMetadata = new ArrayList<>();
-                    backingStore.getIndexedValues().put(identifier, existingMetadata);
-                } else if (!existingMetadata.isEmpty()) {
-                    log.warn("Detected duplicate metadata for identifier: {}", identifier);
-                }
-                existingMetadata.add(filteredMetadata);
-                
-                final Instant now = Instant.now();
-                log.debug("For metadata '{}' expiration and refresh computation, 'now' is : {}", identifier, now);
-                
-                mgmtData.setLastUpdateTime(now);
-                
-                mgmtData.setExpirationTime(metadataExpirationTimeStrategy.apply(filteredMetadata, now));
-                log.debug("Computed metadata '{}' expiration time: {}", identifier, mgmtData.getExpirationTime());
-                
-                mgmtData.setRefreshTriggerTime(computeRefreshTriggerTime(mgmtData.getExpirationTime(), now));
-                log.debug("Computed metadata '{}' refresh trigger time: {}", identifier, mgmtData.getRefreshTriggerTime());
-                
-                log.info("Successfully loaded new Metadata with identifer '{}'", identifier);   
-
-            } else {
-                log.warn("Metadata for '{}' could not be resolved from source", identifier);
-            }
-            
-        } finally {
-            sl.unlock(stamp);
-        } 
-    }
-
-    /**
-     * Read metadata from the cache under the lock relating to the Identifier of the metadata to find.
-     *  
-     * <p>The first read attempt is optimistic and occurs without acquiring a read lock. The optimistic
-     * read is validated to ensure another thread has not acquired a write lock in the meantime. If it has,
-     * a write lock is obtained and a further read is attempted - to ensure a consistent state. This
-     * should improve efficiency given that metadata reads will vastly out number metadata fetch/writes.</p>
-     * 
-     * @param mgmtData the metadata managment data.
-     * @param identifier the metadata identifier to use as a key to fetch.
-     * 
-     * @return a list of metadata that matches the given key, an empty list of none are found.
-     */
-    @Nonnull private List<MetadataType> read(@Nonnull final MetadataManagementData<IdentifierType> mgmtData,
-            @Nonnull final IdentifierType identifier){
-        
-        // record access attempt.
-        mgmtData.recordEntityAccess();
-        // get optimistic lock
-        final StampedLock sl = mgmtData.getStampLock();
-        long stamp = sl.tryOptimisticRead();
-        
-        // A single optimistic read. A write lock will break this, but we check for 
-        // that using the validate method e.g. we keep track of locks, but we do not block a write lock
-        // here. Most metadata operations will be read-only. Reads are not expensive, but writes (lookups) are.
-        final List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
-        
-        if (sl.validate(stamp)) {
-            return allMetadata;
-        }
-        else {
-            // OK, was tampered with, lets do it with a hard read lock.
-            stamp = sl.readLock();            
-            try {
-                return lookupIndexedIdentifier(identifier);                  
-            } finally {
-                sl.unlock(stamp);
-            }           
-        }
-        
-    }
-    
-    
-    /**
-     * Get a new instance of {@link MetadataFilterContext} to be used when filtering metadata.
-     *
-     * <p>
-     * This default implementation just returns an empty context with as metadatasource for passing to 
-     * the filter strategy. The strategy can then add to the context as appropriate.
-     * </p>
-     *
-     * @return the new filter context instance
-     */
-    @Nonnull protected MetadataFilterContext newFilterContext() {
-        
-        final MetadataSource source = new MetadataSource();
-        source.setSourceId(getId());
-
-        final MetadataFilterContext context = new MetadataFilterContext();
-        context.add(source);
-        
-        return context;
-    }
-    
-   
-    /**
-     * Determine whether should attempt to refresh the metadata, based on stored refresh trigger time.
-     * 
-     * @param mgmtData the entity'd management data
-     * @return true if should attempt refresh, false otherwise
-     */
-    private boolean shouldAttemptRefresh(@Nonnull final MetadataManagementData<IdentifierType> mgmtData) {
-        return Instant.now().isAfter(mgmtData.getRefreshTriggerTime());
-        
-    }
-    
-    /**
-     * Remove/discard from the backing store all metadata for the entity with the given identifier.
-     * 
-     * @param identifier the ID of the metadata to remove
-     * @param backingStore the backing store instance to update
-     */
-    private void invalidate(@Nonnull final IdentifierType identifier) {
-        final Map<IdentifierType, List<MetadataType>> indexedDescriptors = backingStore.getIndexedValues();
-        final List<MetadataType> descriptors = indexedDescriptors.get(identifier);
-        if (metadataBeforeRemovalHook != null) {
-            // descriptors is nullable.
-            metadataBeforeRemovalHook.accept(descriptors, identifier);
-        }
-        if (descriptors != null) {
-            backingStore.getOrderedValues().removeAll(descriptors);
-        }
-        indexedDescriptors.remove(identifier);
-    }
-    
-    /**
-     * Compute the refresh trigger time.
-     * 
-     * @param expirationTime the time at which the metadata effectively expires
-     * @param nowDateTime the current date time instant
-     * 
-     * @return the time after which refresh attempt(s) should be made
-     */
-    @Nonnull private Instant computeRefreshTriggerTime(@Nullable final Instant expirationTime,
-            @Nonnull final Instant nowDateTime) {
-        
-        final long now = nowDateTime.toEpochMilli();
-
-        long expireInstant = 0;
-        if (expirationTime != null) {
-            expireInstant = expirationTime.toEpochMilli();
-        }
-        long refreshDelay = (long) ((expireInstant - now) * refreshDelayFactor);
-
-        // if the expiration time was null or the calculated refresh delay was less than the floor
-        // use the floor
-        if (refreshDelay < minCacheDuration.toMillis()) {
-            refreshDelay = minCacheDuration.toMillis();
-        }
-
-        return nowDateTime.plusMillis(refreshDelay);
-    }
-    
-    /**
-     * Create a wrapper for runnables that catches any throwable and logs it. Useful
-     * to prevent thread death from an uncaught exception.
-     * 
-     * @param action the runnable to wrap
-     * 
-     * @return the wrapped runnable.
-     */
-    @Nonnull private Runnable errorHandlingWrapper(@Nonnull final Runnable action) {
-        return () -> {
-            try {
-                action.run();
-            } catch (final Throwable e) {
-                log.error("{} Error executing thread task", getId(), e);
-            }
-        };
-    }
-    
-    /**
-     * Cleanup task that removes expired and idle metadata from the backing store.
-     */
-    private class ExpiredAndIdleMetadataCleanupTask implements Runnable {
-        
-        /** Logger. */
-        @Nonnull private final Logger log = LoggerFactory.getLogger(ExpiredAndIdleMetadataCleanupTask.class);
-
-        @Override
-        public void run() {
-            if (isDestroyed() || !isInitialized()) {
-                // just in case the metadata resolver was destroyed before this task runs, 
-                // or if it somehow is being called on a non-successfully-inited cache instance.
-                log.debug("BackingStoreCleanupSweeper will not run because: inited: {}, destroyed: {}",
-                        isInitialized(), isDestroyed());
-                return;
-            }
-            log.info("Running metadata cleanup background timer task {}",this);
-            removeExpiredAndIdleMetadata();
-        }
-        
-        /**
-         *  Purge metadata which is either 1) expired or 2) (if {@link #isRemoveIdleEntityData()} is true) 
-         *  which hasn't been accessed within the last {@link #getMaxIdleEntityData()} duration.
-         */
-        private void removeExpiredAndIdleMetadata() {
-            final Instant now = Instant.now();
-            final Instant earliestValidLastAccessed = now.minus(maxIdleEntityData);
-            
-            final BackingStore<IdentifierType, MetadataType> store = getBackingStore();
-            final Set<IdentifierType> ids = new HashSet<>();
-            ids.addAll(store.getIndexedValues().keySet());
-            ids.addAll(store.getManagementDataIdentifiers());
-            
-            for (final IdentifierType identifier : ids) {
-                final MetadataManagementData<IdentifierType> mgmtData = store.computeManagementDataIfAbsent(identifier);
-                final long stamp = mgmtData.getStampLock().writeLock();
-                try {                                       
-                    if (isRemoveData(mgmtData, now, earliestValidLastAccessed)) {
-                        invalidate(identifier);
-                        store.removeManagementData(identifier);
-                    }                    
-                } finally {
-                    mgmtData.getStampLock().unlock(stamp);
-                }
-            }
-            
-        }
-        
-        /**
-         * Determine whether metadata should be removed based on expiration and idle time data.
-         * 
-         * @param mgmtData the management data instance for the entity
-         * @param now the current time
-         * @param earliestValidLastAccessed the earliest last accessed time which would be valid
-         * 
-         * @return true if the entity is expired or exceeds the max idle time, false otherwise
-         */
-        private boolean isRemoveData(@Nonnull final MetadataManagementData<IdentifierType> mgmtData, 
-                @Nonnull final Instant now, @Nonnull final Instant earliestValidLastAccessed) {
-            if (removeIdleEntityData && mgmtData.getLastAccessedTime().isBefore(earliestValidLastAccessed)) {
-                log.debug("Metadata exceeds maximum idle time, removing: {}", mgmtData.getID());
-                return true;
-            } else if (now.isAfter(mgmtData.getExpirationTime())) {
-                log.debug("Entity metadata is expired, removing: {}", mgmtData.getID());
-                return true;
-            } else {
-                return false;
-            }
-        }
-        
-    }
-
-}
-
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
index fa1fb78..d59e561 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
@@ -27,12 +27,20 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
-/** Strategy for computing an expiry time for {@link OIDCProviderMetadata}.  Defaults to now plus the provided expiry time.*/
-public class DefaultOIDCProviderMetadataExpirationTimeStrategy implements BiFunction<OIDCProviderMetadata, Instant, Instant> {
+/** Strategy for computing an expiry time for {@link OIDCProviderMetadata}.  
+ * Defaults to now plus the provided expiry time.*/
+public class DefaultOIDCProviderMetadataExpirationTimeStrategy 
+                        implements BiFunction<OIDCProviderMetadata, Instant, Instant> {
     
     /** How long after now should the metadata expire.*/
     @Nonnull private final Duration expiryDuration;
     
+    /**
+     * 
+     * Constructor.
+     *
+     * @param duration the expiry duration.
+     */
     public DefaultOIDCProviderMetadataExpirationTimeStrategy(@Nonnull final Duration duration) {
         expiryDuration = Constraint.isNotNull(duration, "Expiry duration can not be null");
     }
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCache.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCache.java
new file mode 100644
index 0000000..4445d9c
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCache.java
@@ -0,0 +1,434 @@
+/*
+ * 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.oidc.metadata.cache.impl;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.StampedLock;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.oidc.metadata.DynamicBackingStore;
+import net.shibboleth.oidc.metadata.MetadataManagementData;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+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.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * A metadata cache implementation that supports 'read-through' semantics. Does not support 'refresh-ahead' semantics
+ * for loading about to expire values asynchronously ahead of time.
+ *
+ * @param <IdentifierType> the metadata identifier type.
+ * @param <MetadataType> the metadata type.
+ */
+public class DynamicMetadataCache<IdentifierType, MetadataType> 
+                            extends AbstractMetadataCache<IdentifierType, MetadataType> {
+    
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(DynamicMetadataCache.class);
+    
+    /** The interval at which the cleanup task should run. */
+    @NonnullAfterInit private Duration cleanupTaskInterval;
+    
+    /** The initial cleanup task delay.*/
+    @NonnullAfterInit private Duration initialCleanupTaskDelay;      
+    
+    /** The maximum idle time for which the resolver will keep data for a given entityID, 
+     * before it is removed. */
+    @NonnullAfterInit private Duration maxIdleEntityData;
+    
+    /** Flag indicating whether idle entity data should be removed. */
+    private boolean removeIdleEntityData;
+    
+    /** The function to use to fetch/load metadata if either none exists, or the existing is stale.*/
+    @Nonnull private final Function<CriteriaSet, MetadataType> dynamicFetchStrategy;
+    
+    /** 
+     * Constructor.
+     *
+     * @param store the backing store to use as the cache store.
+     * @param metadataFetchStrategy the strategy used to fetch metadata using the 'read-through' semantics. 
+     */
+    public DynamicMetadataCache(@Nonnull final DynamicBackingStore<IdentifierType, MetadataType> store,
+            @Nonnull final Function<CriteriaSet, MetadataType> metadataFetchStrategy) {
+        super(store);
+        dynamicFetchStrategy = 
+                Constraint.isNotNull(metadataFetchStrategy, "Dynamic Metadata fetch strategy can not be null");
+    }
+    
+    /**
+     * 
+     * Package private constructor. Used mainly for testing.
+     *
+     * @param store the backing store to use as the cache store.
+     * @param metadataFetchStrategy the strategy used to fetch metadata using the 'read-through' semantics. 
+     * @param executor override the executor service.
+     */
+    protected DynamicMetadataCache(@Nonnull final DynamicBackingStore<IdentifierType, MetadataType> store, 
+            @Nonnull final Function<CriteriaSet, MetadataType> metadataFetchStrategy,
+            @Nullable final ScheduledExecutorService executor) { 
+        super(store, executor);
+        dynamicFetchStrategy = 
+                Constraint.isNotNull(metadataFetchStrategy, "Dynamic Metadata fetch strategy can not be null");
+    }
+    
+    /**
+     * Set the initial cleanup task delay.
+     * 
+     * @param delay The initialCleanupTaskDelay to set.
+     */
+    public void setInitialCleanupTaskDelay(@Nonnull final Duration delay) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        Constraint.isNotNull(delay, "Cleanup task delay can not be null");
+        Constraint.isFalse(delay.isNegative() || delay.isZero(), "Cleanup task delay must be positive");
+        initialCleanupTaskDelay = delay;
+        
+    }
+    
+    /**
+     * Set the interval at which the cleanup task should run.
+     * 
+     * <p>Defaults to: 30 minutes.</p>
+     * 
+     * @param interval the interval to set
+     */
+    public void setCleanupTaskInterval(@Nonnull final Duration interval) {    
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        Constraint.isNotNull(interval, "Cleanup task interval may not be null");
+        Constraint.isFalse(interval.isNegative() || interval.isZero(), "Cleanup task interval must be positive");
+        
+        cleanupTaskInterval = interval;
+    }
+    
+    /**
+     * Set the flag indicating whether idle entity data should be removed. 
+     * 
+     * @param flag true if idle entity data should be removed, false otherwise
+     */
+    public void setRemoveIdleEntityData(final boolean flag) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        removeIdleEntityData = flag;
+    }
+    
+    /**
+     * Set the maximum idle time for which the resolver will keep data for a given entityID, 
+     * before it is removed.
+     * 
+     * <p>Defaults to: 8 hours.</p>
+     * 
+     * @param max the maximum entity data idle time
+     */
+    public void setMaxIdleEntityData(@Nonnull final Duration max) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        Constraint.isNotNull(max, "Max idle time cannot be null");
+        Constraint.isFalse(max.isNegative(), "Max idle time cannot be negative");
+
+        maxIdleEntityData = max;
+    }
+    
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();  
+        
+        if ( maxIdleEntityData == null  || cleanupTaskInterval == null || initialCleanupTaskDelay == null) {
+            throw new ComponentInitializationException("Dynamic metadata cache not property initialized"); 
+        }
+        
+        getExecutorService().scheduleAtFixedRate(
+                errorHandlingWrapper(new ExpiredAndIdleMetadataCleanupTask()), initialCleanupTaskDelay.toMillis(), 
+                cleanupTaskInterval.toMillis(), TimeUnit.MILLISECONDS);
+    }
+        
+    /**
+     * {@inheritDoc}
+     * 
+     * <p>Cast the backing store to the type used by this cache type.</p>
+     */
+    @Override @Nonnull protected DynamicBackingStore<IdentifierType, MetadataType> getBackingStore() {
+        return (DynamicBackingStore<IdentifierType, MetadataType>) super.getBackingStore();
+    }
+    
+    
+    @Override
+    @Nonnull @NonnullElements public List<MetadataType> get(
+            @Nonnull @NotEmpty final CriteriaSet criteria) throws MetadataCacheException {
+        
+        if (!isInitialized()) {
+            throw new MetadataCacheException("Metadata cache has not been initialized");
+        }
+        
+        final IdentifierType identifier = getCriteriaToIdentifierStrategy().apply(criteria);
+        log.debug("{} Resolved criteria to identifier: {}", getLogPrefix(), identifier);
+        
+        if (identifier != null) {       
+            //TODO check we can do this here, as another thread could change this concurrently?
+            final MetadataManagementData<IdentifierType> mgmtData = getBackingStore()
+                    .computeManagementDataIfAbsent(identifier);
+            
+            // check metadata refresh is not needed before reading.
+            List<MetadataType> allMetadata = Collections.emptyList();
+            if (!shouldAttemptRefresh(mgmtData)) {
+                allMetadata = read(mgmtData, identifier);
+            }
+            if (allMetadata.isEmpty()) {
+                log.debug("Metadata for '{}' does not exist or is stale, attempting to fetch it", identifier);
+                fetch(mgmtData, identifier, criteria);
+                return read(mgmtData, identifier);
+            } else {
+                log.debug("Metadata for '{}' found in cache", identifier);
+                return allMetadata;
+            }   
+        } else {
+            //TODO: see SAML version, could resolve from criteria even if no identifier.
+            log.debug("Identifier not resolvable from criteria, can not fetch metadata");
+            return Collections.emptyList();
+        }
+            
+    }
+    
+    /**
+     * Fetch metadata using the supplied fetch function, filter the metadata, then save it to the
+     * backing store - updating the management data at the same time.
+     * 
+     * @param mgmtData the metadata management data.
+     * @param identifier the identifier of the metadata to fetch.
+     * @param criteria the criteria used in determining how to fetch the metadata.
+     */
+    private void fetch(@Nonnull final MetadataManagementData<IdentifierType> mgmtData,
+            @Nonnull final IdentifierType identifier,
+            @Nonnull @NotEmpty final CriteriaSet criteria){
+        
+        final StampedLock sl = mgmtData.getStampLock();
+        final long stamp = sl.writeLock();
+        
+        try {            
+            if (!shouldAttemptRefresh(mgmtData)){
+                // re-check another thread has not acquired this lock before hand - and therefore obtained
+                // the metadata.
+                final List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
+                if (!allMetadata.isEmpty()) {
+                    log.debug("{} Metadata for '{}' was acquired while waiting for the write lock", 
+                            getLogPrefix(), identifier);
+                    return;                    
+                }
+            } else {
+                log.debug("{} Metadata for '{}' is stale and requires refreshing", getLogPrefix(), identifier);
+            }
+            
+            final MetadataType resolvedMetadata = dynamicFetchStrategy.apply(criteria);   
+            
+            if (resolvedMetadata != null) {
+                storeNewMetadata(mgmtData, resolvedMetadata, identifier);
+            } else {
+                log.warn("{} Metadata for '{}' could not be resolved from source", getLogPrefix(), identifier);
+            }
+            
+        } finally {
+            sl.unlock(stamp);
+        } 
+    }
+    
+    /**
+     * Process the new metadata as follows:
+     * <ol>
+     * <li>Apply the metadata filtering strategy configured.</li>
+     * <li>Check the fetched metadata's identifier matches that expected.</li>
+     * <li>Write the metadata to the backing store (cache).</li>
+     * <li>Update the metadata's management information with new refresh information.</li>
+     * </ol>
+     * @param mgmtData
+     * @param metadata
+     * @param expectedIdentifier
+     */
+    private void storeNewMetadata(@Nonnull final MetadataManagementData<IdentifierType> mgmtData,
+            @Nonnull final MetadataType metadata,
+            @Nonnull final IdentifierType expectedIdentifier) {
+        
+        final MetadataType filteredMetadata = getMetadataFilterStrategy().apply(metadata, newFilterContext());
+        
+        final IdentifierType extractedIdentifier = getIdentifierExtractionStrategy().apply(filteredMetadata);
+        // equality method of the identifier is required to be implemented correctly.
+        if (!Objects.equals(expectedIdentifier, extractedIdentifier)) {
+            log.warn("{} New metadata's identifer '{}' does not match expected identifier '{}', will not process", 
+                    getLogPrefix(), extractedIdentifier, expectedIdentifier); 
+            return;
+        }                   
+        
+        log.debug("{} Resolved metadata with identifier '{}'",getLogPrefix(), extractedIdentifier);
+       
+        writeToBackingStore(filteredMetadata);
+        
+        final Instant now = Instant.now();
+        log.debug("{} For metadata '{}' expiration and refresh computation, 'now' is : {}", 
+                getLogPrefix(), extractedIdentifier, now);
+        
+        mgmtData.setLastUpdateTime(now);
+        
+        mgmtData.setExpirationTime(getMetadataExpirationTimeStrategy().apply(filteredMetadata, now));
+        log.debug("{} Computed metadata '{}' expiration time: {}", getLogPrefix(), 
+                extractedIdentifier, mgmtData.getExpirationTime());
+        
+        mgmtData.setRefreshTriggerTime(computeRefreshTriggerTime(mgmtData.getExpirationTime(), now));
+        log.debug("{} Computed metadata '{}' refresh trigger time: {}", getLogPrefix(), 
+                extractedIdentifier, mgmtData.getRefreshTriggerTime());
+        
+        log.info("{} Successfully loaded new Metadata with identifer '{}'", getLogPrefix(), extractedIdentifier);   
+    }
+    
+    /**
+     * Read metadata from the cache under the lock relating to the Identifier of the metadata to find.
+     *  
+     * <p>The first read attempt is optimistic and occurs without acquiring a read lock. The optimistic
+     * read is validated to ensure another thread has not acquired a write lock in the meantime. If it has,
+     * a write lock is obtained and a further read is attempted - to ensure a consistent state. This
+     * should improve efficiency given that metadata reads will vastly out number metadata fetch/writes.</p>
+     * 
+     * @param mgmtData the metadata managment data.
+     * @param identifier the metadata identifier to use as a key to fetch.
+     * 
+     * @return a list of metadata that matches the given key, an empty list of none are found.
+     */
+    //TODO: Support isValid checks on returned metadata? see AbstractMetadataResolver#lookupEntityID
+    @Nonnull private List<MetadataType> read(@Nonnull final MetadataManagementData<IdentifierType> mgmtData,
+            @Nonnull final IdentifierType identifier){
+        
+        // record access attempt.
+        mgmtData.recordEntityAccess();
+        // get optimistic lock
+        final StampedLock sl = mgmtData.getStampLock();
+        long stamp = sl.tryOptimisticRead();
+        
+        // A single optimistic read. A write lock will break this, but we check for 
+        // that using the validate method e.g. we keep track of locks, but we do not block a write lock
+        // here. Most metadata operations will be read-only. Reads are not expensive, but writes (lookups) are.
+        final List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
+        
+        if (sl.validate(stamp)) {
+            return allMetadata;
+        } else {
+            // OK, was tampered with, lets do it with a hard read lock.
+            stamp = sl.readLock();            
+            try {
+                return lookupIndexedIdentifier(identifier);                  
+            } finally {
+                sl.unlock(stamp);
+            }           
+        }
+        
+    }
+    
+    /**
+     * Cleanup task that removes expired and idle metadata from the backing store.
+     */
+    private class ExpiredAndIdleMetadataCleanupTask implements Runnable {
+        
+        /** Logger. */
+        @Nonnull private final Logger log = LoggerFactory.getLogger(ExpiredAndIdleMetadataCleanupTask.class);
+
+        @Override
+        public void run() {
+            if (isDestroyed() || !isInitialized()) {
+                // just in case the metadata resolver was destroyed before this task runs, 
+                // or if it somehow is being called on a non-successfully-inited cache instance.
+                log.debug("BackingStoreCleanupSweeper will not run because: inited: {}, destroyed: {}",
+                        isInitialized(), isDestroyed());
+                return;
+            }
+            log.info("Running metadata cleanup background timer task {}",this);
+            removeExpiredAndIdleMetadata();
+        }
+        
+        /**
+         *  Purge metadata which is either 1) expired or 2) (if {@link #isRemoveIdleEntityData()} is true) 
+         *  which hasn't been accessed within the last {@link #getMaxIdleEntityData()} duration.
+         */
+        private void removeExpiredAndIdleMetadata() {
+            final Instant now = Instant.now();
+            final Instant earliestValidLastAccessed = now.minus(maxIdleEntityData);
+            
+            final DynamicBackingStore<IdentifierType, MetadataType> store = getBackingStore();
+            final Set<IdentifierType> ids = new HashSet<>();
+            ids.addAll(store.getIndexedValues().keySet());
+            ids.addAll(store.getManagementDataIdentifiers());
+            
+            for (final IdentifierType identifier : ids) {
+                final MetadataManagementData<IdentifierType> mgmtData = store.computeManagementDataIfAbsent(identifier);
+                final long stamp = mgmtData.getStampLock().writeLock();
+                try {                                       
+                    if (isRemoveData(mgmtData, now, earliestValidLastAccessed)) {
+                        invalidate(identifier);
+                        store.removeManagementData(identifier);
+                    }                    
+                } finally {
+                    mgmtData.getStampLock().unlock(stamp);
+                }
+            }
+            
+        }
+        
+        /**
+         * Determine whether metadata should be removed based on expiration and idle time data.
+         * 
+         * @param mgmtData the management data instance for the entity
+         * @param now the current time
+         * @param earliestValidLastAccessed the earliest last accessed time which would be valid
+         * 
+         * @return true if the entity is expired or exceeds the max idle time, false otherwise
+         */
+        private boolean isRemoveData(@Nonnull final MetadataManagementData<IdentifierType> mgmtData, 
+                @Nonnull final Instant now, @Nonnull final Instant earliestValidLastAccessed) {
+            if (removeIdleEntityData && mgmtData.getLastAccessedTime().isBefore(earliestValidLastAccessed)) {
+                log.debug("Metadata exceeds maximum idle time, removing: {}", mgmtData.getID());
+                return true;
+            } else if (now.isAfter(mgmtData.getExpirationTime())) {
+                log.debug("Entity metadata is expired, removing: {}", mgmtData.getID());
+                return true;
+            } else {
+                return false;
+            }
+        }
+        
+    }
+
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/MetadataCacheBuilder.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/MetadataCacheBuilder.java
index d8da9a0..063a0cf 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/MetadataCacheBuilder.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/MetadataCacheBuilder.java
@@ -29,8 +29,11 @@ import javax.annotation.Nullable;
 
 import org.springframework.beans.factory.config.AbstractFactoryBean;
 
+import net.shibboleth.oidc.metadata.cache.CacheLoadingContext;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
 import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
+import net.shibboleth.utilities.java.support.component.DestructableComponent;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
@@ -40,7 +43,7 @@ import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
  * @param <T> the metadata identifier/key
  * @param <U> the metadata type. 
  */
-public abstract class MetadataCacheBuilder<T,U> extends AbstractFactoryBean<DefaultMetadataCache<T, U>>{
+public abstract class MetadataCacheBuilder<T,U> extends AbstractFactoryBean<MetadataCache<U>>{
     
     /** Maximum cache duration. */
     @Nonnull private Duration maxCacheDuration;
@@ -48,6 +51,15 @@ public abstract class MetadataCacheBuilder<T,U> extends AbstractFactoryBean<Defa
     /** Minimum cache duration. */
     @Nonnull private Duration minCacheDuration;
     
+    /**
+     * Refresh interval used when metadata does not contain any validUntil or cacheDuration information. Default value:
+     * 4 hours
+     */
+    @Nonnull @Positive private Duration maxRefreshDelay;
+
+    /** Floor, in milliseconds, for the refresh interval. Default value: 5 minutes */
+    @Nonnull @Positive private Duration minRefreshDelay;
+    
     /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
     @Positive private Float refreshDelayFactor;
     
@@ -62,49 +74,178 @@ public abstract class MetadataCacheBuilder<T,U> extends AbstractFactoryBean<Defa
     @Nonnull private Duration initialCleanupTaskDelay;
     
     /** Strategy used to extract an identifier from the given metadata.*/
-    @Nullable private Function<@Nonnull U, @Nonnull T> identifierExtractionStrategy;
+    @Nullable private Function<U, T> identifierExtractionStrategy;
     
     /** Strategy used to compute an expiration time. */
-    @Nullable private BiFunction<@Nonnull U, @Nonnull Instant, @Nonnull Instant> metadataExpirationTimeStrategy;
+    @Nullable private BiFunction<U, Instant, Instant> metadataExpirationTimeStrategy;
     
     /** Map criteria to identifiers to use as keys to the backing store.*/
-    @Nullable private Function<@Nonnull CriteriaSet, @Nonnull T> criteriaToIdentifierStrategy;
+    @Nullable private Function<CriteriaSet, T> criteriaToIdentifierStrategy;
     
     /** A strategy to filter metadata. */
-    @Nonnull private BiFunction<@Nonnull U, @Nonnull MetadataFilterContext , @Nonnull U> metadataFilterStrategy;
+    @Nonnull private BiFunction<U, MetadataFilterContext , U> metadataFilterStrategy;
     
     /** 
      * A hook that is executed just before a cache entry will been removed/invalidated/evicted.
      * The metadata list could be {@literal null}, the identifier is never {@literal null}.
      */
-    @Nullable private BiConsumer<@Nullable List<U>, @Nonnull T> metadataBeforeRemovalHook;
+    @Nullable private BiConsumer<List<U>, T> metadataBeforeRemovalHook;
     
     /** Flag indicating whether idle entity data should be removed. */
     private boolean removeIdleEntityData;
     
+    /** The function to use to fetch metadata if either none exists, or the existing is stale.*/
+    @Nullable private Function<CriteriaSet, U> fetchStrategy;
+    
+    /** How to parse the loaded metadata from the loadingStrategy into a usable metadatatype.*/
+    @Nullable private Function<byte[], List<U>> parsingStrategy;
+    
+    /** The function to use to load metadata. Applicable for {@link BatchMetadataCache} types.*/
+    @Nullable private Function<CacheLoadingContext, byte[]> loadingStrategy;
+    
+    /** 
+     * Should the metadata be background refreshed ahead of time. If true, a {@link BatchMetadataCache}
+     * will be constructed.
+     */
+    private boolean refreshAhead;
+    
     
+    
+    /** Constructor.*/
     protected MetadataCacheBuilder() {
-        //defaults
+        // defaults
         maxCacheDuration = Duration.ofHours(8);
         minCacheDuration = Duration.ofMinutes(10);
         maxIdleEntityData = Duration.ofHours(8);
         refreshDelayFactor = 0.75f;
         cleanupTaskInterval = Duration.ofMinutes(30);
         initialCleanupTaskDelay = Duration.ofMinutes(1);
+        maxRefreshDelay = Duration.ofHours(4);
+        minRefreshDelay = Duration.ofMinutes(5);
         removeIdleEntityData = true;
         // create a default direct in/out filter
         metadataFilterStrategy = (metadata, context) -> metadata;
     }
     
+    
+    /**
+     * Get the min delay to wait before refreshing metadata.
+     * 
+     * @return the min delay.
+     */
+    @Nonnull protected Duration getMinRefreshDelay() {
+        return minRefreshDelay;
+    }
+    
+    /**
+     * Get the max delay to wait before refreshing metadata.
+     * 
+     * @return the max delay.
+     */
+    @Nonnull protected Duration getMaxRefreshDelay() {
+        return maxRefreshDelay;
+    }
+    
+    /**
+     * Set he raw batch metadata to metadata type parsing strategy.
+     * 
+     * @param strategy the parsing strategy.
+     */
+    public void setParsingStrategy(@Nonnull final Function<byte[], List<U>> strategy) {
+        parsingStrategy = Constraint.isNotNull(strategy, "Parsing strategy can not be null");
+    }
+    
+    /**
+     * Get the raw batch metadata to metadata type parsing strategy.
+     * 
+     * @return the parsing strategy.
+     */
+    @Nullable protected Function<byte[], List<U>> getParsingStrategy() {
+        return parsingStrategy;
+    }
+    
+    /**
+     * Set the batch metadata loading strategy, applies to the  {@link BatchMetadataCache} type.
+     * 
+     * @param strategy the strategy to set.
+     */
+    public void setLoadingStrategy(@Nonnull final Function<CacheLoadingContext, byte[]> strategy) {
+        loadingStrategy =  Constraint.isNotNull(strategy,"Batch metadata loading strategy can not be null");
+    }
+    
+    /**
+     * Get the batch metadata loading strategy. 
+     * 
+     * @return the strategy.
+     */
+    @Nullable protected Function<CacheLoadingContext, byte[]> getLoadingStrategy() {
+        return loadingStrategy;
+    }
+    
+    /**
+     * Sets the minimum amount of time between refreshes.
+     * 
+     * @param delay minimum amount of time between refreshes
+     */
+    public void setMinRefreshDelay(@Positive @Nonnull final Duration delay) {
+        Constraint.isFalse(delay == null || delay.isNegative(), "Minimum refresh delay must be greater than 0");
+        minRefreshDelay = delay;
+    }
+    
+    /**
+     * Sets the maximum amount of time between refresh intervals.
+     * 
+     * @param delay maximum amount of time, in milliseconds, between refresh intervals
+     */
+    public void setMaxRefreshDelay(@Positive @Nonnull final Duration delay) {        
+        Constraint.isFalse(delay == null || delay.isNegative(), "Maximum refresh delay must be greater than 0");
+        maxRefreshDelay = delay;
+    }
+    
+    /**
+     * Should the metadata be refreshed ahead of time?
+     * 
+     * @return true if the metadata should be refreshed ahead of time. False otherwise.
+     */
+    public boolean isRefreshAhead() {
+        return refreshAhead;
+    }
+    
+    /**
+     * Should the metadata be refreshed ahead of time.
+     * @param refresh
+     */
+    public void setRefreshAhead(final boolean refresh) {
+        refreshAhead = refresh;
+    }
+    
+    /**
+     * Set the metadata fetching strategy.
+     * 
+     * @param strategy the strategy to use.
+     */
+    public void setFetchStrategy(@Nonnull final Function<CriteriaSet, U> strategy) {
+        fetchStrategy = Constraint.isNotNull(strategy, "Metadata fetch strategy can not be null");
+    }
+    
+    /**
+     * Get the metadata fetching strategy.
+     * 
+     * @return the fetching strategy.
+     */
+    @Nullable protected Function<CriteriaSet, U> getFetchStrategy() {
+        return fetchStrategy;
+    }
+    
     /**
      * {@inheritDoc}
      * 
-     * Call destroy on the default cache implementation.
+     * Call destroy if the cache implementation supports it.
      */
     @Override protected void destroyInstance(
-            @Nullable DefaultMetadataCache<T, U> instance) throws Exception {
-        if (instance != null) {
-            instance.destroy();
+            final @Nullable MetadataCache<U> instance) throws Exception {
+        if (instance instanceof DestructableComponent) {
+            ((DestructableComponent)instance).destroy();
         }
     }
     
@@ -230,13 +371,14 @@ public abstract class MetadataCacheBuilder<T,U> extends AbstractFactoryBean<Defa
      * @param strategy the strategy to set.
      */
     public void setCriteriaToIdentifierStrategy(@Nonnull final Function<CriteriaSet, T> strategy) {
-        criteriaToIdentifierStrategy =  Constraint.isNotNull(strategy,"Criteria to identifier strategy can not be null");
+        criteriaToIdentifierStrategy =  
+                Constraint.isNotNull(strategy,"Criteria to identifier strategy can not be null");
     }
     
     /**
      * Get the criteria set to identifier lookup strategy.
      * 
-     * @param strategy the strategy.
+     * @return strategy the strategy.
      */
     @Nullable protected Function<CriteriaSet, T> getCriteriaToIdentifierStrategy() {
         return criteriaToIdentifierStrategy;
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/OIDCProviderMetadataCacheFactoryBean.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/OIDCProviderMetadataCacheFactoryBean.java
index 51ca0ab..179bcf6 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/OIDCProviderMetadataCacheFactoryBean.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/OIDCProviderMetadataCacheFactoryBean.java
@@ -3,38 +3,63 @@ package net.shibboleth.oidc.metadata.cache.impl;
 import com.nimbusds.oauth2.sdk.id.Issuer;
 import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
-import net.shibboleth.oidc.metadata.impl.DefaultBackingStore;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
+import net.shibboleth.oidc.metadata.impl.DefaultBatchBackingStore;
+import net.shibboleth.oidc.metadata.impl.DefaultDynamicBackingStore;
 
 /**
- * Factory bean to create an OIDC specific metadata cache.
+ * Factory bean to create an OIDC specific metadata cache. Either a {@link DynamicMetadataCache} 
+ * or {@link BatchMetadataCache} is created depending on the cache properties configured.
  */
 public class OIDCProviderMetadataCacheFactoryBean extends MetadataCacheBuilder<Issuer, OIDCProviderMetadata> {
 
     @SuppressWarnings("rawtypes")
     @Override
-    public Class<DefaultMetadataCache> getObjectType() {
-        return DefaultMetadataCache.class;
+    public Class<MetadataCache> getObjectType() {
+        return MetadataCache.class;
     }
 
     @Override
-    protected DefaultMetadataCache<Issuer, OIDCProviderMetadata> createInstance() throws Exception {
+    protected MetadataCache<OIDCProviderMetadata> createInstance() throws Exception {
         
-        final DefaultMetadataCache<Issuer, OIDCProviderMetadata> cache = new DefaultMetadataCache<>(
-                new DefaultBackingStore<>(getMaxCacheDuration()));
-        cache.setMinCacheDuration(getMinCacheDuration());
-        cache.setRefreshDelayFactor(getRefreshDelayFactor());
-        cache.setMaxIdleEntityData(getMaxIdleEntityData());
-        cache.setMetadataExpirationTimeStrategy(getMetadataExpirationTimeStrategy());
-        cache.setIdentifierExtractionStrategy(getIdentifierExtractionStrategy());
-        cache.setCriteriaToIdentifierStrategy(getCriteriaToIdentifierStrategy());
-        cache.setCleanupTaskInterval(getCleanupTaskInterval());
-        cache.setRemoveIdleEntityData(isRemoveIdleEntityData());
-        cache.setInitialCleanupTaskDelay(getInitialCleanupTaskDelay());
-        cache.setMetadataFilterStrategy(getMetadataFilterStrategy());
-        cache.setMetadataBeforeRemovalHook(getMetadataBeforeRemovalHook());
-        cache.setId("OIDCProviderMetadataCache");
-        cache.initialize();
-        return cache;
+        // refresh ahead is only supported by the refreshable 
+        if (!isRefreshAhead()) {
+            final DynamicMetadataCache<Issuer, OIDCProviderMetadata> cache = new DynamicMetadataCache<>(
+                    new DefaultDynamicBackingStore<>(getMaxCacheDuration()), getFetchStrategy());
+            cache.setMinCacheDuration(getMinCacheDuration());
+           // cache.setMaxCacheDuration(getMaxCacheDuration());
+            cache.setRefreshDelayFactor(getRefreshDelayFactor());
+            cache.setMaxIdleEntityData(getMaxIdleEntityData());
+            cache.setMetadataExpirationTimeStrategy(getMetadataExpirationTimeStrategy());
+            cache.setIdentifierExtractionStrategy(getIdentifierExtractionStrategy());
+            cache.setCriteriaToIdentifierStrategy(getCriteriaToIdentifierStrategy());
+            cache.setCleanupTaskInterval(getCleanupTaskInterval());
+            cache.setRemoveIdleEntityData(isRemoveIdleEntityData());
+            cache.setInitialCleanupTaskDelay(getInitialCleanupTaskDelay());
+            cache.setMetadataFilterStrategy(getMetadataFilterStrategy());
+            cache.setMetadataBeforeRemovalHook(getMetadataBeforeRemovalHook());
+            cache.setId("OIDCProviderDynamicMetadataCache");
+            cache.initialize();
+            return cache;
+        } else {
+            //TODO why have the backing stores as interfaces if you are setting the concrete type here? inject somehow?
+            final BatchMetadataCache<Issuer, OIDCProviderMetadata> cache = 
+                    new BatchMetadataCache<>(
+                    new DefaultBatchBackingStore<>(), getLoadingStrategy(), getParsingStrategy());
+            cache.setMinCacheDuration(getMinCacheDuration());
+           // cache.setMaxCacheDuration(getMaxCacheDuration());
+            cache.setMinRefreshDelay(getMinRefreshDelay());
+            cache.setMaxRefreshDelay(getMaxRefreshDelay());
+            cache.setRefreshDelayFactor(getRefreshDelayFactor());
+            cache.setMetadataExpirationTimeStrategy(getMetadataExpirationTimeStrategy());
+            cache.setIdentifierExtractionStrategy(getIdentifierExtractionStrategy());
+            cache.setCriteriaToIdentifierStrategy(getCriteriaToIdentifierStrategy());
+            cache.setMetadataFilterStrategy(getMetadataFilterStrategy());
+            cache.setMetadataBeforeRemovalHook(getMetadataBeforeRemovalHook());
+            cache.setId("OIDCProviderRefreshableMetadataCache");
+            cache.initialize();
+            return cache;
+        }
     }
    
 
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractBackingStore.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractBackingStore.java
new file mode 100644
index 0000000..222edc1
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractBackingStore.java
@@ -0,0 +1,67 @@
+/*
+ * 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.oidc.metadata.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.oidc.metadata.BackingStore;
+
+/**
+ * A base backing store implementation.
+ *
+ * @param <I> The metadata identifier type.
+ * @param <T> The metadata type.
+ */
+ at ThreadSafe
+public abstract class AbstractBackingStore<I, T> implements BackingStore<I, T> {
+    
+    /** Index of entity IDs to their descriptors. */
+    private Map<I, List<T>> indexedValues;
+
+    /** Ordered list of entity descriptors. */
+    private List<T> orderedValues;
+
+    
+    /**
+     * Constructor.
+     */
+    AbstractBackingStore() {
+        super();
+        indexedValues = new ConcurrentHashMap<>();
+        orderedValues = new ArrayList<>();               
+    }
+
+    @Override
+    @Nonnull public Map<I, List<T>> getIndexedValues() {
+        return indexedValues;
+    }
+
+    @Override
+    @Nonnull public List<T> getOrderedValues() {
+        return orderedValues;
+    }
+    
+  
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java
index 2803734..5262681 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java
@@ -1,3 +1,20 @@
+/*
+ * 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.oidc.metadata.impl;
 
 import java.io.IOException;
@@ -199,7 +216,7 @@ public abstract class AbstractDynamicHTTPFetchingStrategy<CriteriaType, Metadata
             final MetadataType result = httpClient.execute(request, responseHandler, context);
             HttpClientSecuritySupport.checkTLSCredentialEvaluated(context, request.getURI().getScheme());
             return result;
-        } catch (IOException e) {
+        } catch (final IOException e) {
             log.warn("Unable to fetch metadata from remote HTTP source",e);
             return null;
         } finally {
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCMetadataResolver.java
index 643949c..d48c9c8 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCMetadataResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCMetadataResolver.java
@@ -18,7 +18,6 @@
 package net.shibboleth.oidc.metadata.impl;
 
 import java.util.List;
-import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 
@@ -28,10 +27,8 @@ import org.slf4j.LoggerFactory;
 import net.shibboleth.oidc.metadata.DynamicOIDCMetadataResolver;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
@@ -40,34 +37,28 @@ import net.shibboleth.utilities.java.support.resolver.ResolverException;
  * 
  * Is instrumented to collect timming metrics.
  * 
- * Has a cache to ...
+ * Has a fetching strategy which is used to acquire metadata is the cache does not serve it up.
  * 
  * @param <IdentifierType> The identifier type in the backing store
  * @param <MetadataType> The metadata type in the backing store
  */
+//TODO collapse this hierarchy and finish off the mess.
 public abstract class AbstractDynamicOIDCMetadataResolver<IdentifierType, MetadataType> 
                                     extends AbstractOIDCMetadataResolver<IdentifierType, MetadataType> 
                                     implements DynamicOIDCMetadataResolver<MetadataType> {
     
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(AbstractDynamicOIDCMetadataResolver.class);
-       
-    /** The strategy used to fetch metadata from a source.*/
-    @NonnullAfterInit private final Function<CriteriaSet, MetadataType> metadataFetchingStrategy;
-   
+          
     
     /**
-     * 
      * Constructor.
      *
      * @param metadataCache the cache to hold metadata.
-     * @param fetchingStrategy the strategy used to fetch metadata.
      */
     protected AbstractDynamicOIDCMetadataResolver(
-            @Nonnull final MetadataCache<MetadataType> metadataCache,
-            @Nonnull final Function<CriteriaSet, MetadataType> fetchingStrategy) {
+            @Nonnull final MetadataCache<MetadataType> metadataCache) {
         super(metadataCache);
-        metadataFetchingStrategy = Constraint.isNotNull(fetchingStrategy, "Metadata fetching strategy can not be null");
     }
     
     
@@ -75,10 +66,6 @@ public abstract class AbstractDynamicOIDCMetadataResolver<IdentifierType, Metada
     @Override
     protected void initMetadataResolver() throws ComponentInitializationException {
         
-        if (metadataFetchingStrategy == null) {
-            throw new ComponentInitializationException("Metadating fetching strategy can not be null");
-        }
-        
         try { //TODO metrics, cache loading.
           
 //            initializeMetricsInstrumentation();
@@ -124,11 +111,10 @@ public abstract class AbstractDynamicOIDCMetadataResolver<IdentifierType, Metada
                
         //final Context contextResolve = MetricsSupport.startTimer(timerResolve);
         try {            
-            final List<MetadataType> metadata = getCache()
-                    .getOrFetchIfAbsent(criteria, metadataFetchingStrategy);            
+            final List<MetadataType> metadata = getCache().get(criteria);            
             return predicateFilterCandidates(metadata, criteria, false);
             
-        } catch (MetadataCacheException e) {
+        } catch (final MetadataCacheException e) {
             throw new ResolverException(e);
         } finally {
             //MetricsSupport.stopTimer(contextResolve);
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java
index e98e6ef..0a3c275 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractOIDCMetadataResolver.java
@@ -39,6 +39,7 @@ import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.component.DestructableComponent;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.CriterionPredicateRegistry;
@@ -79,7 +80,11 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
      */
     private boolean failFastInitialization;
     
-    /** Constructor.*/
+    /**
+     * Constructor.
+     *
+     * @param metadataCache the metadata cache to use.
+     */
     protected AbstractOIDCMetadataResolver(@Nonnull final MetadataCache<MetadataType> metadataCache) {
         failFastInitialization = true;
         cache = Constraint.isNotNull(metadataCache, "Metadata cache can not be null");
@@ -98,6 +103,21 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
         return logPrefix;
     }
     
+    /** {@inheritDoc} */
+    @Override @Nullable public MetadataType resolveSingle(final CriteriaSet criteria) throws ResolverException {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+
+        final Iterable<MetadataType> iterable = resolve(criteria);
+        if (iterable != null) {
+            final Iterator<MetadataType> iterator = iterable.iterator();
+            if (iterator != null && iterator.hasNext()) {
+                return iterator.next();
+            }
+        }
+        return null;
+    }
+    
     /**
      * Filter the supplied candidates by resolving predicates from the supplied criteria and applying
      * the predicates to return a filtered {@link Iterable}.
@@ -124,7 +144,8 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
         
         // TODO: The criterion should be a subtype of AbstractEvaluableMetadataCriterion to avoid errors.
         @SuppressWarnings("unchecked") final Set<Predicate<MetadataType>> predicates = 
-                ResolverSupport.getPredicates(criteria, EvaluableMetadataCriterion.class, getCriterionPredicateRegistry());
+                ResolverSupport.getPredicates(criteria, EvaluableMetadataCriterion.class, 
+                        getCriterionPredicateRegistry());
         
         log.trace("{} Resolved {} Predicates: {}", getLogPrefix(), predicates.size(), predicates);
         
@@ -248,25 +269,14 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
     }  
     
     @Override protected void doDestroy() {
-        log.warn("Destroying");
+        if (cache instanceof DestructableComponent) {
+            ((DestructableComponent)cache).destroy();
+        }
     }
     
     /** Initialise this metadata provider. Subclasses will need to override this method.*/
     protected abstract void initMetadataResolver() throws ComponentInitializationException;
     
-    /** {@inheritDoc} */
-    @Override @Nullable public MetadataType resolveSingle(final CriteriaSet criteria) throws ResolverException {
-        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
 
-        final Iterable<MetadataType> iterable = resolve(criteria);
-        if (iterable != null) {
-            final Iterator<MetadataType> iterator = iterable.iterator();
-            if (iterator != null && iterator.hasNext()) {
-                return iterator.next();
-            }
-        }
-        return null;
-    }
     
 }
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBatchBackingStore.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBatchBackingStore.java
new file mode 100644
index 0000000..ba93b88
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBatchBackingStore.java
@@ -0,0 +1,64 @@
+/*
+ * 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.oidc.metadata.impl;
+
+import java.time.Instant;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+import net.shibboleth.oidc.metadata.BatchBackingStore;
+
+/**
+ * Default implementation of a {@link BatchBackingStore}.
+ *
+ * @param <I> the metadata identifier type.
+ * @param <T> the metadata type.
+ */
+ at ThreadSafe
+public class DefaultBatchBackingStore<I,T> extends AbstractBackingStore<I, T> implements BatchBackingStore<I, T> {
+    
+    /** Last time the metadata was updated. */
+    @Nullable @GuardedBy("this") private Instant lastUpdate;
+
+    /** Last time a refresh cycle occurred. */
+    @Nullable @GuardedBy("this") private Instant lastRefresh;
+
+    @Override
+    @Nullable public synchronized Instant getLastUpdate() {
+       return lastUpdate;
+    }
+
+    @Override
+    @Nullable public synchronized Instant getLastRefresh() {
+        return lastRefresh;
+    }
+
+    @Override
+    public synchronized void setLastUpdate(@Nullable final Instant updatedAt) {
+        lastUpdate = updatedAt;
+        
+    }
+
+    @Override
+    public synchronized void setLastRefresh(@Nullable final Instant refreshedAt) {
+        lastRefresh = refreshedAt;        
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBackingStore.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java
similarity index 79%
rename from oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBackingStore.java
rename to oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java
index a2a7460..773159b 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBackingStore.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java
@@ -15,12 +15,11 @@
  * limitations under the License.
  */
 
+
 package net.shibboleth.oidc.metadata.impl;
 
 import java.time.Duration;
 import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -28,24 +27,25 @@ import java.util.concurrent.ConcurrentHashMap;
 import javax.annotation.Nonnull;
 import javax.annotation.concurrent.ThreadSafe;
 
-import net.shibboleth.oidc.metadata.BackingStore;
+import net.shibboleth.oidc.metadata.DynamicBackingStore;
 import net.shibboleth.oidc.metadata.MetadataManagementData;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
 import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
+/**
+ * Default implementation of a {@link DynamicBackingStore}.
+ *
+ * @param <I> the metadata identifier type.
+ * @param <T> the metadata type.
+ */
 @ThreadSafe
-public class DefaultBackingStore<I, T> implements BackingStore<I, T> {
+public class DefaultDynamicBackingStore<I,T> extends AbstractBackingStore<I,T> implements DynamicBackingStore<I, T>{
     
-    /** Index of entity IDs to their descriptors. */
-    private Map<I, List<T>> indexedDescriptors;
-
-    /** Ordered list of entity descriptors. */
-    private List<T> orderedDescriptors;
     
     /** Map holding management data for each entityID. */
-    private final Map<I, MetadataManagementData<I>> mgmtDataMap;
+    private final Map<I, MetadataManagementData<I>> mgmtDataMap;    
     
     /** The maximum cache duration for metadata.*/  
     @Nonnull private final Duration maxCacheDuration;
@@ -55,23 +55,11 @@ public class DefaultBackingStore<I, T> implements BackingStore<I, T> {
      *
      * @param cacheDuration TODO should this be here?
      */
-    public DefaultBackingStore(@Nonnull final Duration cacheDuration) {
+    public DefaultDynamicBackingStore(@Nonnull final Duration cacheDuration) {
         super();
-        maxCacheDuration = Constraint.isNotNull(cacheDuration,"Max cache duration can not be null");
-        indexedDescriptors = new ConcurrentHashMap<>();
-        orderedDescriptors = new ArrayList<>();               
+        maxCacheDuration = Constraint.isNotNull(cacheDuration,"Max cache duration can not be null");          
         mgmtDataMap = new ConcurrentHashMap<>();
     }
-
-    @Override
-    @Nonnull public Map<I, List<T>> getIndexedValues() {
-        return indexedDescriptors;
-    }
-
-    @Override
-    @Nonnull public List<T> getOrderedValues() {
-        return orderedDescriptors;
-    }
     
     @Override
     public MetadataManagementData<I> computeManagementDataIfAbsent(@Nonnull final I identifier) {
@@ -94,6 +82,7 @@ public class DefaultBackingStore<I, T> implements BackingStore<I, T> {
         mgmtDataMap.remove(identifier);
         
     }
+   
     
     @Override
     @Nonnull @NonnullElements @Unmodifiable @NotLive
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolver.java
index 7c7a32f..19f80ac 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolver.java
@@ -17,8 +17,6 @@
 
 package net.shibboleth.oidc.metadata.impl;
 
-import java.util.function.Function;
-
 import javax.annotation.Nonnull;
 
 import com.nimbusds.oauth2.sdk.id.Issuer;
@@ -26,7 +24,6 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
 import net.shibboleth.oidc.metadata.ProviderMetadataResolver;
 import net.shibboleth.oidc.metadata.cache.MetadataCache;
-import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
 /** Concrete metadata resolver for dynamic OIDC resolution.*/
 public class DynamicOIDCProviderMetadataResolver 
@@ -37,12 +34,10 @@ public class DynamicOIDCProviderMetadataResolver
      * Constructor.
      *
      * @param metadataCache the cache to hold metadata.
-     * @param fetchingStrategy the strategy used to fetch metadata.
      */
     protected DynamicOIDCProviderMetadataResolver(
-            @Nonnull final MetadataCache<OIDCProviderMetadata> metadataCache,
-            @Nonnull final Function<CriteriaSet, OIDCProviderMetadata> fetchingStrategy) {
-        super(metadataCache, fetchingStrategy);
+            @Nonnull final MetadataCache<OIDCProviderMetadata> metadataCache) {
+        super(metadataCache);
         
     }
 
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java
index d9a3eeb..b50c72f 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java
@@ -1,3 +1,20 @@
+/*
+ * 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.oidc.metadata.impl;
 
 import java.io.IOException;
@@ -116,7 +133,7 @@ public class HTTPProviderConfigurationFetchingStrategy
         @Nullable public String apply(@Nonnull final Issuer issuer, @Nonnull @NotEmpty final String wellKnownPath) {
             // remove trailing slash if any (see openid-connect-discovery 4.1)
             final String normalizedIssuer = StringUtils.removeEnd(issuer.getValue(), "/");
-            StringBuilder builder = new StringBuilder();
+            final StringBuilder builder = new StringBuilder();
             builder.append(normalizedIssuer).append(wellKnownPath);
             return builder.toString();
         }       
@@ -125,13 +142,14 @@ public class HTTPProviderConfigurationFetchingStrategy
     /** The response handler for parsing the providers's configuration information into {@link OIDCProviderMetadata}.*/
     @Immutable
     @ThreadSafe
-    public static final class OIDCProviderMetadataResponseHandler implements ResponseHandler<OIDCProviderMetadata> {        
+    public static final class OIDCProviderMetadataResponseHandler 
+                                                implements ResponseHandler<OIDCProviderMetadata> {        
         
         /** Class logger. */
         @Nonnull private final Logger log = LoggerFactory.getLogger(OIDCProviderMetadataResponseHandler.class);
 
         @Override
-        @Nullable public OIDCProviderMetadata handleResponse(HttpResponse response) throws IOException {
+        @Nullable public OIDCProviderMetadata handleResponse(final HttpResponse response) throws IOException {
             
             final int httpStatusCode = response.getStatusLine().getStatusCode();
             
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadatCacheTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadatCacheTest.java
new file mode 100644
index 0000000..4684895
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadatCacheTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.oidc.metadata.cache.impl;
+
+import static org.testng.Assert.assertTrue;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.SubjectType;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+import net.shibboleth.oidc.metadata.cache.CacheLoadingContext;
+import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
+import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
+import net.shibboleth.oidc.metadata.impl.DefaultBatchBackingStore;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+public class BatchMetadatCacheTest {
+
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(BatchMetadatCacheTest.class);
+
+    // OIDC provider metadata cache
+    private BatchMetadataCache<Issuer, OIDCProviderMetadata> cache;
+
+    private ManuallyTriggeredScheduledExecutorService scheduler;
+
+    @Nonnull
+    private Function<CacheLoadingContext, byte[]> defaultLoadingStrategy;
+
+    @Nonnull
+    private Function<byte[], List<OIDCProviderMetadata>> defaultParsingStrategy;
+
+    @BeforeMethod
+    void setup() throws Exception {
+
+        defaultLoadingStrategy = context -> {
+            try {
+                return new OIDCProviderMetadata(new Issuer("http://www.example.org"), List.of(SubjectType.PUBLIC),
+                        new URI("http://example.oidc.op.org")).toJSONObject().toJSONString().getBytes();
+            } catch (final URISyntaxException e) {
+                return null;
+            }
+        };
+
+        defaultParsingStrategy = in -> {
+            try {
+                return List.of(OIDCProviderMetadata.parse(new String(in, "UTF-8")));
+            } catch (final ParseException | UnsupportedEncodingException e) {
+                return null;
+            }
+        };
+
+        // Give our own executor, so we can manually handle the cleanup task
+        scheduler = new ManuallyTriggeredScheduledExecutorService();
+        cache = new BatchMetadataCache<Issuer, OIDCProviderMetadata>(
+                new DefaultBatchBackingStore<Issuer, OIDCProviderMetadata>(), defaultLoadingStrategy,
+                defaultParsingStrategy, scheduler);
+        cache.setIdentifierExtractionStrategy(m -> m.getIssuer());
+        cache.setMinRefreshDelay(Duration.ofMinutes(5));
+        cache.setMaxRefreshDelay(Duration.ofMinutes(10));
+        cache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
+        cache.setCriteriaToIdentifierStrategy(crit -> {
+            final IssuerIDCriterion issuerId = crit.get(IssuerIDCriterion.class);
+            if (issuerId != null) {
+                return issuerId.getIssuerID();
+            }
+            return null;
+        });
+
+        cache.setRefreshDelayFactor(0.75f);
+        cache.setMinCacheDuration(Duration.ofMinutes(10));
+        // cache.setMaxCacheDuration(Duration.ofMinutes(20));
+        cache.setMetadataFilterStrategy((metadata, context) -> metadata);
+        cache.setId("MockRefreshableCache");
+        // Initialise when you need to use it, if creating a local version, do not init this one.
+        // cache.initialize();
+
+    }
+
+    @AfterMethod
+    public void tearDown() {
+        if (cache != null) {
+            cache.destroy();
+        }
+    }
+
+    /* Not for CI, turn off.*/
+    @Test(enabled = false)
+    public void testNormalRefreshDelay() throws ComponentInitializationException, InterruptedException {
+        
+        BatchMetadataCache<Issuer, OIDCProviderMetadata> localCache = 
+                new BatchMetadataCache<Issuer, OIDCProviderMetadata>(
+                new DefaultBatchBackingStore<Issuer, OIDCProviderMetadata>(), defaultLoadingStrategy,
+                defaultParsingStrategy, scheduler);
+        
+        localCache.setIdentifierExtractionStrategy(m -> m.getIssuer());
+        localCache.setMinRefreshDelay(Duration.ofMillis(100));
+        localCache.setMaxRefreshDelay(Duration.ofMillis(200));
+        localCache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
+        localCache.setCriteriaToIdentifierStrategy(crit -> {
+            final IssuerIDCriterion issuerId = crit.get(IssuerIDCriterion.class);
+            if (issuerId != null) {
+                return issuerId.getIssuerID();
+            }
+            return null;
+        });
+
+        localCache.setRefreshDelayFactor(0.75f);
+        localCache.setMinCacheDuration(Duration.ofMinutes(10));
+        // cache.setMaxCacheDuration(Duration.ofMinutes(20));
+        localCache.setMetadataFilterStrategy((metadata, context) -> metadata);
+        localCache.setId("MockLocalRefreshableCache");
+        localCache.initialize();
+        
+        Thread.sleep(2000);
+    }
+
+    @Test
+    public void testGetNotCached_Success() throws MetadataCacheException, ComponentInitializationException {
+        cache.initialize();
+        List<OIDCProviderMetadata> metadata =
+                cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("http://www.example.org"))));
+        assertTrue(metadata.isEmpty() == false);
+
+    }
+
+    @Test
+    public void testGetNotCached_RefreshAHead_Success()
+            throws MetadataCacheException, ComponentInitializationException {
+        cache.initialize();
+        scheduler.triggerScheduledTasks();
+        List<OIDCProviderMetadata> metadata =
+                cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("http://www.example.org"))));
+        assertTrue(metadata.isEmpty() == false);
+
+    }
+
+    @Test
+    public void testGetAndLoad_Success()
+            throws ComponentInitializationException, InterruptedException, ExecutionException {
+        cache.initialize();
+
+        final ExecutorService service = Executors.newFixedThreadPool(3);
+        Future<?> futureOne = service.submit(() -> {
+            return cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("http://www.example.org"))));
+        });
+        scheduler.triggerScheduledTasks();
+
+        List<?> firstMetadata = (List<?>) futureOne.get();
+        assertTrue(firstMetadata.size() == 1);
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCacheTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheTest.java
similarity index 70%
rename from oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCacheTest.java
rename to oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheTest.java
index fb6e320..32f1c37 100644
--- a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCacheTest.java
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheTest.java
@@ -1,3 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.oidc.metadata.cache.impl;
 
@@ -14,10 +31,14 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
 
 import org.opensaml.core.criterion.EntityIdCriterion;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
@@ -28,26 +49,38 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 import net.shibboleth.oidc.metadata.MetadataManagementData;
 import net.shibboleth.oidc.metadata.cache.MetadataCacheException;
 import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
-import net.shibboleth.oidc.metadata.impl.AbstractDynamicOIDCMetadataResolver;
-import net.shibboleth.oidc.metadata.impl.DefaultBackingStore;
+import net.shibboleth.oidc.metadata.impl.DefaultDynamicBackingStore;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
 /** Test for the MetadataCache. */
-public class DefaultMetadataCacheTest {
+public class DynamicMetadataCacheTest {
     
     /** Class logger. */
-    private final Logger log = LoggerFactory.getLogger(DefaultMetadataCacheTest.class);
+    private final Logger log = LoggerFactory.getLogger(DynamicMetadataCacheTest.class);
 
     // OIDC provider metadata cache
-    private DefaultMetadataCache<Issuer, OIDCProviderMetadata> cache;
+    private DynamicMetadataCache<Issuer, OIDCProviderMetadata> cache;
+    
+    @Nonnull private Function<CriteriaSet, OIDCProviderMetadata> defaultFetchStrategy;
 
     @BeforeMethod
     void setup() throws Exception {
         
+        defaultFetchStrategy = crit -> {
+            try {
+                final Issuer iss = crit.get(IssuerIDCriterion.class).getIssuerID();                       
+                return new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC),
+                        new URI("http://example.oidc.op.org"));
+            } catch (URISyntaxException e) {
+                return null;
+            }
+        };
+        
         // Give our own executor, so we can manually handle the cleanup task
         ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
-        cache = new DefaultMetadataCache<>(new DefaultBackingStore<>(Duration.ofMinutes(5)), scheduler);
+        cache = new DynamicMetadataCache<Issuer, OIDCProviderMetadata>
+                        (new DefaultDynamicBackingStore<>(Duration.ofMinutes(5)),defaultFetchStrategy, scheduler);
         cache.setIdentifierExtractionStrategy(m -> m.getIssuer());
         cache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
         cache.setCriteriaToIdentifierStrategy(crit -> {
@@ -63,7 +96,7 @@ public class DefaultMetadataCacheTest {
         cache.setRemoveIdleEntityData(true);
         cache.setRefreshDelayFactor(0.75f);
         cache.setMinCacheDuration(Duration.ofMinutes(10));
-        cache.setMaxCacheDuration(Duration.ofMinutes(20));
+       //cache.setMaxCacheDuration(Duration.ofMinutes(20));
         cache.setMetadataFilterStrategy((metadata, context) -> metadata);
         cache.setId("MockCache");
         // Initialise when you need to use it, if creating a local version, do not init this one.
@@ -71,13 +104,21 @@ public class DefaultMetadataCacheTest {
 
     }
     
+    @AfterMethod
+    public void tearDown() {
+        if (cache != null) {
+            cache.destroy();
+        }
+    }
+    
     @Test
     public void testBackgroundCleanup_Expired_Success() throws ComponentInitializationException, URISyntaxException, InterruptedException {
 
         // Give our own executor, so we do not need to wait.
         ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
-        DefaultMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =  
-                new DefaultMetadataCache<Issuer, OIDCProviderMetadata>(new DefaultBackingStore<>(Duration.ofMinutes(5)), scheduler);
+        DynamicMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =  
+                new DynamicMetadataCache<Issuer, OIDCProviderMetadata>(new DefaultDynamicBackingStore<>(Duration.ofMinutes(5)),
+                        defaultFetchStrategy, scheduler);
         
         // use a cache local to this method
         cacheLocal.setIdentifierExtractionStrategy(m -> m.getIssuer());
@@ -94,7 +135,7 @@ public class DefaultMetadataCacheTest {
         cacheLocal.setRemoveIdleEntityData(true);      
         cacheLocal.setRefreshDelayFactor(0.75f);
         cacheLocal.setMinCacheDuration(Duration.ofMinutes(10));
-        cacheLocal.setMaxCacheDuration(Duration.ofMinutes(20));
+        //cacheLocal.setMaxCacheDuration(Duration.ofMinutes(20));
         cacheLocal.setMetadataFilterStrategy((metadata, context) -> metadata);
         cacheLocal.setId("MockCache");
         cacheLocal.initialize();
@@ -132,17 +173,11 @@ public class DefaultMetadataCacheTest {
     public void testCacheNotInitialized() throws Exception {
 
         // Create but do not initialise
-        DefaultMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =  new DefaultMetadataCache<>(
-                new DefaultBackingStore<>(Duration.ofMinutes(5)));
+        DynamicMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =  new DynamicMetadataCache<>(
+                new DefaultDynamicBackingStore<>(Duration.ofMinutes(5)),
+                defaultFetchStrategy);
         final Issuer iss = new Issuer("https://example.oidc.op.org");
-        cacheLocal.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(iss)), id -> {
-            try {
-                return new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC),
-                        new URI("http://example.oidc.op.org"));
-            } catch (URISyntaxException e) {
-                return null;
-            }
-        });
+        cacheLocal.get(new CriteriaSet(new IssuerIDCriterion(iss)));
     }
 
     @Test(enabled = true)
@@ -150,9 +185,10 @@ public class DefaultMetadataCacheTest {
 
         // Give our own executor, so we do not need to wait.
         ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
-        DefaultMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =  
-                new DefaultMetadataCache<Issuer, OIDCProviderMetadata>(
-                        new DefaultBackingStore<>(Duration.ofMinutes(5)), scheduler);
+        DynamicMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =  
+                new DynamicMetadataCache<Issuer, OIDCProviderMetadata>(
+                        new DefaultDynamicBackingStore<>(Duration.ofMinutes(5)),
+                        defaultFetchStrategy, scheduler);
         
         // use a cache local to this method
         cacheLocal.setIdentifierExtractionStrategy(m -> m.getIssuer());
@@ -169,7 +205,7 @@ public class DefaultMetadataCacheTest {
         cacheLocal.setRemoveIdleEntityData(true);      
         cacheLocal.setRefreshDelayFactor(0.75f);
         cacheLocal.setMinCacheDuration(Duration.ofMinutes(10));
-        cacheLocal.setMaxCacheDuration(Duration.ofMinutes(20));
+       // cacheLocal.setMaxCacheDuration(Duration.ofMinutes(20));
         cacheLocal.setMetadataFilterStrategy((metadata, context) -> metadata);
         cacheLocal.setId("MockCache");
         cacheLocal.initialize();
@@ -230,14 +266,7 @@ public class DefaultMetadataCacheTest {
         // should have been created now
         assertTrue(mgmtData.getLastUpdateTime().equals(now));
         
-        cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(iss)), id -> {
-            try {
-                return new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC),
-                        new URI("http://example.oidc.op.org"));
-            } catch (URISyntaxException e) {
-                return null;
-            }
-        });
+        cache.get(new CriteriaSet(new IssuerIDCriterion(iss)));
         
         // should have been updated after now
         assertFalse(mgmtData.getLastUpdateTime().equals(now));
@@ -248,30 +277,45 @@ public class DefaultMetadataCacheTest {
     public void testGetNotCached_Success() throws MetadataCacheException, ComponentInitializationException {
         cache.initialize();
         List<OIDCProviderMetadata> metadata = 
-                cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
-            try {
-                return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
-                        new URI("http://example.com/metadata"));
-            } catch (URISyntaxException e) {
-                return null;
-            }
-        });
+                cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))));
         assertTrue(metadata.isEmpty() == false);
 
     }
     
     @Test
     public void testGetNotCached_WrongIdentifier_Fail() throws MetadataCacheException, ComponentInitializationException {
-        cache.initialize();
-        List<OIDCProviderMetadata> metadata = 
-                cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
-            try {
-                return new OIDCProviderMetadata(new Issuer("different-issuer"), List.of(SubjectType.PUBLIC),
-                        new URI("http://example.com/metadata"));
-            } catch (URISyntaxException e) {
-                return null;
+        ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
+        final DynamicMetadataCache<Issuer, OIDCProviderMetadata> localCache = 
+                new DynamicMetadataCache<Issuer, OIDCProviderMetadata>(
+                        new DefaultDynamicBackingStore<>(Duration.ofMinutes(5)),crit -> {
+                    try {                    
+                        return new OIDCProviderMetadata(new Issuer("wrong-id"), List.of(SubjectType.PUBLIC),
+                                new URI("http://example.oidc.op.org"));
+                    } catch (URISyntaxException e) {
+                        return null;
+                    }}, scheduler);
+        localCache.setIdentifierExtractionStrategy(m -> m.getIssuer());
+        localCache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
+        localCache.setCriteriaToIdentifierStrategy(crit -> {
+            final IssuerIDCriterion issuerId = crit.get(IssuerIDCriterion.class);
+            if (issuerId != null) {
+                return issuerId.getIssuerID();
             }
-        });
+            return null;});
+        localCache.setCleanupTaskInterval(Duration.ofSeconds(100));
+        
+        localCache.setInitialCleanupTaskDelay(Duration.ofSeconds(1));
+        localCache.setMaxIdleEntityData(Duration.ofMinutes(10));
+        localCache.setRemoveIdleEntityData(true);
+        localCache.setRefreshDelayFactor(0.75f);
+        localCache.setMinCacheDuration(Duration.ofMinutes(10));
+       //localCache.setMaxCacheDuration(Duration.ofMinutes(20));
+        localCache.setMetadataFilterStrategy((metadata, context) -> metadata);
+        localCache.setId("MockCache");
+        localCache.initialize();
+        
+        List<OIDCProviderMetadata> metadata = 
+                localCache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))));
         assertTrue(metadata.isEmpty());
 
     }
@@ -280,14 +324,7 @@ public class DefaultMetadataCacheTest {
     public void testGetNotCached_WrongCriteria_Fail() throws MetadataCacheException, ComponentInitializationException {
         cache.initialize();
         List<OIDCProviderMetadata> metadata = 
-                cache.getOrFetchIfAbsent(new CriteriaSet(new EntityIdCriterion("wrong-criteria")), id -> {
-            try {
-                return new OIDCProviderMetadata(new Issuer("different-issuer"), List.of(SubjectType.PUBLIC),
-                        new URI("http://example.com/metadata"));
-            } catch (URISyntaxException e) {
-                return null;
-            }
-        });
+                cache.get(new CriteriaSet(new EntityIdCriterion("wrong-criteria")));
         assertTrue(metadata.isEmpty());
 
     }
@@ -298,35 +335,14 @@ public class DefaultMetadataCacheTest {
         cache.initialize();
         final ExecutorService service = Executors.newFixedThreadPool(3);
         Future<?> futureOne = service.submit(() -> {
-            return cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
-                try {
-                    return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
-                            new URI("http://example.com/metadata"));
-                } catch (URISyntaxException e) {
-                    return null;
-                }
-            });
+            return cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))));
         });
         Future<?> futureTwo = service.submit(() -> {
-            return cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
-                try {
-                    return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
-                            new URI("http://example.com/metadata"));
-                } catch (URISyntaxException e) {
-                    return null;
-                }
-            });
+            return cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))));
         });
         // different entity
         Future<?> futureThree = service.submit(() -> {
-            return cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
-                try {
-                    return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
-                            new URI("http://example.com/metadata"));
-                } catch (URISyntaxException e) {
-                    return null;
-                }
-            });
+            return cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))));
         });
         List<?> firstMetadata = (List<?>) futureOne.get();
         List<?> secondMetadata = (List<?>) futureThree.get();
@@ -334,14 +350,7 @@ public class DefaultMetadataCacheTest {
 
         // non-interleaved request
         List<OIDCProviderMetadata> provider =
-                cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
-                    try {
-                        return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
-                                new URI("http://example.com/metadata"));
-                    } catch (URISyntaxException e) {
-                        return null;
-                    }
-                });
+                cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))));
         assertTrue(firstMetadata.size() == 1);
         assertTrue(secondMetadata.size() == 1);
         assertTrue(thirdMetadata.size() == 1);
@@ -358,25 +367,11 @@ public class DefaultMetadataCacheTest {
     public void testGetCached_Success() throws MetadataCacheException, ComponentInitializationException {
         cache.initialize();
         List<OIDCProviderMetadata> metadata =
-                cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
-                    try {
-                        return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
-                                new URI("http://example.com/metadata"));
-                    } catch (URISyntaxException e) {
-                        return null;
-                    }
-                });
+                cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))));
         assertTrue(metadata.size() == 1);
 
         List<OIDCProviderMetadata> metadataCached =
-                cache.getOrFetchIfAbsent(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))), id -> {
-                    try {
-                        return new OIDCProviderMetadata(new Issuer("the-issuer"), List.of(SubjectType.PUBLIC),
-                                new URI("http://example.com/metadata"));
-                    } catch (URISyntaxException e) {
-                        return null;
-                    }
-                });
+                cache.get(new CriteriaSet(new IssuerIDCriterion(new Issuer("the-issuer"))));
         assertTrue(metadata.size() == 1);
         // first was cached, so this should be the same
         assertTrue(metadata.get(0) == metadataCached.get(0));
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java
index a43ce2a..d44e6b3 100644
--- a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java
@@ -12,6 +12,8 @@ import java.io.IOException;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.function.Function;
 
 import org.apache.http.HttpHeaders;
 import org.apache.http.HttpStatus;
@@ -33,10 +35,10 @@ import com.nimbusds.oauth2.sdk.id.Issuer;
 import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
 import net.shibboleth.oidc.metadata.AbstractEvaluableMetadataCriterion;
+import net.shibboleth.oidc.metadata.DynamicBackingStore;
 import net.shibboleth.oidc.metadata.MetadataManagementData;
-import net.shibboleth.oidc.metadata.cache.impl.DefaultMetadataCache;
-import net.shibboleth.oidc.metadata.cache.impl.MetadataCacheBuilder;
-import net.shibboleth.oidc.metadata.cache.impl.OIDCProviderMetadataCacheFactoryBean;
+import net.shibboleth.oidc.metadata.cache.impl.DynamicMetadataCache;
+import net.shibboleth.oidc.metadata.cache.impl.ManuallyTriggeredScheduledExecutorService;
 import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
 import net.shibboleth.oidc.metadata.impl.HTTPProviderConfigurationFetchingStrategy.OIDCProviderMetadataResponseHandler;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
@@ -108,9 +110,9 @@ public class DynamicOIDCProviderMetadataResolverTest {
     
     private HttpClient httpClient;
 
-    private DefaultMetadataCache<Issuer, OIDCProviderMetadata> cache;
-    
-    private MetadataCacheBuilder<Issuer, OIDCProviderMetadata> builder;
+    /** Cast to the abstract metadata cache and not the interface to allow access to backing store.*/
+    private TestableDynamicMetadataCache<Issuer, OIDCProviderMetadata> cache;
+   
     
     @SuppressWarnings("unchecked")
     @BeforeMethod
@@ -123,28 +125,42 @@ public class DynamicOIDCProviderMetadataResolverTest {
                 execute(any(HttpUriRequest.class),any(ResponseHandler.class),any(HttpContext.class)))
                 .thenReturn(OIDCProviderMetadata.parse(GOOD_PROVIDER_CONFIGURATION_INFO));
         
-        builder = new OIDCProviderMetadataCacheFactoryBean();
-        builder.setSingleton(true);
-        
-        //setup mock functions
-        builder.setIdentifierExtractionStrategy(m -> m.getIssuer());
-        builder.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(15)) );
-        builder.setCriteriaToIdentifierStrategy(c -> c.get(IssuerIDCriterion.class).getIssuerID());
-        builder.setMinCacheDuration(Duration.ofMinutes(5));
-        builder.setMaxCacheDuration(Duration.ofMinutes(15));
-        builder.setInitialCleanupTaskDelay(Duration.ofSeconds(1));
-        
-        builder.afterPropertiesSet();
-        cache = builder.getObject();
         
         final HTTPProviderConfigurationFetchingStrategy fetchingStrategy = 
-                new HTTPProviderConfigurationFetchingStrategy(httpClient, new OIDCProviderMetadataResponseHandler());
+                new HTTPProviderConfigurationFetchingStrategy(httpClient, new OIDCProviderMetadataResponseHandler());   
+       
         fetchingStrategy.setId("Mock HTTP Fetching Strategy");
         fetchingStrategy.initialize();
         
-        resolver = new DynamicOIDCProviderMetadataResolver(cache, fetchingStrategy);
+        // Give our own executor, so we can manually handle the cleanup task
+        ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
+        cache = new TestableDynamicMetadataCache<Issuer, OIDCProviderMetadata>
+                        (new DefaultDynamicBackingStore<>(Duration.ofMinutes(5)),fetchingStrategy, scheduler);
+        cache.setIdentifierExtractionStrategy(m -> m.getIssuer());
+        cache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
+        cache.setCriteriaToIdentifierStrategy(crit -> {
+            final IssuerIDCriterion issuerId = crit.get(IssuerIDCriterion.class);
+            if (issuerId != null) {
+                return issuerId.getIssuerID();
+            }
+            return null;});
+        cache.setCleanupTaskInterval(Duration.ofSeconds(100));
+        
+        cache.setInitialCleanupTaskDelay(Duration.ofSeconds(1));
+        cache.setMaxIdleEntityData(Duration.ofMinutes(10));
+        cache.setRemoveIdleEntityData(true);
+        cache.setRefreshDelayFactor(0.75f);
+        cache.setMinCacheDuration(Duration.ofMinutes(10));
+       //cache.setMaxCacheDuration(Duration.ofMinutes(20));
+        cache.setMetadataFilterStrategy((metadata, context) -> metadata);
+        cache.setId("MockCache");
+        cache.initialize();
+        
+       
+        
+   
+        resolver = new DynamicOIDCProviderMetadataResolver(cache);
         resolver.setId("mockHttpOIDCProvider");
-        //resolver.setMetadataFetchingStrategy(fetchingStrategy);
         resolver.initialize();
         
     }
@@ -307,6 +323,20 @@ public class DynamicOIDCProviderMetadataResolverTest {
         assertFalse(found.iterator().hasNext());
     }
     
-    
+    /** Extension of the {@link DynamicMetadataCache} to expose certain internals.*/
+    class TestableDynamicMetadataCache<IdentifierType, MetadataType> 
+                            extends DynamicMetadataCache<IdentifierType, MetadataType> {
+
+        TestableDynamicMetadataCache(DynamicBackingStore<IdentifierType, MetadataType> store,
+                Function<CriteriaSet, MetadataType> metadataFetchStrategy, ScheduledExecutorService executor) {
+            super(store, metadataFetchStrategy, executor);
+        }
+        
+        /* Expose the backing store with a public method.*/
+        public DynamicBackingStore<IdentifierType, MetadataType> getBackingStore(){
+            return super.getBackingStore();
+        }
+        
+    }
 
 }

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


More information about the commits mailing list