[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