diff --git a/README.md b/README.md index 685be15..61083b1 100644 --- a/README.md +++ b/README.md @@ -76,14 +76,14 @@ Install the Contentful dependency: com.contentful.java java-sdk - 10.5.26 + 10.6.0 ``` * _Gradle_ ```groovy -compile 'com.contentful.java:java-sdk:10.5.26' +compile 'com.contentful.java:java-sdk:10.6.0' ``` This library requires Java 8 (or higher version) or Android 21. @@ -259,6 +259,38 @@ CDAArray found = client.fetch(CDAEntry.class) This only resolves the first level of includes. `10` is the maximum number of levels to be included and should be used sparingly, since this will bloat up the response by a lot. +Cross-Space References +---------------------- + +In version 10.6.0 and later the library supports resolving cross-space references, which allows you to link content across multiple spaces. When cross-space tokens are configured, entries and assets from other spaces will be automatically included in the response's `includes` section and resolved by the library link resolution. + +To enable cross-space reference resolution, provide access tokens for the additional spaces: + +```java +Map crossSpaceTokens = new HashMap<>(); +crossSpaceTokens.put("space-id-1", "cda-token-for-space-1"); +crossSpaceTokens.put("space-id-2", "cda-token-for-space-2"); + +CDAClient client = CDAClient.builder() + .setSpace("main-space-id") + .setToken("main-space-token") + .setCrossSpaceTokens(crossSpaceTokens) + .build(); + +// Cross-space references will now be automatically resolved +CDAArray entries = client.fetch(CDAEntry.class) + .include(2) + .all(); +``` + +**Limitations:** +- Maximum 20 extra spaces can be configured (21 total including the main space) +- Only the first level of cross-space references is resolved (similar to `include=1` for cross-space) +- The main space can still resolve up to 10 levels of includes +- Cross-space errors are returned in the `CDAArray.getErrors()` method + +For more information, see the [Contentful Resource Links documentation](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/resource-links). + Unwrapping ---------- @@ -303,7 +335,7 @@ In addition to returning the Content in a fashion flexible for various use-cases > * A `locale` can be used to specify a given locale of this entry. If no locale is given, the default locale will be used. > * `@ContentfulSystemField` is used for CDAEntries attributes (`sys.id`, etc) to be inserted. > * If another type is wanted to be transformed, it should have `@ContentfulEntryModel`-annotation specified similarly as in `Cat`. -> * **Limitation on Unwrapping**: Using Unwrapping does not currently allow direct access to the raw JSON for rich text fields, as the SDK automatically transforms fields into the custom model structure. For cases where raw JSON is needed: +> * **Limitation on Unwrapping**: Using Unwrapping does not currently allow direct access to the raw JSON for rich text fields, as the library automatically transforms fields into the custom model structure. For cases where raw JSON is needed: > * Use the `rawFields` map in `CDAEntry` to directly access the unprocessed JSON of any field, including rich text. > * Alternatively, make a direct HTTP request to the Contentful API to retrieve the full raw JSON response. @@ -387,7 +419,7 @@ CDAClient cdaClient = clientBuilder.setCallFactory(httpClient).build(); Android and OkHttp 5 -------------------- -OkHttp 5 splits platform artifacts. This SDK depends on `okhttp-jvm` so it works out of the box for JVM users. For Android apps, depend on `okhttp-android` and exclude `okhttp-jvm` from this SDK to avoid duplicate-class errors. +OkHttp 5 splits platform artifacts. This library depends on `okhttp-jvm` so it works out of the box for JVM users. For Android apps, depend on `okhttp-android` and exclude `okhttp-jvm` from this library to avoid duplicate-class errors. Gradle (Kotlin DSL): @@ -396,7 +428,7 @@ dependencies { implementation(platform("com.squareup.okhttp3:okhttp-bom:5.1.0")) implementation("com.squareup.okhttp3:okhttp-android") - implementation("com.contentful.java:java-sdk:10.5.24") { + implementation("com.contentful.java:java-sdk:10.6.0") { exclude(group = "com.squareup.okhttp3", module = "okhttp-jvm") } } @@ -409,7 +441,7 @@ dependencies { implementation platform('com.squareup.okhttp3:okhttp-bom:5.1.0') implementation 'com.squareup.okhttp3:okhttp-android' - implementation('com.contentful.java:java-sdk:10.5.24') { + implementation('com.contentful.java:java-sdk:10.6.0') { exclude group: 'com.squareup.okhttp3', module: 'okhttp-jvm' } } diff --git a/pom.xml b/pom.xml index 76c1569..a9d857f 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ com.contentful.java java-sdk - 10.5.26 + 10.6.0 jar ${project.groupId}:${project.artifactId} diff --git a/src/main/java/com/contentful/java/cda/CDAClient.java b/src/main/java/com/contentful/java/cda/CDAClient.java index 2d0f2ac..b033d01 100644 --- a/src/main/java/com/contentful/java/cda/CDAClient.java +++ b/src/main/java/com/contentful/java/cda/CDAClient.java @@ -8,6 +8,7 @@ import com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section.OperatingSystem; import com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section.Version; import com.contentful.java.cda.interceptor.ErrorInterceptor; +import com.contentful.java.cda.interceptor.HeaderInterceptor; import com.contentful.java.cda.interceptor.LogInterceptor; import com.contentful.java.cda.interceptor.UserAgentHeaderInterceptor; import io.reactivex.rxjava3.core.Flowable; @@ -35,6 +36,7 @@ import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; +import java.util.Base64; import static com.contentful.java.cda.Constants.ENDPOINT_PROD; import static com.contentful.java.cda.Constants.PATH_CONTENT_TYPES; @@ -58,6 +60,7 @@ */ public class CDAClient { private static final int CONTENT_TYPE_LIMIT_MAX = 1000; + private static final int CROSS_SPACE_TOKENS_MAX = 20; final String spaceId; @@ -75,6 +78,8 @@ public class CDAClient { final boolean logSensitiveData; + final boolean hasCrossSpaceTokens; + CDAClient(Builder builder) { this(new Cache(), Platform.get().callbackExecutor(), @@ -92,6 +97,8 @@ public class CDAClient { this.token = builder.token; this.preview = builder.preview; this.logSensitiveData = builder.logSensitiveData; + this.hasCrossSpaceTokens = builder.crossSpaceTokens != null + && !builder.crossSpaceTokens.isEmpty(); } private void validate(Builder builder) { @@ -579,6 +586,8 @@ public static class Builder { Section application; Section integration; + Map crossSpaceTokens; + private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient(); Builder() { @@ -720,6 +729,21 @@ Converter.Factory createOrGetConverterFactory(Builder clientBuilder) { return converterFactory; } + /** + * Encodes cross-space tokens to a base64-encoded JSON string for the + * x-contentful-resource-resolution header. + * + * @param tokens map of space IDs to access tokens + * @return base64-encoded JSON string + */ + private String encodeCrossSpaceTokens(Map tokens) { + Map> payload = new HashMap<>(); + payload.put("spaces", tokens); + String json = ResourceFactory.GSON.toJson(payload); + return Base64.getEncoder().encodeToString( + json.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } + private OkHttpClient.Builder setLogger(OkHttpClient.Builder okBuilder) { if (logger != null) { switch (logLevel) { @@ -812,8 +836,16 @@ public OkHttpClient.Builder defaultCallFactoryBuilder() { .connectionPool(OK_HTTP_CLIENT.connectionPool()) .addInterceptor(new AuthorizationHeaderInterceptor(token)) .addInterceptor(new UserAgentHeaderInterceptor(createUserAgent())) - .addInterceptor(new ContentfulUserAgentHeaderInterceptor(sections)) - .addInterceptor(new ErrorInterceptor(logSensitiveData)); + .addInterceptor(new ContentfulUserAgentHeaderInterceptor(sections)); + + // Add cross-space resolution header if tokens are configured + if (crossSpaceTokens != null && !crossSpaceTokens.isEmpty()) { + String encodedHeader = encodeCrossSpaceTokens(crossSpaceTokens); + okBuilder.addInterceptor(new HeaderInterceptor( + "x-contentful-resource-resolution", encodedHeader)); + } + + okBuilder.addInterceptor(new ErrorInterceptor(logSensitiveData)); setLogger(okBuilder); useTls12IfWanted(okBuilder); @@ -868,6 +900,31 @@ public Builder setIntegration(String name, String version) { return this; } + /** + * Sets cross-space tokens for resolving cross-space references. + *

+ * This enables automatic resolution of entries and assets from other spaces by providing + * access tokens for those spaces. Cross-space resources will be included in the response's + * includes section. + *

+ * The maximum number of extra spaces supported is 20 (21 total including the main space). + * + * @param spaceIdToToken a map of space IDs to their corresponding access tokens (CDA tokens). + * @return this builder for chaining. + * @throws IllegalArgumentException if the map is null or contains more than 20 spaces. + */ + public Builder setCrossSpaceTokens(Map spaceIdToToken) { + checkNotNull(spaceIdToToken, "Cross-space tokens map must not be null."); + if (spaceIdToToken.size() > CROSS_SPACE_TOKENS_MAX) { + throw new IllegalArgumentException( + String.format("Maximum %d extra spaces supported " + + "for cross-space resolution, but %d provided.", + CROSS_SPACE_TOKENS_MAX, spaceIdToToken.size())); + } + this.crossSpaceTokens = spaceIdToToken; + return this; + } + /** * Create CDAClient, using the specified configuration options. * diff --git a/src/main/java/com/contentful/java/cda/ResourceUtils.java b/src/main/java/com/contentful/java/cda/ResourceUtils.java index 1df1e6a..a856c75 100644 --- a/src/main/java/com/contentful/java/cda/ResourceUtils.java +++ b/src/main/java/com/contentful/java/cda/ResourceUtils.java @@ -65,7 +65,12 @@ static SynchronizedSpace nextSpace(SynchronizedSpace space, CDAClient client) { static void resolveLinks(ArrayResource array, CDAClient client) { for (CDAEntry entry : array.entries().values()) { ensureContentType(entry, client); - for (CDAField field : entry.contentType().fields()) { + CDAContentType contentType = entry.contentType(); + if (contentType == null || contentType.fields() == null) { + // Content type may be null for cross-space entries + continue; + } + for (CDAField field : contentType.fields()) { if (field.linkType() != null) { resolveSingleLink(entry, field, array); } else if ("Array".equals(field.type) && "Link".equals(field.items().get("type"))) { @@ -88,13 +93,19 @@ public static void ensureContentType(CDAEntry entry, CDAClient client) { } String contentTypeId = extractNested(entry.attrs(), "contentType", "sys", "id"); + if (contentTypeId == null) { + return; + } + try { contentType = client.cacheTypeWithId(contentTypeId).blockingFirst(); + entry.setContentType(contentType); } catch (CDAResourceNotFoundException e) { + if (client.hasCrossSpaceTokens) { + return; + } throw new CDAContentTypeNotFoundException(entry.id(), CDAEntry.class, contentTypeId, e); } - - entry.setContentType(contentType); } @SuppressWarnings("unchecked") diff --git a/src/main/java/com/contentful/java/cda/rich/RichTextFactory.java b/src/main/java/com/contentful/java/cda/rich/RichTextFactory.java index 71aa9c7..a3a2afb 100644 --- a/src/main/java/com/contentful/java/cda/rich/RichTextFactory.java +++ b/src/main/java/com/contentful/java/cda/rich/RichTextFactory.java @@ -2,6 +2,7 @@ import com.contentful.java.cda.ArrayResource; import com.contentful.java.cda.CDAClient; +import com.contentful.java.cda.CDAContentType; import com.contentful.java.cda.CDAEntry; import com.contentful.java.cda.CDAField; @@ -78,7 +79,12 @@ public class RichTextFactory { public static void resolveRichTextField(ArrayResource array, CDAClient client) { for (CDAEntry entry : array.entries().values()) { ensureContentType(entry, client); - for (CDAField field : entry.contentType().fields()) { + CDAContentType contentType = entry.contentType(); + if (contentType == null || contentType.fields() == null) { + // Content type may be null for cross-space entries + continue; + } + for (CDAField field : contentType.fields()) { if ("RichText".equals(field.type())) { resolveRichDocument(entry, field); resolveRichLink(array, entry, field);