Actions

  Written by The Jahia Team
   Estimated reading time:
Note: Actions are the legacy method for creating custom business logic in Jahia. Jahia now recommends using GraphQL because it is easier to integrate with modern web applications.

End-users and other systems can interact with your content by using actions. Jahia includes default actions that allow many types of interactions, like adding a comment on any node, starting workflows, sending form submission by mail, registering new users, and more. Actions are activated on HTTP POST requests to comply with the REST methodology. 

This topic describes actions, shows how to use actions to validate forms, and lists default actions.

About actions

An Action is a simple class extending the org.jahia.bin.Action abstract class, which is responsible of handling permissions and other restrictions. You need to override the method doExecute to provide your own Action.

public abstract ActionResult doExecute(HttpServletRequest req, RenderContext renderContext, Resource resource,
                                       JCRSessionWrapper session, Map<String, List<String>> parameters,
                                       URLResolver urlResolver)
        throws Exception;

You can do anything you want in an action but keep in mind that the shortest time your action takes to execute is the better end-user experience.

As said previously, Actions are called through HTTP POST methods but, as a result of an action, you can let the user stay on its current web page or redirect to another web page. The result can be a JSON serialized object. This allows to easily interact with framework like JQuery. To describe this behavior, the method doExecute returns a org.jahia.bin.ActionResult object. It contains a HTTP response code and the eventual redirect URL and org.json.JSONObject instance.

Example based on the RateContent Action

@Component(service = RateContent.class)
public class RateContent extends Action {
    JCRTemplate jcrTemplate;
    @Activate
    public void activate() {
        setName("rate");
    }
    @Reference
    public void setJcrTemplate(JCRTemplate jcrTemplate) {
        this.jcrTemplate = jcrTemplate;
    }
    @Override
    public ActionResult doExecute(HttpServletRequest req, RenderContext renderContext, final Resource resource, JCRSessionWrapper session, final Map<String, List<String>> parameters, URLResolver urlResolver) throws Exception {
        return (ActionResult) jcrTemplate.doExecuteWithSystemSession(null,session.getWorkspace().getName(),session.getLocale(),new JCRCallback<Object>() {
            public Object doInJCR(JCRSessionWrapper session) throws RepositoryException {
                JCRNodeWrapper node = session.getNodeByUUID(resource.getNode().getIdentifier());
                if (!node.isNodeType("jmix:rating")) {
                    session.checkout(node);
                    node.addMixin("jmix:rating");
                    session.save();
                }
                List<String> values = parameters.get("j:lastVote");
                node.setProperty("j:lastVote", values.get(0));
                node.setProperty("j:nbOfVotes",node.getProperty("j:nbOfVotes").getLong()+1);
                node.setProperty("j:sumOfVotes",node.getProperty("j:sumOfVotes").getLong()+values.get(0));
                session.save();
                try {
                    return new ActionResult(HttpServletResponse.SC_OK, node.getPath(), Render.serializeNodeToJSON(node));
                } catch (IOException e) {
                    e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
                } catch (JSONException e) {
                    e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates.
                }
                return null;
            }
        });
    }
}

The org.jahia.bin.Render.serializeNodeToJSON method is a utility function that returns a JSONObject representing a node.

Calling an action from a form

Calling an action is simple as calling node/path.actionName.do. In our example for the RateContent Action, here is an extract of a jQuery function in rateable.jsp:

$.post("<c:url value='${url.base}${bindedComponent.path}'/>.rate.do", {'j:lastVote': value,'jcrMethodToCall':"post",
    'jcrCookieName':"rated${bindedComponent.identifier}",
    'jcrCookieValue':"${currentNode.identifier}"}, function(
        result) {
    <%-- Select stars from "Average rating" control to match the returned average rating value --%>
    $("#avg${id}").stars("select", Math.round(result.j_sumOfVotes / result.j_nbOfVotes));
    <%-- Update other text controls... --%>
    $("#all_votes${id}").text(result.j_nbOfVotes);
    $("#all_avg${id}").text(('' + result.j_sumOfVotes / result.j_nbOfVotes).substring(0, 3));
    <%-- Display confirmation message to the user --%>
    $("#messages${id}").html("<br/><fmt:message key="label.ratingSaved"/> (" + value + "). <fmt:message key="label.thanks"/>!").stop().css("opacity", 1).fadeIn(30);
    <%-- Hide confirmation message and enable stars for "Rate this" control, after 2 sec... --%>
    setTimeout(function() {
        $("#messages${id}").fadeOut(1000, function() {
//            ui.enable();
        });
    }, 2000);
}, "json");

