Prismic.io Integration in Java: Headless CMS Content Management

Prismic.io is a headless CMS (Content Management System) that provides content as a service. This Java implementation demonstrates how to integrate with Prismic.io for content management, delivery, and synchronization.

Prismic.io Overview

Key features:

  • Content repository management
  • Structured content with custom types
  • Multi-language support
  • Content preview and versioning
  • GraphQL and REST APIs
  • Webhook integrations

Dependencies and Setup

Maven Configuration:

<dependencies>
<!-- Prismic.io Java Kit -->
<dependency>
<groupId>io.prismic</groupId>
<artifactId>java-kit</artifactId>
<version>2.6.0</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- Caching -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
</dependency>
</dependencies>

Configuration Management

PrismicConfig.java:

@Configuration
@ConfigurationProperties(prefix = "prismic")
@Data
public class PrismicConfig {
private String repositoryName;
private String accessToken;
private String apiUrl = "https://{repository}.prismic.io/api/v2";
private CacheConfig cache = new CacheConfig();
private PreviewConfig preview = new PreviewConfig();
private WebhookConfig webhook = new WebhookConfig();
@Data
public static class CacheConfig {
private boolean enabled = true;
private Duration ttl = Duration.ofMinutes(5);
private int maxSize = 1000;
private CacheType type = CacheType.CAFFEINE;
}
@Data
public static class PreviewConfig {
private boolean enabled = true;
private String previewToken;
private Duration sessionDuration = Duration.ofHours(2);
private String redirectUrl = "/preview";
}
@Data
public static class WebhookConfig {
private String secret;
private String endpoint = "/api/webhooks/prismic";
private List<String> allowedIps = new ArrayList<>();
}
public String getApiUrl() {
return apiUrl.replace("{repository}", repositoryName);
}
public enum CacheType {
CAFFEINE, REDIS, MEMORY
}
}

Prismic Client Service

PrismicClientService.java:

@Service
@Slf4j
public class PrismicClientService {
private final PrismicConfig config;
private final Api api;
private final Cache<String, Object> cache;
private final ObjectMapper objectMapper;
public PrismicClientService(PrismicConfig config) {
this.config = config;
this.api = initializeApi();
this.cache = createCache();
this.objectMapper = new ObjectMapper();
}
private Api initializeApi() {
try {
Cache cache = new Cache.BuiltInCache(1000);
Logger logger = new Logger.PrintlnLogger();
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
return Api.get(config.getApiUrl(), config.getAccessToken(), 
httpClient, cache, logger);
} catch (Exception e) {
throw new PrismicException("Failed to initialize Prismic API", e);
}
}
private Cache<String, Object> createCache() {
if (!config.getCache().isEnabled()) {
return null;
}
return Caffeine.newBuilder()
.expireAfterWrite(config.getCache().getTtl())
.maximumSize(config.getCache().getMaxSize())
.build();
}
public Document getDocumentById(String documentId, String language) {
String cacheKey = String.format("doc:%s:%s", documentId, language);
if (cache != null) {
Document cached = (Document) cache.getIfPresent(cacheKey);
if (cached != null) {
return cached;
}
}
try {
Document document = api.getByID(documentId, 
api.getData().getRef(), language);
if (cache != null && document != null) {
cache.put(cacheKey, document);
}
return document;
} catch (Exception e) {
throw new PrismicException("Failed to get document by ID: " + documentId, e);
}
}
public List<Document> getDocumentsByType(String documentType, String language) {
String cacheKey = String.format("docs:type:%s:%s", documentType, language);
if (cache != null) {
@SuppressWarnings("unchecked")
List<Document> cached = (List<Document>) cache.getIfPresent(cacheKey);
if (cached != null) {
return cached;
}
}
try {
Response response = api.query(
Predicates.at("document.type", documentType),
new QueryOptions()
.lang(language)
.ref(api.getData().getRef())
);
List<Document> documents = response.getResults();
if (cache != null) {
cache.put(cacheKey, documents);
}
return documents;
} catch (Exception e) {
throw new PrismicException("Failed to get documents by type: " + documentType, e);
}
}
public Document getSingleDocument(String documentType, String language) {
List<Document> documents = getDocumentsByType(documentType, language);
return documents.isEmpty() ? null : documents.get(0);
}
public Response searchDocuments(String query, String language, 
Map<String, String> filters, 
PaginationParams pagination) {
try {
List<Predicate> predicates = new ArrayList<>();
// Add search query
if (query != null && !query.trim().isEmpty()) {
predicates.add(Predicates.fulltext("document", query));
}
// Add filters
if (filters != null) {
filters.forEach((field, value) -> 
predicates.add(Predicates.at(field, value)));
}
// Build query options
QueryOptions options = new QueryOptions()
.lang(language)
.ref(api.getData().getRef());
if (pagination != null) {
options.page(pagination.getPage())
.pageSize(pagination.getPageSize());
}
// Execute query
return api.query(predicates.toArray(new Predicate[0]), options);
} catch (Exception e) {
throw new PrismicException("Document search failed", e);
}
}
public List<Document> getDocumentsByTags(List<String> tags, String language) {
String cacheKey = String.format("docs:tags:%s:%s", 
String.join(",", tags), language);
if (cache != null) {
@SuppressWarnings("unchecked")
List<Document> cached = (List<Document>) cache.getIfPresent(cacheKey);
if (cached != null) {
return cached;
}
}
try {
Response response = api.query(
Predicates.any("document.tags", tags),
new QueryOptions()
.lang(language)
.ref(api.getData().getRef())
);
List<Document> documents = response.getResults();
if (cache != null) {
cache.put(cacheKey, documents);
}
return documents;
} catch (Exception e) {
throw new PrismicException("Failed to get documents by tags", e);
}
}
public Map<String, Object> getDocumentAsMap(Document document) {
try {
Map<String, Object> result = new HashMap<>();
result.put("id", document.getId());
result.put("type", document.getType());
result.put("tags", document.getTags());
result.put("slug", document.getSlug());
result.put("lang", document.getLang());
result.put("lastPublicationDate", document.getLastPublicationDate());
// Convert fragments to map
Map<String, Object> data = new HashMap<>();
for (String fragmentName : document.getFragments().keySet()) {
Fragment fragment = document.getFragment(fragmentName);
data.put(fragmentName, convertFragmentToObject(fragment));
}
result.put("data", data);
return result;
} catch (Exception e) {
throw new PrismicException("Failed to convert document to map", e);
}
}
private Object convertFragmentToObject(Fragment fragment) {
if (fragment instanceof Group) {
return convertGroupToMap((Group) fragment);
} else if (fragment instanceof StructuredText) {
return convertStructuredTextToObject((StructuredText) fragment);
} else if (fragment instanceof Image) {
return convertImageToMap((Image) fragment);
} else if (fragment instanceof Link) {
return convertLinkToMap((Link) fragment);
} else if (fragment instanceof Date) {
return ((Date) fragment).getValue();
} else if (fragment instanceof Number) {
return ((Number) fragment).getValue();
} else if (fragment instanceof Text) {
return ((Text) fragment).getValue();
} else if (fragment instanceof Color) {
return ((Color) fragment).getHexValue();
} else if (fragment instanceof Select) {
return ((Select) fragment).getValue();
} else if (fragment instanceof Embed) {
return convertEmbedToMap((Embed) fragment);
} else {
return fragment.toString();
}
}
private Map<String, Object> convertGroupToMap(Group group) {
List<Map<String, Object>> groupData = new ArrayList<>();
for (GroupDoc groupDoc : group.getGroupDocs()) {
Map<String, Object> docData = new HashMap<>();
for (String fragmentName : groupDoc.getFragments().keySet()) {
Fragment fragment = groupDoc.getFragment(fragmentName);
docData.put(fragmentName, convertFragmentToObject(fragment));
}
groupData.add(docData);
}
return Map.of("group", groupData);
}
private Object convertStructuredTextToObject(StructuredText structuredText) {
if (structuredText.getParagraphs().isEmpty()) {
return structuredText.getFirstParagraph() != null ? 
structuredText.getFirstParagraph().getText() : "";
}
List<Map<String, Object>> blocks = new ArrayList<>();
for (StructuredText.Block block : structuredText.getBlocks()) {
Map<String, Object> blockData = new HashMap<>();
blockData.put("type", block.getLabel());
blockData.put("text", block.getText());
if (block instanceof StructuredText.Heading) {
blockData.put("level", ((StructuredText.Heading) block).getLevel());
}
if (block instanceof StructuredText.ListItem) {
blockData.put("ordered", ((StructuredText.ListItem) block).isOrdered());
}
blocks.add(blockData);
}
return Map.of("blocks", blocks, "text", structuredText.getFirstParagraph().getText());
}
private Map<String, Object> convertImageToMap(Image image) {
Map<String, Object> imageData = new HashMap<>();
imageData.put("url", image.getUrl());
imageData.put("alt", image.getAlt());
imageData.put("copyright", image.getCopyright());
Map<String, Object> dimensions = new HashMap<>();
dimensions.put("width", image.getWidth());
dimensions.put("height", image.getHeight());
imageData.put("dimensions", dimensions);
// Add views (different image sizes)
Map<String, Object> views = new HashMap<>();
for (Image.View view : image.getViews()) {
Map<String, Object> viewData = new HashMap<>();
viewData.put("url", view.getUrl());
viewData.put("width", view.getWidth());
viewData.put("height", view.getHeight());
views.put(view.getName(), viewData);
}
imageData.put("views", views);
return imageData;
}
private Map<String, Object> convertLinkToMap(Link link) {
Map<String, Object> linkData = new HashMap<>();
linkData.put("url", link.getUrl());
linkData.put("target", link.getTarget());
if (link instanceof DocumentLink) {
DocumentLink docLink = (DocumentLink) link;
linkData.put("type", "document");
linkData.put("documentId", docLink.getId());
linkData.put("isBroken", docLink.isBroken());
} else if (link instanceof WebLink) {
linkData.put("type", "web");
} else if (link instanceof FileLink) {
FileLink fileLink = (FileLink) link;
linkData.put("type", "file");
linkData.put("name", fileLink.getFilename());
linkData.put("size", fileLink.getFilesize());
} else if (link instanceof ImageLink) {
linkData.put("type", "image");
}
return linkData;
}
private Map<String, Object> convertEmbedToMap(Embed embed) {
Map<String, Object> embedData = new HashMap<>();
embedData.put("html", embed.getHtml());
embedData.put("type", embed.getType());
embedData.put("provider", embed.getProvider());
embedData.put("url", embed.getUrl());
Map<String, Object> dimensions = new HashMap<>();
dimensions.put("width", embed.getWidth());
dimensions.put("height", embed.getHeight());
embedData.put("dimensions", dimensions);
return embedData;
}
public void clearCache() {
if (cache != null) {
cache.invalidateAll();
}
}
public void clearCacheForType(String documentType) {
if (cache != null) {
// Invalidate all cache entries for this document type
cache.asMap().keySet().stream()
.filter(key -> key.startsWith("docs:type:" + documentType))
.forEach(cache::invalidate);
}
}
public Api getApi() {
return api;
}
}

Content Management Service

ContentService.java:

@Service
@Slf4j
public class ContentService {
private final PrismicClientService prismicClient;
private final ObjectMapper objectMapper;
public ContentService(PrismicClientService prismicClient) {
this.prismicClient = prismicClient;
this.objectMapper = new ObjectMapper();
}
public ContentResponse getPageContent(String pageId, String language) {
try {
Document document = prismicClient.getDocumentById(pageId, language);
if (document == null) {
throw new ContentNotFoundException("Page not found: " + pageId);
}
Map<String, Object> content = prismicClient.getDocumentAsMap(document);
return ContentResponse.builder()
.success(true)
.content(content)
.metadata(extractMetadata(document))
.build();
} catch (Exception e) {
log.error("Failed to get page content: {}", pageId, e);
return ContentResponse.builder()
.success(false)
.error(e.getMessage())
.build();
}
}
public ContentResponse getContentByType(String contentType, String language) {
try {
List<Document> documents = prismicClient.getDocumentsByType(contentType, language);
List<Map<String, Object>> contentList = documents.stream()
.map(prismicClient::getDocumentAsMap)
.collect(Collectors.toList());
return ContentResponse.builder()
.success(true)
.content(contentList)
.metadata(Map.of("count", contentList.size()))
.build();
} catch (Exception e) {
log.error("Failed to get content by type: {}", contentType, e);
return ContentResponse.builder()
.success(false)
.error(e.getMessage())
.build();
}
}
public ContentResponse searchContent(ContentSearchRequest request) {
try {
Response response = prismicClient.searchDocuments(
request.getQuery(),
request.getLanguage(),
request.getFilters(),
request.getPagination()
);
List<Map<String, Object>> results = response.getResults().stream()
.map(prismicClient::getDocumentAsMap)
.collect(Collectors.toList());
PaginationMetadata pagination = PaginationMetadata.builder()
.page(response.getPage())
.resultsPerPage(response.getResultsPerPage())
.totalResults(response.getTotalResultsSize())
.totalPages(response.getTotalPages())
.build();
return ContentResponse.builder()
.success(true)
.content(results)
.pagination(pagination)
.metadata(Map.of("query", request.getQuery()))
.build();
} catch (Exception e) {
log.error("Content search failed", e);
return ContentResponse.builder()
.success(false)
.error(e.getMessage())
.build();
}
}
public ContentResponse getContentBySlug(String contentType, String slug, String language) {
try {
List<Document> documents = prismicClient.getDocumentsByType(contentType, language);
Optional<Document> document = documents.stream()
.filter(doc -> slug.equals(doc.getSlug()))
.findFirst();
if (document.isPresent()) {
Map<String, Object> content = prismicClient.getDocumentAsMap(document.get());
return ContentResponse.builder()
.success(true)
.content(content)
.metadata(extractMetadata(document.get()))
.build();
} else {
return ContentResponse.builder()
.success(false)
.error("Content not found for slug: " + slug)
.build();
}
} catch (Exception e) {
log.error("Failed to get content by slug: {}", slug, e);
return ContentResponse.builder()
.success(false)
.error(e.getMessage())
.build();
}
}
public NavigationResponse getNavigation(String navigationId, String language) {
try {
Document navigationDoc = prismicClient.getDocumentById(navigationId, language);
if (navigationDoc == null) {
throw new ContentNotFoundException("Navigation not found: " + navigationId);
}
Map<String, Object> navigationData = prismicClient.getDocumentAsMap(navigationDoc);
List<NavigationItem> navItems = extractNavigationItems(navigationData);
return NavigationResponse.builder()
.success(true)
.navigation(navItems)
.metadata(extractMetadata(navigationDoc))
.build();
} catch (Exception e) {
log.error("Failed to get navigation: {}", navigationId, e);
return NavigationResponse.builder()
.success(false)
.error(e.getMessage())
.build();
}
}
public SettingsResponse getSiteSettings(String settingsId, String language) {
try {
Document settingsDoc = prismicClient.getDocumentById(settingsId, language);
if (settingsDoc == null) {
throw new ContentNotFoundException("Settings not found: " + settingsId);
}
Map<String, Object> settingsData = prismicClient.getDocumentAsMap(settingsDoc);
SiteSettings settings = extractSiteSettings(settingsData);
return SettingsResponse.builder()
.success(true)
.settings(settings)
.metadata(extractMetadata(settingsDoc))
.build();
} catch (Exception e) {
log.error("Failed to get site settings: {}", settingsId, e);
return SettingsResponse.builder()
.success(false)
.error(e.getMessage())
.build();
}
}
public ContentPreviewResponse previewContent(String token, String language) {
try {
// Use preview token to get preview content
Api api = prismicClient.getApi();
String previewRef = api.getPreviewSession(token, 
config.getPreview().getRedirectUrl(), 
api.getData().getRefs());
// Query documents with preview ref
Response response = api.query(
Predicates.at("document.type", "page"),
new QueryOptions()
.lang(language)
.ref(previewRef)
);
List<Map<String, Object>> previewContent = response.getResults().stream()
.map(prismicClient::getDocumentAsMap)
.collect(Collectors.toList());
return ContentPreviewResponse.builder()
.success(true)
.content(previewContent)
.previewRef(previewRef)
.build();
} catch (Exception e) {
log.error("Content preview failed", e);
return ContentPreviewResponse.builder()
.success(false)
.error(e.getMessage())
.build();
}
}
private Map<String, Object> extractMetadata(Document document) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("id", document.getId());
metadata.put("type", document.getType());
metadata.put("lastModified", document.getLastPublicationDate());
metadata.put("tags", document.getTags());
metadata.put("language", document.getLang());
return metadata;
}
private List<NavigationItem> extractNavigationItems(Map<String, Object> navigationData) {
List<NavigationItem> navItems = new ArrayList<>();
try {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) navigationData.get("data");
if (data != null && data.containsKey("navigation_links")) {
@SuppressWarnings("unchecked")
Map<String, Object> navLinks = (Map<String, Object>) data.get("navigation_links");
if (navLinks.containsKey("group")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> links = (List<Map<String, Object>>) 
navLinks.get("group");
for (Map<String, Object> link : links) {
NavigationItem item = NavigationItem.builder()
.label((String) link.get("label"))
.url((String) link.get("url"))
.target((String) link.get("target"))
.build();
navItems.add(item);
}
}
}
} catch (Exception e) {
log.warn("Failed to extract navigation items", e);
}
return navItems;
}
private SiteSettings extractSiteSettings(Map<String, Object> settingsData) {
SiteSettings.SiteSettingsBuilder builder = SiteSettings.builder();
try {
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) settingsData.get("data");
if (data != null) {
builder.siteTitle(getStringValue(data, "site_title"))
.siteDescription(getStringValue(data, "site_description"))
.logo(extractImage(data, "logo"))
.favicon(extractImage(data, "favicon"))
.socialLinks(extractSocialLinks(data));
}
} catch (Exception e) {
log.warn("Failed to extract site settings", e);
}
return builder.build();
}
private String getStringValue(Map<String, Object> data, String key) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> field = (Map<String, Object>) data.get(key);
return field != null ? (String) field.get("text") : null;
} catch (Exception e) {
return null;
}
}
private ImageData extractImage(Map<String, Object> data, String key) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> imageField = (Map<String, Object>) data.get(key);
if (imageField != null) {
return ImageData.builder()
.url((String) imageField.get("url"))
.alt((String) imageField.get("alt"))
.build();
}
} catch (Exception e) {
log.debug("Failed to extract image: {}", key, e);
}
return null;
}
private Map<String, String> extractSocialLinks(Map<String, Object> data) {
Map<String, String> socialLinks = new HashMap<>();
try {
@SuppressWarnings("unchecked")
Map<String, Object> socialField = (Map<String, Object>) data.get("social_links");
if (socialField != null && socialField.containsKey("group")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> links = (List<Map<String, Object>>) 
socialField.get("group");
for (Map<String, Object> link : links) {
String platform = (String) link.get("platform");
String url = (String) link.get("url");
if (platform != null && url != null) {
socialLinks.put(platform, url);
}
}
}
} catch (Exception e) {
log.debug("Failed to extract social links", e);
}
return socialLinks;
}
}

