Skip to content

Commit f4b0080

Browse files
Address review comments on agent docs
- Taglib: drop nested <cl:transformation> examples, use attributes and the transformation attribute, fetch_format, publicId and SingletonManager behaviour - Signed uploads: the upload must send exactly the signed parameters - Configuration: secure/analytics defaults per config path, first error for unparsed credentials, analytics opt-out, default timeout - Uploads: uploadLarge accepts an InputStream, buildUploadParams keys, per-file progress, Spring examples without Map.of or temp files - Five Api methods accept an asset ID; poster uses the middle frame - AGENTS.md: shorter, matches CI, drops the http4x and contract items - README shows the released version again Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 81d6d35 commit f4b0080

13 files changed

Lines changed: 142 additions & 182 deletions

‎AGENTS.md‎

Lines changed: 49 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,29 @@ project, read [`docs/`](docs/README.md) instead — task-oriented pages for agen
55

66
## Commands
77

8-
**Use JDK 11.** The Gradle 6.0.1 wrapper cannot run on a modern JVM: under JDK 17+ it fails
9-
before any task with `Could not initialize class org.codehaus.groovy.reflection.ReflectionCache`.
8+
Use JDK 8 or 11; CI runs on JDK 8. The Gradle 6.0.1 wrapper fails on JDK 17 and later before any
9+
task runs.
1010

1111
```bash
12-
export JAVA_HOME=/path/to/jdk-11
13-
./gradlew :cloudinary-core:jar :cloudinary-http5:jar # build the two artifacts users depend on
14-
./gradlew :cloudinary-core:test # unit tests, no cloud needed
15-
./gradlew :cloudinary-http5:test -DCLOUDINARY_URL="cloudinary://key:secret@cloud" # live integration tests
16-
./gradlew createTestSubAccount -PmoduleName=http5 # throwaway cloud -> tools/cloudinary_url.txt
12+
./gradlew :cloudinary-core:jar :cloudinary-http5:jar :cloudinary-taglib:jar # build the published modules
13+
./gradlew :cloudinary-core:test # unit tests, no cloud needed
14+
./gradlew createTestSubAccount -PmoduleName=http5 # throwaway cloud -> tools/cloudinary_url.txt
15+
source tools/cloudinary_url.txt # sets CLOUDINARY_URL
16+
./gradlew ciTest -p cloudinary-http5 -DCLOUDINARY_URL="$CLOUDINARY_URL" # integration tests, as CI runs them
1717
```
1818

19-
`CLOUDINARY_URL` is read as a **system property** by the tests (see `java_shared.gradle`), not
20-
from the environment. `tools/cloudinary_url.txt` is git-ignored and **contains live
21-
credentials — never commit it or paste its contents anywhere.**
19+
Gradle passes `-DCLOUDINARY_URL` to the test JVM as the `CLOUDINARY_URL` environment variable
20+
(`java_shared.gradle`). `tools/cloudinary_url.txt` is git-ignored and contains live credentials;
21+
never commit it or paste its contents anywhere.
2222

2323
## Testing
2424

25-
- `cloudinary-core` tests are offline. Everything else needs a live product environment.
26-
- Do not share one product environment between doc verification and the integration suite.
27-
Doc probes leave mandatory metadata fields and pending moderated assets behind, and
28-
`ApiTest.testGetResourcesWithMetadata` then fails for reasons unrelated to the branch. Use a
29-
fresh sub-account for each.
30-
- For docs-only changes, run the suite against the base branch and compare failure counts
31-
rather than asserting "tests pass". The integration suite takes about 3.5 minutes plus a
32-
throwaway cloud.
33-
- Before committing a `docs/` change, two cheap checks: every `java` snippet compiles against
34-
the built jars (wrap fragments in a harness with the ambient names — `cloudinary`, `file`,
35-
`publicId` — declared as fields, then `javac`; this catches invented methods, which is the
36-
whole point), and every internal link, cross-page anchor, and external URL resolves. GitHub
37-
**preserves underscores** in heading slugs, so a naive slugifier reports false positives.
25+
- `cloudinary-core` tests run offline. `cloudinary-http5` tests need a live product environment.
26+
`cloudinary-taglib` has no tests.
27+
- Use a fresh sub-account for each integration run. Assets and metadata fields left by earlier
28+
runs can fail unrelated tests.
29+
- `java` snippets in `docs/` must compile against the built jars, and links in `docs/` must
30+
resolve.
3831

3932
## Project structure
4033