Here we see that by posting on the "<c:url value='$url.base$bindedComponent.path'/>.rate.do URL, we are calling the RateContent Action on the bound component.

In rating.hidden.plusone_minorone_form.jsp, you can see another example of calling our Action, this time from a form:

<c:set var="cookieName" value="rated${currentNode.identifier}"/>
<c:if test="${renderContext.loggedIn and (empty cookie[cookieName])}">
    <form action="<c:url value='${url.base}${currentNode.path}'/>.rate.do" method="post"
          id="jahia-forum-post-vote-${currentNode.identifier}">
        <input type="hidden" name="jcrRedirectTo" value="<c:url value='${url.base}${renderContext.mainResource.node.path}'/>"/>
            <%-- Define the output format for the newly created node by default html or by redirectTo--%>
        <input type="hidden" name="jcrNewNodeOutputFormat" value="html"/>
        <input type="hidden" name="jcrMethodToCall" value="post"/>
        <input type="hidden" name="j:lastVote" value="1"/>
        <input type="hidden" name="jcrCookieValue" value="${currentNode.identifier}"/>
        <input type="hidden" name="jcrCookieName" value="${cookieName}"/>
        <input type="hidden" name="jcrReturnContentType" value="html"/>
    </form>
</c:if>

Form parameters and file uploading

In an Action, you will receive your form parameters inside the parameters map. Your form submission will pass through several components before reaching your Action. Those components are responsible for managing the security (captcha validation, permissions check) and the uploading of files. If your action needs to manage files, the file will be uploaded in temporary folder so you will only have to choose where to save it in the JCR or elsewhere.
An example of getting an uploaded file in your action and handling it in there :

final FileUpload fu = (FileUpload) req.getAttribute(FileUpload.FILEUPLOAD_ATTRIBUTE);
DiskFileItem inputFile = fu.getFileItems().get("fileField");

This example is from the DocumentConverterAction class.

Chaining actions in one request

Jahia provides a ChainAction class that allows to chain actions called. You will have to pass a chainOfAction parameter containing the list of action to execute - all actions will be executed sequentially with the same parameters, and the the result will be one of the last actions.

For example, the following form will call mail and redirect actions :

<form action="mynode.chain.do" method="post">
    <input type="hidden" name="chainOfAction" value="mail,redirect"/>
    ...
</form>

Interacting with other Jahia subsystems

Your action can also interact or be called by other Jahia subsystems like rules or GWT UI; for this your action must also implement the <BackgroundAction> interface:

public class SendAsNewsletterAction extends Action implements BackgroundAction {
    public void executeBackgroundAction(JCRNodeWrapper node) {
        // do local post on node.getPath/sendAsNewsletter.do
        try {
            Map<String,String> headers = new HashMap<String,String>();
            headers.put("jahiatoken",TokenAuthValveImpl.addToken(node.getSession().getUser()));
            String out = httpClientService.executePost("http://localhost:8080"+
                    Jahia.getContextPath() + Render.getRenderServletPath() + "/live/"
                    + node.getResolveSite().getDefaultLanguage() + node.getPath()
                    + ".sendAsNewsletter.do", null, headers);
            logger.info(out);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }
}

Example of usage in rules:

rule "Schedule as a newsletter" 
    when
        A property j:scheduled has been set on a node
            - the node has the type jnt:newsletterIssue
    then
        Log "Scheduling newsletter " + node.getPath() + " at " + propertyValueAsString
        Execute the action "sendAsNewsletter" at j:scheduled on the node
end

Using actions to validate forms

Form validation works in two phases, first you have to do some javascript validation on client side. Then Jahia will validate the captcha for you if present, and then your Action could do any validation needed.

Here the code of a typical form, from the add comment example

<template:addResources type="javascript" resources="jquery.js,jquery.validate.js"/>
<template:addResources type="inlinejavascript">
    <script type="text/javascript">
        $(document).ready(function() {
            $("#newCommentForm").validate({
                rules: {
                    'jcr:title': "required",
                    <c:if test="${not renderContext.loggedIn}">
                    pseudo: "required",
                    captcha: "required"
                    </c:if>
                }
            });
        });
    </script>
</template:addResources>

<template:tokenizedForm>
    <form action="<c:url value='${url.base}${boundComponent.path}.addComment.do'/>" method="post" id="newCommentForm">
        <input type="hidden" name="jcrNodeType" value="jnt:post"/>
        <input type="hidden" name="jcrRedirectTo" value="<c:url value='${url.base}${renderContext.mainResource.node.path}'/>"/>
        <input type="hidden" name="jcrNewNodeOutputFormat" value="html"/>
        <input type="hidden" name="jcrResourceID" value="${currentNode.identifier}"/>
        <div id="formGenericComment">
            <fieldset>
                <c:if test="${not renderContext.loggedIn}">
                    <p class="field">
                        <label for="comment_pseudo"><fmt:message key="comment.pseudo"/></label>
                        <input value="${sessionScope.formDatas['pseudo'][0]}"
                               type="text" size="35" name="pseudo" id="comment_pseudo"
                               tabindex="1"/>
                    </p>
                </c:if>
                <p class="field">
                    <label class="left" for="comment-title"><fmt:message key="comment.title"/></label>
                    <input class="" value="${sessionScope.formDatas['jcr:title'][0]}"
                           type="text" size="35" id="comment-title" name="jcr:title"
                           tabindex="1"/>
                </p>
                <p class="field">
                    <label class="left" for="jahia-comment-${boundComponent.identifier}"><fmt:message
                            key="comment.body"/></label>
                    <textarea rows="7" cols="35" id="jahia-comment-${boundComponent.identifier}"
                              name="content"
                              tabindex="2"><c:if
                            test="${not empty sessionScope.formDatas['content']}">${fn:escapeXml(sessionScope.formDatas['content'][0])}</c:if></textarea>
                </p>
                <c:if test="${not renderContext.loggedIn}">
                    <p class="field">
                        <label class="left" for="captcha"><template:captcha/></label>
                        <input type="text" id="captcha" name="jcrCaptcha"/>
                    </p>
                </c:if>
                <p>
                    <input type="reset" value="<fmt:message key='label.reset'/>" class="button"
                           tabindex="3" ${disabled}/>

