Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@ Install the Contentful dependency:
<dependency>
<groupId>com.contentful.java</groupId>
<artifactId>java-sdk</artifactId>
<version>10.5.26</version>
<version>10.6.0</version>
</dependency>
```

* _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.
Expand Down Expand Up @@ -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<String, String> 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
----------

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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):

Expand All @@ -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")
}
}
Expand All @@ -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'
}
}
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<groupId>com.contentful.java</groupId>
<artifactId>java-sdk</artifactId>
<version>10.5.26</version>
<version>10.6.0</version>
<packaging>jar</packaging>

<name>${project.groupId}:${project.artifactId}</name>
Expand Down
61 changes: 59 additions & 2 deletions src/main/java/com/contentful/java/cda/CDAClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;

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

final boolean logSensitiveData;

final boolean hasCrossSpaceTokens;

CDAClient(Builder builder) {
this(new Cache(),
Platform.get().callbackExecutor(),
Expand All @@ -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) {
Expand Down Expand Up @@ -579,6 +586,8 @@ public static class Builder {
Section application;
Section integration;

Map<String, String> crossSpaceTokens;

private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient();

Builder() {
Expand Down Expand Up @@ -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<String, String> tokens) {
Map<String, Map<String, String>> 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) {
Expand Down Expand Up @@ -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));
}
Comment on lines +841 to +846

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have actual cross-space requests been tested using this new interceptor? I had been able to add an interceptor by creating my own OkHttpClient, but when I would run the code, I'd get errors like:

com.contentful.java.cda.CDAContentTypeNotFoundException: Could not find content type 'myContentType' for resource with id '...entryId...' of type 'CDAEntry’.
…
Caused by: com.contentful.java.cda.CDAResourceNotFoundException: Could not find id 'myContentType' of type 'CDAContentType’.

noting that myContentType exists in the remote space but not in the one directly being queried

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was an issue with the resource factory, thanks for catching it!


okBuilder.addInterceptor(new ErrorInterceptor(logSensitiveData));

setLogger(okBuilder);
useTls12IfWanted(okBuilder);
Expand Down Expand Up @@ -868,6 +900,31 @@ public Builder setIntegration(String name, String version) {
return this;
}

/**
* Sets cross-space tokens for resolving cross-space references.
* <p>
* 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.
* <p>
* 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<String, String> 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.
*
Expand Down
17 changes: 14 additions & 3 deletions src/main/java/com/contentful/java/cda/ResourceUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"))) {
Expand All @@ -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;
}
Comment on lines +104 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get what's going on here. if the client isn't able to cache the type, it's likely it's because that type is in a different space. OTOH, it could be a race and the type was removed, which would be a valid error. dunno if you have a good way to sort between the two. thanks!

throw new CDAContentTypeNotFoundException(entry.id(), CDAEntry.class, contentTypeId, e);
}

entry.setContentType(contentType);
}

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down