Webhook Handler

PrismicWebhookHandler.java:

@Service
@Slf4j
public class PrismicWebhookHandler {
private final PrismicClientService prismicClient;
private final PrismicConfig config;
private final ObjectMapper objectMapper;
public PrismicWebhookHandler(PrismicClientService prismicClient,
PrismicConfig config) {
this.prismicClient = prismicClient;
this.config = config;
this.objectMapper = new ObjectMapper();
}
public WebhookResponse handleWebhook(WebhookRequest request, 
HttpServletRequest httpRequest) {
try {
// Verify webhook secret
if (!isValidWebhook(request, httpRequest)) {
log.warn("Invalid webhook request received");
return WebhookResponse.builder()
.success(false)
.message("Invalid webhook signature")
.build();
}
String eventType = request.getType();
log.info("Processing webhook event: {}", eventType);
switch (eventType) {
case "api-update":
handleApiUpdate(request);
break;
case "document-update":
handleDocumentUpdate(request);
break;
case "document-create":
handleDocumentCreate(request);
break;
case "document-delete":
handleDocumentDelete(request);
break;
case "experiment-update":
handleExperimentUpdate(request);
break;
default:
log.info("Unhandled webhook event type: {}", eventType);
}
return WebhookResponse.builder()
.success(true)
.message("Webhook processed successfully")
.build();
} catch (Exception e) {
log.error("Webhook processing failed", e);
return WebhookResponse.builder()
.success(false)
.message("Webhook processing failed: " + e.getMessage())
.build();
}
}
private void handleApiUpdate(WebhookRequest request) {
log.info("API update detected, clearing cache");
prismicClient.clearCache();
// Perform any API-level cache invalidation
broadcastCacheClear();
}
private void handleDocumentUpdate(WebhookRequest request) {
String documentId = request.getDocumentId();
String documentType = extractDocumentType(request);
log.info("Document update: {} (type: {})", documentId, documentType);
// Clear cache for this document and its type
prismicClient.clearCacheForType(documentType);
// Clear specific document cache
String cacheKey = String.format("doc:%s:%s", documentId, "*");
clearCacheByPattern(cacheKey);
// Notify subscribers
notifyDocumentUpdate(documentId, documentType);
}
private void handleDocumentCreate(WebhookRequest request) {
String documentId = request.getDocumentId();
String documentType = extractDocumentType(request);
log.info("Document created: {} (type: {})", documentId, documentType);
// Clear cache for this document type
prismicClient.clearCacheForType(documentType);
// Notify subscribers
notifyDocumentCreate(documentId, documentType);
}
private void handleDocumentDelete(WebhookRequest request) {
String documentId = request.getDocumentId();
String documentType = extractDocumentType(request);
log.info("Document deleted: {} (type: {})", documentId, documentType);
// Clear cache for this document type
prismicClient.clearCacheForType(documentType);
// Clear specific document cache
String cacheKey = String.format("doc:%s:%s", documentId, "*");
clearCacheByPattern(cacheKey);
// Notify subscribers
notifyDocumentDelete(documentId, documentType);
}
private void handleExperimentUpdate(WebhookRequest request) {
log.info("Experiment update detected");
// Clear all cache as experiments might affect multiple documents
prismicClient.clearCache();
// Notify about experiment changes
notifyExperimentUpdate();
}
private boolean isValidWebhook(WebhookRequest request, HttpServletRequest httpRequest) {
// Verify IP address
if (!isAllowedIp(httpRequest.getRemoteAddr())) {
log.warn("Webhook from unauthorized IP: {}", httpRequest.getRemoteAddr());
return false;
}
// Verify secret (if configured)
if (config.getWebhook().getSecret() != null) {
String signature = httpRequest.getHeader("X-Prismic-Secret");
if (!config.getWebhook().getSecret().equals(signature)) {
log.warn("Invalid webhook signature");
return false;
}
}
return true;
}
private boolean isAllowedIp(String ipAddress) {
if (config.getWebhook().getAllowedIps().isEmpty()) {
return true; // No restrictions
}
return config.getWebhook().getAllowedIps().contains(ipAddress);
}
private String extractDocumentType(WebhookRequest request) {
try {
// Extract document type from webhook payload
if (request.getDocuments() != null && !request.getDocuments().isEmpty()) {
return request.getDocuments().get(0); // Simplified
}
} catch (Exception e) {
log.warn("Failed to extract document type from webhook", e);
}
return "unknown";
}
private void clearCacheByPattern(String pattern) {
// Implementation would depend on cache implementation
// This is a simplified version
log.debug("Clearing cache for pattern: {}", pattern);
}
private void broadcastCacheClear() {
// Notify other instances in a distributed environment
// This could use Redis pub/sub, Kafka, or other messaging systems
log.info("Broadcasting cache clear to all instances");
}
private void notifyDocumentUpdate(String documentId, String documentType) {
// Notify subscribers about document updates
// This could be through WebSocket, Server-Sent Events, or message queues
log.debug("Notifying subscribers about document update: {}", documentId);
}
private void notifyDocumentCreate(String documentId, String documentType) {
log.debug("Notifying subscribers about document creation: {}", documentId);
}
private void notifyDocumentDelete(String documentId, String documentType) {
log.debug("Notifying subscribers about document deletion: {}", documentId);
}
private void notifyExperimentUpdate() {
log.debug("Notifying subscribers about experiment update");
}
}

