The Nuxeo Platform proposes an easy model for implementing custom logic through the Event Listener system. Sometimes, you want to "bubble" in the UI up to the user errors that happens in that lower layer. This page explains how this can be done by using the RecoverableClientException mechanism, throughout an example that executes a chain in the listener which can easily be configured using Studio. In the end, this provides a nice pattern for implementing integrity checks based on automation.
Make sure that none of the listeners running before your custom listener modify the current document, otherwise your pending changes will be lost when coming back on the form (a rollback would have been done).
Example
Create a custom listener (e.g. listening to About To Create event). This listener is going to trigger your Automation chain.
package org.nuxeo.sample; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.automation.AutomationService; import org.nuxeo.ecm.automation.OperationContext; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.RecoverableClientException; import org.nuxeo.ecm.core.api.event.DocumentEventTypes; import org.nuxeo.ecm.core.event.DeletedDocumentModel; import org.nuxeo.ecm.core.event.Event; import org.nuxeo.ecm.core.event.EventListener; import org.nuxeo.ecm.core.event.impl.DocumentEventContext; import org.nuxeo.ecm.platform.web.common.exceptionhandling.ExceptionHelper; import org.nuxeo.runtime.api.Framework; public class ListenerIntegrityCheck implements EventListener { private static final Log log = LogFactory.getLog(ListenerTest.class); public void handleEvent(Event event) throws ClientException { String eventName = event.getName(); if (!DocumentEventTypes.ABOUT_TO_CREATE.equals(eventName)) { return; } if (!(event.getContext() instanceof DocumentEventContext)) { return; } DocumentEventContext ctx = (DocumentEventContext) event.getContext(); DocumentModel doc = ctx.getSourceDocument(); // Skip shallow document if (doc instanceof DeletedDocumentModel) { return; } if (doc.isProxy() || doc.isVersion()) { return; } AutomationService service = Framework .getLocalService(AutomationService.class); OperationContext automationCtx = new OperationContext(); automationCtx.setInput(doc); try { service.run(automationCtx, "testValidation"); } catch (Exception e) { // The last validation operation throw an exception if there is at // least one invalidation Throwable unwrappedException = ExceptionHelper.unwrapException(e); if (unwrappedException instanceof RecoverableClientException) { event.markBubbleException(); event.markRollBack(); throw new ClientException(unwrappedException); } else { log.error("Error"); } } } }
Create your custom operation, which is going to be deployed in your Studio Automation chain.
package org.nuxeo.sample; import org.nuxeo.ecm.automation.OperationContext; import org.nuxeo.ecm.automation.core.Constants; import org.nuxeo.ecm.automation.core.annotations.Context; import org.nuxeo.ecm.automation.core.annotations.Operation; import org.nuxeo.ecm.automation.core.annotations.OperationMethod; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.DocumentModel; @Operation(id = OperationValidation1.ID, category = Constants.CAT_DOCUMENT, label = "OperationValidation1", description = "") public class OperationValidation1 { public static final String ID = "OperationValidation1"; @Context OperationContext ctx; @OperationMethod public DocumentModel run(DocumentModel input) throws ClientException { if (!input.getTitle().equals("title")) { String value = "- the document title is invalid - <br/>"; // Collecting different invalidation message in one context variable ctx.put("messageError", value); } return input; } }
Check one last validation rule and throw a CustomBubbleException in case of one validation at least has failed.
package org.nuxeo.sample; import org.nuxeo.ecm.automation.OperationContext; import org.nuxeo.ecm.automation.core.Constants; import org.nuxeo.ecm.automation.core.annotations.Context; import org.nuxeo.ecm.automation.core.annotations.Operation; import org.nuxeo.ecm.automation.core.annotations.OperationMethod; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.DocumentModel; @Operation(id = OperationValidation2.ID, category = Constants.CAT_DOCUMENT, label = "OperationValidation2", description = "") public class OperationValidation2 { public static final String ID = "OperationValidation2"; @Context OperationContext ctx; @OperationMethod public void run(DocumentModel input) throws ClientException { if (input.getId() == null) { // Collecting different invalidation message in one context variable Object messageError = ctx.get("messageError"); String error = "- the document ID is not compliant -"; ctx.put("messageError", (messageError != null ? messageError.toString() : "") + error); } // Retrieving all invalidation messages to throw in RecoverableException Object message = ctx.get("messageError"); if (message != null) { throw new CustomBubbleException("Process Invalidation", "Invalidations:<br/>" + message.toString(), null); } } }
Check also the custom exception.
package org.nuxeo.sample; import org.nuxeo.ecm.core.api.RecoverableClientException; public class CustomBubbleException extends RecoverableClientException { private static final long serialVersionUID = 1L; public CustomBubbleException(String message, String localizedMessage, String[] params) { super(message, localizedMessage, params); } public CustomBubbleException(String message, String localizedMessage, String[] params, Throwable cause) { super(message, localizedMessage, params, cause); } }
Finally, you can create in Studio your chain after exporting your custom operations.
Here is the result after trying to create a document: The end user sees error message displaying all collected invalidations:
JSF UI How-To Index Uploading Custom Operations in Nuxeo Studio