This article covers a Bloomreach Experience Manager version 13. There's an updated version available that covers our most recent release.

Developer's How-to

Bloomreach offers Enterprise support for this feature to Bloomreach Experience customers. The release cycle of this feature may differ from our core product release cycle. 

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:

  1. In a Hippo Repository Addon (Daemon) Module, create a listener and register it on initliazation.
  2. 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

 

Did you find this page helpful?
How could this documentation serve you better?
On this page
    Did you find this page helpful?
    How could this documentation serve you better?