From 5c5d05f6df9b5ab4905f2c0c769b7b8ee489a3a2 Mon Sep 17 00:00:00 2001 From: Constantine Nathanson Date: Thu, 22 Oct 2026 17:56:59 +0300 Subject: [PATCH] Add agent-readable docs, AGENTS.md, and restructure README - docs/: 16 task pages for coding agents using the SDK (setup, upload, large/raw upload, signed browser upload, image/video delivery, search and asset management, moderation, structured metadata, Spring Boot, JSP taglib, troubleshooting, platform capabilities). Not packaged into any jar. - examples/: four runnable programs matching the doc pages. - AGENTS.md (with CLAUDE.md pointing to it): contributor guide for agents. - SECURITY.md: private reporting path and server-side usage guidance. - context7.json for Context7 indexing. - README.md: restructured; install snippet now names cloudinary-http5 with an X.Y.Z placeholder; fixed the GitHub issues link. - .gitignore: artifacts left by running the examples. No source changes. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 4 + AGENTS.md | 157 ++++++++++++++ CLAUDE.md | 1 + README.md | 223 ++++++++++---------- SECURITY.md | 40 ++++ context7.json | 4 + docs/README.md | 96 +++++++++ docs/configure-cloudinary.md | 144 +++++++++++++ docs/get-credentials.md | 102 ++++++++++ docs/import-and-call.md | 119 +++++++++++ docs/moderate-upload.md | 134 ++++++++++++ docs/platform-capabilities.md | 95 +++++++++ docs/search-and-manage-assets.md | 195 ++++++++++++++++++ docs/sign-browser-upload.md | 152 ++++++++++++++ docs/transform-and-deliver-image.md | 210 +++++++++++++++++++ docs/transform-and-deliver-video.md | 106 ++++++++++ docs/troubleshoot-errors.md | 304 ++++++++++++++++++++++++++++ docs/upload-image.md | 219 ++++++++++++++++++++ docs/upload-large-video.md | 134 ++++++++++++ docs/use-structured-metadata.md | 191 +++++++++++++++++ docs/use-with-jsp-taglib.md | 203 +++++++++++++++++++ docs/use-with-spring-boot.md | 243 ++++++++++++++++++++++ examples/SearchAndManage.java | 86 ++++++++ examples/TransformAndDeliver.java | 99 +++++++++ examples/UploadImage.java | 72 +++++++ examples/UploadLargeVideo.java | 96 +++++++++ 26 files changed, 3321 insertions(+), 108 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 SECURITY.md create mode 100644 context7.json create mode 100644 docs/README.md create mode 100644 docs/configure-cloudinary.md create mode 100644 docs/get-credentials.md create mode 100644 docs/import-and-call.md create mode 100644 docs/moderate-upload.md create mode 100644 docs/platform-capabilities.md create mode 100644 docs/search-and-manage-assets.md create mode 100644 docs/sign-browser-upload.md create mode 100644 docs/transform-and-deliver-image.md create mode 100644 docs/transform-and-deliver-video.md create mode 100644 docs/troubleshoot-errors.md create mode 100644 docs/upload-image.md create mode 100644 docs/upload-large-video.md create mode 100644 docs/use-structured-metadata.md create mode 100644 docs/use-with-jsp-taglib.md create mode 100644 docs/use-with-spring-boot.md create mode 100644 examples/SearchAndManage.java create mode 100644 examples/TransformAndDeliver.java create mode 100644 examples/UploadImage.java create mode 100644 examples/UploadLargeVideo.java diff --git a/.gitignore b/.gitignore index e732dc7e..e332090f 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,7 @@ cloudinary-android/src/androidTest/AndroidManifest.xml /tools/cloudinary_url.txt /tools/History.md +## Artifacts left behind by running examples/ +/example-video.mp4 +/out/ + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..574eec0c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,157 @@ +# Contributor guide for coding agents + +This file is for agents contributing to this repository. If you are *using* the SDK in another +project, read [`docs/`](docs/README.md) instead — task-oriented pages for agents using the SDK. + +## Commands + +**Use JDK 11.** The Gradle 6.0.1 wrapper cannot run on a modern JVM: under JDK 17+ it fails +before any task with `Could not initialize class org.codehaus.groovy.reflection.ReflectionCache`. + +```bash +export JAVA_HOME=/path/to/jdk-11 +./gradlew :cloudinary-core:jar :cloudinary-http5:jar # build the two artifacts users depend on +./gradlew :cloudinary-core:test # unit tests, no cloud needed +./gradlew :cloudinary-http5:test -DCLOUDINARY_URL="cloudinary://key:secret@cloud" # live integration tests +./gradlew createTestSubAccount -PmoduleName=http5 # throwaway cloud -> tools/cloudinary_url.txt +``` + +`CLOUDINARY_URL` is read as a **system property** by the tests (see `java_shared.gradle`), not +from the environment. `tools/cloudinary_url.txt` is git-ignored and **contains live +credentials — never commit it or paste its contents anywhere.** + +## Testing + +- `cloudinary-core` tests are offline. Everything else needs a live product environment. +- Do not share one product environment between doc verification and the integration suite. + Doc probes leave mandatory metadata fields and pending moderated assets behind, and + `ApiTest.testGetResourcesWithMetadata` then fails for reasons unrelated to the branch. Use a + fresh sub-account for each. +- For docs-only changes, run the suite against the base branch and compare failure counts + rather than asserting "tests pass". The integration suite takes about 3.5 minutes plus a + throwaway cloud. +- Before committing a `docs/` change, two cheap checks: every `java` snippet compiles against + the built jars (wrap fragments in a harness with the ambient names — `cloudinary`, `file`, + `publicId` — declared as fields, then `javac`; this catches invented methods, which is the + whole point), and every internal link, cross-page anchor, and external URL resolves. GitHub + **preserves underscores** in heading slugs, so a naive slugifier reports false positives. + +## Project structure + +| Path | Published | Purpose | +|---|---|---| +| `cloudinary-core/` | Yes | URL/transformation building, parameter serialisation. No HTTP client, no runtime dependencies. | +| `cloudinary-http5/` | Yes | Apache HttpClient 5 transport. **This is what users depend on.** | +| `cloudinary-taglib/` | Yes | JSP tags. Targets `javax.*`, not Jakarta EE 9+. | +| `cloudinary-test-common/` | Yes | Shared test helpers. Not for application code. | +| `docs/` | No | Task pages for agents *using* the SDK. Every claim produced by running something. | +| `examples/` | No | One runnable program per doc page where the task is genuinely runnable. | +| `samples/` | No | Legacy sample applications. Not in the build. | +| `tools/` | No | Release and test-cloud scripts. | + +`settings.gradle` includes only the four modules. The `cloudinary-http42/43/44` directories +present in a working copy are **untracked leftovers** from the 1.x line — not in git, not in the +build. Do not add them back and do not "tidy" them as part of unrelated work. + +`org.cloudinary.json` inside `cloudinary-core` is a vendored JSON implementation. It exists to +keep the core dependency-free. Do not replace it with Jackson or Gson. + +**`docs/` is deliberately not packaged into any jar.** A survey of 15 widely-used Java libraries +on Maven Central — Guava, OkHttp, Jackson, Gson, commons-lang3, SLF4J, Retrofit, AWS SDK v2, +google-cloud-storage, Stripe, Twilio, SendGrid, java-jwt, Spring Core, jsoup — found that only +jsoup ships any Markdown: its 3 KB project README in `META-INF/`. Everything else carries +manifests, licences, and machine-consumed config only. Embedding these pages would grow +`cloudinary-core` by 27%, from 175 KB to 223 KB, for files no runtime reads, and retrieval +through `unzip -p` is slower than reading the repository or Cloudinary's `llms.txt`. Java's +convention for documentation attached to an artifact is the `-javadoc.jar`, which this SDK +already publishes. Reversing this needs a reason better than "other Cloudinary SDKs do it". + +`examples/` is likewise not shipped: loose `.java` files inside a library artifact are dead +weight, and each doc page carries its full flow inline. Keep examples uniform — one entry point +taking no arguments, no CLI parsing, constants at the top, one error-handling block mapping every +failure to a non-zero exit with a readable message and no stack trace. + +## Code style + +- The build targets **Java 8** (`sourceCompatibility = 1.8` in `java_shared.gradle`). Do not use + newer language features in `src/main`. +- There is **no linter, formatter, or static-analysis tool**, deliberately. Match the surrounding + code and do not introduce one as a side effect. +- Options are untyped `Map`s built with `ObjectUtils.asMap(k, v, ...)`; responses are `Map`s keyed + by the raw API field names (`public_id`, `secure_url`, `asset_id`). There are no typed response + objects. +- `Cloudinary` is safe to share as a singleton. `Url` is a **mutable** builder and is not + thread-safe; call `cloudinary.url()` per URL. +- In docs, prefer `asset_id` (immutable) over `public_id` (changes on rename) when showing how to + store a reference. Only six `Api` methods accept an asset ID; nothing in `Uploader` does. +- Docs describe **this SDK**, not how it differs from other Cloudinary SDKs. Link Cloudinary docs + in `.md` form (`https://cloudinary.com/documentation/.md`); the repository `README.md` + may link HTML, since people read it. Do not recommend third-party packages — community + Java/Spring integrations exist and are not maintained by Cloudinary. +- Docs are reference documentation: state what the API does and how to use it. Do not frame + documented behaviour as a bug or an oversight, and do not tell the reader what they might + wrongly assume. +- Docs and README carry **no version literal**. Install snippets use `X.Y.Z` and point at the + Maven Central badge and a `maven-metadata.xml` one-liner. The literal once drifted to a + never-published `2.3.1` while Central was at `2.4.0`, and nothing rewrites `docs/` at release. + +## Git workflow + +- Branch from `master`; keep changes focused; one topic per pull request. +- Do not edit `CHANGELOG.md` or bump the version. Releases are a separate process: + `tools/update_version.sh` rewrites `Cloudinary.VERSION` in `Cloudinary.java` and `version=` + in `gradle.properties`. **It greps the `VERSION` line with a regex; do not reformat it.** Its + `sed` over `README.md` is now a harmless no-op. `Cloudinary.VERSION` feeds the `User-Agent` and + the `_a=` analytics parameter, so drift misreports SDK usage rather than breaking visibly. +- Keep secrets out of everything — code, tests, fixtures, docs, commit messages. Grep the diff + for `cloudinary://` with real values before pushing. +- Never commit `tools/cloudinary_url.txt`, `/out/`, or media left behind by running examples. + +## Boundaries + +**Always** +- Produce every claim in `docs/` by running something — not by reading source, and not by + copying another Cloudinary SDK's docs. Behaviour differs between the SDKs. +- Keep `docs/` and `examples/` consistent with the code they document. +- Keep API secrets out of examples, docs, tests, and fixtures. + +**Ask first** +- Changing the published artifact list, packaging, or what ships inside a jar — including + moving `docs/` into `src/main/resources`. +- Adding a dependency. `cloudinary-core` has no runtime dependencies at all, which is a feature + for consumers. +- Changing public API signatures or behaviour. This is a widely deployed library; a behaviour + change can break callers. Document current behaviour and raise an issue before changing it. +- Changing any of these documented behaviours. Each is part of the public contract as observed + by execution and described in `docs/`; changing one is a behaviour change for existing + callers: + 1. `Configuration.from()` parses with `java.net.URI`, so a cloud name containing an + underscore yields `null` cloud name, key, and secret (`getHost()` and `getUserInfo()` + return `null` for a hostname with an underscore). Such environments are configured with a + `Map`. An alternative implementation would parse `getAuthority()` directly. + 2. Error types differ by entry point: `java.lang.RuntimeException` carrying a message for + uploads (`AbstractUploaderStrategy.processResponse`), typed `ApiException` subclasses for + the Admin API, and `java.lang.UnknownError` (an `Error`, not an `Exception`) when no HTTP + adapter is present. + 3. `Api.CLOUDINARY_API_ERROR_CLASSES` maps HTTP 400/401/403/404/409/420/500 to typed + exceptions; other status codes become `GeneralError`. This includes 429, which is not + routed to `RateLimited`. + 4. `EnumMetadataField` has a package-private constructor and no factory, unlike + `SetMetadataField`. Application code uses `new MetadataField(MetadataFieldType.ENUM)`. + 5. `ProgressCallback` overloads compile on `cloudinary-http5` and throw + `IllegalArgumentException: Progress callback is not supported` when called. Progress + reporting is available in the Android SDK. + 6. `Url.resourcType(String)` is retained alongside `resourceType(String)` for compatibility. + Both work. + 7. `Cloudinary.VERSION` and `version=` in `gradle.properties` are maintained separately and + can drift; during 2.3.x they read `2.3.1` and `2.3.2`. Both read `2.5.0` at the time of + writing. +- Touching `publish.gradle`, `gradle.properties`, `MAVEN_CENTRAL_PUBLISHING_GUIDE.md`, or the + release script. +- Adding a typed response object, a linter, or a formatter. +- Modifying the legacy sample apps in `samples/`. + +**Never** +- Commit credentials or real account identifiers. +- Document a Cloudinary platform capability as an SDK method unless this library implements it + (see `docs/platform-capabilities.md`). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index a58574bd..f45ff18c 100644 --- a/README.md +++ b/README.md @@ -1,148 +1,155 @@ -[![Build Status](https://travis-ci.org/cloudinary/cloudinary_java.svg?branch=master)](https://travis-ci.org/cloudinary/cloudinary_java) +# Cloudinary Java SDK -Cloudinary -========== +Upload, transform, optimize, and manage images and videos with Cloudinary from Java — the +`com.cloudinary` artifacts on Maven Central. -## About -The Cloudinary Java SDK allows you to quickly and easily integrate your application with Cloudinary. -Effortlessly optimize and transform your cloud's assets. +[![Build](https://github.com/cloudinary/cloudinary_java/actions/workflows/build.yml/badge.svg)](https://github.com/cloudinary/cloudinary_java/actions/workflows/build.yml) +[![Maven Central](https://img.shields.io/maven-central/v/com.cloudinary/cloudinary-http5.svg)](https://central.sonatype.com/artifact/com.cloudinary/cloudinary-http5) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -### Additional documentation -This Readme provides basic installation and usage information. -For the complete documentation, see the [Java SDK Guide](https://cloudinary.com/documentation/java_integration). +## Install -## Table of Contents -- [Key Features](#key-features) -- [Version Support](#Version-Support) -- [Installation](#installation) -- [Usage](#usage) - - [Setup](#Setup) - - [Transform and Optimize Assets](#Transform-and-Optimize-Assets) - - [File upload](#File-upload) - -## Key Features -- [Transform](https://cloudinary.com/documentation/java_video_manipulation) and [optimize](https://cloudinary.com/documentation/java_image_manipulation#image_optimizations) assets (links to docs). -- [Upload assets to cloud](https://cloudinary.com/documentation/java_image_and_video_upload) - -## Version Support -| SDK Version | Java 6+ | Java 8 | -|----------------|---------|--------| -| 1.1.0 - 1.39.0 | V | | -| 2.0.0+ | | V | - - - -## Installation -The cloudinary_java library is available in [Maven Central](https://mvnrepository.com/artifact/com.cloudinary/cloudinary-core). To use it, add the following dependency to your pom.xml : +Use the version shown on the Maven Central badge above. ```xml com.cloudinary - cloudinary-http45 - 2.5.0 + cloudinary-http5 + X.Y.Z ``` -## Usage -### Setup +```groovy +implementation 'com.cloudinary:cloudinary-http5:X.Y.Z' +``` -Each request for building a URL of a remote cloud resource must have the `cloud_name` parameter set. -Each request to our secure APIs (e.g., image uploads, eager sprite generation) must have the `api_key` and `api_secret` parameters set. -See [API, URLs and access identifiers](https://cloudinary.com/documentation/solution_overview#account_and_api_setup) for more details. +For JSP tags, also add `cloudinary-taglib`. -Setting the `cloud_name`, `api_key` and `api_secret` parameters can be done either directly in each call to a Cloudinary method, -by when initializing the Cloudinary object, or by using the CLOUDINARY_URL environment variable / system property. +## Quick start -The entry point of the library is the Cloudinary object. -```java -Cloudinary cloudinary = new Cloudinary(); +Set your API environment variable (Console > Settings > API Keys): + +```bash +export CLOUDINARY_URL=cloudinary://:@ ``` -Here's an example of setting the configuration parameters programatically: +Upload an image and get an optimized delivery URL: ```java -Map config = new HashMap(); -config.put("cloud_name", "n07t21i7"); -config.put("api_key", "123456789012345"); -config.put("api_secret", "abcdeghijklmnopqrstuvwxyz12"); -Cloudinary cloudinary = new Cloudinary(config); +import com.cloudinary.Cloudinary; +import com.cloudinary.Transformation; +import com.cloudinary.utils.ObjectUtils; +import java.util.Map; + +public class QuickStart { + public static void main(String[] args) { + try { + // Reads CLOUDINARY_URL from the environment + Cloudinary cloudinary = new Cloudinary(); + + // Upload a remote image (a local File or path works the same way) + Map result = cloudinary.uploader().upload( + "https://res.cloudinary.com/demo/image/upload/sample.jpg", + ObjectUtils.asMap("public_id", "quickstart-sample")); + System.out.println("Uploaded: " + result.get("public_id")); + + // Build a 400x400 auto-cropped URL with automatic format and quality + String url = cloudinary.url() + .transformation(new Transformation() + .width(400).height(400).crop("fill").gravity("auto") + .fetchFormat("auto").quality("auto")) + .generate((String) result.get("public_id")); + System.out.println("Optimized URL: " + url); + } catch (Exception e) { + System.err.println("Quick start failed: " + e.getMessage()); + System.err.println("Check that CLOUDINARY_URL is set (Console > Settings > API Keys)."); + System.exit(1); + } + } +} ``` -Another example of setting the configuration parameters by providing the CLOUDINARY_URL value to the constructor: +Save as `QuickStart.java`, compile against `cloudinary-http5` and its dependencies, and run. +[Create a free account](https://cloudinary.com/users/register_free) if you don't have one — see +[Get credentials](docs/get-credentials.md) for the options, including a throwaway environment for +testing. - Cloudinary cloudinary = new Cloudinary("cloudinary://123456789012345:abcdeghijklmnopqrstuvwxyz12@n07t21i7"); +## Common tasks -### Transform and Optimize Assets -- [See full documentation](https://cloudinary.com/documentation/java_image_manipulation) -Any image uploaded to Cloudinary can be transformed and embedded using powerful view helper methods: +- [Get Cloudinary credentials](docs/get-credentials.md) +- [Add the dependency and make a first call](docs/import-and-call.md) +- [Configure credentials in code, environment, or a framework](docs/configure-cloudinary.md) +- [Upload an image](docs/upload-image.md) +- [Upload a large video](docs/upload-large-video.md) +- [Sign a browser upload](docs/sign-browser-upload.md) +- [Transform and deliver an image](docs/transform-and-deliver-image.md) +- [Transform and deliver a video](docs/transform-and-deliver-video.md) +- [Search and manage assets](docs/search-and-manage-assets.md) +- [Moderate an upload](docs/moderate-upload.md) +- [Use structured metadata](docs/use-structured-metadata.md) +- [Use with Spring Boot](docs/use-with-spring-boot.md) +- [Use the JSP tag library](docs/use-with-jsp-taglib.md) +- [Troubleshoot errors](docs/troubleshoot-errors.md) -The following example generates the url for accessing an uploaded `sample` image while transforming it to fill a 100x150 rectangle: +Runnable versions live in [`examples/`](examples/) — each is a complete file you can run directly. -```java -cloudinary.url().transformation(new Transformation().width(100).height(150).crop("fill")).generate("sample.jpg"); -``` +## When to use this SDK -Another example, emedding a smaller version of an uploaded image while generating a 90x90 face detection based thumbnail: +Use this library in **server-side Java**: uploads, signed operations, asset administration, +search, moderation, and delivery URL generation. -```java -cloudinary.url().transformation(new Transformation().width(90).height(90).crop("thumb").gravity("face")).generate("woman.jpg"); -``` +For other jobs, better-fitting tools exist: -You can provide either a Facebook name or a numeric ID of a Facebook profile or a fan page. +- Android apps: the [Android SDK](https://cloudinary.com/documentation/android_integration) ([md](https://cloudinary.com/documentation/android_integration.md)); `cloudinary-http5` targets server-side JVMs. +- Complete in-browser upload UI: [Upload Widget](https://cloudinary.com/documentation/upload_widget) ([md](https://cloudinary.com/documentation/upload_widget.md)). +- Text-to-image generation and image-to-video: [platform APIs](https://cloudinary.com/documentation/image_generation_addon) ([md](https://cloudinary.com/documentation/image_generation_addon.md)), not wrapped by this library. +- Multi-step media workflow automation: [MediaFlows](https://cloudinary.com/documentation/mediaflows_user_guide) ([md](https://cloudinary.com/documentation/mediaflows_user_guide.md)). +- Interactive agent-driven asset operations: [Cloudinary MCP servers and Skills](https://cloudinary.com/documentation/cloudinary_llm_mcp) ([md](https://cloudinary.com/documentation/cloudinary_llm_mcp.md)). -Embedding a Facebook profile to match your graphic design is very simple: +The full capability map — plus the Skills, MCP servers, and CLI worth setting up first — is in +[docs/platform-capabilities.md](docs/platform-capabilities.md). -```java -cloudinary.url().type("facebook").transformation(new Transformation().width(130).height(130).crop("fill").gravity("north_west")).generate("billclinton.jpg"); -``` +## Status and compatibility -### File upload -Assuming you have your Cloudinary configuration parameters defined (`cloud_name`, `api_key`, `api_secret`), uploading to Cloudinary is very simple. +Stable, actively maintained. See [CHANGELOG.md](CHANGELOG.md). -The following example uploads a local JPG to the cloud: +| SDK version | Java | +|----------------|-------------------------------| +| 2.x | 8 and later | +| 1.1.0 – 1.39.0 | 6 and later (no longer maintained) | -```java -cloudinary.uploader().upload("my_picture.jpg", ObjectUtils.emptyMap()); -``` +`cloudinary-taglib` targets `javax.servlet` / `javax.servlet.jsp`, not Jakarta EE 9+. -The uploaded image is assigned a randomly generated public ID. The image is immediately available for download through a CDN: +## Documentation -```java -cloudinary.url().generate("abcfrmo8zul1mafopawefg.jpg"); +- [Task docs for this repository](docs/README.md) — snippets compile against this SDK, and + documented behaviour was observed against a live environment. +- [Java SDK guide](https://cloudinary.com/documentation/java_integration) — the full documentation + ([md](https://cloudinary.com/documentation/java_integration.md)). -# http://res.cloudinary.com/demo/image/upload/abcfrmo8zul1mafopawefg.jpg -``` +Documentation links in this README point at the browsable HTML page, with an `(md)` companion +link that returns the same page as raw Markdown. Inside `docs/` and `examples/` the links are +Markdown-only, since those files are written to be read by coding agents. Either form works for +any page: add `.md` for Markdown, drop it for HTML. -You can also specify your own public ID: +## For AI coding agents -```java -cloudinary.uploader().upload("http://www.example.com/image.jpg", ObjectUtils.asMap("public_id", "sample_remote")); +- Contributing to this repo: read [AGENTS.md](AGENTS.md). +- Using the SDK in your own project: the pages in [`docs/`](docs/README.md) describe this SDK's + behaviour; start with [platform-capabilities](docs/platform-capabilities.md) for what the + library covers. They describe the 2.x API. -cloudinary.url().generate("sample_remote.jpg"); +## Support -# http://res.cloudinary.com/demo/image/upload/sample_remote.jpg -``` +- SDK bugs and feature requests: [GitHub issues](https://github.com/cloudinary/cloudinary_java/issues) +- Account and platform questions: [Cloudinary support](https://support.cloudinary.com) + +## Security + +See [SECURITY.md](SECURITY.md) for private vulnerability reporting. Keep your `api_secret` in +server-side code; for client uploads, use the server-signed pattern in +[Sign a browser upload](docs/sign-browser-upload.md). + +## License -## Contributions -See [contributing guidelines](/CONTRIBUTING.md). - -## Get Help -- [Open a Github issue](https://github.com/CloudinaryLtd/cloudinary_java/issues) (for issues related to the SDK) -- [Open a support ticket](https://cloudinary.com/contact) (for issues related to your account) - -## About Cloudinary -Cloudinary is a powerful media API for websites and mobile apps alike, Cloudinary enables developers to efficiently manage, transform, optimize, and deliver images and videos through multiple CDNs. Ultimately, viewers enjoy responsive and personalized visual-media experiences—irrespective of the viewing device. - -## Additional Resources -- [Cloudinary Transformation and REST API References](https://cloudinary.com/documentation/cloudinary_references): Comprehensive references, including syntax and examples for all SDKs. -- [MediaJams.dev](https://mediajams.dev/): Bite-size use-case tutorials written by and for Cloudinary Developers -- [DevJams](https://www.youtube.com/playlist?list=PL8dVGjLA2oMr09amgERARsZyrOz_sPvqw): Cloudinary developer podcasts on YouTube. -- [Cloudinary Academy](https://training.cloudinary.com/): Free self-paced courses, instructor-led virtual courses, and on-site courses. -- [Code Explorers and Feature Demos](https://cloudinary.com/documentation/code_explorers_demos_index): A one-stop shop for all code explorers, Postman collections, and feature demos found in the docs. -- [Cloudinary Roadmap](https://cloudinary.com/roadmap): Your chance to follow, vote, or suggest what Cloudinary should develop next. -- [Cloudinary Facebook Community](https://www.facebook.com/groups/CloudinaryCommunity): Learn from and offer help to other Cloudinary developers. -- [Cloudinary Account Registration](https://cloudinary.com/users/register/free): Free Cloudinary account registration. -- [Cloudinary Website](https://cloudinary.com) - -## Licence -Released under the MIT license. +Released under the MIT license — see [LICENSE](LICENSE). Copyright (c) Cloudinary Ltd. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..eaee20d1 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,40 @@ +# Security Policy + +## Supported versions + +| Version | Supported | +|---------|-----------| +| 2.x | Yes | +| 1.x | No | + +## Reporting a vulnerability + +Report vulnerabilities privately through [GitHub private vulnerability reporting](https://github.com/cloudinary/cloudinary_java/security/advisories/new) for this repository. + +If you cannot use GitHub reporting, contact Cloudinary support at [support.cloudinary.com](https://support.cloudinary.com/hc/en-us/requests/new) and mark the ticket as a security issue. + +Use these private channels for anything security-sensitive; public GitHub issues are for regular bugs and feature requests. + +## What to include in a report + +- The affected SDK version, which artifact (`cloudinary-core`, `cloudinary-http5`, `cloudinary-taglib`), and the Java version. +- A minimal reproduction or proof of concept. +- The impact you believe the issue has (for example: credential exposure, signature bypass, markup injection). +- Any suggested remediation, if you have one. + +Never include real credentials in a report. A Cloudinary API secret grants full control of a product environment; redact cloud names, keys, and secrets, and rotate anything you may have exposed while investigating. + +## Response and disclosure process + +- We acknowledge reports and keep you informed while the issue is investigated. +- Fixes are released as patched artifact versions; the changelog notes security-relevant changes without disclosing exploit details before users can upgrade. +- Please give us reasonable time to release a fix before public disclosure. + +## Security guidance for SDK users + +- Your `api_secret` is a server-side credential. Keep it on your server; browsers, mobile binaries, and repositories should only ever hold delivery URLs or short-lived signatures. +- Provide credentials through the `CLOUDINARY_URL` environment variable rather than hardcoding them. +- For uploads initiated from a browser or mobile app, generate the signature on your server, and sign every parameter you intend to constrain — an unsigned parameter is one the client can choose. See [docs/sign-browser-upload.md](docs/sign-browser-upload.md). +- For unsigned uploads, use a deliberately restricted [unsigned upload preset](https://cloudinary.com/documentation/upload_presets) ([md](https://cloudinary.com/documentation/upload_presets.md)). +- For attribute values that come from user input, build the URL with `generate()` and render the tag in your template engine. See [docs/transform-and-deliver-image.md](docs/transform-and-deliver-image.md#attributes-from-user-input). +- Cloudinary platform security documentation: https://cloudinary.com/documentation/solution_overview#security diff --git a/context7.json b/context7.json new file mode 100644 index 00000000..2affccf9 --- /dev/null +++ b/context7.json @@ -0,0 +1,4 @@ +{ + "url": "https://context7.com/cloudinary/cloudinary_java", + "public_key": "pk_dAgXWo5YsHXdnbg3TCE9R" +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..847c7894 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,96 @@ + + +# cloudinary_java — documentation for coding agents + +Task documentation for the Cloudinary Java SDK. Each page is self-contained: imports, +configuration, a complete runnable flow, expected results, and common failures. Every Java snippet +compiles against this SDK, and the documented behaviour was observed against a live Cloudinary +product environment. Runnable versions of several tasks are in [`examples/`](../examples/). + +These pages describe the 2.x API. Methods from the 1.x line are not present in 2.x. + +## Where these pages live + +In the repository, at `docs/`. They are **not** shipped inside the published jars — Java +libraries distribute prose documentation alongside the artifact, not inside it. Read them from +a checkout, or from +[the repository on GitHub](https://github.com/cloudinary/cloudinary_java/tree/master/docs) at +the tag matching your dependency version. Without repository access, Cloudinary's own +documentation serves raw Markdown at `https://cloudinary.com/documentation/.md`, indexed +by [llms.txt](https://cloudinary.com/documentation/llms.txt); that covers the platform rather +than this SDK's Java specifics. + +To confirm which version you are running, compare `Cloudinary.VERSION` with the version these +pages describe. That constant can differ from the released artifact version. + +## Start here + +- [What this library covers](platform-capabilities.md) — the agent tooling to set up first + (Skills, MCP servers, CLI, documentation indexes), what this library covers, and what lives + elsewhere on the platform. +- [Get Cloudinary credentials](get-credentials.md) — console signup, or a throwaway environment + for testing. +- [Add the dependency and make a first call](import-and-call.md) — the two-artifact dependency + and the `Cloudinary` entry point. + +## Tasks + +- [Configure Cloudinary](configure-cloudinary.md) +- [Upload an image](upload-image.md) +- [Upload a large video](upload-large-video.md) +- [Sign a browser upload](sign-browser-upload.md) +- [Transform and deliver an image](transform-and-deliver-image.md) +- [Transform and deliver a video](transform-and-deliver-video.md) +- [Search and manage assets](search-and-manage-assets.md) +- [Moderate an upload](moderate-upload.md) +- [Use structured metadata](use-structured-metadata.md) +- [Use with Spring Boot](use-with-spring-boot.md) +- [Use the JSP tag library](use-with-jsp-taglib.md) +- [Troubleshoot errors](troubleshoot-errors.md) + +## Behaviours to know + +1. `cloudinary-core` provides URL building and parameter handling; uploads and Admin API calls + need `cloudinary-http5` on the classpath. Without it the `Cloudinary` constructor throws + `java.lang.UnknownError`, which is an `Error` rather than an `Exception`. See + [import-and-call.md](import-and-call.md). +2. Upload API errors are `RuntimeException` carrying a message; Admin API errors are typed + `ApiException` subclasses. See [troubleshoot-errors.md](troubleshoot-errors.md). +3. Cloud names containing an underscore are configured with a `Map` rather than a + `cloudinary://` URL; parsing such a URL yields `null` cloud name, API key, and secret. See + [configure-cloudinary.md](configure-cloudinary.md). +4. `buildUploadParams` forwards a defined parameter set; unrecognised keys are not sent. See + [upload-image.md](upload-image.md). +5. Moderation status is metadata to check in your own code; a `pending` asset is deliverable. + See [moderate-upload.md](moderate-upload.md). +6. `uploadLarge` defaults `resource_type` to `image`; pass `video`, `raw`, or `auto` as + appropriate. With the default, a video upload throws `RuntimeException: Invalid image file`. See + [upload-large-video.md](upload-large-video.md). + +## Security boundary + +This is a **server-side** SDK. It holds your `api_secret`, which belongs on your server only. +Frontend code should receive delivery URLs or short-lived signatures generated by your server +([how](sign-browser-upload.md)). For attribute values that come from user input, +render tags with your template engine rather than `imageTag()` / `videoTag()` +([details](transform-and-deliver-image.md#attributes-from-user-input)). + +## Canonical docs + +- [Java SDK guide](https://cloudinary.com/documentation/java_integration.md) +- [Full platform reference](https://cloudinary.com/documentation/cloudinary_references.md) + +**Link convention:** documentation links in these docs end in `.md` and return raw Markdown — +the preferred format for agents and for anything that parses text. Remove the `.md` suffix for +the same page as browsable HTML. The repository README links the HTML form first, since it is +read by people. + +**Snippet conventions:** snippets assume `import com.cloudinary.*;` and +`import com.cloudinary.utils.ObjectUtils;` unless shown otherwise. Placeholder credentials and +identifiers are obviously fake but correctly shaped — a real asset ID is 32 lowercase hex +characters, shown here as `abcdef0123456789abcdef0123456789`. Never copy a placeholder into +production. diff --git a/docs/configure-cloudinary.md b/docs/configure-cloudinary.md new file mode 100644 index 00000000..b24327da --- /dev/null +++ b/docs/configure-cloudinary.md @@ -0,0 +1,144 @@ +# Configure the SDK + +Credentials can be supplied in four ways. Pick one and use it consistently. + +## 1. `CLOUDINARY_URL` environment variable + +```bash +export CLOUDINARY_URL="cloudinary://123456789012345:abcdeghijklmnopqrstuvwxyz12@my-cloud" +``` + +```java +Cloudinary cloudinary = new Cloudinary(); +``` + +The no-argument constructor reads the `CLOUDINARY_URL` system property first, then falls back to +the environment variable of the same name: + +```java +// Equivalent to what the no-arg constructor does: +System.getProperty("CLOUDINARY_URL", System.getenv("CLOUDINARY_URL")) +``` + +`-DCLOUDINARY_URL=...` on the JVM command line therefore overrides the environment variable. If +neither is set, the constructor returns a `Cloudinary` with no credentials; an +`IllegalArgumentException` is thrown on the first call that needs them. + +## 2. Connection URL passed explicitly + +```java +Cloudinary cloudinary = new Cloudinary( + "cloudinary://123456789012345:abcdeghijklmnopqrstuvwxyz12@my-cloud"); +``` + +This form suits tests, where a dependency on ambient environment state is undesirable. + +## 3. A `Map` of individual settings + +```java +Cloudinary cloudinary = new Cloudinary(ObjectUtils.asMap( + "cloud_name", "my-cloud", + "api_key", "123456789012345", + "api_secret", "abcdeghijklmnopqrstuvwxyz12", + "secure", true)); +``` + +Use this when credentials arrive from a secret manager or a framework's configuration binding +rather than from a URL. See [use-with-spring-boot.md](use-with-spring-boot.md). + +## 4. A `Configuration` object + +```java +Configuration configuration = new Configuration(); +configuration.cloudName = "my-cloud"; +configuration.apiKey = "123456789012345"; +configuration.apiSecret = "abcdeghijklmnopqrstuvwxyz12"; +Cloudinary cloudinary = new Cloudinary(configuration); +``` + +`Configuration` fields are public and mutable. A `Configuration.Builder` is also available. + +## Cloud names containing an underscore + +`Configuration.from()` parses the connection URL with `java.net.URI` and reads the cloud name from +`URI.getHost()`. Underscores are not permitted in DNS host names, so for such a URL `getHost()` +returns `null`, and so does `getUserInfo()`. Cloud name, API key, and API secret are all `null` +after parsing, and no exception is thrown at that point: + +```java +// A cloud name with a hyphen — parses correctly. +Configuration.from("cloudinary://key:secret@my-cloud").cloudName; // "my-cloud" + +// A cloud name with an underscore — yields nulls. +Configuration.from("cloudinary://key:secret@my_cloud").cloudName; // null +Configuration.from("cloudinary://key:secret@my_cloud").apiKey; // null +``` + +The exception appears on the first call that needs credentials: + +``` +java.lang.IllegalArgumentException: Must supply cloud_name in tag or in configuration +``` + +For a cloud name containing an underscore, use the `Map` or `Configuration` form (options 3 and 4), +which do not go through URI parsing. This behaviour is also recorded in +[troubleshoot-errors.md](troubleshoot-errors.md). + +## Verifying your configuration works + +`api().ping()` is the cheapest round trip that confirms credentials are valid: + +```java +Map result = cloudinary.api().ping(ObjectUtils.emptyMap()); +System.out.println(result.get("status")); // "ok" +``` + +Reading configuration back confirms that it parsed, not that it is valid: + +```java +System.out.println(cloudinary.config.cloudName); // null means parsing failed +``` + +A `null` check here at startup distinguishes "credentials absent or unparseable" from +"credentials rejected by the server". + +## Configuration options + +| Field / key | Default | Meaning | +|---|---|---| +| `secure` | `true` | Generate `https://` delivery URLs. Set `false` only when there is a specific reason to. | +| `cname` | unset | Custom delivery domain. | +| `private_cdn` | `false` | Use a private CDN distribution. | +| `upload_prefix` | `https://api.cloudinary.com` | API base. Change only for testing against a different endpoint. | +| `timeout` | `0` (no explicit timeout) | Request timeout in seconds. | +| `force_version` | `true` | Include the version segment in delivery URLs. | +| `analytics` | enabled | Appends a `_a=` query parameter to generated URLs. See below. | + +`secure` defaults to `true`, so `url().generate(...)` returns an `https://` URL unless configured +otherwise. The upload response contains both `url` (http) and `secure_url` (https); read +`secure_url`. + +### The `_a=` query parameter + +Generated delivery URLs carry an analytics parameter: + +``` +https://res.cloudinary.com/my-cloud/image/upload/sample.jpg?_a=DAGCg+ALZAA0 +``` + +This parameter encodes the SDK and language version; it is not a cache-buster. It appears on URLs +built by `url().generate()` and is not present in the `secure_url` field of an upload response. + +To turn it off, supply your own `Analytics` instance or disable it in configuration: + +```java +Cloudinary cloudinary = new Cloudinary(ObjectUtils.asMap( + "cloud_name", "my-cloud", + "analytics", false)); +``` + +## Protecting credentials + +The API secret grants full control over the product environment. Keep it server-side, out of +version control, and out of anything shipped to a browser or mobile client. For uploads from a +browser, sign the request server-side; see [sign-browser-upload.md](sign-browser-upload.md). diff --git a/docs/get-credentials.md b/docs/get-credentials.md new file mode 100644 index 00000000..730be6ce --- /dev/null +++ b/docs/get-credentials.md @@ -0,0 +1,102 @@ +# Get credentials + +You need three values: a cloud name, an API key, and an API secret. Together they are usually +expressed as one connection URL: + +``` +cloudinary://:@ +``` + +## From the console + +1. Sign up at [cloudinary.com/users/register_free](https://cloudinary.com/users/register_free). +2. Open the [console dashboard](https://console.cloudinary.com/). The cloud name, API key, and the + full `CLOUDINARY_URL` are shown there; the API secret is behind a reveal control. +3. Put the connection URL in your environment, not in your source tree: + +```bash +export CLOUDINARY_URL="cloudinary://123456789012345:abcdeghijklmnopqrstuvwxyz12@my-cloud" +``` + +## Throwaway environments + +A Claimable Cloud is a real, temporary product environment created without a signup flow. It is +useful for running an example end to end before moving to a permanent account: + +```bash +npx @cloudinary/cloud +``` + +It prints live credentials. Treat them as secret: keep them out of commits, issues, PR +descriptions, and chat logs, and redact them before sharing anything. + +If that command fails with +`delivery_ips must contain at least one public IP address [delivery_ips_not_public]`, the request +reached Cloudinary from a private network address, typically a corporate VPN or secure gateway. This +is a network-routing condition rather than an authentication failure; whether to change VPN or +network settings is a decision for the account owner. + +This repository also has its own test-cloud helper, used by the test suite: + +```bash +./gradlew createTestSubAccount -PmoduleName=http5 +# writes tools/cloudinary_url.txt (already git-ignored) +``` + +## Which value goes where + +| Value | Secret? | Used for | +|---|---|---| +| Cloud name | No | Delivery URLs. Safe in client code. | +| API key | No, but treat it carefully | Identifies your environment on API calls. | +| API secret | Yes | Signing. Server-side only. | + +The API secret grants full control of the product environment, including deletion. Keep it out of +browsers, mobile apps, and public repositories. If a client needs to upload, sign the request +server-side — see [sign-browser-upload.md](sign-browser-upload.md). + +Delivery URLs are built from the cloud name alone, so URL-building code needs no secret: + +```java +Cloudinary cloudinary = new Cloudinary(ObjectUtils.asMap("cloud_name", "my-cloud")); +cloudinary.url().generate("sample.jpg"); // works with no key or secret +``` + +Signed delivery URLs (`signed(true)`) need the API secret. Check that it is loaded before +generating them: + +```java +if (cloudinary.config.apiSecret == null) { + throw new IllegalStateException("API secret not configured"); +} +``` + +## Verifying credentials + +```java +Map result = cloudinary.api().ping(ObjectUtils.emptyMap()); +System.out.println(result.get("status")); // "ok" +``` + +A `ping` response contains exactly one key, `status`. Failures: + +| Result | Meaning | +|---|---| +| `IllegalArgumentException: Must supply api_key` | Credentials never reached the SDK. | +| `AuthorizationRequired: disabled customer` | The environment is suspended or expired — common with throwaway clouds. | +| `AuthorizationRequired` with another message | Key or secret is wrong. | + +If credentials are correct but the SDK reports them as `null`, check whether the cloud name contains +an underscore. A connection URL whose cloud name contains an underscore parses with cloud name, key, +and secret all `null` — see [configure-cloudinary.md](configure-cloudinary.md). + +## Rotating a secret + +If a secret reaches a public place, rotate it in the console immediately — under Settings, then +Access Keys. Removing it from git history does not revoke it; treat anything pushed publicly as +compromised. Generate a new key, deploy it, then disable the old one. + +## Related + +- [configure-cloudinary.md](configure-cloudinary.md) +- [sign-browser-upload.md](sign-browser-upload.md) diff --git a/docs/import-and-call.md b/docs/import-and-call.md new file mode 100644 index 00000000..b42a84e3 --- /dev/null +++ b/docs/import-and-call.md @@ -0,0 +1,119 @@ +# Add the dependency and make your first call + +## Add the dependency + +The SDK is split across two artifacts. `cloudinary-core` contains the URL builder, the +transformation builder, and the parameter serialisation; it has no HTTP client, so on its own it +cannot upload or call the Admin API. The HTTP transport lives in a separate artifact that +`cloudinary-core` locates at runtime by class name. + +For server-side Java, depend on `cloudinary-http5`. It pulls in `cloudinary-core` transitively, so +declaring both is unnecessary. + +Maven: + +```xml + + com.cloudinary + cloudinary-http5 + X.Y.Z + +``` + +Gradle: + +```groovy +implementation 'com.cloudinary:cloudinary-http5:X.Y.Z' +``` + +Replace `X.Y.Z` with the current release. These pages carry no version literal because they are +not tied to a release; resolve it from Maven Central: + +```bash +curl -s https://repo1.maven.org/maven2/com/cloudinary/cloudinary-http5/maven-metadata.xml \ + | grep -o '[^<]*' +``` + +The current version is also shown on +[Maven Central](https://central.sonatype.com/artifact/com.cloudinary/cloudinary-http5). + +### Published artifacts + +| Artifact | Status | +|---|---| +| `cloudinary-http5` | Current. Apache HttpClient 5. Use this. | +| `cloudinary-core` | Current. Not usable alone; it has no HTTP client. | +| `cloudinary-taglib` | Current. JSP tags only; add alongside `cloudinary-http5`. See [use-with-jsp-taglib.md](use-with-jsp-taglib.md). | +| `cloudinary-http45`, `-http44`, `-http43`, `-http42` | Legacy, 1.x line. Last published 1.39.0; no 2.x version of these artifacts exists. | +| `cloudinary-test-common` | Internal test helper. Not for application code. | + +A dependency on `cloudinary-http45` with a 2.x version number does not resolve, because that +combination has never been published. + +## Missing HTTP adapter + +If `cloudinary-core` is on the classpath without an HTTP adapter, the `Cloudinary` constructor +throws: + +``` +java.lang.UnknownError: Can't find Cloudinary platform adapter + [com.cloudinary.android.UploaderStrategy,com.cloudinary.http5.UploaderStrategy] +``` + +- It is thrown from the `Cloudinary` constructor, before any API call. +- `java.lang.UnknownError` extends `Error`, not `Exception`. A `catch (Exception e)` block does + not catch it; `catch (Throwable t)` or `catch (Error e)` does. + +The fix is to add `cloudinary-http5` to the classpath rather than to catch the error. + +## Your first call + +```java +import com.cloudinary.Cloudinary; +import com.cloudinary.utils.ObjectUtils; +import java.util.Map; + +public class FirstCall { + public static void main(String[] args) throws Exception { + // Reads the CLOUDINARY_URL environment variable. + Cloudinary cloudinary = new Cloudinary(); + + Map result = cloudinary.uploader().upload( + "https://res.cloudinary.com/demo/image/upload/sample.jpg", + ObjectUtils.asMap("public_id", "first_upload")); + + System.out.println(result.get("secure_url")); + System.out.println(cloudinary.url().generate("first_upload.jpg")); + } +} +``` + +See [configure-cloudinary.md](configure-cloudinary.md) for the ways to supply credentials, and +[get-credentials.md](get-credentials.md) if you do not have any yet. + +## The shape of the API + +All entry points are methods on a `Cloudinary` instance: + +| Call | Returns | Use for | +|---|---|---| +| `cloudinary.uploader()` | `Uploader` | Uploading, destroying, renaming, tagging, archives | +| `cloudinary.api()` | `Api` | Admin API: listing, resource details, folders, metadata fields, usage | +| `cloudinary.url()` | `Url` | Building delivery URLs and HTML tags | +| `cloudinary.search()` | `Search` | Search API over assets | +| `cloudinary.searchFolders()` | `SearchFolders` | Search API over folders | + +`Uploader` and `Api` methods almost all take a trailing `Map` of options. Use +`ObjectUtils.asMap(k1, v1, k2, v2, ...)` to build one, or `ObjectUtils.emptyMap()` when there are +no options to pass. + +Responses are `Map` (Admin API calls return `ApiResponse`, which implements `Map`). There are no +typed response objects, so fields are read by string key and cast. Field names are the raw API +field names: `public_id`, `secure_url`, `asset_id`, and so on. + +`Url` is a mutable builder and is not safe to share across threads. `Cloudinary` is cheap to +construct and safe to hold as a singleton; call `cloudinary.url()` for each URL you build. + +## Java version + +The 2.x line targets Java 8 and above. The 1.x line supported Java 6. diff --git a/docs/moderate-upload.md b/docs/moderate-upload.md new file mode 100644 index 00000000..1079a617 --- /dev/null +++ b/docs/moderate-upload.md @@ -0,0 +1,134 @@ +# Moderate uploaded content + +## Requesting moderation + +```java +Map result = cloudinary.uploader().upload(file, ObjectUtils.asMap( + "public_id", "ugc/submission_1", + "moderation", "manual")); +``` + +Values are the moderation kind: `manual` for human review, or an add-on identifier such as +`aws_rek` / `google_video_moderation` / `perception_point` for automatic moderation. Add-on kinds +require an active subscription; without one the upload is rejected — see +[troubleshoot-errors.md](troubleshoot-errors.md). + +## Moderation and delivery + +By default, moderation status does not gate delivery. A `pending` moderated asset is still +delivered: its delivery URL returns HTTP 200 with the full image body. Uploading with +`moderation=manual` and then fetching the returned `secure_url` gives: + +``` +HTTP status for pending asset = 200 content-type=image/jpeg len=109669 +``` + +Moderation status is metadata that your own application code gates on. + +Blocking delivery of non-approved assets is configurable per product environment by Cloudinary +support. It is not an upload parameter and cannot be enabled from this SDK. For a hard gate, either +request that configuration or withhold the URL until you have checked the status yourself, as +described under "Gating delivery in your application" below. + +## Where the moderation status appears + +The status is returned in different fields depending on the call: + +| Call | `moderation` | `moderation_status` | +|---|---|---| +| `uploader().upload(...)` | `[{kind=manual, status=pending}]` | absent (`null`) | +| `api().resource(id, ...)` | `[{kind=manual, status=pending}]` | `pending` | +| `api().update(id, ...)` | `[{updated_at=..., kind=manual, status=approved}]` | `approved` | +| `search().execute()` resources | not present | `pending` (flat, plus `moderation_kind`) | + +The upload response has no flat `moderation_status` key; `result.get("moderation_status")` returns +`null` there. Read the status from the `moderation` list: + +```java +Map result = cloudinary.uploader().upload(file, ObjectUtils.asMap( + "public_id", "ugc/submission_1", "moderation", "manual")); + +List moderation = (List) result.get("moderation"); +String status = moderation == null || moderation.isEmpty() + ? null // asset was not submitted for moderation + : (String) moderation.get(0).get("status"); +// status -> "pending" +``` + +A helper that reads every response shape: + +```java +static String moderationStatus(Map response) { + Object flat = response.get("moderation_status"); + if (flat instanceof String) { + return (String) flat; // resource(), update(), search() results + } + Object list = response.get("moderation"); + if (list instanceof List && !((List) list).isEmpty()) { + return (String) ((Map) ((List) list).get(0)).get("status"); // upload() + } + return null; // not moderated +} +``` + +When several moderation kinds are applied to one asset, `moderation` holds one entry per kind, each +with its own status. In that case check every entry, and decide whether your gate requires *all* +kinds to be approved or *any* one of them. + +## Statuses + +`pending`, `approved`, `rejected`. An asset that was not submitted for moderation has neither field +populated; this is distinct from `pending`. + +## Approving and rejecting + +```java +Map updated = cloudinary.api().update("ugc/submission_1", + ObjectUtils.asMap("moderation_status", "approved")); + +// -> moderation = [{updated_at=2026-08-27T09:54:34Z, kind=manual, status=approved}] +// -> moderation_status = approved +``` + +`api().update()` takes a `public_id`, not an `asset_id`; there is no asset-ID variant of `update`. +See [search-and-manage-assets.md](search-and-manage-assets.md). + +## Listing what is waiting for review + +```java +Map pending = cloudinary.api().resourcesByModeration( + "manual", "pending", ObjectUtils.asMap("max_results", 100)); + +for (Map asset : (List) pending.get("resources")) { + System.out.println(asset.get("public_id") + " " + asset.get("moderation_status")); +} +``` + +Paginate with `next_cursor` when there are more results than `max_results`. + +## Gating delivery in your application + +Because delivery is not gated by default, gate it in your own application: + +1. Upload with `moderation`, and store the returned `asset_id` (the `public_id` changes on rename). +2. Record the asset as unpublished in your own datastore and withhold its Cloudinary URL. +3. On approval — via your review UI or a moderation webhook — update your own flag, then start + serving the URL. + +The Cloudinary URL of an unapproved asset returns 200, not 404, so it cannot serve as the gate. + +If you use webhooks, verify the callback before acting on it: + +```java +boolean valid = cloudinary.verifyNotificationSignature( + requestBody, // raw body string + request.getHeader("X-Cld-Timestamp"), + request.getHeader("X-Cld-Signature"), + 7200L); // validity window, seconds +``` + +## Related + +- [upload-image.md](upload-image.md) +- [use-structured-metadata.md](use-structured-metadata.md) — for review notes on an asset +- [Moderation documentation](https://cloudinary.com/documentation/moderate_assets.md) diff --git a/docs/platform-capabilities.md b/docs/platform-capabilities.md new file mode 100644 index 00000000..4eed8d7c --- /dev/null +++ b/docs/platform-capabilities.md @@ -0,0 +1,95 @@ +# Platform capabilities and SDK scope + +This page maps Cloudinary platform capabilities to the part of this SDK that implements them, or to +the route that provides them when the SDK does not. For a capability listed as "not in this SDK", +use the route named in the table. + +## Upload and ingest + +| I want to | In this SDK? | How | +|---|---|---| +| Upload a file, URL, or bytes | Yes | `uploader().upload(...)` — [upload-image.md](upload-image.md) | +| Upload a large file in chunks | Yes | `uploader().uploadLarge(...)` — [upload-large-video.md](upload-large-video.md) | +| Upload a non-media file | Yes | `uploader().uploadLargeRaw(...)`, or `resource_type=raw` | +| Sign a browser/mobile upload | Yes | `uploader().signRequestParams(...)` — [sign-browser-upload.md](sign-browser-upload.md) | +| Tag assets at or after upload | Yes | `tags` option; `uploader().addTag(...)` | +| Attach typed, validated fields | Yes | `metadata` option — [use-structured-metadata.md](use-structured-metadata.md) | +| Request moderation | Yes | `moderation` option — [moderate-upload.md](moderate-upload.md) | +| Report upload progress | No, on the server | `ProgressCallback` overloads compile but throw `IllegalArgumentException` with `cloudinary-http5`. Progress reporting is available on Android only. | +| Show a drop-in upload UI | No | The [Upload Widget](https://cloudinary.com/documentation/upload_widget.md), a JavaScript component. Your server signs; see [sign-browser-upload.md](sign-browser-upload.md). | +| Ingest from S3/Dropbox/etc. | No | [Auto-upload mappings](https://cloudinary.com/documentation/fetch_remote_images.md#auto_upload_remote_files), configured once via `api().createUploadMapping(...)`, then delivery-time fetch. | + +## Delivery and transformation + +| I want to | In this SDK? | How | +|---|---|---| +| Build a delivery URL | Yes | `url().generate(...)` — [transform-and-deliver-image.md](transform-and-deliver-image.md) | +| Resize, crop, effects, overlays | Yes | `Transformation` builder | +| Automatic format and quality | Yes | `.fetchFormat("auto").quality("auto")` | +| Emit an `` or `