From c64f7ab059f1462624306dee4296d88fc7178606 Mon Sep 17 00:00:00 2001 From: rafael <36054+rferreira@users.noreply.github.com> Date: Fri, 1 May 2026 17:25:48 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20v2.2=20API=20surface=20?= =?UTF-8?q?=E2=80=94=20retrieveTrace,=20processFromUrl,=20deprecate=20erro?= =?UTF-8?q?r=20field?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScaniiTraceResult model (resourceId, List) wired into JSON parser - retrieveTrace(String id): GET /v2.2/files/{id}/trace, returns Optional - processFromUrl(URI location): POST /v2.2/files with location multipart field, returns ScaniiProcessingResult - ScaniiProcessingResult.getError/setError deprecated since 8.1.0 - Three integration tests: testRetrieveTrace, testRetrieveTraceUnknownId, testProcessFromUrl - pom.xml bumped to 8.1.0; README and CHANGELOG updated Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 17 +++++ README.md | 24 +++++- pom.xml | 2 +- src/main/java/com/scanii/ScaniiClient.java | 38 +++++++++- .../scanii/internal/DefaultScaniiClient.java | 40 ++++++++++ src/main/java/com/scanii/internal/JSON.java | 22 ++++++ .../scanii/models/ScaniiProcessingResult.java | 12 +++ .../com/scanii/models/ScaniiTraceResult.java | 75 +++++++++++++++++++ .../java/com/scanii/ScaniiClientTest.java | 37 +++++++++ 9 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/scanii/models/ScaniiTraceResult.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a040480..db6d4bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [8.1.0] — 2026-05-01 + +### Added + +- `retrieveTrace(String id)` — retrieves an ordered list of processing events for a given result id + (`GET /v2.2/files/{id}/trace`). Returns `Optional`, empty on 404. + Preview: the trace endpoint may shift before being marked stable. +- `processFromUrl(URI location)` / `processFromUrl(URI location, Map metadata)` — + submits a remote URL for synchronous processing (`POST /v2.2/files` with `location` field). +- `ScaniiTraceResult` model with inner `ScaniiTraceEvent` (timestamp, message). + +### Deprecated + +- `ScaniiProcessingResult.getError()` / `setError()` — the `error` field in the JSON response is + deprecated in the v2.2 spec. Error conditions are signalled via `ScaniiException`. Will be removed + in a future major version. + ## [8.0.0] — 2026-04-23 ### Breaking changes diff --git a/README.md b/README.md index 7f72376..cdbd4ea 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Official Java SDK for the [Scanii](https://www.scanii.com) content processing AP com.scanii scanii-java - 8.0.0 + 8.1.0 ``` @@ -31,6 +31,28 @@ ScaniiProcessingResult result = client.process(Paths.get("/path/to/file")); System.out.printf("findings: %s%n", result.getFindings()); ``` +## API reference + +| Method | Description | +|---|---| +| `process(Path content)` | Synchronous file scan | +| `process(InputStream content)` | Synchronous stream scan | +| `process(Path content, Map metadata)` | Scan with metadata | +| `process(Path content, String callback, Map metadata)` | Scan with callback | +| `processAsync(Path content)` | Async-on-server scan, returns pending result | +| `processFromUrl(URI location)` | Synchronous remote-URL scan | +| `processFromUrl(URI location, Map metadata)` | Remote-URL scan with metadata | +| `retrieve(String id)` | Retrieve previous scan result | +| `retrieveTrace(String id)` | Retrieve ordered processing events for a result (preview) | +| `fetch(String location)` | Server-side async fetch-and-scan of a remote URL | +| `ping()` | Health check | +| `createAuthToken(int timeout, TimeUnit unit)` | Mint short-lived auth token | +| `retrieveAuthToken(String id)` | Inspect auth token | +| `deleteAuthToken(String id)` | Revoke auth token | +| `retrieveAccountInfo()` | Retrieve account information | + +See the [API spec](https://scanii.github.io/openapi/v22/) for full details. + ## Regional endpoints | Constant | Endpoint | diff --git a/pom.xml b/pom.xml index 7459b19..1bedf30 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.scanii scanii-java - 1.0.0-SNAPSHOT + 8.1.0 jar Scanii.com Java SDK scanii-java diff --git a/src/main/java/com/scanii/ScaniiClient.java b/src/main/java/com/scanii/ScaniiClient.java index 0bbe663..daae1b8 100644 --- a/src/main/java/com/scanii/ScaniiClient.java +++ b/src/main/java/com/scanii/ScaniiClient.java @@ -4,8 +4,10 @@ import com.scanii.models.ScaniiAuthToken; import com.scanii.models.ScaniiPendingResult; import com.scanii.models.ScaniiProcessingResult; +import com.scanii.models.ScaniiTraceResult; import java.io.InputStream; +import java.net.URI; import java.nio.file.Path; import java.util.Map; import java.util.Optional; @@ -123,13 +125,47 @@ public interface ScaniiClient { ScaniiPendingResult processAsync(InputStream content); /** - * Fetches the results of a previously processed file @see https://docs.scanii.com/v2.2/resources.html#files + * Fetches the results of a previously processed file @see spec * * @param id id of the content/file to be retrieved * @return optional {@link ScaniiProcessingResult} */ Optional retrieve(String id); + /** + * Retrieves the processing trace for a previously processed file. + * Returns an ordered list of events describing each stage of the processing pipeline. + * + *