Response Models

ContentResponse.java:

@Data
@Builder
public class ContentResponse {
private boolean success;
private Object content;
private Map<String, Object> metadata;
private PaginationMetadata pagination;
private String error;
@Data
@Builder
public static class PaginationMetadata {
private int page;
private int resultsPerPage;
private int totalResults;
private int totalPages;
private String nextPage;
private String prevPage;
}
}
@Data
@Builder
public class NavigationResponse {
private boolean success;
private List<NavigationItem> navigation;
private Map<String, Object> metadata;
private String error;
}
@Data
@Builder
public class SettingsResponse {
private boolean success;
private SiteSettings settings;
private Map<String, Object> metadata;
private String error;
}
@Data
@Builder
public class ContentPreviewResponse {
private boolean success;
private Object content;
private String previewRef;
private String error;
}
@Data
@Builder
public class WebhookResponse {
private boolean success;
private String message;
}
@Data
@Builder
public class NavigationItem {
private String label;
private String url;
private String target;
private List<NavigationItem> children;
}
@Data
@Builder
public class SiteSettings {
private String siteTitle;
private String siteDescription;
private ImageData logo;
private ImageData favicon;
private Map<String, String> socialLinks;
@Data
@Builder
public static class ImageData {
private String url;
private String alt;
private Integer width;
private Integer height;
}
}
@Data
@Builder
public class ContentSearchRequest {
private String query;
private String language;
private Map<String, String> filters;
private PaginationParams pagination;
@Data
@Builder
public static class PaginationParams {
private int page = 1;
private int pageSize = 20;
}
}
@Data
public class WebhookRequest {
private String type;
private String domain;
private String apiUrl;
private String secret;
private List<String> documents;
private List<String> releases;
private List<String> tags;
private String documentId;
@JsonAnySetter
private Map<String, Object> additionalProperties = new HashMap<>();
}

