[java-oidc-common] 03/05: Improve dynamic oidc metadata resolver

Phil Smart philip.smart at jisc.ac.uk
Thu Oct 14 16:48:22 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=7f4e403600d6c948b7c8d6e238b807f4f3a43124

commit 7f4e403600d6c948b7c8d6e238b807f4f3a43124
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Oct 1 14:48:44 2021 +0100

    Improve dynamic oidc metadata resolver
    
     - Metadata filters can be added as a strategy to filter immediatly
    fetched metadata.
     - Metadata can be filtered based on predicates in the criteriaset using
    a type safe evaluablecriterion.
     - Javadoc and cleanup
---
 .../src/main/assembly/plugin-assembly-tgz.xml      |   2 +-
 .../src/main/assembly/plugin-assembly-zip.xml      |   2 +-
 .../AbstractEvaluableMetadataCriterion.java        |  60 ++
 .../net/shibboleth/oidc/metadata/BackingStore.java |  58 +-
 .../oidc/metadata/DynamicBackingStore.java         |  21 -
 .../oidc/metadata/DynamicOIDCMetadataResolver.java |  32 +
 .../DynamicOIDCProviderMetadataResolver.java       |  12 -
 .../oidc/metadata/EvaluableMetadataCriterion.java  |  34 +
 .../oidc/metadata/MetadataManagementData.java      |  55 +-
 .../oidc/metadata/OIDCMetadataResolver.java        |   8 +-
 .../oidc/metadata/cache/MetadataCache.java         |  39 ++
 .../metadata/cache/MetadataCacheException.java     |  48 ++
 .../oidc/metadata/cache/package-info.java          |  22 +
 .../oidc/metadata/filter/MetadataFilter.java       |   5 +-
 .../oidc/metadata/filter/package-info.java         |  22 +
 .../metadata/cache/impl/DefaultMetadataCache.java  | 736 +++++++++++++++++++++
 ...oviderMetadataCriteriaToIdentifierStrategy.java |  42 ++
 ...OIDCProviderMetadataExpirationTimeStrategy.java |  45 ++
 ...oviderMetadataIdentifierExtractionStrategy.java |  35 +
 .../metadata/cache/impl/MetadataCacheBuilder.java  | 341 ++++++++++
 .../impl/OIDCProviderMetadataCacheFactoryBean.java |  43 ++
 .../oidc/metadata/cache/impl/package-info.java     |  22 +
 ...va => AbstractDynamicHTTPFetchingStrategy.java} | 219 +++---
 .../impl/AbstractDynamicOIDCMetadataResolver.java  | 132 ++++
 ...bstractDynamicOIDCProviderMetadataResolver.java | 592 -----------------
 .../impl/AbstractFileOIDCEntityResolver.java       |  17 +
 .../impl/AbstractOIDCMetadataResolver.java         | 248 ++++---
 .../oidc/metadata/impl/DefaultBackingStore.java    |  78 ++-
 .../metadata/impl/DefaultDynamicBackingStore.java  |  48 --
 .../impl/DynamicOIDCProviderMetadataResolver.java  |  43 ++
 ...HTTPProviderConfigurationFetchingStrategy.java} | 134 +---
 .../impl/ReloadingProviderMetadataProvider.java    |   4 +-
 .../cache/impl/DefaultMetadataCacheTest.java       | 386 +++++++++++
 .../ManuallyTriggeredScheduledExecutorService.java | 336 ++++++++++
 .../oidc/metadata/cache/impl/ScheduledTask.java    | 123 ++++
 ...> DynamicOIDCProviderMetadataResolverTest.java} | 138 +++-
 .../src/test/resources/logback-test.xml            |   2 +-
 37 files changed, 3086 insertions(+), 1098 deletions(-)

diff --git a/oidc-common-dist/src/main/assembly/plugin-assembly-tgz.xml b/oidc-common-dist/src/main/assembly/plugin-assembly-tgz.xml
index fcef674..47cbe9a 100644
--- a/oidc-common-dist/src/main/assembly/plugin-assembly-tgz.xml
+++ b/oidc-common-dist/src/main/assembly/plugin-assembly-tgz.xml
@@ -1,5 +1,5 @@
 <assembly>
-	<id>assembly</id>
+	<id>assembly-tar</id>
 	<formats>
 		<format>tar.gz</format>
 	</formats>
diff --git a/oidc-common-dist/src/main/assembly/plugin-assembly-zip.xml b/oidc-common-dist/src/main/assembly/plugin-assembly-zip.xml
index dc27f6e..9853d48 100644
--- a/oidc-common-dist/src/main/assembly/plugin-assembly-zip.xml
+++ b/oidc-common-dist/src/main/assembly/plugin-assembly-zip.xml
@@ -1,5 +1,5 @@
 <assembly>
