Nuxeo Server

Contributing an Operation

Updated: March 18, 2024

This page provides all the information necessary for implementing an operation and is a must-read for getting a good comprehension of the framework. You should also have a look at Develop with Nuxeo Platform which explain how to use Nuxeo CLI in order to create new operations quickly and easily through the provided wizard.

You can use the Codenvy factory that we have setup which provides you with a ready-to-build sample operation and unit test. Just click on Project > Build & Publish to build a JAR from your operation. You can deploy your first operation, "SampleOperation", on Nuxeo server in Codenvy by clicking on the green arrow on the top left panel.

Check out Nuxeo CLI in order to bootstrap your Operation.

Implementing an Operation

To implement an operation, you need a Java class with the @Operation annotation. An operation class should also provide at least one method to be invoked by the automation service when executing the operation. Mark the method as executable by annotating with @OperationMethod.

You can have multiple executable methods; one method for each type of input/output object supported by an operation. The correct operation method will automatically be selected if the method argument matches the current input object and the return type matches the input required by the next operation in an automation chain.

The @OperationMethod annotation also provides an optional priority attribute that can be used to specify which method is preferred over the other matching methods. This situation (having multiple methods that match an execution) can happen if the input and output types are not strictly matched. For example if the input of a method is a DocumentModel object and the input of another method is a DocumentRef object then both methods have the same input signature for the automation framework because DocumentModel and DocumentRef are objects of the same kind - they represent a Nuxeo Document. When you need to treat different Java objects as the same type of input (or output) you must create a type adapter (see the interface TypeAdapter) which knows how to convert a given object to another type. Without type adapters, treating different Java objects as the same type of object is not possible.

Operations can include parameterizable variables so that when a user defines an operation chain, they can define values that will be injected in the operation parameters. To declare parameters you must use the @Param annotation.

There is one more annotation provided by the automation service: the @Context annotation. This annotation can be used to inject execution context objects or Nuxeo Service instances into a running operation.

When registering an automation chain, the chain will be checked to find a path from the first operation to the last one to be sure the chain can be executed at runtime. This means identifying at least one method in each operation that matches the signature of the next operation. If such a path cannot be found, an error is thrown (at registration time). For more details on registering an operation chain, see Automation Chain.

To register your operation, create a Nuxeo XML extension and contribute it to the operations extension point. Example:

<extension target="org.nuxeo.ecm.core.operation.OperationServiceComponent" point="operations">
  <operation class="org.nuxeo.example.TestOperation" />
</extension>

where org.nuxeo.example.TestOperation is the class name of your operation (the one annotated with @Operation).

Let's look at the following operation class to see how annotations are used:

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.automation.core.annotations.Param;
import org.nuxeo.ecm.automation.core.util.DocumentHelper;
import org.nuxeo.ecm.automation.core.util.Properties;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.DocumentRef;
import org.nuxeo.ecm.core.api.DocumentRefList;
import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;

@Operation(id = CreateDocument.ID, category = Constants.CAT_DOCUMENT, label = "Create", description = "Create a new document in the input folder ...")
public class CreateDocument {

    public final static String ID = "Document.Create";

    @Context
    protected CoreSession session;

    @Param(name = "type")
    protected String type;

    @Param(name = "name", required = false)
    protected String name;

    @Param(name = "properties", required = false)
    protected Properties content;

    @OperationMethod
    public DocumentModel run(DocumentModel doc) throws Exception {
        if (name == null) {
            name = "Untitled";
        }
        DocumentModel newDoc = session.createDocumentModel(
                doc.getPathAsString(), name, type);
        if (content != null) {
            DocumentHelper.setProperties(session, newDoc, content);
        }
        return session.createDocument(newDoc);
    }

    @OperationMethod
    public DocumentModelList run(DocumentModelList docs) throws Exception {
        DocumentModelListImpl result = new DocumentModelListImpl(
                (int) docs.totalSize());
        for (DocumentModel doc : docs) {
            result.add(run(doc));
        }
        return result;
    }

    @OperationMethod
    public DocumentModelList run(DocumentRefList docs) throws Exception {
        DocumentModelListImpl result = new DocumentModelListImpl(
                (int) docs.totalSize());
        for (DocumentRef doc : docs) {
            result.add(run(session.getDocument(doc)));
        }
        return result;
    }
}

