Skip to content

Commit 634bbe0

Browse files
authored
feat: Add Cross-Space Reference Resolution Support (#332)
* Add support for cross space resolution * Checkstyle issues * Checkstyle issues * Resoruce factory error fix * Update major value * Remove test
1 parent a8e9df2 commit 634bbe0

File tree

5 files changed

+119
-13
lines changed

5 files changed

+119
-13
lines changed

README.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,14 @@ Install the Contentful dependency:
7676
<dependency>
7777
<groupId>com.contentful.java</groupId>
7878
<artifactId>java-sdk</artifactId>
79-
<version>10.5.26</version>
79+
<version>10.6.0</version>
8080
</dependency>
8181
```
8282

8383
* _Gradle_
8484

8585
```groovy
86-
compile 'com.contentful.java:java-sdk:10.5.26'
86+
compile 'com.contentful.java:java-sdk:10.6.0'
8787
```
8888

8989
This library requires Java 8 (or higher version) or Android 21.
@@ -259,6 +259,38 @@ CDAArray found = client.fetch(CDAEntry.class)
259259

260260
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.
261261

262+
Cross-Space References
263+
----------------------
264+
265+
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.
266+
267+
To enable cross-space reference resolution, provide access tokens for the additional spaces:
268+
269+
```java
270+
Map<String, String> crossSpaceTokens = new HashMap<>();
271+
crossSpaceTokens.put("space-id-1", "cda-token-for-space-1");
272+
crossSpaceTokens.put("space-id-2", "cda-token-for-space-2");
273+
274+
CDAClient client = CDAClient.builder()
275+
.setSpace("main-space-id")
276+
.setToken("main-space-token")
277+
.setCrossSpaceTokens(crossSpaceTokens)
278+
.build();
279+
280+
// Cross-space references will now be automatically resolved
281+
CDAArray entries = client.fetch(CDAEntry.class)
282+
.include(2)
283+
.all();
284+
```
285+
286+
**Limitations:**
287+
- Maximum 20 extra spaces can be configured (21 total including the main space)
288+
- Only the first level of cross-space references is resolved (similar to `include=1` for cross-space)
289+
- The main space can still resolve up to 10 levels of includes
290+
- Cross-space errors are returned in the `CDAArray.getErrors()` method
291+
292+
For more information, see the [Contentful Resource Links documentation](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/resource-links).
293+
262294
Unwrapping
263295
----------
264296

@@ -303,7 +335,7 @@ In addition to returning the Content in a fashion flexible for various use-cases
303335
> * A `locale` can be used to specify a given locale of this entry. If no locale is given, the default locale will be used.
304336
> * `@ContentfulSystemField` is used for CDAEntries attributes (`sys.id`, etc) to be inserted.
305337
> * If another type is wanted to be transformed, it should have `@ContentfulEntryModel`-annotation specified similarly as in `Cat`.
306-
> * **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:
338+
> * **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:
307339
> * Use the `rawFields` map in `CDAEntry` to directly access the unprocessed JSON of any field, including rich text.
308340
> * Alternatively, make a direct HTTP request to the Contentful API to retrieve the full raw JSON response.
309341
@@ -387,7 +419,7 @@ CDAClient cdaClient = clientBuilder.setCallFactory(httpClient).build();
387419
Android and OkHttp 5
388420
--------------------
389421

390-
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.
422+
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.
391423

392424
Gradle (Kotlin DSL):
393425

@@ -396,7 +428,7 @@ dependencies {
396428
implementation(platform("com.squareup.okhttp3:okhttp-bom:5.1.0"))
397429
implementation("com.squareup.okhttp3:okhttp-android")
398430

399-
implementation("com.contentful.java:java-sdk:10.5.24") {
431+
implementation("com.contentful.java:java-sdk:10.6.0") {
400432
exclude(group = "com.squareup.okhttp3", module = "okhttp-jvm")
401433
}
402434
}
@@ -409,7 +441,7 @@ dependencies {
409441
implementation platform('com.squareup.okhttp3:okhttp-bom:5.1.0')
410442
implementation 'com.squareup.okhttp3:okhttp-android'
411443
412-
implementation('com.contentful.java:java-sdk:10.5.24') {
444+
implementation('com.contentful.java:java-sdk:10.6.0') {
413445
exclude group: 'com.squareup.okhttp3', module: 'okhttp-jvm'
414446
}
415447
}

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
<groupId>com.contentful.java</groupId>
55
<artifactId>java-sdk</artifactId>
6-
<version>10.5.26</version>
6+
<version>10.6.0</version>
77
<packaging>jar</packaging>
88

99
<name>${project.groupId}:${project.artifactId}</name>

src/main/java/com/contentful/java/cda/CDAClient.java

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section.OperatingSystem;
99
import com.contentful.java.cda.interceptor.ContentfulUserAgentHeaderInterceptor.Section.Version;
1010
import com.contentful.java.cda.interceptor.ErrorInterceptor;
11+
import com.contentful.java.cda.interceptor.HeaderInterceptor;
1112
import com.contentful.java.cda.interceptor.LogInterceptor;
1213
import com.contentful.java.cda.interceptor.UserAgentHeaderInterceptor;
1314
import io.reactivex.rxjava3.core.Flowable;
@@ -35,6 +36,7 @@
3536
import java.util.Properties;
3637
import java.util.concurrent.ConcurrentHashMap;
3738
import java.util.concurrent.Executor;
39+
import java.util.Base64;
3840

3941
import static com.contentful.java.cda.Constants.ENDPOINT_PROD;
4042
import static com.contentful.java.cda.Constants.PATH_CONTENT_TYPES;
@@ -58,6 +60,7 @@
5860
*/
5961
public class CDAClient {
6062
private static final int CONTENT_TYPE_LIMIT_MAX = 1000;
63+
private static final int CROSS_SPACE_TOKENS_MAX = 20;
6164

6265
final String spaceId;
6366

@@ -75,6 +78,8 @@ public class CDAClient {
7578

7679
final boolean logSensitiveData;
7780

81+
final boolean hasCrossSpaceTokens;
82+
7883
CDAClient(Builder builder) {
7984
this(new Cache(),
8085
Platform.get().callbackExecutor(),
@@ -92,6 +97,8 @@ public class CDAClient {
9297
this.token = builder.token;
9398
this.preview = builder.preview;
9499
this.logSensitiveData = builder.logSensitiveData;
100+
this.hasCrossSpaceTokens = builder.crossSpaceTokens != null
101+
&& !builder.crossSpaceTokens.isEmpty();
95102
}
96103

97104
private void validate(Builder builder) {
@@ -579,6 +586,8 @@ public static class Builder {
579586
Section application;
580587
Section integration;
581588

589+
Map<String, String> crossSpaceTokens;
590+
582591
private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient();
583592

584593
Builder() {
@@ -720,6 +729,21 @@ Converter.Factory createOrGetConverterFactory(Builder clientBuilder) {
720729
return converterFactory;
721730
}
722731

732+
/**
733+
* Encodes cross-space tokens to a base64-encoded JSON string for the
734+
* x-contentful-resource-resolution header.
735+
*
736+
* @param tokens map of space IDs to access tokens
737+
* @return base64-encoded JSON string
738+
*/
739+
private String encodeCrossSpaceTokens(Map<String, String> tokens) {
740+
Map<String, Map<String, String>> payload = new HashMap<>();
741+
payload.put("spaces", tokens);
742+
String json = ResourceFactory.GSON.toJson(payload);
743+
return Base64.getEncoder().encodeToString(
744+
json.getBytes(java.nio.charset.StandardCharsets.UTF_8));
745+
}
746+
723747
private OkHttpClient.Builder setLogger(OkHttpClient.Builder okBuilder) {
724748
if (logger != null) {
725749
switch (logLevel) {
@@ -812,8 +836,16 @@ public OkHttpClient.Builder defaultCallFactoryBuilder() {
812836
.connectionPool(OK_HTTP_CLIENT.connectionPool())
813837
.addInterceptor(new AuthorizationHeaderInterceptor(token))
814838
.addInterceptor(new UserAgentHeaderInterceptor(createUserAgent()))
815-
.addInterceptor(new ContentfulUserAgentHeaderInterceptor(sections))
816-
.addInterceptor(new ErrorInterceptor(logSensitiveData));
839+
.addInterceptor(new ContentfulUserAgentHeaderInterceptor(sections));
840+
841+
// Add cross-space resolution header if tokens are configured
842+
if (crossSpaceTokens != null && !crossSpaceTokens.isEmpty()) {
843+
String encodedHeader = encodeCrossSpaceTokens(crossSpaceTokens);
844+
okBuilder.addInterceptor(new HeaderInterceptor(
845+
"x-contentful-resource-resolution", encodedHeader));
846+
}
847+
848+
okBuilder.addInterceptor(new ErrorInterceptor(logSensitiveData));
817849

818850
setLogger(okBuilder);
819851
useTls12IfWanted(okBuilder);
@@ -868,6 +900,31 @@ public Builder setIntegration(String name, String version) {
868900
return this;
869901
}
870902

903+
/**
904+
* Sets cross-space tokens for resolving cross-space references.
905+
* <p>
906+
* This enables automatic resolution of entries and assets from other spaces by providing
907+
* access tokens for those spaces. Cross-space resources will be included in the response's
908+
* includes section.
909+
* <p>
910+
* The maximum number of extra spaces supported is 20 (21 total including the main space).
911+
*
912+
* @param spaceIdToToken a map of space IDs to their corresponding access tokens (CDA tokens).
913+
* @return this builder for chaining.
914+
* @throws IllegalArgumentException if the map is null or contains more than 20 spaces.
915+
*/
916+
public Builder setCrossSpaceTokens(Map<String, String> spaceIdToToken) {
917+
checkNotNull(spaceIdToToken, "Cross-space tokens map must not be null.");
918+
if (spaceIdToToken.size() > CROSS_SPACE_TOKENS_MAX) {
919+
throw new IllegalArgumentException(
920+
String.format("Maximum %d extra spaces supported "
921+
+ "for cross-space resolution, but %d provided.",
922+
CROSS_SPACE_TOKENS_MAX, spaceIdToToken.size()));
923+
}
924+
this.crossSpaceTokens = spaceIdToToken;
925+
return this;
926+
}
927+
871928
/**
872929
* Create CDAClient, using the specified configuration options.
873930
*

src/main/java/com/contentful/java/cda/ResourceUtils.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,12 @@ static SynchronizedSpace nextSpace(SynchronizedSpace space, CDAClient client) {
6565
static void resolveLinks(ArrayResource array, CDAClient client) {
6666
for (CDAEntry entry : array.entries().values()) {
6767
ensureContentType(entry, client);
68-
for (CDAField field : entry.contentType().fields()) {
68+
CDAContentType contentType = entry.contentType();
69+
if (contentType == null || contentType.fields() == null) {
70+
// Content type may be null for cross-space entries
71+
continue;
72+
}
73+
for (CDAField field : contentType.fields()) {
6974
if (field.linkType() != null) {
7075
resolveSingleLink(entry, field, array);
7176
} else if ("Array".equals(field.type) && "Link".equals(field.items().get("type"))) {
@@ -88,13 +93,19 @@ public static void ensureContentType(CDAEntry entry, CDAClient client) {
8893
}
8994

9095
String contentTypeId = extractNested(entry.attrs(), "contentType", "sys", "id");
96+
if (contentTypeId == null) {
97+
return;
98+
}
99+
91100
try {
92101
contentType = client.cacheTypeWithId(contentTypeId).blockingFirst();
102+
entry.setContentType(contentType);
93103
} catch (CDAResourceNotFoundException e) {
104+
if (client.hasCrossSpaceTokens) {
105+
return;
106+
}
94107
throw new CDAContentTypeNotFoundException(entry.id(), CDAEntry.class, contentTypeId, e);
95108
}
96-
97-
entry.setContentType(contentType);
98109
}
99110

100111
@SuppressWarnings("unchecked")

src/main/java/com/contentful/java/cda/rich/RichTextFactory.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.contentful.java.cda.ArrayResource;
44
import com.contentful.java.cda.CDAClient;
5+
import com.contentful.java.cda.CDAContentType;
56
import com.contentful.java.cda.CDAEntry;
67
import com.contentful.java.cda.CDAField;
78

@@ -78,7 +79,12 @@ public class RichTextFactory {
7879
public static void resolveRichTextField(ArrayResource array, CDAClient client) {
7980
for (CDAEntry entry : array.entries().values()) {
8081
ensureContentType(entry, client);
81-
for (CDAField field : entry.contentType().fields()) {
82+
CDAContentType contentType = entry.contentType();
83+
if (contentType == null || contentType.fields() == null) {
84+
// Content type may be null for cross-space entries
85+
continue;
86+
}
87+
for (CDAField field : contentType.fields()) {
8288
if ("RichText".equals(field.type())) {
8389
resolveRichDocument(entry, field);
8490
resolveRichLink(array, entry, field);

0 commit comments

Comments
 (0)