google-site-verification: google61fe8ba583a51912.html
Sanity.io Java Client: Complete Implementation Guide

Introduction to Sanity.io Java Client

Sanity.io is a headless CMS platform that provides real-time content management. This guide covers building a comprehensive Java client for interacting with Sanity.io's APIs, including content querying, mutations, real-time subscriptions, and asset management.

Key Features

  • Content Querying with GROQ (Graph-Relational Object Queries)
  • Real-time Subscriptions for live content updates
  • Asset Management for file uploads and transformations
  • Mutation Operations for creating, updating, and deleting content
  • Type-safe Models for Java integration
  • Error Handling and retry mechanisms

Dependencies and Setup

Maven Configuration

<properties> <okhttp.version>4.12.0</okhttp.version> <jackson.version>2.15.2</jackson.version> <websocket.version>1.5.3</websocket.version> </properties> <dependencies> <!-- HTTP Client --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp.version}</version> </dependency> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp-sse</artifactId> <version>${okhttp.version}</version> </dependency> <!-- WebSocket Client --> <dependency> <groupId>org.java-websocket</groupId> <artifactId>Java-WebSocket</artifactId> <version>${websocket.version}</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>${jackson.version}</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.3</version> <scope>test</scope> </dependency> <!-- Mock Web Server for Testing --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>mockwebserver</artifactId> <version>${okhttp.version}</version> <scope>test</scope> </dependency> </dependencies>

Core Sanity Client Implementation

Main Sanity Client