REST API Controller

PrismicController.java:

@RestController
@RequestMapping("/api/content")
@Slf4j
public class PrismicController {
private final ContentService contentService;
private final PrismicWebhookHandler webhookHandler;
public PrismicController(ContentService contentService,
PrismicWebhookHandler webhookHandler) {
this.contentService = contentService;
this.webhookHandler = webhookHandler;
}
@GetMapping("/pages/{pageId}")
public ResponseEntity<ContentResponse> getPage(
@PathVariable String pageId,
@RequestParam(defaultValue = "en-us") String language) {
ContentResponse response = contentService.getPageContent(pageId, language);
return createResponse(response);
}
@GetMapping("/types/{contentType}")
public ResponseEntity<ContentResponse> getContentByType(
@PathVariable String contentType,
@RequestParam(defaultValue = "en-us") String language) {
ContentResponse response = contentService.getContentByType(contentType, language);
return createResponse(response);
}
@GetMapping("/types/{contentType}/slug/{slug}")
public ResponseEntity<ContentResponse> getContentBySlug(
@PathVariable String contentType,
@PathVariable String slug,
@RequestParam(defaultValue = "en-us") String language) {
ContentResponse response = contentService.getContentBySlug(contentType, slug, language);
return createResponse(response);
}
@PostMapping("/search")
public ResponseEntity<ContentResponse> searchContent(
@RequestBody ContentSearchRequest request) {
ContentResponse response = contentService.searchContent(request);
return createResponse(response);
}
@GetMapping("/navigation/{navigationId}")
public ResponseEntity<NavigationResponse> getNavigation(
@PathVariable String navigationId,
@RequestParam(defaultValue = "en-us") String language) {
NavigationResponse response = contentService.getNavigation(navigationId, language);
return createResponse(response);
}
@GetMapping("/settings/{settingsId}")
public ResponseEntity<SettingsResponse> getSiteSettings(
@PathVariable String settingsId,
@RequestParam(defaultValue = "en-us") String language) {
SettingsResponse response = contentService.getSiteSettings(settingsId, language);
return createResponse(response);
}
@PostMapping("/preview")
public ResponseEntity<ContentPreviewResponse> previewContent(
@RequestParam String token,
@RequestParam(defaultValue = "en-us") String language) {
ContentPreviewResponse response = contentService.previewContent(token, language);
return createResponse(response);
}
@PostMapping("/webhooks/prismic")
public ResponseEntity<WebhookResponse> handleWebhook(
@RequestBody WebhookRequest request,
HttpServletRequest httpRequest) {
WebhookResponse response = webhookHandler.handleWebhook(request, httpRequest);
return createResponse(response);
}
@PostMapping("/cache/clear")
public ResponseEntity<Map<String, Object>> clearCache(
@RequestParam(required = false) String documentType) {
try {
if (documentType != null) {
// Clear cache for specific document type
contentService.clearCacheForType(documentType);
} else {
// Clear all cache
contentService.clearCache();
}
return ResponseEntity.ok(Map.of(
"success", true,
"message", "Cache cleared successfully"
));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of(
"success", false,
"error", e.getMessage()
));
}
}
private <T> ResponseEntity<T> createResponse(T response) {
try {
// Check if response indicates success
Method successMethod = response.getClass().getMethod("isSuccess");
Boolean isSuccess = (Boolean) successMethod.invoke(response);
if (Boolean.TRUE.equals(isSuccess)) {
return ResponseEntity.ok(response);
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(response);
}
} catch (Exception e) {
// If we can't determine success, return as is
return ResponseEntity.ok(response);
}
}
}

Best Practices

  1. Implement caching to reduce API calls to Prismic.io
  2. Use webhooks for real-time content updates
  3. Handle errors gracefully with proper error responses
  4. Implement content preview for editors
  5. Use structured content types for consistent data
  6. Implement proper logging for debugging and monitoring
  7. Secure webhook endpoints with IP whitelisting and secrets

Conclusion

Prismic.io integration in Java provides:

  • Efficient content retrieval with caching
  • Structured content management with custom types
  • Multi-language support for global applications
  • Real-time updates through webhooks
  • Content preview capabilities
  • RESTful API for easy integration

This implementation demonstrates how to build a robust content management system using Prismic.io as a headless CMS, providing flexibility and performance for modern web applications.

Leave a Reply

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


Macro Nepal Helper