Addons

HOWTO: Bubble Errors from the Core Layer in the JSF UI

Updated: March 18, 2024

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

  1. 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.NuxeoException;
    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);
    
      @Override
    public void handleEvent(Event event) {
            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.getService(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 NuxeoException(unwrappedException);
                } else {
                    log.error("Error");
                }
            }
        }
    }
    
  2. 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.NuxeoException;
    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) {
            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;
        }
    }
    
    
  3. 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.NuxeoException;
    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) {
            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);
            }
        }
    }
    
  4. 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);
        }
    }
    
  5. 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: