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
- Implement caching to reduce API calls to Prismic.io
- Use webhooks for real-time content updates
- Handle errors gracefully with proper error responses
- Implement content preview for editors
- Use structured content types for consistent data
- Implement proper logging for debugging and monitoring
- 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.