Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<ScaniiTraceResult>`, empty on 404.
Preview: the trace endpoint may shift before being marked stable.
- `processFromUrl(URI location)` / `processFromUrl(URI location, Map<String,String> 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
Expand Down
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Official Java SDK for the [Scanii](https://www.scanii.com) content processing AP
<dependency>
<groupId>com.scanii</groupId>
<artifactId>scanii-java</artifactId>
<version>8.0.0</version>
<version>8.1.0</version>
</dependency>
```

Expand All @@ -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<String,String> metadata)` | Scan with metadata |
| `process(Path content, String callback, Map<String,String> 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<String,String> 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 |
Expand Down
38 changes: 37 additions & 1 deletion src/main/java/com/scanii/ScaniiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -123,13 +125,47 @@ public interface ScaniiClient {
ScaniiPendingResult processAsync(InputStream content);

/**
* Fetches the results of a previously processed file @see <a href="https://docs.scanii.com/v2.2/resources.html#files">https://docs.scanii.com/v2.2/resources.html#files</a>
* Fetches the results of a previously processed file @see <a href="https://scanii.github.io/openapi/v22/">spec</a>
*
* @param id id of the content/file to be retrieved
* @return optional {@link ScaniiProcessingResult}
*/
Optional<ScaniiProcessingResult> 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.
*
* <p><strong>Preview:</strong> 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 <a href="https://scanii.github.io/openapi/v22/">spec</a>
*/
Optional<ScaniiTraceResult> 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 <a href="https://scanii.github.io/openapi/v22/">spec</a>
*/
ScaniiProcessingResult processFromUrl(URI location, Map<String, String> 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 <a href="https://scanii.github.io/openapi/v22/">spec</a>
*/
ScaniiProcessingResult processFromUrl(URI location);

/**
* Makes a fetch call to scanii @see <a href="https://docs.scanii.com/v2.2/resources.html#files">https://docs.scanii.com/v2.2/resources.html#files</a>
*
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/scanii/internal/DefaultScaniiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,46 @@ public Optional<ScaniiProcessingResult> retrieve(String id) {
return Optional.of(result);
}

@Override
public Optional<ScaniiTraceResult> retrieveTrace(String id) {
Objects.requireNonNull(id, "resource id cannot be null");

HttpRequest req = buildGet(target.resolve("/v2.2/files/" + id + "/trace"));
HttpResponse<String> 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<String, String> 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());
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/scanii/internal/JSON.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand All @@ -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")
Expand Down Expand Up @@ -55,6 +57,26 @@ private static ScaniiProcessingResult toProcessingResult(Map<String, Object> m)
return r;
}

@SuppressWarnings("unchecked")
private static ScaniiTraceResult toTraceResult(Map<String, Object> m) {
ScaniiTraceResult r = new ScaniiTraceResult();
r.setResourceId(str(m, "id"));
List<Object> eventsRaw = (List<Object>) m.get("events");
if (eventsRaw != null) {
List<ScaniiTraceResult.ScaniiTraceEvent> events = new ArrayList<>(eventsRaw.size());
for (Object o : eventsRaw) {
Map<String, Object> em = (Map<String, Object>) 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<String, Object> m) {
ScaniiAuthToken r = new ScaniiAuthToken();
r.setResourceId(str(m, "id"));
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/scanii/models/ScaniiProcessingResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,22 @@ public class ScaniiProcessingResult extends ScaniiResult {
private Map<String, String> 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;
}
Expand Down
75 changes: 75 additions & 0 deletions src/main/java/com/scanii/models/ScaniiTraceResult.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p><strong>Preview:</strong> 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 <a href="https://scanii.github.io/openapi/v22/">spec</a>
*/
public class ScaniiTraceResult extends ScaniiResult {
private String resourceId;
private List<ScaniiTraceEvent> events = new ArrayList<>();

public String getResourceId() {
return resourceId;
}

public void setResourceId(String resourceId) {
this.resourceId = resourceId;
}

public List<ScaniiTraceEvent> getEvents() {
return events;
}

public void setEvents(List<ScaniiTraceEvent> 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 + '\'' +
'}';
}
}
}
37 changes: 37 additions & 0 deletions src/test/java/com/scanii/ScaniiClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;
Expand Down Expand Up @@ -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<ScaniiTraceResult> 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<ScaniiTraceResult> 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
Expand Down
Loading