Skip to content

Commit

Permalink
add ocfl extension spec to storage root
Browse files Browse the repository at this point in the history
  • Loading branch information
pwinckles committed Mar 17, 2021
1 parent 114e794 commit ec2d62e
Show file tree
Hide file tree
Showing 60 changed files with 6,039 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ private OcflConstants() {
public static final String EXT_CONFIG_JSON = "config.json";
public static final String INIT_EXT = "init";

public static final String OBJECT_NAMASTE_PREFIX = "0=ocfl_object";
public static final String OBJECT_NAMASTE_1_0 = "0=" + OcflVersion.OCFL_1_0.getOcflObjectVersion();

public static final String DEFAULT_INITIAL_VERSION_ID = "v1";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@
*/
public abstract class OcflObjectRootDirIterator implements Iterator<String>, Closeable {

protected static final String OCFL_OBJECT_MARKER_PREFIX = "0=ocfl_object";

private final String start;
private boolean started = false;
private boolean closed = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@

import java.util.Iterator;

import static edu.wisc.library.ocfl.api.OcflConstants.OBJECT_NAMASTE_PREFIX;

/**
* Implementation of {@link OcflObjectRootDirIterator} that iterates over cloud objects
*/
Expand All @@ -48,7 +50,7 @@ public CloudOcflObjectRootDirIterator(String start, CloudClient cloudClient) {

@Override
protected boolean isObjectRoot(String path) {
var listResult = cloudClient.list(FileUtil.pathJoinFailEmpty(path, OCFL_OBJECT_MARKER_PREFIX));
var listResult = cloudClient.list(FileUtil.pathJoinFailEmpty(path, OBJECT_NAMASTE_PREFIX));
return !listResult.getObjects().isEmpty();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
import java.util.Map;
import java.util.stream.Collectors;

import static edu.wisc.library.ocfl.api.OcflConstants.OBJECT_NAMASTE_PREFIX;

/**
* Initializes an OCFL repository in cloud storage. If the repository does not already exist, a new one is created. If it
* does exist, the client configuration is verified and {@link OcflStorageLayoutExtension} is created.
Expand All @@ -60,9 +62,9 @@ public class CloudOcflStorageInitializer {
private static final Logger LOG = LoggerFactory.getLogger(CloudOcflStorageInitializer.class);

private static final String SPECS_DIR = "specs/";
private static final String EXT_SPEC = "ocfl_extensions_1.0.md";
private static final String MEDIA_TYPE_TEXT = "text/plain; charset=UTF-8";
private static final String MEDIA_TYPE_JSON = "application/json; charset=UTF-8";
private static final String OBJECT_MARKER_PREFIX = "0=ocfl_object";

private final CloudClient cloudClient;
private final ObjectMapper objectMapper;
Expand Down Expand Up @@ -169,7 +171,7 @@ private String identifyRandomObjectRoot(String prefix) {
var response = cloudClient.listDirectory(prefix);

for (var object : response.getObjects()) {
if (object.getKeySuffix().startsWith(OBJECT_MARKER_PREFIX)) {
if (object.getKeySuffix().startsWith(OBJECT_NAMASTE_PREFIX)) {
var path = object.getKey().getPath();
return (String) path.subSequence(0, path.lastIndexOf('/'));
}
Expand Down Expand Up @@ -217,6 +219,7 @@ private OcflStorageLayoutExtension initNewRepo(OcflVersion ocflVersion, OcflExte
keys.add(writeOcflSpec(ocflVersion));
keys.addAll(writeOcflLayout(layoutConfig, layoutExtension.getDescription()));
keys.add(writeOcflLayoutSpec(layoutConfig));
keys.add(writeSpecFile(EXT_SPEC));
return layoutExtension;
} catch (RuntimeException e) {
LOG.error("Failed to initialize OCFL repository", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import java.nio.file.Paths;
import java.util.Iterator;

import static edu.wisc.library.ocfl.api.OcflConstants.OBJECT_NAMASTE_PREFIX;

/**
* Implementation of {@link OcflObjectRootDirIterator} that iterates over the filesystem
*/
Expand All @@ -49,7 +51,7 @@ public FileSystemOcflObjectRootDirIterator(Path start) {
@Override
protected boolean isObjectRoot(String path) {
try (var objectMarkers = Files.newDirectoryStream(Paths.get(path),
p -> p.getFileName().toString().startsWith(OCFL_OBJECT_MARKER_PREFIX))) {
p -> p.getFileName().toString().startsWith(OBJECT_NAMASTE_PREFIX))) {
return objectMarkers.iterator().hasNext();
} catch (IOException e) {
throw new OcflIOException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static edu.wisc.library.ocfl.api.OcflConstants.OBJECT_NAMASTE_PREFIX;

/**
* Prepares an OCFL storage root for use.
*/
Expand All @@ -62,7 +64,7 @@ public class FileSystemOcflStorageInitializer {
private static final Logger LOG = LoggerFactory.getLogger(FileSystemOcflStorageInitializer.class);

private static final String SPECS_DIR = "specs/";
private static final String OBJECT_MARKER_PREFIX = "0=ocfl_object";
private static final String EXT_SPEC = "ocfl_extensions_1.0.md";

private final ObjectMapper objectMapper;

Expand Down Expand Up @@ -186,7 +188,7 @@ private Path identifyRandomObjectRoot(Path root) {
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.getFileName().toString().startsWith(OBJECT_MARKER_PREFIX)) {
if (file.getFileName().toString().startsWith(OBJECT_NAMASTE_PREFIX)) {
ref.set(file.getParent());
return FileVisitResult.TERMINATE;
}
Expand Down Expand Up @@ -228,6 +230,7 @@ private OcflStorageLayoutExtension initNewRepo(Path repositoryRoot, OcflVersion
writeOcflSpec(repositoryRoot, ocflVersion);
writeOcflLayout(repositoryRoot, layoutConfig, layoutExtension.getDescription());
writeOcflLayoutSpec(repositoryRoot, layoutConfig);
writeSpecFile(repositoryRoot, EXT_SPEC);
return layoutExtension;
} catch (RuntimeException e) {
LOG.error("Failed to initialize OCFL repository at {}", repositoryRoot, e);
Expand Down
118 changes: 118 additions & 0 deletions ocfl-java-core/src/main/resources/specs/ocfl_extensions_1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# OCFL Community Extensions

**Version**: 1.0

This repository contains community extensions to the [OCFL Specification and Implementation Notes](https://ocfl.io/). Extensions are a means of adding new functionality and documenting standards outside of the main OCFL specification process. For example, storage layout extensions define how OCFL object IDs are mapped to OCFL object root directories within an OCFL storage root. This mapping is outside of the scope of the OCFL specification, but is valuable information to capture so that repositories are self-describing and easily accessible using generic OCFL tooling.

This is a community driven repository. Community members are encouraged to contribute by submitting new extensions and reviewing others' submissions. For more details, see the [review/merge policy](#review--merge-policy) below.

See the current set of [adopted extensions](https://ocfl.github.io/extensions/) and [extensions open for review and discussion](https://github.com/OCFL/extensions/pulls).

## Using Community Extensions

To use OCFL extensions you first need an OCFL client that supports the desired extensions. OCFL clients are not required to support extensions to be compliant with the OCFL specification, and the extensions that any given client supports will vary. The idea behind this repository is to encourage the development and implementation of common extensions so that there can be interoperability between OCFL clients.

## Implementing Community Extensions

Reference the OCFL specification's description of [object extensions](https://ocfl.io/1.0/spec/#object-extensions) and [storage root extensions](https://ocfl.io/1.0/spec/#storage-root-extensions).

The OCFL storage root MAY contain a copy of an extension's specification.

Each extension specification details how it should be implemented, but there are a few rules that apply to every extension.

A *root extension directory* refers to the directory named `extensions` that is located in either the storage root or an object root. An *extension directory* is an extension specific directory that is the child of a root extension directory and MUST be named using the extension's *Registered Name*, or `initial` (see [Optional Initial Extension](#optional-initial-extension)). For example, `extensions/0000-example-extension` is the extension directory for the extension [0000-example-extension](docs/0000-example-extension.md).

### Configuration Files

An extension's parameters are serialized as a JSON object and written to a configuration file named `config.json` within the extension's extension directory.

If an extension includes a configuration file, one of the properties in that file MUST be `extensionName`, where the value is the *Registered Name* of the extension.

For example, the extension [0000-example-extension](docs/0000-example-extension.md) could be parameterized as follows:

```json
{
"extensionName": "0000-example-extension",
"firstExampleParameter": 12,
"secondExampleParameter": "Hello",
"thirdExampleParameter": "Green"
}
```

Based on how the extension is used, its configuration file is written to one of the following locations, relative the storage root:

* `extensions/0000-example-extension/config.json`, if it is a [storage root extension](https://ocfl.io/1.0/spec/#storage-root-extensions)
* `OBJECT_ROOT/extensions/0000-example-extension/config.json`, if it is an [object extension](https://ocfl.io/1.0/spec/#object-extensions)

### Undefined Behavior

It is conceivable that some extensions may not be compatible with other extensions, or may be rendered incompatible based on how they're implemented in a client. For example, suppose that there are multiple extensions that define how logs should be written to an object's log directory. You could declare that your objects are using multiple log extensions, but the result is undefined and up to the implementing client. It may only write one log format or the other, it may write all of them, or it may reject the configuration entirely.

Because OCFL clients are not required to implement any or all extensions, it is also possible that a client may encounter an extension that it does not implement. In these cases, it is up to the client to decide how to proceed. A client may fail on unsupported extensions, or it may choose to ignore the extensions and carry on.

### Optional Initial Extension

A _root extension directory_ MAY optionally contain an _initial_ extension that, if it exists, SHOULD be applied before all other extensions in the directory.
An _initial extension_ is identified by the extension directory name "initial".

An _initial extension_ could be used to address some of the [undefined behaviors](#undefined-behavior), define how extensions are applied, and answer questions such as:

- Is an extension deactivated, only applying to earlier versions of the object?
- Should extensions be applied in a specific order?
- Does one extension depend on another?

## Specifying Community Extensions

### Layout

Community extensions MUST be written as GitHub flavored markdown files in the `docs` directory of this repository. The
filename of an extension is based on its *Registered Name* with a `.md` extension.

Extensions are numbered sequentially, and the *Registered Name* of an extension is prefixed with this 4-digit, zero-padded
decimal number. The *Registered Name* should be descriptive, use hyphens to separate words, and have a maximum of 250
characters in total.

New extensions should use `NNNN` as a place-holder for the next available prefix number at the time of merging. New extension pull-requests should not update the index document (`docs/index.md`), this will be done post-approval.

Extensions are intended to be mostly static once published. Substantial revisions of content beyond simple fixes warrants publishing a new extension, and marking the old extension obsolete by updating the *Obsoletes/Obsoleted by* sections in each extension respectively.

An example/template is available in this repository as "[OCFL Community Extension 0000: Example Extension](docs/0000-example-extension.md)" and is rendered
via GitHub pages as https://ocfl.github.io/extensions/0000-example-extension

### Headers

Extension definitions MUST contain a header section that defines the following fields:

* **Extension Name**: The extension's unique *Registered Name*
* **Authors**: The names of the individuals who authored the extension
* **Minimum OCFL Version**: The minimum OCFL version that the extension requires, eg. *1.0*
* **OCFL Community Extensions Version**: The version of the OCFL Extensions Specification that the extension conforms to, eg. *1.0*
* **Obsoletes**: The *Registered Name* of the extension that this extension obsoletes, or *n/a*
* **Obsoleted by**: The *Registered Name* of the extension that obsoletes this extension, or *n/a*

### Parameters

Extension definitions MAY define parameters to enable configuration as needed. Extension parameters are serialized as JSON values, and therefore must conform to the [JSON specification](https://tools.ietf.org/html/rfc8259). Parameters MUST be defined in the following structure:

* **Name**: A short, descriptive name for the parameter. The name is used as the parameter's key within its JSON representation.
* **Description**: A brief description of the function of the parameter. This should be expanded on in the main description of the extension which MUST reference all the parameters.
* **Type**: The JSON data type of the parameter value. One of `string`, `number`, `boolean`, `array`, or `object`. The structure of complex types MUST be further described.
* **Constraints**: A description of any constraints to apply to parameter values. Constraints may be plain text, regular expressions, [JSON Schema](https://www.ietf.org/archive/id/draft-handrews-json-schema-02.txt), or whatever makes the most sense for the extension.
* **Default**: The default value of parameter. If no default is specified, then the parameter is mandatory.

### Body

Each specification MUST thoroughly document how it is intended to be implemented and used, including detailed examples is helpful. If the extension uses parameters, the parameters MUST be described in detail in the body of the specification.

## Review / Merge Policy

1. A pull-request is submitted per the guidelines described in the "[Organization of this repository](https://github.com/OCFL/extensions#organization-of-this-repository)" section of this document
1. Authors of (legitimate) pull-requests will be added by an owner of the OCFL GitHub organization to the [extension-authors](https://github.com/orgs/OCFL/teams/extension-authors) team
- The purpose of being added to this team is to enable adding `labels` to their pull-request(s)
1. If a pull-request is submitted in order to facilitate discussion, the `draft` label should be applied by the author
1. If a pull-request is ready for review, it should have a title that is suitable for merge (i.e. not have a title indicating "draft"), and optionally have the `in-review` label applied by the author
1. A pull-request must be merged by an OCFL Editor if the following criteria are met:
1. At least two OCFL Editors have "[Approved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/approving-a-pull-request-with-required-reviews)" the pull-request
1. At least one other community member has "[Approved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/approving-a-pull-request-with-required-reviews)" the pull-request
1. The approvers represent three distinct organizations
1. After the pull-request has been merged with `NNNN` as a placeholder for the extension number in the _Registered Name_, an OCFL Editor will determine the extension number based on the next sequentially available number. They will create an additional administrative pull-request to change `NNNN` to the appropriate number in the extension file name and the extension document itself, as well as adding an entry to the index page entry (`docs/index.md`).
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ private void assertRootHasFiles(Path root) {
assertThat(children, containsInAnyOrder(
aFileNamed(equalTo("0=ocfl_1.0")),
aFileNamed(equalTo("ocfl_1.0.txt")),
aFileNamed(equalTo("ocfl_extensions_1.0.md")),
aFileNamed(equalTo(OcflConstants.OCFL_LAYOUT)),
aFileNamed(equalTo(HashedNTupleLayoutExtension.EXTENSION_NAME + ".md")),
aFileNamed(equalTo(OcflConstants.EXTENSIONS_DIR))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1100,7 +1100,7 @@ public void purgeObjectWhenExists() {
repo.describeObject(objectId);
});

assertEquals(5, listFilesInRepo(repoName).size());
assertEquals(6, listFilesInRepo(repoName).size());
}

@Test
Expand All @@ -1120,7 +1120,7 @@ public void purgeObjectDoNothingWhenDoesNotExist() {
repo.describeObject("o4");
});

assertEquals(12, listFilesInRepo(repoName).size());
assertEquals(13, listFilesInRepo(repoName).size());
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public void purgeShouldRemoveEmptyParentDirs() throws IOException {
repo.purgeObject(objectId);

assertThat(new ArrayList<>(Arrays.asList(repoDir(repoName).toFile().list())),
containsInAnyOrder("0=ocfl_1.0", "ocfl_1.0.txt",
containsInAnyOrder("0=ocfl_1.0", "ocfl_1.0.txt", "ocfl_extensions_1.0.md",
OcflConstants.EXTENSIONS_DIR, OcflConstants.OCFL_LAYOUT,
HashedNTupleLayoutExtension.EXTENSION_NAME + ".md"));
}
Expand Down
Loading

0 comments on commit ec2d62e

Please sign in to comment.