Developer's How-to
How to subscribe image/asset creation/update event
Gallery Asset Bulk Upload Plugin initializes and registers a com.onehippo.cms7.galleryasset.bulkupload.SynchronousEventBus service to post a gallery item creation/update event. The reason why it doesn't use Hippo Event Bus is that Hippo Event Bus is based on Guava AsyncEventBus which executes event handlers asynchronously through an internal ThreadPoolExecutor. In contrast, SynchronousEventBus is implemented with Guava EventBus which executes event handlers synchronously to allow to create/update associated documents for instance.
Therefore, if your application needs to subscribe a gallery event and perform an action synchronously, you need to register your listener which has Guava's @Subscribe annotation on method(s) to the SynchronousEventBus service.
How to register a listener to SynchronousEventBus
When Hippo CMS starts up, the plugin registers a SynchronousEventBus service automatically. Therefore, if you want to register your listener to it, you need to do the following in Hippo CMS web application module:
- In a Hippo Repository Addon (Daemon) Module, create a listener and register it on initliazation.
- When an event is propagated, you need to read the gallery event object (category, action and subjectId) and you can do whatever you want by using that information.
Here's an example of DaemonModule implementation which initializes and registers an event listener:
@RequiresService(types = { SynchronousEventBus.class }) public class GalleryEventListenerRegisteringDaemonModule extends AbstractReconfigurableDaemonModule { private DemoGalleryEventListener demoGalleryEventListener; private boolean registered; public GalleryEventListenerRegisteringDaemonModule() { demoGalleryEventListener = new DemoGalleryEventListener(); } @Override protected void doInitialize(Session session) throws RepositoryException { if (!registered) { SynchronousEventBus synchronousEventBus = HippoServiceRegistry.getService(SynchronousEventBus.class); if (synchronousEventBus != null) { synchronousEventBus.register(demoGalleryEventListener); registered = true; } } } @Override protected void doShutdown() { SynchronousEventBus synchronousEventBus = HippoServiceRegistry.getService(SynchronousEventBus.class); if (synchronousEventBus != null) { synchronousEventBus.unregister(demoGalleryEventListener); } } }
And your listener can be implemented like like the following:
public class DemoGalleryEventListener { private static Logger log = LoggerFactory.getLogger(DemoGalleryEventListener.class); private static final Credentials SYSTEM_CREDENTIALS = new SimpleCredentials("system", new char[] {}); private static final String IMAGE_DOCUMENT_TYPE = "hippoaddongalleryassetbulkuploaddemo:imageDocument"; private static final String IMAGE_LINK_NODE_NAME = "hippoaddongalleryassetbulkuploaddemo:image"; private static final String ASSET_DOCUMENT_TYPE = "hippoaddongalleryassetbulkuploaddemo:assetDocument"; private static final String ASSET_LINK_NODE_NAME = "hippoaddongalleryassetbulkuploaddemo:asset"; private static final String LAST_MODIFIED_TIME_PROP_NAME = "hippoaddongalleryassetbulkuploaddemo:lastModified"; private boolean enabled; public DemoGalleryEventListener() { } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } /** * Event handler method. * @param event gallery event */ @Subscribe public void handleEvent(GalleryEvent event) { if (!isEnabled()) { log.debug("Ignoring gallery event because the listener is disabled."); return; } // Make sure if the gallery event is of 'gallery' category and of 'createGalleryItem' action. if ("gallery".equals(event.application()) && "createGalleryItem".equals(event.get("action"))) { createOrUpdateAssociatedDocument((String) event.get("subjectId")); } } /** * Creates or update an associated document for the gallery image or asset document pointed by {@code subjectId}. * @param subjectId node id of a gallery image or asset document node, or a gallery folder node */ private void createOrUpdateAssociatedDocument(final String subjectId) { // Retrieves the user-selected options in CMS UI from GalleryProcessingContext which maintains all those // selections in a ThreadLocal map internally. final boolean bulkUploadMode = GalleryProcessingContext.isBulkUploadMode(); final boolean overwriteOption = GalleryProcessingContext.isOverwriteOption(); final boolean publishOption = GalleryProcessingContext.isPublishOption(); // Get the current CMS user's JCR session. Session jcrSession = UserSession.get().getJcrSession(); try { final Node subjectNode = jcrSession.getNodeByIdentifier(subjectId); if (subjectNode.isNodeType(HippoStdNodeType.NT_FOLDER)) { log.debug("Skipping the subject node because it is a folder."); return; } // Retrieve handle node from the gallery image or asset node. final Node handleNode = HippoNodeUtils.getHippoDocumentHandle(subjectNode); final String handleNodePath = handleNode.getPath(); boolean imageHandle; if (StringUtils.startsWith(handleNodePath, "/content/assets/")) { imageHandle = false; } else if (StringUtils.startsWith(handleNodePath, "/content/gallery/")) { imageHandle = true; } else { throw new IllegalArgumentException("Non image or asset handle path: " + handleNodePath); } // Create a DocumentManager (from Content-EXIM forge module). DocumentManager documentManager = new WorkflowDocumentManagerImpl(jcrSession); String documentLocation = getAssociatedDocumentLocation(handleNode, imageHandle); // Check if the associated document exists at the location. boolean existing = documentManager.documentExists(documentLocation); if (existing && !overwriteOption) { log.debug("Skipping document creation at {} because overwriting option is disabled.", documentLocation); } else { log.debug("{} associated document at {}.", (existing ? "Overwriting" : "Creating"), documentLocation); final String documentName = StringUtils.substringAfterLast(documentLocation, "/"); // Create a ContentNode object to create a document through DocumentVariantImportTask. ContentNode contentNode; if (imageHandle) { contentNode = createImageDocumentContentNode(documentName, handleNode.getIdentifier()); } else { contentNode = createAssetDocumentContentNode(documentName, handleNode.getIdentifier()); } DocumentVariantImportTask importTask = new WorkflowDocumentVariantImportTask(documentManager); documentLocation = importTask.createOrUpdateDocumentFromVariantContentNode(contentNode, contentNode.getPrimaryType(), documentLocation, "en", documentName); if (publishOption) { documentManager.publishDocument(documentLocation); } jcrSession.save(); } } catch (Exception e) { log.error("Failed to create or update associated document.", e); } } /** * Get the path of the associated document for the a gallery image or asset {@code handle} node. * <P> * This example determines the associated document location like the following example: * </P> * <XMP> * /content/gallery/myhippoproject/samples/test.png * --> /content/documents/myhippoproject/images/samples/test.png * </XMP> * @param handle a gallery image or asset {@code handle} node * @return the path of the associated document * @throws RepositoryException if repository exception occurs */ private String getAssociatedDocumentLocation(final Node handle, final boolean imageHandle) throws RepositoryException { final String handlePath = handle.getPath(); String galleryRelPath = null; if (StringUtils.startsWith(handlePath, "/content/assets/")) { galleryRelPath = StringUtils.removeStart(handle.getPath(), "/content/assets/"); } else if (StringUtils.startsWith(handlePath, "/content/gallery/")) { galleryRelPath = StringUtils.removeStart(handle.getPath(), "/content/gallery/"); } else { throw new IllegalArgumentException("Non image or asset handle path: " + handlePath); } int offset = galleryRelPath.indexOf('/'); final String projectFolderName = galleryRelPath.substring(0, offset); final String documentLocation = "/content/documents/" + projectFolderName + (imageHandle ? "/images" : "/assets") + galleryRelPath.substring(offset); return documentLocation; } /** * Create a {@link ContentNode} object for the image container document content, to be used by {@link DocumentVariantImportTask}. * @param name document name * @param imageHandleIdentifier image handle node identifier * @return a {@link ContentNode} object to be used by to be used by {@link DocumentVariantImportTask} */ private ContentNode createImageDocumentContentNode(final String name, final String imageHandleIdentifier) { ContentNode contentNode = new ContentNode(name, IMAGE_DOCUMENT_TYPE); contentNode.addMixinType("mix:referenceable"); ContentNode linkNode = new ContentNode(IMAGE_LINK_NODE_NAME, "hippogallerypicker:imagelink"); linkNode.setProperty(new ContentProperty(HippoNodeType.HIPPO_FACETS, ContentPropertyType.STRING, true)); linkNode.setProperty(new ContentProperty(HippoNodeType.HIPPO_VALUES, ContentPropertyType.STRING, true)); linkNode.setProperty(new ContentProperty(HippoNodeType.HIPPO_MODES, ContentPropertyType.STRING, true)); linkNode.setProperty(HippoNodeType.HIPPO_DOCBASE, imageHandleIdentifier); contentNode.addNode(linkNode); ContentProperty lastModifiedProp = new ContentProperty(LAST_MODIFIED_TIME_PROP_NAME, ContentPropertyType.DATE); lastModifiedProp.setValue(ISO8601.format(Calendar.getInstance())); contentNode.setProperty(lastModifiedProp); return contentNode; } /** * Create a {@link ContentNode} object for the asset container document content, to be used by {@link DocumentVariantImportTask}. * @param name document name * @param assetHandleIdentifier asset handle node identifier * @return a {@link ContentNode} object to be used by to be used by {@link DocumentVariantImportTask} */ private ContentNode createAssetDocumentContentNode(final String name, final String assetHandleIdentifier) { ContentNode contentNode = new ContentNode(name, ASSET_DOCUMENT_TYPE); contentNode.addMixinType("mix:referenceable"); ContentNode linkNode = new ContentNode(ASSET_LINK_NODE_NAME, HippoNodeType.NT_MIRROR); linkNode.setProperty(HippoNodeType.HIPPO_DOCBASE, assetHandleIdentifier); contentNode.addNode(linkNode); ContentProperty lastModifiedProp = new ContentProperty(LAST_MODIFIED_TIME_PROP_NAME, ContentPropertyType.DATE); lastModifiedProp.setValue(ISO8601.format(Calendar.getInstance())); contentNode.setProperty(lastModifiedProp); return contentNode; } }
Bootstrapping Your DaemonModule
In the preceding, we saw a DaemonModule implementation which initializes and registers an event listener. This DaemonModule should be bootstrapped to apply it in the next deployment cycle. Here's an example configuration at repository-data/application/src/main/resources/hcm-config/configuration/modules/demo-gallery-eventhandler.yaml:
definitions: config: /hippo:configuration/hippo:modules/demo-gallery-eventhandler: jcr:primaryType: hipposys:module hipposys:className: com.onehippo.cms7.galleryasset.bulkupload.plugin.gallery.demo.event.GalleryEventListenerRegisteringDaemonModule /hippo:moduleconfig: jcr:primaryType: nt:unstructured enabled: true