                    <input type="submit" value="<fmt:message key='label.submit'/>" class="button"
                           tabindex="4" ${disabled}/>
                </p>
            </fieldset>
        </div>
    </form>
</template:tokenizedForm>

First of all you design your form classically

<form action="<c:url value='${url.base}${boundComponent.path}.addComment.do'/>" method="post" id="newCommentForm">
    <div id="formGenericComment">
        <fieldset>
            <c:if test="${not renderContext.loggedIn}">
                <p class="field">
                    <label for="comment_pseudo"><fmt:message key="comment.pseudo"/></label>
                    <input value=""
                           type="text" size="35" name="pseudo" id="comment_pseudo"
                           tabindex="1"/>
                </p>
            </c:if>
            <p class="field">
                <label class="left" for="comment-title"><fmt:message key="comment.title"/></label>
                <input class="" value=""
                       type="text" size="35" id="comment-title" name="jcr:title"
                       tabindex="1"/>
            </p>
            <p class="field">
                <label class="left" for="jahia-comment-${boundComponent.identifier}"><fmt:message
                        key="comment.body"/></label>
                <textarea rows="7" cols="35" id="jahia-comment-${boundComponent.identifier}"
                          name="content"
                          tabindex="2"></textarea>
            </p>
            <p>
                <input type="reset" value="<fmt:message key='label.reset'/>" class="button"
                       tabindex="3"/>

                <input type="submit" value="<fmt:message key='label.submit'/>" class="button"
                       tabindex="4"/>
            </p>
        </fieldset>
    </div>
</form>

Then you add specific hidden fields for Jahia :

<input type="hidden" name="jcrNodeType" value="jnt:post"/>
<input type="hidden" name="jcrRedirectTo" value="<c:url value='${url.base}${renderContext.mainResource.node.path}'/>"/>
<input type="hidden" name="jcrNewNodeOutputFormat" value="html"/>
<input type="hidden" name="jcrResourceID" value="${currentNode.identifier}"/>

Here the AddComment action expects some specific control parameters :

