From 5558bfd2923fc9aed8bf632b824f7689af6cfd21 Mon Sep 17 00:00:00 2001 From: Valdeva Crema Date: Tue, 25 Jan 2022 11:18:58 -0500 Subject: [PATCH] NTuple Omit Prefix Implementation (#58) * NTuple Omit Prefix Implementation --- README.md | 4 + .../core/extension/OcflExtensionRegistry.java | 6 +- ...TupleOmitPrefixStorageLayoutExtension.java | 157 +++++++++++ .../NTupleOmitPrefixStorageLayoutConfig.java | 197 ++++++++++++++ ...0007-n-tuple-omit-prefix-storage-layout.md | 183 +++++++++++++ .../NTupleOmitPrefixStorageExtensionTest.java | 255 ++++++++++++++++++ pom.xml | 3 +- 7 files changed, 801 insertions(+), 4 deletions(-) create mode 100644 ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/storage/layout/NTupleOmitPrefixStorageLayoutExtension.java create mode 100644 ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/storage/layout/config/NTupleOmitPrefixStorageLayoutConfig.java create mode 100644 ocfl-java-core/src/main/resources/ocfl-specs/0007-n-tuple-omit-prefix-storage-layout.md create mode 100644 ocfl-java-core/src/test/java/edu/wisc/library/ocfl/core/extension/storage/layout/NTupleOmitPrefixStorageExtensionTest.java diff --git a/README.md b/README.md index 484d1047..7685dcb2 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ following extensions are implemented: * [0003-hash-and-id-n-tuple-storage-layout](https://ocfl.github.io/extensions/0003-hash-and-id-n-tuple-storage-layout.html): `HashedNTupleIdEncapsulationLayoutConfig` * [0004-hashed-n-tuple-storage-layout](https://ocfl.github.io/extensions/0004-hashed-n-tuple-storage-layout.html): `HashedNTupleLayoutConfig` * [0006-flat-omit-prefix-storage-layout](https://ocfl.github.io/extensions/0006-flat-omit-prefix-storage-layout.html): `FlatOmitPrefixLayoutConfig` + * [0007-n-tuple-omit-prefix-storage-layout](https://ocfl.github.io/extensions/0007-n-tuple-omit-prefix-storage-layout.html): `NTupleOmitPrefixStorageLayoutConfig` ### Optional Properties @@ -397,6 +398,9 @@ extensions: * [0006-flat-omit-prefix-storage-layout](https://ocfl.github.io/extensions/0006-flat-omit-prefix-storage-layout.html) * Configuration class: `FlatOmitPrefixLayoutConfig` * Implementation class: `FlatOmitPrefixLayoutExtension` +* [0007-n-tuple-omit-prefix-storage-layout](https://ocfl.github.io/extensions/0007-n-tuple-omit-prefix-storage-layout.html) + * Configuration class: `NTupleOmitPrefixStorageLayoutConfig` + * Implementation class: `NTupleOmitPrefixStorageLayoutExtension` #### Custom Storage Layout Extensions diff --git a/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/OcflExtensionRegistry.java b/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/OcflExtensionRegistry.java index a3a4b652..322dbc43 100644 --- a/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/OcflExtensionRegistry.java +++ b/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/OcflExtensionRegistry.java @@ -28,6 +28,7 @@ import edu.wisc.library.ocfl.api.util.Enforce; import edu.wisc.library.ocfl.core.extension.storage.layout.FlatLayoutExtension; import edu.wisc.library.ocfl.core.extension.storage.layout.FlatOmitPrefixLayoutExtension; +import edu.wisc.library.ocfl.core.extension.storage.layout.NTupleOmitPrefixStorageLayoutExtension; import edu.wisc.library.ocfl.core.extension.storage.layout.HashedNTupleIdEncapsulationLayoutExtension; import edu.wisc.library.ocfl.core.extension.storage.layout.HashedNTupleLayoutExtension; import org.slf4j.Logger; @@ -55,8 +56,9 @@ public final class OcflExtensionRegistry { HashedNTupleLayoutExtension.EXTENSION_NAME, HashedNTupleLayoutExtension.class, HashedNTupleIdEncapsulationLayoutExtension.EXTENSION_NAME, HashedNTupleIdEncapsulationLayoutExtension.class, FlatLayoutExtension.EXTENSION_NAME, FlatLayoutExtension.class, - FlatOmitPrefixLayoutExtension.EXTENSION_NAME, FlatOmitPrefixLayoutExtension.class - )); + FlatOmitPrefixLayoutExtension.EXTENSION_NAME, FlatOmitPrefixLayoutExtension.class, + NTupleOmitPrefixStorageLayoutExtension.EXTENSION_NAME, NTupleOmitPrefixStorageLayoutExtension.class + )); private OcflExtensionRegistry() { diff --git a/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/storage/layout/NTupleOmitPrefixStorageLayoutExtension.java b/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/storage/layout/NTupleOmitPrefixStorageLayoutExtension.java new file mode 100644 index 00000000..3423136c --- /dev/null +++ b/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/storage/layout/NTupleOmitPrefixStorageLayoutExtension.java @@ -0,0 +1,157 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 University of Wisconsin Board of Regents + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.wisc.library.ocfl.core.extension.storage.layout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.wisc.library.ocfl.core.extension.storage.layout.config.NTupleOmitPrefixStorageLayoutConfig; +import edu.wisc.library.ocfl.api.OcflConstants; +import edu.wisc.library.ocfl.api.exception.OcflExtensionException; +import edu.wisc.library.ocfl.api.util.Enforce; +import edu.wisc.library.ocfl.core.extension.OcflExtensionConfig; +import edu.wisc.library.ocfl.core.extension.storage.layout.OcflStorageLayoutExtension; + +/** + * Implementation of the + * N Tuple Storage Layout extension. + * + * @author vcrema + */ +public class NTupleOmitPrefixStorageLayoutExtension implements OcflStorageLayoutExtension { + + public static final String EXTENSION_NAME = "0007-n-tuple-omit-prefix-storage-layout"; + + private static final Logger LOG = LoggerFactory.getLogger(NTupleOmitPrefixStorageLayoutExtension.class); + + private NTupleOmitPrefixStorageLayoutConfig config; + + /** + * {@inheritDoc} + */ + @Override + public String getExtensionName() { + return EXTENSION_NAME; + } + + /** + * {@inheritDoc} + */ + @Override + public String getDescription() { + return "This storage root extension describes an OCFL storage layout " + "combining a pairtree-like root directory structure derived from " + + "prefix-omitted object identifiers, followed by the prefix-omitted " + "object identifier themselves. The OCFL object identifiers are " + + "expected to contain prefixes which are removed in the mapping to " + "directory names. The OCFL object identifier prefix is defined as " + + "all characters before and including a configurable delimiter. " + "Where the prefix-omitted identifier length is less than " + + "tuple size * number of tuples, the remaining object id (prefix omitted) " + + "is left or right-side, zero-padded (configurable, left default), " + "or not padded (none), and optionally reversed (default false). " + + "The object id is then divided into N n-tuple segments, and used " + "to create nested paths under the OCFL storage root, followed by " + + "the prefix-omitted object id directory."; + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized void init(OcflExtensionConfig config) { + Enforce.notNull(config, "configFile cannot be null"); + + if (!(config instanceof NTupleOmitPrefixStorageLayoutConfig)) { + throw new OcflExtensionException(String.format("This extension only supports %s configuration. Received: %s", getExtensionConfigClass(), config)); + } + + NTupleOmitPrefixStorageLayoutConfig castConfig = (NTupleOmitPrefixStorageLayoutConfig) config; + + this.config = castConfig; + } + + @Override + public Class getExtensionConfigClass() { + return NTupleOmitPrefixStorageLayoutConfig.class; + } + + /** + * {@inheritDoc} + */ + @Override + public String mapObjectId(String objectId) { + if (config == null) { + throw new OcflExtensionException("This extension must be initialized before it can be used."); + } + + if (!objectId.matches("\\A\\p{ASCII}*\\z")) { + throw new OcflExtensionException(String.format("This id %s must contain only ASCII characters.", objectId)); + } + // Split by delimiter and get the last part + String id = objectId.toLowerCase(); + int index = id.lastIndexOf(config.getDelimiter()); + String section = objectId; + String baseObjectId = ""; + + if (index > -1) { + section = objectId.substring(index + config.getDelimiter().length()); + baseObjectId = section; + } else { + throw new OcflExtensionException(String.format("The delimiter %s cannot be found in %s.", config.getDelimiter(), objectId)); + } + + if (OcflConstants.EXTENSIONS_DIR.equals(section) || section.isEmpty()) { + throw new OcflExtensionException(String.format( + "The object id <%s> is incompatible with layout extension " + "%s because it is empty or conflicts with the extensions directory.", + objectId, EXTENSION_NAME)); + } + if (section.length() == 0) { + throw new OcflExtensionException(String.format("The delimiter %s is only found at the end of %s.", config.getDelimiter(), objectId)); + } + + if (config.reverseObjectRoot()) { + // Reverse the section + section = new StringBuilder(section).reverse().toString(); + } + // Add padding if needed and requested + if (section.length() < config.getTupleSize() * config.getNumberOfTuples()) { + + int paddingAmount = config.getTupleSize() * config.getNumberOfTuples(); + if (config.getZeroPadding().equals(NTupleOmitPrefixStorageLayoutConfig.ZeroPadding.LEFT)) { + section = "0".repeat(paddingAmount - section.length()) + section; + } else if (config.getZeroPadding().equals(NTupleOmitPrefixStorageLayoutConfig.ZeroPadding.RIGHT)) { + section = section + "0".repeat(paddingAmount - section.length()); + } + } + StringBuilder pathBuilder = new StringBuilder(); + // Split into even sections + for (int i = 0; i < config.getNumberOfTuples(); i++) { + int start = i * config.getTupleSize(); + int end = start + config.getTupleSize(); + pathBuilder.append(section, start, end).append("/"); + } + + // Append the original object id after the delimiter + pathBuilder.append(baseObjectId); + return pathBuilder.toString(); + } + +} \ No newline at end of file diff --git a/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/storage/layout/config/NTupleOmitPrefixStorageLayoutConfig.java b/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/storage/layout/config/NTupleOmitPrefixStorageLayoutConfig.java new file mode 100644 index 00000000..9632bdc8 --- /dev/null +++ b/ocfl-java-core/src/main/java/edu/wisc/library/ocfl/core/extension/storage/layout/config/NTupleOmitPrefixStorageLayoutConfig.java @@ -0,0 +1,197 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 University of Wisconsin Board of Regents + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package edu.wisc.library.ocfl.core.extension.storage.layout.config; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import edu.wisc.library.ocfl.core.extension.storage.layout.NTupleOmitPrefixStorageLayoutExtension; +import edu.wisc.library.ocfl.api.util.Enforce; +import edu.wisc.library.ocfl.core.extension.OcflExtensionConfig; + +/** + * Configuration for the + * N Tuple Storage Layout extension. + * + * @author vcrema + * @since 2021-10-25 + */ +public class NTupleOmitPrefixStorageLayoutConfig implements OcflExtensionConfig { + + public enum ZeroPadding { + LEFT, RIGHT + } + + private String delimiter; + private int tupleSize = 3; + private int numberOfTuples = 3; + private ZeroPadding zeroPadding = ZeroPadding.LEFT; + private boolean reverseObjectRoot = false; + + @JsonIgnore + @Override + public String getExtensionName() { + return NTupleOmitPrefixStorageLayoutExtension.EXTENSION_NAME; + } + + @JsonIgnore + @Override + public boolean hasParameters() { + return true; + } + + /** + * @return the delimiter marking end of prefix + */ + public String getDelimiter() { + return delimiter; + } + + /** + * @return tupleSize - the segment size (in characters) to split the digest + * into + */ + public int getTupleSize() { + return tupleSize; + } + + /** + * @return The number of segments to use for path generation + */ + public int getNumberOfTuples() { + return numberOfTuples; + } + + /** + * return zeroPadding - Indicates whether to use left or right zero padding + * for ids less than tupleSize * numberOfTuples + */ + public ZeroPadding getZeroPadding() { + return zeroPadding; + } + + /** + * return true or false, indicates that the prefix-omitted, padded object + * identifier should be reversed + */ + public boolean reverseObjectRoot() { + return reverseObjectRoot; + } + + /** + * The case-insensitive, delimiter marking the end of the OCFL object + * identifier prefix; MUST consist of a character string of length one or + * greater. If the delimiter is found multiple times in the OCFL object + * identifier, its last occurrence (right-most) will be used to select the + * termination of the prefix. + * + * @param delimiter + * marking the end of prefix + */ + public NTupleOmitPrefixStorageLayoutConfig setDelimiter(String delimiter) { + this.delimiter = Enforce.expressionTrue(delimiter != null && !delimiter.isEmpty(), delimiter, "delimiter must not be empty"); + + return this; + } + + /** + * the segment size (in characters) to split the digest into + * + * @param tupleSize + * - the segment size (in characters) to split the digest into + * + */ + public NTupleOmitPrefixStorageLayoutConfig setTupleSize(int tupleSize) { + this.tupleSize = Enforce.expressionTrue(tupleSize >= 1 && tupleSize <= 32, tupleSize, "tupleSize must be between 1 and 32 inclusive"); + return this; + } + + /** + * The number of segments to use for path generation + * + * @param numberOfTuples + * - The number of segments to use for path generation + * + */ + public NTupleOmitPrefixStorageLayoutConfig setNumberOfTuples(int numberOfTuples) { + this.numberOfTuples = Enforce.expressionTrue(numberOfTuples >= 1 && numberOfTuples <= 32, numberOfTuples, + "numberOfTuples must be between 1 and 32 inclusive"); + return this; + } + + /** + * Indicates whether to use left or right zero padding for ids less than + * tupleSize * numberOfTuples + * + * @param zeroPadding + * + */ + public NTupleOmitPrefixStorageLayoutConfig setZeroPadding(ZeroPadding zeroPadding) { + Enforce.notNull(zeroPadding, "Zero padding cannot be null"); + this.zeroPadding = zeroPadding; + return this; + } + + /** + * indicates that the prefix-omitted, padded object identifier should be + * reversed + * + * @param reverseObjectRoot + * - indicates that the prefix-omitted, padded object identifier + * should be reversed + * + */ + public NTupleOmitPrefixStorageLayoutConfig setReverseObjectRoot(boolean reverseObjectRoot) { + this.reverseObjectRoot = reverseObjectRoot; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NTupleOmitPrefixStorageLayoutConfig that = (NTupleOmitPrefixStorageLayoutConfig) o; + return delimiter.equals(that.delimiter) && tupleSize == that.tupleSize && numberOfTuples == that.numberOfTuples && zeroPadding == that.zeroPadding + && reverseObjectRoot == that.reverseObjectRoot; + } + + @Override + public int hashCode() { + return Objects.hash(delimiter, tupleSize, numberOfTuples, reverseObjectRoot, zeroPadding); + } + + @Override + public String toString() { + return "NTupleOmitPrefixStorageLayoutConfig{ delimiter='" + delimiter + "', tupleSize='" + tupleSize + "', numberOfTuples='" + numberOfTuples + + "', zeroPadding='" + zeroPadding + "', reverseObjectRoot='" + reverseObjectRoot + "' }"; + } + +} \ No newline at end of file diff --git a/ocfl-java-core/src/main/resources/ocfl-specs/0007-n-tuple-omit-prefix-storage-layout.md b/ocfl-java-core/src/main/resources/ocfl-specs/0007-n-tuple-omit-prefix-storage-layout.md new file mode 100644 index 00000000..a7a7eb33 --- /dev/null +++ b/ocfl-java-core/src/main/resources/ocfl-specs/0007-n-tuple-omit-prefix-storage-layout.md @@ -0,0 +1,183 @@ +# OCFL Community Extension 0007: N Tuple Omit Prefix Storage Layout + + * **Extension Name:** 0007-n-tuple-omit-prefix-storage-layout + * **Authors:** Michael Vandermillen and Andrew Woods + * **Minimum OCFL Version:** 1.0 + * **OCFL Community Extensions Version:** 1.0 + * **Obsoletes:** n/a + * **Obsoleted by:** n/a + +## Overview + +This storage root extension describes an OCFL storage layout combining a pairtree-like root directory structure derived from prefix-omitted object identifiers, followed by the prefix-omitted object identifier themselves. +The OCFL object identifiers are expected to contain prefixes which are removed in the mapping to directory names. +The OCFL object identifier prefix is defined as all characters before and including a configurable delimiter. +Where the prefix-omitted identifier length is less than tuple size * number of tuples, the remaining object id (prefix omitted) is left or right-side, zero-padded (configurable, left default), and optionally reversed (default false). +The object id is then divided into N n-tuple segments, and used to create nested paths under the OCFL storage root, followed by the prefix-omitted object id directory. + +This layout combines the advantages of 0006-flat-omit-prefix-storage-layout (directory name transparency) and the 0004-hashed-n-tuple-storage-layout (enhanced file system/bucket performance). + +The limitations of this layout are filesystem dependent (with one exception), and are generally as follows: + +* The size of object identifiers, minus the length of the prefix, cannot exceed the maximum allowed directory name size + (eg. 255 characters) +* Object identifiers cannot include characters that are illegal in directory names +* This extension is defined over the ASCII subset of UTF-8 (code points 0x20 to 0x7F). Any character outside of this range in either an identifier or a path is an error. + +## Parameters + +### Summary + +* **Name:** `delimiter` + * **Description:** The case-insensitive, delimiter marking the end of the OCFL object identifier prefix; MUST consist + of a character string of length one or greater. If the delimiter is found multiple times in the OCFL object + identifier, its last occurence (right-most) will be used to select the termination of the prefix. + * **Type:** string + * **Constraints:** Must not be empty + * **Default:** : +* **Name**: `tupleSize` + * **Description:** Indicates the segment size (in characters) to split the + identifier into + * **Type:** number + * **Constraints:** An integer between 1 and 32 inclusive + * **Default:** 3 +* **Name:** `numberOfTuples` + * **Description:** Indicates the number of segments to use for path generation + * **Type:** number + * **Constraints:** An integer between 1 and 32 inclusive + * **Default:** 3 +* **Name:** `zeroPadding` + * **Description:** Indicates whether to use left or right zero padding for ids less than tupleSize * numberOfTuples + * **Type:** string + * **Constraints:** Must be either "left" or "right" + * **Default:** left +* **Name:** `reverseObjectRoot` + * **Description:** When true, indicates that the prefix-omitted, padded object identifier should be reversed + * **Type:** boolean + * **Default:** false + +## Procedure + +The following is an outline of the steps to map an OCFL object identifier to an +OCFL object root path: + +1. Remove the prefix, which is everything to the left of the right-most instance of the delimiter, as well as the delimiter. If there is no delimiter, the whole id is used; if the delimiter is found at the end, an error is thrown. +2. Optionally, add zero-padding to the left or right of the remaining id, depending on `zeroPadding` configuration. +3. Optionally reverse the remaining id, depending on `reverseObjectRoot` +4. Starting at the leftmost character of the resulting id and working right, divide the id into `numberOfTuples` each containing `tupleSize` characters. +5. Create the start of the object root path by joining the tuples, in order, using the filesystem path separator. +6. Complete the object root path by joining the prefix-omitted id (from step 1) onto the end. + +## Examples + +### Example 1 + +This example demonstrates mappings where the single-character delimiter is found one or more times in the OCFL object +identifier, with default `zeroPadding`, modified `tupleSize` and `numberOfTuples`, and `reverseObjectRoot` true. + +#### Parameters + +```json +{ + "extensionName": "0007-n-tuple-omit-prefix-storage-layout", + "delimiter": ":", + "tupleSize": 4, + "numberOfTuples": 2, + "zeroPadding": "left", + "reverseObjectRoot": true +} +``` + +#### Mappings + +| Object ID | Object Root Path | +| --- | --- | +| namespace:12887296 | `6927/8821/12887296` | +| urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66 | `66a9/c002/6e8bc430-9c3a-11d9-9669-0800200c9a66` | +| abc123 | `321c/ba00/abc123` + +#### Storage Hierarchy + +``` +[storage_root]/ +├── 0=ocfl_1.0 +├── ocfl_layout.json +├── extensions/ +│ └── 0007-n-tuple-omit-prefix-storage-layout/ +│ └── config.json +├── 6927/ +│ └── 8821/ +│ └── 12887296/ +│ ├── 0=ocfl_object_1.0 +│ ├── inventory.json +│ ├── inventory.json.sha512 +│ └── v1 [...] +├── 66a9/ +│ └── c002/ +│ └── 6e8bc430-9c3a-11d9-9669-0800200c9a66/ +│ ├── 0=ocfl_object_1.0 +│ ├── inventory.json +│ ├── inventory.json.sha512 +│ └── v1 [...] +└── 321c/ + └── ba00/ + └── abc123/ + ├── 0=ocfl_object_1.0 + ├── inventory.json + ├── inventory.json.sha512 + └── v1 [...] + +``` + +### Example 2 + +This example demonstrates mappings where the multi-character delimiter is found one or more times in the OCFL object +identifier, with default `tupleSize`, `numberOfTuples`, and `reverseObjectRoot`, and modified `zeroPadding`. + +#### Parameters + +```json +{ + "extensionName": "0007-n-tuple-omit-prefix-storage-layout", + "delimiter": "edu/", + "tupleSize": 3, + "numberOfTuples": 3, + "zeroPadding": "right", + "reverseObjectRoot": false +} +``` + +#### Mappings + +| Object ID | Object Root Path | +| --- | --- | +| https://institution.edu/3448793 | `344/879/300/3448793` | +| https://institution.edu/abc/edu/f8.05v | `f8./05v/000/f8.05v` | + +#### Storage Hierarchy + +``` +[storage_root]/ +├── 0=ocfl_1.0 +├── ocfl_layout.json +├── extensions/ +│ └── 0007-n-tuple-omit-prefix-storage-layout/ +│ └── config.json +├── 344/ +│ └── 879/ +│ └── 300/ +│ └── 3448793/ +│ ├── 0=ocfl_object_1.0 +│ ├── inventory.json +│ ├── inventory.json.sha512 +│ └── v1 [...] +└── f8./ + └── 05v/ + └── 000/ + └── f8.05v/ + ├── 0=ocfl_object_1.0 + ├── inventory.json + ├── inventory.json.sha512 + └── v1 [...] +``` + diff --git a/ocfl-java-core/src/test/java/edu/wisc/library/ocfl/core/extension/storage/layout/NTupleOmitPrefixStorageExtensionTest.java b/ocfl-java-core/src/test/java/edu/wisc/library/ocfl/core/extension/storage/layout/NTupleOmitPrefixStorageExtensionTest.java new file mode 100644 index 00000000..d24fb439 --- /dev/null +++ b/ocfl-java-core/src/test/java/edu/wisc/library/ocfl/core/extension/storage/layout/NTupleOmitPrefixStorageExtensionTest.java @@ -0,0 +1,255 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 University of Wisconsin Board of Regents + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package edu.wisc.library.ocfl.core.extension.storage.layout; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import edu.wisc.library.ocfl.api.exception.OcflExtensionException; +import edu.wisc.library.ocfl.api.exception.OcflInputException; +import edu.wisc.library.ocfl.core.extension.storage.layout.NTupleOmitPrefixStorageLayoutExtension; +import edu.wisc.library.ocfl.core.extension.storage.layout.config.NTupleOmitPrefixStorageLayoutConfig; + +/** + * @author vcrema + * @since 2021-10-25 + */ +public class NTupleOmitPrefixStorageExtensionTest { + + private NTupleOmitPrefixStorageLayoutExtension ext; + private NTupleOmitPrefixStorageLayoutConfig config; + + @BeforeEach + public void setUp() { + ext = new NTupleOmitPrefixStorageLayoutExtension(); + config = new NTupleOmitPrefixStorageLayoutConfig(); + } + + @Test + public void testNullDelimiter() { + assertThrows(OcflInputException.class, () -> config.setDelimiter(null), + "Expected OcflInputException"); + } + + @Test + public void testEmptyDelimiter() { + assertThrows(OcflInputException.class, () -> config.setDelimiter(""), + "Expected OcflInputException"); + } + + @Test + public void testNegativeTupleSize() { + assertThrows(OcflInputException.class, () -> config.setTupleSize(-1), + "Expected OcflInputException"); + } + + @Test + public void testZeroTupleSize() { + assertThrows(OcflInputException.class, () -> config.setTupleSize(0), + "Expected OcflInputException"); + } + + @Test + public void testNegativeNumberOfTuples() { + assertThrows(OcflInputException.class, () -> config.setNumberOfTuples(-1), + "Expected OcflInputException"); + } + + @Test + public void testZeroNumberOfTuples() { + assertThrows(OcflInputException.class, () -> config.setNumberOfTuples(0), + "Expected OcflInputException"); + } + + @Test + public void testIncorrectZeroPadding() { + assertThrows(OcflInputException.class, () -> config.setZeroPadding(null), + "Expected OcflInputException"); + } + + @Test + public void testNonAsciiId() { + config.setDelimiter("/"); + ext.init(config); + assertThrows(OcflExtensionException.class, () -> ext.mapObjectId("jå∫∆a/vµa2bl√øog"), + "Expected OcflExtensionException"); + } + + + @Test + public void testDelimiterNotFound() { + config.setDelimiter("/"); + ext.init(config); + //Defaults: + //tupleSize: 3 + //numberOfTuples: 3, + //zeroPadding: "left", + //reverseObjectRoot: false + + assertThrows(OcflExtensionException.class, () -> ext.mapObjectId("namespace:12887296"), + "Expected OcflExtensionException"); + } + + @Test + public void testOneOccurrenceOfSingleCharDelimiter() { + config.setDelimiter(":"); + ext.init(config); + //Defaults: + //tupleSize: 3 + //numberOfTuples: 3, + //zeroPadding: "left", + //reverseObjectRoot: false + + String result = ext.mapObjectId("namespace:128872961"); + assertEquals("128/872/961/128872961", result); + } + + @Test + public void testTwoOccurrencesOfSingleCharDelimiter() { + config.setDelimiter(":"); + ext.init(config); + //Defaults: + //tupleSize: 3 + //numberOfTuples: 3, + //zeroPadding: "left", + //reverseObjectRoot: false + + String result = ext.mapObjectId("urn:uuid:6e8bc430-9c3a-11d9-9669-0800200c9a66"); + assertEquals("6e8/bc4/30-/6e8bc430-9c3a-11d9-9669-0800200c9a66", result); + } + + @Test + public void testTwoOccurrencesOfSingleCharDelimiterDRS() { + config.setDelimiter(":"); + ext.init(config); + //Defaults: + //tupleSize: 3 + //numberOfTuples: 3, + //zeroPadding: "left", + //reverseObjectRoot: false + + String result = ext.mapObjectId("urn-3:HUL.DRS.OBJECT:128872961"); + assertEquals("128/872/961/128872961", result); + } + + @Test + public void testOneOccurrenceOfMultiCharDelimiter() { + config.setDelimiter("edu/"); + ext.init(config); + //Defaults: + //tupleSize: 3 + //numberOfTuples: 3, + //zeroPadding: "left", + //reverseObjectRoot: false + + String result = ext.mapObjectId("https://institution.edu/344879388"); + assertEquals("344/879/388/344879388", result); + } + + @Test + public void testTwoOccurrencesOfMultiCharDelimiter() { + config.setDelimiter("edu/"); + ext.init(config); + //Defaults: + //tupleSize: 3 + //numberOfTuples: 3, + //zeroPadding: "left", + //reverseObjectRoot: false + + String result = ext.mapObjectId("https://institution.edu/abc/edu/f8.05v"); + assertEquals("000/f8./05v/f8.05v", result); + } + + @Test + public void testTupleAndLeftPadding() { + config.setDelimiter(":"); + config.setTupleSize(4); + config.setNumberOfTuples(2); + ext.init(config); + //Defaults: + //zeroPadding: "left", + //reverseObjectRoot: false + + String result = ext.mapObjectId("namespace:1288729"); + assertEquals("0128/8729/1288729", result); + } + + @Test + public void testTupleAndRightPadding() { + config.setDelimiter(":"); + config.setTupleSize(4); + config.setNumberOfTuples(2); + config.setZeroPadding(NTupleOmitPrefixStorageLayoutConfig.ZeroPadding.RIGHT); + ext.init(config); + //Defaults: + //reverseObjectRoot: false + + String result = ext.mapObjectId("namespace:1288729"); + assertEquals("1288/7290/1288729", result); + } + + @Test + public void testReverseAndRightPadding() { + config.setDelimiter(":"); + config.setTupleSize(4); + config.setNumberOfTuples(2); + config.setReverseObjectRoot(true); + config.setZeroPadding(NTupleOmitPrefixStorageLayoutConfig.ZeroPadding.RIGHT); + ext.init(config); + + String result = ext.mapObjectId("namespace:1288729"); + assertEquals("9278/8210/1288729", result); + } + + @Test + public void testOneOccurrenceOfMultiCharDelimiterAtEnd() { + config.setDelimiter("edu/"); + ext.init(config); + //Defaults: + //tupleSize: 3 + //numberOfTuples: 3, + //zeroPadding: "left", + //reverseObjectRoot: false + + assertThrows(OcflExtensionException.class, () -> ext.mapObjectId("https://institution.edu/"), + "Expected OcflExtensionException"); + } + + @Test + public void testMultiCharDelimiterNotFound() { + config.setDelimiter("com/"); + ext.init(config); + //Defaults: + //tupleSize: 3 + //numberOfTuples: 3, + //zeroPadding: "left", + //reverseObjectRoot: false + + assertThrows(OcflExtensionException.class, () -> ext.mapObjectId("https://institution.edu/344879388"), + "Expected OcflExtensionException"); + } +} diff --git a/pom.xml b/pom.xml index 27cec3c2..5126c9ec 100644 --- a/pom.xml +++ b/pom.xml @@ -351,7 +351,6 @@ bcprov-jdk15on 1.69 - software.amazon.awssdk @@ -367,7 +366,7 @@ hamcrest-library 2.2 - + org.junit.jupiter junit-jupiter 5.8.1