@@ -44,108 +37,76 @@ credentials — never commit it or paste its contents anywhere.**
4437
| `cloudinary-http5/` | Yes | Apache HttpClient 5 transport. **This is what users depend on.** |
4538
| `cloudinary-taglib/` | Yes | JSP tags. Targets `javax.*`, not Jakarta EE 9+. |
4639
| `cloudinary-test-common/` | Yes | Shared test helpers. Not for application code. |
47-
| `docs/` | No | Task pages for agents *using* the SDK. Every claim produced by running something. |
40+
| `docs/` | No | Task pages for agents *using* the SDK. |
4841
| `examples/` | No | One runnable program per doc page where the task is genuinely runnable. |
4942
| `samples/` | No | Legacy sample applications. Not in the build. |
5043
| `tools/` | No | Release and test-cloud scripts. |
5144

52-
`settings.gradle` includes only the four modules. The `cloudinary-http42/43/44` directories
53-
present in a working copy are **untracked leftovers** from the 1.x line — not in git, not in the
54-
build. Do not add them back and do not "tidy" them as part of unrelated work.
45+
`settings.gradle` includes the four modules above.
5546

5647
`org.cloudinary.json` inside `cloudinary-core` is a vendored JSON implementation. It exists to
5748
keep the core dependency-free. Do not replace it with Jackson or Gson.
5849

59-
**`docs/` is deliberately not packaged into any jar.** A survey of 15 widely-used Java libraries
60-
on Maven Central — Guava, OkHttp, Jackson, Gson, commons-lang3, SLF4J, Retrofit, AWS SDK v2,
61-
google-cloud-storage, Stripe, Twilio, SendGrid, java-jwt, Spring Core, jsoup — found that only
62-
jsoup ships any Markdown: its 3 KB project README in `META-INF/`. Everything else carries
63-
manifests, licences, and machine-consumed config only. Embedding these pages would grow
64-
`cloudinary-core` by 27%, from 175 KB to 223 KB, for files no runtime reads, and retrieval
65-
through `unzip -p` is slower than reading the repository or Cloudinary's `llms.txt`. Java's
66-
convention for documentation attached to an artifact is the `-javadoc.jar`, which this SDK
67-
already publishes. Reversing this needs a reason better than "other Cloudinary SDKs do it".
68-
69-
`examples/` is likewise not shipped: loose `.java` files inside a library artifact are dead
70-
weight, and each doc page carries its full flow inline. Keep examples uniform — one entry point
71-
taking no arguments, no CLI parsing, constants at the top, one error-handling block mapping every
72-
failure to a non-zero exit with a readable message and no stack trace.
50+
`docs/` and `examples/` are not packaged into any jar; the published `-javadoc.jar` is the
51+
documentation attached to each artifact.
52+
53+
Keep examples uniform: one entry point taking no arguments, no CLI parsing, constants at the top,
54+
and one error-handling block mapping every failure to a non-zero exit with a readable message.
7355

7456
## Code style
7557

7658
- The build targets **Java 8** (`sourceCompatibility = 1.8` in `java_shared.gradle`). Do not use
7759
newer language features in `src/main`.
78-
- There is **no linter, formatter, or static-analysis tool**, deliberately. Match the surrounding
79-
code and do not introduce one as a side effect.
60+
- There is no linter, formatter, or static-analysis tool. Match the surrounding code.
8061
- Options are untyped `Map`s built with `ObjectUtils.asMap(k, v, ...)`; responses are `Map`s keyed
8162
by the raw API field names (`public_id`, `secure_url`, `asset_id`). There are no typed response
8263
objects.
8364
- `Cloudinary` is safe to share as a singleton. `Url` is a **mutable** builder and is not
8465
thread-safe; call `cloudinary.url()` per URL.
85-
- In docs, prefer `asset_id` (immutable) over `public_id` (changes on rename) when showing how to
86-
store a reference. Only six `Api` methods accept an asset ID; nothing in `Uploader` does.
87-
- Docs describe **this SDK**, not how it differs from other Cloudinary SDKs. Link Cloudinary docs
88-
in `.md` form (`https://cloudinary.com/documentation/<page>.md`); the repository `README.md`
89-
may link HTML, since people read it. Do not recommend third-party packages — community
90-
Java/Spring integrations exist and are not maintained by Cloudinary.
91-
- Docs are reference documentation: state what the API does and how to use it. Do not frame
92-
documented behaviour as a bug or an oversight, and do not tell the reader what they might
93-
wrongly assume.
94-
- Docs and README carry **no version literal**. Install snippets use `X.Y.Z` and point at the
95-
Maven Central badge and a `maven-metadata.xml` one-liner. The literal once drifted to a
96-
never-published `2.3.1` while Central was at `2.4.0`, and nothing rewrites `docs/` at release.
66+
67+
## Docs
68+
69+
- Docs describe this SDK. Link Cloudinary documentation in `.md` form
70+
(`https://cloudinary.com/documentation/<page>.md`); `README.md` may link HTML. Do not recommend
71+
third-party packages.
72+
- Prefer `asset_id` (immutable) over `public_id` (changes on rename) when showing how to store a
73+
reference. Five `Api` methods accept an asset ID; nothing in `Uploader` does.
74+
- `README.md` shows the released version, which the release script updates. Pages in `docs/` use
75+
`X.Y.Z`, because the release script does not touch them.
9776

