Nuxeo Server

Contributing an Operation

Updated: March 18, 2024

This page gives 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 some of our tutorials that show how to use Nuxeo CLI to create new operations easily and quickly, using the provided wizard.

You can also use the Codenvy factory that we have setup and that offers you a ready-to-build sample operation, with its unitary test. Just click on Project > Build & Publish so as to get a JAR of your operation. You can deploy your first operation "SampleOperation" into Nuxeo server in Codenvy by clicking on the green arrow on panel left top.

Check Nuxeo CLI to bootstrap your Operation

Implementing an Operation

In order to implement an operation you need to create a Java class annotated with @Operation. An operation class should also provide at least one method that will be invoked by the automation service when executing the operation. To mark a method as executable you must annotate it using @OperationMethod.

You can have multiple executable methods - one method for each type of input/output objects supported by an operation. The right method will be selected at runtime depending on the type of the input object (and of the type of the required input of the next operation when in an operation chain).

So, an operation method will be selected if the method argument matches the current input object and the return type matches the input required by the next operation if in an operation chain.

The @OperationMethod annotation is also providing an optional priority attribute that can be used to specify which method is preferred over the other matching methods. This situation (having multiple method that matches an execution) can happen because 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 org.nuxeo.ecm.automation.TypeAdapter) that 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.

Also operations can provide parametrizable variables so that when a user defined an operation chain, they can define values that will be injected in the operation parameters. To declare parameters you must use the @Param annotation.

Apart from these annotations 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. Finding a path means to identify at least one method in each operation that is matching the signature of the next operation. If such a path could not be found an error is thrown (at registration time). For more detail on registering an operation chains see Automation Chain.

To register your operation you should create a Nuxeo XML extension 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 were 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 use @Context also to inject any Nuxeo Service or the instance of the OperationContext object that represents the current execution context and that 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 - like 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 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 can 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 are providing the initial input for the chain (or operation) to be executed. For example the core event listener will use as the initial input the document that is the source of the event. 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 that 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 to do 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
  }

To invoke operations is a little more complicated since you also need to set the operation parameters.

Let's look again 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 like 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 useful only 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. But 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 following:

  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 using @Param and identified using the name attribute of the annotation.
  4. The method matching the execution input and output types is invoked by passing as argument the current input. (Before invoking the method the input is adapted if any TypeAdapter was registered for the input type).

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

So, 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 with 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 that case (if the value is an expression) then 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 on the value returned by the expression).

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

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

  • document. Java type: org.nuxeo.ecm.core.api.DocumentModel
    Known adapters: From string, from DocumentRef
    String representation: The document UID or the document absolute path. Example: "96bfb9cb-a13d-48a2-9bbd-9341fcf24801", "/default-domain/workspaces/myws" etc.

  • documents. Java type: org.nuxeo.ecm.core.api.DocumentModelList
    Known adapters: From DocumentRefList, from DocumentModel, from DocumentRef
    No String representation exists. Cannot be used as a parameter value in an XML chain descriptor. You should use EL expressions instead.

  • blob. Java type: 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. Java type: 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. Java type: org.nuxeo.ecm.automation.core.util.Properties
    Known adapters: From string.
    String representation: A list of key value pairs in Java properties file format.

  • resource. Java type: java.net.URL
    Known adapters: From string.

  • script. Java type: org.nuxeo.ecm.automation.core.scripting.Expression
    String representation: Use the "expr:" prefix before your EL expression.
    Example: "expr: Document.title"
    For the complete list of scripting objects and functions see Use of MVEL in Automation Chains.

  • date. Java type: java.util.Date.
    Known type adapters: From string and from java.util.Calendar
    String representation: W3C date format.

  • integer. Java type: java.lang.Long or the long primitive type.
    Natural string representation.

  • float. Java type: java.lang.Double or the double primitive type.
    Natural string representation.

  • boolean. Java type: java.lang.Boolean or the boolean primitive type.
    Natural string representation.

  • string. Java type: java.lang.String
    Already a string.

  • stringlist. Java Type: org.nuxeo.ecm.automation.core.util.StringList
    Known adapters: From string
    String representation: Comma separated list of strings. Example: "foo, bar".

    Starting from 8.10-HF03, 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 that case the operation should use a method with no parameters. Such methods will match any input - thus it is not indicated to use two void methods in the same operation - since 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 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;
    }

}