[java-opensaml] branch main updated: OSJ-304: Support hash-based directory structure in LocalDynamic

Brent Putman putmanb at georgetown.edu
Fri Nov 6 02:41:07 UTC 2020


This is an automated email from the git hooks/post-receive script.

putmanb pushed a commit to branch main
in repository java-opensaml.

View the commit online:
http://git.shibboleth.net/view/?p=java-opensaml.git;a=commit;h=5a5ecb18728517ea13f20572b2785c9690a8e4d8

The following commit(s) were added to refs/heads/main by this push:
       new  5a5ecb187 OSJ-304: Support hash-based directory structure in LocalDynamic
5a5ecb187 is described below

commit 5a5ecb18728517ea13f20572b2785c9690a8e4d8
Author: Brent Putman <putmanb at georgetown.edu>
AuthorDate: Thu Aug 6 22:47:17 2020 -0400

    OSJ-304: Support hash-based directory structure in LocalDynamic
---
 .../xml/persist/FilesystemLoadSaveManager.java     | 158 +++++++++++++++------
 .../persist/impl/PassthroughSourceStrategy.java    |  37 +++++
 .../SegmentingIntermediateDirectoryStrategy.java   |  99 +++++++++++++
 .../xml/persist/FilesystemLoadSaveManagerTest.java | 113 ++++++++++++++-
 ...egmentingIntermediateDirectoryStrategyTest.java |  55 +++++++
 5 files changed, 412 insertions(+), 50 deletions(-)

diff --git a/opensaml-core/src/main/java/org/opensaml/core/xml/persist/FilesystemLoadSaveManager.java b/opensaml-core/src/main/java/org/opensaml/core/xml/persist/FilesystemLoadSaveManager.java
index 4c7f9a2c5..21ce3936f 100644
--- a/opensaml-core/src/main/java/org/opensaml/core/xml/persist/FilesystemLoadSaveManager.java
+++ b/opensaml-core/src/main/java/org/opensaml/core/xml/persist/FilesystemLoadSaveManager.java
@@ -19,18 +19,17 @@ package org.opensaml.core.xml.persist;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
-import java.io.FileFilter;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.time.Instant;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Set;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
@@ -78,12 +77,13 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
     /** The base directory used for storing individual serialized XML files. */
     private File baseDirectory;
     
+    /** Optional strategy function which produces the intermediate directory path(s) between
+     * the <code>baseDirectory</code> and the actual file. */
+    private Function<String, List<String>> intermediateDirectoryStrategy;
+
     /** Parser pool instance for deserializing XML from the filesystem. */
     private ParserPool parserPool;
     
-    /** File filter used in filtering files in {@link #listKeys()} and {@link #listAll()}. */
-    private FileFilter fileFilter;
-    
     /**
      * Constructor.
      *
@@ -94,23 +94,25 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
         this(new File(Constraint.isNotNull(StringSupport.trimOrNull(baseDir), 
                 "Base directory string instance was null or empty")),
                 null,
-                false);
+                false,
+                null);
     }
 
     /**
      * Constructor.
      *
      * @param baseDir the base directory, must be an absolute path
-     * @param conditionalLoad whether {@link #load(String)} should behave 
+     * @param conditionalLoad whether {@link #load(String)} should behave
      *      as defined in {@link ConditionalLoadXMLObjectLoadSaveManager}
      */
     public FilesystemLoadSaveManager(
-            @ParameterName(name="baseDir") @Nonnull final String baseDir, 
+            @ParameterName(name="baseDir") @Nonnull final String baseDir,
             @ParameterName(name="conditionalLoad") final boolean conditionalLoad) {
         this(new File(Constraint.isNotNull(StringSupport.trimOrNull(baseDir), 
                 "Base directory string instance was null or empty")),
                 null,
-                conditionalLoad);
+                conditionalLoad,
+                null);
     }
     
     /**
@@ -120,9 +122,22 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
      */
     public FilesystemLoadSaveManager(
             @ParameterName(name="baseDirFile") @Nonnull final File baseDir) {
-        this(baseDir, null, false);
+        this(baseDir, null, false, null);
     }
     
+    /**
+     * Constructor.
+     *
+     * @param baseDir the base directory, must be an absolute path
+     * @param dirStrategy the intermediate directory strategy
+     */
+    public FilesystemLoadSaveManager(
+            @ParameterName(name="baseDirFile") @Nonnull final File baseDir,
+            @ParameterName(name="intermediateDirectoryStrategy")
+                @Nullable final Function<String, List<String>> dirStrategy) {
+        this(baseDir, null, false, dirStrategy);
+    }
+
     /**
      * Constructor.
      *
@@ -133,9 +148,25 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
     public FilesystemLoadSaveManager(
             @ParameterName(name="baseDirFile") @Nonnull final File baseDir, 
             @ParameterName(name="conditionalLoad") final boolean conditionalLoad) {
-        this(baseDir, null, conditionalLoad);
+        this(baseDir, null, conditionalLoad, null);
     }
     
+    /**
+     * Constructor.
+     *
+     * @param baseDir the base directory, must be an absolute path
+     * @param conditionalLoad whether {@link #load(String)} should behave
+     *      as defined in {@link ConditionalLoadXMLObjectLoadSaveManager}
+     * @param dirStrategy the intermediate directory strategy
+     */
+    public FilesystemLoadSaveManager(
+            @ParameterName(name="baseDirFile") @Nonnull final File baseDir,
+            @ParameterName(name="conditionalLoad") final boolean conditionalLoad,
+            @ParameterName(name="intermediateDirectoryStrategy")
+                @Nullable final Function<String, List<String>> dirStrategy) {
+        this(baseDir, null, conditionalLoad, dirStrategy);
+    }
+
     /**
      * Constructor.
      *
@@ -148,7 +179,8 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
         this(new File(Constraint.isNotNull(StringSupport.trimOrNull(baseDir), 
                 "Base directory string instance was null or empty")),
                 pp, 
-                false);
+                false,
+                null);
     }
 
     /**
@@ -165,7 +197,7 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
             @ParameterName(name="conditionalLoad") final boolean conditionalLoad) {
         this(new File(Constraint.isNotNull(StringSupport.trimOrNull(baseDir), 
                 "Base directory string instance was null or empty")),
-                pp, conditionalLoad);
+                pp, conditionalLoad, null);
     }
     /**
      * Constructor.
@@ -176,22 +208,40 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
     public FilesystemLoadSaveManager(
             @ParameterName(name="baseDirFile") @Nonnull final File baseDir, 
             @ParameterName(name="parserPool") @Nullable final ParserPool pp) {
-        this(baseDir, pp, false);
+        this(baseDir, pp, false, null);
     }
-    
+
     /**
      * Constructor.
      *
      * @param baseDir the base directory, must be an absolute path
      * @param pp the parser pool instance to use
-     * @param conditionalLoad whether {@link #load(String)} should behave 
+     * @param conditionalLoad whether {@link #load(String)} should behave
      *      as defined in {@link ConditionalLoadXMLObjectLoadSaveManager}
      */
     public FilesystemLoadSaveManager(
-            @ParameterName(name="baseDirFile") @Nonnull final File baseDir, 
+            @ParameterName(name="baseDirFile") @Nonnull final File baseDir,
             @ParameterName(name="parserPool") @Nullable final ParserPool pp,
             @ParameterName(name="conditionalLoad") final boolean conditionalLoad) {
-        
+        this(baseDir, pp, conditionalLoad, null);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param baseDir the base directory, must be an absolute path
+     * @param pp the parser pool instance to use
+     * @param conditionalLoad whether {@link #load(String)} should behave
+     *      as defined in {@link ConditionalLoadXMLObjectLoadSaveManager}
+     *  @param dirStrategy the intermediate directory strategy
+     */
+    public FilesystemLoadSaveManager(
+            @ParameterName(name="baseDirFile") @Nonnull final File baseDir,
+            @ParameterName(name="parserPool") @Nullable final ParserPool pp,
+            @ParameterName(name="conditionalLoad") final boolean conditionalLoad,
+            @ParameterName(name="intermediateDirectoryStrategy")
+                @Nullable final Function<String, List<String>> dirStrategy) {
+
         super(conditionalLoad);
         
         baseDirectory = Constraint.isNotNull(baseDir, "Base directory File instance was null");
@@ -207,18 +257,17 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
             parserPool = Constraint.isNotNull(XMLObjectProviderRegistrySupport.getParserPool(),
                     "Specified ParserPool was null and global ParserPool was not available");
         }
-        
-        fileFilter = new DefaultFileFilter();
+
+        intermediateDirectoryStrategy = dirStrategy;
     }
 
     /** {@inheritDoc} */
     public Set<String> listKeys() throws IOException {
-        final File[] files = baseDirectory.listFiles(fileFilter);
-        final HashSet<String> keys = new HashSet<>();
-        for (final File file : files) {
-            keys.add(file.getName());
-        }
-        return Collections.unmodifiableSet(keys);
+        return java.nio.file.Files.walk(baseDirectory.toPath())
+                .filter(java.nio.file.Files::isRegularFile)
+                .map(Path::getFileName)
+                .map(Path::toString)
+                .collect(Collectors.toUnmodifiableSet());
     }
 
     /** {@inheritDoc} */
@@ -280,6 +329,9 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
         }
         
         final File file = buildFile(key);
