[java-oidc-common] branch main updated: Add source metadata into cache and expiration time strategy.

Phil Smart philip.smart at jisc.ac.uk
Fri Nov 5 13:07:27 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=333d3adce73b8c10c7527a518b8bf6fa14cb9845

The following commit(s) were added to refs/heads/main by this push:
     new 333d3ad  Add source metadata into cache and expiration time strategy.
333d3ad is described below

commit 333d3adce73b8c10c7527a518b8bf6fa14cb9845
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Nov 5 13:07:24 2021 +0000

    Add source metadata into cache and expiration time strategy.
    
     - Although is computed from a byte array, so would need re-parsing to
    be useful - when it has already been parsed in by the calling class - so
    that needs thinking about.
---
 .../oidc/metadata/BatchBackingStore.java           |  18 +++
 .../metadata/cache/impl/AbstractMetadataCache.java | 109 ++--------------
 .../cache/impl/BaseMetadataCacheBuilderSpec.java   |  85 +-----------
 .../metadata/cache/impl/BatchMetadataCache.java    | 115 ++++++++++++----
 .../cache/impl/BatchMetadataCacheBuilder.java      |   8 +-
 .../cache/impl/BatchMetadataCacheBuilderSpec.java  |  24 ++++
 .../cache/impl/DefaultJSONMapParsingStrategy.java  |   6 +-
 ...oviderMetadataCriteriaToIdentifierStrategy.java |  19 +--
 ...OIDCProviderMetadataExpirationTimeStrategy.java |   3 +-
 ...oviderMetadataIdentifierExtractionStrategy.java |   7 +-
 ...DefaultOIDCProviderMetadataParsingStrategy.java |  27 +++-
 ...viderSourceMetadataExpirationTimeStrategy.java} |  23 ++--
 .../metadata/cache/impl/DynamicMetadataCache.java  | 117 +++++++++++++++--
 .../cache/impl/DynamicMetadataCacheBuilder.java    |   5 +-
 .../impl/DynamicMetadataCacheBuilderSpec.java      |  83 ++++++++++++
 .../metadata/impl/DefaultBatchBackingStore.java    |  13 ++
 .../cache/impl/BatchMetadataCacheBuilderTest.java  |  23 +++-
 ...tCacheTest.java => BatchMetadataCacheTest.java} | 145 +++++++++++++++++----
 .../cache/impl/DynamicMetadataCacheTest.java       |   7 +-
 .../metadata/impl/OIDCMapMetadataResolverTest.java |  40 ++++--
 .../impl/OIDCProviderMetadataResolverTest.java     |  32 ++++-
 21 files changed, 606 insertions(+), 303 deletions(-)

diff --git a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BatchBackingStore.java b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BatchBackingStore.java
index ca4984e..6d00549 100644
--- a/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BatchBackingStore.java
+++ b/oidc-common-metadata-api/src/main/java/net/shibboleth/oidc/metadata/BatchBackingStore.java
@@ -22,6 +22,8 @@ import java.time.Instant;
 
 import javax.annotation.Nullable;
 
+import net.shibboleth.oidc.metadata.cache.LoadingStrategy;
+
 /**
  * A specialisation of a {@link BackingStore} that deals with batch metadata. Operations on batch metadata
  * happen together e.g. a reload will evict all previous entries and all new entries are reloaded in one go. 
@@ -63,6 +65,22 @@ public interface BatchBackingStore<I,T> extends BackingStore<I,T> {
      * @param refreshedAt the refreshed time. 
      */
     void setLastRefresh(@Nullable final Instant refreshedAt);
+
+
+    /**
+     * Gets the source bytes of the source before it was parsed.
+     *  
+     * @return the source bytes.
+     */
+    @Nullable byte[] getOriginalValue();
+    
+    /**
+     * Sets the source bytes of the source before it was parsed.
+     *  
+     * @return the source bytes.
+     */
+    void setOriginalValue(@Nullable final byte[] originalValue);
+    
     
 
 }
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/AbstractMetadataCache.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/AbstractMetadataCache.java
index bef03bf..bb8f12d 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/AbstractMetadataCache.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/AbstractMetadataCache.java
@@ -73,15 +73,9 @@ public abstract class AbstractMetadataCache<IdentifierType, MetadataType>
     /** Class logger. */
     private final Logger log = LoggerFactory.getLogger(AbstractMetadataCache.class);    
     
-    /** Maximum cache duration. */
-    //@NonnullAfterInit private Duration maxCacheDuration;
-    
     /** Cached log prefix. */
     @Nullable private String logPrefix;
     
-    /** Minimum cache duration. */
-    @NonnullAfterInit private Duration minCacheDuration;
-    
     /** Factor used to compute when the next refresh interval will occur. Default value: 0.75 */
     @NonnullAfterInit @Positive private Float refreshDelayFactor;
 
@@ -96,9 +90,6 @@ public abstract class AbstractMetadataCache<IdentifierType, MetadataType>
     /** 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;
     
@@ -168,18 +159,14 @@ public abstract class AbstractMetadataCache<IdentifierType, MetadataType>
         if (identifierExtractionStrategy == null) {
             throw new ComponentInitializationException("Identifier extraction strategy can not be null");
         }
-        if (metadataExpirationTimeStrategy == null) {
-            throw new ComponentInitializationException("Metadata expiration strategy can not be null");
-        }
+
         if (criteriaToIdentifierStrategy == null) {
             throw new ComponentInitializationException("Criteria to identifier strategy can not be null");
         }
         if (metadataFilterStrategy == null) {
             throw new ComponentInitializationException("Metadata filter strategy can not be null");
         }
-        if (//maxCacheDuration == null || 
-                minCacheDuration == null || refreshDelayFactor == null ||               
-                backingStore == null) {
+        if (refreshDelayFactor == null || backingStore == null) {
             throw new ComponentInitializationException("Metadata cache not property initialized");
         }
         
@@ -253,29 +240,6 @@ public abstract class AbstractMetadataCache<IdentifierType, MetadataType>
         return identifierExtractionStrategy;
     }
     
-
-    /**
-     * Set the metadata expiration time strategy.
-     * 
-     * @param strategy the strategy.
-     */
-    public void setMetadataExpirationTimeStrategy(
-            @Nonnull final BiFunction<MetadataType, Instant, Instant> strategy) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-        
-        metadataExpirationTimeStrategy = Constraint.isNotNull(strategy, "Metadata expiration strategy can not be null");
-    }
-    
-    /**
-     * Get the metadata expiration time strategy. 
-     * 
-     * @return the expiration time strategy.
-     */
-    @NonnullAfterInit protected BiFunction<MetadataType, Instant, Instant> getMetadataExpirationTimeStrategy() {
-        return metadataExpirationTimeStrategy;
-    }
-    
     /**
      * Set the metadata filtering strategy.
      * 
@@ -299,41 +263,6 @@ public abstract class AbstractMetadataCache<IdentifierType, MetadataType>
         return metadataFilterStrategy;
     }
     
-    /**
-     *  Set the maximum cache duration for metadata.
-     *  
-     *  <p>Defaults to: 8 hours.</p>
-     *  
-     * @param duration the maximum cache duration
-     */
-//    public void setMaxCacheDuration(@Nonnull final Duration duration) {
-//        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-//        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-//
-//        Constraint.isNotNull(duration, "Duration cannot be null");
-//        Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
-//        
-//        maxCacheDuration = duration;
-//    }
-
-
-    /**
-     *  Set the minimum cache duration for metadata.
-     *  
-     *  <p>Defaults to: 10 minutes.</p>
-     *  
-     * @param duration the minimum cache duration
-     */
-    public void setMinCacheDuration(@Nonnull final Duration duration) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
-
-        Constraint.isNotNull(duration, "Duration cannot be null");
-        Constraint.isFalse(duration.isNegative(), "Duration cannot be negative");
-        
-        minCacheDuration = duration;
-    }
-    
     /**
      * Set a hook to run before a metadata cache entry is removed from the cache.
      * <p>The hook is required to gracefully handle null metadata lists.</p>
@@ -425,7 +354,12 @@ public abstract class AbstractMetadataCache<IdentifierType, MetadataType>
      */
     protected void writeToBackingStore(@Nonnull final MetadataType metadata) {
         
-        final IdentifierType extractedIdentifier = identifierExtractionStrategy.apply(metadata);
+        final IdentifierType extractedIdentifier = getIdentifierExtractionStrategy().apply(metadata);
+        
+        if (extractedIdentifier == null) {
+            log.warn("{} Identifier could not be extracted from metadata, metadata not stored",getLogPrefix());
+            return;
+        }
         
         // will only effect the dynamic cache case, static loading should have cleared the backing store
         // by this point
@@ -508,33 +442,6 @@ public abstract class AbstractMetadataCache<IdentifierType, MetadataType>
         backingStore.getOrderedValues().clear();
     }
     