9877
## Git workflow
9978

10079
- Branch from `master`; keep changes focused; one topic per pull request.
101-
- Do not edit `CHANGELOG.md` or bump the version. Releases are a separate process:
102-
`tools/update_version.sh` rewrites `Cloudinary.VERSION` in `Cloudinary.java` and `version=`
103-
in `gradle.properties`. **It greps the `VERSION` line with a regex; do not reformat it.** Its
104-
`sed` over `README.md` is now a harmless no-op. `Cloudinary.VERSION` feeds the `User-Agent` and
105-
the `_a=` analytics parameter, so drift misreports SDK usage rather than breaking visibly.
80+
- Do not edit `CHANGELOG.md` or bump the version. Releases run `tools/update_version.sh <version>`,
81+
which rewrites `Cloudinary.VERSION` in `Cloudinary.java`, `version=` in `gradle.properties`, and
82+
the version in `README.md`. It reads the `VERSION` line with a regex; do not reformat it.
10683
- Keep secrets out of everything — code, tests, fixtures, docs, commit messages. Grep the diff
10784
for `cloudinary://` with real values before pushing.
10885
- Never commit `tools/cloudinary_url.txt`, `/out/`, or media left behind by running examples.
10986

11087
## Boundaries
11188

11289
**Always**
113-
- Produce every claim in `docs/` by running something — not by reading source, and not by
114-
copying another Cloudinary SDK's docs. Behaviour differs between the SDKs.
90+
- Verify claims in `docs/` against the source and, where possible, by running them. Do not copy
91+
another Cloudinary SDK's docs; behaviour differs between the SDKs.
11592
- Keep `docs/` and `examples/` consistent with the code they document.
11693
- Keep API secrets out of examples, docs, tests, and fixtures.
11794

11895
**Ask first**
119-
- Changing the published artifact list, packaging, or what ships inside a jar — including
120-
moving `docs/` into `src/main/resources`.
121-
- Adding a dependency. `cloudinary-core` has no runtime dependencies at all, which is a feature
122-
for consumers.
123-
- Changing public API signatures or behaviour. This is a widely deployed library; a behaviour
124-
change can break callers. Document current behaviour and raise an issue before changing it.
125-
- Changing any of these documented behaviours. Each is part of the public contract as observed
126-
by execution and described in `docs/`; changing one is a behaviour change for existing
127-
callers:
128-
1. `Configuration.from()` parses with `java.net.URI`, so a cloud name containing an
129-
underscore yields `null` cloud name, key, and secret (`getHost()` and `getUserInfo()`
130-
return `null` for a hostname with an underscore). Such environments are configured with a
131-
`Map`. An alternative implementation would parse `getAuthority()` directly.
132-
2. Error types differ by entry point: `java.lang.RuntimeException` carrying a message for
96+
- Changing the published artifact list, packaging, or what ships inside a jar.
97+
- Adding a dependency. `cloudinary-core` has no runtime dependencies.
98+
- Changing public API signatures or behaviour. Document current behaviour and raise an issue
99+
before changing it. This includes these behaviours, which `docs/` describes and callers may
100+
depend on:
101+
1. Error types differ by entry point: `java.lang.RuntimeException` carrying a message for
133102
uploads (`AbstractUploaderStrategy.processResponse`), typed `ApiException` subclasses for
134103
the Admin API, and `java.lang.UnknownError` (an `Error`, not an `Exception`) when no HTTP
135104
adapter is present.
136-
3. `Api.CLOUDINARY_API_ERROR_CLASSES` maps HTTP 400/401/403/404/409/420/500 to typed
137-
exceptions; other status codes become `GeneralError`. This includes 429, which is not
138-
routed to `RateLimited`.
139-
4. `EnumMetadataField` has a package-private constructor and no factory, unlike
105+
2. `EnumMetadataField` has a package-private constructor and no factory, unlike
140106
`SetMetadataField`. Application code uses `new MetadataField<String>(MetadataFieldType.ENUM)`.
141-
5. `ProgressCallback` overloads compile on `cloudinary-http5` and throw
142-
`IllegalArgumentException: Progress callback is not supported` when called. Progress
143-
reporting is available in the Android SDK.
144-
6. `Url.resourcType(String)` is retained alongside `resourceType(String)` for compatibility.
145-
Both work.
146-
7. `Cloudinary.VERSION` and `version=` in `gradle.properties` are maintained separately and
147-
can drift; during 2.3.x they read `2.3.1` and `2.3.2`. Both read `2.5.0` at the time of
148-
writing.
107+
3. `ProgressCallback` overloads compile on `cloudinary-http5` and throw
108+
`IllegalArgumentException: Progress callback is not supported` when called.
109+
4. `Url.resourcType(String)` is retained alongside `resourceType(String)` for compatibility.
149110
- Touching `publish.gradle`, `gradle.properties`, `MAVEN_CENTRAL_PUBLISHING_GUIDE.md`, or the
150111
release script.
151112
- Adding a typed response object, a linter, or a formatter.

