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
2 changes: 1 addition & 1 deletion .github/workflows/java-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:

# Offline unit tests (mock HTTP backend): prove the retry/error/signature logic without the API.
- name: Unit tests
run: mvn -B test -Dtest='UnitHttpTest,ReviewFixesTest,ClientMetaTest,SignatureTest,ConfigTest' -DfailIfNoTests=true
run: mvn -B test -Dtest='UnitHttpTest,ReviewFixesTest,ClientMetaTest,SignatureTest,ConfigTest,CompressionTest' -DfailIfNoTests=true

# Fail fast if the integration-test secret is missing or empty. Without this guard the
# integration tests silently "pass" (JUnit assumptions skip them when APIFY_TOKEN is unset),
Expand Down
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ All notable changes to the Apify Java client are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.3] - 2026-07-10

### Added

- Request-body compression now prefers brotli (`Content-Encoding: br`), matching the reference JS
client, via the `brotli4j` native codec. Gzip (`Content-Encoding: gzip`) remains the fallback when
no brotli native binary is available for the running platform.

### Changed

- Extended the `User-Agent` OS-token mapping test to assert the emitted token exactly matches the
reference JS client's `os.platform()` value for every platform the JVM can run on (`sunos`,
`freebsd`, `openbsd`, `aix`, in addition to `linux`, `darwin`, `win32`, `android`).

## [0.1.2] - 2026-07-09

### Changed

- Verified the client against OpenAPI specification version `v2-2026-07-08T143931Z` and bumped
`Version.API_SPEC_VERSION` accordingly.
- Aligned the `User-Agent` OS token with the reference JS client's `os.platform()` token: it now
uses the short, lowercase platform identifier (`linux`, `darwin`, `win32`, `android`, …) instead
of the human-readable `os.name` value.

### Added

- Request bodies of 1024 bytes or more are now gzip-compressed and sent with
`Content-Encoding: gzip`.

## [0.1.1] - 2026-07-07

### Changed
Expand Down
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,49 @@ Maven:
<dependency>
<groupId>com.apify</groupId>
<artifactId>apify-client</artifactId>
<version>0.1.1</version>
<version>0.1.3</version>
</dependency>
```

Gradle:

```groovy
implementation 'com.apify:apify-client:0.1.1'
implementation 'com.apify:apify-client:0.1.3'
```

## Quick start

A complete, copy-pasteable first program (save as `HelloApify.java`). Populate a `lib/` directory
with the client and its runtime dependencies (from a directory whose `pom.xml` declares the
dependency shown in [Installation](#installation) above), then compile and run against the JVM's
`lib/*` classpath wildcard — quote it so the shell does not expand it:

```bash
# 1. Collect apify-client and its runtime dependencies (Jackson, brotli4j codecs, …) into lib/.
mvn dependency:copy-dependencies -DoutputDirectory=lib -DincludeScope=runtime

# 2. Compile and run. '.' is for the compiled HelloApify.class; lib/* is the JVM classpath wildcard.
javac -cp 'lib/*' HelloApify.java
java -cp '.:lib/*' HelloApify # Windows: java -cp ".;lib/*" HelloApify
```

```java
import com.apify.client.ApifyClient;
import com.apify.client.ActorRun;
import com.apify.client.ActorStartOptions;

class HelloApify {
public static void main(String[] args) {
ApifyClient client = ApifyClient.create("my-api-token");
ActorRun run = client.actor("apify/hello-world").call(null, new ActorStartOptions(), 120L);
System.out.println("Run " + run.getId() + " finished with status " + run.getStatus());
}
}
```

The remaining snippets below are fragments that assume a configured `client` (see the imports note
after the next block):

```java
ApifyClient client = ApifyClient.create("my-api-token");

Expand All @@ -43,7 +74,7 @@ ActorRun run = client.actor("apify/hello-world").call(null, new ActorStartOption
// Read items from the run's default dataset.
PaginationList<JsonNode> items =
client.dataset(run.getDefaultDatasetId()).listItems(new DatasetListItemsOptions());
System.out.println("Item count: " + items.getCount());
System.out.println("Items in this page: " + items.getCount());
```

All public client types live in the `com.apify.client` package (e.g. `import com.apify.client.*;`).
Expand Down Expand Up @@ -122,9 +153,9 @@ try {

## Versioning

- `Version.CLIENT_VERSION` — the semantic version of this client (`0.1.1`).
- `Version.CLIENT_VERSION` — the semantic version of this client (`0.1.3`).
- `Version.API_SPEC_VERSION` — the Apify OpenAPI specification version this client was verified
against (`v2-2026-07-07T132551Z`).
against (`v2-2026-07-08T143931Z`).

Changes to the public interface other than additive ones are considered breaking changes and follow
[Semantic Versioning](https://semver.org/).
Expand Down
8 changes: 6 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
> in production and report issues on the repository.

This directory documents the public API of the Apify Java client, organized by resource. Each page
lists the available methods with their parameters and short, runnable snippets. For an overview,
configuration, error handling and the full resource table, see the [top-level README](../README.md).
lists the available methods with their parameters and short snippets. The snippets are code
fragments that assume a configured `client` and the imports listed below, not standalone `main`
programs; for complete, runnable programs see [examples.md](examples.md) and
[`src/test/java/com/apify/client/examples/`](../src/test/java/com/apify/client/examples). For an
overview, configuration, error handling and the full resource table, see the
[top-level README](../README.md).

All snippets assume a configured client:

Expand Down
12 changes: 7 additions & 5 deletions docs/examples.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
# Runnable examples

Each example below is a self-contained snippet assuming a configured `client`. The same programs
live under [`src/test/java/com/apify/client/examples/`](../src/test/java/com/apify/client/examples)
and are executed end-to-end against the live API by the `Test examples` CI step (see
`ExamplesTest`), so they are guaranteed to stay runnable.
Each example below is a code fragment that assumes a configured `client` and the imports listed in
the [documentation index](README.md#imports-and-dependencies); it is not a standalone `main`. The
complete, runnable programs live under
[`src/test/java/com/apify/client/examples/`](../src/test/java/com/apify/client/examples) and are
executed end-to-end against the live API by the `Test examples` CI step (see `ExamplesTest`), so
they are guaranteed to stay runnable.

## Run a store Actor and read its default dataset

```java
ActorRun run = client.actor("apify/hello-world").call(null, new ActorStartOptions(), 120L);
PaginationList<JsonNode> items =
client.dataset(run.getDefaultDatasetId()).listItems(new DatasetListItemsOptions());
System.out.println("Item count: " + items.getCount());
System.out.println("Items in this page: " + items.getCount());
```

## Each storage: create, push, read
Expand Down
43 changes: 42 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.apify</groupId>
<artifactId>apify-client</artifactId>
<version>0.1.1</version>
<version>0.1.3</version>
<packaging>jar</packaging>

<name>Apify Java Client</name>
Expand Down Expand Up @@ -41,6 +41,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.17.2</jackson.version>
<junit.version>5.10.2</junit.version>
<brotli4j.version>1.23.0</brotli4j.version>
</properties>

<dependencies>
Expand All @@ -55,6 +56,46 @@
<version>${jackson.version}</version>
</dependency>

<!-- Brotli request-body compression. The client prefers brotli (Content-Encoding: br) and
falls back to gzip when no native codec is available for the running platform. The core
artifact provides the Java API; the native codecs are pulled in per platform at runtime
scope. Platforms without a bundled native transparently use the JDK's gzip codec. -->
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>brotli4j</artifactId>
<version>${brotli4j.version}</version>
</dependency>
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>native-linux-x86_64</artifactId>
<version>${brotli4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>native-linux-aarch64</artifactId>
<version>${brotli4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>native-osx-x86_64</artifactId>
<version>${brotli4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>native-osx-aarch64</artifactId>
<version>${brotli4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>
<artifactId>native-windows-x86_64</artifactId>
<version>${brotli4j.version}</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand Down
53 changes: 52 additions & 1 deletion src/main/java/com/apify/client/ApifyClientBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ static boolean defaultIsAtHome() {
* ApifyClient/{version} ({os}; Java/{javaVersion}); isAtHome/{true|false}}.
*/
static String buildUserAgent(String suffix, BooleanSupplier isAtHomeFn) {
String os = System.getProperty("os.name", "unknown").toLowerCase(java.util.Locale.ROOT);
String os = platformToken();
String javaVersion = System.getProperty("java.version", "unknown");
String atHome = isAtHomeFn.getAsBoolean() ? "true" : "false";
String ua =
Expand All @@ -150,4 +150,55 @@ static String buildUserAgent(String suffix, BooleanSupplier isAtHomeFn) {
}
return ua;
}

