|
25 | 25 | import com.inrupt.client.Client; |
26 | 26 | import com.inrupt.client.ClientCache; |
27 | 27 | import com.inrupt.client.ClientProvider; |
| 28 | +import com.inrupt.client.Headers; |
28 | 29 | import com.inrupt.client.Request; |
29 | 30 | import com.inrupt.client.Response; |
30 | 31 | import com.inrupt.client.auth.Session; |
@@ -347,6 +348,110 @@ public CompletionStage<AccessCredentialVerification> verify(final AccessCredenti |
347 | 348 | }); |
348 | 349 | } |
349 | 350 |
|
| 351 | + /** |
| 352 | + * Perform an Access Credentials query and return a page of access credentials. |
| 353 | + * |
| 354 | + * @param <T> the credential type |
| 355 | + * @param filter the query filter |
| 356 | + * @return the page of query results |
| 357 | + */ |
| 358 | + public <T extends AccessCredential> CompletionStage<CredentialResult<T>> query(final CredentialFilter<T> filter) { |
| 359 | + final Class<T> clazz = filter.getCredentialType(); |
| 360 | + final Set<String> supportedTypes; |
| 361 | + if (AccessGrant.class.isAssignableFrom(clazz)) { |
| 362 | + supportedTypes = ACCESS_GRANT_TYPES; |
| 363 | + } else if (AccessRequest.class.isAssignableFrom(clazz)) { |
| 364 | + supportedTypes = ACCESS_REQUEST_TYPES; |
| 365 | + } else if (AccessDenial.class.isAssignableFrom(clazz)) { |
| 366 | + supportedTypes = ACCESS_DENIAL_TYPES; |
| 367 | + } else { |
| 368 | + throw new AccessGrantException("Unsupported type " + clazz + " in query request"); |
| 369 | + } |
| 370 | + |
| 371 | + return v1Metadata().thenCompose(metadata -> { |
| 372 | + // TODO check that query endpoint is nonnull |
| 373 | + final Request req = Request.newBuilder(filter.asURI(metadata.queryEndpoint)).GET().build(); |
| 374 | + return client.send(req, Response.BodyHandlers.ofInputStream()).thenApply(response -> { |
| 375 | + try (final InputStream input = response.body()) { |
| 376 | + if (isSuccess(response.statusCode())) { |
| 377 | + final Map<String, CredentialFilter<T>> links = processFilterResponseHeaders(response.headers(), |
| 378 | + filter); |
| 379 | + final List<T> items = processFilterResponseBody(input, supportedTypes, clazz); |
| 380 | + return new CredentialResult<>(items, links.get("first"), links.get("prev"), |
| 381 | + links.get("next"), links.get("last")); |
| 382 | + } else { |
| 383 | + throw new AccessGrantException("Error querying access grant: HTTP response " + |
| 384 | + response.statusCode()); |
| 385 | + } |
| 386 | + } catch (final IOException ex) { |
| 387 | + throw new AccessGrantException( |
| 388 | + "Unexpected I/O exception while processing Access Grant query", ex); |
| 389 | + } |
| 390 | + }); |
| 391 | + }); |
| 392 | + } |
| 393 | + |
| 394 | + <T extends AccessCredential> Map<String, CredentialFilter<T>> processFilterResponseHeaders(final Headers headers, |
| 395 | + final CredentialFilter<T> filter) { |
| 396 | + final Map<String, CredentialFilter<T>> links = new HashMap<>(); |
| 397 | + final List<String> linkHeaders = headers.allValues("Link"); |
| 398 | + if (!linkHeaders.isEmpty()) { |
| 399 | + Headers.Link.parse(linkHeaders.toArray(linkHeaders.toArray(new String[0]))) |
| 400 | + .forEach(link -> { |
| 401 | + final String rel = link.getParameter("rel"); |
| 402 | + final URI uri = link.getUri(); |
| 403 | + if (rel != null && uri != null) { |
| 404 | + final String page = getPageQueryParam(uri); |
| 405 | + links.put(rel, CredentialFilter.newBuilder(filter).page(page) |
| 406 | + .build(filter.getCredentialType())); |
| 407 | + } |
| 408 | + }); |
| 409 | + } |
| 410 | + return links; |
| 411 | + } |
| 412 | + |
| 413 | + static String getPageQueryParam(final URI uri) { |
| 414 | + final String params = uri.getQuery(); |
| 415 | + if (params != null) { |
| 416 | + for (final String param : params.split("&")) { |
| 417 | + final String parts[] = param.split("=", 2); |
| 418 | + if (parts.length == 2 && "page".equals(parts[0])) { |
| 419 | + return parts[1]; |
| 420 | + } |
| 421 | + } |
| 422 | + } |
| 423 | + return null; |
| 424 | + } |
| 425 | + |
| 426 | + @SuppressWarnings("unchecked") |
| 427 | + <T extends AccessCredential> List<T> processFilterResponseBody(final InputStream input, |
| 428 | + final Set<String> validTypes, final Class<T> clazz) throws IOException { |
| 429 | + |
| 430 | + final List<T> items = new ArrayList<>(); |
| 431 | + final List<Object> data = jsonService.fromJson(input, |
| 432 | + new ArrayList<Object>(){}.getClass().getGenericSuperclass()); |
| 433 | + for (final Object item : data) { |
| 434 | + Utils.asMap(item).ifPresent(credential -> |
| 435 | + Utils.asSet(credential.get(TYPE)).ifPresent(types -> { |
| 436 | + types.retainAll(validTypes); |
| 437 | + if (!types.isEmpty()) { |
| 438 | + final Map<String, Object> presentation = new HashMap<>(); |
| 439 | + presentation.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI)); |
| 440 | + presentation.put(TYPE, Arrays.asList("VerifiablePresentation")); |
| 441 | + presentation.put(VERIFIABLE_CREDENTIAL, Arrays.asList(credential)); |
| 442 | + if (AccessGrant.class.equals(clazz)) { |
| 443 | + items.add((T) AccessGrant.of(new String(serialize(presentation), UTF_8))); |
| 444 | + } else if (AccessRequest.class.equals(clazz)) { |
| 445 | + items.add((T) AccessRequest.of(new String(serialize(presentation), UTF_8))); |
| 446 | + } else if (AccessDenial.class.equals(clazz)) { |
| 447 | + items.add((T) AccessDenial.of(new String(serialize(presentation), UTF_8))); |
| 448 | + } |
| 449 | + } |
| 450 | + })); |
| 451 | + } |
| 452 | + return items; |
| 453 | + } |
| 454 | + |
350 | 455 | /** |
351 | 456 | * Perform an Access Credentials query and returns 0 to N matching access credentials. |
352 | 457 | * |
@@ -404,7 +509,7 @@ private <T extends AccessCredential> CompletionStage<List<T>> query(final URI re |
404 | 509 | final List<T> responses = new ArrayList<>(); |
405 | 510 | for (final Map<String, Object> data : |
406 | 511 | buildQuery(config.getIssuer(), type, resource, creator, recipient, purposes, modes)) { |
407 | | - final Request req = Request.newBuilder(metadata.queryEndpoint) |
| 512 | + final Request req = Request.newBuilder(metadata.deriveEndpoint) |
408 | 513 | .header(CONTENT_TYPE, APPLICATION_JSON) |
409 | 514 | .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build(); |
410 | 515 | final Response<InputStream> response = client.send(req, Response.BodyHandlers.ofInputStream()) |
@@ -572,7 +677,8 @@ CompletionStage<Metadata> v1Metadata() { |
572 | 677 | }) |
573 | 678 | .thenApply(metadata -> { |
574 | 679 | final Metadata m = new Metadata(); |
575 | | - m.queryEndpoint = asUri(metadata.get("derivationService")); |
| 680 | + m.deriveEndpoint = asUri(metadata.get("derivationService")); |
| 681 | + m.queryEndpoint = asUri(metadata.get("queryService")); |
576 | 682 | m.issueEndpoint = asUri(metadata.get("issuerService")); |
577 | 683 | m.verifyEndpoint = asUri(metadata.get("verifierService")); |
578 | 684 | m.statusEndpoint = asUri(metadata.get("statusService")); |
|
0 commit comments