[java-plugin-shibd] branch main updated: Add a "batch-load" read only StorageService supporting delimited files.
Codeberg
noreply at shibboleth.net
Tue Oct 6 13:41:50 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-plugin-shibd.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd/commit/f0f0d5ffb1c44708e67f8242a03955e04410138e
The following commit(s) were added to refs/heads/main by this push:
new f0f0d5f Add a "batch-load" read only StorageService supporting delimited files.
f0f0d5f is described below
commit f0f0d5ffb1c44708e67f8242a03955e04410138e
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Tue Oct 6 09:41:40 2026 -0400
Add a "batch-load" read only StorageService supporting delimited files.
---
.../META-INF/net.shibboleth.idp/postconfig.xml | 3 +
.../impl/ResourceBackedMemoryStorageService.java | 292 +++++++++++++++++++++
.../ResourceBackedMemoryStorageServiceTest.java | 111 ++++++++
.../sp/storage/impl/invalid-storage1.txt | 1 +
.../sp/storage/impl/invalid-storage2.txt | 1 +
.../shibboleth/sp/storage/impl/valid-storage.txt | 4 +
6 files changed, 412 insertions(+)
diff --git a/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 080fde1..18d188d 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -82,6 +82,9 @@
class="net.shibboleth.sp.storage.impl.FilesystemStorageService" abstract="true"
p:readOnly="false"
p:encodeData="true" />
+ <bean id="shibboleth.ResourceBackedStorageService"
+ class="net.shibboleth.sp.storage.impl.ResourceBackedMemoryStorageService" abstract="true" />
+
<bean id="shibboleth.RemotedHttpServletRequestSupplier" class="net.shibboleth.sp.messaging.impl.RemotedHttpServletRequestSupplier" />
<bean id="shibboleth.RemotedHttpServletResponseSupplier" class="net.shibboleth.sp.messaging.impl.RemotedlHttpServletResponseSupplier" />
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/ResourceBackedMemoryStorageService.java b/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/ResourceBackedMemoryStorageService.java
new file mode 100644
index 0000000..88b4c1a
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/ResourceBackedMemoryStorageService.java
@@ -0,0 +1,292 @@
+/*
+ * Licensed 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.sp.storage.impl;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TimerTask;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import net.shibboleth.shared.annotation.constraint.Live;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.shared.resource.Resource;
+
+import org.opensaml.storage.AbstractMapBackedStorageService;
+import org.opensaml.storage.MutableStorageRecord;
+import org.opensaml.storage.StorageCapabilities;
+import org.opensaml.storage.VersionMismatchException;
+import org.slf4j.Logger;
+
+/**
+ * Implementation of {@link AbstractMapBackedStorageService} that stores data in-memory in a shared data structure
+ * after loading it from a resource containing a simple one-line per record, delimited format.
+ *
+ * <p>Records do not carry expirations.</p>
+ *
+ * <p>The implementation is read-only and throws exceptions for any methods that would mutate the data. The "cleanup"
+ * task is instead used to periodically check the source file for updates and reloads the in-memory data under
+ * an exclusive lock.</p>
+ */
+public class ResourceBackedMemoryStorageService extends AbstractMapBackedStorageService implements StorageCapabilities {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ResourceBackedMemoryStorageService.class);
+
+ /** Map of contexts. */
+ @NonnullAfterInit private Map<String, Map<String, MutableStorageRecord<?>>> contextMap;
+
+ /** A shared lock to synchronize access. */
+ @NonnullAfterInit private ReadWriteLock lock;
+
+ /** Source of information. */
+ @Nullable private Resource storageResource;
+
+ /** Field delimiter in resource. */
+ @Nonnull @NotEmpty private String delimiter;
+
+ /** File timestamp. */
+ private long lastModified;
+
+ /** Constructor. */
+ public ResourceBackedMemoryStorageService() {
+ lastModified = 0;
+ delimiter = ",";
+ }
+
+ /** {@inheritDoc} */
+ public boolean isServerSide() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isClustered() {
+ return true;
+ }
+
+ /**
+ * Sets the time between checks for changes to the resource and reload of the records.
+ *
+ * <p>A value of 0 (the default) indicates that no reloading will be performed.</p>
+ *
+ * <p>Non-file resources are detected and reload is skipped, though the task
+ * may still run and log warnings.</p>
+ *
+ * @param interval time between reload checking
+ */
+ public void setReloadInterval(@Nonnull final Duration interval) {
+ setCleanupInterval(interval);
+ }
+
+ /**
+ * Set the resource to use.
+ *
+ * @param resource resource to use
+ */
+ public void setResource(@Nonnull final Resource resource) {
+ checkSetterPreconditions();
+ storageResource = Constraint.isNotNull(resource, "Resource cannot be null");
+ }
+
+ /**
+ * Set the field delimiter to use to split the records.
+ *
+ * <p>Defaults to "," (comma).</p>
+ *
+ * @param delim delimiter string
+ */
+ public void setDelimiter(@Nonnull @NotEmpty final String delim) {
+ checkSetterPreconditions();
+ delimiter = Constraint.isNotNull(StringSupport.trimOrNull(delim), "Delimiter cannot be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ lock = new ReentrantReadWriteLock(true);
+
+ try {
+ final Resource resource = storageResource;
+ if (resource == null) {
+ throw new ComponentInitializationException("Resource cannot be null");
+ }
+
+ try (final InputStream is = resource.getInputStream()) {
+ contextMap = load(is);
+ }
+
+ if (resource.isFile()) {
+ lastModified = resource.lastModified();
+ } else {
+ storageResource = null;
+ }
+
+ } catch (final IOException e) {
+ throw new ComponentInitializationException("Error reading backing resource", e);
+ }
+
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull @Live protected Map<String, Map<String, MutableStorageRecord<?>>> getContextMap() {
+ checkComponentActive();
+ assert contextMap != null;
+ return contextMap;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull protected ReadWriteLock getLock() {
+ checkComponentActive();
+ assert lock != null;
+ return lock;
+ }
+
+
+
+/** {@inheritDoc} */
+ @Override
+ public boolean create(String context, String key, String value, Long expiration) throws IOException {
+ throw new UnsupportedOperationException("Creates are not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Long updateImpl(Long version, String context, String key, String value, Long expiration)
+ throws IOException, VersionMismatchException {
+ throw new UnsupportedOperationException("Updates are not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void updateContextExpiration(@Nonnull @NotEmpty final String context, @Nullable final Long expiration) {
+ throw new UnsupportedOperationException("Updates are not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean deleteImpl(Long version, String context, String key)
+ throws IOException, VersionMismatchException {
+ throw new UnsupportedOperationException("Deletes are not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void deleteContext(@Nonnull @NotEmpty final String context) throws IOException {
+ throw new UnsupportedOperationException("Deletes are not supported by this StorageService implementation.");
+ }
+
+ // Checkstyle: AnonInnerLength OFF
+ /** {@inheritDoc} */
+ @Override
+ @Nullable protected TimerTask getCleanupTask() {
+ return new TimerTask() {
+
+ /** {@inheritDoc} */
+ @Override
+ public void run() {
+ log.debug("Running reload task");
+
+ final Lock writeLock = getLock().writeLock();
+
+ try {
+ writeLock.lock();
+
+ final Resource resource = storageResource;
+
+ try {
+ if (resource != null && resource.isFile() && resource.exists()) {
+ final long modTime = resource.lastModified();
+ if (modTime > lastModified) {
+ log.info("Resource {} modified, attempting reload", resource);
+ try (final InputStream is = resource.getInputStream()) {
+ contextMap = load(is);
+ }
+ lastModified = modTime;
+ } else {
+ log.info("Resource {} not modified, skipping reload", resource);
+ }
+ } else {
+ log.warn("Non-file resource, reload monitoring not supported");
+ }
+ } catch (final IOException e) {
+ log.error("Error reloading records", e);
+ }
+
+ } finally {
+ writeLock.unlock();
+ }
+ }
+ };
+ }
+// Checkstyle: AnonInnerLength ON
+
+ /**
+ * Loads data from the supplied input stream.
+ *
+ * <p>Must be called while holding the write lock.</p>
+ *
+ * @param is
+ *
+ * @return the constructed map of records
+ *
+ * @throws IOException on error
+ */
+ @Nonnull @Unmodifiable @NotLive private Map<String, Map<String, MutableStorageRecord<?>>> load(
+ @Nonnull final InputStream is) throws IOException {
+
+ final Map<String, Map<String, MutableStorageRecord<?>>> theMap = new HashMap<>();
+
+ try (final BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ final String[] sploot = line.split(delimiter, 3);
+ if (sploot != null && sploot.length == 3 &&
+ sploot[0] != null && sploot[1] != null && sploot[2] != null) {
+
+ if (!theMap.containsKey(sploot[0])) {
+ theMap.put(sploot[0], new HashMap<>());
+ }
+ theMap.get(sploot[0]).put(sploot[1], new MutableStorageRecord<>(sploot[2], null));
+
+ } else {
+ throw new IOException("Improper format detected.");
+ }
+ }
+ }
+
+ return theMap;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/ResourceBackedMemoryStorageServiceTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/ResourceBackedMemoryStorageServiceTest.java
new file mode 100644
index 0000000..3b5acf5
--- /dev/null
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/ResourceBackedMemoryStorageServiceTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed 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.sp.storage.impl;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.opensaml.storage.VersionMismatchException;
+import org.springframework.core.io.ClassPathResource;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.resource.Resource;
+import net.shibboleth.shared.spring.resource.ResourceHelper;
+
+/**
+ * Unit tests for {@link FilesystemStorageService}.
+ */
+ at SuppressWarnings("javadoc")
+public class ResourceBackedMemoryStorageServiceTest {
+
+ @Nonnull @NotEmpty private static final String DATA_PATH = "/net/shibboleth/sp/storage/impl/";
+
+ private Resource validResource;
+ private Resource invalidResource1;
+ private Resource invalidResource2;
+
+ @BeforeClass
+ public void setUp() throws IOException {
+ validResource = ResourceHelper.of(new ClassPathResource(DATA_PATH + "valid-storage.txt"));
+ invalidResource1 = ResourceHelper.of(new ClassPathResource(DATA_PATH + "invalid-storage1.txt"));
+ invalidResource2 = ResourceHelper.of(new ClassPathResource(DATA_PATH + "invalid-storage2.txt"));
+ }
+
+ @Test(expectedExceptions=ComponentInitializationException.class)
+ public void testInvalid1() throws ComponentInitializationException {
+ getService(invalidResource1);
+ }
+
+ @Test(expectedExceptions=ComponentInitializationException.class)
+ public void testInvalid2() throws ComponentInitializationException {
+ getService(invalidResource2);
+ }
+
+ @Test(expectedExceptions=UnsupportedOperationException.class)
+ public void testCreate() throws ComponentInitializationException, IOException {
+ final var ss = getService(validResource);
+ ss.create("foo", "bar", "baz", null);
+ }
+
+ @Test
+ public void test() throws ComponentInitializationException, IOException {
+ final var ss = getService(validResource);
+
+ StorageRecord<String> record = ss.read("bork", "bar");
+ Assert.assertNull(record);
+
+ record = ss.read("foo", "bar");
+ assert record != null;
+ Assert.assertEquals(record.getVersion(), 1);
+ Assert.assertEquals(record.getExpiration(), null);
+ Assert.assertEquals(record.getValue(), "baz");
+
+ record = ss.read("foo", "bar2");
+ assert record != null;
+ Assert.assertEquals(record.getValue(), "baz2");
+
+ record = ss.read("foo2", "bar");
+ assert record != null;
+ Assert.assertEquals(record.getValue(), "zork:infocom");
+ }
+
+ /**
+ * Instantiates the service with the specified resource.
+ *
+ * @param resource resource to use
+ *
+ * @return the service
+ *
+ * @throws ComponentInitializationException
+ */
+ @Nonnull private StorageService getService(@Nonnull final Resource resource)
+ throws ComponentInitializationException {
+
+ final var ss = new ResourceBackedMemoryStorageService();
+ ss.setId("test");
+ ss.setResource(resource);
+ ss.setDelimiter(":");
+ ss.initialize();
+ return ss;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/test/resources/net/shibboleth/sp/storage/impl/invalid-storage1.txt b/sp-server-impl/src/test/resources/net/shibboleth/sp/storage/impl/invalid-storage1.txt
new file mode 100644
index 0000000..ae3cdf5
--- /dev/null
+++ b/sp-server-impl/src/test/resources/net/shibboleth/sp/storage/impl/invalid-storage1.txt
@@ -0,0 +1 @@
+foo:bar
diff --git a/sp-server-impl/src/test/resources/net/shibboleth/sp/storage/impl/invalid-storage2.txt b/sp-server-impl/src/test/resources/net/shibboleth/sp/storage/impl/invalid-storage2.txt
new file mode 100644
index 0000000..0152a79
--- /dev/null
+++ b/sp-server-impl/src/test/resources/net/shibboleth/sp/storage/impl/invalid-storage2.txt
@@ -0,0 +1 @@
+foo:
diff --git a/sp-server-impl/src/test/resources/net/shibboleth/sp/storage/impl/valid-storage.txt b/sp-server-impl/src/test/resources/net/shibboleth/sp/storage/impl/valid-storage.txt
new file mode 100644
index 0000000..9e6c248
--- /dev/null
+++ b/sp-server-impl/src/test/resources/net/shibboleth/sp/storage/impl/valid-storage.txt
@@ -0,0 +1,4 @@
+foo:bar:baz2
+foo:bar:baz
+foo:bar2:baz2
+foo2:bar:zork:infocom
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list