Interface IPluginActionNodeHandler<TData extends BaseActionProps>

  • Type Parameters:
    TData - Type of the node's properties model, which must extend BaseActionProps. The properties themselves are stored as JSON and serialized to / deserialized from this type automatically. You should make sure this type can be serialized / deserialized via JSON. This means you should only basic field types such as number, booleans, or strings, or nested types. Furthermore, If you wish, you can also use the type JSONObject to skip serialization / deserialization and work with the raw JSON data.
    All Superinterfaces:
    IBaseActionClientHandlerNode<TData>, IBaseActionNode<TData>, IBeanValidatingElement<TData,​WorkflowNode>, IBeanValidatingNode<TData>, ICustomParametersUpdateable, IElementHandler<TData,​WorkflowNode>, IExecutingLikeActionNode<TData>, IExecutionResultDescriptor, IFCPlugin, IHierarchyValidatingNode<TData>, INamedUiElement, INameProviding, INodeHandler<TData>, IPluginGenericCustomGUI<IPluginWorkflowNodeBean>, IPluginWorkflowNode, IRefinedExecutionResultDescriptor<TData>, IResourceBundleLocator, ISingleBaseActionNodePrototype<TData>, ISingleElementPrototype<TData,​WorkflowNode>, ISingleNodePrototype<TData>, ITransferable, IWorkflowElementTypeProviding, IWorkflowNodeFlowAnalyzer<TData>, IWorkflowNodeTypeProviding, Serializable
    All Known Implementing Classes:
    APluginActionNodeHandler

    public interface IPluginActionNodeHandler<TData extends BaseActionProps>
    extends IPluginWorkflowNode, INodeHandler<TData>, IBaseActionNode<TData>, IExecutingLikeActionNode<TData>, IBaseActionClientHandlerNode<TData>, IBeanValidatingNode<TData>, ISingleBaseActionNodePrototype<TData>
    Mixin meant for IPluginWorkflowNode plugins that only wish to provide a workflow action that executes some business logic. The INodeHandler offers many methods that are irrelevant for this use case - this mixin implements most methods with the appropriate defaults.

    By default, you only need to implement execute(INodeExecutionParams) and getName(), as well as getPropertiesViewXhtmlName() for the UI. Make sure that the getName() is unique among all plugins. This name is also used as the getType() of the workflow node.

    The XHTML page with the UI for editing the action's property must be placed in the directory getPropertiesViewXhtmlPath(), with the file name getPropertiesViewXhtmlName(). You can also add properties files in getResourceBundlePath() with localized messages for the UI.

    Assume the getType() is MyActionPlugin. Then, if you do not override the defaults:

    • For each language you want to support, add a properties file in /src/main/resources/WEB-INF/properties/i18n_LOCALECODE.properties. The LOCALECODE is en for English, de for German, fr for French etc. These properties are then available in the XHTML page via the variable msg, see IElementHandler.getPropertiesViewXhtml()
    • Add an entry in the properties file for the keys MyActionPlugin.name, MyActionPlugin.desc, MyActionPlugin.label to customize the name of the plugin, the description of the plugin, and the name of the workflow action.

    You can also override most methods of this mixin to customize the action plugin. The following lists a few common use cases:

    To use a localized name or description
    Add an entry to your getResourceBundle(Locale). For the name of the plugin as it appears in the backend plugin configuration menu, use getI18nKeyDisplayName(). For the description that also appears in the backend plugin configuration menu, use getI18nKeyDescription(). For the label of the action as it appears in the workflow designer, use getI18nKeyActionLabel().
    To update the properties model for new versions of the plugin
    First make sure the MANIFEST.MF of your plugin contains the appropriate version information, see getVersion(). Then, override ICustomParametersUpdateable.updateCustomParams(de.xima.fc.interfaces.IUpdateCustomParametersParams). If you are using the SemVer (semantic version) format (i.e. your version looks like x.y.z), you may want to use the default methods provided by the mixin ISemverUpdating.
    To return a value form the exec method and have it displayed in the workflow designer
    Override getSuccessValueDescriptor(IValueDescriptorFactory) (which defines which values the exec method may return, and also contain a human-readable description for each) and adjust the return value of execute(INodeExecutionParams) accordingly.
    To provide pre-configured actions to the drawer panel to the left of the workflow designer
    Override INodeHandler.getNodePrototypes(de.xima.fc.interfaces.workflow.params.IGetNodePrototypesParams), optionally adding the list of prototypes (which contains just one item) returned by the super method implementation.
    To display the action in a different category in the workflow designer
    Override ISingleElementPrototype.getSubCategory(IGetElementPrototypesParams)
    To use a custom UI
    Create an XHTML page in getPropertiesViewXhtmlPath(); and override getPropertiesViewXhtmlName() with the name of the XHTML page. For example, if you name the XHTML page myCustomUi and do not override the XHTML path, add the file /src/main/resources/WEB-INF/ui/myCustomUi.xhtml. If you need a custom bean as a controller, also override getMainPluginBeanClass(). See IElementHandler.getPropertiesViewXhtml() for more details on the XHTML page.
    To use more than one bean
    Override getUnmanagedBeans(), but make sure to include getMainPluginBeanClass(). The additional beans can be used in XHTML pages, or referenced via Inject.
    To customize the serialization / deserialization process of the properties model
    Use the JSONField annotation on the fields of your properties model class, and/or override IElementHandler.getFastJsonConverter()

    Examples

    A simple skeletal implementation for an action that sends an HTTP post request might look like as follows. Note that you replace APluginActionNodeHandler with this interface IPluginActionNodeHandler if you need a custom super class (you only need to implement the getter getPluginInitializeData()).

     package com.example;
     import java.net.MalformedURLException;
     import java.net.URL;
     import de.xima.fc.exceptions.AbstractAbruptCompletionException;
     import de.xima.fc.interfaces.workflow.execution.INormalCompletionResult;
     import de.xima.fc.interfaces.workflow.params.INodeExecutionParams;
     import de.xima.fc.plugin.escalation.MyPostRequestPlugin.EMyPostRequestError;
     import de.xima.fc.plugin.escalation.MyPostRequestPlugin2.MyPostRequestProps;
     import de.xima.fc.workflow.mixin.APluginActionNodeHandler;
     import de.xima.fc.workflow.taglib.model.BaseActionProps;
     @SuppressWarnings("serial")
     public class MyPostRequestPlugin extends APluginActionNodeHandler<MyPostRequestProps> {
       public static class MyPostRequestProps extends BaseActionProps {
         private String url;
         public String getUrl() {
           return url;
         }
         public void setUrl(String url) {
           this.url = url;
         }
       }
       public Class<EMyPostRequestError> getErrorCodeClass() {
         return EMyPostRequestError.class;
       }
       @Override
       public String getName() {
         return "My Awesome Post Plugin";
       }
       @Override
       public INormalCompletionResult execute(INodeExecutionParams<MyPostRequestProps params) throws AbstractAbruptCompletionException {
         try {
           URL url = new URL(params.getData().getUrl());
           // todo: implement sending an HTTP post request
           // ...
         }
         catch (MalformedURLException e) {
           // Do not catch unexpected "Exception" - this is handled by the system.
           // Only catch exceptions you anticipate may occur.
           throw params.throwingException().cause(e).build();
         }
         return params.normalResult().build();
       }
     }
     

    A more advanced version with custom return values for the success and error case might look like this:

     package com.example;
     import java.net.MalformedURLException;
     import java.net.URL;
     import java.util.Map;
     import org.apache.commons.lang3.tuple.Pair;
     import de.xima.fc.exceptions.AbstractAbruptCompletionException;
     import de.xima.fc.interfaces.workflow.execution.INormalCompletionResult;
     import de.xima.fc.interfaces.workflow.params.INodeExecutionParams;
     import de.xima.fc.interfaces.workflow.value.IRecordValueDescriptor;
     import de.xima.fc.interfaces.workflow.value.IUnionValueDescriptor;
     import de.xima.fc.interfaces.workflow.value.IValueBuilder;
     import de.xima.fc.interfaces.workflow.value.IValueDescriptorFactory;
     import de.xima.fc.plugin.escalation.MyPostRequestPlugin.EMyPostRequestError;
     import de.xima.fc.plugin.escalation.MyPostRequestPlugin.MyPostRequestProps;
     import de.xima.fc.workflow.mixin.APluginActionNodeHandler;
     import de.xima.fc.workflow.taglib.model.BaseActionProps;
     @SuppressWarnings("serial")
     public class MyPostRequestPlugin extends APluginActionNodeHandler<MyPostRequestProps> {
       public static enum EMyPostRequestError {
         MALFORMED_URL,
       }
       public static class MyPostRequestProps extends BaseActionProps {
         private String url;
         public String getUrl() {
           return url;
         }
         public void setUrl(String url) {
           this.url = url;
         }
       }
       @Override
       public String getName() {
         return "My New Action Plugin";
       }
       @Override
       public INormalCompletionResult execute(INodeExecutionParams<MyPostRequestProps> params) throws AbstractAbruptCompletionException {
         MyPostRequestProps props = params.getData();
         try {
           // do something awesome
           Pair<Integer, String> result = sendPostRequest(props.getUrl());
           return params.normalResult().success(f -> f.asRecord() //
               .property("responseCode", result.getLeft()) //
               .property("responseMessage", result.getRight())) //
               .build();
         }
         catch (MalformedURLException e) {
           throw params.throwingException().error(EMyPostRequestError.MALFORMED_URL.name(), Map.class, b -> b.asRecord() //
               .property("message", e.getMessage()) //
               .property("invalidUrl", props.getUrl()) //
               .build() //
           ).build();
         }
         // Do not catch unexpected "Exception" - this is handled by the system.
         // Only catch exceptions you anticipate may occur.
       }
       private Pair<Integer, String> sendPostRequest(String urlString) throws MalformedURLException {
         URL url = new URL(urlString);
         // todo: implement sending an HTTP post request
         return Pair.of(404, "Not Found");
       }
       public Class<EMyPostRequestError> getErrorCodeClass() {
         return EMyPostRequestError.class;
       }
       @Override
       public IUnionValueDescriptor<String> getErrorValueDescriptor(IValueDescriptorFactory factory) {
         return factory.unionStringBuilder() //
             // When GENERAL error occurs, a string with the error message is returned
             .addAndUseAsDefault(EMyPostRequestError.GENERAL.name(), f -> f.string()) //
             // When the malformed URL error occurs, an JSON object with two properties is returned
             .add(EMyPostRequestError.MALFORMED_URL.name(), f -> f.recordBuilder() //
                 .requiredProperty("message", v -> v.string()) //
                 .requiredProperty("invalidUrl", v -> v.string()) //
                 .build()) //
             .build();
       }
       @Override
       public IRecordValueDescriptor getSuccessValueDescriptor(IValueDescriptorFactory factory) {
         return factory.recordBuilder() //
             .requiredProperty("responseCode", f -> f.integer(200)) //
             .requiredProperty("responseMessage", f -> f.string()) //
             .build();
       }
     }
     
    Since:
    7.0.0
    Author:
    XIMA MEDIA GmbH
    • Method Detail

      • getAlwaysValueDescriptor

        default IValueDescriptor<?,​? extends IValueBuilder<?>> getAlwaysValueDescriptor​(IValueDescriptorFactory factory)
        This default implementation returns a void value descriptor. This is appropriate when this plugin does not need to return any values. If you wish to return values (and make them available to other action via variables / placeholders), override this method. If you do, make sure you also modify execute(INodeExecutionParams) and supply the appropriate values upon execution.
        Specified by:
        getAlwaysValueDescriptor in interface IExecutionResultDescriptor
        Parameters:
        factory - Factory that may be used for creating the descriptor. You may also use ValueDescriptorFactory.getInstance() if you wish to cache the descriptor in a static or instance field.
        Returns:
        The value descriptor for that data that is made available to the workflow when the node is executed, regardless of whether is succeeded or failed.
        See Also:
        IExecutionResultDescriptor.getAlwaysValueDescriptor(IValueDescriptorFactory)
      • getDescription

        default String getDescription​(Locale locale)
        The default implementation returns the localized value of the getI18nKeyDescription() in the getResourceBundle(Locale). When no value exists for that key, returns an empty string (= no description).
        Specified by:
        getDescription in interface IFCPlugin
        Parameters:
        locale - The locale for which to get the description.
        Returns:
        An HTML markup string with the description for this plugin, or null if no description is available.
      • getErrorCodeClass

        default Class<? extends Enum<?>> getErrorCodeClass()
        Returns the type of the enumeration that defines the error types that may occur during the execution of a node. The default returns null, which corresponds to no custom errors. In that case, if the plugin throws an exception, it is treated as a general exception.
        Returns:
        The enumeration class that defines the possible error codes.
      • getSoftErrorCodeClass

        default Class<? extends Enum<?>> getSoftErrorCodeClass()
        Returns the type of the enumeration that defines the soft error types that may occur during the execution of a node. The default returns null, which corresponds to no custom soft errors. In that case the plugin cannot add any soft errors.
        Returns:
        The enumeration class that defines the possible soft error codes.
      • getErrorValueDescriptor

        default IUnionValueDescriptor<String> getErrorValueDescriptor​(IValueDescriptorFactory factory)
        This default implementation returns the error codes from the getSoftErrorCodeClass(), if any. No error data is made available for the error codes. When the action can have errors with error data, override this method and use IValueDescriptorFactory.unionStringBuilder() to add the errors.
         public IUnionValueDescriptor<String> getSoftErrorValueDescriptor(IValueDescriptorFactory f) {
           return f.recordBuilder() //
             .add("FILE_NOT_FOUND", f.recordBuilder().requiredProperty("file", f.string("")).build()) //
             .add("NETWORK_ERROR", f.recordBuilder().requiredProperty("url", f.string("")).build()) //
             .build();
         
        Note that it is recommended that you keep the available error codes in an enum, so you can reuse them later.

        In your execute(INodeExecutionParams) method, you can then add a soft error when necessary while building an error result:

         public INormalCompletionResult execute(INodeExecutionParams<TData> params) throws AbstractAbruptCompletionException {
           try {
             // Run business logic...
             return params.normalResult().success(result).build();
           }
           catch (final IOException e) {
             // Add the soft error
             final var errorData = new HashMap<String, Object>();
             errorData.put("file", fileName);
             throw throw params.throwingException().error("FILE_NOT_FOUND", errorData).cause(e).build();
           }
         }
         

        Instead of the factory passed in to this method, you may also use ValueDescriptorFactory.getInstance() if you wish to cache the descriptor in a static or instance field. *

        Specified by:
        getErrorValueDescriptor in interface IExecutionResultDescriptor
        Parameters:
        factory - Factory that may be used for creating the descriptor. You may also use ValueDescriptorFactory.getInstance() if you wish to cache the descriptor in a static or instance field.
        Returns:
        The value descriptor for the data that is made available to the workflow when the execution of the node failed (such as by throwing an exception).
        See Also:
        IExecutionResultDescriptor.getErrorValueDescriptor(IValueDescriptorFactory)
      • getI18nKeyActionLabel

        default String getI18nKeyActionLabel()
        Returns:
        The i18n key for the name of the action, as shown in the workflow designer. Defaults to the getI18nKeyDisplayName().
      • getI18nKeyActionSearchText

        default String getI18nKeyActionSearchText()
        Returns:
        The i18n key for the search text of the action, not shown in the UI but used for filtering. Defaults to "getName().searchtext".
      • getI18nKeyActionSubLabel

        default String getI18nKeyActionSubLabel()
        Returns:
        The i18n key for the sub label of the action, as shown in the workflow designer below the name of the item. Defaults to "getName().sublabel".
      • getI18nKeyActionTitle

        default String getI18nKeyActionTitle()
        Returns:
        The i18n key for the title of the action, as shown in the workflow designer when hovering over the item with the mouse. Defaults to "getName().title".
      • getI18nKeyDescription

        default String getI18nKeyDescription()
        Returns:
        The i18n key for the description of this plugin. Defaults to "getName().desc".
      • getI18nKeyDisplayName

        default String getI18nKeyDisplayName()
        Returns:
        The i18n key for the display name of this plugin, as shown in the plugin configuration backend menu. Defaults to "getName().name".
      • getNodeHandler

        default INodeHandler<TData> getNodeHandler()
        Specified by:
        getNodeHandler in interface IPluginWorkflowNode
        Returns:
        The node handler that implements all the logic required by the workflow node. This lets you customize all features available to built-in workflow nodes.
      • getPropertiesViewXhtmlName

        String getPropertiesViewXhtmlName()
        Returns:
        The file name (without the path) to the XHTML page with the custom UI. The file extension .xhtml is added automatically if the returned name does not contain it already.
        See Also:
        getPropertiesViewXhtmlPath()
      • getPropertiesViewXhtmlPath

        default String getPropertiesViewXhtmlPath()
        Returns:
        The path (without the file name) to the XHTML page with the custom UI. Defaults to WEB-INF/ui. That is, you should put the file in src/main/resources/WEB-INF/ui.
        See Also:
        getPropertiesViewXhtmlName()
      • getResourceBundlePath

        default String getResourceBundlePath()
        Returns:
        The path to the resource bundle with the localized message for this plugin. Defaults to WEB-INF/properties/i18n. That is, you should create the properties files src/main/resources/WEB-INF/properties/i18n_en.properties (for English), src/main/resources/WEB-INF/properties/i18n_de.properties (for German), etc.
      • getSoftErrorValueDescriptor

        default IUnionValueDescriptor<String> getSoftErrorValueDescriptor​(IValueDescriptorFactory factory)
        This default implementation returns the error codes from the getSoftErrorCodeClass(), if any. No error data is made available for the error codes. When the action can have soft errors with error data, override this method and use IValueDescriptorFactory.unionStringBuilder() to add the soft errors.
         public IUnionValueDescriptor<String> getSoftErrorValueDescriptor(IValueDescriptorFactory f) {
           return f.recordBuilder() //
             .add("FILE_NOT_FOUND", f.recordBuilder().requiredProperty("file", f.string("")).build()) //
             .add("NETWORK_ERROR", f.recordBuilder().requiredProperty("url", f.string("")).build()) //
             .build();
         
        Note that it is recommended that you keep the avaiable soft error codes in an enum, so you can reuse them later.

        In your execute(INodeExecutionParams) method, you can then add a soft error when necessary, which you can do like this:

         public INormalCompletionResult execute(INodeExecutionParams<TData> params) throws AbstractAbruptCompletionException {
           try {
             // Run business logic...
           }
           catch (final IOException e) {
             // Add the soft error
             final var errorData = new HashMap<String, Object>();
             errorData.put("file", fileName);
             params.softError("FILE_NOT_FOUND", e, b -> b.value(errorData))
           }
           return params.normalResult().success(result).build();
         }
         

        Instead of the factory passed in to this method, you may also use ValueDescriptorFactory.getInstance() if you wish to cache the descriptor in a static or instance field. *

        Specified by:
        getSoftErrorValueDescriptor in interface IExecutionResultDescriptor
        Parameters:
        factory - Factory that may be used for creating the descriptor. You may also use ValueDescriptorFactory.getInstance() if you wish to cache the descriptor in a static or instance field.
        Returns:
        The value descriptor for the data that is made available to the workflow when the execution of the node was successful, but contains one or more soft errors.
        See Also:
        IExecutionResultDescriptor.getSoftErrorValueDescriptor(IValueDescriptorFactory)
      • getSuccessValueDescriptor

        default IValueDescriptor<?,​? extends IValueBuilder<?>> getSuccessValueDescriptor​(IValueDescriptorFactory factory)
        This default implementation returns a void value descriptor. This is appropriate when this plugin does not need to return any values. If you wish to return values (and make them available to other action via variables / placeholders), override this method. If you do, make sure you also modify execute(INodeExecutionParams) and supply the appropriate values upon execution.

        Recommended best practice is to use a IRecordValueDescriptor, with an entry for each value that may be returned. This also allows you to add additional entries later on. For example, to indicate that your action may return a status code (default value 0) and a status message (default value <empty>), you can use:

         public IValueDescriptor<?, ? extends IValueBuilder<?>> getSuccessValueDescriptor(IValueDescriptorFactory f) {
           return f.recordBuilder() //
             .requiredProperty("statusCode", f.integer(0L)) //
             .requiredProperty("statusMessage", f.string("")) //
             .build();
         

        In your execute(INodeExecutionParams) method, you then need to supply a value for the status code and message, which you can do like this:

         public INormalCompletionResult execute(INodeExecutionParams<TData> params) throws AbstractAbruptCompletionException {
           // Run business logic and get status code and message
           final Map<String, Object> result = new HashMap<>();
           result.put("statusCode", 200);
           result.put("statusMessage", "OK");
           // Create a success result with the data
           return params.normalResult().success(result).build();
         }
         

        Instead of the factory passed in to this method, you may also use ValueDescriptorFactory.getInstance() if you wish to cache the descriptor in a static or instance field. *

        Specified by:
        getSuccessValueDescriptor in interface IExecutionResultDescriptor
        Parameters:
        factory - Factory that may be used for creating the descriptor. Instead of the factory passed in to this method, you may also use ValueDescriptorFactory.getInstance() if you wish to cache the descriptor in a static or instance field.
        Returns:
        The data provided by the node after having been executed successfully.
        See Also:
        IExecutionResultDescriptor.getSuccessValueDescriptor(IValueDescriptorFactory)
      • getType

        default String getType()
        This default implementation simply returns the getName() of the plugin as the type. Make sure that the name is unique. It is recommended you hardcode the name / type, as any change will result in broken backwards compatibility. In particular we advice against using Class.getCanonicalName() etc., as a refactoring would then break your plugin.
        Specified by:
        getType in interface IWorkflowElementTypeProviding
        Returns:
        The type of the workflow element that determines how the workflow element behaves. Usually there is a registered handler for each type.
      • getUnmanagedBeans

        default Iterable<Class<? extends IPluginWorkflowNodeBean>> getUnmanagedBeans()
        Description copied from interface: IPluginGenericCustomGUI
        This must return a list of backing bean classes that control the user interface and are required by the XHTML Facelet view. A new instance of the bean will be created automatically when the view is opened. Make sure each bean has got a no-argument constructor or instantiation will fail.

        Each bean should be annotated with Named. If this annotation is not present or no name is specified, the name defaults to the simple name of the bean class, with the first character changed to lower case.

        Also, each bean needs to be annotated one of the following scopes: @RequestScoped, @ViewScoped, @SessionScoped, or @ApplicationScoped. Note that it depends on the type of plugin which scopes are actually supported (most plugins expect @ViewScoped beans). In case you do not specify a scope, an appropriate scope will be determined automatically.

        Please note that the beans are fundamentally unmanaged - functionality specific to managed CDI beans may not be supported, depending on the type of plugin. Certain features of CDI managed beans may be supported partially, depending on the type of plugin, but may work slightly differently. This includes, but is not limited to:

        • The exact timing at which PostConstruct and PreDestroy are called may differ.
        • A field marked with Inject may not work with all values allowed by the CDI specification, and may not perform certain validation passes such as the check for circular dependencies. Also, no new bean instances will be created when those beans have not yet been created as part of the current page.
        Specified by:
        getUnmanagedBeans in interface IPluginGenericCustomGUI<TData extends BaseActionProps>
        Returns:
        A list of unmanaged bean classes required by the Facelet page.
      • getVersion

        default String getVersion()
        Instead of the formcycle version, this default implementation returns the current version of this plugin. The version is read from the MANIFEST.MF file in the plugin JAR file. Make sure that the manifest contains an appropriate version entry, e.g.:
         Implementation-Version = 3.1.4
         
        We recommend that you use version numbers that conform to the format and semantics as laid out by SemVer (semantic version).
        Specified by:
        getVersion in interface IElementHandler<TData extends BaseActionProps,​WorkflowNode>
        Returns:
        The version of this plugin.
        See Also:
        IElementHandler.getVersion()