  • <nodeType> defines the type of node this form will create on submission
  • <redirectTo> defines to which node we want to redirect (you can also specify the template here), here we redirect to the current page
  • <newNodeOutputFormat> specify in which format the results will be displayed here <html> (can be ommited as it is the default value)
  • <resourceID> specify the identifier of the currently displayed node to allow the cache system to correctly redisplay the form in case of errors.

Allowing content creation in live workspaces by authenticated or non-authenticated users

If your form is designed to be used in live mode (render/live) to allow your users to create content, then you must use the token system of Jahia for your forms and enforce the usage of captcha for at least your non authenticated users.

When Jahia receive data from a tokenized form, it will execute the action with system user. This ensure that anybody seeing the form will be able to execute the action, even if it requires write access. It will also bypass any action restrictions that were set on the action, such as requiredWorkspace, requiredPermission, requiredAuthenticatedUser. If you need to restrict the form display or submission to a specific workspace, you will have to add <c:if> statements around the tokenized form rendering that test the current workspace and also implement checks inside your actions implementation (make sure you do both because a malicious user could always try to submit a HTTP POST request directly to your action, bypassing the HTML form entirely).

First you will need to tokenize your form for that simply wrap your form inside the <template:tokenizedForm> tag

<template:tokenizedForm>
    <form>
        ~~~~~~form code~~~~~~
    </form>
</template:tokenizedForm>

If you look at your html source code in your browser, you will see that the tokenizedForm tag has added an hidden input field in your form containing a uuid that identifies this instance of the form. The uuid is generated by a macro so that the uuid is regenerated each time the form is displayed even if delivered from the cache system.

You also need to add a captcha for at least the non authenticated users to avoid spamming of fake form submission.

To add a captcha, you need to have an input field named <captcha> and display the captcha itself using the <template:captcha> tag.

<c:if test="${not renderContext.loggedIn}">
    <p class="field">
        <label class="left" for="captcha"><template:captcha/></label>
        <input type="text" id="captcha" name="jcrCaptcha"/>
    </p>
</c:if>

Validation of data before and after submission

To validate your form on the client side we advise you to use the JQuery Validate plugin. Here an example of configuring it for the previous form:

<template:addResources type="javascript" resources="jquery.js,jquery.validate.js"/>
<template:addResources type="inlinejavascript">
    <script type="text/javascript">
        $(document).ready(function() {
            $("#newCommentForm").validate({
                rules: {
                    'jcr:title': "required",
                    <c:if test="${not renderContext.loggedIn}">
                    pseudo: "required",
                    captcha: "required"
                    </c:if>
                }
            });
        });
    </script>
</template:addResources>

To be noticed that this plugin refer to the field by their <name> attribute and not their <id> attribute.

On token or captcha error Jahia will send you back the submitted data so you can refill the form with previously submitted data, so that your users do not have to retype everything.

All submitted data is stored under map in the session of the user. Here an example on a simple filed to display the previously submitted data if needed.

<input class="" value="${sessionScope.formDatas['jcr:title'][0]}"
    type="text" size="35" id="comment-title" name="jcr:title"
    tabindex="1"/>

We see here the formDatas map under the session of the user <$sessionScope.formDatas['jcr:title'][0]>

Actions provided by the default module

This chapter in under construction - if you need more details about this topic, please as directly through the forum.

Permissions

In order to use some actions, you might need to setup permissions if you intend to perform them with users other than the root user.

General actions

addMemberToGroup

chain

This action is used to execute many actions one after the other.

Class: ChainAction.java

URL: /path/to/the/node.chain.do

Parameters:

Name Value Example
chainOfAction comma separated list of action names default,startWorkflow

Example:

<!--
Submitting this form :
1. creates a main content into the home page of ACME-SPACE demo website
2. starts the simple publication workflow  
-->
<form method="post" action="cms/render/default/en/sites/ACME-SPACE/home.chain.do" />
  <input type="hidden" name="chainOfAction" value="default,startWorkflow"/>
 
  <!-- Parameters for the default action (content creation) -->
  <input type="hidden" name="jcrNodeType" value="jnt:mainContent"/>
  <input type="hidden" name="jcr:title" value="Title of my new content"/>
  <input type="hidden" name="body" value="Body of my new content"/>
  <!-- Parameters for StartWorkflowAction -->
  <input type="hidden" name="process" value="jBPM:1-step-publication" />
  <input type="submit" value="Start the actions" />
</form>

lock

unlock

move

multiplePublish

publish

startWorkflow

This action starts a workflow on a node.

Class: StartWorkflowAction.java

URL: /path/to/the/node.startWorkflow.do

Parameters:

Name Value Example
process <workflowProvider>:<workflowKey> jBPM:1-step-publication

Example:

<!-- Submitting this form starts the simple publication workflow on the homepage of ACME-SPACE demo website -->
<form method="post" action="cms/render/default/en/sites/ACME-SPACE/home.startWorkflow.do" />
    <input type="hidden" name="process" value="jBPM:1-step-publication" />
    <input type="submit" value="Start workflow" />
</form>

Comments

addComment

Clipboard

checkClipboard

cleanClipboard

multipleCopy

multipleCut

multipleDelete

multiplePaste

Tasks

commentTask

executeTask