+
+        checkAndCreateIntermediateDirectories(file);
+
         try (FileOutputStream fos = new FileOutputStream(file)) {
             final List<XMLObjectSource> sources = xmlObject.getObjectMetadata().get(XMLObjectSource.class);
             if (sources.size() == 1) {
@@ -325,12 +377,34 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
         if (newFile.exists()) {
             throw new IOException(String.format("Specified new key already exists: %s", newKey));
         }
+
+        checkAndCreateIntermediateDirectories(newFile);
+
         Files.move(currentFile, newFile);
         updateLoadLastModified(newKey, getLoadLastModified(currentKey));
         clearLoadLastModified(currentKey);
         return true;
     }
-    
+
+    /**
+     * Check and create intermediate directories between the <code>baseDirectory</code> and the actual file,
+     * if necessary.
+     *
+     * @param file the target file whose path is to be evaluated
+     *
+     * @throws IOException if the intermediate directory creation fails
+     */
+    protected void checkAndCreateIntermediateDirectories(@Nonnull final File file) throws IOException {
+        final File parentDir = new File(file.getParent());
+
+        if (!baseDirectory.equals(parentDir) && !parentDir.exists()) {
+            if (!parentDir.mkdirs()) {
+                throw new IOException(String.format("Could not create intermediate directories for target file: %s",
+                        file.getAbsolutePath()));
+            }
+        }
+    }
+
     /**
      * Build the target file name from the specified index key and the configured base directory.
      * 
@@ -339,7 +413,17 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
      * @throws IOException if there is a fatal error constructing or evaluating the candidate target path
      */
     protected File buildFile(final String key) throws IOException {
-        final File path = new File(baseDirectory, 
+        File parentDirectory = baseDirectory;
+        if (intermediateDirectoryStrategy != null) {
+            final List<String> intermediateDirs = intermediateDirectoryStrategy.apply(key);
+            if (intermediateDirs != null && !intermediateDirs.isEmpty()) {
+                for (final String dir : intermediateDirs) {
+                    parentDirectory = new File(parentDirectory, dir);
+                }
+            }
+        }
+
+        final File path = new File(parentDirectory,
                 Constraint.isNotNull(StringSupport.trimOrNull(key), "Input key was null or empty"));
         if (path.exists() && !path.isFile()) {
             throw new IOException(String.format("Path exists based on specified key, but is not a file: %s", 
@@ -348,22 +432,6 @@ public class FilesystemLoadSaveManager<T extends XMLObject> extends AbstractCond
         return path;
     }
     
-    /**
-     * Default filter used to filter data returned in {@link FilesystemLoadSaveManager#listKeys()}
-     * and {@link FilesystemLoadSaveManager#listAll()}.
-     */
-    public static class DefaultFileFilter implements FileFilter {
-
-        /** {@inheritDoc} */
-        public boolean accept(final File pathname) {
-            if (pathname == null) {
-                return false;
-            }
-            return pathname.isFile();
-        }
-        
-    }
-    
     /**
      * Iterable which provides lazy iteration over the managed files.
      */
diff --git a/opensaml-core/src/main/java/org/opensaml/core/xml/persist/impl/PassthroughSourceStrategy.java b/opensaml-core/src/main/java/org/opensaml/core/xml/persist/impl/PassthroughSourceStrategy.java
new file mode 100644
index 000000000..ab7cb5aa1
--- /dev/null
+++ b/opensaml-core/src/main/java/org/opensaml/core/xml/persist/impl/PassthroughSourceStrategy.java
@@ -0,0 +1,37 @@
+/*
+ * 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 org.opensaml.core.xml.persist.impl;
+
+import java.util.function.Function;
+
+/**
+ * Pass-through source strategy function.
+ * 
+ * <p>
+ * Typically used with {@link SegmentingIntermediateDirectoryStrategy}. 
+ * </p>
+ */
+public class PassthroughSourceStrategy implements Function<String, String> {
+
+    /** {@inheritDoc} */
+    public String apply(final String key) {
+        return key;
+    }
+
+
+}
diff --git a/opensaml-core/src/main/java/org/opensaml/core/xml/persist/impl/SegmentingIntermediateDirectoryStrategy.java b/opensaml-core/src/main/java/org/opensaml/core/xml/persist/impl/SegmentingIntermediateDirectoryStrategy.java
new file mode 100644
index 000000000..062976c18
--- /dev/null
+++ b/opensaml-core/src/main/java/org/opensaml/core/xml/persist/impl/SegmentingIntermediateDirectoryStrategy.java
@@ -0,0 +1,99 @@
+/*
+ * 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 org.opensaml.core.xml.persist.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.core.xml.XMLRuntimeException;
+import org.opensaml.core.xml.persist.FilesystemLoadSaveManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.shibboleth.utilities.java.support.annotation.ParameterName;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Strategy function for producing intermediate directories from an input key.
+ *
+ * <p>
+ * Typically used with {@link FilesystemLoadSaveManager}.
+ * </p>
+ */
+public class SegmentingIntermediateDirectoryStrategy implements Function<String, List<String>> {
+
+    /** Logger. **/
+    private Logger log = LoggerFactory.getLogger(SegmentingIntermediateDirectoryStrategy.class);
+
+    /** Strategy function for generating the source data from the input key.*/
+    private Function<String,String> sourceStrategy;
+
+    /** The number of segments to produce. **/
+    private int segmentNumber;
+
+    /** The length of each produced segment. **/
+    private int segmentLength;
+
+    /**
+     * Constructor.
+     * @param number number of segments
+     * @param length length of each segment
+     * @param source source strategy function
+     */
+    public SegmentingIntermediateDirectoryStrategy(
+            @ParameterName(name="segmentNumber") final int number,
+            @ParameterName(name="segmentLength") final int length,
+            @ParameterName(name="sourceStrategy") final @Nonnull Function<String,String> source) {
+        segmentNumber = Constraint.isGreaterThan(0, number, "Number of segments was zero");
+        segmentLength = Constraint.isGreaterThan(0, length, "Length of segments was zero");
+        sourceStrategy = Constraint.isNotNull(source, "Source strategy was null");
+    }
+
+    /** {@inheritDoc} */
+    public List<String> apply(final String key) {
+        final String source = sourceStrategy.apply(key);
+        if (source == null || source.length() == 0) {
+            log.trace("Source strategy returned null or empty, returning null");
+            return null;
+        }
+
+        log.trace("Resolved source: {}", source);
+
+        if (source.length() < segmentNumber * segmentLength) {
+            final String msg = String.format("Source length %d is less than number (%d) * length (%d) of segments: %s",
+                    source.length(), segmentNumber, segmentLength, source);
+            log.warn(msg);
+            throw new XMLRuntimeException(msg);
+        }
+
+        final ArrayList<String> segments = new ArrayList<>();
+        for (int i=0; i<segmentNumber; i++) {
+            final int startIndex = i * segmentLength;
+            final int endIndex = startIndex + segmentLength;
+            final String segment = key.substring(startIndex, endIndex);
+            log.trace("Produced directory segment: {}", segment);
+            segments.add(segment);
+        }
+
+        return segments;
+    }
+
+}
diff --git a/opensaml-core/src/test/java/org/opensaml/core/xml/persist/FilesystemLoadSaveManagerTest.java b/opensaml-core/src/test/java/org/opensaml/core/xml/persist/FilesystemLoadSaveManagerTest.java
index a1777e909..6297d49d1 100644
--- a/opensaml-core/src/test/java/org/opensaml/core/xml/persist/FilesystemLoadSaveManagerTest.java
+++ b/opensaml-core/src/test/java/org/opensaml/core/xml/persist/FilesystemLoadSaveManagerTest.java
@@ -21,12 +21,16 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.time.Instant;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Iterator;
+import java.util.List;
 import java.util.NoSuchElementException;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 
 import javax.xml.namespace.QName;
 
@@ -35,6 +39,8 @@ import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.XMLRuntimeException;
 import org.opensaml.core.xml.io.MarshallingException;
 import org.opensaml.core.xml.mock.SimpleXMLObject;
+import org.opensaml.core.xml.persist.impl.PassthroughSourceStrategy;
+import org.opensaml.core.xml.persist.impl.SegmentingIntermediateDirectoryStrategy;
 import org.opensaml.core.xml.util.XMLObjectSource;
 import org.opensaml.core.xml.util.XMLObjectSupport;
 import org.slf4j.Logger;
@@ -58,15 +64,21 @@ public class FilesystemLoadSaveManagerTest extends XMLObjectBaseTestCase {
     
     private FilesystemLoadSaveManager<SimpleXMLObject> manager;
     
+    private Function<String, List<String>> intermediateDirectoryStrategy;
+    
     @BeforeMethod
     public void setUp() throws IOException {
         baseDir = new File(System.getProperty("java.io.tmpdir"), "load-save-manager-test");
         baseDir.deleteOnExit();
         log.debug("Using base directory: {}", baseDir.getAbsolutePath());
         resetBaseDir();
-        Assert.assertTrue(baseDir.mkdirs());
+        if (!baseDir.exists()) {
+            Assert.assertTrue(baseDir.mkdirs());
+        }
         
         manager = new FilesystemLoadSaveManager<>(baseDir);
+        
+        intermediateDirectoryStrategy = new SegmentingIntermediateDirectoryStrategy(1, 2, new PassthroughSourceStrategy());
     }
     
     @AfterMethod
@@ -144,6 +156,80 @@ public class FilesystemLoadSaveManagerTest extends XMLObjectBaseTestCase {
         testState(Collections.emptySet());
     }
     
+    @Test(dataProvider="saveLoadUpdateRemoveParams")
+    public void saveLoadUpdateRemoveWithIntermediateDirs(Boolean buildWithObjectSourceByteArray) throws IOException {
+        manager = new FilesystemLoadSaveManager<>(baseDir, intermediateDirectoryStrategy);
+        
+        testState(Collections.emptySet());
+        
+        Assert.assertNull(manager.load("bogus"));
+        
+        Assert.assertFalse(new File(parentPath(baseDir, "fo"), "foo").exists());
+        manager.save("foo", (SimpleXMLObject) buildXMLObject(SimpleXMLObject.ELEMENT_NAME, buildWithObjectSourceByteArray));
+        testState(Collections.singleton("foo"));
+        Assert.assertTrue(new File(parentPath(baseDir, "fo"), "foo").exists());
+        
+        Assert.assertFalse(new File(parentPath(baseDir, "ba"), "bar").exists());
+        Assert.assertFalse(new File(parentPath(baseDir, "ba"), "baz").exists());
+        manager.save("bar", (SimpleXMLObject) buildXMLObject(SimpleXMLObject.ELEMENT_NAME, buildWithObjectSourceByteArray));
+        manager.save("baz", (SimpleXMLObject) buildXMLObject(SimpleXMLObject.ELEMENT_NAME, buildWithObjectSourceByteArray));
+        testState(Set.of("foo", "bar", "baz"));
+        Assert.assertTrue(new File(parentPath(baseDir, "ba"), "bar").exists());
+        Assert.assertTrue(new File(parentPath(baseDir, "ba"), "baz").exists());
+        
+        // Duplicate with overwrite
+        manager.save("bar", (SimpleXMLObject) buildXMLObject(SimpleXMLObject.ELEMENT_NAME, buildWithObjectSourceByteArray), true);
+        testState(Set.of("foo", "bar", "baz"));
+        
+        // Duplicate without overwrite
+        try {
+            manager.save("bar", (SimpleXMLObject) buildXMLObject(SimpleXMLObject.ELEMENT_NAME, buildWithObjectSourceByteArray), false);
+            Assert.fail("Should have failed on duplicate save without overwrite");
+        } catch (IOException e) {
+            // expected, do nothing
+        }
+        testState(Set.of("foo", "bar", "baz"));
+        
+        // Test again. Since checkModifyTime=false, we should get back data even though unmodified
+        testState(Set.of("foo", "bar", "baz"));
+        
+        Assert.assertFalse(new File(parentPath(baseDir, "fo"), "foo2").exists());
+        Assert.assertTrue(manager.updateKey("foo", "foo2"));
+        testState(Set.of("foo2", "bar", "baz"));
+        Assert.assertTrue(new File(parentPath(baseDir, "fo"), "foo2").exists());
+        
+        // Doesn't exist anymore
+        Assert.assertFalse(manager.updateKey("foo", "foo2"));
+        testState(Set.of("foo2", "bar", "baz"));
+        
+        // Can't update to an existing name
+        try {
+            manager.updateKey("bar", "baz");
+            Assert.fail("updateKey should have failed to due existing new key name");
+        } catch (IOException e) {
+            // expected, do nothing
+        }
+        testState(Set.of("foo2", "bar", "baz"));
+        
+        // Doesn't exist anymore
+        Assert.assertFalse(manager.remove("foo"));
+        testState(Set.of("foo2", "bar", "baz"));
+        Assert.assertFalse(new File(parentPath(baseDir, "fo"), "foo").exists());
+        
+        Assert.assertTrue(new File(parentPath(baseDir, "fo"), "foo2").exists());
+        Assert.assertTrue(manager.remove("foo2"));
+        testState(Set.of("bar", "baz"));
+        Assert.assertFalse(new File(parentPath(baseDir, "fo"), "foo2").exists());
+        
+        Assert.assertTrue(new File(parentPath(baseDir, "ba"), "bar").exists());
+        Assert.assertTrue(new File(parentPath(baseDir, "ba"), "baz").exists());
+        Assert.assertTrue(manager.remove("bar"));
+        Assert.assertTrue(manager.remove("baz"));
+        testState(Collections.emptySet());
+        Assert.assertFalse(new File(parentPath(baseDir, "ba"), "bar").exists());
+        Assert.assertFalse(new File(parentPath(baseDir, "ba"), "baz").exists());
+    }
+    
     @Test
     public void checkCheckModifyTimeTracking() throws IOException {
         manager = new FilesystemLoadSaveManager<>(baseDir, true);
@@ -196,6 +282,13 @@ public class FilesystemLoadSaveManagerTest extends XMLObjectBaseTestCase {
         Assert.assertEquals(target, new File(baseDir, "abc"));
     }
     
+    @Test
+    public void buildTargetFileFromKeyWithIntermediateDirs() throws IOException {
+        manager = new FilesystemLoadSaveManager<>(baseDir, intermediateDirectoryStrategy);
+        File target = manager.buildFile("abc");
+        Assert.assertEquals(target, new File(parentPath(baseDir, "ab"), "abc"));
+    }
+    
     @Test(expectedExceptions=IOException.class)
     public void targetExistsButIsNotAFile() throws IOException {
         File target = new File(baseDir, "abc");
@@ -341,6 +434,14 @@ public class FilesystemLoadSaveManagerTest extends XMLObjectBaseTestCase {
     
     // Helpers
     
+    private File parentPath(File base, String ... dirs) {
+        File parentPath = base;
+        for (String dir : dirs) {
+            parentPath = new File(parentPath, dir);
+        }
+        return parentPath;
+    }
+    
     private void testState(Set<String> expectedKeys) throws IOException {
         Assert.assertEquals(manager.listKeys().isEmpty(), expectedKeys.isEmpty() ? true : false);
         Assert.assertEquals(manager.listKeys(), expectedKeys);
@@ -365,11 +466,13 @@ public class FilesystemLoadSaveManagerTest extends XMLObjectBaseTestCase {
     private void resetBaseDir() throws IOException {
         if (baseDir.exists()) {
             if (baseDir.isDirectory()) {
-                for (File child : baseDir.listFiles()) {
-                    Files.delete(child.toPath());
-                }
+                Files.walk(baseDir.toPath())
+                    .sorted(Comparator.reverseOrder())
+                    .map(Path::toFile)
+                    .forEach(File::delete);
+            } else {
+                baseDir.delete();
             }
-            Files.delete(baseDir.toPath());
         }
     }
     
diff --git a/opensaml-core/src/test/java/org/opensaml/core/xml/persist/impl/SegmentingIntermediateDirectoryStrategyTest.java b/opensaml-core/src/test/java/org/opensaml/core/xml/persist/impl/SegmentingIntermediateDirectoryStrategyTest.java
new file mode 100644
index 000000000..1f66eef89
--- /dev/null
+++ b/opensaml-core/src/test/java/org/opensaml/core/xml/persist/impl/SegmentingIntermediateDirectoryStrategyTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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 org.opensaml.core.xml.persist.impl;
+
+import java.util.function.Function;
+
+import org.opensaml.core.xml.XMLRuntimeException;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.Lists;
+
+/**
+ *
+ */
+public class SegmentingIntermediateDirectoryStrategyTest {
+    
+    @Test
+    public void basic() {
+        SegmentingIntermediateDirectoryStrategy strategy = null;
+        Function<String, String> sourceStrategy = new PassthroughSourceStrategy();
+        
+        strategy = new SegmentingIntermediateDirectoryStrategy(1, 2, sourceStrategy);
+        Assert.assertEquals(strategy.apply("aabbccddeeffgg"), Lists.newArrayList("aa"));
+        
+        strategy = new SegmentingIntermediateDirectoryStrategy(3, 2, sourceStrategy);
+        Assert.assertEquals(strategy.apply("aabbccddeeffgg"), Lists.newArrayList("aa", "bb", "cc"));
+        
+        strategy = new SegmentingIntermediateDirectoryStrategy(3, 4, sourceStrategy);
+        Assert.assertEquals(strategy.apply("aabbccddeeffgg"), Lists.newArrayList("aabb", "ccdd", "eeff"));
+        
+    }
+    
+    @Test(expectedExceptions = XMLRuntimeException.class)
+    public void sourceTooShort() {
+        SegmentingIntermediateDirectoryStrategy strategy = new SegmentingIntermediateDirectoryStrategy(8, 2, new PassthroughSourceStrategy());
+        strategy.apply("aabbccddeeffgg");
+    }
+
+}

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


More information about the commits mailing list