/**
* Returns the short, lowercase OS token used in the {@code User-Agent}. It matches the
* identifiers emitted by the reference JS client's {@code os.platform()} (Node) — {@code linux},
* {@code darwin}, {@code win32}, {@code android}, etc. — so all Apify clients report the platform
* uniformly. Java's {@code os.name} system property returns human-readable names ({@code Linux},
* {@code Mac OS X}, {@code Windows 10}), so it is mapped to the aligned token here.
*/
static String platformToken() {
return platformToken(System.getProperty("os.name", ""), System.getProperty("java.vm.name", ""));
}

/**
* Maps the given {@code os.name} / {@code java.vm.name} property values to the aligned platform
* token. Split out from {@link #platformToken()} as a pure function so the mapping can be tested
* for every platform without depending on the host the tests run on.
*/
static String platformToken(String osName, String vmName) {
// Android runs on a Linux kernel (os.name == "Linux"); detect it via its VM name ("Dalvik").
String vm = vmName.toLowerCase(java.util.Locale.ROOT);
if (vm.contains("dalvik")) {
return "android";
}
String os = osName.toLowerCase(java.util.Locale.ROOT);
// macOS/Darwin is checked before Windows because the literal "darwin" contains "win"; the
// reverse order would misclassify a "Darwin" os.name as "win32".
if (os.contains("mac") || os.contains("darwin")) {
return "darwin";
}
if (os.contains("win")) {
return "win32";
}
if (os.contains("nux") || os.contains("nix")) {
return "linux";
}
if (os.contains("aix")) {
return "aix";
}
if (os.contains("sunos") || os.contains("solaris")) {
return "sunos";
}
if (os.contains("freebsd")) {
return "freebsd";
}
if (os.contains("openbsd")) {
return "openbsd";
}
// Fallback: the first whitespace-delimited word, lowercased, to keep the token short and
// stable.
return os.isEmpty() ? "unknown" : os.split("\\s+")[0];
}
}
Loading
Loading