diff --git a/.bito.yaml b/.bito.yaml new file mode 100644 index 0000000..1217a08 --- /dev/null +++ b/.bito.yaml @@ -0,0 +1,18 @@ +# Generated by seed-golden-context | Last updated: 2026-05-11 +suggestion_mode: comprehensive +post_description: true +post_changelist: true +exclude_files: '' +exclude_draft_pr: false +secret_scanner_feedback: true +linters_feedback: true +repo_level_guidelines_enabled: true +sequence_diagram_enabled: true +custom_guidelines: + general: + - name: 'Review Posture' + path: './.bito/guidelines/review-posture.txt' + - name: 'Repo Truth And Alignment' + path: './.bito/guidelines/repo-truth-and-boundaries.txt' + - name: 'Domain Invariants' + path: './.bito/guidelines/domain-invariants.txt' diff --git a/.bito/guidelines/domain-invariants.txt b/.bito/guidelines/domain-invariants.txt new file mode 100644 index 0000000..0496bcc --- /dev/null +++ b/.bito/guidelines/domain-invariants.txt @@ -0,0 +1,49 @@ +Critical invariants for contentful.java. Violations are bugs, not style issues. + +POST-PROCESSING PIPELINE ORDER (ResourceFactory.array()): + 1. localizeResources() — apply locale fallback chain + 2. mapResources() — index assets and entries by ID + 3. setRawFields() — capture raw JSON field map before any mutation + 4. resolveRichTextField() — parse rich text JSON into CDARich* node tree + 5. resolveLinks() — replace link stubs with resolved CDAResource references + Never reorder these steps without understanding the full cascade of side effects. + +LINK RESOLUTION: + - Unresolved links are silent placeholder objects, NOT exceptions. Consumer code must handle null/stub gracefully. + - Include depth max is 10 (API limit). Links beyond that depth remain unresolved. + - Cross-space resolution only resolves the first level of cross-space references. + +PUBLIC API CONTRACT: + - CDAClient, CDAClient.Builder, CDAEntry, CDAAsset, CDAArray, SynchronizedSpace, and all CDAResource subclasses are public API. + - Adding new builder methods is non-breaking. Removing or renaming them is a breaking change requiring a major version bump. + - The sys.* fields are always returned, regardless of select() usage. + +ANDROID COMPATIBILITY: + - Source/target must remain Java 1.8. Do not use APIs requiring Java 9+. + - Do not introduce Android API calls above API 21 in main source. + - TlsSocketFactory only activates below Android API 20 — do not change this guard. + - logSensitiveData MUST default to false — auth tokens must never be logged by default. + +OKHTTP / ANDROID: + - okhttp-jvm is declared as a dependency for JVM consumers. + - Android consumers must exclude okhttp-jvm and depend on okhttp-android — this is a known setup step (documented in README). + +SENSITIVE DATA: + - logSensitiveData defaults to false. Never flip in production code. AuthorizationHeaderInterceptor must redact the token by default. + +TRANSFORMQUERY: + - observeAndTransform() auto-injects a select filter — only annotated fields are fetched. + - Rich text fields are NOT accessible via TransformQuery — consumers must use rawFields or direct HTTP. + - Only works with CDA, not CMA. + +SYNC: + - Sync tokens are environment-specific. Passing a SynchronizedSpace from environment A to a client pointed at environment B produces incorrect deltas. + - Deleted resources appear ONLY in deletedEntries()/deletedAssets() — they do not appear in the items() list. + +CROSS-SPACE: + - Maximum 20 additional spaces (21 total). Exceeding this limit requires a different architecture. + - Cross-space errors surface via CDAArray.getErrors() — they do not throw. + +RELEASE: + - GPG signing is required for Maven Central publication. Do not attempt release without the Contentful GPG key. + - master is the trunk branch. There is no main branch. diff --git a/.bito/guidelines/repo-truth-and-boundaries.txt b/.bito/guidelines/repo-truth-and-boundaries.txt new file mode 100644 index 0000000..7a200a8 --- /dev/null +++ b/.bito/guidelines/repo-truth-and-boundaries.txt @@ -0,0 +1,9 @@ +Use the repository's written documentation as review context and check whether the change matches documented intent. + +- Start from README.md, ARCHITECTURE.md, AGENTS.md, CONTRIBUTING.md, and docs/ADRs/ for architectural context. +- Check whether code, tests, and documentation all tell the same story. Flag mismatches between implementation and documented architecture or ADRs. +- Treat AGENTS.md as the authoritative guide for sharp edges and invariants. If a change violates an invariant documented there, flag it. +- Public API changes (CDAClient builder methods, CDAResource subclasses, query builder methods) require extra scrutiny — this is a published library used by external consumers. +- Architecture-significant changes (new dependency, new module, new HTTP interceptor, new deserialization path) should be accompanied by an ADR update or new ADR in docs/ADRs/. +- If CI or another required check already enforces a merge rule, do not ask for duplicate PR sections or manual checklists. +- The SDK targets Java 8 (source/target 1.8) and Android API 21 minimum — flag any change that requires a higher Java or Android API level. diff --git a/.bito/guidelines/review-posture.txt b/.bito/guidelines/review-posture.txt new file mode 100644 index 0000000..36dae22 --- /dev/null +++ b/.bito/guidelines/review-posture.txt @@ -0,0 +1,9 @@ +Review this pull request like the tech lead of the contentful.java project — the official Java/Android SDK for Contentful's Content Delivery API. + +- Prefer a few high-signal findings to a long list of minor or style-only comments. +- Prioritize: public API contract changes, behavioral correctness in link resolution and deserialization, Android compatibility, backwards compatibility, and security (auth header handling, sensitive data logging). +- Flag any change to the post-processing pipeline in ResourceFactory (localization → raw fields → rich text → link resolution) — order matters and regressions are silent. +- Keep feedback actionable: explain why it matters, how it would surface in practice, and the clearest next step. +- If a concern is only a risk or assumption rather than a confirmed bug, say that clearly. +- If you find no issues, say so explicitly and call out any residual uncertainty that still deserves human attention. +- Do not ask for duplicate PR template sections or manual checklists when CI already enforces them. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7a6a65d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,62 @@ + + +# Agent Guide + +Read this file first. It tells you where to find context in this repo. + +## Quick Reference + +| What you need | Where to look | +|---|---| +| How this repo is structured | [ARCHITECTURE.md](./ARCHITECTURE.md) | +| How to build/test/run | [CONTRIBUTING.md](./CONTRIBUTING.md) | +| Why decisions were made | [docs/ADRs/](./docs/ADRs/) | +| What this repo does | [README.md](./README.md) | +| PR review rules | [.bito/guidelines/](./.bito/guidelines/) | +| Active specs/work | [docs/specs/](./docs/specs/) | + +## Sharp Edges & Invariants + +- **Link resolution order matters** — `ResourceUtils.resolveLinks()` must run after localization and raw-field capture. Never reorder the post-processing pipeline in `ResourceFactory.array()` without understanding all downstream side effects. +- **Unresolved links are silent, not exceptions** — if an included entry's depth exceeds the API limit (10), unresolved links remain as placeholder objects. Code that dereferences resolved links must handle null/stub gracefully. +- **`sys` fields are always present** — the SDK enforces that `sys.*` attributes are always returned. When using `.select()` with `fields.*` selections, `.withContentType()` must be called first or the SDK throws a client-side `IllegalStateException` before the request is sent. Selecting only `sys` fields does not require a content type. +- **`rawFields` is the only path to raw rich text JSON** — `TransformQuery` (unwrapping) does not expose raw rich text; use `CDAEntry.rawFields` or make a direct HTTP request. +- **Cross-space token limit is 20** — `setCrossSpaceTokens()` accepts at most 20 extra spaces (21 total). Only the first level of cross-space references is resolved. +- **Sync tokens are stateful and environment-aware** — passing a `SynchronizedSpace` from the wrong environment to `client.sync()` will produce incorrect deltas. +- **Android: never depend on `okhttp-jvm` directly when using `okhttp-android`** — exclude `okhttp-jvm` from this library to avoid duplicate-class errors on Android (see README and ADR 0002). +- **`TlsSocketFactory` only activates below Android API 20** — do not remove or gate it differently; it protects older devices. +- **`TransformQuery.observeAndTransform()` auto-injects a `select` filter** — if the annotation scanner has a bug, fields are silently dropped, not thrown as errors. Always validate field mapping in tests. +- **`logSensitiveData` defaults to `true`** — always set this to `false` in production builds to avoid logging auth tokens. +- **Release requires GPG signing** — the Maven Central publication flow requires the Contentful GPG key imported locally. Do not attempt to release without it. +- **`master` is the trunk** — there is no `main` branch. All CI and release flows target `master`. + +## Key Conventions + +- **Commit format**: Conventional Commits preferred (`feat:`, `fix:`, `chore:`, `docs:`, etc.) — see recent history for examples +- **Branch strategy**: Trunk-based development off `master`; feature branches merged via PR with squash or merge commit +- **Test location**: `src/test/java/…/cda/` mirroring the main package; fixtures in `src/test/resources/` +- **Java version**: Java 8 source/target (`java.version=1.8` in `pom.xml`); devcontainer uses `eclipse-temurin:8-jdk-jammy` by default +- **Build tool**: Maven Wrapper (`./mvnw`) — do not require a globally installed Maven + +## Integration Points + +**Upstream (this repo consumes):** +- Contentful Content Delivery API — `https://cdn.contentful.com` +- Contentful Content Preview API — `https://preview.contentful.com` + +**Downstream (consumes this repo):** +- Java/Kotlin server and Android applications via Maven Central (`com.contentful.java:java-sdk`) +- `rich-text-renderer-java` companion library (consumes the `CDARich*` node model) + +## Build & Quality + +```bash +# Spin up the devcontainer (first time or after Docker changes) +devcontainer up --workspace-folder . + +# Full verification loop inside the container +devcontainer exec --workspace-folder . bash -lc "./mvnw -B test" + +# Checkstyle only +devcontainer exec --workspace-folder . bash -lc "./mvnw -B checkstyle:checkstyle" +``` diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..91cb350 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,122 @@ + + +# Architecture + +## Overview + +`contentful.java` is the official Java SDK for Contentful's Content Delivery API (CDA) and Content Preview API (CPA). It is a pure-Java library (ships as a JAR) that wraps the Contentful HTTP APIs in an idiomatic Java client, providing automatic link resolution, locale fallback, rich text parsing, sync, and optional reactive (RxJava 3) access patterns. + +## System Context + +```mermaid +graph TD + A[Java / Android Application] --> B[CDAClient] + B --> C[Contentful Content Delivery API] + B --> D[Contentful Content Preview API] + B --> E[Cross-Space CDA APIs] + C --> F[Contentful Platform] + D --> F + E --> F +``` + +Consumers are JVM and Android applications. The SDK wraps all outbound HTTP via OkHttp + Retrofit and returns parsed, link-resolved Java objects. No server-side component; no persistent store. + +## Internal Structure + +| Package / Directory | Purpose | +|---|---| +| `com.contentful.java.cda` | Root package — `CDAClient`, query builders, resource model classes, sync, serialization | +| `com.contentful.java.cda.interceptor` | OkHttp interceptors: auth header, user-agent, error translation, logging | +| `com.contentful.java.cda.rich` | Rich Text node model (`CDARichDocument`, `CDARichParagraph`, etc.) and `RichTextFactory` | +| `com.contentful.java.cda.image` | `ImageOption` helper for Images API URL construction | +| `src/main/resources/…/build` | Build-time generated `GeneratedBuildParameters.java` (injects SDK version) | +| `src/test/java/…/cda` | Unit + mock-server tests mirroring the main package | +| `src/test/java/…/cda/integration` | Integration tests against live Contentful environments | +| `src/test/resources/` | Canned JSON fixtures used by mock-server tests | +| `.devcontainer/` | Reproducible dev environment (eclipse-temurin JDK on Ubuntu Jammy) | +| `.github/workflows/` | CI pipeline (runs tests inside the devcontainer) | + +## Data Flow + +1. **Client construction** — `CDAClient.builder()` configures space, token, environment, optional preview mode, cross-space tokens, and HTTP client settings. Retrofit is built with a Gson converter factory and a RxJava3 call adapter factory. OkHttp interceptors inject the `Authorization` header, the `X-Contentful-User-Agent` header, and error/logging behavior. + +2. **Query execution** — `CDAClient.fetch(Type.class)` returns an `AbsQuery` (or `FetchQuery`) that builds a URL and dispatches via `CDAService` (a Retrofit interface). Calls can be synchronous (`.all()`, `.one()`), callback-based (`CDACallback`), or reactive (`.observe()`/`RxJava Flowable`). + +3. **Deserialization** — Gson deserializes the raw JSON response into `CDAArray`. `ResourceDeserializer` dispatches each item to the concrete `CDAResource` subclass based on the `sys.type` field. + +4. **Post-processing** — `ResourceFactory.array()` orchestrates: + - `ResourceUtils.localizeResources()` — applies locale fallback chain from the space's locale list + - `ResourceUtils.mapResources()` — indexes assets and entries by ID + - `ResourceUtils.setRawFields()` — preserves the unprocessed field map for rich text access + - `RichTextFactory.resolveRichTextField()` — parses rich text JSON into the `CDARich*` node tree + - `ResourceUtils.resolveLinks()` — iterates each entry's content-type field definitions to find Link and Array-of-Link fields, then replaces link stubs with the already-indexed `CDAResource` objects from `array.assets()` / `array.entries()` (in-memory object graph). Links not present in the index are silently dropped, not thrown as errors. + +5. **Result** — `CDAArray` (or single `CDAResource`) is returned to the caller with fully resolved links and localized fields. + +6. **Sync path** — `CDAClient.sync()` uses `SyncQuery` and `SynchronizedSpace`. Subsequent sync calls pass the token from the prior `SynchronizedSpace` to fetch only the delta. Deleted resources appear in `deletedEntries()` / `deletedAssets()` sets. + +## Key Dependencies + +| Dependency | Version | Why it's here | +|---|---|---| +| `retrofit2` | 2.11.0 | HTTP client abstraction — declarative API definition via `CDAService` interface | +| `okhttp3` / `okhttp-jvm` | 5.1.0 | Underlying HTTP engine; customizable via `defaultCallFactoryBuilder()` | +| `rxjava3` | 3.1.9 | Optional reactive access pattern via `.observe()` | +| `adapter-rxjava3` | 2.11.0 | Retrofit adapter bridging Retrofit `Call` to RxJava3 `Flowable` | +| `converter-gson` | 2.11.0 | JSON deserialization via Gson | +| `gson` | 2.11.0 | Core JSON parsing; custom `ResourceDeserializer` registered for polymorphic types | +| `named-regexp` | 0.2.5 | Named capture group support used in locale/link processing | +| `android` (optional) | 4.1.1.4 | Platform detection for callback executor (main thread on Android, background thread on JVM) | +| `junit 4` | 4.13.1 | Test framework | +| `mockwebserver` | 5.1.0 | Local OkHttp mock server for unit tests | +| `mockito-core` | 2.23.4 | Mocking framework | +| `truth` | 0.42 | Fluent test assertions | + +See [ADR 0002](./docs/ADRs/0002-retrofit-okhttp-as-http-layer.md) and [ADR 0003](./docs/ADRs/0003-rxjava3-reactive-access.md) for rationale on key dependency choices. + +## Configuration + +| Builder Method / Flag | Purpose | Default | +|---|---|---| +| `.setSpace(id)` | Contentful Space ID | Required | +| `.setToken(token)` | CDA or CPA access token | Required | +| `.setEnvironment(id)` | Environment ID | `master` | +| `.preview()` | Switches base URL to Content Preview API | `false` (CDA) | +| `.setCrossSpaceTokens(map)` | Tokens for additional spaces (max 20) | `null` (disabled) | +| `.setCallFactory(factory)` | Replace the underlying OkHttpClient | Auto-configured | +| `.defaultCallFactoryBuilder()` | Returns a pre-configured `OkHttpClient.Builder` for customization | — | +| `.setLogger(logger)` | Custom `Logger` implementation | `null` | +| `.setLogLevel(level)` | Verbosity of HTTP logs | `NONE` | +| `.logSensitiveData(bool)` | Whether to log auth header values | `false` | +| `.setCallbackExecutor(executor)` | Executor for async callbacks | Platform default (main thread on Android) | + +## Integration Points + +### Upstream (this repo consumes) +- **Contentful Content Delivery API** — `https://cdn.contentful.com` — primary data source +- **Contentful Content Preview API** — `https://preview.contentful.com` — preview (unpublished) content + +### Downstream (consumes this repo) +- **Java / Kotlin server applications** — backend content rendering, import pipelines +- **Android applications** — content delivery on mobile (minimum Android API 21 / Java 8) +- **`rich-text-renderer-java`** — companion library that consumes the `CDARich*` node model from this SDK to render rich text + +## Android Specifics + +The library is distributed as a JAR (not AAR). It uses `android.os.Handler` and `android.os.Looper` (in `Platform.java`) for main-thread callback delivery on Android — both are stable public APIs. The `TlsSocketFactory` is only activated below Android API 20; on modern Android the system TLS defaults are used. + +OkHttp 5 splits platform artifacts: JVM users get `okhttp-jvm` (included by default). Android apps should depend on `okhttp-android` and exclude `okhttp-jvm` to avoid duplicate-class errors. See README for Gradle snippet. + +## Release & Distribution + +- **Registry**: Maven Central (via Sonatype Central Publishing plugin), `groupId: com.contentful.java`, `artifactId: java-sdk` +- **Pre-release**: Sonatype snapshots and jitpack.io +- **Versioning**: Semantic Versioning — patch = bug fixes / dependency updates; minor = new non-breaking features; major = breaking API changes +- **Branching**: Trunk-based development off `master` +- **Release cadence**: On-demand +- **Build**: Maven Wrapper (`./mvnw`) — no global Maven installation required +- **Code coverage**: Codecov +- **Checkstyle**: Enforced at `verify` phase via `checkstyle.xml` +- **GPG signing**: Required for Maven Central publication (private key managed by Contentful infrastructure team) + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full release procedure. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 448f977..d032668 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,3 +30,69 @@ devcontainer exec --workspace-folder . bash 1. Fork the repository and create a branch for your change. 2. Run the relevant checks from the dev container. 3. Open a pull request with a short summary of the change and any follow-up context. + + + +## Testing + +- **Framework:** JUnit 4 +- **Location:** `src/test/java/com/contentful/java/cda/` (unit + mock-server tests); `src/test/java/com/contentful/java/cda/integration/` (integration tests against live environments) +- **Fixtures:** `src/test/resources/` — canned JSON responses used by mock-server tests +- **Run all (unit):** + +```bash +./mvnw -B test +``` + +- **Integration tests** connect to live Contentful spaces using hardcoded read-only credentials (space IDs and tokens are baked into the test classes under `src/test/java/…/cda/integration/`). No environment variables are required. Integration tests are included in the `./mvnw -B test` run and therefore execute in CI. + +## Commit Convention + +Conventional Commits are preferred: + +``` +type: description +``` + +Common types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci` + +Examples: +``` +feat: add cross-space reference resolution support +fix: strip extensions from locale identifiers +chore: update okhttp to 5.1.0 +``` + +## Branch Strategy + +- `master` — trunk branch; all development targets master directly or via short-lived feature branches +- Feature branches — `feat/`, `fix/` +- Release branches are not used; releases are cut directly from master + +## Release Process + +Releases are on-demand and require the Contentful GPG key imported locally (obtain from your team lead): + +```bash +# 1. Ensure all tests pass on master +./mvnw -B test + +# 2. Add -SNAPSHOT postfix to in pom.xml if not already present + +# 3. Prepare the release (prompts for release version and next snapshot version) +./mvnw release:prepare + +# 4. Perform the release (builds, signs, and publishes to Maven Central) +./mvnw release:perform +``` + +Artifacts are published to Maven Central under `com.contentful.java:java-sdk`. Pre-releases are available via Sonatype snapshots and jitpack.io. + +## CI/CD + +| Job | Trigger | What it does | +|---|---|---| +| `Test (Java 8)` | Push or PR to `master` | Spins up devcontainer, runs `./mvnw -B test` | + +CI runs inside the same devcontainer used for local development (`eclipse-temurin:8-jdk-jammy`), ensuring environment parity. The `JAVA_VERSION` build arg defaults to `8` and can be overridden. + diff --git a/README.md b/README.md index f7cf631..6ddf9dd 100644 --- a/README.md +++ b/README.md @@ -529,3 +529,17 @@ Code of Conduct Contentful wants to provide a safe, inclusive, welcoming, and harassment-free space and experience for all participants, regardless of their identity markers. [Full Code of Conduct](https://github.com/contentful-developer-relations/community-code-of-conduct). + + + +For Agents & Contributors +========================= + +| Document | What it covers | +|---|---| +| [AGENTS.md](./AGENTS.md) | Agent-first context directory — read this first | +| [ARCHITECTURE.md](./ARCHITECTURE.md) | Internal structure, data flows, component map, integration points | +| [CONTRIBUTING.md](./CONTRIBUTING.md) | Development setup, workflow, release process, CI | +| [docs/ADRs/](./docs/ADRs/) | Why things look the way they do — architecture decisions | +| [docs/specs/](./docs/specs/) | Active and recent implementation specs | +| [.bito/guidelines/](./.bito/guidelines/) | PR review posture and domain invariants | diff --git a/docs/ADRs/0001-maven-as-build-tool.md b/docs/ADRs/0001-maven-as-build-tool.md new file mode 100644 index 0000000..d56a79c --- /dev/null +++ b/docs/ADRs/0001-maven-as-build-tool.md @@ -0,0 +1,23 @@ + + +# 0001 — Maven as Build Tool + +## Status + +Accepted + +## Context + +The SDK is a pure-Java library targeting both JVM and Android consumers. A build tool was needed that: integrates with Maven Central (the standard distribution channel for Java libraries), supports source and javadoc JAR attachment, GPG signing, checkstyle enforcement, and code coverage. Gradle is an alternative in the Java/Android space but introduces Groovy/Kotlin DSL complexity and a less direct integration story with the Sonatype Central Publishing plugin at the time this library was established. + +## Decision + +Maven (via the Maven Wrapper, `./mvnw`) was chosen as the build tool. The `pom.xml` defines dependencies, the compiler plugin (Java 1.8 source/target), checkstyle, JaCoCo coverage, the Sonatype Central Publishing plugin for Maven Central distribution, and GPG signing. The Maven Wrapper ensures contributors and CI can build without a globally installed Maven. + +## Consequences + +- Reproducible builds via `./mvnw` with no global Maven requirement. +- Direct, well-documented path for Maven Central publication via `mvn release:prepare` / `mvn release:perform`. +- GPG signing is a hard requirement at the `verify` phase — releasing without the Contentful GPG key fails. +- Checkstyle is enforced at `verify` phase; violations fail the build. +- Gradle-based Android consumers must use the Gradle dependency syntax (`implementation 'com.contentful.java:java-sdk:x.y.z'`), which works seamlessly since Maven Central serves standard JARs. diff --git a/docs/ADRs/0002-retrofit-okhttp-as-http-layer.md b/docs/ADRs/0002-retrofit-okhttp-as-http-layer.md new file mode 100644 index 0000000..73c2989 --- /dev/null +++ b/docs/ADRs/0002-retrofit-okhttp-as-http-layer.md @@ -0,0 +1,25 @@ + + +# 0002 — Retrofit + OkHttp as HTTP Layer + +## Status + +Accepted + +## Context + +The SDK needs to make HTTP requests to the Contentful CDA and CPA REST APIs, handle authentication headers, translate HTTP errors into typed exceptions, and support both synchronous and asynchronous call patterns. A raw `HttpURLConnection` approach was considered but offers no type-safe API definition, no interceptor chain, and poor async ergonomics. + +## Decision + +Retrofit 2 is used for the declarative API definition (`CDAService` interface with annotated methods). OkHttp is the underlying call factory, providing a composable interceptor chain used for: authorization headers (`AuthorizationHeaderInterceptor`), user-agent headers (`ContentfulUserAgentHeaderInterceptor`, `UserAgentHeaderInterceptor`), error translation (`ErrorInterceptor`), and optional request logging (`LogInterceptor`). + +OkHttp 5 (`okhttp-jvm`) is declared as a direct dependency. The `defaultCallFactoryBuilder()` method exposes the pre-configured `OkHttpClient.Builder` so consumers can add interceptors, set timeouts, or attach an HTTP cache without losing the SDK's built-in configuration. + +## Consequences + +- Type-safe HTTP definitions; easy to add new API endpoints. +- OkHttp 5 splits into `okhttp-jvm` and `okhttp-android` artifacts — Android consumers must exclude `okhttp-jvm` and depend on `okhttp-android` to avoid duplicate-class errors (documented in README and AGENTS.md). +- The interceptor chain is the correct extension point; do not bypass it by constructing a separate `OkHttpClient`. +- Retrofit's Gson converter factory is registered with a custom `ResourceDeserializer` for polymorphic CDA types — changing the converter factory requires re-registering this deserializer. +- OkHttp version upgrades must be validated against Android compatibility (minimum API 21). diff --git a/docs/ADRs/0003-rxjava3-reactive-access.md b/docs/ADRs/0003-rxjava3-reactive-access.md new file mode 100644 index 0000000..18045bd --- /dev/null +++ b/docs/ADRs/0003-rxjava3-reactive-access.md @@ -0,0 +1,22 @@ + + +# 0003 — RxJava 3 for Reactive Access + +## Status + +Accepted + +## Context + +Java applications (especially Android) commonly use reactive programming for async operations. The SDK needs to expose an async access pattern beyond callbacks. RxJava is the de facto reactive library for Java/Android. Version 3 is the current major version with active maintenance; RxJava 2 reached end-of-life. + +## Decision + +RxJava 3 (`io.reactivex.rxjava3:rxjava`) and the Retrofit RxJava3 adapter (`retrofit2:adapter-rxjava3`) are included as regular (non-optional) dependencies. `CDAClient.observe()` returns an `ObserveQuery` that wraps the Retrofit call in a `Flowable`, enabling reactive composition. The synchronous and callback paths remain available for consumers that do not use RxJava. + +## Consequences + +- RxJava 3 and Retrofit's adapter are included in all deployments, adding ~3 MB to the transitive dependency footprint. +- Consumers not using RxJava carry the dependency for no benefit — a future improvement could make it optional via a separate artifact. +- RxJava 3 is compatible with Java 8 and Android API 21+, matching the SDK's minimum requirements. +- Upgrading to a future RxJava major version (4+) would require updating `adapter-rxjava3` and potentially changing the `Flowable` return type, which is a breaking API change. diff --git a/docs/ADRs/0004-automatic-link-resolution.md b/docs/ADRs/0004-automatic-link-resolution.md new file mode 100644 index 0000000..c356837 --- /dev/null +++ b/docs/ADRs/0004-automatic-link-resolution.md @@ -0,0 +1,26 @@ + + +# 0004 — Automatic In-Memory Link Resolution + +## Status + +Accepted + +## Context + +The Contentful CDA returns entries and assets with links represented as stub objects (`sys.type: Link`). To be useful, consumer code needs fully resolved Java objects — traversing the raw includes array manually is verbose and error-prone. An alternative is to leave resolution to the consumer (as some lightweight HTTP wrappers do), but this significantly increases boilerplate for every SDK user. + +Source: Java SDK Contractor Handover Template (Glean, 2026-03). + +## Decision + +The SDK automatically resolves Entry and Asset links in API responses and builds an in-memory object graph. After deserialization, `ResourceFactory.array()` calls `ResourceUtils.resolveLinks()`, which traverses the includes section and replaces link stubs with references to the actual `CDAResource` objects. Circular references and nested includes are handled. If include depth exceeds the API limit (10), unresolved links are left as placeholder objects — not thrown as errors. + +## Consequences + +- Consumer code gets fully resolved Java objects; no manual includes traversal required. +- Circular reference handling adds complexity to `ResourceUtils.resolveLinks()` — changes here must be tested carefully against deeply nested and circular fixtures. +- Unresolved links are silent (placeholder objects), not exceptions — callers must null-check or handle missing resolution. +- The in-memory graph can be large for responses with high include depth; consumers should use `include(N)` intentionally and avoid `include(10)` for large spaces. +- Any change to resolution order or null-handling can break existing consumer apps silently. This code path is a high-risk zone. +- `rawFields` preserves the unprocessed JSON for cases (e.g., rich text) where the resolved object graph is insufficient. diff --git a/docs/ADRs/0005-devcontainer-for-reproducible-dev-env.md b/docs/ADRs/0005-devcontainer-for-reproducible-dev-env.md new file mode 100644 index 0000000..9dc1361 --- /dev/null +++ b/docs/ADRs/0005-devcontainer-for-reproducible-dev-env.md @@ -0,0 +1,25 @@ + + +# 0005 — Dev Container for Reproducible Development Environment + +## Status + +Accepted + +## Context + +Contributors to the SDK need a consistent Java development environment. Previously, contributors needed to install Java manually, and CI used a separate setup from the local workflow, leading to "works on my machine" issues. The DevEx team standardized SDK repos on devcontainers (DX-822) to give each repo its own reproducible dev environment and to align local and CI workflows. + +Source: Slack thread in `#prd-alpine-chat` by Tyler Collins (2026-03-21), PR #335. + +## Decision + +A `.devcontainer/` directory was added with a `Dockerfile` (based on `eclipse-temurin:${JAVA_VERSION}-jdk-jammy`) and `devcontainer.json`. The `JAVA_VERSION` build arg defaults to `8`, matching the SDK's `pom.xml` target. The `postCreateCommand` pre-fetches Maven dependencies offline so the first build is fast. CI (`.github/workflows/ci.yml`) uses the same devcontainer via the `@devcontainers/cli` package. + +## Consequences + +- Local and CI environments are identical — "works on my machine" issues are eliminated. +- Contributors must have Docker installed; the devcontainer CLI is installed automatically in CI. +- The `JAVA_VERSION` build arg allows testing against different JDK versions without changing the `Dockerfile`. +- VS Code users get the Red Hat Java extension pre-configured via `customizations.vscode.extensions`. +- Dependency pre-fetch (`mvnw -B -q -DskipTests dependency:go-offline`) adds ~30s to container creation but speeds up subsequent builds. diff --git a/docs/ADRs/0006-transform-query-annotation-mapping.md b/docs/ADRs/0006-transform-query-annotation-mapping.md new file mode 100644 index 0000000..05f7ac9 --- /dev/null +++ b/docs/ADRs/0006-transform-query-annotation-mapping.md @@ -0,0 +1,25 @@ + + +# 0006 — TransformQuery: Annotation-Based Entry Mapping + +## Status + +Accepted + +## Context + +Consumers often want to map CDA Entry responses to their own domain model classes rather than working with the generic `CDAEntry` and field maps. Without a mapping layer, consumers write repetitive boilerplate to extract typed fields from `CDAEntry.getField()`. A code generation approach (similar to ORM annotation processors) was considered but adds build complexity. + +Source: Java SDK Contractor Handover Template (Glean, 2026-03). + +## Decision + +`TransformQuery` provides a runtime annotation-based mapping system. Consumers annotate their POJO with `@ContentfulEntryModel(contentTypeId)` and fields with `@ContentfulField` (or `@ContentfulSystemField` for `sys.*` attributes). `CDAClient.observeAndTransform(MyType.class)` fetches entries and maps them to the annotated POJO. The `TransformQuery` path automatically injects a `select` filter based on the annotated fields to minimize response payload size. + +## Consequences + +- Reduced boilerplate for consumers — typed POJOs instead of generic field maps. +- The auto-injected `select` filter means only annotated fields are returned; non-annotated fields are null/default and silently absent. Bugs in annotation scanning produce missing data, not exceptions. +- Rich text fields are not accessible via unwrapping — consumers requiring raw rich text JSON must use `CDAEntry.rawFields` or a direct HTTP request. +- Only works with the CDA, not the CMA. +- Reflection-based at runtime — no compile-time safety for field name typos in annotations. diff --git a/docs/ADRs/0007-cross-space-reference-resolution.md b/docs/ADRs/0007-cross-space-reference-resolution.md new file mode 100644 index 0000000..9267c34 --- /dev/null +++ b/docs/ADRs/0007-cross-space-reference-resolution.md @@ -0,0 +1,29 @@ + + +# 0007 — Cross-Space Reference Resolution (v10.6.0) + +## Status + +Accepted + +## Context + +Contentful supports cross-space references, which allow entries to link to content in other spaces. Before v10.6.0, the SDK did not resolve these links — cross-space linked entries appeared as unresolved stubs. Consumers building multi-space architectures had to resolve cross-space links manually using separate clients. + +## Decision + +Added in v10.6.0 (PR #332). `CDAClient.Builder.setCrossSpaceTokens(Map)` accepts a map of `spaceId -> cdaToken` for up to 20 additional spaces. When the SDK encounters cross-space link stubs in a response, it fetches the linked content from the appropriate space using the configured token and includes it in the in-memory resolution graph. + +Constraints: +- Maximum 20 extra spaces (21 total including the main space). +- Only the first level of cross-space references is resolved (similar to `include=1` for cross-space links). +- The main space can still resolve up to 10 levels of includes independently. +- Cross-space fetch errors are returned via `CDAArray.getErrors()`, not thrown as exceptions. + +## Consequences + +- Multi-space architectures can now use the SDK without manual cross-space resolution. +- The 20-space cap is an API constraint; exceeding it requires a different architecture. +- Silent error handling via `getErrors()` means consumers must check for errors explicitly. +- Each additional space token increases the number of outbound HTTP requests for a single `fetch()` call when cross-space links are present. +- The first-level-only constraint for cross-space links is a known limitation — deeper resolution requires manual follow-up fetches. diff --git a/docs/ADRs/README.md b/docs/ADRs/README.md new file mode 100644 index 0000000..664fc25 --- /dev/null +++ b/docs/ADRs/README.md @@ -0,0 +1,37 @@ + + +# Architecture Decision Records + +This directory contains Architecture Decision Records (ADRs) for `contentful.java`. + +An ADR documents a significant technical decision: the context that made it necessary, the options considered, the choice made, and the consequences. ADRs are written at the time of the decision and are not retroactively updated. If a decision is reversed or superseded, a new ADR is written that references the old one. + +## Index + +| ADR | Title | Status | Date | +|---|---|---|---| +| [0001](./0001-maven-as-build-tool.md) | Maven as Build Tool | Accepted | 2026-05-11 | +| [0002](./0002-retrofit-okhttp-as-http-layer.md) | Retrofit + OkHttp as HTTP Layer | Accepted | 2026-05-11 | +| [0003](./0003-rxjava3-reactive-access.md) | RxJava 3 for Reactive Access | Accepted | 2026-05-11 | +| [0004](./0004-automatic-link-resolution.md) | Automatic In-Memory Link Resolution | Accepted | 2026-05-11 | +| [0005](./0005-devcontainer-for-reproducible-dev-env.md) | Dev Container for Reproducible Development Environment | Accepted | 2026-05-11 | +| [0006](./0006-transform-query-annotation-mapping.md) | TransformQuery: Annotation-Based Entry Mapping | Accepted | 2026-05-11 | +| [0007](./0007-cross-space-reference-resolution.md) | Cross-Space Reference Resolution (v10.6.0) | Accepted | 2026-05-11 | + +## Format + +``` +# YYYY-MM-DD: Title + +## Status +Proposed | Accepted | Deprecated | Superseded by ADR-XXXX + +## Context +What situation or constraint made this decision necessary? + +## Decision +What was decided? + +## Consequences +What are the results — good and bad — of this decision? +``` diff --git a/docs/specs/.gitkeep b/docs/specs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/specs/README.md b/docs/specs/README.md new file mode 100644 index 0000000..f44f9bd --- /dev/null +++ b/docs/specs/README.md @@ -0,0 +1,30 @@ + + +# Specs + +This directory contains implementation-level specs for active and recent work on `contentful.java`. + +Specs are small, focused documents that capture "what we're building now" for both engineers and agents. They are written before or alongside implementation and are marked as `status: done` once shipped. + +## Format + +``` +# YYYY-MM-DD: Title + +**Status:** Draft | In Progress | Done +**Author:** @name +**Related:** [DX-XXX](link), PR #NNN + +## Problem +What are we solving and why? + +## Design +How will it work? Include key decisions, API surface changes, and edge cases. + +## Acceptance Criteria +How do we know it's done? +``` + +## Active Specs + +_No active specs at this time._