Skip to content

Commit a9d6a6d

Browse files
committed
Add VerificationMethodException
1 parent c58b6d4 commit a9d6a6d

3 files changed

Lines changed: 213 additions & 39 deletions

File tree

src/main/java/com/apicatalog/cid/IdentifierDocumentResolver.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,41 @@
44

55
import com.apicatalog.cid.document.IdentifierDocument;
66

7+
/**
8+
* Resolves {@link IdentifierDocument} instances for given identifiers.
9+
*
10+
* <p>
11+
* An {@code IdentifierDocumentResolver} provides the mechanism to determine
12+
* whether it can handle a specific identifier and, if so, resolve that
13+
* identifier to its corresponding {@link IdentifierDocument}.
14+
* </p>
15+
*
16+
* <p>
17+
* This abstraction allows multiple resolver implementations to coexist, each
18+
* supporting different identifier schemes, persistence layers, or resolution
19+
* protocols.
20+
* </p>
21+
*
22+
* @see <a href="https://www.w3.org/TR/cid-1.0/">W3C Controlled Identifiers
23+
* 1.0</a>
24+
*/
725
public interface IdentifierDocumentResolver {
826

27+
/**
28+
* Checks whether this resolver accepts the given identifier.
29+
*
30+
* @param id the identifier to test (must not be {@code null})
31+
* @return {@code true} if this resolver can resolve the identifier,
32+
* {@code false} otherwise
33+
*/
934
boolean isAccepted(URI id);
10-
35+
36+
/**
37+
* Resolves the given identifier into a {@link IdentifierDocument}.
38+
*
39+
* @param id the identifier to resolve (must not be {@code null})
40+
* @return the resolved {@link IdentifierDocument}
41+
* @throws IllegalArgumentException if the identifier cannot be resolved
42+
*/
1143
IdentifierDocument resolve(URI id);
12-
1344
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.apicatalog.cid;
2+
3+
/**
4+
* Thrown when resolution of a verification method fails.
5+
*
6+
* <p>
7+
* The {@link Code} enum indicates the reason for failure, allowing callers to
8+
* branch on error conditions.
9+
* </p>
10+
*/
11+
public class VerificationMethodException extends Exception {
12+
13+
private static final long serialVersionUID = -9089776596803069731L;
14+
15+
/** Categorical error code for resolution failures. */
16+
public enum Code {
17+
INVALID_RELATIONSHIP_FOR_VERIFICATION_METHOD,
18+
INVALID_VERIFICATION_METHOD,
19+
INVALID_CONTROLLER_DOCUMENT,
20+
INVALID_CONTROLLER_DOCUMENT_ID,
21+
INVALID_METHOD_ID
22+
}
23+
24+
private final Code code;
25+
26+
public VerificationMethodException(Code code, String message) {
27+
super(message);
28+
this.code = code;
29+
}
30+
31+
public VerificationMethodException(Code code, String message, Throwable cause) {
32+
super(message, cause);
33+
this.code = code;
34+
}
35+
36+
/** @return the error code indicating why resolution failed */
37+
public Code getCode() {
38+
return code;
39+
}
40+
}

src/main/java/com/apicatalog/cid/VerificationMethodResolver.java

Lines changed: 140 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,32 @@
55
import java.util.Collection;
66
import java.util.HashMap;
77
import java.util.Map;
8+
import java.util.Objects;
89
import java.util.Set;
910
import java.util.function.Function;
1011

1112
import com.apicatalog.cid.document.IdentifierDocument;
1213
import com.apicatalog.cid.document.VerificationMethod;
1314

1415
/**
15-
* Retrieves a verification method from {@link IdentifierDocument}.
16+
* Resolves a {@link VerificationMethod} referenced from an
17+
* {@link IdentifierDocument}, optionally resolving the controller document from
18+
* a {@link URI}.
19+
*
20+
* <p>
21+
* Supported verification relationship IRIs:
22+
* </p>
23+
* <ul>
24+
* <li><code>https://w3id.org/security#authentication</code></li>
25+
* <li><code>https://w3id.org/security#assertionMethod</code></li>
26+
* <li><code>https://w3id.org/security#keyAgreementMethod</code></li>
27+
* <li><code>https://w3id.org/security#capabilityInvocationMethod</code></li>
28+
* <li><code>https://w3id.org/security#capabilityDelegationMethod</code></li>
29+
* </ul>
1630
*/
1731
public class VerificationMethodResolver {
1832

33+
/** Supported verification relationships → document accessors. */
1934
protected final static Map<URI, Function<IdentifierDocument, Set<VerificationMethod>>> RELS;
2035

2136
static {
@@ -34,52 +49,140 @@ public class VerificationMethodResolver {
3449

3550
protected final Collection<IdentifierDocumentResolver> resolvers;
3651

37-
public VerificationMethodResolver(Collection<IdentifierDocumentResolver> resolvers) {
52+
/**
53+
* Creates a resolver that can delegate to the given
54+
* {@link IdentifierDocumentResolver}s when a controller document must be
55+
* fetched.
56+
*
57+
* @param resolvers non-empty collection of resolvers
58+
* @throws NullPointerException if {@code resolvers} is {@code null}
59+
* @throws IllegalArgumentException if {@code resolvers} is empty
60+
*/
61+
public VerificationMethodResolver(final Collection<IdentifierDocumentResolver> resolvers) {
62+
Objects.requireNonNull(resolvers, "resolvers must not be null");
63+
if (resolvers.isEmpty()) {
64+
throw new IllegalArgumentException("resolvers must not be empty");
65+
}
3866
this.resolvers = resolvers;
3967
}
4068

41-
public VerificationMethod retrieve(final URI methodId, final URI relation) {
42-
43-
try {
44-
// remove fragment
45-
final URI documentUri = new URI(methodId.getScheme(), methodId.getSchemeSpecificPart(), null);
46-
47-
// resolve controller document
48-
final IdentifierDocument document = resolvers.stream()
49-
.filter(r -> r.isAccepted(documentUri))
50-
.findFirst()
51-
.map(r -> r.resolve(documentUri))
52-
.orElseThrow(() -> new IllegalArgumentException("INVALID_CONTROLLER_DOCUMENT"));
53-
54-
if (!document.id().equals(documentUri)) {
55-
throw new IllegalArgumentException("INVALID_CONTROLLER_DOCUMENT_ID");
56-
}
57-
58-
final Function<IdentifierDocument, Set<VerificationMethod>> methodProvider = RELS.get(relation);
59-
60-
if (methodProvider == null) {
61-
throw new IllegalArgumentException("INVALID_RELATIONSHIP_FOR_VERIFICATION_METHOD");
62-
}
69+
public VerificationMethod resolve(final URI methodId, final Set<VerificationMethod> methods, final IdentifierDocument document) throws VerificationMethodException {
70+
final VerificationMethod method = methods.stream()
71+
.filter(m -> methodId.equals(m.id()))
72+
.findFirst()
73+
.orElseThrow(() -> new VerificationMethodException(
74+
VerificationMethodException.Code.INVALID_VERIFICATION_METHOD,
75+
"Verification method not found: " + methodId));
76+
77+
if (!document.id().equals(method.controller())) {
78+
throw new VerificationMethodException(
79+
VerificationMethodException.Code.INVALID_VERIFICATION_METHOD,
80+
"Verification method controller mismatch, expected " + document.id() + " but got " + method.controller());
81+
}
6382

64-
Set<VerificationMethod> methods = methodProvider.apply(document);
83+
return method;
84+
}
6585

66-
if (methods == null || methods.isEmpty()) {
67-
throw new IllegalArgumentException("INVALID_VERIFICATION_METHOD");
68-
}
86+
/**
87+
* Resolves a verification method by {@code methodId} within the supplied
88+
* {@code document}, constrained by the verification {@code relation}.
89+
*
90+
* @param methodId the verification method identifier (must not be {@code null})
91+
* @param relation the verification relationship IRI (must not be {@code null})
92+
* @param document the controller document that lists the method (must not be
93+
* {@code null})
94+
* @return the matching {@link VerificationMethod}
95+
*
96+
* @throws VerificationMethodException with one of:
97+
* <ul>
98+
* <li>{@code INVALID_RELATIONSHIP_FOR_VERIFICATION_METHOD}</li>
99+
* <li>{@code INVALID_VERIFICATION_METHOD}</li>
100+
* </ul>
101+
* @throws NullPointerException if any argument is {@code null}
102+
*/
103+
public VerificationMethod resolve(final URI methodId, final URI relation, final IdentifierDocument document) throws VerificationMethodException {
104+
105+
Objects.requireNonNull(methodId, "methodId must not be null");
106+
Objects.requireNonNull(relation, "relation must not be null");
107+
Objects.requireNonNull(document, "document must not be null");
108+
Objects.requireNonNull(document.id(), "document.id() must not be null");
109+
110+
final Function<IdentifierDocument, Set<VerificationMethod>> methodProvider = RELS.get(relation);
111+
112+
if (methodProvider == null) {
113+
throw new VerificationMethodException(
114+
VerificationMethodException.Code.INVALID_RELATIONSHIP_FOR_VERIFICATION_METHOD,
115+
"Unsupported verification relationship: " + relation);
116+
}
69117

70-
final VerificationMethod method = methods.stream()
71-
.filter(m -> methodId.equals(m.id()))
72-
.findFirst()
73-
.orElseThrow(() -> new IllegalArgumentException("INVALID_VERIFICATION_METHOD"));
118+
final Set<VerificationMethod> methods = methodProvider.apply(document);
119+
if (methods == null || methods.isEmpty()) {
120+
throw new VerificationMethodException(
121+
VerificationMethodException.Code.INVALID_VERIFICATION_METHOD,
122+
"No verification methods for relation: " + relation);
123+
}
74124

75-
if (!documentUri.equals(method.controller())) {
76-
throw new IllegalArgumentException("INVALID_VERIFICATION_METHOD");
77-
}
125+
return resolve(methodId, methods, document);
126+
}
78127

79-
return method;
128+
/**
129+
* Resolves a verification method by {@code methodId} and {@code relation},
130+
* fetching the controller document using the first
131+
* {@link IdentifierDocumentResolver} that
132+
* {@link IdentifierDocumentResolver#isAccepted(URI) accepts} the derived
133+
* document URI.
134+
*
135+
* <p>
136+
* The document URI is derived from {@code methodId} by removing its fragment.
137+
* If no resolver accepts the URI, or the resolved document’s {@code id} does
138+
* not equal the derived URI, resolution fails.
139+
* </p>
140+
*
141+
* @param methodId the verification method identifier (must not be {@code null})
142+
* @param relation the verification relationship IRI (must not be {@code null})
143+
* @return the matching {@link VerificationMethod}
144+
*
145+
* @throws VerificationMethodException with one of:
146+
* <ul>
147+
* <li>{@code INVALID_METHOD_ID}</li>
148+
* <li>{@code INVALID_CONTROLLER_DOCUMENT}</li>
149+
* <li>{@code INVALID_CONTROLLER_DOCUMENT_ID}</li>
150+
* <li>{@code INVALID_RELATIONSHIP_FOR_VERIFICATION_METHOD}</li>
151+
* <li>{@code INVALID_VERIFICATION_METHOD}</li>
152+
* </ul>
153+
* @throws NullPointerException if any argument is {@code null}
154+
*/
155+
public VerificationMethod resolve(final URI methodId, final URI relation) throws VerificationMethodException {
156+
157+
Objects.requireNonNull(methodId, "methodId must not be null");
158+
Objects.requireNonNull(relation, "relation must not be null");
159+
160+
final URI documentUri;
161+
try {
162+
// Strip fragment to obtain the controller document identifier
163+
documentUri = new URI(methodId.getScheme(), methodId.getSchemeSpecificPart(), null);
80164

81165
} catch (URISyntaxException e) {
82-
throw new IllegalArgumentException(e);
166+
throw new VerificationMethodException(
167+
VerificationMethodException.Code.INVALID_METHOD_ID,
168+
"Invalid methodId, failed to derive controller document URI",
169+
e);
170+
}
171+
172+
final IdentifierDocument document = resolvers.stream()
173+
.filter(r -> r.isAccepted(documentUri))
174+
.findFirst()
175+
.map(r -> r.resolve(documentUri))
176+
.orElseThrow(() -> new VerificationMethodException(
177+
VerificationMethodException.Code.INVALID_CONTROLLER_DOCUMENT,
178+
"No resolver accepted controller document: " + documentUri));
179+
180+
if (!documentUri.equals(document.id())) {
181+
throw new VerificationMethodException(
182+
VerificationMethodException.Code.INVALID_CONTROLLER_DOCUMENT_ID,
183+
"Controller document id mismatch, expected " + documentUri + " but got " + document.id());
83184
}
185+
186+
return resolve(methodId, relation, document);
84187
}
85188
}

0 commit comments

Comments
 (0)