You can see how @Context is used to inject the current CoreSession instance into the session member. It is recommended to use this technique to acquire a CoreSession instead of creating a new session. This way you reuse the same CoreSession used by all the other operations in the chain. You don't need to worry about closing the session — the automation service will close the session for you, when needed.

You can also use @Context to inject any Nuxeo Service or the instance of the OperationContext object that represents the current execution context and which holds the execution state, like the last input, the context parameters, the core session, the current principal etc.

The attributes of the @Operation annotation are required by operation chain creation tools, like the one in Nuxeo Studio, to be able to generate the list of existing operations and some additional operation information, such as its name, a short description on how the operation is working etc. For a complete description of these attributes, look into the annotation Javadoc.

You can see that the operation above provides three operation methods with different signatures:

  • One that takes a Document and returns a Document object
  • One that takes a list of document objects and returns a list of documents
  • One that takes a list of document references and returns a list of documents

Depending on what the input object is when calling this operation, only one of these methods will be used to do the processing. You'll notice that there is no method taking a document reference. This is because the document reference is automatically adapted into a DocumentModel object when needed, thanks to a dedicated TypeAdapter.

The initial input of an operation (or operation chain) execution is provided by the caller (the one that creates the execution context). The Nuxeo Platform provides several execution contexts:

  • A core event listener that executes operations in response to core events
  • An action bean that executes operations in response to the user actions (through the Nuxeo Web Interface)
  • A JAX-RS resource which executes operations in response to REST calls
  • A special listener fired by the workflow service to execute an operation chain

Each of these execution contexts provide the initial input for the chain (or operation) to be executed. For example, the core event listener will use the document that is the source of the event as the initial input. The action bean executor will use the document currently opened in the User Interface.

If no input exists, then null will be used as the input. In this case, the first operation in the chain must be a void operation. If you need, you can create your own operation executor. Just look into the existing code for examples (e.g. org.nuxeo.ecm.automation.jsf.OperationActionBean).

The code needed to invoke an operation or an operation chain is pretty simple. You need something like this:

CoreSession session = fetchCoreSession();
AutomationService automation = Framework.getService(AutomationService.class);
OperationContext ctx = new OperationContext(session);
ctx.setInput(navigationContext.getCurrentDocument());
try {
  Object result = automation.run(ctx, "the_chain_name");
  // ... do something with the result
} catch (Throwable t) {
  // handle errors
}

It gets a little more complicated when you need to set the operation parameters.

Let's take another look at the operation class defined above. You can see that operation parameters are declared as class fields using the @Param annotation.

This annotation has several attributes, such as a parameter name, a required flag, a default value if any, a widget type to be used by UI operation chain builders like Nuxeo Studio, etc.

The parameter name is important since it is the key you use when defining an operation chain to refer to a specific operation parameter. If the parameter is required then its value must be specified in the operation chain definition otherwise an exception is thrown at runtime. The other parameters are only useful for UI tools that introspect the operations. For example, when building an operation chain in Nuxeo Studio, you need to render each operation parameter using a widget. The default is to use a TextBox if the parameter is a String, a CheckBox if the parameter is a boolean, a ListBox for lists etc. In some situations you may want to override this default mapping. For example, you may want to use a TextArea instead of a TextBox for a string parameter. In that case, you can use the widget attribute to specify your desired widget.

Parameter Injection

Executing an operation is done as follows:

  1. A new operation instance is created (operations are stateless).
  2. The context objects are injected if any @Context annotation is present.
  3. Corresponding parameters specified by the execution context are injected into the fields, annotated with @Param, and identified using the name attribute of the annotation.
  4. The method matching the execution input and output types is invoked by passing the current input as an argument. (Before invoking the method, the input is adapted if any TypeAdapter was registered for the input type).

Let's look at how parameters are injected into instance fields.

First, the field is identified by using the parameter name. Then, the value to be injected is checked to see if the value type matches the field type. If they don't match, the registered TypeAdapters are consulted for an adapter that knows how to adapt the value type into the field type. If no adapter is found, then an exception is thrown, otherwise the value is adapted and injected into the parameter. An important case is when EL expressions are used as values. In this case, the expression is evaluated and the result will be used as the value to be injected (and the algorithm of type matching described above is applied to the value returned by the expression).

This means you can use string values for almost all field types because a string adapter exists for almost all parameter types used by operations.

Here's a list of the more commonly used parameter types and the string representation for each (the string representation is important since you should use it when defining operation chains through Nuxeo XML extensions):