‎README.md‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,16 @@ Upload, transform, optimize, and manage images and videos with Cloudinary from J
99

1010
## Install
1111

12-
Use the version shown on the Maven Central badge above.
13-
1412
```xml
1513
<dependency>
1614
<groupId>com.cloudinary</groupId>
1715
<artifactId>cloudinary-http5</artifactId>
18-
<version>X.Y.Z</version>
16+
<version>2.6.0</version>
1917
</dependency>
2018
```
2119

2220
```groovy
23-
implementation 'com.cloudinary:cloudinary-http5:X.Y.Z'
21+
implementation 'com.cloudinary:cloudinary-http5:2.6.0'
2422
```
2523

2624
For JSP tags, also add `cloudinary-taglib`.

‎docs/configure-cloudinary.md‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,13 @@ Configuration configuration = new Configuration();
5353
configuration.cloudName = "my-cloud";
5454
configuration.apiKey = "123456789012345";
5555
configuration.apiSecret = "abcdeghijklmnopqrstuvwxyz12";
56+
configuration.secure = true;
5657
Cloudinary cloudinary = new Cloudinary(configuration);
5758
```
5859

59-
`Configuration` fields are public and mutable. A `Configuration.Builder` is also available.
60+
`Configuration` fields are public and mutable. A `Configuration.Builder` is also available. Both
61+
start with `secure` set to `false` and analytics off, so set `secure` explicitly; without it,
62+
`url().generate(...)` returns `http://` URLs.
6063

6164
## Cloud names containing an underscore
6265

@@ -74,7 +77,14 @@ Configuration.from("cloudinary://key:secret@my_cloud").cloudName; // null
7477
Configuration.from("cloudinary://key:secret@my_cloud").apiKey; // null
7578
```
7679

77-
The exception appears on the first call that needs credentials:
80+
The exception appears on the first call that needs configuration. Signed uploads and Admin API
81+
calls check credentials first:
82+
83+
```
84+
java.lang.IllegalArgumentException: Must supply api_key
85+
```
86+
87+
URL generation and unsigned uploads check the cloud name:
7888

7989
```
8090
java.lang.IllegalArgumentException: Must supply cloud_name in tag or in configuration
@@ -106,16 +116,16 @@ A `null` check here at startup distinguishes "credentials absent or unparseable"
106116