-    /**
-     * Compute the refresh trigger time.
-     * 
-     * @param expirationTime the time at which the metadata effectively expires
-     * @param nowDateTime the current date time instant
-     * 
-     * @return the time after which refresh attempt(s) should be made
-     */
-    @Nonnull protected Instant computeRefreshTriggerTime(@Nullable final Instant expirationTime,
-            @Nonnull final Instant nowDateTime) {
-        
-        final long now = nowDateTime.toEpochMilli();
-
-        long expireInstant = 0;
-        if (expirationTime != null) {
-            expireInstant = expirationTime.toEpochMilli();
-        }
-        long refreshDelay = (long) ((expireInstant - now) * refreshDelayFactor);
-
-        // if the expiration time was null or the calculated refresh delay was less than the floor
-        // use the floor
-        if (refreshDelay < minCacheDuration.toMillis()) {
-            refreshDelay = minCacheDuration.toMillis();
-        }
-
-        return nowDateTime.plusMillis(refreshDelay);
-    }
     
     /**
      * Create a wrapper for runnables that catches any throwable and logs it. Useful
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BaseMetadataCacheBuilderSpec.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BaseMetadataCacheBuilderSpec.java
index 55e4bec..86f813d 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BaseMetadataCacheBuilderSpec.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BaseMetadataCacheBuilderSpec.java
@@ -19,7 +19,6 @@
 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;
@@ -31,8 +30,6 @@ import javax.annotation.Nullable;
 import net.shibboleth.oidc.metadata.filter.MetadataFilterContext;
 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.AbstractInitializableComponent;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
@@ -44,12 +41,6 @@ import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
  * @param <MetadataType> the metadata type.
  */
 public abstract class BaseMetadataCacheBuilderSpec<IdentifierType, MetadataType> {
-    
-    /** 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;   
@@ -57,8 +48,6 @@ public abstract class BaseMetadataCacheBuilderSpec<IdentifierType, MetadataType>
     /** Strategy used to extract an identifier from the given metadata.*/
     @Nullable private Function<MetadataType, IdentifierType> identifierExtractionStrategy;
     
-    /** Strategy used to compute an expiration time. */
-    @Nullable private BiFunction<MetadataType, Instant, Instant> metadataExpirationTimeStrategy;
     
     /** Map criteria to identifiers to use as keys to the backing store.*/
     @Nullable private Function<CriteriaSet, IdentifierType> criteriaToIdentifierStrategy;
@@ -78,9 +67,7 @@ public abstract class BaseMetadataCacheBuilderSpec<IdentifierType, MetadataType>
     
     /** Constructor.*/
     protected BaseMetadataCacheBuilderSpec() {
-        // defaults
-        maxCacheDuration = Duration.ofHours(8);
-        minCacheDuration = Duration.ofMinutes(10);        
+        // defaults       
         refreshDelayFactor = 0.75f;         
         // create a default direct in/out filter
         metadataFilterStrategy = (metadata, context) -> metadata;  
@@ -169,25 +156,6 @@ public abstract class BaseMetadataCacheBuilderSpec<IdentifierType, MetadataType>
         return identifierExtractionStrategy;
     }
     
-    /**
-     * Set the metadata expiration time strategy.
-     * 
-     * @param strategy the strategy.
-     */
-    public void setMetadataExpirationTimeStrategy(
-            @Nonnull final BiFunction<MetadataType, Instant, Instant> strategy) {        
-        metadataExpirationTimeStrategy = Constraint.isNotNull(strategy, "Strategy can not be null");
-    }
-    
-    /**
-     * Get the metadata expiration time strategy.
-     * 
-     * @return the strategy.
-     */
-    @Nullable protected BiFunction<MetadataType, Instant, Instant> getMetadataExpirationTimeStrategy() {
-        return metadataExpirationTimeStrategy;
-    }
-    
     /**
      * Set the criteria set to identifier lookup strategy.
      * 
@@ -208,57 +176,6 @@ public abstract class BaseMetadataCacheBuilderSpec<IdentifierType, MetadataType>
     }
     
      
-    /**
-     *  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.
      * 
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCache.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCache.java
index ae168d3..688ce8e 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCache.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCache.java
@@ -48,12 +48,21 @@ import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
  * A metadata cache implementation that supports 'refresh-ahead' semantics for batch cache updates. 
  * Does not support 'read-through' semantics if an entry does not exist in the cache.
  * 
+ * <p> The metadata source could either be an aggregate with more than one metadata entry, or a single source
+ * which contains a single metadata entry.</p>
  * 
  * <p>Supports the following:</p>
  * <ul>
- * <li>Batch reloading of metadata using the supplied loading strategy. The cache 
- * is completely reloaded during each refresh cycle.</li>
+ * <li>Batch reloading of metadata from the source using the supplied loading strategy. The cache 
+ * is wiped and reloaded during each succesful refresh cycle.</li>
  * </ul>
+ * 
+ * <p>The schedule for batch metadata reloads are based on:</p>
+ * <ol>
+ * <li>If no expiry is set on the metadata source, or one can not be computed, the max refresh delay is used.</li>
+ * <li>If metadata expiry exists but is less than the min refresh delay, use the min refresh delay.</li>
+ * <li>If metadata expiry exists and is greater than the min, use the metadata expiry.</li>
+ * </ol>
  *
  * @param <IdentifierType> the metadata identifier type.
  * @param <MetadataType> the metadata type.
@@ -73,10 +82,13 @@ public class BatchMetadataCache<IdentifierType, MetadataType>
     @NonnullAfterInit @Positive private Duration minRefreshDelay;
     
     /** The function to use to load metadata.*/
-    @Nonnull private final LoadingStrategy loadingStrategy;
+    @NonnullAfterInit private LoadingStrategy loadingStrategy;
     
     /** How to parse the loaded metadata from the loadingStrategy into a usable metadatatype.*/
-    @Nonnull private final Function<byte[], List<MetadataType>> parsingStrategy;
+    @NonnullAfterInit private Function<byte[], List<MetadataType>> parsingStrategy;
+    
+    /** Determine the expiration time of the source batch loaded metadata.*/
+    @NonnullAfterInit private Function<byte[], Instant> sourceMetadataExpiryStrategy;
     
     /** 
      * Is a match based on an identifier required? If not, 
@@ -90,13 +102,9 @@ public class BatchMetadataCache<IdentifierType, MetadataType>
      * Constructor.
      *
      * @param store the backing store.
-     * @param metadataLoadingStrategy strategy used to load metadata.
-     * @param parseStrategy the strategy used to convert raw metadata in bytes to the given metadata type.
      */
-    public BatchMetadataCache(@Nonnull final BatchBackingStore<IdentifierType, MetadataType> store,
-            @Nonnull final LoadingStrategy metadataLoadingStrategy,
-            @Nonnull final Function<byte[], List<MetadataType>> parseStrategy) {
-        this(store, metadataLoadingStrategy, parseStrategy, null);
+    protected BatchMetadataCache(@Nonnull final BatchBackingStore<IdentifierType, MetadataType> store) {
+        this(store, null);
     }
 
     /**
@@ -104,18 +112,11 @@ public class BatchMetadataCache<IdentifierType, MetadataType>
      * Protected constructor.
      *
      * @param store the backing store.
-     * @param metadataLoadingStrategy strategy used to load metadata.
-     * @param parseStrategy the strategy used to convert raw metadata in bytes to the given metadata type.
      * @param executor the scheduled executor
      */
     protected BatchMetadataCache(@Nonnull final BatchBackingStore<IdentifierType, MetadataType> store,
-            @Nonnull final LoadingStrategy metadataLoadingStrategy,
-            @Nonnull final Function<byte[], List<MetadataType>> parseStrategy,
             @Nullable final ScheduledExecutorService executor) {
         super(store, executor);
-        loadingStrategy = 
-                Constraint.isNotNull(metadataLoadingStrategy, "Metadata loading strategy can not be null");
-        parsingStrategy = Constraint.isNotNull(parseStrategy, "Metadata Parsing strategy can not be null");
         matchOnIdentifierRequired = true;
     }
 
@@ -126,7 +127,16 @@ public class BatchMetadataCache<IdentifierType, MetadataType>
         if (maxRefreshDelay == null || minRefreshDelay == null) {
             throw new ComponentInitializationException("Refreshable metadata cache not property initialized");
         }
-        
+        if (sourceMetadataExpiryStrategy == null) {
+            throw new ComponentInitializationException("Source metadata expiration time strategy can not be null");
+        }
+        if (loadingStrategy == null) {
+            throw new ComponentInitializationException("Loading strategy can not be null");
+        }
+        if (parsingStrategy == null) {
+            throw new ComponentInitializationException("Parsing strategy can not be null");
+        }
+   
         try {
             loadCache();
         } catch (final MetadataCacheException e) {
@@ -134,6 +144,43 @@ public class BatchMetadataCache<IdentifierType, MetadataType>
         }      
     }
     
+    /**
+     * Set the strategy used to batch load metadata from a source.
+     * 
+     * @param strategy the loading strategy.
+     */
+    public void setLoadingStrategy(@Nonnull final LoadingStrategy strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        loadingStrategy = Constraint.isNotNull(strategy, "Loading strategy can not be null");
+    }
+    
+    /**
+     * Set the  strategy used to convert raw metadata in bytes to the given metadata type.
+     * 
+     * @param strategy the parsing strategy.
+     */
+    public void setParsingStrategy(@Nonnull final Function<byte[], List<MetadataType>> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        parsingStrategy = Constraint.isNotNull(strategy, "Parsing strategy can not be null");
+    }
+    
+    /**
+     * Set a strategy to find the metadata expiry date from a source metadata document as bytes.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setSourceMetadataExpiryStrategy(@Nonnull final Function<byte[], Instant> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        sourceMetadataExpiryStrategy = 
+                Constraint.isNotNull(strategy, "Source metadata expiry strategy can not be null");
+    }
+    
     
     /**
      * Set if a match on identifier is required in order to return results. If false and there are no
@@ -142,6 +189,9 @@ public class BatchMetadataCache<IdentifierType, MetadataType>
      * @param required does the metadata lookup need to match the given criteria.
      */
     public void setMatchOnIdentifierRequired(final boolean required) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
         matchOnIdentifierRequired = required;
     }
     