Type Java Type Known Adapters String Representation / Example
document org.nuxeo.ecm.core.api.DocumentModel String, DocumentRef Document UID or document absolute path. For instance 96bfb9cb-a13d-48a2-9bbd-9341fcf24801 or /default-domain/workspaces/myws
documents org.nuxeo.ecm.core.api.DocumentModelList DocumentRefList, DocumentModel, DocumentRef No String representation exists. Cannot be used as a parameter value in an XML chain descriptor. You should use EL expressions instead.
blob org.nuxeo.ecm.core.api.Blob No String representation exists. Cannot be used as a parameter value in an XML chain descriptor. You should use EL expressions instead.
blobs org.nuxeo.ecm.automation.core.util.BlobList No String representation exists. Cannot be used as a parameter value in an XML chain descriptor. You should use EL expressions instead.
properties org.nuxeo.ecm.automation.core.util.Properties String A list of key value pairs in Java properties file format.
resource java.net.URL String
script org.nuxeo.ecm.automation.core.scripting.Expression Use the expr: prefix before your EL expression.
expr: Document.title
For the complete list of scripting objects and functions see Use of MVEL in Automation Chains.
date java.util.Date String, java.util.Calendar W3C date format
integer java.lang.Long or the long primitive type Natural string representation
float java.lang.Double or the double primitive type Natural string representation
boolean java.lang.Boolean or the boolean primitive type Natural string representation
string java.lang.String Already a string
stringlist org.nuxeo.ecm.automation.core.util.StringList String Comma separated list of strings
foo, bar
The comma separator can be escaped with the \ character.

Of course, when defining the parameter values that will be injected into an operation you can either specify static values (as hard coded strings) or an EL expression to compute the actual values at runtime.

Void Operation Methods

Sometimes operations may not require any input. In this case the operation should use a method with no parameters. Such methods will match any input, so it is not recommended to use more than one void method in the same operation, as you cannot know which method will be selected for execution.

For example, the Log operation does not requires an input, since it is only writing in the log:

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.automation.core.Constants;
import org.nuxeo.ecm.automation.core.annotations.Operation;
import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
import org.nuxeo.ecm.automation.core.annotations.Param;

@Operation(id = LogOperation.ID, category = Constants.CAT_NOTIFICATION, label = "Log", description = "Logging with log4j", aliases = { "LogOperation" })
public class LogOperation {

    public static final String ID = "Log";

    @Param(name = "category", required = false)
    protected String category;

    @Param(name = "message", required = true)
    protected String message;

    @Param(name = "level", required = true, widget = Constants.W_OPTION, values = { "info", "debug", "warn", "error" })
    protected String level = "info";

    @OperationMethod
    public void run() {
        if (category == null) {
            category = "org.nuxeo.ecm.automation.logger";
        }

        Log log = LogFactory.getLog(category);

        if ("debug".equals(level)) {
            log.debug(message);
            return;
        }

        if ("warn".equals(level)) {
            log.warn(message);
            return;
        }

        if ("error".equals(level)) {
            log.error(message);
            return;
        }
        // in any other case, use info log level
        log.info(message);
    }
}

Also there are rare cases when you don't want to return anything from an operation. In that case the operation method must use the void Java keyword and the result of the operation will be the null Java object (in the previous example, the Log operation returns no value).

Aliases

In Automation, you can add aliases for each operation (and create a chain defining itself with operations aliases):

package org.nuxeo.ecm.automation.core.test;

import org.nuxeo.ecm.automation.core.annotations.Operation;
import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
import org.nuxeo.ecm.automation.core.annotations.Param;

@Operation(id = ParamNameWithAliasOperation.ID, aliases = { "aliasOp1",
        "aliasOp2" })
public class ParamNameWithAliasOperation {

    public static final String ID = "OperationWithParamNameAlias";

    @OperationMethod
    public String run() {
        return param;
    }

}

And operation parameter aliases as:

import org.nuxeo.ecm.automation.core.annotations.Operation;
import org.nuxeo.ecm.automation.core.annotations.OperationMethod;
import org.nuxeo.ecm.automation.core.annotations.Param;

@Operation(id = ParamNameWithAliasOperation.ID)
public class ParamNameWithAliasOperation {

    public static final String ID = "OperationWithParamNameAlias";

    public static final String ALIAS1 = "alias1";

    public static final String ALIAS2 = "alias2";

    @Param(name = "paramName", alias = { ALIAS1, ALIAS2 })
    protected String param;

    @OperationMethod
    public String run() {
        return param;
    }

}