107117
| Field / key | Default | Meaning |
108118
|---|---|---|
109-
| `secure` | `true` | Generate `https://` delivery URLs. Set `false` only when there is a specific reason to. |
119+
| `secure` | `true` from a `Map` or `CLOUDINARY_URL`; `false` from `new Configuration()` or `Configuration.Builder` | Generate `https://` delivery URLs. |
110120
| `cname` | unset | Custom delivery domain. |
111121
| `private_cdn` | `false` | Use a private CDN distribution. |
112122
| `upload_prefix` | `https://api.cloudinary.com` | API base. Change only for testing against a different endpoint. |
113-
| `timeout` | `0` (no explicit timeout) | Request timeout in seconds. |
123+
| `timeout` | `0` (the SDK sets none; HttpClient's default 3-minute read timeout applies) | Request timeout in seconds. |
114124
| `force_version` | `true` | Include the version segment in delivery URLs. |
115-
| `analytics` | enabled | Appends a `_a=` query parameter to generated URLs. See below. |
125+
| `analytics` | enabled from a `Map` or `CLOUDINARY_URL`; off from `new Configuration()` or `Configuration.Builder` | Appends a `_a=` query parameter to generated URLs. See below. |
116126

117-
`secure` defaults to `true`, so `url().generate(...)` returns an `https://` URL unless configured
118-
otherwise. The upload response contains both `url` (http) and `secure_url` (https); read
127+
With configuration from a `Map` or `CLOUDINARY_URL`, `secure` defaults to `true`, so
128+
`url().generate(...)` returns an `https://` URL unless configured otherwise. The upload response contains both `url` (http) and `secure_url` (https); read
119129
`secure_url`.
120130

121131
### The `_a=` query parameter
@@ -127,9 +137,11 @@ https://res.cloudinary.com/my-cloud/image/upload/sample.jpg?_a=DAGCg+ALZAA0
127137
```
128138

129139
This parameter encodes the SDK and language version; it is not a cache-buster. It appears on URLs
130-
built by `url().generate()` and is not present in the `secure_url` field of an upload response.
140+
built by `url().generate()` and is not present in the `secure_url` field of an upload response. It
141+
is not added to a URL that already has a query string, such as one carrying an auth token.
131142

132-
To turn it off, supply your own `Analytics` instance or disable it in configuration:
143+
`cloudinary.setAnalytics(...)` changes the value of `_a=`. To turn it off, disable it in
144+
configuration:
133145

134146
```java
135147
Cloudinary cloudinary = new Cloudinary(ObjectUtils.asMap(

‎docs/search-and-manage-assets.md‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@
1010
Store `asset_id` as your durable reference. Store `public_id` only if you need to reconstruct a
1111
URL, and re-read it when you need it.
1212

13-
Six `Api` methods accept an `asset_id`:
13+
Five `Api` methods accept an `asset_id`:
1414

1515
```
1616
Api.resourceByAssetID(String, Map)
1717
Api.resourcesByAssetIDs(Iterable, Map)
18-
Api.resourcesByAssetFolder(String, Map)
1918
Api.deleteResourcesByAssetIds(Iterable, Map)
2019
Api.restoreByAssetIds(Iterable, Map)
2120
Api.deleteBackedUpAssets(String, String[], Map)

‎docs/sign-browser-upload.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,13 @@ does not compile.
4343
Return that map to the client as JSON. It contains the API key and a signature but not the API
4444
secret.
4545

46-
### What you sign is what you constrain
46+
### The signed map is the full parameter set
4747

48-
The signature covers exactly the parameters you put in the map. Parameters you do not sign are set
49-
by the client.
48+
The upload must send exactly the parameters in the signed map, with the same values. Cloudinary
49+
rejects the upload with `Invalid Signature` if the client adds a parameter, omits one, or changes a
50+
value. Only `file`, `cloud_name`, `resource_type`, and `api_key` are outside the signature.
5051

51-
Sign every parameter you want to fix:
52+
Put every parameter the upload needs into the map on the server:
5253

5354
```java
5455
params.put("timestamp", System.currentTimeMillis() / 1000L);

‎docs/transform-and-deliver-image.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ String url = cloudinary.url().generate("docs_sample.jpg");
1212

1313
Two properties of that output:
1414

15-
- It is `https`. `secure` defaults to `true`.
15+
- It is `https`. `secure` defaults to `true` when configuration comes from a `Map` or
16+
`CLOUDINARY_URL`; with `new Configuration()` or `Configuration.Builder`, set it explicitly.
1617
- The `?_a=` parameter is SDK analytics. See
1718
[configure-cloudinary.md](configure-cloudinary.md) to disable it.
1819

‎docs/transform-and-deliver-video.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Note:
5454

5555
- Pass the public ID without an extension. `videoTag` appends `.webm`, `.mp4`, and `.ogv`
5656
itself, and derives the `.jpg` poster from the same ID.
57-
- The poster is a generated thumbnail of the first frame.
57+
- The poster is a generated thumbnail of the middle frame.
5858
- Default source formats are WebM, MP4, and OGV.
5959

6060
For attribute values that come from user input, use `generate()` and let your template engine

‎docs/troubleshoot-errors.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,10 @@ try {
108108
}
109109
```
110110

111-
Only `ApiException` subclasses are thrown in practice, but the `throws Exception` declaration means
112-
the compiler requires the broader catch. Catch `ApiException` first, then `Exception`.
111+
Besides `ApiException` subclasses, an Admin API call throws `RuntimeException: Invalid JSON response
112+
from server ...` when the response body is not JSON, such as an HTML 502 page from a proxy, and
113+
`IllegalArgumentException: Must supply api_key` when credentials are missing. The `Exception`
114+
branch catches both. Catch `ApiException` first, then `Exception`.
113115

114116
Neither block catches `UnknownError`; see the next section.
115117

0 commit comments

Comments
 (0)