-	<id>assembly</id>
+	<id>assembly-zip</id>
 	<formats>
 		<format>zip</format>
 	</formats>
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
new file mode 100644
index 0000000..15c06d2
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/AbstractEvaluableMetadataCriterion.java
@@ -0,0 +1,60 @@
+package net.shibboleth.oidc.metadata;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Base class for all metadata criterion classes. If the correct object type this criterion accepts is not provided, 
+ * {@link #test(Object)} returns ({@code defaultResultOnWrongType}). 
+ *
+ * @param <T> The metadata type this criterion accepts.
+ */
+public abstract class AbstractEvaluableMetadataCriterion<T> implements EvaluableMetadataCriterion<T> {
+    
+    /** Object type. */
+    @Nonnull private final Class<T> objectType;
+    
+    /** What should the default return type be if the wrong type <T> is supplied.*/
+    @Nonnull private final boolean defaultResultOnWrongType;
+    
+    /**
+     * 
+     * Constructor.
+     *
+     * @param claz the class this criterion accepts.
+     */
+    protected AbstractEvaluableMetadataCriterion(@Nonnull final Class<T> claz,
+            @Nonnull  final boolean defaultResult) {
+        objectType = Constraint.isNotNull(claz, "Object type cannot be null");
+        defaultResultOnWrongType = Constraint.isNotNull(defaultResult, "Default result on wrong type can not be null");
+    }
+    
+    /**
+     * Get the object type this criterion accepts.
+     * 
+     * @return the object type.
+     */
+    @Nonnull public Class<T> getType() {
+        return objectType;
+    }
+
+    
+    @Override
+    public boolean test(T t) {
+        if (objectType.isInstance(t)) {
+            return doTest(t);        
+        }       
+        return defaultResultOnWrongType;
+    }
+    
+    /**
+     * Evaluates the predicate on the given argument. Concrete implementations should override this method. 
+     * 
+     * @param metadata the metadata.
+     * 
+     * @return true if the predicate is true, false otherwise.
+     */
+    public abstract boolean doTest(final T metadata);
+    
+}
\ No newline at end of file
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 b68b68c..bd21547 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
@@ -1,11 +1,35 @@
+/*
+ * 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;
 
+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.
  * 
  * @param <I> the identifier type
  * @param <T> the type of object stored, referenced by the key.
@@ -17,14 +41,44 @@ public interface BackingStore<I, T> {
      * 
      * @return the index.
      */
-    @Nonnull public Map<I, List<T>> getIndexedDescriptors();
+    @Nonnull Map<I, List<T>> getIndexedValues();
     
     /**
      * Get the list of ordered values.
      * 
      * @return the list of ordered values.
      */
-    @Nonnull public List<T> getOrderedDescriptors();
+    @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/DynamicBackingStore.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicBackingStore.java
deleted file mode 100644
index c4d4403..0000000
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicBackingStore.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package net.shibboleth.oidc.metadata;
-
-import javax.annotation.Nonnull;
-
-/**
- * Specialized metadata backing store implementation for dynamic metadata resolvers.
- */
-public interface DynamicBackingStore<Identifier, Type> extends BackingStore<Identifier, Type> {
-    
-    /**
-    * Get the management data for the specified identifier. If the management data does not exist
-    * it should be created.
-    * 
-    * <p> Should do so in a thread-safe way.</p>
-    * 
-    * @param identifier the identifier of the entity to find management data aboout
-    * @return the corresponding management data
-    */
-   @Nonnull public MetadataManagementData<Identifier> computeManagementDataIfAbsent(@Nonnull final Identifier identifier);
-
-}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCMetadataResolver.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCMetadataResolver.java
new file mode 100644
index 0000000..26ca82c
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCMetadataResolver.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Marker interface for {@link OIDCMetadataResolver} implementations which resolve
+ * metadata by dynamically querying for the requested data individually at the time of the
+ * resolution operation, for example by invoking a request to the well-known provider configuration
+ * endpoint. Implementations may cache the results of previous resolutions so that 
+ * subsequent queries may be answered locally.
+ * 
+ * @param <T> the metadata type
+ */
+public interface DynamicOIDCMetadataResolver<T> extends OIDCMetadataResolver<T> {
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCProviderMetadataResolver.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCProviderMetadataResolver.java
deleted file mode 100644
index 9277068..0000000
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/DynamicOIDCProviderMetadataResolver.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.shibboleth.oidc.metadata;
-
-/**
- * Marker interface for {@link ProviderMetadataResolver} implementations which resolve
- * metadata by dynamically querying for the requested data individually at the time of the
- * resolution operation, for example by invoking a request to the well-known provider configuration
- * endpoint. Implementations may cache the results of previous resolutions so that 
- * subsequent queries may be answered locally.
- */
-public interface DynamicOIDCProviderMetadataResolver<ProductType, CriteriaType> extends OIDCMetadataResolver<ProductType, CriteriaType> {
-
-}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/EvaluableMetadataCriterion.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/EvaluableMetadataCriterion.java
new file mode 100644
index 0000000..2a0b963
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/EvaluableMetadataCriterion.java
@@ -0,0 +1,34 @@
+/*
+ * 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.function.Predicate;
+
+import net.shibboleth.utilities.java.support.resolver.Criterion;
+
+/**
+ * Marker interface for evaluable metadata criteria. 
+ *
+ * @param <T> The metadata type this criterion applies to. 
+ */
+public interface EvaluableMetadataCriterion<T> extends Predicate<T>, Criterion {    
+    
+}
+
+
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 0a1fe72..6a94131 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,24 +1,45 @@
 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
+ * 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.
+ */
 
 import java.time.Duration;
 import java.time.Instant;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.locks.StampedLock;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
-/** Class that holds management data about an entities metadata.*/
+/** 
+ * Class that holds management data about an entities metadata.
+ * 
+ * @param <MetadataIdentifier> the key to the stored object. 
+ * 
+ */
 //TODO removed negative lookup cache.
+//TODO change to a generic ObjectManagmentData type - if we want to broaden beyond metadata.
 public class MetadataManagementData<MetadataIdentifier> {
     
     /** The identifier of the entity managed by this instance. */
-    private final MetadataIdentifier id;
+    @Nonnull private final MetadataIdentifier id;
     
     /** Last update time of the associated metadata. */
-    private Instant lastUpdateTime;
+    @Nullable private Instant lastUpdateTime;
     
     /** Expiration time of the associated metadata. */
     private Instant expirationTime;
@@ -29,21 +50,19 @@ public class MetadataManagementData<MetadataIdentifier> {
     /** The last time at which the entity's backing store data was accessed. */
     private Instant lastAccessedTime;
     
-    /** Read-write lock instance which governs access to the entity's backing store data. */
-    private ReadWriteLock readWriteLock;
+    /** Read-write stamped lock which governs access to the metadata's backing store data. */
+    private final StampedLock stmpLock;
     
     /** Constructor. 
      * 
      * @param identifier the entity ID managed by this instance
      * @param maxCacheDuration the maximum cache duration for metadata
      */
-    public MetadataManagementData(@Nonnull final MetadataIdentifier identifier, @Nonnull final Duration maxCacheDuration) {
+    public MetadataManagementData(@Nonnull final MetadataIdentifier identifier) {
         id = Constraint.isNotNull(identifier, "ID was null");
-        final Instant now = Instant.now();
-        expirationTime = now.plus(maxCacheDuration);
-        refreshTriggerTime = now.plus(maxCacheDuration);
+        final Instant now = Instant.now();    
         lastAccessedTime = now;
-        readWriteLock = new ReentrantReadWriteLock(true);
+        stmpLock = new StampedLock();
     }
     
     /**
@@ -78,7 +97,7 @@ public class MetadataManagementData<MetadataIdentifier> {
      * 
      * @return the expiration time
      */
-    @Nonnull public Instant getExpirationTime() {
+    @Nullable public Instant getExpirationTime() {
         return expirationTime;
     }
 
@@ -96,7 +115,7 @@ public class MetadataManagementData<MetadataIdentifier> {
      * 
      * @return the refresh trigger time
      */
-    @Nonnull public Instant getRefreshTriggerTime() {
+    @Nullable public Instant getRefreshTriggerTime() {
         return refreshTriggerTime;
     }
 
@@ -114,7 +133,7 @@ public class MetadataManagementData<MetadataIdentifier> {
      * 
      * @return last access time
      */
-    @Nonnull public Instant getLastAccessedTime() {
+    @Nullable public Instant getLastAccessedTime() {
         return lastAccessedTime;
     }
     
@@ -126,12 +145,12 @@ public class MetadataManagementData<MetadataIdentifier> {
     }
     
     /**
-     * Get the read-write lock instance which governs access to the entity's backing store data. 
+     * Get the read-write lock instance which governs access to the metadata's backing store data. 
      * 
      * @return the lock instance
      */
-    @Nonnull public ReadWriteLock getReadWriteLock() {
-        return readWriteLock;
+    @Nonnull public StampedLock getStampLock() {
+        return stmpLock;
     }
 
 }
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/OIDCMetadataResolver.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/OIDCMetadataResolver.java
index b0e40e4..48ac035 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/OIDCMetadataResolver.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/OIDCMetadataResolver.java
@@ -1,6 +1,7 @@
 package net.shibboleth.oidc.metadata;
 
 import net.shibboleth.utilities.java.support.component.IdentifiedComponent;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.Resolver;
 
 
@@ -8,11 +9,8 @@ import net.shibboleth.utilities.java.support.resolver.Resolver;
  * Generic interface for OIDC Metadata resolvers which process specified criteria and produce some implementation-specific
  * result information.
  * 
- * @param <MetadataType> the type of objects produced by this metadata resolver
- * @param <CriteriaType> the type of criteria to process during resolution
+ * @param <T> the type of objects produced by this metadata resolver
  */
-// TODO do we need a specific resolver interface with only generic types? why not straight to Resolver.
-public interface OIDCMetadataResolver<MetadataType, CriteriaType> 
-        extends Resolver<MetadataType, CriteriaType>, IdentifiedComponent {
+public interface OIDCMetadataResolver<T> extends Resolver<T, CriteriaSet>, IdentifiedComponent {
 
 }
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
new file mode 100644
index 0000000..3918eb0
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCache.java
@@ -0,0 +1,39 @@
+package net.shibboleth.oidc.metadata.cache;
+
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/**
+ * 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. 
+     * 
+     * <p>The implementing method is required to be thread safe. The fetching function must be called within an
+     * appropriate lock.</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;
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCacheException.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCacheException.java
new file mode 100644
index 0000000..83a2958
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/MetadataCacheException.java
@@ -0,0 +1,48 @@
+package net.shibboleth.oidc.metadata.cache;
+
+import javax.annotation.Nullable;
+
+/**
+ * Base exception for metadata cache related errors.
+ */
+public class MetadataCacheException extends Exception {
+
+    /** SerialUID */
+    private static final long serialVersionUID = 4749191928630000314L;
+
+    /**
+     * Constructor.
+     */
+    public MetadataCacheException() {
+        super();
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     */
+    public MetadataCacheException(@Nullable final String message) {
+        super(message);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param wrappedException exception to be wrapped by this one
+     */
+    public MetadataCacheException(@Nullable final Exception wrappedException) {
+        super(wrappedException);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param message exception message
+     * @param wrappedException exception to be wrapped by this one
+     */
+    public MetadataCacheException(@Nullable final String message, @Nullable final Exception wrappedException) {
+        super(message, wrappedException);
+    }
+
+}
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/package-info.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/package-info.java
new file mode 100644
index 0000000..278486d
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/cache/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * 
+ * Metadata cache API.  
+ */
+package net.shibboleth.oidc.metadata.cache;
\ No newline at end of file
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java
index 74706d3..accb3dc 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/MetadataFilter.java
@@ -15,13 +15,14 @@ import org.opensaml.core.xml.XMLObject;
  * digital signature.
  * </p>
  * <p>
- * Some example OIDC filters might throw a filter exception or return null if the metadata no longer supports the ES-algorithms etc.
+ * Some example OIDC filters might throw a filter exception or return null if the metadata no longer supports, for example,
+ * the ES-algorithms.
  * </p>
  * 
  * <p>
  * If a filter wishes to completely remove the metadata, or otherwise indicate that it
  * has successfully produced an empty data set from the input metadata, <code>null</code> may be returned
- * by the filter's {@link #filter(XMLObject, MetadataFilterContext)} method.
+ * by the filter's {@link #filter(Object, MetadataFilterContext)} method.
  * </p>
  * 
  * @param <T> The metadata type.
diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/package-info.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/package-info.java
new file mode 100644
index 0000000..e16d054
--- /dev/null
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/filter/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * 
+ * Metadata cache filter API.  
+ */
+package net.shibboleth.oidc.metadata.filter;
\ No newline at end of file
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
new file mode 100644
index 0000000..37e3d22
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCache.java
@@ -0,0 +1,736 @@
+/*
+ * 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;
+
+
+/**
+ * 
+ * The executor service is shutdown when the {@link #doDestroy()} method is called.
+ *
+ * @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/DefaultOIDCProviderMetadataCriteriaToIdentifierStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataCriteriaToIdentifierStrategy.java
new file mode 100644
index 0000000..40291b0
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataCriteriaToIdentifierStrategy.java
@@ -0,0 +1,42 @@
+/*
+ * 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.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.oauth2.sdk.id.Issuer;
+
+import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/** Strategy for extracting the {@link IssuerIDCriterion} from a {@link CriteriaSet}.*/
+public class DefaultOIDCProviderMetadataCriteriaToIdentifierStrategy implements Function<CriteriaSet, Issuer> {
+
+    @Override
+    @Nullable public Issuer apply(@Nonnull final CriteriaSet criteria) {
+       final IssuerIDCriterion issuerId = criteria.get(IssuerIDCriterion.class);
+       if (issuerId != null) {
+           return issuerId.getIssuerID();
+       }
+       return null;
+    }
+
+}
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
new file mode 100644
index 0000000..fa1fb78
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
@@ -0,0 +1,45 @@
+/*
+ * 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.function.BiFunction;
+
+import javax.annotation.Nonnull;
+
+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> {
+    
+    /** How long after now should the metadata expire.*/
+    @Nonnull private final Duration expiryDuration;
+    
+    public DefaultOIDCProviderMetadataExpirationTimeStrategy(@Nonnull final Duration duration) {
+        expiryDuration = Constraint.isNotNull(duration, "Expiry duration can not be null");
+    }
+
+    @Override
+    public Instant apply(@Nonnull final OIDCProviderMetadata metadata, @Nonnull final Instant now) {
+        return now.plus(expiryDuration);
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataIdentifierExtractionStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataIdentifierExtractionStrategy.java
new file mode 100644
index 0000000..6e86e76
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataIdentifierExtractionStrategy.java
@@ -0,0 +1,35 @@
+/*
+ * 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.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.oauth2.sdk.id.Issuer;
+import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
+
+/** Strategy for finding the Issuer of the given OIDCProviderMetadata.*/
+public class DefaultOIDCProviderMetadataIdentifierExtractionStrategy implements Function<OIDCProviderMetadata, Issuer>{
+
+    @Override
+    public Issuer apply(@Nonnull final OIDCProviderMetadata metadata) {
+        return metadata.getIssuer();
+    }
+
+}
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
new file mode 100644
index 0000000..4536833
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/MetadataCacheBuilder.java
@@ -0,0 +1,341 @@
+/*
+ * 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.List;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.springframework.beans.factory.config.AbstractFactoryBean;
+
+import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/** 
+ * Base metadata cache builder instance.
+ * 
+ * @param <T> the metadata identifier/key
+ * @param <U> the metadata type. 
+ */
+public abstract class MetadataCacheBuilder<T,U> extends AbstractFactoryBean<DefaultMetadataCache<T, U>>{
+    
+    /** Maximum cache duration. */
+    @Nonnull private Duration maxCacheDuration;
+    
+    /** Minimum cache duration. */
+    @Nonnull private Duration minCacheDuration;
+    
+    /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
+    @Positive private Float refreshDelayFactor;
+    
+    /** The maximum idle time for which the resolver will keep data for a given entityID, 
+     * before it is removed. */
+    @Nullable private Duration maxIdleEntityData;
+    
+    /** The interval at which the cleanup task should run. */
+    @Nonnull private Duration cleanupTaskInterval;
+    
+    /** The initial cleanup task delay.*/
+    @Nonnull private Duration initialCleanupTaskDelay;
+    
+    /** Strategy used to extract an identifier from the given metadata.*/
+    @Nullable private Function<U, T> identifierExtractionStrategy;
+    
+    /** Strategy used to compute an expiration time. */
+    @Nullable private BiFunction<U, Instant, Instant> metadataExpirationTimeStrategy;
+    
+    /** Map criteria to identifiers to use as keys to the backing store.*/
+    @Nullable private Function<CriteriaSet, T> criteriaToIdentifierStrategy;
+    
+    /** A strategy to filter metadata. */
+    @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 null, the identifier is never null.
+     */
+    @Nullable private BiConsumer<List<U>, T> metadataBeforeRemovalHook;
+    
+    /** Flag indicating whether idle entity data should be removed. */
+    private boolean removeIdleEntityData;
+    
+    
+    protected MetadataCacheBuilder(){
+        //defaults
+        maxCacheDuration = Duration.ofHours(8);
+        minCacheDuration = Duration.ofMinutes(10);
+        maxIdleEntityData = Duration.ofHours(8);
+        refreshDelayFactor = 0.75f;
+        cleanupTaskInterval = Duration.ofMinutes(30);
+        initialCleanupTaskDelay = Duration.ofMinutes(1);
+        removeIdleEntityData = true;
+        // create a default direct in/out filter
+        metadataFilterStrategy = (metadata, context) -> metadata;
+    }
+    
+    /**
+     * {@inheritDoc}
+     * 
+     * Call destroy on the default cache implementation.
+     */
+    @Override protected void destroyInstance(
+            @Nullable DefaultMetadataCache<T, U> instance) throws Exception {
+        if (instance != null) {
+            instance.destroy();
+        }
+    }
+    
+    /**
+     * 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<U>, T> hook) {        
+        metadataBeforeRemovalHook = hook;
+    }
+    
+    /**
+     * Get the metadata before removal hook. Could be {@literal null}.
+     * 
+     * @return the hook.
+     */
+    @Nullable public BiConsumer<List<U>, T> getMetadataBeforeRemovalHook() {
+        return metadataBeforeRemovalHook;
+    }
+    
+    /**
+     * Set the metadata filtering strategy.
+     * 
+     * @param strategy the metadata filtering strategy.
+     */
+    public void setMetadataFilterStrategy(@Nonnull final BiFunction<U, MetadataFilterContext, U> strategy) {        
+        this.metadataFilterStrategy = Constraint.isNotNull(strategy, "Metadata filter strategy can not be null");;
+    }
+    
+    /**
+     * Get the metadata filter strategy.
+     * 
+     * @return the metadata filtering strategy
+     */
+    public BiFunction<U, MetadataFilterContext, U> getMetadataFilterStrategy() {
+        return metadataFilterStrategy;
+    }
+    
+    /**
+     * Get the initial cleanup task delay.
+     * 
+     * @return Returns the initialCleanupTaskDelay.
+     */
+    protected Duration getInitialCleanupTaskDelay() {
+        return initialCleanupTaskDelay;
+    }
+
+    /**
+     * Set the initial cleanup task delay.
+     * 
+     * @param delay The initialCleanupTaskDelay to set.
+     */
+    public void setInitialCleanupTaskDelay(@Nonnull final Duration delay) {
+        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 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) {       
+        removeIdleEntityData = flag;
+    }
+    
+    protected boolean isRemoveIdleEntityData() {
+        return removeIdleEntityData;
+    }
+    
+    public void setIdentifierExtractionStrategy(@Nonnull final Function<U, T> strategy) {        
+        identifierExtractionStrategy = Constraint.isNotNull(strategy, "Strategy can not be null");
+    }
+    
+    @Nullable protected Function<U, T> getIdentifierExtractionStrategy() {
+        return identifierExtractionStrategy;
+    }
+    
+    public void setMetadataExpirationTimeStrategy(
+            @Nonnull final BiFunction<U, Instant, Instant> strategy) {        
+        metadataExpirationTimeStrategy = Constraint.isNotNull(strategy, "Strategy can not be null");
+    }
+    
+    @Nullable protected BiFunction<U, Instant, Instant> getMetadataExpirationTimeStrategy() {
+        return metadataExpirationTimeStrategy;
+    }
+    
+    public void setCriteriaToIdentifierStrategy(@Nonnull final Function<CriteriaSet, T> strategy) {
+        criteriaToIdentifierStrategy =  Constraint.isNotNull(strategy,"Criteria to identifier strategy can not be null");
+    }
+    
+    @Nullable protected Function<CriteriaSet, T> getCriteriaToIdentifierStrategy() {
+        return criteriaToIdentifierStrategy;
+    }
+    
+    /**
+     * Get the interval at which the cleanup task should run.
+     * 
+     * <p>Defaults to: 30 minutes.</p>
+     * 
+     * @return return the interval
+     */
+    @Nonnull protected Duration getCleanupTaskInterval() {
+        return cleanupTaskInterval;
+    }
+
+    /**
+     * 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) {        
+        Constraint.isNotNull(interval, "Cleanup task interval may not be null");
+        Constraint.isFalse(interval.isNegative() || interval.isZero(), "Cleanup task interval must be positive");
+        
+        cleanupTaskInterval = interval;
+    }
+    
+    /**
+     * Get 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>
+     * 
+     * @return return the maximum idle time
+     */
+    @Nonnull protected Duration getMaxIdleEntityData() {
+        return maxIdleEntityData;
+    }
+
+    /**
+     * 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) {
+
+        Constraint.isNotNull(max, "Max idle time cannot be null");
+        Constraint.isFalse(max.isNegative(), "Max idle time cannot be negative");
+
+        maxIdleEntityData = max;
+    }
+    
+    /**
+     *  Get the maximum cache duration for metadata.
+     *  
+     *  <p>Defaults to: 8 hours.</p>
+     *  
+     * @return the maximum cache duration
+     */
+    @Nonnull protected Duration getMaxCacheDuration() {
+        return maxCacheDuration;
+    }
+
+    /**
+     *  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) {
+        
+        Constraint.isNotNull(duration, "Duration cannot be null");
+        Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
+        
+        maxCacheDuration = duration;
+    }
+    
+    /**
+     *  Get the minimum cache duration for metadata.
+     *  
+     *  <p>Defaults to: 10 minutes.</p>
+     *  
+     * @return the minimum cache duration
+     */
+    @Nonnull protected Duration getMinCacheDuration() {
+        return minCacheDuration;
+    }
+    
+
+    /**
+     *  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) {
+        Constraint.isNotNull(duration, "Duration cannot be null");
+        Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
+        
+        minCacheDuration = duration;
+    }
+    
+    /**
+     * Gets the delay factor used to compute the next refresh time.
+     * 
+     * <p>Defaults to:  0.75.</p>
+     * 
+     * @return delay factor used to compute the next refresh time
+     */
+    @Nonnull protected Float getRefreshDelayFactor() {
+        return refreshDelayFactor;
+    }
+    
+
+    /**
+     * 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) {
+
+        if (factor <= 0 || factor >= 1) {
+            throw new IllegalArgumentException("Refresh delay factor must be a number between 0.0 and 1.0, exclusive");
+        }
+
+        refreshDelayFactor = factor;
+    }
+
+}
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
new file mode 100644
index 0000000..8d14d94
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/OIDCProviderMetadataCacheFactoryBean.java
@@ -0,0 +1,43 @@
+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;
+
+/**
+ * Factory bean to create an OIDC specific metadata cache.
+ */
+public class OIDCProviderMetadataCacheFactoryBean extends MetadataCacheBuilder<Issuer, OIDCProviderMetadata> {
+
+    //TODO raw type?
+    @SuppressWarnings("rawtypes")
+    @Override
+    public Class<DefaultMetadataCache> getObjectType() {
+        return DefaultMetadataCache.class;
+    }
+
+    @Override
+    protected DefaultMetadataCache<Issuer, 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;
+    }
+   
+
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/package-info.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/package-info.java
new file mode 100644
index 0000000..a9d747a
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/**
+ * 
+ * Metadata cache implementation.  
+ */
+package net.shibboleth.oidc.metadata.cache.impl;
\ No newline at end of file
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCHTTPProviderMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java
similarity index 60%
rename from oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCHTTPProviderMetadataResolver.java
rename to oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java
index adf9687..405b968 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCHTTPProviderMetadataResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicHTTPFetchingStrategy.java
@@ -1,25 +1,20 @@
 package net.shibboleth.oidc.metadata.impl;
 
 import java.io.IOException;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.ResponseHandler;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.client.protocol.HttpClientContext;
-import org.apache.http.util.EntityUtils;
-import org.opensaml.saml.metadata.resolver.impl.AbstractDynamicHTTPMetadataResolver;
 import org.opensaml.security.httpclient.HttpClientSecurityParameters;
 import org.opensaml.security.httpclient.HttpClientSecuritySupport;
 import org.slf4j.Logger;
@@ -28,28 +23,18 @@ import org.slf4j.MDC;
 
 import com.google.common.base.Strings;
 import com.google.common.net.MediaType;
-import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotLive;
 import net.shibboleth.utilities.java.support.annotation.constraint.Unmodifiable;
-import net.shibboleth.utilities.java.support.collection.LazySet;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
 import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.net.MediaTypeSupport;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
-import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
-/**
- * Abstract subclass for dynamic metadata resolvers that implement OIDC provider
- * metadata resolution based on HTTP requests.
- * 
- */
-//TODO this is a singleton, and needs guarding?
-public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType> 
-        extends AbstractDynamicOIDCProviderMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType> {
+public abstract class AbstractDynamicHTTPFetchingStrategy<CriteriaType, MetadataType> 
+    extends AbstractIdentifiableInitializableComponent implements Function<CriteriaType, MetadataType> {
     
     /** Default list of supported content MIME types. */
     private static final String[] DEFAULT_CONTENT_TYPES = new String[] {"application/json",
@@ -58,10 +43,10 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
     /** MDC attribute representing the current request URI. Will be available during the execution of the 
      * configured {@link ResponseHandler}. */
     public static final String MDC_ATTRIB_CURRENT_REQUEST_URI = 
-            AbstractDynamicOIDCHTTPProviderMetadataResolver.class.getName() + ".currentRequestURI";
+            AbstractDynamicHTTPFetchingStrategy.class.getName() + ".currentRequestURI";
     
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractDynamicOIDCHTTPProviderMetadataResolver.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractDynamicHTTPFetchingStrategy.class);
     
     /** HTTP Client used to pull the configuration information. */
     @Nonnull private final HttpClient httpClient;
@@ -88,80 +73,12 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
      *
      * @param client the instance of {@link HttpClient} used to fetch remote OIDC metadata
      */
-    protected AbstractDynamicOIDCHTTPProviderMetadataResolver(@Nonnull final HttpClient client, 
+    protected AbstractDynamicHTTPFetchingStrategy(@Nonnull final HttpClient client, 
             @Nonnull final ResponseHandler<MetadataType> handler) {
         responseHandler = Constraint.isNotNull(handler, "Response handler can not be null");
         httpClient = Constraint.isNotNull(client, "HTTP Client can not be null");
     }
     
-    /**
-     * Get the list of supported MIME types for use in Accept request header and validation of 
-     * response Content-Type header.
-     * 
-     * @return the supported content types
-     */
-    @NonnullAfterInit @NotLive @Unmodifiable
-    public List<String> getSupportedContentTypes() {
-        return supportedContentTypes;
-    }
-    
-    /**
-     * Set the list of supported MIME types for use in Accept request header and validation of 
-     * response Content-Type header. Values will be effectively lower-cased at runtime.
-     * 
-     * @param types the new supported content types to set
-     */
-    public void setSupportedContentTypes(@Nullable final List<String> types) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        if (types == null) {
-            supportedContentTypes = Collections.emptyList();
-        } else {
-            supportedContentTypes = StringSupport.normalizeStringCollection(types)
-                    .stream()
-                    .filter(s -> s != null)
-                    .map(String::toLowerCase)
-                    .collect(Collectors.toUnmodifiableList());
-        }
-    }
-    
-    /**
-    * Get the list of supported MIME {@link MediaType} instances used in validation of 
-    * the response Content-Type header.
-    * 
-    * <p>
-    * Is generated at init time from {@link #getSupportedContentTypes()}.
-    * </p>
-    * 
-    * @return the supported content types
-    */
-   @NonnullAfterInit @NotLive @Unmodifiable
-   protected Set<MediaType> getSupportedMediaTypes() {
-       return supportedMediaTypes;
-   }
-    
-    @Override
-    protected void initMetadataResolver() throws ComponentInitializationException {
-        super.initMetadataResolver();
-        
-        if (getSupportedContentTypes() == null) {
-            setSupportedContentTypes(Arrays.asList(DEFAULT_CONTENT_TYPES));
-        }
-        
-        if (! getSupportedContentTypes().isEmpty()) {
-            supportedContentTypesValue = StringSupport.listToStringValue(getSupportedContentTypes(), ", ");
-            supportedMediaTypes = new LazySet<>();
-            for (final String contentType : getSupportedContentTypes()) {
-                supportedMediaTypes.add(MediaType.parse(contentType));
-            }
-        } else {
-            supportedMediaTypes = Collections.emptySet();
-        }
-        
-        log.debug("{} Supported content types are: {}", getLogPrefix(), getSupportedContentTypes());
-    }
-    
-    
     /**
      * Set an instance of {@link HttpClientSecurityParameters} which provides various parameters to influence
      * the security behavior of the HttpClient instance.
@@ -203,24 +120,43 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
         httpClientSecurityParameters = params;
     }
     
-    @Override
-    protected void doDestroy() {
-        httpClientSecurityParameters = null;
-        
-        supportedContentTypes = null;
-        supportedContentTypesValue = null;
-        supportedMediaTypes = null;
-        
-        super.doDestroy();
+    /**
+     * Get the list of supported MIME types for use in Accept request header and validation of 
+     * response Content-Type header.
+     * 
+     * @return the supported content types
+     */
+    @NonnullAfterInit @NotLive @Unmodifiable
+    public List<String> getSupportedContentTypes() {
+        return supportedContentTypes;
     }
     
+    /**
+     * Set the list of supported MIME types for use in Accept request header and validation of 
+     * response Content-Type header. Values will be effectively lower-cased at runtime.
+     * 
+     * @param types the new supported content types to set
+     */
+    public void setSupportedContentTypes(@Nullable final List<String> types) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        if (types == null) {
+            supportedContentTypes = Collections.emptyList();
+        } else {
+            supportedContentTypes = StringSupport.normalizeStringCollection(types)
+                    .stream()
+                    .filter(s -> s != null)
+                    .map(String::toLowerCase)
+                    .collect(Collectors.toUnmodifiableList());
+        }
+    }
+
     @Override
-    @Nullable protected MetadataType fetchFromOriginSource(@Nonnull final CriteriaType criteria) 
-            throws IOException {
-            
+    @Nullable public MetadataType apply(@Nonnull final CriteriaType criteria) {
+        log.info("{} fetching metadata based on criteria: {}", getId(), criteria);
         final HttpUriRequest request = buildHttpRequest(criteria);
         if (request == null) {
-            log.debug("{} Could not build request based on input criteria, unable to query", getLogPrefix());
+            log.debug("Could not build request based on input criteria, unable to query");
             return null;
         }
         
@@ -231,50 +167,53 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
             final MetadataType result = httpClient.execute(request, responseHandler, context);
             HttpClientSecuritySupport.checkTLSCredentialEvaluated(context, request.getURI().getScheme());
             return result;
+        } catch (IOException e) {
+            log.warn("Unable to fetch metadata from remote HTTP source",e);
+            return null;
         } finally {
             MDC.remove(MDC_ATTRIB_CURRENT_REQUEST_URI);
         }
     }
     
     /**
-     * Build the {@link HttpClientContext} instance which will be used to invoke the {@link HttpClient} request.
-     * 
-     * @param request the current HTTP request
-     * 
-     * @return a new instance of {@link HttpClientContext}
-     */
-    protected HttpClientContext buildHttpClientContext(@Nonnull final HttpUriRequest request) {
-        final HttpClientContext context = HttpClientContext.create();
-        
-        HttpClientSecuritySupport.marshalSecurityParameters(context, httpClientSecurityParameters, true);
-        HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(context, request);
-        
-        return context;
-    }
+    * Build the {@link HttpClientContext} instance which will be used to invoke the {@link HttpClient} request.
+    * 
+    * @param request the current HTTP request
+    * 
+    * @return a new instance of {@link HttpClientContext}
+    */
+   private HttpClientContext buildHttpClientContext(@Nonnull final HttpUriRequest request) {
+       final HttpClientContext context = HttpClientContext.create();
+       
+       HttpClientSecuritySupport.marshalSecurityParameters(context, httpClientSecurityParameters, true);
+       HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(context, request);
+       
+       return context;
+   }
     
     /**
-     * Build an appropriate instance of {@link HttpUriRequest} based on the input criteria set.
-     * 
-     * @param criteria the input criteria set
-     * @return the newly constructed request, or null if it can not be built from the supplied criteria
-     */
-    @Nullable protected HttpUriRequest buildHttpRequest(@Nonnull final CriteriaType criteria) {
-        final String url = buildRequestURL(criteria);
-        log.debug("{} Built request URL of: {}", getLogPrefix(), url);
-        
-        if (url == null) {
-            log.debug("{} Could not construct request URL from input criteria, unable to query", getLogPrefix());
-            return null;
-        }
-            
-        final HttpGet getMethod = new HttpGet(url);
-      
-        if (!Strings.isNullOrEmpty(supportedContentTypesValue)) {
-            getMethod.addHeader("Accept", supportedContentTypesValue);
-        }
-        
-        return getMethod;
-    }
+    * Build an appropriate instance of {@link HttpUriRequest} based on the input criteria set.
+    * 
+    * @param criteria the input criteria set
+    * @return the newly constructed request, or null if it can not be built from the supplied criteria
+    */
+   @Nullable private HttpUriRequest buildHttpRequest(@Nonnull final CriteriaType criteria) {
+       final String url = buildRequestURL(criteria);
+       log.debug("Built request URL of: {}", url);
+       
+       if (url == null) {
+           log.debug("Could not construct request URL from input criteria, unable to query");
+           return null;
+       }
+           
+       final HttpGet getMethod = new HttpGet(url);
+     
+       if (!Strings.isNullOrEmpty(supportedContentTypesValue)) {
+           getMethod.addHeader("Accept", supportedContentTypesValue);
+       }
+       
+       return getMethod;
+   }
     
     /**
      * Build the request URL based on the input criteria set.
@@ -283,7 +222,5 @@ public abstract class AbstractDynamicOIDCHTTPProviderMetadataResolver<MetadataId
      * @return the request URL, or null if it can not be built based on the supplied criteria
      */
     @Nullable protected abstract String buildRequestURL(@Nonnull final CriteriaType criteria);
-    
-    
 
 }
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
new file mode 100644
index 0000000..291595b
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCMetadataResolver.java
@@ -0,0 +1,132 @@
+/*
+ * 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.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.slf4j.Logger;
+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;
+
+/**
+ * Abstract subclass for metadata resolvers that resolve provider metadata dynamically, as needed and on demand.
+ * 
+ * Is instrumented to collect timming metrics.
+ * 
+ * Has a cache to ...
+ * 
+ * @param <IdentifierType> The identifier type in the backing store
+ * @param <MetadataType> The metadata type in the backing store
+ */
+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.*/
+    protected AbstractDynamicOIDCMetadataResolver(
+            @Nonnull final MetadataCache<MetadataType> metadataCache,
+            @Nonnull final Function<CriteriaSet, MetadataType> fetchingStrategy) {
+        super(metadataCache);
+        metadataFetchingStrategy = Constraint.isNotNull(fetchingStrategy, "Metadata fetching strategy can not be null");
+    }
+    
+    
+    /** {@inheritDoc} */
+    @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();
+//            
+//            if (getPersistentCacheKeyGenerator() == null) {
+//                setPersistentCacheKeyGenerator(new DefaultCacheKeyGenerator());
+//            }
+//            
+//            if (getInitializationFromCachePredicate() == null) {
+//                setInitializationFromCachePredicate(Predicates.alwaysTrue());
+//            }
+//            
+//            persistentCacheInitMetrics = new PersistentCacheInitializationMetrics();
+//            if (isPersistentCachingEnabled()) {
+//                persistentCacheInitMetrics.enabled = true;
+//                if (isInitializeFromPersistentCacheInBackground()) {
+//                    log.debug("{} Initializing from the persistent cache in the background in {} ms", 
+//                            getLogPrefix(), getBackgroundInitializationFromCacheDelay());
+//                    final TimerTask initTask = new TimerTask() {
+//                        public void run() {
+//                            initializeFromPersistentCache();
+//                        }
+//                    };
+//                    taskTimer.schedule(initTask, getBackgroundInitializationFromCacheDelay().toMillis());
+//                } else {
+//                    log.debug("{} Initializing from the persistent cache in the foreground", getLogPrefix());
+//                    initializeFromPersistentCache();
+//                }
+//            }
+       
+
+        } finally {
+            
+        }
+    }
+       
+    
+    /** {@inheritDoc} */
+    @Override
+    @Nonnull public Iterable<MetadataType> resolve(@Nonnull final CriteriaSet criteria) throws ResolverException {
+        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+               
+        //final Context contextResolve = MetricsSupport.startTimer(timerResolve);
+        try {            
+            final List<MetadataType> metadata = getCache()
+                    .getOrFetchIfAbsent(criteria, metadataFetchingStrategy);            
+            return predicateFilterCandidates(metadata, criteria, false);
+            
+        } catch (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/AbstractDynamicOIDCProviderMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCProviderMetadataResolver.java
deleted file mode 100644
index 4a7709c..0000000
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractDynamicOIDCProviderMetadataResolver.java
+++ /dev/null
@@ -1,592 +0,0 @@
-package net.shibboleth.oidc.metadata.impl;
-
-import java.io.IOException;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.locks.Lock;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.core.xml.XMLObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
-
-import net.shibboleth.oidc.metadata.DynamicBackingStore;
-import net.shibboleth.oidc.metadata.DynamicOIDCProviderMetadataResolver;
-import net.shibboleth.oidc.metadata.MetadataManagementData;
-import net.shibboleth.oidc.metadata.filter.FilterException;
-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.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.ResolverException;
-
-/**
- * Abstract subclass for metadata resolvers that resolve provider metadata dynamically, as needed and on demand.
- * 
- * Is instrumented to collect timming metrics.
- * 
- * Has a cache to ...
- * 
- * @param <MetadataIdentifier> The identifier type in the backing store
- * @param <MetadataType> The metadata type in the backing store
- * @param <CriteriaType> 
- */
-public abstract class AbstractDynamicOIDCProviderMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType> 
-                                    extends AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType> 
-                                    implements DynamicOIDCProviderMetadataResolver<MetadataType, CriteriaType> {
-    
-    /** Class logger. */
-    private final Logger log = LoggerFactory.getLogger(AbstractDynamicOIDCProviderMetadataResolver.class);
-    
-    /** Flag used to track state of whether currently initializing or not. */
-    //TODO is this actually used/needed?
-    private boolean initializing;
-    
-    /** Maximum cache duration. */
-    @Nonnull private Duration maxCacheDuration;
-    
-    /** Minimum cache duration. */
-    @Nonnull private Duration minCacheDuration;
-    
-    /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
-    @Positive private Float refreshDelayFactor;
-
-    
-    
-    /** Constructor.*/
-    protected AbstractDynamicOIDCProviderMetadataResolver() {
-        super();
-        
-        maxCacheDuration = Duration.ofHours(8);
-        minCacheDuration = Duration.ofMinutes(10);
-        refreshDelayFactor = 0.75f;
-    }
-    
-    /**
-     *  Get the maximum cache duration for metadata.
-     *  
-     *  <p>Defaults to: 8 hours.</p>
-     *  
-     * @return the maximum cache duration
-     */
-    @Nonnull public Duration getMaxCacheDuration() {
-        return maxCacheDuration;
-    }
-
-    /**
-     *  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;
-    }
-    
-    /**
-     *  Get the minimum cache duration for metadata.
-     *  
-     *  <p>Defaults to: 10 minutes.</p>
-     *  
-     * @return the minimum cache duration
-     */
-    @Nonnull public Duration getMinCacheDuration() {
-        return minCacheDuration;
-    }
-    
-
-    /**
-     *  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;
-    }
-    
-    /**
-     * Gets the delay factor used to compute the next refresh time.
-     * 
-     * <p>Defaults to:  0.75.</p>
-     * 
-     * @return delay factor used to compute the next refresh time
-     */
-    @Nonnull public Float getRefreshDelayFactor() {
-        return refreshDelayFactor;
-    }
-
-    /**
-     * 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;
-    }
-    
-    /**
-     * Fetch the metadata from the origin source.
-     * 
-     * @param criteria the input criteria set
-     * @return the resolved metadata parsed as a {@link OIDCProviderMetadata}, or null if metadata could not be fetched
-     * @throws IOException if there is a fatal error fetching metadata from the origin source
-     */
-    @Nullable protected abstract MetadataType fetchFromOriginSource(@Nonnull final CriteriaType criteria) 
-            throws IOException;
-
-
-    
-    /** {@inheritDoc} */
-    @Override
-    protected void initMetadataResolver() throws ComponentInitializationException {
-        try {
-            initializing = true;
-//            
-//            super.initMetadataResolver();
-//            
-//            initializeMetricsInstrumentation();
-//            
-              setBackingStore(createNewBackingStore());
-//            
-//            if (getPersistentCacheKeyGenerator() == null) {
-//                setPersistentCacheKeyGenerator(new DefaultCacheKeyGenerator());
-//            }
-//            
-//            if (getInitializationFromCachePredicate() == null) {
-//                setInitializationFromCachePredicate(Predicates.alwaysTrue());
-//            }
-//            
-//            persistentCacheInitMetrics = new PersistentCacheInitializationMetrics();
-//            if (isPersistentCachingEnabled()) {
-//                persistentCacheInitMetrics.enabled = true;
-//                if (isInitializeFromPersistentCacheInBackground()) {
-//                    log.debug("{} Initializing from the persistent cache in the background in {} ms", 
-//                            getLogPrefix(), getBackgroundInitializationFromCacheDelay());
-//                    final TimerTask initTask = new TimerTask() {
-//                        public void run() {
-//                            initializeFromPersistentCache();
-//                        }
-//                    };
-//                    taskTimer.schedule(initTask, getBackgroundInitializationFromCacheDelay().toMillis());
-//                } else {
-//                    log.debug("{} Initializing from the persistent cache in the foreground", getLogPrefix());
-//                    initializeFromPersistentCache();
-//                }
-//            }
-//            
-//            cleanupTask = new BackingStoreCleanupSweeper();
-//            // Start with a delay of 1 minute, run at the user-specified interval
-//            taskTimer.schedule(cleanupTask, 1*60*1000, getCleanupTaskInterval().toMillis());
-//
-        } finally {
-            initializing = false;
-        }
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    @Nonnull protected DynamicBackingStore<MetadataIdentifier, MetadataType> createNewBackingStore() {
-        return new DefaultDynamicBackingStore<>(getMaxCacheDuration());
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    @NonnullAfterInit protected DynamicBackingStore<MetadataIdentifier, MetadataType> getBackingStore() {
-        return (DynamicBackingStore<MetadataIdentifier, MetadataType>) super.getBackingStore();
-    }
-    
-    /** {@inheritDoc} */
-    @Override
-    @Nonnull public Iterable<MetadataType> resolve(@Nullable final CriteriaType criteria) throws ResolverException {
-        ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        // Find identifier from criteria (impl specific)
-        
-        // Lock and check cache for metadata relating to entity
-        // // If exists check it should not be refershed. 
-        
-        // if not in cache or requires refresh lookup from source.
-        
-        // Filter candidates
-        
-        //final Context contextResolve = MetricsSupport.startTimer(timerResolve);
-        try {
-            Iterable<MetadataType> candidates = null;
-            
-            final MetadataIdentifier identifier = resolveIdentifier(criteria);
-            if (identifier != null) {
-                log.debug("{} Resolved criteria to identifier: {}", getLogPrefix(), identifier);
-
-                final MetadataManagementData<MetadataIdentifier> mgmtData = 
-                        getBackingStore().computeManagementDataIfAbsent(identifier);
-                final Lock readLock = mgmtData.getReadWriteLock().readLock();
-                try {
-                    readLock.lock();
-
-                    final List<MetadataType> metadata = lookupIdentifier(identifier);
-                    if (metadata.isEmpty()) {
-//                        if (mgmtData.isNegativeLookupCacheActive()) {
-//                            log.debug("{} Did not find requested metadata in backing store, " 
-//                                    + "and negative lookup cache is active, returning empty result", 
-//                                    getLogPrefix());
-//                            return Collections.emptyList();
-//                        }
-                        log.debug("{} Did not find requested metadata in backing store, " 
-                                + "attempting to resolve dynamically", 
-                                getLogPrefix());
-                    } else {
-                        if (shouldAttemptRefresh(mgmtData)) {
-                            log.debug("{} Metadata was indicated to be refreshed based on refresh trigger time", 
-                                    getLogPrefix());
-                        } else {
-                            log.debug("{} Found requested metadata in backing store", getLogPrefix());
-                            candidates = metadata;
-                        }
-                    }
-                } finally {
-                    readLock.unlock();
-                }
-            } else {
-                log.debug("{} Single entityID unresolveable from criteria, will resolve from origin by criteria only",
-                        getLogPrefix());
-            }
-
-            if (candidates == null) {
-                candidates = resolveFromOriginSource(criteria, identifier);
-            }
-
-            //return predicateFilterCandidates(candidates, criteria, false);
-            return candidates;
-        } finally {
-            //MetricsSupport.stopTimer(contextResolve);
-        }
-    }
-    
-    /**
-     * 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<MetadataIdentifier> mgmtData) {
-        return Instant.now().isAfter(mgmtData.getRefreshTriggerTime());
-        
-    }
-    
-    //TODO We do not support fetching without an identifier for now.
-    /**
-     * Fetch metadata from an origin source based on the input criteria, store it in the backing store 
-     * and then return it.
-     * 
-     * @param criteria the input criteria set
-     * @param identifier the previously resolved single identifier
-     * @return the resolved metadata
-     * @throws ResolverException  if there is a fatal error attempting to resolve the metadata
-     */
-    @Nonnull @NonnullElements protected Iterable<MetadataType> resolveFromOriginSource(
-            @Nonnull final CriteriaType criteria, @Nonnull final MetadataIdentifier identifier) throws ResolverException {
-        
-         log.debug("{} Resolving from origin source based on identifier: {}", getLogPrefix(), identifier);
-         final MetadataManagementData<MetadataIdentifier> mgmtData = getBackingStore().computeManagementDataIfAbsent(identifier);
-         final Lock writeLock = mgmtData.getReadWriteLock().writeLock(); 
-         
-         try {
-             writeLock.lock();
-             
-             // It's possible that multiple threads fall into here and attempt to preemptively refresh. 
-             // This check should ensure that only 1 actually successfully does it, b/c the refresh
-             // trigger time will be updated as seen by the subsequent ones. 
-             final List<MetadataType> descriptors = lookupIdentifier(identifier);
-             if (!descriptors.isEmpty() && !shouldAttemptRefresh(mgmtData)) {
-                 log.debug("{} Metadata was resolved and stored by another thread " 
-                         + "while this thread was waiting on the write lock", getLogPrefix());
-                 return descriptors;
-             }
-             log.debug("{} Resolving metadata dynamically for ID: {}", getLogPrefix(), identifier);
-             
-             //final Context contextFetchFromOriginSource = MetricsSupport.startTimer(timerFetchFromOriginSource);
-             MetadataType metadata = null;
-             try {
-                 metadata = fetchFromOriginSource(criteria);
-             } finally {
-                // MetricsSupport.stopTimer(contextFetchFromOriginSource);
-             }
-             
-             if (metadata == null) {
-               //  mgmtData.initNegativeLookupCache();
-                 log.debug("{} No metadata was fetched from the origin source", getLogPrefix());
-    
-                 if (!descriptors.isEmpty()) {
-                     mgmtData.setRefreshTriggerTime(computeRefreshTriggerTime(mgmtData.getExpirationTime(), 
-                             Instant.now()));
-                     log.debug("{} Had existing data, recalculated refresh trigger time as: {}", 
-                             getLogPrefix(), mgmtData.getRefreshTriggerTime());
-                 }
-             } else {
-                // mgmtData.clearNegativeLookupCache();
-                 try {
-                     processNewMetadata(metadata, identifier, false);
-                 } catch (final FilterException e) {
-                     log.error("{} Metadata filtering problem processing new metadata", getLogPrefix(), e);
-                 }
-             }
-             // Return the metadata from the cache if it now exists.
-             return lookupIdentifier(identifier);
-             
-         } catch (final IOException e) {
-             log.error("{} Error fetching metadata from origin source", getLogPrefix(), e);
-             return lookupIdentifier(identifier);
-         } finally {
-             writeLock.unlock();
-         }         
-        
-    }
-    
-    
-    // Happens inside a lock
-    @Nonnull protected void processNewMetadata(@Nonnull final MetadataType metadata, 
-            @Nonnull final MetadataIdentifier expectedIdentifier, final boolean fromPersistentCache) 
-                    throws FilterException, ResolverException {
-        
-        
-        // Filter metadata if required and configured.
-        final MetadataType filteredMetadata = filterMetadata(prepareForFiltering(metadata));
-        if (filteredMetadata == null) {
-            log.info("{} Metadata filtering process produced a null document, resulting in an empty data set", 
-                    getLogPrefix());
-            finalizeMetadataProcessing(metadata);
-            return;
-        }
-        
-        if (!isNewMetadataValid(filteredMetadata, expectedIdentifier)) {
-            return;
-        }
-        
-        // determine identifier
-        final MetadataIdentifier identifier = extractIdentifier(metadata);
-        if (identifier == null) {
-            //FIXME do what?
-        }
-        
-        // remove the existing
-        removeByIdentifier(identifier, getBackingStore());
-        
-        // add new metadata back to ordered list
-        getBackingStore().getOrderedDescriptors().add(metadata);
-        
-        // add new metadata to index
-        List<MetadataType> existingMetadata = getBackingStore().getIndexedDescriptors().get(identifier);
-        if (existingMetadata == null) {
-            existingMetadata = new ArrayList<>();
-            getBackingStore().getIndexedDescriptors().put(identifier, existingMetadata);
-        } else if (!existingMetadata.isEmpty()) {
-            log.warn("{} Detected duplicate metadata for identifier: {}", getLogPrefix(), identifier);
-        }
-        existingMetadata.add(metadata);
-        
-        // Update/create managment data for entity
-        final MetadataManagementData<MetadataIdentifier> mgmtData = getBackingStore().computeManagementDataIfAbsent(identifier);
-        
-        final Instant now = Instant.now();
-        log.debug("{} For metadata expiration and refresh computation, 'now' is : {}", getLogPrefix(), now);
-        
-        mgmtData.setLastUpdateTime(now);
-        
-        mgmtData.setExpirationTime(computeExpirationTime(metadata, now));
-        log.debug("{} Computed metadata expiration time: {}", getLogPrefix(), mgmtData.getExpirationTime());
-        
-        mgmtData.setRefreshTriggerTime(computeRefreshTriggerTime(mgmtData.getExpirationTime(), now));
-        log.debug("{} Computed refresh trigger time: {}", getLogPrefix(), mgmtData.getRefreshTriggerTime());
-        
-        log.info("{} Successfully loaded new EntityDescriptor with entityID '{}' from {}",
-                getLogPrefix(), identifier, 
-                fromPersistentCache ? "persistent cache" : "origin source");
-        
-        //finalize processing of both the filtered and original metadata.
-        finalizeMetadataProcessing(filteredMetadata);
-        finalizeMetadataProcessing(metadata);
-        
-        //TODO log the new metadata expiration
-        
-        //TODO  save metadata to persistent cache if enabled.
-        
-    }
-    
-    /**
-     * Check the metadata is valid e.g. is the correct type. Is implementation specific. 
-     * 
-     * @param metadata the metadata to check.
-     * @param expectedIdentifier the expected identifier of the metadata.
-     * 
-     * @return true iff the metadata is valid, false otherwise.
-     * 
-     * @throws FilterException if there is a fatal error validating the metadata.
-     */
-    @Nonnull protected abstract boolean isNewMetadataValid(@Nonnull final MetadataType metadata,
-            @Nonnull final MetadataIdentifier expectedIdentifier) throws ResolverException;
-
-    /**
-     * Prepare the object for filtering. This is implementation specific.
-     * 
-     * @param input the metadata on which to operate
-     * 
-     * @return the metadata instance to be filtered
-     */
-    @Nonnull protected abstract MetadataType prepareForFiltering(@Nonnull final MetadataType input);
-    
-    /**
-    * Finalize the metadata object after processing. This is implementation specific.
-    * 
-    * @param input the metadata on which to operate
-    * 
-    * @return the metadata instance to be filtered
-    */
-   @Nonnull protected abstract void finalizeMetadataProcessing(@Nonnull final MetadataType input);
-    
-    /**
-     * Filters the given metadata.
-     * 
-     * @param metadata the metadata to be filtered
-     * 
-     * @return the filtered metadata
-     * 
-     * @throws FilterException thrown if there is an error filtering the metadata
-     */
-    @Nullable protected MetadataType filterMetadata(@Nullable final MetadataType metadata) throws FilterException {
-        if (getMetadataFilter() != null) {
-            log.debug("{} Applying metadata filter", getLogPrefix());
-            return getMetadataFilter().filter(metadata, newFilterContext());
-        }
-        return metadata;
-    }
-    
-    /**
-     * Get a new instance of {@link MetadataFilterContext} to be used when filtering metadata.
-     *
-     * <p>
-     * This default implementation will just return an empty context.  Subclasses would override
-     * to add contextual info specific to the implementation.
-     * </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;
-    }
-    
-    /**
-     * Compute the effective expiration time for the specified metadata.
-     * 
-     * @param entityDescriptor the EntityDescriptor instance to evaluate
-     * @param now the current date time instant
-     * @return the effective expiration time for the metadata
-     */
-    @Nonnull protected abstract Instant computeExpirationTime(@Nonnull final MetadataType metadata,
-            @Nonnull final Instant now); 
-    
-    @Nullable protected abstract MetadataIdentifier extractIdentifier(@Nonnull final MetadataType metadata);
-
-    /**
-     * 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) * getRefreshDelayFactor());
-
-        // if the expiration time was null or the calculated refresh delay was less than the floor
-        // use the floor
-        if (refreshDelay < getMinCacheDuration().toMillis()) {
-            refreshDelay = getMinCacheDuration().toMillis();
-        }
-
-        return nowDateTime.plusMillis(refreshDelay);
-    }
-    
-    /**
-     * Attempt to resolve the single identifier for the operation from the criteria set.
-     * 
-     * @param criteria the criteria set on which to operate
-     * @return the resolve entityID, or null if a single entityID could not be resolved
-     */
-     @Nullable protected MetadataIdentifier resolveIdentifier(@Nonnull final CriteriaType criteria) {
-         final Set<MetadataIdentifier> identifiers = resolveIdentifiers(criteria);
-         if (identifiers.size() == 1) {
-             return identifiers.iterator().next();
-         }
-         return null;
-     }
-     
-     /**
-      * Attempt to resolve all the identifiers represented by the criteria set.
-      * 
-      * <p> This is an implementation specific method.</p>
-      * 
-      * @param criteria the criteria on which to operate.
-      * @return the resolved identifiers, may be empty, never null.
-      */
-     @Nonnull protected abstract Set<MetadataIdentifier> resolveIdentifiers(@Nonnull final CriteriaType criteria);
-    
-     /** {@inheritDoc} */
-     @Override
-     @Nonnull @NonnullElements protected List<MetadataType> lookupIdentifier(@Nonnull final MetadataIdentifier identifier) 
-             throws ResolverException {
-         getBackingStore().computeManagementDataIfAbsent(identifier).recordEntityAccess();
-         return super.lookupIdentifier(identifier);
-     }
-
-}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java
index 7d8892a..b8015bc 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/AbstractFileOIDCEntityResolver.java
@@ -36,6 +36,23 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
+/*
+ * 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.
+ */
+
 /**
  * Based on {@link org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver}.
  * 
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 6a266c7..dfe1689 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
@@ -1,34 +1,54 @@
+/*
+ * 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.Collections;
 import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.opensaml.core.criterion.SatisfyAnyCriterion;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import net.shibboleth.oidc.metadata.BackingStore;
+import com.google.common.collect.Iterables;
+
+import net.shibboleth.oidc.metadata.EvaluableMetadataCriterion;
 import net.shibboleth.oidc.metadata.OIDCMetadataResolver;
-import net.shibboleth.oidc.metadata.filter.MetadataFilter;
+import net.shibboleth.oidc.metadata.cache.MetadataCache;
 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.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.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.CriterionPredicateRegistry;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
+import net.shibboleth.utilities.java.support.resolver.ResolverSupport;
+
 
-// TODO if this is generic enough it could replace AbstractMetadataResolver. Although that seems hard at this stage.
 // TODO would also need to implement a generic metadata resolver interface.
-public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataType, CriteriaType> 
-        extends AbstractIdentifiableInitializableComponent 
-        implements OIDCMetadataResolver<MetadataType, CriteriaType> {
+public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataType> 
+        extends AbstractIdentifiableInitializableComponent implements OIDCMetadataResolver<MetadataType> {
     
     
     /** Class logger. */
@@ -37,12 +57,16 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
     /** Logging prefix. */
     private String logPrefix;
     
-    // TODO does this need synchronisation?
-    /** Backing store for runtime metadata.*/
-    @NonnullAfterInit private BackingStore<MetadataIdentifier, MetadataType> backingStore;    
+    /** The metadata cache.*/
+    @Nonnull private final MetadataCache<MetadataType> cache;   
+    
+    
+    /** Flag which determines whether predicates used in filtering are connected by 
+     * a logical 'OR' (true) or by logical 'AND' (false). Defaults to false. */
+    private boolean satisfyAnyPredicates;
     
-    /** Filter applied to all metadata. */
-    private MetadataFilter<MetadataType> mdFilter;
+    /** Registry used in resolving predicates from criteria. */
+    @NonnullAfterInit private CriterionPredicateRegistry<MetadataType> criterionPredicateRegistry;
     
     /**
      * Whether problems during initialization should cause the provider to fail or go on without metadata. The
@@ -51,8 +75,9 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
     private boolean failFastInitialization;
     
     /** Constructor.*/
-    protected AbstractOIDCMetadataResolver() {
+    protected AbstractOIDCMetadataResolver(@Nonnull final MetadataCache<MetadataType> metadataCache) {
         failFastInitialization = true;
+        cache = Constraint.isNotNull(metadataCache, "Metadata cache can not be null");
     }
     
     
@@ -69,35 +94,113 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
     }
     
     /**
-     * Gets the metadata filter applied to the metadata.
+     * Filter the supplied candidates by resolving predicates from the supplied criteria and applying
+     * the predicates to return a filtered {@link Iterable}.
      * 
-     * @return the metadata filter applied to the metadata
+     * @param candidates the candidates to evaluate
+     * @param criteria the criteria set to evaluate
+     * @param onEmptyPredicatesReturnEmpty if true and no predicates are supplied, then return an empty iterable;
+     *          otherwise return the original input candidates
+     * 
+     * @return an iterable of the candidates filtered by the resolved predicates
+     * 
+     * @throws ResolverException if there is a fatal error during resolution
      */
-    @Nullable public MetadataFilter<MetadataType> getMetadataFilter() {
-        return mdFilter;
+    protected Iterable<MetadataType> predicateFilterCandidates(@Nonnull final Iterable<MetadataType> candidates,
+            @Nonnull final CriteriaSet criteria, final boolean onEmptyPredicatesReturnEmpty)
+                    throws ResolverException {
+        
+        if (!candidates.iterator().hasNext()) {
+            log.debug("{} Candidates iteration was empty, nothing to filter via predicates", getLogPrefix());
+            return Collections.emptySet();
+        }
+        
+        log.debug("{} Attempting to filter candidate metadata via resolved Predicates", getLogPrefix());
+        
+        // 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());
+        
+        log.trace("{} Resolved {} Predicates: {}", getLogPrefix(), predicates.size(), predicates);
+        
+        final boolean satisfyAny;
+        final SatisfyAnyCriterion satisfyAnyCriterion = criteria.get(SatisfyAnyCriterion.class);
+        if (satisfyAnyCriterion  != null) {
+            log.trace("{} CriteriaSet contained SatisfyAnyCriterion", getLogPrefix());
+            satisfyAny = satisfyAnyCriterion.isSatisfyAny();
+        } else {
+            log.trace("{} CriteriaSet did NOT contain SatisfyAnyCriterion", getLogPrefix());
+            satisfyAny = isSatisfyAnyPredicates();
+        }
+        
+        log.trace("{} Effective satisyAny value: {}", getLogPrefix(), satisfyAny);
+        
+        final Iterable<MetadataType> result = 
+                ResolverSupport.getFilteredIterable(candidates, predicates, satisfyAny, onEmptyPredicatesReturnEmpty);
+        if (log.isDebugEnabled()) {
+            log.debug("{} After predicate filtering {} Metadata entities remain", 
+                    getLogPrefix(), Iterables.size(result));
+        }
+        return result;
     }
+   
     
+    /**
+     * Get the flag indicating whether resolved credentials may satisfy any predicates 
+     * (i.e. connected by logical 'OR') or all predicates (connected by logical 'AND').
+     * 
+     * <p>Defaults to false.</p>
+     * 
+     * @return true if must satisfy all, false otherwise
+     */
+    public boolean isSatisfyAnyPredicates() {
+        return satisfyAnyPredicates;
+    }
     
     /**
-     * Set the entity backing store currently in use by the metadata resolver.
+     * Set the flag indicating whether resolved credentials may satisfy any predicates 
+     * (i.e. connected by logical 'OR') or all predicates (connected by logical 'AND').
+     * 
+     * <p>Defaults to false.</p>
      * 
-     * @param newBackingStore the new entity backing store
+     * @param flag true if must satisfy all, false otherwise
      */
-    protected void setBackingStore(@Nonnull final BackingStore<MetadataIdentifier, MetadataType> newBackingStore) {
+    public void setSatisfyAnyPredicates(final boolean flag) {
         ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
         ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        backingStore = Constraint.isNotNull(newBackingStore, "BackingStore may not be null");
+        satisfyAnyPredicates = flag;
     }
     
     /**
-     * Get the EntityDescriptor backing store currently in use by the metadata resolver.
+     * Get the registry used in resolving predicates from criteria.
      * 
-     * @return the current effective entity backing store
+     * @return the effective registry instance used
      */
-    @Nonnull protected BackingStore<MetadataIdentifier, MetadataType> getBackingStore() {
-        return backingStore;
+    @NonnullAfterInit public CriterionPredicateRegistry<MetadataType> getCriterionPredicateRegistry() {
+        return criterionPredicateRegistry;
     }
+
+    /**
+     * Set the registry used in resolving predicates from criteria.
+     * 
+     * @param registry the registry instance to use
+     */
+    public void setCriterionPredicateRegistry(@Nullable final CriterionPredicateRegistry<MetadataType> registry) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        criterionPredicateRegistry = registry;
+    }
+
+    
+    /**
+     * Get the metadata cache currently in use by the metadata resolver.
+     * 
+     * @return the current effective entity backing store
+     */
+    @Nonnull protected MetadataCache<MetadataType> getCache() {
+        return cache;
+    }    
+    
     
     /**
      * Gets whether problems during initialization should cause the provider to fail or go on without metadata. The
@@ -137,13 +240,17 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
             log.error("{} Metadata provider failed to properly initialize, fail-fast=false, "
                     + "continuing on in a degraded state", getLogPrefix(), e);
         }
-    }   
+    }  
+    
+    @Override protected void doDestroy() {
+        log.warn("Destroying");
+    }
     
     /** Initialise this metadata provider. Subclasses will need to override this method.*/
     protected abstract void initMetadataResolver() throws ComponentInitializationException;
     
     /** {@inheritDoc} */
-    @Override @Nullable public MetadataType resolveSingle(final CriteriaType criteria) throws ResolverException {
+    @Override @Nullable public MetadataType resolveSingle(final CriteriaSet criteria) throws ResolverException {
         ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
         ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
 
@@ -157,83 +264,4 @@ public abstract class AbstractOIDCMetadataResolver<MetadataIdentifier, MetadataT
         return null;
     }
     
-    /**
-     * Get list of metadata matching an identifier.
-     * 
-     * @param indentifier  indentifier to lookup
-     * @return  a list of metadata
-     * @throws ResolverException if an error occurs
-     */
-    @Nonnull @NonnullElements protected List<MetadataType> lookupIdentifier(@Nonnull @NotEmpty final MetadataIdentifier identifier)
-            throws ResolverException {
-        if (!isInitialized()) {
-            throw new ResolverException("Metadata resolver has not been initialized");
-        }
-
-        if (identifier == null) { // add a null check? || Strings.isNullOrEmpty(identifier)) {
-            log.debug("{} Identifier was null or empty, skipping search for it", getLogPrefix());
-            return Collections.emptyList();
-        }
-
-        final List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
-        if (allMetadata.isEmpty()) {
-            log.debug("{} Metadata backing store does not contain any metadata with the ID: {}", 
-                    getLogPrefix(), identifier);
-            return allMetadata;
-        }
-
-        // TODO do isValid via strategy function?
-//        final Iterator<MetadataType> metadataIter = allMetadata.iterator();
-//        while (metadataIter.hasNext()) {
-//            final MetadataType metadata = metadataIter.next();
-//            
-//            if (!isValid(metadata)) {
-//                log.warn("{} Metadata backing store contained metadata with the ID: {}, " 
-//                        + " but it was no longer valid", getLogPrefix(), identifier);
-//                metadataIter.remove();
-//            }
-//        }
-
-        return allMetadata;
-    }
-    
-    /**
-     * Remove 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
-     */
-    protected void removeByIdentifier(@Nonnull final MetadataIdentifier identifier, 
-            @Nonnull final BackingStore<MetadataIdentifier, MetadataType> backingStore) {
-        final Map<MetadataIdentifier, List<MetadataType>> indexedDescriptors = backingStore.getIndexedDescriptors();
-        final List<MetadataType> descriptors = indexedDescriptors.get(identifier);
-        if (descriptors != null) {
-            backingStore.getOrderedDescriptors().removeAll(descriptors);
-        }
-        indexedDescriptors.remove(identifier);
-    }
-    
-    /**
-     * 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 MetadataIdentifier identifier) {
-        final List<MetadataType> metadata = getBackingStore().getIndexedDescriptors().get(identifier);
-        if (metadata != null) {
-            return new ArrayList<>(metadata);
-        }
-        return Collections.emptyList();
-    }
-
-    /** Create a new backing store.*/
-    @Nonnull protected BackingStore<MetadataIdentifier, MetadataType> createNewBackingStore() {
-        return new DefaultBackingStore<>();
-    }
-
-
 }
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/DefaultBackingStore.java
index b4dcfce..a2a7460 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/DefaultBackingStore.java
@@ -1,14 +1,41 @@
+/*
+ * 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.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;
 
 import javax.annotation.Nonnull;
+import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.oidc.metadata.BackingStore;
+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;
 
+ at ThreadSafe
 public class DefaultBackingStore<I, T> implements BackingStore<I, T> {
     
     /** Index of entity IDs to their descriptors. */
@@ -17,20 +44,61 @@ public class DefaultBackingStore<I, T> implements BackingStore<I, T> {
     /** Ordered list of entity descriptors. */
     private List<T> orderedDescriptors;
     
-    /** Constructor.*/
-    public DefaultBackingStore() {
+    /** Map holding management data for each entityID. */
+    private final Map<I, MetadataManagementData<I>> mgmtDataMap;
+    
+    /** The maximum cache duration for metadata.*/  
+    @Nonnull private final Duration maxCacheDuration;
+    
+    /**
+     * Constructor.
+     *
+     * @param cacheDuration TODO should this be here?
+     */
+    public DefaultBackingStore(@Nonnull final Duration cacheDuration) {
+        super();
+        maxCacheDuration = Constraint.isNotNull(cacheDuration,"Max cache duration can not be null");
         indexedDescriptors = new ConcurrentHashMap<>();
-        orderedDescriptors = new ArrayList<>();
+        orderedDescriptors = new ArrayList<>();               
+        mgmtDataMap = new ConcurrentHashMap<>();
     }
 
     @Override
-    @Nonnull public Map<I, List<T>> getIndexedDescriptors() {
+    @Nonnull public Map<I, List<T>> getIndexedValues() {
         return indexedDescriptors;
     }
 
     @Override
-    @Nonnull public List<T> getOrderedDescriptors() {
+    @Nonnull public List<T> getOrderedValues() {
         return orderedDescriptors;
     }
+    
+    @Override
+    public MetadataManagementData<I> computeManagementDataIfAbsent(@Nonnull final I identifier) {
+        Constraint.isNotNull(identifier, "identifier may not be null");
+        Constraint.isNotNull(maxCacheDuration, "Max cache duration can not be null");
+        
+        return mgmtDataMap.computeIfAbsent(identifier, id -> {
+            final Instant now = Instant.now();
+            final MetadataManagementData<I> mgmt = new MetadataManagementData<>(id);
+            mgmt.setRefreshTriggerTime(now.plus(maxCacheDuration));
+            return mgmt;
+        });        
+
+    }
+    
+    @Override
+    //TODO is concurrent hashmap threadsafe for remove and get - do we need the synchronized
+    public synchronized void removeManagementData(@Nonnull final I identifier) {
+        Constraint.isNotNull(identifier, "Identifier may not be null");
+        mgmtDataMap.remove(identifier);
+        
+    }
+    
+    @Override
+    @Nonnull @NonnullElements @Unmodifiable @NotLive
+    public synchronized Set<I> getManagementDataIdentifiers() {
+        return Set.copyOf(mgmtDataMap.keySet());        
+    }
 
 }
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java
deleted file mode 100644
index 3624807..0000000
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultDynamicBackingStore.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package net.shibboleth.oidc.metadata.impl;
-
-import java.time.Duration;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-
-import javax.annotation.Nonnull;
-import javax.annotation.concurrent.ThreadSafe;
-
-import net.shibboleth.oidc.metadata.DynamicBackingStore;
-import net.shibboleth.oidc.metadata.MetadataManagementData;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-
- at ThreadSafe
-public class DefaultDynamicBackingStore<Identifier, Type> extends DefaultBackingStore<Identifier, Type> 
-    implements DynamicBackingStore<Identifier, Type> {
-    
-    /** Map holding management data for each entityID. */
-    private final Map<Identifier, MetadataManagementData<Identifier>> mgmtDataMap;
-    
-    /** The maximum cache duration for metadata.*/  
-    @Nonnull private final Duration maxCacheDuration;
-    
-    /**
-     * 
-     * Constructor.
-     *
-     * @param cacheDuration TODO should this be here?
-     */
-    public DefaultDynamicBackingStore(@Nonnull final Duration cacheDuration) {
-        super();
-        maxCacheDuration = Objects.requireNonNull(cacheDuration,"Max cache duration can not be null");
-        mgmtDataMap = new ConcurrentHashMap<>();
-    }
-
-    @Override
-    public MetadataManagementData<Identifier> computeManagementDataIfAbsent(@Nonnull final Identifier identifier) {
-        Constraint.isNotNull(identifier, "identifier may not be null");
-        
-        return mgmtDataMap.computeIfAbsent(identifier, id -> 
-                new MetadataManagementData<Identifier>(id, maxCacheDuration));        
-
-    }
-
-
-
-}
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
new file mode 100644
index 0000000..05be062
--- /dev/null
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolver.java
@@ -0,0 +1,43 @@
+/*
+ * 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.function.Function;
+
+import javax.annotation.Nonnull;
+
+import com.nimbusds.oauth2.sdk.id.Issuer;
+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 
+        extends AbstractDynamicOIDCMetadataResolver<Issuer, OIDCProviderMetadata> 
+        implements ProviderMetadataResolver {
+
+    protected DynamicOIDCProviderMetadataResolver(
+            @Nonnull final MetadataCache<OIDCProviderMetadata> metadataCache,
+            @Nonnull final Function<CriteriaSet, OIDCProviderMetadata> fetchingStrategy) {
+        super(metadataCache, fetchingStrategy);
+        
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolver.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java
similarity index 57%
rename from oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolver.java
rename to oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java
index 4345015..4e37204 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolver.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationFetchingStrategy.java
@@ -1,10 +1,6 @@
 package net.shibboleth.oidc.metadata.impl;
 
 import java.io.IOException;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
 import java.util.Set;
 import java.util.function.BiFunction;
 
@@ -30,22 +26,15 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
 import net.shibboleth.oidc.metadata.criterion.IssuerIDCriterion;
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.net.MediaTypeSupport;
 import net.shibboleth.utilities.java.support.primitive.StringSupport;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
-/**
- * A dynamic {@link OIDCProviderMetadata metadata} provider that HTTP GETs an OpenID Connect provider's
- * JSON configuration information from its well-known location. The well-known location is derived
- * by default by concatenating the string {@literal /.well-known/openid-configuration} to the Issuer of 
- * the given query - although this is configurable.
- * See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig.
- */
-public class HTTPProviderConfigurationMetadataResolver 
-                extends AbstractDynamicOIDCHTTPProviderMetadataResolver<Issuer, OIDCProviderMetadata, CriteriaSet> {
+
+ at ThreadSafe //? is a singleton?
+public class HTTPProviderConfigurationFetchingStrategy 
+                            extends AbstractDynamicHTTPFetchingStrategy<CriteriaSet, OIDCProviderMetadata> {
     
     /** The returned content_type, must be application/json see openid-connect-discovery section 4.*/
     @Nonnull private static final MediaType CONTENT_TYPE = MediaType.JSON_UTF_8;
@@ -55,96 +44,33 @@ public class HTTPProviderConfigurationMetadataResolver
     private static final String DEFAULT_OPENID_PROVIDER_WELL_KNOWN_PATH = "/.well-known/openid-configuration";
 
     /** Class logger. */
-    @Nonnull private final Logger log = LoggerFactory.getLogger(HTTPProviderConfigurationMetadataResolver.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(HTTPProviderConfigurationFetchingStrategy.class);
  
     /** The well-known path for OpenID Provider metadata.  */
     @Nonnull @NotEmpty private String wellKnownPath;
     
     /** Strategy for composing an issuer with the well-known configuration path.*/
     @Nonnull private BiFunction<Issuer, String, String> wellKnownLocationCompositionStrategy;
-  
-    /**
-     * 
-     * Constructor.
-     *
-     * @param client the instance of {@link HttpClient} used to fetch remote OIDC metadata
-     * @param handler the response type handler use to convert the HTTP response to an {@link OIDCProviderMetadata} instance.
-     */
-    public HTTPProviderConfigurationMetadataResolver(@Nonnull final HttpClient client) {    
-        // The response handler is fixed to that provided by this resolver.
-        super(client, new OIDCProviderMetadataResponseHandler());
+
+    protected HTTPProviderConfigurationFetchingStrategy(@Nonnull final HttpClient client,
+            @Nonnull final ResponseHandler<OIDCProviderMetadata> handler) {
+        super(client, handler);
         wellKnownPath = DEFAULT_OPENID_PROVIDER_WELL_KNOWN_PATH;
         wellKnownLocationCompositionStrategy = new DefaultWellKnownPathCompositionStrategy();
-        setSupportedContentTypes(List.of(CONTENT_TYPE.toString()));
-    }
-    
-    /**
-     * Set the strategy for composing a string URL to fetch the provider's configuration information.
-     * Derived from the issuer and the well-known path. 
-     *  
-     * @param strategy the strategy.
-     */
-    public void setWellKnownLocationCompositionStrategy(@Nonnull final
-            BiFunction<Issuer, String, String> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        this.wellKnownLocationCompositionStrategy = 
-                Constraint.isNotNull(strategy, "WellKnownLocationCompositionStrategy can not be null");
     }
-    
-    /**
-     * Set the well-known provider configuration path.
-     * 
-     * @param path the well-known path.
-     */
-    public void setWellKnownPath(@Nonnull @NotEmpty final String path) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        this.wellKnownPath = Constraint.isNotEmpty(path, wellKnownPath);
-    }
-    
+
     @Override
-    @Nullable protected String buildRequestURL(CriteriaSet criteria) {
+    protected String buildRequestURL(@Nonnull final CriteriaSet criteria) {
         if (criteria.contains(IssuerIDCriterion.class)) {
             final String url = wellKnownLocationCompositionStrategy
                     .apply(criteria.get(IssuerIDCriterion.class).getIssuerID(),wellKnownPath);
             
-            log.debug("{} URL generated by request builder was: {}", getLogPrefix(), url);            
+            log.debug("URL generated by request builder was: {}", url);            
             return url;
         }
         return null;
     }
-
-
-    @Override
-    protected Set<Issuer> resolveIdentifiers(@Nonnull final CriteriaSet criteria) {
-        final IssuerIDCriterion issuerIDCriterion = criteria.get(IssuerIDCriterion.class);
-        if (issuerIDCriterion != null) {
-            log.debug("{} Found issuer in criteria: {}", getLogPrefix(), issuerIDCriterion.getIssuerID());
-            return Collections.singleton(issuerIDCriterion.getIssuerID());
-        }
-        log.debug("{} No issuerIDs resolved.", getLogPrefix());
-        return Collections.emptySet();
-    }
     
-    /**
-     * {@inheritDoc}
-     * OIDC Provider configuration information does not expire. There is no specification that describes that. 
-     * 
-     * TODO maybe based on the HTTP response headers?
-     */
-    @Override
-    protected Instant computeExpirationTime(@Nonnull final OIDCProviderMetadata metadata, @Nonnull final Instant now) {
-       return now.plus(getMaxCacheDuration());
-    }
-
-    @Override
-    protected Issuer extractIdentifier(@Nonnull final OIDCProviderMetadata metadata) {
-        return metadata.getIssuer();
-    }
-
     /** Default strategy for composing a well-known URL to fetch a provider's configuration document from.*/
     @Immutable
     @ThreadSafe
@@ -235,40 +161,4 @@ public class HTTPProviderConfigurationMetadataResolver
         
     }
 
-    @Override
-    @Nonnull protected boolean isNewMetadataValid(@Nonnull final OIDCProviderMetadata metadata,
-            @Nonnull final Issuer expectedIdentifier) throws ResolverException {
-        
-        if (!Objects.equals(metadata.getIssuer(), expectedIdentifier)) {
-            log.warn("{} New metadata's issuer '{}' does not match expected issuer '{}', will not process", 
-                    getLogPrefix(), metadata.getIssuer(), expectedIdentifier);
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * No-op method for this resolver.
-     */
-    @Override
-    @Nonnull protected OIDCProviderMetadata prepareForFiltering(@Nonnull final  OIDCProviderMetadata input) {
-        //do nothing, just return
-        return input;
-    }
-
-    /**
-     * {@inheritDoc}
-     * 
-     * No-op method for this resolver.
-     */
-    @Override
-    @Nonnull protected void finalizeMetadataProcessing(@Nonnull final  OIDCProviderMetadata input) {
-        //do nothing, just return
-        return;       
-    }
-
-   
-
 }
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingProviderMetadataProvider.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingProviderMetadataProvider.java
index 9e13bf8..f23f19c 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingProviderMetadataProvider.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/ReloadingProviderMetadataProvider.java
@@ -42,7 +42,7 @@ public class ReloadingProviderMetadataProvider extends AbstractIdentifiableIniti
     }
 
     @Override
-    public Iterable<OIDCProviderMetadata> resolve(CriteriaSet criteria) throws ResolverException {
+    public Iterable<OIDCProviderMetadata> resolve(@Nonnull final CriteriaSet criteria) throws ResolverException {
         ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
         ServiceableComponent<ProviderMetadataResolver> component = null;
         try {
@@ -65,7 +65,7 @@ public class ReloadingProviderMetadataProvider extends AbstractIdentifiableIniti
     }
 
     @Override
-    public OIDCProviderMetadata resolveSingle(CriteriaSet criteria) throws ResolverException {
+    public OIDCProviderMetadata resolveSingle(@Nonnull final CriteriaSet criteria) throws ResolverException {
         ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this);
         ServiceableComponent<ProviderMetadataResolver> component = null;
         try {
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/DefaultMetadataCacheTest.java
new file mode 100644
index 0000000..fb6e320
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DefaultMetadataCacheTest.java
@@ -0,0 +1,386 @@
+
+package net.shibboleth.oidc.metadata.cache.impl;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.time.Instant;
+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 org.opensaml.core.criterion.EntityIdCriterion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+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.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.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+/** Test for the MetadataCache. */
+public class DefaultMetadataCacheTest {
+    
+    /** Class logger. */
+    private final Logger log = LoggerFactory.getLogger(DefaultMetadataCacheTest.class);
+
+    // OIDC provider metadata cache
+    private DefaultMetadataCache<Issuer, OIDCProviderMetadata> cache;
+
+    @BeforeMethod
+    void setup() throws Exception {
+        
+        // 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.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");
+        // Initialise when you need to use it, if creating a local version, do not init this one.
+        //cache.initialize();
+
+    }
+    
+    @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);
+        
+        // use a cache local to this method
+        cacheLocal.setIdentifierExtractionStrategy(m -> m.getIssuer());
+        cacheLocal.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(10)));
+        cacheLocal.setCriteriaToIdentifierStrategy(crit -> crit.get(IssuerIDCriterion.class).getIssuerID());
+        //test a simple logging hook
+        cacheLocal.setMetadataBeforeRemovalHook((metadata, identifer) ->  log.info("Before removal hook ran"));
+        
+        // background task settings make no difference as it is manually scheduled.
+        cacheLocal.setCleanupTaskInterval(Duration.ofSeconds(20));
+        cacheLocal.setInitialCleanupTaskDelay(Duration.ofSeconds(1));
+        
+        cacheLocal.setMaxIdleEntityData(Duration.ofSeconds(10));
+        cacheLocal.setRemoveIdleEntityData(true);      
+        cacheLocal.setRefreshDelayFactor(0.75f);
+        cacheLocal.setMinCacheDuration(Duration.ofMinutes(10));
+        cacheLocal.setMaxCacheDuration(Duration.ofMinutes(20));
+        cacheLocal.setMetadataFilterStrategy((metadata, context) -> metadata);
+        cacheLocal.setId("MockCache");
+        cacheLocal.initialize();
+        
+        final Issuer iss = new Issuer("https://example.oidc.op.org");
+        final MetadataManagementData<Issuer> mgmtData = cacheLocal.getBackingStore().computeManagementDataIfAbsent(iss);
+        final Instant now = Instant.now();
+        mgmtData.setLastUpdateTime(now);
+        // expire metadata
+        mgmtData.setExpirationTime(now.minus(Duration.ofMinutes(10)));
+        // refresh is OK
+        mgmtData.setRefreshTriggerTime(now.plus(Duration.ofMinutes(10)));
+       
+
+        // create some metadata to add - probably ignored as needs refereshing
+        OIDCProviderMetadata metadata =
+                new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC), new URI("http://example.oidc.op.org/metadata"));
+        cacheLocal.getBackingStore().getOrderedValues().add(metadata);
+        cacheLocal.getBackingStore().getIndexedValues().put(iss, List.of(metadata));   
+        
+        //should exist in the cache
+        assertFalse(cacheLocal.getBackingStore().getIndexedValues().isEmpty());
+        assertFalse(cacheLocal.getBackingStore().getOrderedValues().isEmpty());
+        
+        // manually trigger the task
+        scheduler.triggerScheduledTasks();
+        
+        //should no longer exist in the cache
+        assertTrue(cacheLocal.getBackingStore().getIndexedValues().isEmpty());
+        assertTrue(cacheLocal.getBackingStore().getOrderedValues().isEmpty());
+    }
+    
+    /* Should fail to fetch because the cache is not properly initialized.*/
+    @Test(expectedExceptions = MetadataCacheException.class)
+    public void testCacheNotInitialized() throws Exception {
+
+        // Create but do not initialise
+        DefaultMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =  new DefaultMetadataCache<>(
+                new DefaultBackingStore<>(Duration.ofMinutes(5)));
+        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;
+            }
+        });
+    }
+
+    @Test(enabled = true)
+    public void testBackgroundCleanup_Idle_Success() throws Exception {
+
+        // 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);
+        
+        // use a cache local to this method
+        cacheLocal.setIdentifierExtractionStrategy(m -> m.getIssuer());
+        cacheLocal.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(10)));
+        cacheLocal.setCriteriaToIdentifierStrategy(crit -> crit.get(IssuerIDCriterion.class).getIssuerID());
+        
+        // background task settings make no difference as it is manually scheduled.
+        cacheLocal.setCleanupTaskInterval(Duration.ofSeconds(20));
+        cacheLocal.setInitialCleanupTaskDelay(Duration.ofSeconds(1));
+        
+        // should be immediately idle
+        cacheLocal.setMaxIdleEntityData(Duration.ofMillis(0));
+        
+        cacheLocal.setRemoveIdleEntityData(true);      
+        cacheLocal.setRefreshDelayFactor(0.75f);
+        cacheLocal.setMinCacheDuration(Duration.ofMinutes(10));
+        cacheLocal.setMaxCacheDuration(Duration.ofMinutes(20));
+        cacheLocal.setMetadataFilterStrategy((metadata, context) -> metadata);
+        cacheLocal.setId("MockCache");
+        cacheLocal.initialize();
+        
+
+        final Issuer iss = new Issuer("https://example.oidc.op.org");
+        final MetadataManagementData<Issuer> mgmtData = cacheLocal.getBackingStore().computeManagementDataIfAbsent(iss);
+        final Instant now = Instant.now();
+        mgmtData.setLastUpdateTime(now);
+        // metadata not expired
+        mgmtData.setExpirationTime(now.plus(Duration.ofMinutes(10)));
+        // refresh is OK
+        mgmtData.setRefreshTriggerTime(now.plus(Duration.ofMinutes(10)));
+        // Is idle for too long
+        mgmtData.recordEntityAccess();        
+
+        // create some metadata to add - probably ignored as needs refereshing
+        OIDCProviderMetadata metadata =
+                new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC), new URI("http://example.oidc.op.org/metadata"));
+        cacheLocal.getBackingStore().getOrderedValues().add(metadata);
+        cacheLocal.getBackingStore().getIndexedValues().put(iss, List.of(metadata));  
+        
+        // should exist in the cache
+        assertFalse(cacheLocal.getBackingStore().getIndexedValues().isEmpty());
+        assertFalse(cacheLocal.getBackingStore().getOrderedValues().isEmpty());
+
+        // manually trigger the task
+        scheduler.triggerScheduledTasks();
+        
+        // should no longer exist in the cache
+        assertTrue(cacheLocal.getBackingStore().getIndexedValues().isEmpty());
+        assertTrue(cacheLocal.getBackingStore().getOrderedValues().isEmpty());
+    }
+    
+    
+    @Test(enabled = true)
+    public void testStaleMetadata_Success() throws Exception {
+        cache.initialize();
+        
+        final Issuer iss = new Issuer("https://example.oidc.op.org");
+        final MetadataManagementData<Issuer> mgmtData = cache.getBackingStore().computeManagementDataIfAbsent(iss);
+        final Instant now = Instant.now();
+        mgmtData.setLastUpdateTime(now);
+        // expire metadata
+        mgmtData.setExpirationTime(now.minus(Duration.ofMinutes(10)));
+        // refresh is needed
+        mgmtData.setRefreshTriggerTime(now.minus(Duration.ofMinutes(10)));
+        
+        // create some metadata to add - probably ignored as needs refreshing
+        OIDCProviderMetadata metadata =
+                new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC), new URI("http://example.oidc.op.org/metadata"));
+        cache.getBackingStore().getOrderedValues().add(metadata);
+        cache.getBackingStore().getIndexedValues().put(iss, List.of(metadata));   
+        
+        //should exist in the cache
+        assertFalse(cache.getBackingStore().getIndexedValues().isEmpty());
+        assertFalse(cache.getBackingStore().getOrderedValues().isEmpty());
+        // 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;
+            }
+        });
+        
+        // should have been updated after now
+        assertFalse(mgmtData.getLastUpdateTime().equals(now));
+        
+    }
+
+    @Test
+    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;
+            }
+        });
+        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;
+            }
+        });
+        assertTrue(metadata.isEmpty());
+
+    }
+    
+    @Test
+    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;
+            }
+        });
+        assertTrue(metadata.isEmpty());
+
+    }
+
+    @Test(enabled = true)
+    public void testMultiGet_Success() throws InterruptedException, ExecutionException, 
+                                    MetadataCacheException, ComponentInitializationException {
+        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;
+                }
+            });
+        });
+        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;
+                }
+            });
+        });
+        // 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;
+                }
+            });
+        });
+        List<?> firstMetadata = (List<?>) futureOne.get();
+        List<?> secondMetadata = (List<?>) futureThree.get();
+        List<?> thirdMetadata = (List<?>) futureTwo.get();
+
+        // 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;
+                    }
+                });
+        assertTrue(firstMetadata.size() == 1);
+        assertTrue(secondMetadata.size() == 1);
+        assertTrue(thirdMetadata.size() == 1);
+        assertTrue(provider.size() == 1);
+
+        // all three should have the same metadata reference
+        assertSame(firstMetadata.get(0), secondMetadata.get(0));
+        assertSame(firstMetadata.get(0), thirdMetadata.get(0));
+        assertSame(firstMetadata.get(0), provider.get(0));
+
+    }
+
+    @Test
+    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;
+                    }
+                });
+        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;
+                    }
+                });
+        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/cache/impl/ManuallyTriggeredScheduledExecutorService.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ManuallyTriggeredScheduledExecutorService.java
new file mode 100644
index 0000000..e872c0c
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ManuallyTriggeredScheduledExecutorService.java
@@ -0,0 +1,336 @@
+/*
+ * 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 java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+
+
+/**
+ * This code is copied verbatim from org.apache.flink.core.testutils.ManuallyTriggeredScheduledExecutorService
+ * <p>
+ * Simple {@link ScheduledExecutorService} implementation for testing purposes. It spawns no
+ * threads, but lets you trigger the execution of tasks manually.
+ *
+ * <p>This class is helpful when implementing tests tasks synchronous and control when they run,
+ * which would otherwise asynchronous and require complex triggers and latches to test.
+ */
+public class ManuallyTriggeredScheduledExecutorService implements ScheduledExecutorService {
+
+    private final ArrayDeque<Runnable> queuedRunnables = new ArrayDeque<>();
+
+    private final ConcurrentLinkedQueue<ScheduledTask<?>> nonPeriodicScheduledTasks =
+            new ConcurrentLinkedQueue<>();
+
+    private final ConcurrentLinkedQueue<ScheduledTask<?>> periodicScheduledTasks =
+            new ConcurrentLinkedQueue<>();
+
+    private boolean shutdown;
+
+    // ------------------------------------------------------------------------
+    //  (scheduled) execution
+    // ------------------------------------------------------------------------
+
+    @Override
+    public void execute(@Nonnull Runnable command) {
+        synchronized (queuedRunnables) {
+            queuedRunnables.addLast(command);
+        }
+    }
+
+    @Override
+    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+        return insertNonPeriodicTask(command, delay, unit);
+    }
+
+    @Override
+    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+        return insertNonPeriodicTask(callable, delay, unit);
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleAtFixedRate(
+            Runnable command, long initialDelay, long period, TimeUnit unit) {
+        return insertPeriodicRunnable(command, initialDelay, period, unit);
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleWithFixedDelay(
+            Runnable command, long initialDelay, long delay, TimeUnit unit) {
+        return insertPeriodicRunnable(command, initialDelay, delay, unit);
+    }
+
+    // ------------------------------------------------------------------------
+    //  service shutdown
+    // ------------------------------------------------------------------------
+
+    @Override
+    public void shutdown() {
+        shutdown = true;
+    }
+
+    @Override
+    public List<Runnable> shutdownNow() {
+        shutdown();
+        return Collections.emptyList();
+    }
+
+    @Override
+    public boolean isShutdown() {
+        return false;
+    }
+
+    @Override
+    public boolean isTerminated() {
+        return shutdown;
+    }
+
+    @Override
+    public boolean awaitTermination(long timeout, TimeUnit unit) {
+        return true;
+    }
+
+    // ------------------------------------------------------------------------
+    //  non-implemented future task methods
+    // ------------------------------------------------------------------------
+
+    @Override
+    public <T> Future<T> submit(Callable<T> task) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <T> Future<T> submit(Runnable task, T result) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Future<?> submit(Runnable task) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <T> List<Future<T>> invokeAll(
+            Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <T> T invokeAny(Collection<? extends Callable<T>> tasks) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) {
+        throw new UnsupportedOperationException();
+    }
+
+    // ------------------------------------------------------------------------
+    // Execution triggering and access to the queued tasks
+    // ------------------------------------------------------------------------
+
+    /**
+     * Executes all runnable and scheduled non-periodic tasks until none are left to run. This is
+     * essentially a combination of {@link #triggerAll()} and {@link
+     * #triggerNonPeriodicScheduledTasks()} that allows making a test agnostic of how exactly a
+     * runnable is passed to the executor.
+     */
+    public void triggerAllNonPeriodicTasks() {
+        while (numQueuedRunnables() > 0 || !nonPeriodicScheduledTasks.isEmpty()) {
+            triggerAll();
+            triggerNonPeriodicScheduledTasks();
+        }
+    }
+
+    /** Triggers all {@code queuedRunnables}. */
+    public void triggerAll() {
+        while (numQueuedRunnables() > 0) {
+            trigger();
+        }
+    }
+
+    /**
+     * Triggers the next queued runnable and executes it synchronously. This method throws an
+     * exception if no Runnable is currently queued.
+     */
+    public void trigger() {
+        final Runnable next;
+
+        synchronized (queuedRunnables) {
+            next = queuedRunnables.removeFirst();
+        }
+
+        next.run();
+    }
+
+    /** Gets the number of Runnables currently queued. */
+    public int numQueuedRunnables() {
+        synchronized (queuedRunnables) {
+            return queuedRunnables.size();
+        }
+    }
+
+    public Collection<ScheduledFuture<?>> getActiveScheduledTasks() {
+        final ArrayList<ScheduledFuture<?>> scheduledTasks =
+                new ArrayList<>(nonPeriodicScheduledTasks.size() + periodicScheduledTasks.size());
+        scheduledTasks.addAll(getActiveNonPeriodicScheduledTask());
+        scheduledTasks.addAll(getActivePeriodicScheduledTask());
+        return scheduledTasks;
+    }
+
+    public Collection<ScheduledFuture<?>> getActivePeriodicScheduledTask() {
+        return periodicScheduledTasks.stream()
+                .filter(scheduledTask -> !scheduledTask.isCancelled())
+                .collect(Collectors.toList());
+    }
+
+    public Collection<ScheduledFuture<?>> getActiveNonPeriodicScheduledTask() {
+        return nonPeriodicScheduledTasks.stream()
+                .filter(scheduledTask -> !scheduledTask.isCancelled())
+                .collect(Collectors.toList());
+    }
+
+    public List<ScheduledFuture<?>> getAllScheduledTasks() {
+        final ArrayList<ScheduledFuture<?>> scheduledTasks =
+                new ArrayList<>(nonPeriodicScheduledTasks.size() + periodicScheduledTasks.size());
+        scheduledTasks.addAll(getAllNonPeriodicScheduledTask());
+        scheduledTasks.addAll(getAllPeriodicScheduledTask());
+        return scheduledTasks;
+    }
+
+    public List<ScheduledFuture<?>> getAllPeriodicScheduledTask() {
+        return new ArrayList<>(periodicScheduledTasks);
+    }
+
+    public List<ScheduledFuture<?>> getAllNonPeriodicScheduledTask() {
+        return new ArrayList<>(nonPeriodicScheduledTasks);
+    }
+
+    /** Triggers all registered tasks. */
+    public void triggerScheduledTasks() {
+        triggerPeriodicScheduledTasks();
+        triggerNonPeriodicScheduledTasks();
+    }
+
+    /**
+     * Triggers a single non-periodically scheduled task.
+     *
+     * @throws NoSuchElementException If there is no such task.
+     */
+    public void triggerNonPeriodicScheduledTask() {
+        final ScheduledTask<?> poll = nonPeriodicScheduledTasks.remove();
+        if (poll != null) {
+            poll.execute();
+        }
+    }
+
+    /**
+     * Triggers all non-periodically scheduled tasks. In contrast to {@link
+     * #triggerNonPeriodicScheduledTasks()}, if such a task schedules another non-periodically
+     * schedule task, then this new task will also be triggered.
+     */
+    public void triggerNonPeriodicScheduledTasksWithRecursion() {
+        while (!nonPeriodicScheduledTasks.isEmpty()) {
+            final ScheduledTask<?> scheduledTask = nonPeriodicScheduledTasks.poll();
+
+            if (!scheduledTask.isCancelled()) {
+                scheduledTask.execute();
+            }
+        }
+    }
+
+    public void triggerNonPeriodicScheduledTasks() {
+        final Iterator<ScheduledTask<?>> iterator = nonPeriodicScheduledTasks.iterator();
+
+        while (iterator.hasNext()) {
+            final ScheduledTask<?> scheduledTask = iterator.next();
+
+            if (!scheduledTask.isCancelled()) {
+                scheduledTask.execute();
+            }
+            iterator.remove();
+        }
+    }
+
+    public void triggerPeriodicScheduledTasks() {
+        for (ScheduledTask<?> scheduledTask : periodicScheduledTasks) {
+            if (!scheduledTask.isCancelled()) {
+                scheduledTask.execute();
+            }
+        }
+    }
+
+    private ScheduledFuture<?> insertPeriodicRunnable(
+            Runnable command, long delay, long period, TimeUnit unit) {
+
+        final ScheduledTask<?> scheduledTask =
+                new ScheduledTask<>(
+                        () -> {
+                            command.run();
+                            return null;
+                        },
+                        unit.convert(delay, TimeUnit.MILLISECONDS),
+                        unit.convert(period, TimeUnit.MILLISECONDS));
+
+        periodicScheduledTasks.offer(scheduledTask);
+
+        return scheduledTask;
+    }
+
+    private ScheduledFuture<?> insertNonPeriodicTask(Runnable command, long delay, TimeUnit unit) {
+        return insertNonPeriodicTask(
+                () -> {
+                    command.run();
+                    return null;
+                },
+                delay,
+                unit);
+    }
+
+    private <V> ScheduledFuture<V> insertNonPeriodicTask(
+            Callable<V> callable, long delay, TimeUnit unit) {
+        final ScheduledTask<V> scheduledTask =
+                new ScheduledTask<>(callable, unit.convert(delay, TimeUnit.MILLISECONDS));
+
+        nonPeriodicScheduledTasks.offer(scheduledTask);
+
+        return scheduledTask;
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ScheduledTask.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ScheduledTask.java
new file mode 100644
index 0000000..e17c67e
--- /dev/null
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/ScheduledTask.java
@@ -0,0 +1,123 @@
+/*
+ * 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 java.util.Objects;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Delayed;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import javax.annotation.Nonnull;
+
+
+/**
+ * This code is copied verbatim from org.apache.flink.core.testutils.ScheduledTask
+ * 
+ * ScheduledTask represents a task which is executed at a later point in time.
+ *
+ * @param <T> type of the result
+ */
+public class ScheduledTask<T> implements ScheduledFuture<T> {
+    
+    private final Callable<T> callable;
+
+    private final long delay;
+
+    private final long period;
+
+    private final CompletableFuture<T> result;
+
+    public ScheduledTask(Callable<T> callable, long delay) {
+        this(callable, delay, 0);
+    }
+
+    public ScheduledTask(Callable<T> callable, long delay, long period) {
+        this.callable = Objects.requireNonNull(callable);
+        this.result = new CompletableFuture<>();
+        this.delay = delay;
+        this.period = period;
+    }
+
+    public boolean isPeriodic() {
+        return period > 0;
+    }
+
+    public void execute() {
+        if (!result.isDone()) {
+            if (!isPeriodic()) {
+                try {
+                    result.complete(callable.call());
+                } catch (Exception e) {
+                    result.completeExceptionally(e);
+                }
+            } else {
+                try {
+                    callable.call();
+                } catch (Exception e) {
+                    result.completeExceptionally(e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public long getDelay(TimeUnit unit) {
+        return unit.convert(delay, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public int compareTo(Delayed o) {
+        return Long.compare(getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        return result.cancel(mayInterruptIfRunning);
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return result.isCancelled();
+    }
+
+    @Override
+    public boolean isDone() {
+        return result.isDone();
+    }
+
+    @Override
+    public T get() throws InterruptedException, ExecutionException {
+        return result.get();
+    }
+
+    @Override
+    public T get(long timeout, @Nonnull TimeUnit unit)
+            throws InterruptedException, ExecutionException, TimeoutException {
+        return result.get(timeout, unit);
+    }
+
+    public Callable<T> getCallable() {
+        return this.callable;
+    }
+
+}
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolverTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java
similarity index 58%
rename from oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolverTest.java
rename to oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java
index d15b604..92fcc3f 100644
--- a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/HTTPProviderConfigurationMetadataResolverTest.java
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/DynamicOIDCProviderMetadataResolverTest.java
@@ -9,6 +9,10 @@ import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
 
 import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.List;
 
 import org.apache.http.HttpHeaders;
@@ -21,20 +25,29 @@ import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.message.BasicHeader;
 import org.apache.http.message.BasicHttpResponse;
 import org.apache.http.protocol.HttpContext;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
 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.AbstractEvaluableMetadataCriterion;
+import net.shibboleth.oidc.metadata.EvaluableMetadataCriterion;
+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.criterion.IssuerIDCriterion;
-import net.shibboleth.oidc.metadata.impl.HTTPProviderConfigurationMetadataResolver.OIDCProviderMetadataResponseHandler;
+import net.shibboleth.oidc.metadata.impl.HTTPProviderConfigurationFetchingStrategy.OIDCProviderMetadataResponseHandler;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 import net.shibboleth.utilities.java.support.resolver.ResolverException;
 
-/** Tests for the {@link HTTPProviderConfigurationMetadataResolver} .*/
-public class HTTPProviderConfigurationMetadataResolverTest {
+/** Tests for the {@link DynamicOIDCProviderMetadataResolver} .*/
+public class DynamicOIDCProviderMetadataResolverTest {
     
     private final String GOOD_PROVIDER_CONFIGURATION_INFO = "{\n"
             + "\"issuer\": \"https://example.oidc.op.org\",\n"
@@ -95,9 +108,13 @@ public class HTTPProviderConfigurationMetadataResolverTest {
             + "]\n"
             + "}";
     
-    private HTTPProviderConfigurationMetadataResolver resolver;
+    private DynamicOIDCProviderMetadataResolver resolver;
     
     private HttpClient httpClient;
+
+    private DefaultMetadataCache<Issuer, OIDCProviderMetadata> cache;
+    
+    private MetadataCacheBuilder<Issuer, OIDCProviderMetadata> builder;
     
     @SuppressWarnings("unchecked")
     @BeforeMethod
@@ -110,9 +127,28 @@ public class HTTPProviderConfigurationMetadataResolverTest {
                 execute(any(HttpUriRequest.class),any(ResponseHandler.class),any(HttpContext.class)))
                 .thenReturn(OIDCProviderMetadata.parse(GOOD_PROVIDER_CONFIGURATION_INFO));
         
-        resolver = new HTTPProviderConfigurationMetadataResolver(httpClient);
+        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());
+        fetchingStrategy.setId("Mock HTTP Fetching Strategy");
+        fetchingStrategy.initialize();
+        
+        resolver = new DynamicOIDCProviderMetadataResolver(cache, fetchingStrategy);
         resolver.setId("mockHttpOIDCProvider");
-        resolver.setSupportedContentTypes(List.of("application/json"));
+        //resolver.setMetadataFetchingStrategy(fetchingStrategy);
         resolver.initialize();
         
     }
@@ -124,13 +160,6 @@ public class HTTPProviderConfigurationMetadataResolverTest {
         }
     }
     
-    @Test
-    void testFetch_Success() throws ResolverException, IOException {
-        OIDCProviderMetadata metadata = resolver
-                .fetchFromOriginSource(new CriteriaSet(new IssuerIDCriterion(new Issuer("https://example.oidc.op.org"))));
-        assertNotNull(metadata);
-        assertTrue("https://example.oidc.op.org".equals(metadata.getIssuer().getValue()));
-    }
     
     @Test
     void testResolve_Success() throws ResolverException, IOException {
@@ -140,6 +169,83 @@ public class HTTPProviderConfigurationMetadataResolverTest {
         assertTrue(found.iterator().hasNext());
     }
     
+    @Test
+    void testResolve_Filter_Success() throws ResolverException, IOException {
+        Iterable<OIDCProviderMetadata> found = 
+                resolver.resolve(new CriteriaSet(
+                        new IssuerIDCriterion(new Issuer("https://example.oidc.op.org")),
+                        new AlwaysFilterEvaluableMetadataCriterion(OIDCProviderMetadata.class, true)));
+        assertNotNull(found);
+        //was filtered
+        assertTrue(found.iterator().hasNext() == false);
+    }
+    
+    @Test
+    void testResolve_MetadataNeedsRefresh_Success() throws ResolverException, IOException, ParseException {
+        
+        final Issuer iss = new Issuer("https://example.oidc.op.org");
+        final MetadataManagementData<Issuer> mgmtData = cache.getBackingStore()
+                .computeManagementDataIfAbsent(iss);
+        final Instant now = Instant.now();
+        mgmtData.setLastUpdateTime(now);
+        //metadata not expired
+        mgmtData.setExpirationTime(now.plus(Duration.ofMinutes(1)));
+        //refresh please
+        mgmtData.setRefreshTriggerTime(now.minus(Duration.ofMinutes(1)));
+        
+        //create some metadata to add - probably ignored as needs refereshing
+        OIDCProviderMetadata metadata = OIDCProviderMetadata.parse(GOOD_PROVIDER_CONFIGURATION_INFO);
+        cache.getBackingStore().getOrderedValues().add(metadata);
+        cache.getBackingStore().getIndexedValues().put(iss, List.of(metadata));
+        
+        Iterable<OIDCProviderMetadata> found = 
+                resolver.resolve(new CriteriaSet(
+                        new IssuerIDCriterion(iss)));
+        assertNotNull(found);
+        assertTrue(found.iterator().hasNext());
+    }
+    
+    @Test
+    void testResolve_Filter_WrongType_Fail() throws ResolverException, IOException {
+        Iterable<OIDCProviderMetadata> found = 
+                resolver.resolve(new CriteriaSet(
+                        new IssuerIDCriterion(new Issuer("https://example.oidc.op.org")),
+                        new WrongTypeEvaluableMetadataCriterion(EntityDescriptor.class, true)));
+        assertNotNull(found);
+        //was filtered
+        assertTrue(found.iterator().hasNext());
+    }
+    
+    /* Supports OIDCProviderMetadata, so will filter it.*/
+    class AlwaysFilterEvaluableMetadataCriterion extends AbstractEvaluableMetadataCriterion<OIDCProviderMetadata> {
+
+        protected AlwaysFilterEvaluableMetadataCriterion(final Class<OIDCProviderMetadata> claz,
+                final boolean defaultResult) {
+            super(claz, defaultResult);            
+        }
+
+        @Override
+        public boolean doTest(final OIDCProviderMetadata metadata) {
+            return false;
+        }
+        
+    }
+    
+    /* Does not support OIDCProviderMetadata, should just return the default.*/
+    class WrongTypeEvaluableMetadataCriterion extends AbstractEvaluableMetadataCriterion<EntityDescriptor> {
+
+        protected WrongTypeEvaluableMetadataCriterion(final Class<EntityDescriptor> claz,
+                final boolean defaultResult) {
+            super(claz, defaultResult);            
+        }
+
+        @Override
+        public boolean doTest(final EntityDescriptor metadata) {
+            return false;
+        }
+        
+    }
+    
     @Test
     void testResponseHandler_Success() throws IOException {
         final OIDCProviderMetadataResponseHandler handler = new OIDCProviderMetadataResponseHandler();
@@ -171,7 +277,7 @@ public class HTTPProviderConfigurationMetadataResolverTest {
     void testResolve_FromCache_Success() throws ResolverException, IOException {
         
         // test not in cache
-        assertFalse(resolver.getBackingStore().getIndexedDescriptors().containsKey(new Issuer("https://example.oidc.op.org")));
+        assertFalse(cache.getBackingStore().getIndexedValues().containsKey(new Issuer("https://example.oidc.op.org")));
         
         // find and cache
         Iterable<OIDCProviderMetadata> found = 
@@ -180,7 +286,7 @@ public class HTTPProviderConfigurationMetadataResolverTest {
         assertTrue(found.iterator().hasNext());        
         
         // test is in cache
-        assertTrue(resolver.getBackingStore().getIndexedDescriptors().containsKey(new Issuer("https://example.oidc.op.org")));
+        assertTrue(cache.getBackingStore().getIndexedValues().containsKey(new Issuer("https://example.oidc.op.org")));
         
         
         // Take down the source and see if it still resolves from the cache.
@@ -204,5 +310,7 @@ public class HTTPProviderConfigurationMetadataResolverTest {
         assertNotNull(found);
         assertFalse(found.iterator().hasNext());
     }
+    
+    
 
 }
diff --git a/oidc-common-metadata-impl/src/test/resources/logback-test.xml b/oidc-common-metadata-impl/src/test/resources/logback-test.xml
index 0769b60..39274d0 100644
--- a/oidc-common-metadata-impl/src/test/resources/logback-test.xml
+++ b/oidc-common-metadata-impl/src/test/resources/logback-test.xml
@@ -6,7 +6,7 @@
 
     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
-            <pattern>%level [%logger:%line] - %msg%n</pattern>
+            <pattern>%level [%logger:%line] [%t] - %msg%n</pattern>
             <charset>UTF-8</charset>
         </encoder>
     </appender>

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


More information about the commits mailing list