diff --git a/.github/maven-central-settings.xml b/.github/maven-central-settings.xml
deleted file mode 100644
index d0e71a1..0000000
--- a/.github/maven-central-settings.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- ossrh
- ${env.MAVEN_USERNAME}
- ${env.MAVEN_PASSWORD}
-
-
-
\ No newline at end of file
diff --git a/.github/workflows/java11-push.yml b/.github/workflows/codacy-coverage.yml
similarity index 75%
rename from .github/workflows/java11-push.yml
rename to .github/workflows/codacy-coverage.yml
index d934d2c..bae0f36 100644
--- a/.github/workflows/java11-push.yml
+++ b/.github/workflows/codacy-coverage.yml
@@ -1,9 +1,7 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
-name: Java 11 CI
-permissions:
- contents: read
+name: Codacy Coverage
on:
push:
@@ -16,12 +14,13 @@ jobs:
steps:
- uses: actions/checkout@v4
- - name: Set up JDK 11
- uses: actions/setup-java@v1
+ - name: Set up JDK 8
+ uses: actions/setup-java@v4
with:
- java-version: 11
+ java-version: 8
+ distribution: 'temurin'
- name: Build with Maven
- run: mvn -B package jacoco:report --file pom.xml
+ run: mvn -B package jacoco:report
- name: Run codacy-coverage-reporter
uses: codacy/codacy-coverage-reporter-action@master
with:
diff --git a/.github/workflows/java11-publish.yml b/.github/workflows/java11-publish.yml
deleted file mode 100644
index 0c60bcc..0000000
--- a/.github/workflows/java11-publish.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-permissions:
- contents: read
- packages: write
-name: Publish JRE11 to the Maven Central
-on:
- release:
- types: [created]
-
- workflow_dispatch:
-
-jobs:
- publish:
- runs-on: ubuntu-latest
- environment: maven-central
- steps:
- - uses: actions/checkout@v4
- - name: Set up JDK 11
- uses: actions/setup-java@v4
- with:
- java-version: 11
- distribution: 'temurin'
- server-id: ossrh
- - name: Import GPG Key
- uses: crazy-max/ghaction-import-gpg@v6
- with:
- gpg_private_key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
- passphrase: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
- - name: Publish package
- run: mvn -B -Pmaven-central -Dgpg.passphrase=${{secrets.MAVEN_GPG_PASSPHRASE}} -s .github/maven-central-settings.xml deploy
- env:
- MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
- MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
diff --git a/.github/workflows/java11-build.yml b/.github/workflows/java8-build.yml
similarity index 61%
rename from .github/workflows/java11-build.yml
rename to .github/workflows/java8-build.yml
index d2d2612..184fc8d 100644
--- a/.github/workflows/java11-build.yml
+++ b/.github/workflows/java8-build.yml
@@ -1,10 +1,7 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
-name: Java 11 PR
-
-permissions:
- contents: read
+name: Java 8 CI
on:
pull_request:
@@ -16,10 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Set up JDK 11
- uses: actions/setup-java@v1
+ - uses: actions/checkout@v4
+ - name: Set up JDK 8
+ uses: actions/setup-java@v4
with:
- java-version: 11
+ java-version: 8
+ distribution: 'temurin'
- name: Build with Maven
- run: mvn -f pom.xml package
+ run: mvn package
diff --git a/.github/workflows/java8-publish.yml b/.github/workflows/java8-publish.yml
new file mode 100644
index 0000000..34e5632
--- /dev/null
+++ b/.github/workflows/java8-publish.yml
@@ -0,0 +1,33 @@
+name: Publish JRE8 to the Maven Central
+
+on:
+ release:
+ types: [created]
+
+ workflow_dispatch:
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ environment: maven-central
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up JDK 8
+ uses: actions/setup-java@v4
+ with:
+ java-version: 8
+ distribution: 'temurin'
+ cache: 'maven'
+ server-id: central
+ server-username: CENTRAL_TOKEN_ID
+ server-password: CENTRAL_TOKEN_SECRET
+ gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
+ gpg-passphrase: MAVEN_GPG_PASSPHRASE
+ - name: Publish package
+ run: mvn -B -Pmaven-central deploy
+ env:
+ CENTRAL_TOKEN_ID: ${{ secrets.CENTRAL_TOKEN_ID }}
+ CENTRAL_TOKEN_SECRET: ${{ secrets.CENTRAL_TOKEN_SECRET }}
+ MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
+ MAVEN_GPG_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
+
diff --git a/README.md b/README.md
index 33d67de..2818dbe 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,28 @@
+# Carbon Decentralized Identifiers
-> [!IMPORTANT]
-> Your feedback is essential to the improvement of this library. Please share any concerns, primary use cases, areas for enhancement, or challenges you have encountered. Your insights help refine and optimize the library to better meet user needs. Thank you for your time and contributions.
-
-# Carbon DID
An implementation of the [Decentralized Identifiers (DIDs) v1.0](https://www.w3.org/TR/did-core/) in Java.
-
-[](https://github.com/filip26/carbon-decentralized-identifiers/actions/workflows/java11-push.yml)
-[](https://app.codacy.com/gh/filip26/carbon-decentralized-identifiers/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
-[](https://app.codacy.com/gh/filip26/carbon-decentralized-identifiers/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
+[](https://github.com/filip26/carbon-did-core/actions/workflows/java8-build.yml)
+[](https://app.codacy.com/gh/filip26/carbon-did-core/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
[](https://search.maven.org/search?q=g:com.apicatalog%20AND%20a:carbon-did)
[](https://opensource.org/licenses/Apache-2.0)
-
## Features
-* DID, DID URL, DID Document
-* Methods
- * [did:key method v0.7](https://w3c-ccg.github.io/did-method-key/)
+* DID, DID URL primitives
+* DID Document API & primitives
+* DID Resolver API
+ * [`did:key`](https://github.com/filip26/carbon-did-key) method
## Installation
-### Carbon DID
-
-#### Maven
+### Maven
```xml
com.apicatalog
carbon-did
- 0.6.0
-
-
- com.apicatalog
- copper-multibase
- 0.5.0
-
-```
-
-### JSON-P Provider
-
-Add JSON-P provider, if it is not on the classpath already.
-
-#### Maven
-
-```xml
-
- org.glassfish
- jakarta.json
- 2.0.1
+ 0.9.1
```
@@ -66,17 +40,18 @@ All PR's welcome!
Fork and clone the project repository.
-#### Java 11+
+#### Java 1.8
```bash
-> cd carbon-decentralized-identifiers
+> cd carbon-did-core
> mvn clean package
```
-## Resources-
-- [Decentralized Identifiers (DIDs) v1.0](https://www.w3.org/TR/did-core/)
-- [The did:key Method v0.7](https://w3c-ccg.github.io/did-method-key/)
-- [Copper Multicodec](https://github.com/filip26/copper-multicodec)
-- [Copper Multibase](https://github.com/filip26/copper-multibase)
+## Resources
+
+- [W3C Decentralized Identifiers (DIDs) v1.0](https://www.w3.org/TR/did-core/)
+- [W3C Controlled Identifiers v1.0](https://www.w3.org/TR/cid-1.0/)
+- [Carbon DID Key Method](https://github.com/filip26/carbon-did-key)
+- [Carbon Controlled Identifiers](https://github.com/filip26/carbon-cid)
## Sponsors
@@ -86,4 +61,3 @@ Fork and clone the project repository.
## Commercial Support
Commercial support is available at filip26@gmail.com
-
diff --git a/SECURITY.md b/SECURITY.md
index 615086c..42bb4fe 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,10 +2,16 @@
## Supported Versions
-| Version | Supported |
-| ------- |:------------------:|
-| 0.x.x | โ |
+| Version | Supported |
+| ------- |:---------:|
+| 0.x.x | โ
Supported |
+| 1.x.x | โ Not supported |
## Reporting a Vulnerability
-Please report security vulnerabilities to [Filip Kolarik](mailto:filip26@gmail.com). Thank you!
+If you discover a security vulnerability, please report it responsibly by contacting:
+
+**Filip Kolarik**
+๐ง [filip26@gmail.com](mailto:filip26@gmail.com)
+
+We will investigate promptly and work with you to address the issue. Thank you for helping keep the project secure!
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 3abe262..1f948ed 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,213 +1,175 @@
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- 4.0.0
- com.apicatalog
- carbon-did
+ 4.0.0
+ com.apicatalog
+ carbon-did
- 0.6.0
+ 0.9.1
jar
- https://github.com/filip26/carbon-decentralized-identifiers
+ https://github.com/filip26/carbon-did-core
- scm:git:git://github.com/filip26/carbon-decentralized-identifiers.git
- scm:git:git://github.com/filip26/carbon-decentralized-identifiers.git
- https://github.com/filip26/carbon-decentralized-identifiers/tree/main
+ scm:git:git://github.com/filip26/carbon-did-core.git
+ scm:git:git://github.com/filip26/carbon-did-core.git
+ https://github.com/filip26/carbon-did-core/tree/main
- Carbon DIDs
+ Carbon DID Core
-
- Decentralized Identifiers (DIDs) API
+
+ Decentralized Identifiers (DIDs) Primitives & API
-
-
- Apache License, Version 2.0
- http://apache.org/licenses/LICENSE-2.0
-
-
+
+
+ Apache License, Version 2.0
+ http://apache.org/licenses/LICENSE-2.0
+
+
+
github
- https://github.com/filip26/carbon-decentralized-identifiers/issues
+ https://github.com/filip26/carbon-did-core/issues
-
-
- filip26
- Filip Kolarik
- filip26@gmail.com
-
- author
-
-
-
-
- 2022
-
-
- UTF-8
- UTF-8
- -Dfile.encoding=UTF-8
-
- 11
- 11
-
- 2.0.1
-
- 4.0.0
-
-
- 5.13.4
- 1.6.0
-
-
-
- jakarta.json
- jakarta.json-api
- ${jakarta.json.version}
- provided
-
-
-
- com.apicatalog
- copper-multibase
- ${copper.multibase.version}
- provided
-
-
-
-
- org.junit.jupiter
- junit-jupiter
- ${junit.version}
- test
-
-
-
- org.glassfish
- jakarta.json
- ${jakarta.json.version}
- test
-
-
-
- com.apicatalog
- titanium-json-ld
- ${titanium.version}
- test
-
-
-
-
-
-
- maven-jar-plugin
- 3.4.2
-
-
- org.apache.maven.plugins
- maven-source-plugin
- 3.3.1
-
-
- attach-sources
-
- jar-no-fork
-
-
-
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
- 3.11.3
-
- all,-missing
-
-
-
- attach-javadocs
-
- jar
-
-
-
-
-
- org.jacoco
- jacoco-maven-plugin
- 0.8.13
-
-
- default-prepare-agent
-
- prepare-agent
-
-
-
- report
- test
-
- report
-
-
-
-
-
- maven-surefire-plugin
- 3.5.3
-
-
- maven-failsafe-plugin
- 3.5.3
-
-
-
-
-
- maven-central
-
- false
-
-
-
-
- org.apache.maven.plugins
- maven-gpg-plugin
- 3.2.8
-
-
- sign-artifacts
- verify
-
- sign
-
-
-
-
-
- org.sonatype.plugins
- nexus-staging-maven-plugin
- 1.7.0
- true
-
- ossrh
- https://oss.sonatype.org/
- true
-
-
-
-
-
-
- ossrh
- https://oss.sonatype.org/content/repositories/snapshots
-
-
-
-
+
+
+ filip26
+ Filip Kolarik
+ filip26@gmail.com
+
+ author
+
+
+
+
+ 2022
+
+
+ UTF-8
+ UTF-8
+ -Dfile.encoding=UTF-8
+
+ 1.8
+ 1.8
+
+
+ 5.11.3
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit.version}
+ test
+
+
+
+
+
+
+ maven-jar-plugin
+ 3.4.2
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.11.1
+
+ all,-missing
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.12
+
+
+ default-prepare-agent
+
+ prepare-agent
+
+
+
+ report
+ test
+
+ report
+
+
+
+
+
+ maven-surefire-plugin
+ 3.5.2
+
+
+ maven-failsafe-plugin
+ 3.5.2
+
+
+
+
+
+ maven-central
+
+ false
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.7
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ org.sonatype.plugins
+ nexus-staging-maven-plugin
+ 1.7.0
+ true
+
+ ossrh
+ https://oss.sonatype.org/
+ true
+
+
+
+
+
+
diff --git a/src/main/java/com/apicatalog/did/Did.java b/src/main/java/com/apicatalog/did/Did.java
index 4e116a0..3b40b48 100644
--- a/src/main/java/com/apicatalog/did/Did.java
+++ b/src/main/java/com/apicatalog/did/Did.java
@@ -2,191 +2,414 @@
import java.io.Serializable;
import java.net.URI;
-import java.net.URISyntaxException;
import java.util.Objects;
import java.util.function.IntPredicate;
+/**
+ * Immutable value object representing a
+ * Decentralized Identifier (DID).
+ *
+ * This type models a bare DID (not a DID URL). It holds and preserves
+ * the {@code method} and the {@code method-specific-id} exactly as supplied,
+ * including any percent-encoding (no decoding or normalization is performed).
+ *
+ *
+ * Syntax
+ *
+ * {@code
+ * did = "did" ":" method-name ":" method-specific-id
+ * method-name = 1*method-char
+ * method-char = %x61-7A / DIGIT ; "a"โ"z" or "0"โ"9"
+ * method-specific-id = *( *idchar ":" ) 1*idchar
+ * idchar = ALPHA / DIGIT / "." / "-" / "_" / pct-encoded
+ * pct-encoded = "%" HEXDIG HEXDIG
+ * }
+ *
+ *
+ * Notes
+ *
+ *
+ * - Authority, path, query, and fragment are not allowed for a bare DID
+ * (e.g., {@code did:example:123#frag} and {@code did:example:123/path} are
+ * invalid).
+ * - The final segment of the {@code method-specific-id} MUST contain at least
+ * one {@code idchar}; earlier segments may be empty (i.e., {@code "::"} is
+ * allowed) per the ABNF above.
+ * - Percent-encoded octets ({@code %HH}) are validated for shape only and are
+ * not decoded.
+ *
+ */
public class Did implements Serializable {
- private static final long serialVersionUID = 8800667526627004412L;
+ private static final long serialVersionUID = -2933853082203788425L;
+
+ /** DID URI scheme literal: {@code "did"}. */
+ public static final String SCHEME = "did";
/*
* method-char = %x61-7A / DIGIT
*/
- static final IntPredicate METHOD_CHAR = ch -> (0x61 <= ch && ch <= 0x7A) || ('0' <= ch && ch <= '9');
+ static final IntPredicate METHOD_CHAR = ch -> (0x61 <= ch && ch <= 0x7A)
+ || ('0' <= ch && ch <= '9');
/*
* idchar = ALPHA / DIGIT / "." / "-" / "_" / pct-encoded
+ *
+ * This predicate intentionally covers only the single-codepoint, unescaped part
+ * (ALPHA / DIGIT / "." / "-" / "_"). pct-encoded is validated in the scanner.
*/
- static final IntPredicate ID_CHAR = ch -> Character.isLetter(ch) || Character.isDigit(ch) || ch == '.' || ch == '-' || ch == '_';
+ static final IntPredicate ID_CHAR = ch -> ch >= 'a' && ch <= 'z'
+ || 'A' <= ch && ch <= 'Z'
+ || '0' <= ch && ch <= '9'
+ || ch == '.'
+ || ch == '-'
+ || ch == '_';
- public static final String SCHEME = "did";
+ /*
+ * HEXDIG = 0-9 / A-F / a-f
+ */
+ static final IntPredicate HEXDIG = ch -> ('0' <= ch && ch <= '9') ||
+ ('A' <= ch && ch <= 'F') ||
+ ('a' <= ch && ch <= 'f');
- protected final String method;
- protected final String specificId;
+ /** Lowercase method name. */
+ protected final String methodName;
+ /** Raw (pct-encoded) method-specific-id, preserved as provided. */
+ protected final String methodSpecificId;
- protected Did(final String method, final String specificId) {
- this.method = method;
- this.specificId = specificId;
+ /**
+ * Creates a DID with already-validated components.
+ *
+ * @param methodName validated method name
+ * @param methodSpecificId validated, raw pct-encoded method-specific-id
+ */
+ protected Did(final String methodName, final String methodSpecificId) {
+ this.methodName = methodName;
+ this.methodSpecificId = methodSpecificId;
}
+ /**
+ * Tests whether the given {@link URI} is a syntactically valid bare
+ * DID.
+ *
+ * Validation enforces the ABNF in the class Javadoc and additionally requires:
+ *
+ *
+ * - scheme is {@code did} (case-sensitive)
+ * - no authority, user-info, host, path, query, or fragment
+ *
+ *
+ * @param uri candidate URI
+ * @return {@code true} if the URI is a valid DID, otherwise {@code false}
+ * @throws NullPointerException if {@code uri} is {@code null}
+ */
public static boolean isDid(final URI uri) {
- if (!Did.SCHEME.equalsIgnoreCase(uri.getScheme())
- || isBlank(uri.getSchemeSpecificPart())
+
+ Objects.requireNonNull(uri);
+
+ if (!Did.SCHEME.equals(uri.getScheme())
+ || isBlank(uri.getRawSchemeSpecificPart())
|| isNotBlank(uri.getAuthority())
|| isNotBlank(uri.getUserInfo())
|| isNotBlank(uri.getHost())
- || isNotBlank(uri.getPath())
- || isNotBlank(uri.getQuery())
- || isNotBlank(uri.getFragment())) {
+ || isNotBlank(uri.getRawPath())
+ || isNotBlank(uri.getRawQuery())
+ || uri.getRawFragment() != null) {
return false;
}
- final String[] parts = uri.getSchemeSpecificPart().split(":", 2);
+ final String[] parts = uri.getRawSchemeSpecificPart().split(":", 2);
return parts.length == 2
- && parts[0].length() > 0
- && parts[1].length() > 0
- && parts[0].codePoints().allMatch(METHOD_CHAR)
- // FIXME does not validate pct-encoded correctly
- && parts[1].codePoints().allMatch(ID_CHAR.or(ch -> ch == ':' || ch == '%'));
-
+ && isValidMethodName(parts[0])
+ && isValidMethodSpecificId(parts[1]);
}
+ /**
+ * Tests whether the given string is a syntactically valid bare DID.
+ *
+ * @param uri candidate string (e.g., {@code "did:example:123"})
+ * @return {@code true} if valid, otherwise {@code false}
+ * @throws NullPointerException if {@code uri} is {@code null}
+ */
public static boolean isDid(final String uri) {
- if (uri == null) {
- return false;
- }
+ Objects.requireNonNull(uri);
+ // "did:" method-name ":" method-specific-id
final String[] parts = uri.split(":", 3);
return parts.length == 3
- && Did.SCHEME.equalsIgnoreCase(parts[0])
- && parts[1].length() > 0
- && parts[2].length() > 0
- && parts[1].codePoints().allMatch(METHOD_CHAR)
- // FIXME does not validate pct-encoded correctly
- && parts[2].codePoints().allMatch(ID_CHAR.or(ch -> ch == ':' || ch == '%'));
+ && Did.SCHEME.equals(parts[0])
+ && isValidMethodName(parts[1])
+ && isValidMethodSpecificId(parts[2]);
}
/**
- * Creates a new DID instance from the given {@link URI}.
- *
- * @param uri The source URI to be transformed into DID
- * @return The new DID
- *
- * @throws NullPointerException If {@code uri} is {@code null}
- *
- * @throws IllegalArgumentException If the given {@code uri} is not valid DID
+ * @deprecated Use {@link Did#of(URI)}.
*/
+ @Deprecated
public static Did from(final URI uri) {
+ return of(uri);
+ }
- if (uri == null) {
- throw new IllegalArgumentException("The DID must not be null.");
- }
+ /**
+ * Parses and returns a {@code Did} from the given {@link URI}.
+ *
+ * The URI must be a bare DID: {@code did:method:method-specific-id}. The
+ * method-specific-id is treated as raw pct-encoded data and is not
+ * decoded.
+ *
+ *
+ * @param uri source URI
+ * @return a new {@code Did}
+ * @throws NullPointerException if {@code uri} is {@code null}
+ * @throws IllegalArgumentException if the URI is not a syntactically valid DID
+ */
+ public static Did of(final URI uri) {
+
+ Objects.requireNonNull(uri);
- if (!Did.SCHEME.equalsIgnoreCase(uri.getScheme())) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, must start with 'did:' prefix.");
+ if (!Did.SCHEME.equals(uri.getScheme())) {
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID; it must start with the 'did:' prefix.");
}
-
- if (uri.getFragment() != null) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, must be in form 'did:method:method-specific-id'.");
+
+ if (isBlank(uri.getRawSchemeSpecificPart())
+ || isNotBlank(uri.getAuthority())
+ || isNotBlank(uri.getUserInfo())
+ || isNotBlank(uri.getHost())
+ || isNotBlank(uri.getRawPath())
+ || isNotBlank(uri.getRawQuery())
+ || uri.getRawFragment() != null) {
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID; it must be in the form 'did:method:method-specific-id'.");
}
- final String[] parts = uri.getSchemeSpecificPart().split(":", 2);
+ final String[] parts = uri.getRawSchemeSpecificPart().split(":", 2);
if (parts.length != 2) {
throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, must be in form 'did:method:method-specific-id'.");
}
- return from(uri, parts[0], parts[1]);
+ validate(parts[0], parts[1]);
+
+ return of(parts[0], parts[1]);
}
/**
- * Creates a new DID instance from the given URI.
- *
- * @param uri The source URI to be transformed into DID
- * @return The new DID
- *
- * @throws NullPointerException If {@code uri} is {@code null}
- *
- * @throws IllegalArgumentException If the given {@code uri} is not valid DID
+ * @deprecated Use {@link Did#of(String)}.
*/
+ @Deprecated
public static Did from(final String uri) {
- if (uri == null || uri.length() == 0) {
- throw new IllegalArgumentException("The DID must not be null or blank string.");
+ return of(uri);
+ }
+
+ /**
+ * Parses and returns a {@code Did} from the given string.
+ *
+ * The string must be a bare DID: {@code did:method:method-specific-id}. The
+ * method-specific-id is treated as raw pct-encoded data and is not
+ * decoded.
+ *
+ *
+ * @param uri source string
+ * @return a new {@code Did}
+ * @throws IllegalArgumentException if {@code uri} is blank, or not a valid DID
+ * @throws NullPointerException if {@code uri} is {@code null}
+ */
+ public static Did of(final String uri) {
+
+ Objects.requireNonNull(uri);
+
+ if (uri.length() == 0) {
+ throw new IllegalArgumentException("DID string must not be blank.");
}
final String[] parts = uri.split(":", 3);
if (parts.length != 3) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, must be in form 'did:method:method-specific-id'.");
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID; it must be in the form 'did:method:method-specific-id'.");
}
- if (!Did.SCHEME.equalsIgnoreCase(parts[0])) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, must start with 'did:' prefix.");
+ if (!Did.SCHEME.equals(parts[0])) {
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID; it must start with the 'did:' prefix.");
}
- return from(uri, parts[1], parts[2]);
+ validate(parts[1], parts[2]);
+
+ return of(parts[1], parts[2]);
+ }
+
+ /**
+ * Creates a {@code Did} from already-separated components.
+ *
+ * @param methodName the method name (ASCII {@code [a-z0-9]+})
+ * @param methodSpecificId the raw method-specific-id (must follow the ABNF and
+ * use pct-encoding where required); no decoding is
+ * performed
+ * @return a new {@code Did}
+ * @throws NullPointerException if {@code methodName} or
+ * {@code methodSpecificId} is {@code null}
+ */
+ public static Did of(final String methodName, final String methodSpecificId) {
+
+ Objects.requireNonNull(methodName);
+ Objects.requireNonNull(methodSpecificId);
+
+ return new Did(methodName, methodSpecificId);
}
- protected static Did from(final Object uri, final String method, final String specificId) {
- // check method
- if (method == null
- || method.length() == 0
- || !method.codePoints().allMatch(METHOD_CHAR)) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, method [" + method + "] syntax is blank or invalid.");
+ public static void validate(final String methodName, final String methodSpecificId) {
+ // check method name
+ if (!isValidMethodName(methodName)) {
+ throw new IllegalArgumentException("Not a valid DID: method name [" + methodName + "] is blank or invalid.");
}
// check method specific id
- if (specificId == null
- || specificId.length() == 0
- // FIXME does not validate pct-encoded correctly
- || !specificId.codePoints().allMatch(ID_CHAR.or(ch -> ch == ':' || ch == '%'))) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, method specific id [" + specificId + "] is blank.");
+ if (!isValidMethodSpecificId(methodSpecificId)) {
+ throw new IllegalArgumentException("Not a valid DID: method-specific-id [" + methodSpecificId + "] is blank or invalid.");
}
+ }
- return new Did(method, specificId);
+ /**
+ * Validates the method name: {@code 1*(%x61-7A / DIGIT)} i.e.
+ * {@code [a-z0-9]+}.
+ *
+ * @param methodName candidate method name
+ * @return {@code true} if valid
+ */
+ public static boolean isValidMethodName(final String methodName) {
+ return (methodName.length() > 0
+ && methodName.codePoints().allMatch(METHOD_CHAR));
}
- public String getMethod() {
- return method;
+ /**
+ * Validates the method-specific-id using a single-pass scanner that enforces:
+ *
+ * - {@code method-specific-id = *( *idchar ":" ) 1*idchar}
+ * - {@code idchar = ALPHA / DIGIT / "." / "-" / "_" / pct-encoded}
+ * - {@code pct-encoded = "%" HEXDIG HEXDIG}
+ *
+ * Empty segments before {@code ':'} are allowed; the final segment must contain
+ * at least one {@code idchar}.
+ *
+ * @param methodSpecificId candidate method-specific-id (raw pct-encoded)
+ * @return {@code true} if valid
+ */
+ public static boolean isValidMethodSpecificId(final String methodSpecificId) {
+ if (methodSpecificId.isEmpty()) {
+ return false;
+ }
+
+ boolean lastSegHasIdChar = false;
+
+ for (int i = 0; i < methodSpecificId.length();) {
+ final char c = methodSpecificId.charAt(i);
+
+ if (c == ':') {
+ // Empty segments are allowed; reset for next segment.
+ lastSegHasIdChar = false;
+ i++;
+ continue;
+ }
+
+ if (c == '%') {
+ // pct-encoded = "%" HEXDIG HEXDIG
+ if ((i + 2 >= methodSpecificId.length())
+ || !HEXDIG.test(methodSpecificId.charAt(i + 1))
+ || !HEXDIG.test(methodSpecificId.charAt(i + 2))) {
+ return false;
+ }
+ i += 3;
+ lastSegHasIdChar = true;
+ continue;
+ }
+
+ final int cp = methodSpecificId.codePointAt(i);
+ if (!ID_CHAR.test(cp)) {
+ return false;
+ }
+
+ i += Character.charCount(cp);
+
+ lastSegHasIdChar = true;
+ }
+
+ // final segment must have at least one idchar
+ return lastSegHasIdChar;
}
- public String getMethodSpecificId() {
- return specificId;
+ /**
+ * Returns the DID method name (lowercase ASCII).
+ *
+ * @return method name
+ */
+ public String getMethod() {
+ return methodName;
}
- public URI toUri() {
- try {
- return new URI(SCHEME, method + ":" + specificId, null);
- } catch (URISyntaxException e) {
- throw new IllegalStateException(e);
- }
+ /**
+ * Returns the raw method-specific-id as provided (may contain pct-encoded
+ * octets).
+ *
+ * @return raw, pct-encoded method-specific-id
+ */
+ public String getMethodSpecificId() {
+ return methodSpecificId;
}
+ /**
+ * Indicates whether this instance represents a DID URL (always {@code false}
+ * here).
+ *
+ * This class models bare DIDs only. Use {@link #asDidUrl()} in
+ * implementations that support DID URLs.
+ *
+ *
+ * @return {@code false}
+ */
public boolean isDidUrl() {
return false;
}
+ /**
+ * Casts this instance to a DID URL.
+ *
+ * @return never returns; this class does not represent DID URLs
+ * @throws ClassCastException always
+ */
public DidUrl asDidUrl() {
throw new ClassCastException();
}
+ /**
+ * Converts this DID to a {@link URI} by rendering {@link #toString()}.
+ *
+ * @return a {@code URI} equal to {@code URI.create(toString())}
+ */
+ public URI toUri() {
+ return URI.create(toString());
+ }
+
+ /**
+ * Renders the bare DID in its wire form:
+ * {@code did::}.
+ *
+ * The {@code method-specific-id} is returned exactly as stored (pct-encoded as
+ * needed).
+ *
+ */
@Override
public String toString() {
return new StringBuilder()
- .append(SCHEME)
- .append(':')
- .append(method)
- .append(':')
- .append(specificId).toString();
+ .append(SCHEME).append(':')
+ .append(methodName).append(':')
+ .append(methodSpecificId)
+ .toString();
}
@Override
public int hashCode() {
- return Objects.hash(method, specificId);
+ return Objects.hash(methodName, methodSpecificId);
}
@Override
@@ -201,14 +424,22 @@ public boolean equals(final Object obj) {
return false;
}
Did other = (Did) obj;
- return Objects.equals(method, other.method) && Objects.equals(specificId, other.specificId);
+ return Objects.equals(methodName, other.methodName) && Objects.equals(methodSpecificId, other.methodSpecificId);
}
+ /**
+ * @return {@code true} if the value is non-null and not blank after
+ * {@code trim()}
+ */
static final boolean isNotBlank(String value) {
return value != null && !value.trim().isEmpty();
}
+ /**
+ * @return {@code true} if the value is {@code null} or blank after
+ * {@code trim()}
+ */
static final boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
diff --git a/src/main/java/com/apicatalog/did/DidResolver.java b/src/main/java/com/apicatalog/did/DidResolver.java
deleted file mode 100644
index aa6a4b3..0000000
--- a/src/main/java/com/apicatalog/did/DidResolver.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.apicatalog.did;
-
-import com.apicatalog.did.document.DidDocument;
-
-/**
- * Performs {@link Did} resolution by expanding {@link Did} into {@link DidDocument}.
- *
- * @see DID resolvers
- */
-public interface DidResolver {
-
- /**
- * Resolves the given {@link Did} into {@link DidDocument}
- *
- * @param did To resolve
- * @return The new {@link DidDocument}
- */
- DidDocument resolve(Did did);
-}
diff --git a/src/main/java/com/apicatalog/did/DidUrl.java b/src/main/java/com/apicatalog/did/DidUrl.java
index 2b73269..ba4730e 100644
--- a/src/main/java/com/apicatalog/did/DidUrl.java
+++ b/src/main/java/com/apicatalog/did/DidUrl.java
@@ -1,164 +1,332 @@
package com.apicatalog.did;
import java.net.URI;
-import java.net.URISyntaxException;
import java.util.Objects;
+/**
+ * Immutable value object for a DID
+ * URL.
+ *
+ * Extends {@link Did} with optional {@code path}, {@code query}, and
+ * {@code fragment} components. Values are preserved exactly as supplied,
+ * including any percent-encoding (no decoding).
+ *
+ *
+ * Form
+ *
+ * {@code
+ * did::[][?][#]
+ * }
+ */
public class DidUrl extends Did {
- private static final long serialVersionUID = 5752880077497569763L;
+ private static final long serialVersionUID = -4371252270461483928L;
+ /** Path (percent-encoded if applicable). {@code null} if absent. */
protected final String path;
+ /**
+ * Query (percent-encoded, without leading {@code '?'}). {@code null} if absent.
+ */
protected final String query;
+ /**
+ * Fragment (percent-encoded, without leading {@code '#'}). {@code null} if
+ * absent.
+ */
protected final String fragment;
- protected DidUrl(Did did, String path, String query, String fragment) {
- super(did.method, did.specificId);
+ /**
+ * Constructs a DID URL from validated DID parts.
+ *
+ * @param methodName lowercase method name ({@code [a-z0-9]+})
+ * @param methodSpecificId method-specific-id (percent-encoded as needed)
+ * @param path optional path (may be {@code null})
+ * @param query optional query (may be {@code null})
+ * @param fragment optional fragment (may be {@code null})
+ */
+ protected DidUrl(String methodName, String methodSpecificId, String path, String query, String fragment) {
+ super(methodName, methodSpecificId);
this.path = path;
this.query = query;
this.fragment = fragment;
}
- public static DidUrl from(Did did, String path, String query, String fragment) {
- return new DidUrl(did, path, query, fragment);
+ /**
+ * Creates a DID URL from separated parts. Values are preserved; only minimal
+ * normalization of leading {@code /}, {@code ?}, and {@code #} markers is
+ * applied.
+ *
+ * @param methodName lowercase method name
+ * @param methodSpecificId method-specific-id (percent-encoded as needed)
+ * @param path optional path (leading {@code '/'} added if missing;
+ * {@code null} unchanged)
+ * @param query optional query (leading {@code '?'} removed if
+ * present; {@code null} unchanged)
+ * @param fragment optional fragment (leading {@code '#'} removed if
+ * present; {@code null} unchanged)
+ * @return a new {@code DidUrl}
+ */
+ public static DidUrl of(
+ final String methodName,
+ final String methodSpecificId,
+ final String path,
+ final String query,
+ final String fragment) {
+ return new DidUrl(
+ methodName,
+ methodSpecificId,
+ normalizePath(path),
+ normalizeQuery(query),
+ normalizeFragment(fragment));
}
- public static DidUrl from(final URI uri) {
+ /**
+ * Creates a DID URL from a base {@link Did} and optional path, query, and
+ * fragment. Percent-encoding is preserved.
+ *
+ * @param did base DID (must not be {@code null})
+ * @param path optional path (see {@link #normalizePath(String)})
+ * @param query optional query (see {@link #normalizeQuery(String)})
+ * @param fragment optional fragment (see {@link #normalizeFragment(String)})
+ * @return a new {@code DidUrl}
+ * @throws NullPointerException if {@code did} is {@code null}
+ */
+ public static DidUrl of(final Did did, final String path, final String query, final String fragment) {
+ Objects.requireNonNull(did);
+ return new DidUrl(
+ did.methodName,
+ did.methodSpecificId,
+ normalizePath(path),
+ normalizeQuery(query),
+ normalizeFragment(fragment));
+ }
+
+ /**
+ * Parses a DID URL from a {@link URI}. Percent-encoding is preserved.
+ *
+ * @param uri source URI
+ * @return a new {@code DidUrl}
+ * @throws NullPointerException if {@code uri} is {@code null}
+ * @throws IllegalArgumentException if the URI is not a valid DID URL
+ */
+ public static DidUrl of(final URI uri) {
+
+ Objects.requireNonNull(uri);
- if (uri == null) {
- throw new IllegalArgumentException("The DID URL must not be null.");
+ if (!SCHEME.equalsIgnoreCase(uri.getScheme())) {
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID URL; must start with 'did:'.");
}
- if (!Did.SCHEME.equalsIgnoreCase(uri.getScheme())) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID URL, must start with 'did:' prefix.");
+ if (isNotBlank(uri.getAuthority())
+ || isNotBlank(uri.getUserInfo())
+ || isNotBlank(uri.getHost())) {
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID URL; authority is not allowed.");
+ }
+
+ final String ssp = uri.getRawSchemeSpecificPart();
+
+ if (isBlank(ssp)) {
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID URL; expected 'did:method:method-specific-id'.");
}
- final String[] didParts = uri.getSchemeSpecificPart().split(":", 2);
+ final String[] parts = ssp.split(":", 2);
- if (didParts.length != 2) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, must be in form 'did:method:method-specific-id'.");
+ if (parts.length != 2) {
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID URL; expected 'did:method:method-specific-id'.");
}
- return from(uri, didParts[0], didParts[1], uri.getFragment());
+ return of(
+ parts[0],
+ parts[1],
+ uri.getRawFragment() // preserve raw pct-encoding
+ );
}
- public static DidUrl from(final String uri) {
+ /**
+ * Parses a DID URL from a string. Percent-encoding is preserved.
+ *
+ * @param uri source string
+ * @return a new {@code DidUrl}
+ * @throws NullPointerException if {@code uri} is {@code null}
+ * @throws IllegalArgumentException if blank or not a DID URL
+ */
+ public static DidUrl of(final String uri) {
+
+ Objects.requireNonNull(uri);
- if (uri == null || uri.length() == 0) {
- throw new IllegalArgumentException("The DID must not be null or blank string.");
+ if (uri.isEmpty()) {
+ throw new IllegalArgumentException("DID URL string must not be blank.");
}
final String[] parts = uri.split(":", 3);
if (parts.length != 3) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, must be in form 'did:method:method-specific-id'.");
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID URL.");
}
- if (!Did.SCHEME.equalsIgnoreCase(parts[0])) {
- throw new IllegalArgumentException("The URI [" + uri + "] is not valid DID, must start with 'did:' prefix.");
+ if (!Did.SCHEME.equals(parts[0])) {
+ throw new IllegalArgumentException("The URI [" + uri + "] is not a valid DID URL; it must start with the 'did:' prefix.");
}
- String rest = parts[2];
+ String ssp = parts[2];
String fragment = null;
- int fragmentIndex = rest.indexOf('#');
+ int fragmentIndex = parts[2].indexOf('#');
+
if (fragmentIndex != -1) {
- fragment = rest.substring(fragmentIndex + 1);
- rest = rest.substring(0, fragmentIndex);
+ ssp = parts[2].substring(0, fragmentIndex);
+ fragment = parts[2].substring(fragmentIndex + 1);
}
- return from(uri, parts[1], rest, fragment);
+ return of(parts[1], ssp, fragment);
}
- protected static DidUrl from(Object uri, final String method, final String rest, final String fragment) {
- String specificId = rest;
-
- String path = null;
- String query = null;
-
- int urlPartIndex = specificId.indexOf('?');
- if (urlPartIndex != -1) {
- query = specificId.substring(urlPartIndex + 1);
- specificId = specificId.substring(0, urlPartIndex);
- }
+ /**
+ * Creates a DID URL from a base {@link Did} with only a fragment component.
+ * Percent-encoding is preserved.
+ *
+ * @param did base DID (must not be {@code null})
+ * @param fragment fragment (may be empty; leading {@code '#'} is removed if
+ * present)
+ * @return a new {@code DidUrl}
+ * @throws NullPointerException if {@code fragment} is {@code null}
+ */
+ public static DidUrl fragment(final Did did, final String fragment) {
+ Objects.requireNonNull(fragment);
+ return of(did, null, null, fragment);
+ }
- urlPartIndex = specificId.indexOf('/');
- if (urlPartIndex != -1) {
- path = specificId.substring(urlPartIndex);
- specificId = specificId.substring(0, urlPartIndex);
- }
+ /** @deprecated use {@link DidUrl#of(String)} */
+ @Deprecated
+ public static DidUrl from(final String uri) {
+ return of(uri);
+ }
- Did did = from(uri, method, specificId);
+ /** @deprecated use {@link DidUrl#of(Did, String, String, String)} */
+ @Deprecated
+ public static DidUrl from(Did did, String path, String query, String fragment) {
+ return of(did, path, query, fragment);
+ }
- return new DidUrl(did, path, query, fragment);
+ /** @deprecated use {@link DidUrl#of(URI)} */
+ @Deprecated
+ public static DidUrl from(final URI uri) {
+ return of(uri);
}
+ /**
+ * Returns whether the given {@link URI} is a syntactically valid DID URL.
+ * Validation checks scheme, absence of authority/host, and that the method and
+ * method-specific-id are valid per {@link Did}.
+ *
+ * @param uri candidate URI
+ * @return {@code true} if valid; {@code false} otherwise
+ */
public static boolean isDidUrl(final URI uri) {
- if (!Did.SCHEME.equalsIgnoreCase(uri.getScheme())
- || isBlank(uri.getSchemeSpecificPart())
- || isNotBlank(uri.getAuthority())
- || isNotBlank(uri.getUserInfo())
+ if (uri == null
+ || !SCHEME.equals(uri.getScheme())
+ || isBlank(uri.getRawSchemeSpecificPart())
+ || isNotBlank(uri.getRawAuthority())
+ || isNotBlank(uri.getRawUserInfo())
|| isNotBlank(uri.getHost())) {
return false;
}
- final String[] parts = uri.getSchemeSpecificPart().split(":", 2);
+ final String[] parts = uri.getRawSchemeSpecificPart().split(":", 2);
- return parts.length == 2
- && parts[0].length() > 0
- && parts[1].length() > 0
- && parts[0].codePoints().allMatch(METHOD_CHAR);
- }
+ if (parts.length != 2) {
+ return false;
+ }
- public static boolean isDidUrl(final String uri) {
- if (uri == null) {
+ if (!isValidMethodName(parts[0])) {
return false;
}
- final String[] parts = uri.split(":", 3);
+ int msiEndIndex = methodSpecificIdEndIndex(parts[1]);
- return parts.length == 3
- && Did.SCHEME.equalsIgnoreCase(parts[0])
- && parts[1].length() > 0
- && parts[2].length() > 0
- && parts[1].codePoints().allMatch(METHOD_CHAR);
+ if (msiEndIndex == 0) {
+ return false;
+ }
+
+ return isValidMethodSpecificId(parts[1].substring(0, msiEndIndex));
}
- @Override
- public URI toUri() {
+ /**
+ * Returns whether the given string is a syntactically valid DID URL.
+ *
+ * @param uri candidate string
+ * @return {@code true} if valid; {@code false} otherwise
+ */
+ public static boolean isDidUrl(final String uri) {
+ if (uri == null || uri.isEmpty()) {
+ return false;
+ }
try {
- return new URI(SCHEME,
- appendPathQuery(new StringBuilder()
- .append(method)
- .append(':')
- .append(specificId)).toString(),
- fragment);
- } catch (URISyntaxException e) {
- throw new IllegalStateException(e);
+ return isDidUrl(URI.create(uri));
+ } catch (IllegalArgumentException e) {
+ return false;
}
}
+ /**
+ * Converts this DID URL to a {@link URI} by rendering {@link #toString()}.
+ *
+ * @return a {@code URI} equal to {@code URI.create(toString())}
+ */
+ @Override
+ public URI toUri() {
+ // Preserve exact raw form produced by toString()
+ return URI.create(toString());
+ }
+
+ /**
+ * Indicates that this instance represents a DID URL.
+ *
+ * @return always {@code true}
+ */
@Override
public boolean isDidUrl() {
return true;
}
+ /**
+ * Returns this instance as a {@link DidUrl}.
+ *
+ * @return {@code this}
+ */
@Override
public DidUrl asDidUrl() {
return this;
}
+ /**
+ * Returns the bare {@link Did} portion of this DID URL (method and
+ * method-specific-id only).
+ *
+ * @return a new {@code Did}
+ */
+ public Did toDid() {
+ return new Did(super.methodName, super.methodSpecificId);
+ }
+
+ /**
+ * Renders
+ * {@code did::[][?][#]} with
+ * percent-encoding preserved.
+ *
+ * @return the canonical string form
+ */
@Override
public String toString() {
- final StringBuilder builder = new StringBuilder(super.toString());
+ final StringBuilder builder = new StringBuilder()
+ .append(SCHEME).append(':')
+ .append(methodName).append(':')
+ .append(methodSpecificId);
- appendPathQuery(builder);
+ appendPathAndQuery(builder);
if (fragment != null) {
- if (fragment.length() == 0 || fragment.charAt(0) != '#') {
- builder.append('#');
- }
- if (fragment.length() > 0) {
+ builder.append('#');
+ if (!fragment.isEmpty()) {
builder.append(fragment);
}
}
@@ -166,21 +334,26 @@ public String toString() {
return builder.toString();
}
- protected StringBuilder appendPathQuery(final StringBuilder builder) {
+ /**
+ * Appends the path and query to the provided builder. Path is ensured to start
+ * with {@code '/'} when present; query is appended after {@code '?'} even if
+ * empty.
+ *
+ * @param builder target builder
+ * @return the same {@code builder}
+ */
+ protected StringBuilder appendPathAndQuery(final StringBuilder builder) {
if (path != null) {
- if (path.length() == 0 || path.charAt(0) != '/') {
+ if (path.isEmpty() || path.charAt(0) != '/') {
builder.append('/');
- }
- if (path.length() > 0) {
+ } else if (!path.isEmpty()) {
builder.append(path);
}
}
if (query != null) {
- if (query.length() == 0 || query.charAt(0) != '?') {
- builder.append('?');
- }
- if (query.length() > 0) {
+ builder.append('?');
+ if (!query.isEmpty()) {
builder.append(query);
}
}
@@ -211,15 +384,158 @@ public boolean equals(Object obj) {
&& Objects.equals(query, other.query);
}
+ /**
+ * Returns the fragment (percent-encoded if applicable), without a leading
+ * {@code '#'}.
+ *
+ * @return fragment or {@code null} if absent
+ */
public String getFragment() {
return fragment;
}
+ /**
+ * Returns the path (percent-encoded if applicable). When present it starts with
+ * {@code '/'}.
+ *
+ * @return path or {@code null} if absent
+ */
public String getPath() {
return path;
}
+ /**
+ * Returns the query (percent-encoded if applicable), without a leading
+ * {@code '?'}.
+ *
+ * @return query or {@code null} if absent
+ */
public String getQuery() {
return query;
}
+
+ /**
+ * Normalizes a path to start with {@code '/'}. An empty string remains empty
+ * (renders as {@code "/"}).
+ *
+ * @param p input path (may be {@code null})
+ * @return normalized path, empty string, or {@code null}
+ */
+ static final String normalizePath(final String p) {
+ if (p == null) {
+ return null;
+ }
+ if (p.isEmpty()) {
+ return ""; // will render as "/"
+ }
+ return p.charAt(0) == '/' ? p : "/" + p;
+ }
+
+ /**
+ * Removes a leading {@code '?'} from a query if present.
+ *
+ * @param q input query (may be {@code null})
+ * @return query without leading {@code '?'}, or {@code null}
+ */
+ static final String normalizeQuery(final String q) {
+ if (q == null) {
+ return null;
+ }
+ return (q.startsWith("?")) ? q.substring(1) : q;
+ }
+
+ /**
+ * Removes a leading {@code '#'} from a fragment if present.
+ *
+ * @param f input fragment (may be {@code null})
+ * @return fragment without leading {@code '#'}, or {@code null}
+ */
+ static final String normalizeFragment(final String f) {
+ if (f == null) {
+ return null;
+ }
+ return (f.startsWith("#")) ? f.substring(1) : f;
+ }
+
+ /**
+ * Returns the end index of the method-specific-id within the scheme-specific
+ * part. The index is the position before the first {@code '/'} or {@code '?'},
+ * or the end if neither is present.
+ *
+ * @param ssp scheme-specific part (must not be {@code null})
+ * @return end index of the method-specific-id
+ */
+ static final int methodSpecificIdEndIndex(String ssp) {
+ int msiEndIndex = ssp.length();
+ final int slashIndex = ssp.indexOf('/');
+ final int qmarkIndex = ssp.indexOf('?');
+
+ if (slashIndex != -1 && slashIndex < msiEndIndex) {
+ msiEndIndex = slashIndex;
+ }
+ if (qmarkIndex != -1 && qmarkIndex < msiEndIndex) {
+ msiEndIndex = qmarkIndex;
+ }
+
+ return msiEndIndex;
+ }
+
+ /**
+ * Internal parser from method name and scheme-specific part, plus optional
+ * fragment. Extracts method-specific-id, path, and query. Percent-encoding is
+ * preserved.
+ *
+ * @param methodName lowercase method name
+ * @param ssp scheme-specific part beginning with method-specific-id
+ * @param fragment optional fragment (may be {@code null})
+ * @return a new {@code DidUrl}
+ * @throws IllegalArgumentException if method-specific-id is empty or the tail
+ * is malformed
+ */
+ static DidUrl of(final String methodName,
+ final String ssp,
+ final String fragment) {
+
+ // Determine end of method-specific-id (stop before first '/' or '?' or end)
+ int msiEndIndex = methodSpecificIdEndIndex(ssp);
+
+ if (msiEndIndex == 0) {
+ throw new IllegalArgumentException("The URI is not a valid DID URL; method-specific-id is empty.");
+ }
+
+ final String methodSpecificId = ssp.substring(0, msiEndIndex);
+
+ validate(methodName, methodSpecificId);
+
+ final String rest = ssp.substring(msiEndIndex); // starts with '/' or '?' or is empty
+
+ // Extract path and query from tail (both raw, without leading markers)
+ String path = null;
+ String query = null;
+
+ if (!rest.isEmpty()) {
+ if (rest.charAt(0) == '/') {
+ // path-abempty: consume path, maybe followed by ?query
+ final int queryIndex = rest.indexOf('?');
+ if (queryIndex == -1) {
+ path = rest; // includes leading '/'
+ } else {
+ path = rest.substring(0, queryIndex); // includes leading '/'
+ query = rest.substring(queryIndex + 1); // without '?'
+ }
+ } else if (rest.charAt(0) == '?') {
+ query = rest.substring(1); // empty query allowed
+ } else {
+ // Should not happen (we only cut at '/' or '?')
+ throw new IllegalArgumentException("The URI is not a valid DID URL; malformed path/query [" + rest + "].");
+ }
+ }
+
+ return new DidUrl(
+ methodName,
+ methodSpecificId,
+ path,
+ query,
+ fragment);
+ }
}
diff --git a/src/main/java/com/apicatalog/did/datatype/MultibaseEncoded.java b/src/main/java/com/apicatalog/did/datatype/MultibaseEncoded.java
new file mode 100644
index 0000000..1612ebf
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/datatype/MultibaseEncoded.java
@@ -0,0 +1,27 @@
+package com.apicatalog.did.datatype;
+
+/**
+ * A value encoded using the Multibase
+ * format.
+ *
+ * Provides access to the encoding base and the decoded (binary) value.
+ *
+ */
+public interface MultibaseEncoded {
+
+ /**
+ * Returns the name of the encoding base (e.g. {@code base58btc},
+ * {@code base64url}).
+ *
+ * @return multibase name
+ */
+ String baseName();
+
+ /**
+ * Returns the decoded binary value.
+ *
+ * @return raw byte array
+ */
+ byte[] debased();
+}
diff --git a/src/main/java/com/apicatalog/did/datatype/MulticodecEncoded.java b/src/main/java/com/apicatalog/did/datatype/MulticodecEncoded.java
new file mode 100644
index 0000000..391ada2
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/datatype/MulticodecEncoded.java
@@ -0,0 +1,25 @@
+package com.apicatalog.did.datatype;
+
+/**
+ * A value encoded using the
+ * Multicodec format.
+ *
+ * Provides access to the codec code and the decoded (binary) value.
+ *
+ */
+public interface MulticodecEncoded {
+
+ /**
+ * Returns the numeric multicodec code identifying the content type.
+ *
+ * @return codec code
+ */
+ long codecCode();
+
+ /**
+ * Returns the decoded binary value.
+ *
+ * @return raw byte array
+ */
+ byte[] decoded();
+}
diff --git a/src/main/java/com/apicatalog/did/datatype/package-info.java b/src/main/java/com/apicatalog/did/datatype/package-info.java
new file mode 100644
index 0000000..12de8ab
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/datatype/package-info.java
@@ -0,0 +1,12 @@
+/**
+ * Datatypes used in DID Documents and verification methods.
+ *
+ * Includes multibase- and multicodec-encoded values used in verification
+ * methods and service definitions.
+ *
+ *
+ * @see Multibase
+ * @see Multicodec
+ */
+package com.apicatalog.did.datatype;
diff --git a/src/main/java/com/apicatalog/did/document/DidDocument.java b/src/main/java/com/apicatalog/did/document/DidDocument.java
index 7e5a5d4..c16853a 100644
--- a/src/main/java/com/apicatalog/did/document/DidDocument.java
+++ b/src/main/java/com/apicatalog/did/document/DidDocument.java
@@ -1,49 +1,121 @@
package com.apicatalog.did.document;
-import java.util.Set;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
import com.apicatalog.did.Did;
/**
- * DID Document
- *
- * @see DID document properties
+ * A DID
+ * Document.
+ *
+ * Models the top-level properties of a DID Document as defined in the W3C DID
+ * Core specification. All accessors return empty sets by default.
+ *
*/
+public interface DidDocument {
-public class DidDocument {
+ /**
+ * The {@code id} property: the primary identifier of the DID subject.
+ *
+ * @return the DID identifier, or {@code null} if absent
+ */
+ Did id();
- protected final Did id;
+ /**
+ * The {@code controller} property: DIDs that control this DID.
+ *
+ * @return controller set, possibly empty
+ */
+ default Collection controller() {
+ return Collections.emptySet();
+ }
+
+ /**
+ * The {@code verificationMethod} property: verification methods defined in this
+ * document.
+ *
+ * @return verification methods, possibly empty
+ */
+ default Collection verification() {
+ return Collections.emptySet();
+ }
- protected final Set controller;
+ /**
+ * The {@code alsoKnownAs} property: additional URIs that refer to the same
+ * subject.
+ *
+ * @return URIs, possibly empty
+ */
+ default Collection alsoKnownAs() {
+ return Collections.emptySet();
+ }
+
+ /**
+ * The {@code authentication} relationship: methods that can authenticate as the
+ * DID subject.
+ *
+ * @return authentication methods, possibly empty
+ */
+ default Collection authentication() {
+ return Collections.emptySet();
+ }
- protected final Set verificationMethod;
+ /**
+ * The {@code assertionMethod} relationship: methods for asserting claims.
+ *
+ * @return assertion methods, possibly empty
+ */
+ default Collection assertion() {
+ return Collections.emptySet();
+ }
-// protected Set alsoKnownAs;
-// protected Set assertionMethod;
-// protected Set authentication;
-// protected Set capabilityInvocation;
-// protected Set capabilityDelegation;
-// protected Set keyAgreement;
+ /**
+ * The {@code keyAgreement} relationship: methods for key agreement.
+ *
+ * @return key agreement methods, possibly empty
+ */
+ default Collection keyAgreement() {
+ return Collections.emptySet();
+ }
- public DidDocument(
- Did id,
- Set controller,
- Set verificationMethod
- ) {
- this.id = id;
- this.controller = controller;
- this.verificationMethod = verificationMethod;
+ /**
+ * The {@code capabilityInvocation} relationship: methods for invoking
+ * capabilities.
+ *
+ * @return invocation methods, possibly empty
+ */
+ default Collection capabilityInvocation() {
+ return Collections.emptySet();
}
-
- public Did id() {
- return id;
+
+ /**
+ * The {@code capabilityDelegation} relationship: methods for delegating
+ * capabilities.
+ *
+ * @return delegation methods, possibly empty
+ */
+ default Collection capabilityDelegation() {
+ return Collections.emptySet();
}
-
- public Set controller() {
- return controller;
+
+ /**
+ * The {@code service} property: service endpoints in this document.
+ *
+ * @return service definitions, possibly empty
+ */
+ default Collection service() {
+ return Collections.emptySet();
}
-
- public Set verificationMethod() {
- return verificationMethod;
+
+ /**
+ * Checks whether this document has the required {@code id} property.
+ *
+ * @return {@code true} if {@link #id()} is not {@code null}
+ */
+ default boolean hasRequiredProperties() {
+ return id() != null;
}
+
}
diff --git a/src/main/java/com/apicatalog/did/document/DidService.java b/src/main/java/com/apicatalog/did/document/DidService.java
new file mode 100644
index 0000000..8c13442
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/document/DidService.java
@@ -0,0 +1,85 @@
+package com.apicatalog.did.document;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * A DID Document
+ * service.
+ *
+ * Represents a service entry in a DID Document, consisting of an {@code id},
+ * one or more {@code type} values, and one or more {@code serviceEndpoint}
+ * values.
+ *
+ */
+public interface DidService {
+
+ /**
+ * The {@code id} of this service entry.
+ *
+ * @return the unique service identifier
+ */
+ URI id();
+
+ /**
+ * The {@code type} values of this service.
+ *
+ * @return one or more type strings
+ */
+ Collection type();
+
+ /**
+ * The {@code serviceEndpoint} values of this service.
+ *
+ * @return one or more endpoints
+ */
+ Collection endpoint();
+
+ /**
+ * Checks whether this service has the required properties: {@code id},
+ * {@code type}, and at least one {@code serviceEndpoint}.
+ *
+ * @return {@code true} if valid
+ */
+ default boolean hasRequiredProperties() {
+ return id() != null && type() != null && endpoint() != null && !endpoint().isEmpty();
+ }
+
+ /**
+ * Creates a {@code DidService} with a single type and a single endpoint.
+ *
+ * @param id service id
+ * @param type service type
+ * @param endpoint service endpoint
+ * @return a new {@code DidService}
+ */
+ static DidService of(URI id, String type, DidServiceEndpoint endpoint) {
+ return new ImmutableService(id, Collections.singleton(type), Collections.singleton(endpoint));
+ }
+
+ /**
+ * Creates a {@code DidService} with a single type and multiple endpoints.
+ *
+ * @param id service id
+ * @param type service type
+ * @param endpoint service endpoints
+ * @return a new {@code DidService}
+ */
+ static DidService of(URI id, String type, Collection endpoint) {
+ return new ImmutableService(id, Collections.singleton(type), endpoint);
+ }
+
+ /**
+ * Creates a {@code DidService} with multiple types and endpoints.
+ *
+ * @param id service id
+ * @param type service types
+ * @param endpoint service endpoints
+ * @return a new {@code DidService}
+ */
+ static DidService of(URI id, Collection type, Collection endpoint) {
+ return new ImmutableService(id, type, endpoint);
+ }
+
+}
diff --git a/src/main/java/com/apicatalog/did/document/DidServiceEndpoint.java b/src/main/java/com/apicatalog/did/document/DidServiceEndpoint.java
new file mode 100644
index 0000000..75c7ea3
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/document/DidServiceEndpoint.java
@@ -0,0 +1,39 @@
+package com.apicatalog.did.document;
+
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * A serviceEndpoint
+ * entry within a DID Document service.
+ */
+public interface DidServiceEndpoint {
+
+ /**
+ * The {@code id} of this service endpoint.
+ *
+ * @return endpoint identifier
+ */
+ URI id();
+
+ /**
+ * Checks whether this endpoint has the required {@code id} property.
+ *
+ * @return {@code true} if valid
+ */
+ default boolean hasRequiredProperties() {
+ return id() != null;
+ }
+
+ /**
+ * Creates a {@code DidServiceEndpoint} with the given {@code id}.
+ *
+ * @param id endpoint identifier (must not be {@code null})
+ * @return a new {@code DidServiceEndpoint}
+ * @throws NullPointerException if {@code id} is {@code null}
+ */
+ static DidServiceEndpoint of(URI id) {
+ Objects.requireNonNull(id);
+ return new ImmutableServiceEndpoint(id);
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/document/DidVerificationMethod.java b/src/main/java/com/apicatalog/did/document/DidVerificationMethod.java
index 96013f7..8400ea5 100644
--- a/src/main/java/com/apicatalog/did/document/DidVerificationMethod.java
+++ b/src/main/java/com/apicatalog/did/document/DidVerificationMethod.java
@@ -1,38 +1,120 @@
package com.apicatalog.did.document;
+import java.util.Map;
+import java.util.Objects;
+
import com.apicatalog.did.Did;
import com.apicatalog.did.DidUrl;
+import com.apicatalog.did.datatype.MultibaseEncoded;
+
+/**
+ * A verificationMethod
+ * entry within a DID Document.
+ */
+public interface DidVerificationMethod {
+
+ /**
+ * The unique identifier of this verification method.
+ *
+ * @return DID URL identifier
+ */
+ DidUrl id();
+
+ /**
+ * The type of this verification method.
+ *
+ * @return verification method type
+ */
+ String type();
+
+ /**
+ * The controlling DID of this verification method.
+ *
+ * @return controller DID
+ */
+ Did controller();
+
+ /**
+ * A {@code publicKeyMultibase} value if present.
+ *
+ * @return multibase-encoded public key, or {@code null}
+ */
+ MultibaseEncoded publicKeyMultibase();
+
+ /**
+ * A {@code publicKeyJwk} value if present.
+ *
+ * @return JWK representation of the key, or {@code null}
+ */
+ Map publicKeyJwk();
-public class DidVerificationMethod {
-
- final DidUrl id;
- final Did controller;
- final byte[] publicKey;
-
- public DidVerificationMethod(
- DidUrl id,
- Did controller,
- byte[] publicKey
- ) {
- this.id = id;
- this.controller = controller;
- this.publicKey = publicKey;
+ /**
+ * Checks whether this verification method has the required properties:
+ * {@code id}, {@code type}, and {@code controller}.
+ *
+ * @return {@code true} if valid
+ */
+ default boolean hasRequiredProperties() {
+ return id() != null && type() != null && controller() != null;
}
-
- public DidUrl id() {
- return id;
+
+ /**
+ * Compares two verification methods for equality of {@code id}, {@code type},
+ * {@code controller}, {@code publicKeyMultibase}, and {@code publicKeyJwk}.
+ *
+ * @param method1 first method (may be {@code null})
+ * @param method2 second method (may be {@code null})
+ * @return {@code true} if both are equal
+ */
+ static boolean equals(final DidVerificationMethod method1, final DidVerificationMethod method2) {
+ if (method1 == null || method2 == null) {
+ return method1 == method2;
+ }
+ return Objects.equals(method1.id(), method2.id())
+ && Objects.equals(method1.type(), method2.type())
+ && Objects.equals(method1.controller(), method2.controller())
+ && Objects.equals(method1.publicKeyMultibase(), method2.publicKeyMultibase())
+ && Objects.equals(method1.publicKeyJwk(), method2.publicKeyJwk());
}
-
- public Did controller() {
- return controller;
+
+ /**
+ * Creates a JWK-based verification method.
+ *
+ * @param id method identifier (DID URL, not {@code null})
+ * @param type verification method type
+ * @param controller controlling DID
+ * @param publicKeyJwk JWK-formatted key
+ * @return a new {@code DidVerificationMethod}
+ */
+ static DidVerificationMethod jwk(
+ final DidUrl id,
+ final String type,
+ final Did controller,
+ final Map publicKeyJwk) {
+ Objects.requireNonNull(id);
+ Objects.requireNonNull(type);
+ Objects.requireNonNull(controller);
+ return new ImmutableJwkMethod(id, type, controller, publicKeyJwk);
}
-
+
/**
- * A public key encoded with multicodec
- *
- * @return a multicodec encoded public key
+ * Creates a multibase-based verification method.
+ *
+ * @param id method identifier (DID URL, not {@code null})
+ * @param type verification method type
+ * @param controller controlling DID
+ * @param publicKeyMultibase multibase-encoded key
+ * @return a new {@code DidVerificationMethod}
*/
- public byte[] publicKey() {
- return publicKey;
+ static DidVerificationMethod multibase(
+ final DidUrl id,
+ final String type,
+ final Did controller,
+ final MultibaseEncoded publicKeyMultibase) {
+ Objects.requireNonNull(id);
+ Objects.requireNonNull(type);
+ Objects.requireNonNull(controller);
+ return new ImmutableMultibaseMethod(id, type, controller, publicKeyMultibase);
}
}
diff --git a/src/main/java/com/apicatalog/did/document/ImmutableJwkMethod.java b/src/main/java/com/apicatalog/did/document/ImmutableJwkMethod.java
new file mode 100644
index 0000000..fd0387f
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/document/ImmutableJwkMethod.java
@@ -0,0 +1,51 @@
+package com.apicatalog.did.document;
+
+import java.util.Map;
+
+import com.apicatalog.did.Did;
+import com.apicatalog.did.DidUrl;
+import com.apicatalog.did.datatype.MultibaseEncoded;
+
+final class ImmutableJwkMethod implements DidVerificationMethod {
+
+ final DidUrl id;
+ final String type;
+ final Did controller;
+ final Map publicKeyJwk;
+
+ ImmutableJwkMethod(
+ final DidUrl id,
+ final String type,
+ final Did controller,
+ final Map publicKeyJwk) {
+ this.id = id;
+ this.type = type;
+ this.controller = controller;
+ this.publicKeyJwk = publicKeyJwk;
+ }
+
+ @Override
+ public DidUrl id() {
+ return id;
+ }
+
+ @Override
+ public String type() {
+ return type;
+ }
+
+ @Override
+ public Did controller() {
+ return controller;
+ }
+
+ @Override
+ public MultibaseEncoded publicKeyMultibase() {
+ return null;
+ }
+
+ @Override
+ public Map publicKeyJwk() {
+ return publicKeyJwk;
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/document/ImmutableMultibaseMethod.java b/src/main/java/com/apicatalog/did/document/ImmutableMultibaseMethod.java
new file mode 100644
index 0000000..42b79cf
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/document/ImmutableMultibaseMethod.java
@@ -0,0 +1,51 @@
+package com.apicatalog.did.document;
+
+import java.util.Map;
+
+import com.apicatalog.did.Did;
+import com.apicatalog.did.DidUrl;
+import com.apicatalog.did.datatype.MultibaseEncoded;
+
+final class ImmutableMultibaseMethod implements DidVerificationMethod {
+
+ final DidUrl id;
+ final String type;
+ final Did controller;
+ final MultibaseEncoded publicKeyMultibase;
+
+ ImmutableMultibaseMethod(
+ final DidUrl id,
+ final String type,
+ final Did controller,
+ final MultibaseEncoded publicKeyMultibase) {
+ this.id = id;
+ this.type = type;
+ this.controller = controller;
+ this.publicKeyMultibase = publicKeyMultibase;
+ }
+
+ @Override
+ public DidUrl id() {
+ return id;
+ }
+
+ @Override
+ public String type() {
+ return type;
+ }
+
+ @Override
+ public Did controller() {
+ return controller;
+ }
+
+ @Override
+ public MultibaseEncoded publicKeyMultibase() {
+ return publicKeyMultibase;
+ }
+
+ @Override
+ public Map publicKeyJwk() {
+ return null;
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/document/ImmutableService.java b/src/main/java/com/apicatalog/did/document/ImmutableService.java
new file mode 100644
index 0000000..f410a8d
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/document/ImmutableService.java
@@ -0,0 +1,37 @@
+package com.apicatalog.did.document;
+
+import java.net.URI;
+import java.util.Collection;
+
+final class ImmutableService implements DidService {
+
+ final URI id;
+
+ final Collection type;
+
+ final Collection endpoint;
+
+ ImmutableService(
+ final URI id,
+ final Collection type,
+ final Collection endpoint) {
+ this.id = id;
+ this.type = type;
+ this.endpoint = endpoint;
+ }
+
+ @Override
+ public URI id() {
+ return id;
+ }
+
+ @Override
+ public Collection type() {
+ return type;
+ }
+
+ @Override
+ public Collection endpoint() {
+ return endpoint;
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/document/ImmutableServiceEndpoint.java b/src/main/java/com/apicatalog/did/document/ImmutableServiceEndpoint.java
new file mode 100644
index 0000000..8784fa5
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/document/ImmutableServiceEndpoint.java
@@ -0,0 +1,17 @@
+package com.apicatalog.did.document;
+
+import java.net.URI;
+
+final class ImmutableServiceEndpoint implements DidServiceEndpoint {
+
+ final URI id;
+
+ ImmutableServiceEndpoint(URI id) {
+ this.id = id;
+ }
+
+ @Override
+ public URI id() {
+ return id;
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/document/package-info.java b/src/main/java/com/apicatalog/did/document/package-info.java
new file mode 100644
index 0000000..0f70e2e
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/document/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * DID Document model as defined by the
+ * DID Core
+ * specification.
+ *
+ * Includes interfaces for representing DID Documents
+ * ({@link com.apicatalog.did.document.DidDocument}), services
+ * ({@link com.apicatalog.did.document.DidService},
+ * {@link com.apicatalog.did.document.DidServiceEndpoint}), and verification
+ * methods ({@link com.apicatalog.did.document.DidVerificationMethod}).
+ *
+ */
+package com.apicatalog.did.document;
\ No newline at end of file
diff --git a/src/main/java/com/apicatalog/did/io/DidDocumentReader.java b/src/main/java/com/apicatalog/did/io/DidDocumentReader.java
new file mode 100644
index 0000000..a274829
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/io/DidDocumentReader.java
@@ -0,0 +1,33 @@
+package com.apicatalog.did.io;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import com.apicatalog.did.document.DidDocument;
+
+/**
+ * Reader for parsing a {@link DidDocument} from an {@link InputStream}.
+ *
+ * Implementations handle a specific DID document representation (e.g. JSON-LD,
+ * CBOR).
+ *
+ */
+public interface DidDocumentReader {
+
+ /**
+ * Returns the supported content type (e.g. {@code application/did+ld+json}).
+ *
+ * @return MIME type string
+ */
+ String contentType();
+
+ /**
+ * Reads and parses a DID Document from the given input stream.
+ *
+ * @param is input stream (must not be {@code null})
+ * @return parsed {@code DidDocument}
+ * @throws IOException if a low-level I/O error occurs
+ * @throws DidDocumentReaderException if parsing or validation fails
+ */
+ DidDocument read(InputStream is) throws IOException, DidDocumentReaderException;
+}
diff --git a/src/main/java/com/apicatalog/did/io/DidDocumentReaderException.java b/src/main/java/com/apicatalog/did/io/DidDocumentReaderException.java
new file mode 100644
index 0000000..ab642d8
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/io/DidDocumentReaderException.java
@@ -0,0 +1,37 @@
+package com.apicatalog.did.io;
+
+/**
+ * Exception thrown when a DID Document cannot be read or parsed.
+ */
+public class DidDocumentReaderException extends Exception {
+
+ private static final long serialVersionUID = 2956582473611314812L;
+
+ /**
+ * Creates a new exception with the specified detail message.
+ *
+ * @param message the detail message
+ */
+ public DidDocumentReaderException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new exception with the specified cause.
+ *
+ * @param cause the cause of this exception
+ */
+ public DidDocumentReaderException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Creates a new exception with the specified detail message and cause.
+ *
+ * @param message the detail message
+ * @param cause the cause of this exception
+ */
+ public DidDocumentReaderException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/io/DidDocumentWriter.java b/src/main/java/com/apicatalog/did/io/DidDocumentWriter.java
new file mode 100644
index 0000000..0a75258
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/io/DidDocumentWriter.java
@@ -0,0 +1,33 @@
+package com.apicatalog.did.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import com.apicatalog.did.document.DidDocument;
+
+/**
+ * Writer for serializing a {@link DidDocument} to an {@link OutputStream}.
+ *
+ * Implementations produce a specific DID document representation (e.g. JSON-LD,
+ * CBOR).
+ *
+ */
+public interface DidDocumentWriter {
+
+ /**
+ * Returns the supported content type (e.g. {@code application/did+ld+json}).
+ *
+ * @return MIME type string
+ */
+ String contentType();
+
+ /**
+ * Writes the given DID Document to the provided output stream.
+ *
+ * @param document the DID Document to serialize (must not be {@code null})
+ * @param os the output stream to write to (must not be {@code null})
+ * @throws IOException if a low-level I/O error occurs
+ * @throws DidDocumentWriterException if serialization fails
+ */
+ void write(DidDocument document, OutputStream os) throws IOException, DidDocumentWriterException;
+}
diff --git a/src/main/java/com/apicatalog/did/io/DidDocumentWriterException.java b/src/main/java/com/apicatalog/did/io/DidDocumentWriterException.java
new file mode 100644
index 0000000..4455b2a
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/io/DidDocumentWriterException.java
@@ -0,0 +1,37 @@
+package com.apicatalog.did.io;
+
+/**
+ * Exception thrown when a DID Document cannot be written or serialized.
+ */
+public class DidDocumentWriterException extends Exception {
+
+ private static final long serialVersionUID = -3790735310020905272L;
+
+ /**
+ * Creates a new exception with the specified detail message.
+ *
+ * @param message the detail message
+ */
+ public DidDocumentWriterException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new exception with the specified cause.
+ *
+ * @param cause the cause of this exception
+ */
+ public DidDocumentWriterException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Creates a new exception with the specified detail message and cause.
+ *
+ * @param message the detail message
+ * @param cause the cause of this exception
+ */
+ public DidDocumentWriterException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/io/package-info.java b/src/main/java/com/apicatalog/did/io/package-info.java
new file mode 100644
index 0000000..8ad7860
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/io/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * I/O interfaces for DID Documents.
+ *
+ * Defines readers and writers for serializing and parsing DID Documents in
+ * different representations (e.g. JSON-LD, CBOR).
+ *
+ */
+package com.apicatalog.did.io;
diff --git a/src/main/java/com/apicatalog/did/key/DidDocumentBuilder.java b/src/main/java/com/apicatalog/did/key/DidDocumentBuilder.java
deleted file mode 100644
index 014a7e4..0000000
--- a/src/main/java/com/apicatalog/did/key/DidDocumentBuilder.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.apicatalog.did.key;
-
-import java.util.HashSet;
-import java.util.Set;
-
-import com.apicatalog.did.Did;
-import com.apicatalog.did.document.DidDocument;
-import com.apicatalog.did.document.DidVerificationMethod;
-
-final class DidDocumentBuilder {
-
- private Did id;
- private Set verificationMethod;
-
- protected DidDocumentBuilder() {
- this.verificationMethod = new HashSet<>();
- }
-
- public static DidDocumentBuilder create() {
- return new DidDocumentBuilder();
- }
-
- public DidDocumentBuilder id(Did did) {
- this.id = did;
- return this;
- }
-
- public DidDocumentBuilder add(DidVerificationMethod verificationMethod) {
- this.verificationMethod.add(verificationMethod);
- return this;
- }
-
- public DidDocument build() {
- return new DidDocument(id, null, verificationMethod);
- }
-}
diff --git a/src/main/java/com/apicatalog/did/key/DidKey.java b/src/main/java/com/apicatalog/did/key/DidKey.java
deleted file mode 100644
index 210ca26..0000000
--- a/src/main/java/com/apicatalog/did/key/DidKey.java
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.apicatalog.did.key;
-
-import java.net.URI;
-
-import com.apicatalog.did.Did;
-import com.apicatalog.multibase.Multibase;
-import com.apicatalog.multibase.MultibaseDecoder;
-
-/**
- * Immutable DID Key
- *
- * did-key-format := did:key:[version]:MULTIBASE(multiencodedKey)
- *
- *
- * @see DID
- * method key
- *
- */
-public class DidKey extends Did {
-
- private static final long serialVersionUID = 1343361455801198884L;
-
- public static final String METHOD_KEY = "key";
-
- public static final String DEFAULT_VERSION = "1";
-
- protected final String version;
-
- protected final Multibase base;
-
- protected final byte[] debased;
-
- protected DidKey(String version, String specificId, Multibase base, byte[] debased) {
- super(METHOD_KEY, specificId);
- this.base = base;
- this.version = version;
- this.debased = debased;
- }
-
- /**
- * Creates a new DID key instance from the given {@link URI}.
- *
- * @param uri The source URI to be transformed into DID key
- * @param bases
- * @return The new DID key
- *
- * @throws NullPointerException If {@code uri} is {@code null}
- *
- * @throws IllegalArgumentException If the given {@code uri} is not valid DID
- * key
- */
- public static final DidKey from(final URI uri, final MultibaseDecoder bases) {
-
- final Did did = Did.from(uri);
-
- if (!METHOD_KEY.equalsIgnoreCase(did.getMethod())) {
- throw new IllegalArgumentException("The given URI [" + uri + "] is not valid DID key, does not start with 'did:key'.");
- }
-
- return from(did, bases);
- }
-
- public static final DidKey from(final Did did, final MultibaseDecoder bases) {
-
- if (!METHOD_KEY.equalsIgnoreCase(did.getMethod())) {
- throw new IllegalArgumentException("The given DID method [" + did.getMethod() + "] is not 'key'. DID [" + did.toString() + "].");
- }
-
- final String[] parts = did.getMethodSpecificId().split(":", 2);
-
- String version = DEFAULT_VERSION;
- String encoded = parts[0];
-
- if (parts.length == 2) {
- version = parts[0];
- encoded = parts[1];
- }
-
- final Multibase base = bases.getBase(encoded).orElseThrow(() -> new IllegalArgumentException("Unsupported did:key base encoding. DID [" + did.toString() + "]."));
-
- final byte[] debased = base.decode(encoded);
-
- return new DidKey(version, encoded, base, debased);
- }
-
- public static final DidKey create(Multibase base, byte[] key) {
- return new DidKey(null, base.encode(key), base, key);
- }
-
- public static boolean isDidKey(final Did did) {
- return METHOD_KEY.equalsIgnoreCase(did.getMethod());
- }
-
- public static boolean isDidKey(final URI uri) {
- return Did.isDid(uri)
- && uri.getSchemeSpecificPart().toLowerCase().startsWith(METHOD_KEY + ":");
- }
-
- public static boolean isDidKey(final String uri) {
- return Did.isDid(uri)
- && uri.toLowerCase().startsWith(SCHEME + ":" + METHOD_KEY + ":");
- }
-
- public Multibase getBase() {
- return base;
- }
-
- public byte[] getKey() {
- return debased;
- }
-
- public String getVersion() {
- return version;
- }
-}
diff --git a/src/main/java/com/apicatalog/did/key/DidKeyResolver.java b/src/main/java/com/apicatalog/did/key/DidKeyResolver.java
deleted file mode 100644
index 73a6ec3..0000000
--- a/src/main/java/com/apicatalog/did/key/DidKeyResolver.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package com.apicatalog.did.key;
-
-import com.apicatalog.did.Did;
-import com.apicatalog.did.DidResolver;
-import com.apicatalog.did.DidUrl;
-import com.apicatalog.did.document.DidDocument;
-import com.apicatalog.did.document.DidVerificationMethod;
-import com.apicatalog.multibase.MultibaseDecoder;
-
-public class DidKeyResolver implements DidResolver {
-
- protected final MultibaseDecoder bases;
-
- public DidKeyResolver(final MultibaseDecoder bases) {
- this.bases = bases;
- }
-
- @Override
- public DidDocument resolve(final Did did) {
-
- if (!DidKey.isDidKey(did)) {
- throw new IllegalArgumentException();
- }
-
- final DidKey didKey = DidKey.from(did, bases);
-
- final DidDocumentBuilder builder = DidDocumentBuilder.create();
-
- // 4.
- DidVerificationMethod signatureMethod = DidKeyResolver.createSignatureMethod(didKey);
- builder.add(signatureMethod);
-
- // 5.
- builder.add(DidKeyResolver.createEncryptionMethod(didKey));
-
- // 6.
- builder.id(did);
-
- // 7.
-
- // 8.
-
- // 9.
-
- return builder.build();
- }
-
- /**
- * Creates a new verification key by expanding the given DID key.
- *
- * @param didKey
- *
- * @see Signature Method Algorithm
- *
- * @return The new verification key
- */
- public static DidVerificationMethod createSignatureMethod(DidKey didKey) {
- return new DidVerificationMethod(
- DidUrl.from(didKey, null, null, didKey.getMethodSpecificId()),
- DidUrl.from(didKey, null, null, didKey.getMethodSpecificId()),
- didKey.getKey()
- );
- }
-
- public static DidVerificationMethod createEncryptionMethod(final DidKey didKey) {
-
- // 3.
-
- // 5.
-// String encodingType = "MultiKey";
-
- // 6.
-
- // 7.
-
- // 9.
- return new DidVerificationMethod(
- DidUrl.from(didKey, null, null, didKey.getMethodSpecificId()),
- DidUrl.from(didKey, null, null, didKey.getMethodSpecificId()),
- didKey.getKey()
- );
- }
-}
diff --git a/src/main/java/com/apicatalog/did/package-info.java b/src/main/java/com/apicatalog/did/package-info.java
new file mode 100644
index 0000000..90ea1a2
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/package-info.java
@@ -0,0 +1,12 @@
+/**
+ * Core classes for working with
+ * Decentralized Identifiers
+ * (DIDs).
+ *
+ * Provides immutable representations of bare DIDs
+ * ({@link com.apicatalog.did.Did}) and DID URLs
+ * ({@link com.apicatalog.did.DidUrl}), along with validation and conversion
+ * utilities.
+ *
+ */
+package com.apicatalog.did;
diff --git a/src/main/java/com/apicatalog/did/resolver/DidDocumentMetadata.java b/src/main/java/com/apicatalog/did/resolver/DidDocumentMetadata.java
new file mode 100644
index 0000000..3477ac4
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/resolver/DidDocumentMetadata.java
@@ -0,0 +1,87 @@
+package com.apicatalog.did.resolver;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.Set;
+
+import com.apicatalog.did.Did;
+
+/**
+ * Metadata associated with a resolved DID Document, as defined in
+ * DID Core โ
+ * DID Document Metadata.
+ */
+public interface DidDocumentMetadata {
+
+ /**
+ * The timestamp when the DID Document was created.
+ *
+ * @return creation time, or {@code null} if not provided
+ */
+ default Instant created() {
+ return null;
+ }
+
+ /**
+ * The timestamp when the DID Document was last updated.
+ *
+ * @return last update time, or {@code null} if not provided
+ */
+ default Instant updated() {
+ return null;
+ }
+
+ /**
+ * Indicates whether the DID has been deactivated.
+ *
+ * @return {@code true} if deactivated, otherwise {@code false}
+ */
+ default boolean deactivated() {
+ return false;
+ }
+
+ /**
+ * A timestamp after which the DID Document should be refreshed.
+ *
+ * @return refresh time, or {@code null} if not specified
+ */
+ default Instant refresh() {
+ return null;
+ }
+
+ /**
+ * The identifier for the current version of the DID Document.
+ *
+ * @return version identifier, or {@code null} if not provided
+ */
+ default String versionId() {
+ return null;
+ }
+
+ /**
+ * The identifier of the next version of the DID Document.
+ *
+ * @return next version identifier, or {@code null} if not provided
+ */
+ default String nextVersionId() {
+ return null;
+ }
+
+ /**
+ * Equivalent identifiers for the DID, if any.
+ *
+ * @return a set of equivalent DIDs, never {@code null}
+ */
+ default Set equivalentId() {
+ return Collections.emptySet();
+ }
+
+ /**
+ * The canonical identifier for the DID.
+ *
+ * @return canonical DID, or {@code null} if not provided
+ */
+ default Did canonicalId() {
+ return null;
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/resolver/DidMethodResolver.java b/src/main/java/com/apicatalog/did/resolver/DidMethodResolver.java
new file mode 100644
index 0000000..9ae915f
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/resolver/DidMethodResolver.java
@@ -0,0 +1,57 @@
+package com.apicatalog.did.resolver;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import com.apicatalog.did.Did;
+import com.apicatalog.did.resolver.DidResolutionException.Code;
+
+public class DidMethodResolver implements DidResolver {
+
+ protected final Map resolvers;
+
+ protected DidMethodResolver(final Map resolvers) {
+ this.resolvers = resolvers;
+ }
+
+ @Override
+ public ResolvedDidDocument resolve(Did did) throws DidResolutionException {
+
+ Objects.requireNonNull(did);
+
+ final DidResolver resolver = resolvers.get(did.getMethod());
+
+ if (resolver == null) {
+ throw new DidResolutionException(did.toString(), Code.UnsupportedMethod);
+ }
+ return resolver.resolve(did);
+ }
+
+ public static Builder with(String method, DidResolver resolver) {
+ return (new Builder()).with(method, resolver);
+ }
+
+ public static class Builder {
+
+ final Map resolvers;
+
+ Builder() {
+ this.resolvers = new LinkedHashMap<>();
+ }
+
+ public Builder with(String method, DidResolver resolver) {
+ resolvers.put(method, resolver);
+ return this;
+ }
+
+ public DidResolver build() {
+ if (resolvers.size() == 1) {
+ return resolvers.values().iterator().next();
+ }
+ return new DidMethodResolver(Collections.unmodifiableMap(resolvers));
+ }
+ }
+
+}
diff --git a/src/main/java/com/apicatalog/did/resolver/DidResolutionException.java b/src/main/java/com/apicatalog/did/resolver/DidResolutionException.java
new file mode 100644
index 0000000..cc8ec60
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/resolver/DidResolutionException.java
@@ -0,0 +1,106 @@
+package com.apicatalog.did.resolver;
+
+/**
+ * Exception thrown during DID resolution.
+ *
+ * Indicates resolution failures such as invalid input, not found, unsupported
+ * representation, or internal errors.
+ *
+ */
+public class DidResolutionException extends Exception {
+
+ private static final long serialVersionUID = -7104603698482015381L;
+
+ /**
+ * Standard resolution error codes.
+ */
+ public enum Code {
+ /** The DID syntax is invalid and cannot be parsed. */
+ InvalidDid,
+
+ /** The DID method resolution is not supported. */
+ UnsupportedMethod,
+
+ /** The DID could not be found. */
+ NotFound,
+
+ /** An internal error occurred. */
+ Internal,
+ }
+
+ protected final String did;
+ protected final Code code;
+
+ /**
+ * Creates a new resolution exception with a DID and code.
+ *
+ * @param did the DID being resolved (may be {@code null})
+ * @param code error code
+ */
+ public DidResolutionException(String did, Code code) {
+ this.did = did;
+ this.code = code;
+ }
+
+ /**
+ * Creates a new resolution exception with a message.
+ *
+ * @param did the DID being resolved (may be {@code null})
+ * @param code error code
+ * @param message detail message
+ */
+ public DidResolutionException(String did, Code code, String message) {
+ super(message);
+ this.did = did;
+ this.code = code;
+ }
+
+ /**
+ * Creates a new resolution exception with a cause.
+ *
+ * Code is set to {@link Code#Internal}.
+ *
+ *
+ * @param did the DID being resolved (may be {@code null})
+ * @param e the cause
+ */
+ public DidResolutionException(String did, Throwable e) {
+ super(e);
+ this.did = did;
+ this.code = Code.Internal;
+ }
+
+ /**
+ * Creates a new resolution exception with a message and cause.
+ *
+ * Code is set to {@link Code#Internal}.
+ *
+ *
+ * @param did the DID being resolved (may be {@code null})
+ * @param message detail message
+ * @param e the cause
+ */
+ public DidResolutionException(String did, String message, Throwable e) {
+ super(message, e);
+ this.did = did;
+ this.code = Code.Internal;
+ }
+
+ /**
+ * Returns the DID that failed to resolve.
+ *
+ * @return the DID, or {@code null}
+ */
+ public String getDid() {
+ return did;
+ }
+
+ /**
+ * Returns the resolution error code.
+ *
+ * @return error code
+ */
+ public Code getCode() {
+ return code;
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/resolver/DidResolver.java b/src/main/java/com/apicatalog/did/resolver/DidResolver.java
new file mode 100644
index 0000000..7c4eaed
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/resolver/DidResolver.java
@@ -0,0 +1,21 @@
+package com.apicatalog.did.resolver;
+
+import com.apicatalog.did.Did;
+import com.apicatalog.did.document.DidDocument;
+
+/**
+ * A DID
+ * Resolver expands a {@link Did} into a corresponding {@link DidDocument}.
+ */
+@FunctionalInterface
+public interface DidResolver {
+
+ /**
+ * Resolves the given DID into a {@link DidDocument}.
+ *
+ * @param did the DID to resolve (must not be {@code null})
+ * @return the resolution result as a {@link ResolvedDidDocument}
+ * @throws DidResolutionException if resolution fails
+ */
+ ResolvedDidDocument resolve(Did did) throws DidResolutionException;
+}
diff --git a/src/main/java/com/apicatalog/did/resolver/ImmutableResolvedDocument.java b/src/main/java/com/apicatalog/did/resolver/ImmutableResolvedDocument.java
new file mode 100644
index 0000000..89d83a3
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/resolver/ImmutableResolvedDocument.java
@@ -0,0 +1,27 @@
+package com.apicatalog.did.resolver;
+
+import com.apicatalog.did.document.DidDocument;
+
+final class ImmutableResolvedDocument implements ResolvedDidDocument {
+
+ final DidDocument document;
+ final DidDocumentMetadata meta;
+
+ ImmutableResolvedDocument(
+ final DidDocument document,
+ final DidDocumentMetadata meta
+ ) {
+ this.document = document;
+ this.meta = meta;
+ }
+
+ @Override
+ public DidDocument document() {
+ return document;
+ }
+
+ @Override
+ public DidDocumentMetadata metadata() {
+ return meta;
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/resolver/ResolvedDidDocument.java b/src/main/java/com/apicatalog/did/resolver/ResolvedDidDocument.java
new file mode 100644
index 0000000..ae1fd3b
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/resolver/ResolvedDidDocument.java
@@ -0,0 +1,53 @@
+package com.apicatalog.did.resolver;
+
+import com.apicatalog.did.document.DidDocument;
+
+/**
+ * Result of a DID resolution process.
+ *
+ * Contains the resolved {@link DidDocument} and optional
+ * {@link DidDocumentMetadata}.
+ *
+ *
+ * @see DID
+ * Resolution
+ */
+public interface ResolvedDidDocument {
+
+ /**
+ * Returns the resolved DID Document.
+ *
+ * @return the DID Document (never {@code null})
+ */
+ DidDocument document();
+
+ /**
+ * Returns resolution metadata associated with the DID Document.
+ *
+ * @return metadata, or {@code null} if none
+ */
+ default DidDocumentMetadata metadata() {
+ return null;
+ }
+
+ /**
+ * Creates a resolution result containing only a document.
+ *
+ * @param document the resolved DID Document
+ * @return a new {@code ResolvedDidDocument}
+ */
+ static ResolvedDidDocument of(DidDocument document) {
+ return new ImmutableResolvedDocument(document, null);
+ }
+
+ /**
+ * Creates a resolution result containing a document and metadata.
+ *
+ * @param document the resolved DID Document
+ * @param meta associated metadata (may be {@code null})
+ * @return a new {@code ResolvedDidDocument}
+ */
+ static ResolvedDidDocument of(DidDocument document, DidDocumentMetadata meta) {
+ return new ImmutableResolvedDocument(document, meta);
+ }
+}
diff --git a/src/main/java/com/apicatalog/did/resolver/package-info.java b/src/main/java/com/apicatalog/did/resolver/package-info.java
new file mode 100644
index 0000000..66bc40f
--- /dev/null
+++ b/src/main/java/com/apicatalog/did/resolver/package-info.java
@@ -0,0 +1,18 @@
+/**
+ * DID Resolution interfaces and exceptions.
+ *
+ * Provides abstractions for resolving Decentralized Identifiers (DIDs) into DID
+ * Documents and associated metadata, following the
+ * DID Resolution
+ * specification.
+ *
+ *
+ *
+ * - {@link com.apicatalog.did.resolver.DidResolver} โ DID resolution API
+ * - {@link com.apicatalog.did.resolver.ResolvedDidDocument} โ resolution
+ * result
+ * - {@link com.apicatalog.did.resolver.DidResolutionException} โ resolution
+ * errors
+ *
+ */
+package com.apicatalog.did.resolver;
\ No newline at end of file
diff --git a/src/test/java/com/apicatalog/did/DidTest.java b/src/test/java/com/apicatalog/did/DidTest.java
index 94d1afd..8bf0d2c 100644
--- a/src/test/java/com/apicatalog/did/DidTest.java
+++ b/src/test/java/com/apicatalog/did/DidTest.java
@@ -7,24 +7,24 @@
import static org.junit.jupiter.api.Assertions.fail;
import java.net.URI;
-import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@DisplayName("DID")
@TestMethodOrder(OrderAnnotation.class)
class DidTest {
- @DisplayName("from(String)")
+ @DisplayName("of(String)")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
- void fromString(String uri, String method, String specificId) {
- final Did did = Did.from(uri);
+ @MethodSource({ "positiveVectors" })
+ void ofString(String uri, String method, String specificId) {
+ final Did did = Did.of(uri);
assertNotNull(did);
assertFalse(did.isDidUrl());
@@ -32,22 +32,11 @@ void fromString(String uri, String method, String specificId) {
assertEquals(specificId, did.getMethodSpecificId());
}
- @DisplayName("!from(String)")
- @ParameterizedTest()
- @MethodSource({ "negativeVectors" })
- void fromStringNegative(String uri) {
- try {
- Did.from(uri);
- fail();
- } catch (IllegalArgumentException e) {
- /* expected */ }
- }
-
- @DisplayName("from(URI)")
+ @DisplayName("of(URI)")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
- void fromUri(String input, String method, String specificId) {
- final Did did = Did.from(URI.create(input));
+ @MethodSource({ "positiveVectors" })
+ void ofUri(String input, String method, String specificId) {
+ final Did did = Did.of(URI.create(input));
assertNotNull(did);
assertFalse(did.isDidUrl());
@@ -57,20 +46,31 @@ void fromUri(String input, String method, String specificId) {
@DisplayName("toString()")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
+ @MethodSource({ "positiveVectors" })
void toString(String input, String method, String specificId) {
- final Did did = Did.from(input);
+ final Did did = Did.of(input);
assertNotNull(did);
assertEquals(input, did.toString());
}
-
- @DisplayName("!from(URI)")
+
+ @DisplayName("negative: of(String)")
@ParameterizedTest()
@MethodSource({ "negativeVectors" })
- void fromUriNegative(String uri) {
+ void ofStringNegative(String uri) {
try {
- Did.from(URI.create(uri));
+ Did.of(uri);
+ fail();
+ } catch (IllegalArgumentException e) {
+ /* expected */ }
+ }
+
+ @DisplayName("negative: of(URI)")
+ @ParameterizedTest()
+ @MethodSource({ "negativeVectors" })
+ void ofUriNegative(String uri) {
+ try {
+ Did.of(URI.create(uri));
fail();
} catch (IllegalArgumentException | NullPointerException e) {
/* expected */ }
@@ -78,9 +78,9 @@ void fromUriNegative(String uri) {
@DisplayName("toUri()")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
+ @MethodSource({ "positiveVectors" })
void toUri(String input, String method, String specificId) {
- final Did did = Did.from(URI.create(input));
+ final Did did = Did.of(URI.create(input));
assertNotNull(did);
assertEquals(URI.create(input), did.toUri());
@@ -88,12 +88,12 @@ void toUri(String input, String method, String specificId) {
@DisplayName("isDid(String)")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
+ @MethodSource({ "positiveVectors" })
void stringIsDid(String uri) {
assertTrue(Did.isDid(uri));
}
-
- @DisplayName("isNotDid(String)")
+
+ @DisplayName("negative: isDid(String)")
@ParameterizedTest()
@MethodSource({ "negativeVectors" })
void stringIsNotDid(String uri) {
@@ -102,78 +102,200 @@ void stringIsNotDid(String uri) {
@DisplayName("isDid(URI)")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
+ @MethodSource({ "positiveVectors" })
void uriIsDid(String uri) {
assertTrue(Did.isDid(URI.create(uri)));
}
- static Stream validVectors() {
- return Arrays.stream(new String[][] {
- {
+ static Stream positiveVectors() {
+ return Stream.of(
+ Arguments.of(
"did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
"key",
- "z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"
- },
- {
- "did:example:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
+ "z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"),
+ Arguments.of("did:example:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
"example",
- "z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2"
- },
- {
- "did:key:1.1:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
+ "z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2"),
+ Arguments.of("did:key:1.1:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
"key",
- "1.1:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2"
- },
- {
- "did:web:method:specific:identifier",
+ "1.1:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2"),
+ Arguments.of("did:web:method:specific:identifier",
"web",
- "method:specific:identifier"
- },
- {
- "did:tdw:example.com:dids:12345",
+ "method:specific:identifier"),
+ Arguments.of("did:tdw:example.com:dids:12345",
"tdw",
- "example.com:dids:12345"
- },
- {
- "did:tdw:12345.example.com",
+ "example.com:dids:12345"),
+ Arguments.of("did:tdw:12345.example.com",
"tdw",
- "12345.example.com",
- },
- {
- "did:tdw:example.com_12345",
+ "12345.example.com"),
+ Arguments.of("did:tdw:example.com_12345",
"tdw",
- "example.com_12345"
- }
- });
+ "example.com_12345"),
+ Arguments.of("did:0:%E2%9C%93",
+ "0",
+ "%E2%9C%93"),
+ Arguments.of(
+ "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
+ "key",
+ "z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH"),
+
+ Arguments.of(
+ "did:key:z6MkhwTbtMsvDSB8z2pF8A4DRNirMRHkRkNvztNFVQVw1W8H",
+ "key",
+ "z6MkhwTbtMsvDSB8z2pF8A4DRNirMRHkRkNvztNFVQVw1W8H"),
+
+ Arguments.of(
+ "did:web:example.com",
+ "web",
+ "example.com"),
+
+ Arguments.of(
+ "did:web:example.com:user:alice",
+ "web",
+ "example.com:user:alice"),
+
+ /* pct-encoded: space */
+ Arguments.of(
+ "did:web:example.com:users:alice%20smith",
+ "web",
+ "example.com:users:alice%20smith"),
+
+ /* pct-encoded: colon (port-like), plus another segment */
+ Arguments.of(
+ "did:web:example.com%3A8443:users:alice",
+ "web",
+ "example.com%3A8443:users:alice"),
+
+ /* pct-encoded: slash inside a single segment */
+ Arguments.of(
+ "did:web:example.com:dir%2Fsubdir%2Ffile",
+ "web",
+ "example.com:dir%2Fsubdir%2Ffile"),
+
+ /* pct-encoded: percent sign */
+ Arguments.of(
+ "did:web:example.com:foo%25bar",
+ "web",
+ "example.com:foo%25bar"),
+
+ /* pct-encoded: question + hash */
+ Arguments.of(
+ "did:web:example.com:ticket%3F42:ref%23A",
+ "web",
+ "example.com:ticket%3F42:ref%23A"),
+
+ /* pct-encoded: multi-byte UTF-8 (โ) */
+ Arguments.of(
+ "did:web:example.com:unicode%E2%9C%93",
+ "web",
+ "example.com:unicode%E2%9C%93"),
+
+ /* pct-encoded: emoji (๐) */
+ Arguments.of(
+ "did:web:example.com:emoji%F0%9F%98%80",
+ "web",
+ "example.com:emoji%F0%9F%98%80"),
+
+ /* pct-encoded: tilde (unreserved but here encoded for coverage) */
+ Arguments.of(
+ "did:web:sub.example.com%3A8443:%7Ealice",
+ "web",
+ "sub.example.com%3A8443:%7Ealice"),
+
+ Arguments.of(
+ "did:pkh:eip155:1:0xF39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
+ "pkh",
+ "eip155:1:0xF39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
+
+ Arguments.of(
+ "did:ethr:0x5:0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1",
+ "ethr",
+ "0x5:0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1"),
+
+ Arguments.of(
+ "did:sov:2wJPyULfLLnYTEFYzByfUR",
+ "sov",
+ "2wJPyULfLLnYTEFYzByfUR"),
+
+ Arguments.of(
+ "did:uuid:123e4567-e89b-12d3-a456-426614174000",
+ "uuid",
+ "123e4567-e89b-12d3-a456-426614174000"),
+
+ Arguments.of(
+ "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2In0",
+ "jwk",
+ "eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2In0"),
+
+ Arguments.of(
+ "did:btcr:xz35-jzv2-qqs2-7x4",
+ "btcr",
+ "xz35-jzv2-qqs2-7x4"));
}
- static Stream negativeVectors() {
- return Arrays.stream(new String[] {
- "did:example:123456/path",
- "did:example:123456?versionId=1",
- "did:example:123#public-key-0",
- "did:example:123?service=agent&relativeRef=/credentials#degree",
- "did:example:123?service=files&relativeRef=/resume.pdf",
- "did:example:123#",
- "did:example:123?",
- "did:example:123/",
- null,
- "",
- "https://example.com",
- "irc:example:channel",
- "did:example.com:channel",
- "did:example: ",
- "did:example:",
- "did:example",
- "did:",
- "did",
- ":example:channel",
- " :example:channel",
- "did::channel",
- "did: :channel",
- " did:method:id",
- "did:method:id ",
- });
+ static Stream negativeVectors() {
+ return Stream.of(
+ Arguments.of("did:example:123456/path"),
+ Arguments.of("did:example:123456?versionId=1"),
+ Arguments.of("did:example:123#public-key-0"),
+ Arguments.of("did:example:123?service=agent&relativeRef=/credentials#degree"),
+ Arguments.of("did:example:123?service=files&relativeRef=/resume.pdf"),
+ Arguments.of("did:example:123#"),
+ Arguments.of("did:example:123?"),
+ Arguments.of("did:example:123/"),
+ Arguments.of(""),
+ Arguments.of("https://example.com"),
+ Arguments.of("irc:example:channel"),
+ Arguments.of("did:example.com:channel"),
+ Arguments.of("did:example: "),
+ Arguments.of("did:example:"),
+ Arguments.of("did:example"),
+ Arguments.of("did:"),
+ Arguments.of("did"),
+ Arguments.of(":example:channel"),
+ Arguments.of(" :example:channel"),
+ Arguments.of("did::channel"),
+ Arguments.of("did: :channel"),
+ Arguments.of("did:example:123456/path"),
+ Arguments.of("did:example:123 456"),
+ Arguments.of("did:example:"), // missing final 1*idchar
+ Arguments.of("did:example"), // missing method-specific-id
+ Arguments.of("did:"), // missing method and msi
+ Arguments.of("did"), // not a URI
+ Arguments.of("did:EXAMPLE:abc"), // uppercase in method-name
+ Arguments.of("did:ex_ample:abc"), // '_' not allowed in method-name
+ Arguments.of("did:ex.ample:abc"), // '.' not allowed in method-name
+ Arguments.of("did:%65xample:abc"), // pct-encoding not allowed in method-name
+ Arguments.of("did:ExAmple:abc"), // mixed case method-name
+ Arguments.of("did::abc"), // empty method-name
+ Arguments.of("did:example:abc:"), // final empty segment
+ Arguments.of("did:example::"), // final empty segment (last is empty)
+ Arguments.of("did:web:example.com:"), // final empty segment
+ Arguments.of("did:web:example.com::"), // final empty segment
+ Arguments.of("did:web:example.com:users:"), // final empty segment
+ Arguments.of("did:web:example.com/path"), // path not allowed in bare DID
+ Arguments.of("did:web:example.com?query"), // query not allowed in bare DID
+ Arguments.of("did:web:example.com#frag"), // fragment not allowed in bare DID
+ Arguments.of("did:web:example.com#"), // trailing '#'
+ Arguments.of("did://example.com:abc"), // authority present (//...)
+ Arguments.of("did:example:abc/def"), // raw '/' not allowed (must be %2F)
+ Arguments.of("did:example:alice@corp"), // '@' not allowed
+ Arguments.of("did:example:alice,corp"), // ',' not allowed
+ Arguments.of("did:example:;alice"), // ';' not allowed
+ Arguments.of("did:example:(alice)"), // parentheses not allowed
+ Arguments.of("did:example:~alice"), // '~' not allowed unescaped
+ Arguments.of("did:example:%"), // lone '%'
+ Arguments.of("did:example:%2"), // incomplete pct-encoded
+ Arguments.of("did:example:%ZZ"), // non-hex pct-encoded
+ Arguments.of("did:example:foo%2Gbar"), // bad hex digit
+ Arguments.of("did:web:example.com:users:alice%2"), // incomplete pct-encoded
+ Arguments.of("did:web:example.com%3G:users"), // invalid pct-encoded in segment
+ Arguments.of("did:example:รค"), // non-ASCII unescaped
+ Arguments.of("did:examplรฉ:abc"), // non-ASCII in method-name
+ Arguments.of("did%3Aexample:abc"), // encoded ':' in scheme
+ Arguments.of(" did:example:abc"), // leading space
+ Arguments.of("did:example:abc ") // trailing space
+ );
}
}
diff --git a/src/test/java/com/apicatalog/did/DidUrlTest.java b/src/test/java/com/apicatalog/did/DidUrlTest.java
index 16fdac4..5286fe4 100644
--- a/src/test/java/com/apicatalog/did/DidUrlTest.java
+++ b/src/test/java/com/apicatalog/did/DidUrlTest.java
@@ -5,24 +5,24 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.net.URI;
-import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
@DisplayName("DID URL")
@TestMethodOrder(OrderAnnotation.class)
class DidUrlTest {
- @DisplayName("from(String)")
+ @DisplayName("of(String)")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
- void fromString(String uri, String method, String specificId, String path, String query, String fragment) {
- final DidUrl didUrl = DidUrl.from(uri);
+ @MethodSource({ "positiveVectors" })
+ void ofString(String uri, String method, String specificId, String path, String query, String fragment) {
+ final DidUrl didUrl = DidUrl.of(uri);
assertNotNull(didUrl);
assertTrue(didUrl.isDidUrl());
@@ -33,11 +33,11 @@ void fromString(String uri, String method, String specificId, String path, Strin
assertEquals(fragment, didUrl.getFragment());
}
- @DisplayName("from(URI)")
+ @DisplayName("of(URI)")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
- void fromUri(String input, String method, String specificId, String path, String query, String fragment) {
- final DidUrl didUrl = DidUrl.from(URI.create(input));
+ @MethodSource({ "positiveVectors" })
+ void ofUri(String input, String method, String specificId, String path, String query, String fragment) {
+ final DidUrl didUrl = DidUrl.of(URI.create(input));
assertNotNull(didUrl);
assertTrue(didUrl.isDidUrl());
@@ -50,9 +50,9 @@ void fromUri(String input, String method, String specificId, String path, String
@DisplayName("toUri()")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
+ @MethodSource({ "positiveVectors" })
void toUri(String input, String method, String specificId, String path, String query, String fragment) {
- final DidUrl didUrl = DidUrl.from(URI.create(input));
+ final DidUrl didUrl = DidUrl.of(URI.create(input));
assertNotNull(didUrl);
assertEquals(URI.create(input), didUrl.toUri());
@@ -60,127 +60,211 @@ void toUri(String input, String method, String specificId, String path, String q
@DisplayName("isDidUrl(String)")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
+ @MethodSource({ "positiveVectors" })
void stringIsDid(String uri) {
assertTrue(DidUrl.isDidUrl(uri));
}
@DisplayName("isDidUrl(URI)")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
+ @MethodSource({ "positiveVectors" })
void uriIsDid(String uri) {
assertTrue(DidUrl.isDidUrl(URI.create(uri)));
}
@DisplayName("toString()")
@ParameterizedTest(name = "{0}")
- @MethodSource({ "validVectors" })
+ @MethodSource({ "positiveVectors" })
void toString(String input) {
- final DidUrl didUrl = DidUrl.from(input);
+ final DidUrl didUrl = DidUrl.of(input);
assertNotNull(didUrl);
assertEquals(input, didUrl.toString());
}
- static Stream validVectors() {
- return Arrays.stream(new String[][] {
- {
+ static Stream positiveVectors() {
+ return Stream.of(
+ Arguments.of(
"did:example:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
"example",
"z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
null,
null,
- null,
- },
- {
- "did:key:1.1:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
+ null),
+ Arguments.of("did:key:1.1:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
"key",
"1.1:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
null,
null,
- null,
- },
- {
- "did:web:method:specific:identifier",
+ null),
+ Arguments.of("did:web:method:specific:identifier",
"web",
"method:specific:identifier",
null,
null,
- null,
- },
- {
- "did:example:123456/path",
+ null),
+ Arguments.of("did:example:123456/path",
"example",
"123456",
"/path",
null,
- null
- },
- {
- "did:example:123456?versionId=1",
+ null),
+ Arguments.of("did:example:123456?versionId=1",
"example",
"123456",
null,
"versionId=1",
- null
- },
- {
- "did:example:123#public-key-0",
+ null),
+ Arguments.of("did:example:123#public-key-0",
"example",
"123",
null,
null,
- "public-key-0"
- },
- {
- "did:example:123?service=agent&relativeRef=/credentials#degree",
+ "public-key-0"),
+ Arguments.of("did:example:123?service=agent&relativeRef=/credentials#degree",
"example",
"123",
null,
"service=agent&relativeRef=/credentials",
- "degree"
- },
- {
- "did:example:123?service=files&relativeRef=/resume.pdf",
+ "degree"),
+ Arguments.of("did:example:123?service=files&relativeRef=/resume.pdf",
"example",
"123",
null,
"service=files&relativeRef=/resume.pdf",
- null
- },
- {
- "did:example:1?",
+ null),
+ Arguments.of("did:example:1?",
"example",
"1",
null,
"",
- null
- },
- {
- "did:example:a#",
+ null),
+ Arguments.of("did:example:a#",
"example",
"a",
null,
null,
- ""
- },
- {
- "did:example:a/",
+ ""),
+ Arguments.of("did:example:a/",
"example",
"a",
"/",
null,
- null
- },
- {
- "did:example:a/?#",
+ null),
+ Arguments.of("did:example:a/?#",
"example",
"a",
"/",
"",
- ""
- }
+ ""),
+
+ // Basic valid DID URLs
+ Arguments.of(
+ "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
+ "key",
+ "z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
+ null, null, null),
+
+ Arguments.of(
+ "did:key:z6MkhwTbtMsvDSB8z2pF8A4DRNirMRHkRkNvztNFVQVw1W8H",
+ "key",
+ "z6MkhwTbtMsvDSB8z2pF8A4DRNirMRHkRkNvztNFVQVw1W8H",
+ null, null, null),
+
+ // Valid DID URL with path
+ Arguments.of(
+ "did:web:example.com/users/alice",
+ "web",
+ "example.com",
+ "/users/alice", null, null),
+
+ // Valid DID URL with path and query
+ Arguments.of(
+ "did:web:example.com/users/alice?role=admin&active=true",
+ "web",
+ "example.com",
+ "/users/alice", "role=admin&active=true", null),
+
+ // Valid DID URL with fragment
+ Arguments.of(
+ "did:web:example.com/users/alice#section1",
+ "web",
+ "example.com",
+ "/users/alice", null, "section1"),
+
+ // DID URL with encoded characters in path
+ Arguments.of(
+ "did:web:example.com/files%2Fmy%2Fdoc%3Fv=1#overview",
+ "web",
+ "example.com",
+ "/files%2Fmy%2Fdoc%3Fv=1", null, "overview"),
+
+ Arguments.of(
+ "did:web:example.com%3A8443/files%2Fmy%2Fdoc%3Fv=1#overview",
+ "web",
+ "example.com%3A8443",
+ "/files%2Fmy%2Fdoc%3Fv=1", null, "overview"),
- });
+ // DID URL with an empty path and query
+ Arguments.of(
+ "did:web:example.com?#section1",
+ "web",
+ "example.com",
+ null, "", "section1"),
+
+ // DID URL with only query and fragment
+ Arguments.of(
+ "did:web:example.com?role=admin&active=true#section1",
+ "web",
+ "example.com",
+ null, "role=admin&active=true", "section1"),
+
+ // DID URL with multiple segments and pct-encoded
+ Arguments.of(
+ "did:web:example.com/path%2Fto%2Ffile%3Fv%3D1#fragment1",
+ "web",
+ "example.com",
+ "/path%2Fto%2Ffile%3Fv%3D1", null, "fragment1"),
+
+ // Valid DID URL with port encoded in the method
+ Arguments.of(
+ "did:web:example.com%3A8080/path/to/resource",
+ "web",
+ "example.com%3A8080",
+ "/path/to/resource", null, null),
+
+ // Valid DID URL with multiple path segments and query parameters
+ Arguments.of(
+ "did:example:abc%2Fdef:1234/segment1/segment2?param1=value1¶m2=value2#fragment",
+ "example",
+ "abc%2Fdef:1234",
+ "/segment1/segment2", "param1=value1¶m2=value2", "fragment"),
+
+ // Valid DID URL with long segments
+ Arguments.of(
+ "did:web:example.com/path/to/long/segment/with/many/parts/inside",
+ "web",
+ "example.com",
+ "/path/to/long/segment/with/many/parts/inside", null, null),
+
+ // DID URL with query-only (no path)
+ Arguments.of(
+ "did:web:example.com?query=only#end",
+ "web",
+ "example.com",
+ null, "query=only", "end"),
+
+ Arguments.of(
+ "did:web:example.com#section%23end",
+ "web",
+ "example.com",
+ null, null, "section%23end"),
+
+ // Valid DID URL with a mix of encoded and unencoded parts
+ Arguments.of(
+ "did:web:example.com/users%2Fjohn%3Fstatus=active#profile",
+ "web",
+ "example.com",
+ "/users%2Fjohn%3Fstatus=active", null, "profile"));
}
}
diff --git a/src/test/java/com/apicatalog/did/key/DidKeyTest.java b/src/test/java/com/apicatalog/did/key/DidKeyTest.java
deleted file mode 100644
index 52e142a..0000000
--- a/src/test/java/com/apicatalog/did/key/DidKeyTest.java
+++ /dev/null
@@ -1,132 +0,0 @@
-package com.apicatalog.did.key;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.fail;
-
-import java.util.Arrays;
-import java.util.stream.Stream;
-
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
-import org.junit.jupiter.api.TestMethodOrder;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import com.apicatalog.multibase.MultibaseDecoder;
-
-@DisplayName("DID Key")
-@TestMethodOrder(OrderAnnotation.class)
-class DidKeyTest {
-
- @DisplayName("Create DID key from string")
- @ParameterizedTest(name = "{0}")
- @MethodSource({ "testVectors" })
- void fromString(DidKeyTestCase testCase) {
- try {
-
- final DidKey didKey = DidKey.from(testCase.uri, MultibaseDecoder.getInstance());
-
- if (testCase.negative) {
- fail("Expected failure but got " + didKey);
- return;
- }
-
- assertNotNull(didKey);
- assertNotNull(didKey.getKey());
- assertEquals(testCase.version, didKey.getVersion());
- assertEquals(testCase.keyLength, didKey.getKey().length);
-
- } catch (IllegalArgumentException | NullPointerException e) {
- if (!testCase.negative) {
- e.printStackTrace();
- fail(e);
- }
- }
- }
-
- static Stream testVectors() {
- return Arrays.stream(testCases);
- }
-
- static final DidKeyTestCase testCases[] = new DidKeyTestCase[] {
- DidKeyTestCase.create(
- "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH",
- 34
- ),
- DidKeyTestCase.create(
- "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp",
- 34
- ),
- DidKeyTestCase.create(
- "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG",
- 34
- ),
- DidKeyTestCase.create(
- "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf",
- 34
- ),
- DidKeyTestCase.create(
- "did:key:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
- 34
- ),
- DidKeyTestCase.create(
- "did:key:z6MkiVQTYk3L2XKY6yg6MyeN2QLE5QkKcXByUeY1dkdiLx4j",
- 34
- ),
- DidKeyTestCase.create(
- "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme",
- 35
- ),
- DidKeyTestCase.create(
- "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2",
- 35
- ),
- DidKeyTestCase.create(
- "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N",
- 35
- ),
- DidKeyTestCase.create(
- "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169",
- 35
- ),
- DidKeyTestCase.create(
- "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv",
- 35
- ),
- DidKeyTestCase.create(
- "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9",
- 51
- ),
- DidKeyTestCase.create(
- "did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54",
- 51
- ),
- DidKeyTestCase.create(
- "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7",
- 69
- ),
- DidKeyTestCase.create(
- "did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f",
- 69
- ),
-
- // invalid keys
- DidKeyTestCase.create("http:key:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2"),
- DidKeyTestCase.create("did:example:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2"),
- DidKeyTestCase.create(null),
-
- // versioned keys
- DidKeyTestCase.create(
- "did:key:1.1:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
- 34,
- "1.1"
- ),
- DidKeyTestCase.create(
- "did:key:0.7:z6MkicdicToW5HbxPP7zZV1H7RHvXgRMhoujWAF2n5WQkdd2",
- 34,
- "0.7"
- ),
- };
-
-}
diff --git a/src/test/java/com/apicatalog/did/key/DidKeyTestCase.java b/src/test/java/com/apicatalog/did/key/DidKeyTestCase.java
deleted file mode 100644
index 02bef10..0000000
--- a/src/test/java/com/apicatalog/did/key/DidKeyTestCase.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.apicatalog.did.key;
-
-import java.net.URI;
-
-public class DidKeyTestCase {
-
- URI uri;
- boolean negative;
- int keyLength;
- String version;
-
- static DidKeyTestCase create(String uri, int length) {
- return create(uri, length, "1");
- }
-
- static DidKeyTestCase create(String uri, int length, String version) {
- DidKeyTestCase testCase = new DidKeyTestCase();
- testCase.uri = URI.create(uri);
- testCase.negative = false;
- testCase.keyLength = length;
- testCase.version = version;
-
- return testCase;
-
- }
-
- static DidKeyTestCase create(String uri) {
- DidKeyTestCase testCase = new DidKeyTestCase();
- testCase.uri = uri != null ? URI.create(uri) : null ;
- testCase.negative = true;
-
- return testCase;
- }
-
-}