Preview: the trace endpoint is marked preview in the v2.2 spec — + * the API surface may shift before it is marked stable. + * + * @param id id of the previously processed content + * @return optional {@link ScaniiTraceResult}, empty if the id is not found + * @see spec + */ + Optional retrieveTrace(String id); + + /** + * Submits a remote URL for synchronous processing. The Scanii service fetches the content + * at the given URL and scans it, returning the result immediately. + * + * @param location URI of the remote content to process + * @param metadata optional metadata to be attached to this result + * @return scanii result {@link ScaniiProcessingResult} + * @see spec + */ + ScaniiProcessingResult processFromUrl(URI location, Map metadata); + + /** + * Submits a remote URL for synchronous processing. The Scanii service fetches the content + * at the given URL and scans it, returning the result immediately. + * + * @param location URI of the remote content to process + * @return scanii result {@link ScaniiProcessingResult} + * @see spec + */ + ScaniiProcessingResult processFromUrl(URI location); + /** * Makes a fetch call to scanii @see https://docs.scanii.com/v2.2/resources.html#files * diff --git a/src/main/java/com/scanii/internal/DefaultScaniiClient.java b/src/main/java/com/scanii/internal/DefaultScaniiClient.java index ef48943..e8fe499 100644 --- a/src/main/java/com/scanii/internal/DefaultScaniiClient.java +++ b/src/main/java/com/scanii/internal/DefaultScaniiClient.java @@ -187,6 +187,46 @@ public Optional retrieve(String id) { return Optional.of(result); } + @Override + public Optional retrieveTrace(String id) { + Objects.requireNonNull(id, "resource id cannot be null"); + + HttpRequest req = buildGet(target.resolve("/v2.2/files/" + id + "/trace")); + HttpResponse response = send(req); + + if (response.statusCode() == 404) { + return Optional.empty(); + } + + if (response.statusCode() != 200) { + parseAndThrowError(response); + } + + ScaniiTraceResult result = JSON.load(response.body(), ScaniiTraceResult.class); + extractRequestMetadata(result, response); + result.setRawResponse(response.body()); + + return Optional.of(result); + } + + @Override + public ScaniiProcessingResult processFromUrl(URI location, Map metadata) { + Objects.requireNonNull(location, "location cannot be null"); + Objects.requireNonNull(metadata, "metadata cannot be null"); + + MultipartBodyPublisher multipart = new MultipartBodyPublisher() + .addTextBody("location", location.toString()); + metadata.forEach((k, v) -> multipart.addTextBody(String.format("metadata[%s]", k), v)); + + HttpRequest req = buildPost(target.resolve("/v2.2/files"), multipart); + return processResponse(req); + } + + @Override + public ScaniiProcessingResult processFromUrl(URI location) { + return processFromUrl(location, Collections.emptyMap()); + } + @Override public ScaniiPendingResult fetch(String location) { return fetch(location, null, Collections.emptyMap()); diff --git a/src/main/java/com/scanii/internal/JSON.java b/src/main/java/com/scanii/internal/JSON.java index d817e9b..2bad37b 100644 --- a/src/main/java/com/scanii/internal/JSON.java +++ b/src/main/java/com/scanii/internal/JSON.java @@ -4,6 +4,7 @@ import com.scanii.models.ScaniiAuthToken; import com.scanii.models.ScaniiPendingResult; import com.scanii.models.ScaniiProcessingResult; +import com.scanii.models.ScaniiTraceResult; import java.time.Instant; import java.util.*; @@ -18,6 +19,7 @@ class JSON { READERS.put(ScaniiProcessingResult.class, JSON::toProcessingResult); READERS.put(ScaniiAuthToken.class, JSON::toAuthToken); READERS.put(ScaniiAccountInfo.class, JSON::toAccountInfo); + READERS.put(ScaniiTraceResult.class, JSON::toTraceResult); } @SuppressWarnings("unchecked") @@ -55,6 +57,26 @@ private static ScaniiProcessingResult toProcessingResult(Map m) return r; } + @SuppressWarnings("unchecked") + private static ScaniiTraceResult toTraceResult(Map m) { + ScaniiTraceResult r = new ScaniiTraceResult(); + r.setResourceId(str(m, "id")); + List eventsRaw = (List) m.get("events"); + if (eventsRaw != null) { + List events = new ArrayList<>(eventsRaw.size()); + for (Object o : eventsRaw) { + Map em = (Map) o; + ScaniiTraceResult.ScaniiTraceEvent e = new ScaniiTraceResult.ScaniiTraceEvent(); + e.setMessage(str(em, "message")); + String ts = str(em, "timestamp"); + if (ts != null) e.setTimestamp(Instant.parse(ts)); + events.add(e); + } + r.setEvents(events); + } + return r; + } + private static ScaniiAuthToken toAuthToken(Map m) { ScaniiAuthToken r = new ScaniiAuthToken(); r.setResourceId(str(m, "id")); diff --git a/src/main/java/com/scanii/models/ScaniiProcessingResult.java b/src/main/java/com/scanii/models/ScaniiProcessingResult.java index ba7ad7c..bc3d0a7 100644 --- a/src/main/java/com/scanii/models/ScaniiProcessingResult.java +++ b/src/main/java/com/scanii/models/ScaniiProcessingResult.java @@ -16,10 +16,22 @@ public class ScaniiProcessingResult extends ScaniiResult { private Map metadata = new HashMap<>(); private String error; + /** + * @deprecated The {@code error} field is deprecated as of 8.1.0. Use {@link #getFindings()} to + * inspect scan results; error conditions are now signalled via {@link com.scanii.ScaniiException}. + * Will be removed in a future major version. + */ + @Deprecated(since = "8.1.0") public String getError() { return error; } + /** + * @deprecated The {@code error} field is deprecated as of 8.1.0. Use {@link #getFindings()} to + * inspect scan results; error conditions are now signalled via {@link com.scanii.ScaniiException}. + * Will be removed in a future major version. + */ + @Deprecated(since = "8.1.0") public void setError(String error) { this.error = error; } diff --git a/src/main/java/com/scanii/models/ScaniiTraceResult.java b/src/main/java/com/scanii/models/ScaniiTraceResult.java new file mode 100644 index 0000000..61f355b --- /dev/null +++ b/src/main/java/com/scanii/models/ScaniiTraceResult.java @@ -0,0 +1,75 @@ +package com.scanii.models; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +/** + * Result of a {@link com.scanii.ScaniiClient#retrieveTrace(String)} call, + * containing an ordered list of processing events for a given processing id. + * + *

Preview: the trace endpoint ({@code GET /v2.2/files/{id}/trace}) + * is marked preview in the v2.2 spec — the API surface may shift before it is marked stable. + * + * @see spec + */ +public class ScaniiTraceResult extends ScaniiResult { + private String resourceId; + private List events = new ArrayList<>(); + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events; + } + + @Override + public String toString() { + return "ScaniiTraceResult{" + + "resourceId='" + resourceId + '\'' + + ", events=" + events + + '}'; + } + + /** + * A single event in a processing trace. + */ + public static class ScaniiTraceEvent { + private Instant timestamp; + private String message; + + public Instant getTimestamp() { + return timestamp; + } + + public void setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return "ScaniiTraceEvent{" + + "timestamp=" + timestamp + + ", message='" + message + '\'' + + '}'; + } + } +} diff --git a/src/test/java/com/scanii/ScaniiClientTest.java b/src/test/java/com/scanii/ScaniiClientTest.java index f4d9af5..9695614 100644 --- a/src/test/java/com/scanii/ScaniiClientTest.java +++ b/src/test/java/com/scanii/ScaniiClientTest.java @@ -5,15 +5,18 @@ import com.scanii.models.ScaniiAuthToken; import com.scanii.models.ScaniiPendingResult; import com.scanii.models.ScaniiProcessingResult; +import com.scanii.models.ScaniiTraceResult; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; import java.io.FileInputStream; +import java.net.URI; import java.nio.file.Files; import java.time.Duration; import java.util.HashMap; +import java.util.Optional; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; @@ -178,6 +181,40 @@ void testRetrieveAuthToken() { assertEquals(result.getResourceId(), result2.getResourceId()); } + @Test + void testRetrieveTrace() throws Exception { + ScaniiProcessingResult processed = client.process(Systems.randomFile(1024)); + assertNotNull(processed.getResourceId()); + + Optional opt = client.retrieveTrace(processed.getResourceId()); + assertTrue(opt.isPresent()); + ScaniiTraceResult trace = opt.get(); + assertEquals(processed.getResourceId(), trace.getResourceId()); + assertNotNull(trace.getEvents()); + assertFalse(trace.getEvents().isEmpty()); + for (ScaniiTraceResult.ScaniiTraceEvent event : trace.getEvents()) { + assertNotNull(event.getTimestamp()); + assertNotNull(event.getMessage()); + } + } + + @Test + void testRetrieveTraceUnknownId() { + Optional opt = client.retrieveTrace("doesnotexist"); + assertTrue(opt.isEmpty()); + } + + @Test + void testProcessFromUrl() throws Exception { + // scanii-cli serves the EICAR file at this well-known path + ScaniiProcessingResult result = client.processFromUrl(URI.create(ENDPOINT + "/static/eicar.txt")); + assertNotNull(result.getResourceId()); + assertNotNull(result.getChecksum()); + assertNotNull(result.getRequestId()); + assertNotNull(result.getHostId()); + assertNotNull(result.getFindings()); + } + /** * TODO: callback integration test — requires scanii-cli callback support. * See RAFAEL_CHECKLIST.md §1.6 for the prerequisite work. Once scanii-cli From c8f3be74a767167b0d95df44db8bd5041e4ef1bd Mon Sep 17 00:00:00 2001 From: rafael <36054+rferreira@users.noreply.github.com> Date: Fri, 1 May 2026 17:26:14 -0400 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20revert=20pom.xml=20version=20to=20?= =?UTF-8?q?snapshot=20=E2=80=94=20updated=20by=20release=20pipeline=20befo?= =?UTF-8?q?re=20publish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1bedf30..7459b19 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.scanii scanii-java - 8.1.0 + 1.0.0-SNAPSHOT jar Scanii.com Java SDK scanii-java