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