@@ -196,23 +246,23 @@ public class BatchMetadataCache<IdentifierType, MetadataType>
 
             final List<MetadataType> allMetadata = lookupIndexedIdentifier(identifier);
             if (allMetadata.isEmpty()) {
-                log.debug("{} Metadata candidates for '{}' do not exist, returning empty result", 
+                log.debug("{} No metadata candidates for '{}' found, returning empty result", 
                         getLogPrefix(), identifier);
                 return Collections.emptyList();
             } else {
-                log.debug("{} There are {} Metadata candidates for '{}' found in cache", 
+                log.debug("{} There are {} metadata candidates for '{}' found in cache", 
                         getLogPrefix(), allMetadata.size(), identifier);
                 return allMetadata;
             }
         } else if (!matchOnIdentifierRequired) { 
-            log.debug("{} No identifier found to lookup, matchOnIdentifierRequired is false, returning all known "
+            log.debug("{} No identifier found to lookup, identifier match is not required, returning all known "
                     + "metadata",getLogPrefix());
             return Collections.unmodifiableList(getBackingStore().getOrderedValues());
-        } else {
-            // TODO: see SAML version, could resolve from criteria even if no identifier.
+        } else {            
             log.debug("{} No identifier found to lookup, returning empty result", getLogPrefix());
             return Collections.emptyList();
         }
+        // TODO: see SAML version, could resolve from criteriafrom secondary index.
     }
     
     private CacheLoadingContext createLoadingContext() {
@@ -232,7 +282,7 @@ public class BatchMetadataCache<IdentifierType, MetadataType>
         
         log.debug("{} Populating metadata cache for '{}'",getLogPrefix(), loadingStrategy.getSourceIdentifier());
         final Instant now = Instant.now();
-        Duration refreshDelay = null;
+        Instant metadataExpiration = null;
         try {
             if (isDestroyed()) {
                 return;
@@ -246,23 +296,32 @@ public class BatchMetadataCache<IdentifierType, MetadataType>
                     log.info("{} Parsed {} metadata candidates, loading into cache", 
                             getLogPrefix(), parsedMetadata.size());
                     freshLoad(parsedMetadata);
+                    // Store away the original, raw, metadata bytes.
+                    getBackingStore().setOriginalValue(rawFetchedMetadata);
                 } 
             } else {
                 log.info("{} Metadata has not changed since last refresh", getLogPrefix());
             }
+            // Compute metadata expiration from whatever is in the cache (updated or not) will
+            // remain null if no cached original value.
+            metadataExpiration = sourceMetadataExpiryStrategy.apply(getBackingStore().getOriginalValue());
         } catch (final Throwable t) {
             log.error("{} Error loading or parsing metadata",getLogPrefix(), t);
-            refreshDelay = minRefreshDelay;
             if (t instanceof Exception) {
                 throw new MetadataCacheException((Exception) t);
             } else {
                 throw new MetadataCacheException(String.format("Saw an error of type '%s' with message '%s'", 
                         t.getClass().getName(), t.getMessage()));
             }
-        } finally {
-            // TODO compute refresh time from metadata using a function
+        } finally {            
+            if (metadataExpiration == null || metadataExpiration.isBefore(now)) {
+                // Null, so forced to use max refresh delay.
+                scheduleNextRefresh(null);
+            } else {
+                final Duration nextRefreshDelay = computeNextRefreshDelay(metadataExpiration);
+                scheduleNextRefresh(nextRefreshDelay);
+            }
             
-            scheduleNextRefresh(refreshDelay);
             getBackingStore().setLastRefresh(now);
         }
         
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilder.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilder.java
index f2089ef..300b17d 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilder.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilder.java
@@ -62,13 +62,13 @@ public final class BatchMetadataCacheBuilder  {
     
            final BatchMetadataCache<IdentifierType, MetadataType> cache = 
                     new BatchMetadataCache<>(
-                    new DefaultBatchBackingStore<>(), spec.getLoadingStrategy(), spec.getParsingStrategy());            
-            cache.setMinCacheDuration(spec.getMinCacheDuration());
-           // cache.setMaxCacheDuration(getMaxCacheDuration());
+                    new DefaultBatchBackingStore<>());            
+            cache.setSourceMetadataExpiryStrategy(spec.getSourceMetadataExpiryStrategy());
+            cache.setLoadingStrategy(spec.getLoadingStrategy());
+            cache.setParsingStrategy(spec.getParsingStrategy());
             cache.setMinRefreshDelay(spec.getMinRefreshDelay());
             cache.setMaxRefreshDelay(spec.getMaxRefreshDelay());
             cache.setRefreshDelayFactor(spec.getRefreshDelayFactor());
-            cache.setMetadataExpirationTimeStrategy(spec.getMetadataExpirationTimeStrategy());
             cache.setIdentifierExtractionStrategy(spec.getIdentifierExtractionStrategy());
             cache.setCriteriaToIdentifierStrategy(spec.getCriteriaToIdentifierStrategy());
             cache.setMetadataFilterStrategy(spec.getMetadataFilterStrategy());
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilderSpec.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilderSpec.java
index fd0828c..7e3f19b 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilderSpec.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilderSpec.java
@@ -19,6 +19,7 @@
 package net.shibboleth.oidc.metadata.cache.impl;
 
 import java.time.Duration;
+import java.time.Instant;
 import java.util.List;
 import java.util.function.Function;
 
@@ -26,6 +27,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import net.shibboleth.oidc.metadata.cache.LoadingStrategy;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.utilities.java.support.annotation.constraint.Positive;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
@@ -62,6 +64,9 @@ public class BatchMetadataCacheBuilderSpec<IdentifierType, MetadataType>
      */
     @Nonnull private boolean matchOnIdentifierRequired;
     
+    /** Determine the expiration time of the source batch loaded metadata.*/
+    @NonnullAfterInit private Function<byte[], Instant> sourceMetadataExpiryStrategy;
+    
     /** Constructor.*/
     public BatchMetadataCacheBuilderSpec() {
         maxRefreshDelay = Duration.ofHours(4);
@@ -69,6 +74,25 @@ public class BatchMetadataCacheBuilderSpec<IdentifierType, MetadataType>
         matchOnIdentifierRequired = false;
     }
     
+    /**
+     * Set a strategy to find the metadata expiry date from a source metadata document as bytes.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setSourceMetadataExpiryStrategy(@Nonnull final Function<byte[], Instant> strategy) {
+        sourceMetadataExpiryStrategy = 
+                Constraint.isNotNull(strategy, "Source metadata expiry strategy can not be null");
+    }
+    
+    /**
+     * Get the source metadata expiry date strategy.
+     * 
+     * @return the strategy.
+     */
+    protected Function<byte[], Instant> getSourceMetadataExpiryStrategy() {
+        return sourceMetadataExpiryStrategy;
+    }
+    
     
     /**
      * Set if a match on identifier is required in order to return results. If false and there are no
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultJSONMapParsingStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultJSONMapParsingStrategy.java
index 97f0f41..406cdf5 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultJSONMapParsingStrategy.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultJSONMapParsingStrategy.java
@@ -24,6 +24,7 @@ import java.util.Map;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -64,8 +65,11 @@ public class DefaultJSONMapParsingStrategy<V extends Object>
     }
 
     @Override
-    public List<Map<String, V>> apply(@Nonnull final byte[] rawMetadata) {
+    public List<Map<String, V>> apply(@Nullable final byte[] rawMetadata) {
         try {
+            if (rawMetadata == null) {
+                return Collections.emptyList();
+            }
             final Map<String, V> parsed = 
                     objectMapper.readValue(new String(rawMetadata, StandardCharsets.UTF_8), 
                             new TypeReference<Map<String,V>>(){});
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
index 40291b0..bd23885 100644
--- 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
@@ -19,7 +19,6 @@ 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;
@@ -27,16 +26,20 @@ 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}.*/
+/** 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;
+    @Nullable
+    public Issuer apply(@Nullable final CriteriaSet criteria) {
+        if (criteria == null) {
+            return null;
+        }
+        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
index d59e561..f797943 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
@@ -22,6 +22,7 @@ import java.time.Instant;
 import java.util.function.BiFunction;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
@@ -46,7 +47,7 @@ public class DefaultOIDCProviderMetadataExpirationTimeStrategy
     }
 
     @Override
-    public Instant apply(@Nonnull final OIDCProviderMetadata metadata, @Nonnull final Instant now) {
+    public Instant apply(@Nullable final OIDCProviderMetadata metadata, @Nullable 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
index 6e86e76..5b3ee5f 100644
--- 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
@@ -19,7 +19,7 @@ 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 com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
@@ -28,7 +28,10 @@ import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 public class DefaultOIDCProviderMetadataIdentifierExtractionStrategy implements Function<OIDCProviderMetadata, Issuer>{
 
     @Override
-    public Issuer apply(@Nonnull final OIDCProviderMetadata metadata) {
+    public Issuer apply(@Nullable final OIDCProviderMetadata metadata) {
+        if (metadata == null) {
+            return null;
+        }
         return metadata.getIssuer();
     }
 
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataParsingStrategy.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataParsingStrategy.java
index 6148cf0..42a5534 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataParsingStrategy.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataParsingStrategy.java
@@ -1,3 +1,20 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package net.shibboleth.oidc.metadata.cache.impl;
 
 import java.nio.charset.StandardCharsets;
@@ -6,6 +23,7 @@ import java.util.List;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -20,8 +38,11 @@ public class DefaultOIDCProviderMetadataParsingStrategy implements Function<byte
     @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultOIDCProviderMetadataParsingStrategy.class);
 
     @Override
-    @Nonnull public List<OIDCProviderMetadata> apply(@Nonnull final byte[] rawMetdata) {
-        
+    @Nonnull public List<OIDCProviderMetadata> apply(@Nullable final byte[] rawMetdata) {
+        if (rawMetdata == null) {
+            log.warn("Raw metadata is null, unable to parse OIDC Provider Metadata");
+            return Collections.emptyList();
+        }
         try {
             final OIDCProviderMetadata metadata =
                     OIDCProviderMetadata.parse(new String(rawMetdata,StandardCharsets.UTF_8));
@@ -29,7 +50,7 @@ public class DefaultOIDCProviderMetadataParsingStrategy implements Function<byte
                 return List.of(metadata);
             }
             return Collections.emptyList();
-        } catch (ParseException e) {
+        } catch (final ParseException e) {
             log.error("Error parsing bytes to metadata", e);
             return Collections.emptyList();
         }
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/DefaultOIDCProviderSourceMetadataExpirationTimeStrategy.java
similarity index 68%
copy from oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
copy to oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderSourceMetadataExpirationTimeStrategy.java
index d59e561..7062261 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderMetadataExpirationTimeStrategy.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DefaultOIDCProviderSourceMetadataExpirationTimeStrategy.java
@@ -19,18 +19,21 @@ package net.shibboleth.oidc.metadata.cache.impl;
 
 import java.time.Duration;
 import java.time.Instant;
-import java.util.function.BiFunction;
+import java.util.function.Function;
 
 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> {
+/** 
+ * Strategy for computing an expiry time for source byte metadata.  
+ * Given OIDC provider configuration information does not have an expiry, just add the duration
+ * to Now.
+ * 
+ * TODO: check the specs here.
+ */
+public class DefaultOIDCProviderSourceMetadataExpirationTimeStrategy 
+                        implements Function<byte[], Instant> {
     
     /** How long after now should the metadata expire.*/
     @Nonnull private final Duration expiryDuration;
@@ -41,13 +44,13 @@ public class DefaultOIDCProviderMetadataExpirationTimeStrategy
      *
      * @param duration the expiry duration.
      */
-    public DefaultOIDCProviderMetadataExpirationTimeStrategy(@Nonnull final Duration duration) {
+    public DefaultOIDCProviderSourceMetadataExpirationTimeStrategy(@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);
+    public Instant apply(final byte[] sourceMetadata) {
+        return Instant.now().plus(expiryDuration);
     }
 
 }
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCache.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCache.java
index 5f0acf1..e0deb36 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCache.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCache.java
@@ -28,6 +28,7 @@ import java.util.Set;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.StampedLock;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -81,20 +82,22 @@ public class DynamicMetadataCache<IdentifierType, MetadataType>
     /** Flag indicating whether idle entity data should be removed. */
     private boolean removeIdleEntityData;
     
+    /** Minimum cache duration. */
+    @NonnullAfterInit private Duration minCacheDuration;
+    
     /** The function to use to fetch/load metadata if either none exists, or the existing is stale.*/
-    @Nonnull private final Function<CriteriaSet, MetadataType> fetchStrategy;
+    @NonnullAfterInit private Function<CriteriaSet, MetadataType> fetchStrategy;
+    
+    /** Strategy used to compute an expiration time from a metadata instance. */
+    @NonnullAfterInit private BiFunction<MetadataType, Instant, Instant> metadataExpirationTimeStrategy;
     
     /** 
      * Constructor.
      *
      * @param store the backing store to use as the cache store.
-     * @param metadataFetchStrategy the strategy used to fetch metadata using the 'read-through' semantics. 
      */
-    public DynamicMetadataCache(@Nonnull final DynamicBackingStore<IdentifierType, MetadataType> store,
-            @Nonnull final Function<CriteriaSet, MetadataType> metadataFetchStrategy) {
+    protected DynamicMetadataCache(@Nonnull final DynamicBackingStore<IdentifierType, MetadataType> store) {
         super(store);
-        fetchStrategy = 
-                Constraint.isNotNull(metadataFetchStrategy, "Dynamic Metadata fetch strategy can not be null");
     }
     
     /**
@@ -113,6 +116,35 @@ public class DynamicMetadataCache<IdentifierType, MetadataType>
                 Constraint.isNotNull(metadataFetchStrategy, "Dynamic Metadata fetch strategy can not be null");
     }
     
+    /**
+     *  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 the metadata fetching strategy. 
+     * 
+     * @param strategy the strategy used to fetch metadata using a 'read-through' semantic.
+     */
+    public void setFetchStrategy(@Nonnull final Function<CriteriaSet, MetadataType> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        this.fetchStrategy = Constraint.isNotNull(strategy, "Dynamic Metadata fetch strategy can not be null");;
+    }
+    
     /**
      * Set the initial cleanup task delay.
      * 
@@ -127,6 +159,7 @@ public class DynamicMetadataCache<IdentifierType, MetadataType>
         initialCleanupTaskDelay = delay;
         
     }
+   
     
     /**
      * Set the interval at which the cleanup task should run.
@@ -174,13 +207,43 @@ public class DynamicMetadataCache<IdentifierType, MetadataType>
         maxIdleEntityData = max;
     }
     
+    /**
+     * Set the metadata expiration time strategy.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setMetadataExpirationTimeStrategy(
+            @Nonnull final BiFunction<MetadataType, Instant, Instant> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        metadataExpirationTimeStrategy = Constraint.isNotNull(strategy, "Metadata expiration strategy can not be null");
+    }
+    
+    /**
+     * Get the metadata expiration time strategy. 
+     * 
+     * @return the expiration time strategy.
+     */
+    @NonnullAfterInit protected BiFunction<MetadataType, Instant, Instant> getMetadataExpirationTimeStrategy() {
+        return metadataExpirationTimeStrategy;
+    }
+    
+    
     @Override
     protected void doInitialize() throws ComponentInitializationException {
         super.doInitialize();  
         
-        if ( maxIdleEntityData == null  || cleanupTaskInterval == null || initialCleanupTaskDelay == null) {
+        if (minCacheDuration == null || maxIdleEntityData == null  || cleanupTaskInterval == null || 
+                initialCleanupTaskDelay == null) {
             throw new ComponentInitializationException("Dynamic metadata cache not property initialized"); 
         }
+        if (metadataExpirationTimeStrategy == null) {
+            throw new ComponentInitializationException("Metadata expiration strategy can not be null");
+        }
+        if (fetchStrategy == null) {
+            throw new ComponentInitializationException("Metadata fetching strategy can not be null");
+        }
         
         getExecutorService().scheduleAtFixedRate(
                 errorHandlingWrapper(new ExpiredAndIdleMetadataCleanupTask()), initialCleanupTaskDelay.toMillis(), 
@@ -294,7 +357,17 @@ public class DynamicMetadataCache<IdentifierType, MetadataType>
         
         final MetadataType filteredMetadata = getMetadataFilterStrategy().apply(metadata, newFilterContext());
         
+        if (filteredMetadata == null) {
+            log.warn("{} Filtered metadata is null, no further processing performed", getLogPrefix());
+            return;
+        }
+        
         final IdentifierType extractedIdentifier = getIdentifierExtractionStrategy().apply(filteredMetadata);
+        
+        if (extractedIdentifier == null) {
+            log.warn("{} Metadata identifier could not be extracted, no further processing performed", getLogPrefix());
+        }
+        
         // equality method of the identifier is required to be implemented correctly.
         if (!Objects.equals(expectedIdentifier, extractedIdentifier)) {
             log.warn("{} New metadata's identifer '{}' does not match expected identifier '{}', will not process", 
@@ -323,6 +396,34 @@ public class DynamicMetadataCache<IdentifierType, MetadataType>
         log.info("{} Successfully loaded new Metadata with identifer '{}'", getLogPrefix(), extractedIdentifier);   
     }
     
+    /**
+     * 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) * getRefreshDelayFactor());
+
+        // 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);
+    }
+    
     /**
      * Read metadata from the cache under the lock relating to the Identifier of the metadata to find.
      *  
@@ -331,7 +432,7 @@ public class DynamicMetadataCache<IdentifierType, MetadataType>
      * 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 mgmtData the metadata management 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.
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheBuilder.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheBuilder.java
index 85ae283..4c5e4de 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheBuilder.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheBuilder.java
@@ -59,10 +59,11 @@ public final class DynamicMetadataCacheBuilder {
                         throws ComponentInitializationException {
 
             final DynamicMetadataCache<IdentifierType, MetadataType> cache = new DynamicMetadataCache<>(
-                    new DefaultDynamicBackingStore<>(spec.getMaxCacheDuration()), spec.getFetchStrategy());
+                    new DefaultDynamicBackingStore<>(spec.getMaxCacheDuration()));
+            cache.setFetchStrategy(spec.getFetchStrategy());
             cache.setMinCacheDuration(spec.getMinCacheDuration());
             // FIXME what did we do with this.
-            // cache.setMaxCacheDuration(getMaxCacheDuration());
+            //cache.setMaxCacheDuration(spec.getMaxCacheDuration());
             cache.setRefreshDelayFactor(spec.getRefreshDelayFactor());
             cache.setMaxIdleEntityData(spec.getMaxIdleEntityData());
             cache.setMetadataExpirationTimeStrategy(spec.getMetadataExpirationTimeStrategy());
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheBuilderSpec.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheBuilderSpec.java
index df4fdb6..13b20d4 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheBuilderSpec.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheBuilderSpec.java
@@ -19,6 +19,8 @@
 package net.shibboleth.oidc.metadata.cache.impl;
 
 import java.time.Duration;
+import java.time.Instant;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -40,6 +42,12 @@ public class DynamicMetadataCacheBuilderSpec <IdentifierType, MetadataType>
     /** The function to use to fetch metadata if either none exists, or the existing is stale.*/
     @Nullable private Function<CriteriaSet, MetadataType> fetchStrategy;
     
+    /** Maximum cache duration. */
+    @Nonnull private Duration maxCacheDuration;
+    
+    /** Minimum cache duration. */
+    @Nonnull private Duration minCacheDuration;
+    
     /** The maximum idle time for which the cache will keep data for before it is removed. */
     @Nullable private Duration maxIdleEntityData;
     
@@ -52,14 +60,70 @@ public class DynamicMetadataCacheBuilderSpec <IdentifierType, MetadataType>
     /** The initial cleanup task delay.*/
     @Nonnull private Duration initialCleanupTaskDelay;
     
+    /** Strategy used to compute an expiration time. */
+    @Nullable private BiFunction<MetadataType, Instant, Instant> metadataExpirationTimeStrategy;
+    
     /** Constructor. */
     protected DynamicMetadataCacheBuilderSpec() {
+        maxCacheDuration = Duration.ofHours(8);
+        minCacheDuration = Duration.ofMinutes(10); 
         maxIdleEntityData = Duration.ofHours(8);
         cleanupTaskInterval = Duration.ofMinutes(30);
         initialCleanupTaskDelay = Duration.ofMinutes(1);
         removeIdleEntityData = true;
     }
     
+    /**
+     *  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;
+    }
+    
     /**
      * Set the flag indicating whether idle entity data should be removed. 
      * 
@@ -78,6 +142,25 @@ public class DynamicMetadataCacheBuilderSpec <IdentifierType, MetadataType>
         return removeIdleEntityData;
     }
     
+    /**
+     * Set the metadata expiration time strategy.
+     * 
+     * @param strategy the strategy.
+     */
+    public void setMetadataExpirationTimeStrategy(
+            @Nonnull final BiFunction<MetadataType, Instant, Instant> strategy) {        
+        metadataExpirationTimeStrategy = Constraint.isNotNull(strategy, "Strategy can not be null");
+    }
+    
+    /**
+     * Get the metadata expiration time strategy.
+     * 
+     * @return the strategy.
+     */
+    @Nullable protected BiFunction<MetadataType, Instant, Instant> getMetadataExpirationTimeStrategy() {
+        return metadataExpirationTimeStrategy;
+    }
+    
     /**
      * Get the interval at which the cleanup task should run.
      * 
diff --git a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBatchBackingStore.java b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBatchBackingStore.java
index ba93b88..f07ed35 100644
--- a/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBatchBackingStore.java
+++ b/oidc-common-metadata-impl/src/main/java/net/shibboleth/oidc/metadata/impl/DefaultBatchBackingStore.java
@@ -39,6 +39,9 @@ public class DefaultBatchBackingStore<I,T> extends AbstractBackingStore<I, T> im
 
     /** Last time a refresh cycle occurred. */
     @Nullable @GuardedBy("this") private Instant lastRefresh;
+    
+    /** The bytes of the original loaded metadata.*/
+    @Nullable @GuardedBy("this") private byte[] originalValue;
 
     @Override
     @Nullable public synchronized Instant getLastUpdate() {
@@ -60,5 +63,15 @@ public class DefaultBatchBackingStore<I,T> extends AbstractBackingStore<I, T> im
     public synchronized void setLastRefresh(@Nullable final Instant refreshedAt) {
         lastRefresh = refreshedAt;        
     }
+    
+    @Override
+    @Nullable public synchronized byte[] getOriginalValue() {
+        return originalValue;
+    }
+    
+    @Override
+    public synchronized void setOriginalValue(@Nullable final byte[] original) {
+        originalValue = original;
+    }
 
 }
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilderTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilderTest.java
index 1f39ac1..349c849 100644
--- a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilderTest.java
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheBuilderTest.java
@@ -1,4 +1,3 @@
-package net.shibboleth.oidc.metadata.cache.impl;
 /*
  * Licensed to the University Corporation for Advanced Internet Development,
  * Inc. (UCAID) under one or more contributor license agreements.  See the
@@ -16,11 +15,29 @@ package net.shibboleth.oidc.metadata.cache.impl;
  * limitations under the License.
  */
 
+package net.shibboleth.oidc.metadata.cache.impl;/*
+
+ * 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 static org.testng.Assert.assertNotNull;
 
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
 
@@ -46,7 +63,7 @@ public class BatchMetadataCacheBuilderTest {
         spec.setIdentifierExtractionStrategy(m -> m.getIssuer());
         spec.setMinRefreshDelay(Duration.ofMinutes(5));
         spec.setMaxRefreshDelay(Duration.ofMinutes(10));
-        spec.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
+        spec.setSourceMetadataExpiryStrategy(b -> Instant.now().plus(Duration.ofMinutes(5)));
         spec.setCriteriaToIdentifierStrategy(crit -> {
             final IssuerIDCriterion issuerId = crit.get(IssuerIDCriterion.class);
             if (issuerId != null) {
@@ -56,8 +73,6 @@ public class BatchMetadataCacheBuilderTest {
         });
 
         spec.setRefreshDelayFactor(0.75f);
-        spec.setMinCacheDuration(Duration.ofMinutes(10));
-        // cache.setMaxCacheDuration(Duration.ofMinutes(20));
         spec.setMetadataFilterStrategy((metadata, context) -> metadata);
         spec.setLoadingStrategy(new LoadingStrategy() {
             
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadatCacheTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheTest.java
similarity index 62%
rename from oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadatCacheTest.java
rename to oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheTest.java
index ff35478..10d5a2d 100644
--- a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadatCacheTest.java
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/BatchMetadataCacheTest.java
@@ -1,11 +1,10 @@
 /*
- * 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
+ * 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
  *
@@ -19,17 +18,21 @@
 package net.shibboleth.oidc.metadata.cache.impl;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
 
 import java.io.UnsupportedEncodingException;
 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 java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
@@ -52,7 +55,7 @@ import net.shibboleth.oidc.metadata.impl.DefaultBatchBackingStore;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
 
-public class BatchMetadatCacheTest {
+public class BatchMetadataCacheTest {
 
     // OIDC provider metadata cache
     private BatchMetadataCache<Issuer, OIDCProviderMetadata> cache;
@@ -72,7 +75,7 @@ public class BatchMetadatCacheTest {
         defaultLoadingStrategy = new LoadingStrategy() {
             
             @Override
-            public byte[] apply(CacheLoadingContext t) {
+            public byte[] apply(final CacheLoadingContext t) {
                 try {
                     return new OIDCProviderMetadata(new Issuer("http://www.example.org"), List.of(SubjectType.PUBLIC),
                             new URI("http://example.oidc.op.org")).toJSONObject().toJSONString().getBytes();
@@ -98,12 +101,13 @@ public class BatchMetadatCacheTest {
         // Give our own executor, so we can manually handle the cleanup task
         scheduler = new ManuallyTriggeredScheduledExecutorService();
         cache = new BatchMetadataCache<Issuer, OIDCProviderMetadata>(
-                new DefaultBatchBackingStore<Issuer, OIDCProviderMetadata>(), defaultLoadingStrategy,
-                defaultParsingStrategy, scheduler);
+                new DefaultBatchBackingStore<Issuer, OIDCProviderMetadata>(), scheduler);
+        cache.setSourceMetadataExpiryStrategy(b -> Instant.now().plus(Duration.ofMinutes(5)));
+        cache.setParsingStrategy(defaultParsingStrategy);
+        cache.setLoadingStrategy(defaultLoadingStrategy);
         cache.setIdentifierExtractionStrategy(m -> m.getIssuer());
         cache.setMinRefreshDelay(Duration.ofMinutes(5));
         cache.setMaxRefreshDelay(Duration.ofMinutes(10));
-        cache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
         cache.setCriteriaToIdentifierStrategy(crit -> {
             final IssuerIDCriterion issuerId = crit.get(IssuerIDCriterion.class);
             if (issuerId != null) {
@@ -113,8 +117,6 @@ public class BatchMetadatCacheTest {
         });
 
         cache.setRefreshDelayFactor(0.75f);
-        cache.setMinCacheDuration(Duration.ofMinutes(10));
-        // cache.setMaxCacheDuration(Duration.ofMinutes(20));
         cache.setMetadataFilterStrategy((metadata, context) -> metadata);
         cache.setId("MockRefreshableCache");
         // Initialise when you need to use it, if creating a local version, do not init this one.
@@ -135,13 +137,14 @@ public class BatchMetadatCacheTest {
         
         final BatchMetadataCache<Issuer, OIDCProviderMetadata> localCache = 
                 new BatchMetadataCache<Issuer, OIDCProviderMetadata>(
-                new DefaultBatchBackingStore<Issuer, OIDCProviderMetadata>(), defaultLoadingStrategy,
-                defaultParsingStrategy);
+                new DefaultBatchBackingStore<Issuer, OIDCProviderMetadata>());
         
+        localCache.setParsingStrategy(defaultParsingStrategy);
+        localCache.setLoadingStrategy(defaultLoadingStrategy);
+        localCache.setSourceMetadataExpiryStrategy(b -> Instant.now().plus(Duration.ofMinutes(5)));
         localCache.setIdentifierExtractionStrategy(m -> m.getIssuer());
         localCache.setMinRefreshDelay(Duration.ofMillis(100));
         localCache.setMaxRefreshDelay(Duration.ofMillis(200));
-        localCache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
         localCache.setCriteriaToIdentifierStrategy(crit -> {
             final IssuerIDCriterion issuerId = crit.get(IssuerIDCriterion.class);
             if (issuerId != null) {
@@ -150,8 +153,7 @@ public class BatchMetadatCacheTest {
             return null;
         });
 
-        localCache.setRefreshDelayFactor(0.75f);
-        localCache.setMinCacheDuration(Duration.ofMinutes(10));        
+        localCache.setRefreshDelayFactor(0.75f);   
         localCache.setMetadataFilterStrategy((metadata, context) -> metadata);
         localCache.setId("MockLocalRefreshableCache");
         localCache.initialize();
@@ -194,13 +196,14 @@ public class BatchMetadatCacheTest {
 
         final BatchMetadataCache<Issuer, OIDCProviderMetadata> localCache = 
                 new BatchMetadataCache<Issuer, OIDCProviderMetadata>(
-                new DefaultBatchBackingStore<Issuer, OIDCProviderMetadata>(), simpleLoadingStrategy,
-                simpleParsingStrategy, scheduler);
+                new DefaultBatchBackingStore<Issuer, OIDCProviderMetadata>(), scheduler);
         
+        localCache.setParsingStrategy(simpleParsingStrategy);
+        localCache.setLoadingStrategy(simpleLoadingStrategy);
+        localCache.setSourceMetadataExpiryStrategy(b -> Instant.now().plus(Duration.ofMinutes(5)));
         localCache.setIdentifierExtractionStrategy(m -> m.getIssuer());
         localCache.setMinRefreshDelay(Duration.ofMillis(100));
-        localCache.setMaxRefreshDelay(Duration.ofMillis(200));
-        localCache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
+        localCache.setMaxRefreshDelay(Duration.ofMillis(200));        
         localCache.setCriteriaToIdentifierStrategy(crit -> {
             final IssuerIDCriterion issuerId = crit.get(IssuerIDCriterion.class);
             if (issuerId != null) {
@@ -209,8 +212,7 @@ public class BatchMetadatCacheTest {
             return null;
         });
 
-        localCache.setRefreshDelayFactor(0.75f);
-        localCache.setMinCacheDuration(Duration.ofMinutes(10));        
+        localCache.setRefreshDelayFactor(0.75f);       
         localCache.setMetadataFilterStrategy((metadata, context) -> metadata);
         localCache.setId("MockLocalRefreshableCache");
         //If no match, return all
@@ -225,6 +227,99 @@ public class BatchMetadatCacheTest {
 
     }
     
+    
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void testInitialLoadFails() throws ComponentInitializationException, MetadataCacheException {
+        
+        // throw an exception from the loading strategy
+        cache.setLoadingStrategy(new LoadingStrategy() {
+            
+            @Override
+            public byte[] apply(final CacheLoadingContext t) {
+               throw new RuntimeException("Could not load metadata");
+            }
+            
+            @Override
+            public String getSourceIdentifier() {
+                return "Mock loading source";
+            }
+        });
+        
+        // set a source metadata expiry policy that assumes nonnull source metadata
+        // fake a NPE. cache.get should throw a ComponentInitializationException. 
+        cache.setSourceMetadataExpiryStrategy(b -> {throw new NullPointerException("Input bytes are null!");});
+        cache.initialize();
+               
+    }
+    
+    @Test
+    public void testMetadataExpiryBelowMinDelay_UseMinDelay() throws ComponentInitializationException, MetadataCacheException {
+        
+        cache.setMinRefreshDelay(Duration.ofMinutes(10));
+        // Set MD expiry in 1 minute which is below the min refresh delay.
+        cache.setSourceMetadataExpiryStrategy(b -> Instant.now().plus(Duration.ofMinutes(1)));
+        // Set to .99 to make computation easier
+        cache.setRefreshDelayFactor(.99f);
+        cache.initialize();
+        assertTrue(cache.getExecutorService() instanceof ManuallyTriggeredScheduledExecutorService);
+        var manSchedular = (ManuallyTriggeredScheduledExecutorService)cache.getExecutorService();
+        assertNotNull(manSchedular.getAllScheduledTasks());
+        assertEquals(manSchedular.getAllScheduledTasks().size(),1);
+        long delay = manSchedular.getAllScheduledTasks().get(0).getDelay(TimeUnit.SECONDS);
+        // Delay should be close to the min of 10 minutes, not of the metadata 1 minute
+        if (delay > 590 && delay < 610) {
+            // Is close to 10 minutes
+        } else {
+            fail();
+        }
+    }
+    
+    @Test
+    public void testMetadataExpiryGreaterThanMin_UseMetadataDelay() 
+            throws ComponentInitializationException, MetadataCacheException {
+        
+        cache.setMinRefreshDelay(Duration.ofMinutes(10));
+        // Set close to 1 to make computation easier
+        cache.setRefreshDelayFactor(0.99f);
+        // Set MD expiry in 20 minutes which is above the min refresh delay.
+        cache.setSourceMetadataExpiryStrategy(b -> Instant.now().plus(Duration.ofMinutes(20)));
+        cache.initialize();
+        assertTrue(cache.getExecutorService() instanceof ManuallyTriggeredScheduledExecutorService);
+        var manSchedular = (ManuallyTriggeredScheduledExecutorService)cache.getExecutorService();
+        assertNotNull(manSchedular.getAllScheduledTasks());
+        assertEquals(manSchedular.getAllScheduledTasks().size(),1);
+        long delay = manSchedular.getAllScheduledTasks().get(0).getDelay(TimeUnit.SECONDS);
+        // Delay should be close to the metadata value of 20 minutes, not of min delay
+        if (delay > 1100 && delay < 1300) {
+            // Is close to 20 minutes
+        } else {
+            fail();
+        }
+    }
+    
+    @Test
+    public void testMetadataExpiryIsBeforeNow_UseMaxDelay() 
+            throws ComponentInitializationException, MetadataCacheException {
+        
+        cache.setMaxRefreshDelay(Duration.ofMinutes(10));
+        // Set close to 1 to make computation easier
+        cache.setRefreshDelayFactor(0.99f);
+        // Set MD expiry in 20 minutes which is above the min refresh delay.
+        cache.setSourceMetadataExpiryStrategy(b -> Instant.now().minus(Duration.ofMinutes(20)));
+        cache.initialize();
+        assertTrue(cache.getExecutorService() instanceof ManuallyTriggeredScheduledExecutorService);
+        var manSchedular = (ManuallyTriggeredScheduledExecutorService)cache.getExecutorService();
+        assertNotNull(manSchedular.getAllScheduledTasks());
+        assertEquals(manSchedular.getAllScheduledTasks().size(),1);
+        long delay = manSchedular.getAllScheduledTasks().get(0).getDelay(TimeUnit.SECONDS);
+        // Delay should be close to the metadata value of 20 minutes, not of min delay
+        if (delay > 590 && delay < 610) {
+            // Is close to 10 minutes
+        } else {
+            fail();
+        }
+    }
+    
     @Test
     public void testNoUsableIdentifierInCriteria_EmptyList() throws ComponentInitializationException, 
                                                 InterruptedException, MetadataCacheException {
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheTest.java
index 989b8a6..644954c 100644
--- a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheTest.java
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/cache/impl/DynamicMetadataCacheTest.java
@@ -72,13 +72,13 @@ public class DynamicMetadataCacheTest {
                 final Issuer iss = crit.get(IssuerIDCriterion.class).getIssuerID();                       
                 return new OIDCProviderMetadata(iss, List.of(SubjectType.PUBLIC),
                         new URI("http://example.oidc.op.org"));
-            } catch (URISyntaxException e) {
+            } catch (final URISyntaxException e) {
                 return null;
             }
         };
         
         // Give our own executor, so we can manually handle the cleanup task
-        ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
+        final ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
         cache = new DynamicMetadataCache<Issuer, OIDCProviderMetadata>
                         (new DefaultDynamicBackingStore<>(Duration.ofMinutes(5)),defaultFetchStrategy, scheduler);
         cache.setIdentifierExtractionStrategy(m -> m.getIssuer());
@@ -174,8 +174,7 @@ public class DynamicMetadataCacheTest {
 
         // Create but do not initialise
         DynamicMetadataCache<Issuer, OIDCProviderMetadata> cacheLocal =  new DynamicMetadataCache<>(
-                new DefaultDynamicBackingStore<>(Duration.ofMinutes(5)),
-                defaultFetchStrategy);
+                new DefaultDynamicBackingStore<>(Duration.ofMinutes(5)));
         final Issuer iss = new Issuer("https://example.oidc.op.org");
         cacheLocal.get(new CriteriaSet(new IssuerIDCriterion(iss)));
     }
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/OIDCMapMetadataResolverTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/OIDCMapMetadataResolverTest.java
index 28dcf31..0959c97 100644
--- a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/OIDCMapMetadataResolverTest.java
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/OIDCMapMetadataResolverTest.java
@@ -1,3 +1,21 @@
+/*
+ * 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 static org.testng.Assert.assertNotNull;
@@ -6,6 +24,7 @@ import static org.testng.Assert.assertTrue;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.time.Duration;
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ScheduledExecutorService;
@@ -123,18 +142,20 @@ public class OIDCMapMetadataResolverTest {
         final Function<byte[], List<Map<String,Object>>> parsingStrat = 
                 in -> {
                     try {
-                        ObjectMapper mapper = new ObjectMapper();
+                        final ObjectMapper mapper = new ObjectMapper();
                         return List.of(
                                 mapper.readValue(new String(in, "UTF-8"), new TypeReference<Map<String,Object>>(){}));
-                    } catch (UnsupportedEncodingException | JsonProcessingException e) {
+                    } catch (final UnsupportedEncodingException | JsonProcessingException e) {
                         return null;
                     }
                 };
         
-        ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
+        final ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
         batchCache = new TestableBatchMetadataCache<String, Map<String,Object>>(new DefaultBatchBackingStore<>(), 
-                metadataLoadingStrat, parsingStrat, scheduler);
+                scheduler);
         
+        batchCache.setParsingStrategy(parsingStrat);
+        batchCache.setLoadingStrategy(metadataLoadingStrat);
         batchCache.setId("MockBatchCache");
         batchCache.setIdentifierExtractionStrategy(m -> m.get("issuer").toString());
         batchCache.setCriteriaToIdentifierStrategy(crit -> {
@@ -147,10 +168,9 @@ public class OIDCMapMetadataResolverTest {
         batchCache.setRefreshDelayFactor(0.75f);
         batchCache.setMinRefreshDelay(Duration.ofMillis(1000));
         batchCache.setMaxRefreshDelay(Duration.ofMillis(1000));
-        batchCache.setMinCacheDuration(Duration.ofMinutes(10));
         batchCache.setRefreshDelayFactor(0.75f);
         //This needs thinking about
-        batchCache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
+        batchCache.setSourceMetadataExpiryStrategy(b -> Instant.now().plus(Duration.ofMinutes(5)));
         batchCache.initialize();
         
         batchResolver = new OIDCMapBasedMetadataResolver(batchCache);
@@ -162,7 +182,7 @@ public class OIDCMapMetadataResolverTest {
     @Test
     void testBatchResolve_Success() throws ResolverException, IOException, ComponentInitializationException {
         batchResolver.initialize();
-        Iterable<Map<String,Object>> found = 
+        final Iterable<Map<String,Object>> found = 
                 batchResolver.resolve(new CriteriaSet(new IssuerIDCriterion(new Issuer("https://example.oidc.op.org"))));
         assertNotNull(found);
         assertTrue(found.iterator().hasNext());
@@ -172,11 +192,9 @@ public class OIDCMapMetadataResolverTest {
     class TestableBatchMetadataCache<IdentifierType, MetadataType> 
                             extends BatchMetadataCache<IdentifierType, MetadataType> {
 
-        TestableBatchMetadataCache(@Nonnull final BatchBackingStore<IdentifierType, MetadataType> store,
-                @Nonnull final LoadingStrategy metadataLoadingStrategy,
-                @Nonnull final Function<byte[], List<MetadataType>> parseStrategy,
+        TestableBatchMetadataCache(@Nonnull final BatchBackingStore<IdentifierType, MetadataType> store,               
                 @Nullable final ScheduledExecutorService executor) {
-            super(store, metadataLoadingStrategy, parseStrategy, executor);
+            super(store, executor);
         }
         
         /* Expose the backing store with a public method.*/
diff --git a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/OIDCProviderMetadataResolverTest.java b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/OIDCProviderMetadataResolverTest.java
index 0a7985a..d0b9fc5 100644
--- a/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/OIDCProviderMetadataResolverTest.java
+++ b/oidc-common-metadata-impl/src/test/java/net/shibboleth/oidc/metadata/impl/OIDCProviderMetadataResolverTest.java
@@ -1,3 +1,21 @@
+/*
+ * 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 static org.mockito.ArgumentMatchers.any;
@@ -56,7 +74,7 @@ import net.shibboleth.utilities.java.support.resolver.ResolverException;
 /** Tests for the {@link OIDCProviderMetadataResolver} .*/
 public class OIDCProviderMetadataResolverTest {
     
-    private final String GOOD_PROVIDER_CONFIGURATION_INFO = "{\n"
+    private final static String GOOD_PROVIDER_CONFIGURATION_INFO = "{\n"
             + "\"issuer\": \"https://example.oidc.op.org\",\n"
             + "\"authorization_endpoint\": \"https://example.oidc.op.org/o/oauth2/v2/auth\",\n"
             + "\"device_authorization_endpoint\": \"https://oauth2.googleapis.com/device/code\",\n"
@@ -121,6 +139,7 @@ public class OIDCProviderMetadataResolverTest {
     /** A metadata resolver which has a batch based cache.*/
     private OIDCProviderMetadataResolver batchResolver;
     
+    /** A test HTTP client.*/
     private HttpClient httpClient;
 
     /** 
@@ -178,8 +197,10 @@ public class OIDCProviderMetadataResolverTest {
         
         ManuallyTriggeredScheduledExecutorService scheduler = new ManuallyTriggeredScheduledExecutorService();
         batchCache = new TestableBatchMetadataCache<Issuer, OIDCProviderMetadata>(new DefaultBatchBackingStore<>(), 
-                metadataLoadingStrat, parsingStrat, scheduler);
+                scheduler);
         
+        batchCache.setParsingStrategy(parsingStrat);
+        batchCache.setLoadingStrategy(metadataLoadingStrat);
         batchCache.setId("MockBatchCache");
         batchCache.setIdentifierExtractionStrategy(m -> m.getIssuer());
         batchCache.setCriteriaToIdentifierStrategy(crit -> {
@@ -192,10 +213,9 @@ public class OIDCProviderMetadataResolverTest {
         batchCache.setRefreshDelayFactor(0.75f);
         batchCache.setMinRefreshDelay(Duration.ofMillis(1000));
         batchCache.setMaxRefreshDelay(Duration.ofMillis(1000));
-        batchCache.setMinCacheDuration(Duration.ofMinutes(10));
         batchCache.setRefreshDelayFactor(0.75f);
         //This needs thinking about
-        batchCache.setMetadataExpirationTimeStrategy((m, time) -> time.plus(Duration.ofMinutes(5)));
+        batchCache.setSourceMetadataExpiryStrategy(n -> Instant.now().plus(Duration.ofMinutes(5)));
         batchCache.initialize();
         
     }
@@ -434,10 +454,8 @@ public class OIDCProviderMetadataResolverTest {
                             extends BatchMetadataCache<IdentifierType, MetadataType> {
 
         TestableBatchMetadataCache(@Nonnull final BatchBackingStore<IdentifierType, MetadataType> store,
-                @Nonnull final LoadingStrategy metadataLoadingStrategy,
-                @Nonnull final Function<byte[], List<MetadataType>> parseStrategy,
                 @Nullable final ScheduledExecutorService executor) {
-            super(store, metadataLoadingStrategy, parseStrategy, executor);
+            super(store, executor);
         }
         
         /* Expose the backing store with a public method.*/

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


More information about the commits mailing list