package com.sanity.client; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.*; import java.util.concurrent.TimeUnit; public class SanityClient { private static final Logger logger = LoggerFactory.getLogger(SanityClient.class); private final String projectId; private final String dataset; private final String token; private final boolean useCdn; private final OkHttpClient httpClient; private final ObjectMapper objectMapper; private final String baseUrl; public SanityClient(String projectId, String dataset, String token) { this(projectId, dataset, token, true); } public SanityClient(String projectId, String dataset, String token, boolean useCdn) { this.projectId = projectId; this.dataset = dataset; this.token = token; this.useCdn = useCdn; this.objectMapper = new ObjectMapper(); this.httpClient = new OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .build(); this.baseUrl = useCdn ? "https://" + projectId + ".apicdn.sanity.io/v1" : "https://" + projectId + ".api.sanity.io/v1"; } /** * Execute GROQ query and return typed results */ public <T> QueryResponse<T> query(String groqQuery, Class<T> resultType) throws SanityException { return query(groqQuery, Collections.emptyMap(), resultType); } /** * Execute GROQ query with parameters */ public <T> QueryResponse<T> query(String groqQuery, Map<String, Object> params, Class<T> resultType) throws SanityException { try { // Build request URL HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl + "/data/query/" + dataset).newBuilder(); urlBuilder.addQueryParameter("query", groqQuery); // Add parameters as JSON if (params != null && !params.isEmpty()) { String paramsJson = objectMapper.writeValueAsString(params); urlBuilder.addQueryParameter("$params", paramsJson); } Request request = createAuthenticatedRequest() .url(urlBuilder.build()) .get() .build(); try (Response response = httpClient.newCall(request).execute()) { return parseQueryResponse(response, resultType); } } catch (IOException e) { throw new SanityException("Query execution failed", e); } } /** * Execute GROQ query returning list of results */ public <T> QueryResponse<List<T>> queryList(String groqQuery, Class<T> elementType) throws SanityException { return queryList(groqQuery, Collections.emptyMap(), elementType); } public <T> QueryResponse<List<T>> queryList(String groqQuery, Map<String, Object> params, Class<T> elementType) throws SanityException { TypeReference<QueryResponse<List<T>>> typeRef = new TypeReference<QueryResponse<List<T>>>() {}; return queryWithTypeReference(groqQuery, params, typeRef); } /** * Execute query with TypeReference for complex types */ public <T> T queryWithTypeReference(String groqQuery, Map<String, Object> params, TypeReference<T> typeRef) throws SanityException { try { HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl + "/data/query/" + dataset).newBuilder(); urlBuilder.addQueryParameter("query", groqQuery); if (params != null && !params.isEmpty()) { String paramsJson = objectMapper.writeValueAsString(params); urlBuilder.addQueryParameter("$params", paramsJson); } Request request = createAuthenticatedRequest() .url(urlBuilder.build()) .get() .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new SanityException("Query failed: " + response.code() + " - " + response.message()); } String responseBody = response.body().string(); return objectMapper.readValue(responseBody, typeRef); } } catch (IOException e) { throw new SanityException("Query execution failed", e); } } /** * Create a new document */ public MutationResult create(Map<String, Object> document) throws SanityException { List<Map<String, Object>> mutations = Collections.singletonList( Collections.singletonMap("create", document) ); return mutate(mutations); } /** * Create a new document with specified type */ public <T> MutationResult create(T document) throws SanityException { Map<String, Object> documentMap = objectMapper.convertValue(document, Map.class); return create(documentMap); } /** * Update an existing document */ public MutationResult update(String documentId, Map<String, Object> updates) throws SanityException { List<Map<String, Object>> mutations = Collections.singletonList( Collections.singletonMap("patch", Collections.singletonMap("id", documentId) .with("set", updates) ) ); return mutate(mutations); } /** * Update an existing document with typed object */ public <T> MutationResult update(String documentId, T updates) throws SanityException { Map<String, Object> updatesMap = objectMapper.convertValue(updates, Map.class); return update(documentId, updatesMap); } /** * Delete a document */ public MutationResult delete(String documentId) throws SanityException { List<Map<String, Object>> mutations = Collections.singletonList( Collections.singletonMap("delete", Collections.singletonMap("id", documentId) ) ); return mutate(mutations); } /** * Execute multiple mutations in a transaction */ public MutationResult mutate(List<Map<String, Object>> mutations) throws SanityException { try { String mutationsJson = objectMapper.writeValueAsString(mutations); RequestBody body = RequestBody.create( mutationsJson, MediaType.parse("application/json") ); Request request = createAuthenticatedRequest() .url(baseUrl + "/data/mutate/" + dataset) .post(body) .build(); try (Response response = httpClient.newCall(request).execute()) { return parseMutationResponse(response); } } catch (IOException e) { throw new SanityException("Mutation failed", e); } } /** * Upload file to Sanity */ public AssetUploadResult uploadAsset(byte[] fileData, String contentType, String originalFilename) throws SanityException { try { // Step 1: Begin upload Map<String, String> beginUploadRequest = Collections.singletonMap( "contentType", contentType ); String beginUploadJson = objectMapper.writeValueAsString(beginUploadRequest); Request beginRequest = createAuthenticatedRequest() .url(baseUrl + "/assets/images/" + dataset) .post(RequestBody.create(beginUploadJson, MediaType.parse("application/json"))) .build(); String uploadUrl; String assetId; try (Response beginResponse = httpClient.newCall(beginRequest).execute()) { if (!beginResponse.isSuccessful()) { throw new SanityException("Failed to begin upload: " + beginResponse.code()); } String beginResponseBody = beginResponse.body().string(); Map<String, Object> beginResult = objectMapper.readValue(beginResponseBody, Map.class); uploadUrl = (String) beginResult.get("url"); assetId = (String) beginResult.get("id"); } // Step 2: Upload file data Request uploadRequest = new Request.Builder() .url(uploadUrl) .put(RequestBody.create(fileData, MediaType.parse(contentType))) .build(); try (Response uploadResponse = httpClient.newCall(uploadRequest).execute()) { if (!uploadResponse.isSuccessful()) { throw new SanityException("File upload failed: " + uploadResponse.code()); } } // Step 3: Commit upload Map<String, Object> commitRequest = new HashMap<>(); commitRequest.put("id", assetId); commitRequest.put("originalFilename", originalFilename); String commitJson = objectMapper.writeValueAsString(commitRequest); Request commitRequestObj = createAuthenticatedRequest() .url(baseUrl + "/assets/images/" + dataset) .post(RequestBody.create(commitJson, MediaType.parse("application/json"))) .build(); try (Response commitResponse = httpClient.newCall(commitRequestObj).execute()) { if (!commitResponse.isSuccessful()) { throw new SanityException("Upload commit failed: " + commitResponse.code()); } String commitResponseBody = commitResponse.body().string(); return objectMapper.readValue(commitResponseBody, AssetUploadResult.class); } } catch (IOException e) { throw new SanityException("Asset upload failed", e); } } /** * Get project information */ public ProjectInfo getProjectInfo() throws SanityException { try { Request request = createAuthenticatedRequest() .url("https://" + projectId + ".api.sanity.io/v1/projects/" + projectId) .get() .build(); try (Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new SanityException("Failed to get project info: " + response.code()); } String responseBody = response.body().string(); return objectMapper.readValue(responseBody, ProjectInfo.class); } } catch (IOException e) { throw new SanityException("Failed to get project info", e); } } // Helper methods private Request.Builder createAuthenticatedRequest() { Request.Builder builder = new Request.Builder() .addHeader("Content-Type", "application/json"); if (token != null && !token.isEmpty()) { builder.addHeader("Authorization", "Bearer " + token); } return builder; } private <T> QueryResponse<T> parseQueryResponse(Response response, Class<T> resultType) throws IOException { if (!response.isSuccessful()) { throw new SanityException("Query failed: " + response.code() + " - " + response.message()); } String responseBody = response.body().string(); QueryResponse<Map<String, Object>> rawResponse = objectMapper.readValue( responseBody, new TypeReference<QueryResponse<Map<String, Object>>>() {} ); QueryResponse<T> typedResponse = new QueryResponse<>(); typedResponse.setMs(rawResponse.getMs()); typedResponse.setQuery(rawResponse.getQuery()); if (rawResponse.getResult() != null) { T convertedResult = objectMapper.convertValue(rawResponse.getResult(), resultType); typedResponse.setResult(convertedResult); } return typedResponse; } private MutationResult parseMutationResponse(Response response) throws IOException { if (!response.isSuccessful()) { throw new SanityException("Mutation failed: " + response.code() + " - " + response.message()); } String responseBody = response.body().string(); return objectMapper.readValue(responseBody, MutationResult.class); } // Custom exception public static class SanityException extends Exception { public SanityException(String message) { super(message); } public SanityException(String message, Throwable cause) { super(message, cause); } } }

Data Models

Response Data Classes

package com.sanity.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import java.util.Map; // Query Response @JsonIgnoreProperties(ignoreUnknown = true) public class QueryResponse<T> { private String query; private long ms; private T result; // Getters and setters public String getQuery() { return query; } public void setQuery(String query) { this.query = query; } public long getMs() { return ms; } public void setMs(long ms) { this.ms = ms; } public T getResult() { return result; } public void setResult(T result) { this.result = result; } } // Mutation Result @JsonIgnoreProperties(ignoreUnknown = true) class MutationResult { private String transactionId; private List<MutationResultItem> results; // Getters and setters public String getTransactionId() { return transactionId; } public void setTransactionId(String transactionId) { this.transactionId = transactionId; } public List<MutationResultItem> getResults() { return results; } public void setResults(List<MutationResultItem> results) { this.results = results; } } // Mutation Result Item @JsonIgnoreProperties(ignoreUnknown = true) class MutationResultItem { private String id; private String operation; // Getters and setters public String getId() { return id; } public void setId(String id) { this.id = id; } public String getOperation() { return operation; } public void setOperation(String operation) { this.operation = operation; } } // Asset Upload Result @JsonIgnoreProperties(ignoreUnknown = true) class AssetUploadResult { private String id; private String url; private String path; private String originalFilename; private long size; private String mimeType; // Getters and setters public String getId() { return id; } public void setId(String id) { this.id = id; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getOriginalFilename() { return originalFilename; } public void setOriginalFilename(String originalFilename) { this.originalFilename = originalFilename; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } public String getMimeType() { return mimeType; } public void setMimeType(String mimeType) { this.mimeType = mimeType; } } // Project Information @JsonIgnoreProperties(ignoreUnknown = true) class ProjectInfo { private String id; private String displayName; private String studioHost; private String organizationId; private boolean isBlocked; private boolean isDisabled; private boolean isDisabledByUser; private String createdAt; private String updatedAt; private String membership; // Getters and setters public String getId() { return id; } public void setId(String id) { this.id = id; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public String getStudioHost() { return studioHost; } public void setStudioHost(String studioHost) { this.studioHost = studioHost; } public String getOrganizationId() { return organizationId; } public void setOrganizationId(String organizationId) { this.organizationId = organizationId; } public boolean isBlocked() { return isBlocked; } public void setBlocked(boolean blocked) { isBlocked = blocked; } public boolean isDisabled() { return isDisabled; } public void setDisabled(boolean disabled) { isDisabled = disabled; } public boolean isDisabledByUser() { return isDisabledByUser; } public void setDisabledByUser(boolean disabledByUser) { isDisabledByUser = disabledByUser; } public String getCreatedAt() { return createdAt; } public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } public String getUpdatedAt() { return updatedAt; } public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } public String getMembership() { return membership; } public void setMembership(String membership) { this.membership = membership; } } // Sanity Document Base Class @JsonIgnoreProperties(ignoreUnknown = true) class SanityDocument { @JsonProperty("_id") private String id; @JsonProperty("_type") private String type; @JsonProperty("_createdAt") private String createdAt; @JsonProperty("_updatedAt") private String updatedAt; @JsonProperty("_rev") private String revision; // Getters and setters public String getId() { return id; } public void setId(String id) { this.id = id; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getCreatedAt() { return createdAt; } public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } public String getUpdatedAt() { return updatedAt; } public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } public String getRevision() { return revision; } public void setRevision(String revision) { this.revision = revision; } } // Example Content Types class Post extends SanityDocument { private String title; private String slug; private String body; private Author author; private List<String> categories; private SanityImage mainImage; private String publishedAt; // Getters and setters public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getSlug() { return slug; } public void setSlug(String slug) { this.slug = slug; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public Author getAuthor() { return author; } public void setAuthor(Author author) { this.author = author; } public List<String> getCategories() { return categories; } public void setCategories(List<String> categories) { this.categories = categories; } public SanityImage getMainImage() { return mainImage; } public void setMainImage(SanityImage mainImage) { this.mainImage = mainImage; } public String getPublishedAt() { return publishedAt; } public void setPublishedAt(String publishedAt) { this.publishedAt = publishedAt; } } class Author extends SanityDocument { private String name; private String bio; private SanityImage image; private String email; // Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; } public String getBio() { return bio; } public void setBio(String bio) { this.bio = bio; } public SanityImage getImage() { return image; } public void setImage(SanityImage image) { this.image = image; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } class SanityImage { @JsonProperty("_type") private String type = "image"; private Asset asset; // Getters and setters public String getType() { return type; } public void setType(String type) { this.type = type; } public Asset getAsset() { return asset; } public void setAsset(Asset asset) { this.asset = asset; } // Helper method to generate image URL public String getUrl() { if (asset != null && asset.getRef() != null) { return "https://cdn.sanity.io/images/" + extractProjectId(asset.getRef()) + "/" + extractDataset(asset.getRef()) + "/" + extractAssetId(asset.getRef()); } return null; } public String getUrlWithOptions(int width, int height) { String baseUrl = getUrl(); if (baseUrl != null) { return baseUrl + "?w=" + width + "&h=" + height + "&fit=crop"; } return null; } private String extractProjectId(String ref) { // Implementation to extract project ID from asset reference return ref.split("-")[0]; } private String extractDataset(String ref) { // Implementation to extract dataset from asset reference return "production"; // Default dataset } private String extractAssetId(String ref) { // Implementation to extract asset ID from asset reference return ref; } } class Asset { @JsonProperty("_ref") private String ref; @JsonProperty("_type") private String type = "reference"; // Getters and setters public String getRef() { return ref; } public void setRef(String ref) { this.ref = ref; } public String getType() { return type; } public void setType(String type) { this.type = type; } }

Real-time Subscription Service

Live Query Subscriptions

package com.sanity.realtime; import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; public class SanityRealtimeService { private static final Logger logger = LoggerFactory.getLogger(SanityRealtimeService.class); private final String projectId; private final String dataset; private final String token; private final OkHttpClient httpClient; private final ObjectMapper objectMapper; private final Map<String, Subscription> subscriptions; private final AtomicInteger listenerIdCounter; public SanityRealtimeService(String projectId, String dataset, String token) { this.projectId = projectId; this.dataset = dataset; this.token = token; this.httpClient = new OkHttpClient(); this.objectMapper = new ObjectMapper(); this.subscriptions = new ConcurrentHashMap<>(); this.listenerIdCounter = new AtomicInteger(1); } /** * Subscribe to real-time updates for a GROQ query */ public <T> String subscribe(String query, Map<String, Object> params, RealtimeListener<T> listener, Class<T> resultType) { String subscriptionId = generateSubscriptionId(); Subscription subscription = new Subscription( subscriptionId, query, params, listener, resultType ); subscriptions.put(subscriptionId, subscription); startSubscription(subscription); return subscriptionId; } /** * Unsubscribe from real-time updates */ public void unsubscribe(String subscriptionId) { Subscription subscription = subscriptions.remove(subscriptionId); if (subscription != null) { subscription.close(); } } /** * Start a subscription using Server-Sent Events (SSE) */ private <T> void startSubscription(Subscription<T> subscription) { try { String query = subscription.getQuery(); Map<String, Object> params = subscription.getParams(); // Build listen URL HttpUrl.Builder urlBuilder = HttpUrl.parse( "https://" + projectId + ".api.sanity.io/v1/data/listen/" + dataset ).newBuilder(); urlBuilder.addQueryParameter("query", query); if (params != null && !params.isEmpty()) { String paramsJson = objectMapper.writeValueAsString(params); urlBuilder.addQueryParameter("$params", paramsJson); } Request request = new Request.Builder() .url(urlBuilder.build()) .addHeader("Authorization", "Bearer " + token) .addHeader("Accept", "text/event-stream") .build(); RealTimeSSEListener sseListener = new RealTimeSSEListener<>(subscription); subscription.setEventSource(new EventSource(request, sseListener)); } catch (Exception e) { logger.error("Failed to start subscription: {}", e.getMessage(), e); subscription.getListener().onError(e); } } private String generateSubscriptionId() { return "sub-" + System.currentTimeMillis() + "-" + listenerIdCounter.getAndIncrement(); } // Subscription class private static class Subscription<T> { private final String id; private final String query; private final Map<String, Object> params; private final RealtimeListener<T> listener; private final Class<T> resultType; private EventSource eventSource; public Subscription(String id, String query, Map<String, Object> params, RealtimeListener<T> listener, Class<T> resultType) { this.id = id; this.query = query; this.params = params; this.listener = listener; this.resultType = resultType; } public void close() { if (eventSource != null) { eventSource.cancel(); } } // Getters and setters public String getId() { return id; } public String getQuery() { return query; } public Map<String, Object> getParams() { return params; } public RealtimeListener<T> getListener() { return listener; } public Class<T> getResultType() { return resultType; } public EventSource getEventSource() { return eventSource; } public void setEventSource(EventSource eventSource) { this.eventSource = eventSource; } } // SSE Listener for real-time updates private class RealTimeSSEListener<T> extends EventSourceListener { private final Subscription<T> subscription; private final ObjectMapper objectMapper; public RealTimeSSEListener(Subscription<T> subscription) { this.subscription = subscription; this.objectMapper = new ObjectMapper(); } @Override public void onOpen(EventSource eventSource, Response response) { logger.info("Real-time connection opened for subscription: {}", subscription.getId()); subscription.getListener().onConnected(); } @Override public void onEvent(EventSource eventSource, String id, String type, String data) { try { if ("message".equals(type)) { handleMessage(data); } } catch (Exception e) { logger.error("Error processing real-time event: {}", e.getMessage(), e); subscription.getListener().onError(e); } } @Override public void onClosed(EventSource eventSource) { logger.info("Real-time connection closed for subscription: {}", subscription.getId()); subscription.getListener().onDisconnected(); } @Override public void onFailure(EventSource eventSource, Throwable t, Response response) { logger.error("Real-time connection failed for subscription: {}", subscription.getId(), t); subscription.getListener().onError(t); } private void handleMessage(String data) throws IOException { Map<String, Object> message = objectMapper.readValue(data, Map.class); String messageType = (String) message.get("type"); switch (messageType) { case "welcome": handleWelcome(message); break; case "mutation": handleMutation(message); break; case "disconnect": handleDisconnect(message); break; default: logger.debug("Unknown message type: {}", messageType); } } private void handleWelcome(Map<String, Object> message) { logger.info("Received welcome message for subscription: {}", subscription.getId()); } @SuppressWarnings("unchecked") private void handleMutation(Map<String, Object> message) { try { Map<String, Object> result = (Map<String, Object>) message.get("result"); if (result != null) { T typedResult = objectMapper.convertValue(result, subscription.getResultType()); subscription.getListener().onUpdate(typedResult); } } catch (Exception e) { logger.error("Error processing mutation: {}", e.getMessage(), e); } } private void handleDisconnect(Map<String, Object> message) { String reason = (String) message.get("reason"); logger.warn("Disconnected from real-time updates: {}", reason); } } // Custom EventSource implementation for SSE private static class EventSource { private final Request request; private final EventSourceListener listener; private okhttp3.EventSource okhttpEventSource; public EventSource(Request request, EventSourceListener listener) { this.request = request; this.listener = listener; } public void start(OkHttpClient client) { okhttp3.EventSource.Factory factory = okhttp3.EventSource.Factory.create(client); okhttpEventSource = factory.newEventSource(request, new okhttp3.EventSourceListener() { @Override public void onOpen(okhttp3.EventSource eventSource, Response response) { listener.onOpen(EventSource.this, response); } @Override public void onEvent(okhttp3.EventSource eventSource, String id, String type, String data) { listener.onEvent(EventSource.this, id, type, data); } @Override public void onClosed(okhttp3.EventSource eventSource) { listener.onClosed(EventSource.this); } @Override public void onFailure(okhttp3.EventSource eventSource, Throwable t, Response response) { listener.onFailure(EventSource.this, t, response); } }); } public void cancel() { if (okhttpEventSource != null) { okhttpEventSource.cancel(); } } } // Event Source Listener interface private abstract static class EventSourceListener { public void onOpen(EventSource eventSource, Response response) {} public void onEvent(EventSource eventSource, String id, String type, String data) {} public void onClosed(EventSource eventSource) {} public void onFailure(EventSource eventSource, Throwable t, Response response) {} } } // Real-time Listener Interface interface RealtimeListener<T> { void onConnected(); void onDisconnected(); void onUpdate(T data); void onError(Throwable error); }

Advanced Query Builder

GROQ Query Builder

package com.sanity.query; import java.util.*; import java.util.stream.Collectors; public class GroqQueryBuilder { private final StringBuilder query; private final Map<String, Object> parameters; private final Set<String> projections; private final List<String> filters; private final List<String> orders; private Integer limit; private Integer offset; public GroqQueryBuilder() { this.query = new StringBuilder(); this.parameters = new HashMap<>(); this.projections = new LinkedHashSet<>(); this.filters = new ArrayList<>(); this.orders = new ArrayList<>(); } /** * Start building a query for a specific document type */ public static GroqQueryBuilder forType(String documentType) { GroqQueryBuilder builder = new GroqQueryBuilder(); builder.query.append("*[_type == \"").append(documentType).append("\"]"); return builder; } /** * Start building a query with custom base */ public static GroqQueryBuilder from(String baseQuery) { GroqQueryBuilder builder = new GroqQueryBuilder(); builder.query.append(baseQuery); return builder; } /** * Add field projection */ public GroqQueryBuilder project(String field) { projections.add(field); return this; } /** * Add multiple field projections */ public GroqQueryBuilder project(String... fields) { projections.addAll(Arrays.asList(fields)); return this; } /** * Add field projection with alias */ public GroqQueryBuilder project(String field, String alias) { projections.add(alias + ": " + field); return this; } /** * Add filter condition */ public GroqQueryBuilder filter(String condition) { filters.add(condition); return this; } /** * Add equality filter */ public GroqQueryBuilder whereEquals(String field, Object value) { String paramName = addParameter(value); filters.add(field + " == $" + paramName); return this; } /** * Add inequality filter */ public GroqQueryBuilder whereNotEquals(String field, Object value) { String paramName = addParameter(value); filters.add(field + " != $" + paramName); return this; } /** * Add range filter (greater than) */ public GroqQueryBuilder whereGreaterThan(String field, Object value) { String paramName = addParameter(value); filters.add(field + " > $" + paramName); return this; } /** * Add range filter (less than) */ public GroqQueryBuilder whereLessThan(String field, Object value) { String paramName = addParameter(value); filters.add(field + " < $" + paramName); return this; } /** * Add contains filter for arrays */ public GroqQueryBuilder whereContains(String field, Object value) { String paramName = addParameter(value); filters.add("$" + paramName + " in " + field); return this; } /** * Add ordering */ public GroqQueryBuilder orderBy(String field, OrderDirection direction) { orders.add(field + " " + direction.name().toLowerCase()); return this; } /** * Add ordering with nulls first/last */ public GroqQueryBuilder orderBy(String field, OrderDirection direction, NullsOrder nullsOrder) { orders.add(field + " " + direction.name().toLowerCase() + " " + nullsOrder.name().replace('_', ' ').toLowerCase()); return this; } /** * Set result limit */ public GroqQueryBuilder limit(int limit) { this.limit = limit; return this; } /** * Set result offset */ public GroqQueryBuilder offset(int offset) { this.offset = offset; return this; } /** * Build the final GROQ query string */ public String build() { StringBuilder finalQuery = new StringBuilder(query); // Add filters if (!filters.isEmpty()) { finalQuery.append("[").append(String.join(" && ", filters)).append("]"); } // Add projections if (!projections.isEmpty()) { finalQuery.append("{").append(String.join(", ", projections)).append("}"); } // Add ordering if (!orders.isEmpty()) { finalQuery.append(" | order(").append(String.join(", ", orders)).append(")"); } // Add limit if (limit != null) { finalQuery.append("[0...").append(limit).append("]"); } // Add offset if (offset != null) { if (limit == null) { finalQuery.append("[").append(offset).append("]"); } else { // If both limit and offset are specified, use range syntax finalQuery.setLength(finalQuery.length() - (String.valueOf(limit).length() + 4)); finalQuery.append("[").append(offset).append("...").append(offset + limit).append("]"); } } return finalQuery.toString(); } /** * Get query parameters */ public Map<String, Object> getParameters() { return Collections.unmodifiableMap(parameters); } /** * Add parameter and return parameter name */ private String addParameter(Object value) { String paramName = "param" + (parameters.size() + 1); parameters.put(paramName, value); return paramName; } public enum OrderDirection { ASC, DESC } public enum NullsOrder { NULLS_FIRST, NULLS_LAST } }

Spring Boot Integration

Configuration Class

package com.sanity.config; import com.sanity.client.SanityClient; import com.sanity.realtime.SanityRealtimeService; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SanityConfig { @Value("${sanity.project.id}") private String projectId; @Value("${sanity.dataset:production}") private String dataset; @Value("${sanity.token:}") private String token; @Value("${sanity.use-cdn:true}") private boolean useCdn; @Bean public SanityClient sanityClient() { return new SanityClient(projectId, dataset, token, useCdn); } @Bean public SanityRealtimeService sanityRealtimeService() { return new SanityRealtimeService(projectId, dataset, token); } }

REST Controller

package com.sanity.controller; import com.sanity.client.SanityClient; import com.sanity.model.Post; import com.sanity.query.GroqQueryBuilder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.HashMap; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/content") public class ContentController { @Autowired private SanityClient sanityClient; @GetMapping("/posts") public ResponseEntity<Map<String, Object>> getAllPosts( @RequestParam(defaultValue = "10") int limit, @RequestParam(defaultValue = "0") int offset) { Map<String, Object> response = new HashMap<>(); try { // Build query using GroqQueryBuilder String query = GroqQueryBuilder.forType("post") .project("_id", "title", "slug", "body", "publishedAt", "mainImage") .orderBy("publishedAt", GroqQueryBuilder.OrderDirection.DESC) .limit(limit) .offset(offset) .build(); var result = sanityClient.queryList(query, Post.class); response.put("success", true); response.put("posts", result.getResult()); response.put("total", result.getResult().size()); response.put("queryTime", result.getMs()); return ResponseEntity.ok(response); } catch (Exception e) { response.put("success", false); response.put("error", e.getMessage()); return ResponseEntity.badRequest().body(response); } } @GetMapping("/posts/{slug}") public ResponseEntity<Map<String, Object>> getPostBySlug(@PathVariable String slug) { Map<String, Object> response = new HashMap<>(); try { String query = GroqQueryBuilder.forType("post") .project("_id", "title", "slug", "body", "publishedAt", "mainImage", "author") .whereEquals("slug.current", slug) .limit(1) .build(); var result = sanityClient.queryList(query, Post.class); if (result.getResult().isEmpty()) { response.put("success", false); response.put("error", "Post not found"); return ResponseEntity.notFound().build(); } response.put("success", true); response.put("post", result.getResult().get(0)); return ResponseEntity.ok(response); } catch (Exception e) { response.put("success", false); response.put("error", e.getMessage()); return ResponseEntity.badRequest().body(response); } } @GetMapping("/posts/search") public ResponseEntity<Map<String, Object>> searchPosts(@RequestParam String query) { Map<String, Object> response = new HashMap<>(); try { String groqQuery = GroqQueryBuilder.forType("post") .project("_id", "title", "slug", "body", "publishedAt") .filter("title match $searchQuery || body match $searchQuery") .orderBy("publishedAt", GroqQueryBuilder.OrderDirection.DESC) .limit(20) .build(); Map<String, Object> params = new HashMap<>(); params.put("searchQuery", query + "*"); var result = sanityClient.queryList(groqQuery, params, Post.class); response.put("success", true); response.put("posts", result.getResult()); response.put("count", result.getResult().size()); return ResponseEntity.ok(response); } catch (Exception e) { response.put("success", false); response.put("error", e.getMessage()); return ResponseEntity.badRequest().body(response); } } @PostMapping("/posts") public ResponseEntity<Map<String, Object>> createPost(@RequestBody Post post) { Map<String, Object> response = new HashMap<>(); try { var result = sanityClient.create(post); response.put("success", true); response.put("documentId", result.getResults().get(0).getId()); response.put("transactionId", result.getTransactionId()); return ResponseEntity.ok(response); } catch (Exception e) { response.put("success", false); response.put("error", e.getMessage()); return ResponseEntity.badRequest().body(response); } } }

Application Properties

# Sanity.io Configuration sanity.project.id=your-project-id sanity.dataset=production sanity.token=your-sanit-token sanity.use-cdn=true # Server Configuration server.port=8080 # Logging logging.level.com.sanity=INFO

Usage Examples

Basic Usage

package com.sanity.examples; import com.sanity.client.SanityClient; import com.sanity.model.Post; import com.sanity.query.GroqQueryBuilder; import java.util.List; import java.util.Map; public class SanityExamples { public static void main(String[] args) { // Initialize client SanityClient client = new SanityClient( "your-project-id", "production", "your-token" ); try { // Example 1: Query all posts String query = GroqQueryBuilder.forType("post") .project("_id", "title", "slug", "publishedAt") .orderBy("publishedAt", GroqQueryBuilder.OrderDirection.DESC) .limit(10) .build(); var result = client.queryList(query, Post.class); List<Post> posts = result.getResult(); // Example 2: Query with parameters String paramQuery = GroqQueryBuilder.forType("post") .project("title", "slug") .whereEquals("publishedAt", "2023-01-01") .build(); Map<String, Object> params = Map.of("publishedAt", "2023-01-01"); var paramResult = client.queryList(paramQuery, params, Post.class); // Example 3: Create a new post Post newPost = new Post(); newPost.setTitle("New Blog Post"); newPost.setSlug("new-blog-post"); newPost.setBody("This is the content of the new blog post."); var createResult = client.create(newPost); System.out.println("Created post with ID: " + createResult.getResults().get(0).getId()); } catch (Exception e) { e.printStackTrace(); } } }

Conclusion

This comprehensive Sanity.io Java client provides:

  1. Complete API coverage for queries, mutations, and assets
  2. Real-time subscriptions for live content updates
  3. Type-safe models for Java integration
  4. Query builder for constructing GROQ queries
  5. Spring Boot integration for easy configuration
  6. Error handling and comprehensive logging

Key features:

  • GROQ query execution with parameter support
  • Document mutations for create, update, and delete operations
  • Asset management for file uploads
  • Real-time updates using Server-Sent Events
  • Type-safe response parsing with Jackson
  • Flexible query building with the GroqQueryBuilder

This implementation enables Java applications to seamlessly integrate with Sanity.io headless CMS, providing robust content management capabilities with real-time updates and type safety.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper