AEM 65 - Integrating AEM Projects with JIRA for Task Management

Goal


If AEM Inbox (http://localhost:4502/aem/inbox) is not enough for the customer's Task Management needs and would like to integrate with a more complex Task management system like Atlassian JIRA you can try the following solution..

Here the scenario is, gather a team (years of experience using JIRA) for review, approve and publish a bunch of Sites pages. The users are good with JIRA and would like to continue using JIRA, rather than getting trained in a new task management software (AEM Inbox) for assigning and completing the respective tasks.

The solution discussed below uses AEM Projects and Workflow processes for setting up a team, assign tasks for approving and publishing the pages using otb Request for Activation workflow

Check this Adobe tutorial for creating projects

For downloading jira check this page, and this one for REST api

Demo | Package Install | Github


AEM JIRA Integration Project Setup & Execution

1) Select the template Experience AEM Approve Pages and Assets Project (available in Package Install) to create a new project for reviewing the pages



2) Add the users (team) responsible for completing the review and publish page task



3) In the Workflows pod click on Start Workflow (project template is setup with workflow models for projects created using this template)



4) Select the workflow Experience AEM Request for Activation



5) Select the page path and user responsible for reviewing, approving and publishing the page (steps in workflow process)



6) A Task is created in JIRA and assigned to the respective user (assuming the user has same id in AEM and JIRA; additionally a task is created in AEM Inbox). The expectation is, user clicks on AEM page link in ticket description, publishes the page and resolves ticket in JIRA....



Solution


1) Create the AEM project template Experience AEM Approve Pages and Assets Project  /apps/eaem-review-assets-jira-tasks/projects/templates/approve-assets-project (available  in Package Install) with the pods for Team, Tasks, Workflows, Project Info...




<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:description="A project to approve and publish Assets"
    jcr:primaryType="cq:Template"
    jcr:title="Experience AEM Approve Pages and Assets Project"
    ranking="{Long}1"
    wizard="/libs/cq/core/content/projects/wizard/steps/defaultproject.html">
    <jcr:content
        jcr:primaryType="nt:unstructured"
        detailsHref="/projects/details.html"/>
    <gadgets jcr:primaryType="nt:unstructured">
        <team
            jcr:primaryType="nt:unstructured"
            jcr:title="Team"
            sling:resourceType="cq/gui/components/projects/admin/pod/teampod"
            cardWeight="60"/>
        <tasks
            jcr:primaryType="nt:unstructured"
            jcr:title="Tasks"
            sling:resourceType="cq/gui/components/projects/admin/pod/taskpod"
            cardWeight="100"/>
        <work
            jcr:primaryType="nt:unstructured"
            jcr:title="Workflows"
            sling:resourceType="cq/gui/components/projects/admin/pod/workpod"
            cardWeight="80"/>
        <projectinfo
            jcr:primaryType="nt:unstructured"
            jcr:title="Project Info"
            sling:resourceType="cq/gui/components/projects/admin/pod/projectinfopod"
            cardWeight="100"/>
    </gadgets>
    <roles jcr:primaryType="nt:unstructured">
        <approvers
            jcr:primaryType="nt:unstructured"
            jcr:title="EAEM Approvers"
            roleclass="owner"
            roleid="approvers"/>
    </roles>
    <workflows
        jcr:primaryType="nt:unstructured"
        tags="[]">
        <models jcr:primaryType="nt:unstructured">
            <assets-approval
                jcr:primaryType="nt:unstructured"
                modelId="/var/workflow/models/request_for_activation"
                wizard="/apps/eaem-review-assets-jira-tasks/projects/wizards/assets-approval-start.html"/>
        </models>
    </workflows>
</jcr:root>


2) Line #46 with workflow model /var/workflow/models/request_for_activation specifies projects created using this template have access to workflow Experience AEM Request for Activation from the workflow pod

3) The workflow wizard /apps/eaem-review-assets-jira-tasks/projects/wizards/assets-approval-start.html in Line # 47 provides the necessary form for entering workflow specific information like payload, assignee...

4) Create a workflow process step apps.experienceaem.assets.EAEMJiraWFProcess for executing REST calls and create tasks / tickets in JIRA (in the demo a JIRA instance is running locally on http://localhost:8080)

package apps.experienceaem.assets;

import com.day.cq.commons.Externalizer;
import com.day.cq.wcm.api.Page;
import com.day.cq.workflow.WorkflowException;
import com.day.cq.workflow.WorkflowSession;
import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowData;
import com.day.cq.workflow.exec.WorkflowProcess;

import com.day.cq.workflow.metadata.MetaDataMap;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHeaders;
import org.apache.http.message.BasicHeader;
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
import org.json.JSONObject;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.SocketConfig;
import org.apache.http.entity.StringEntity;

import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

import javax.jcr.Node;
import javax.jcr.Session;
import java.nio.charset.StandardCharsets;
import java.util.Collections;

@Component(
        immediate = true,
        service = {WorkflowProcess.class},
        property = {
                "process.label = Experience AEM JIRA Integration Workflow Step"
        }
)
@Designate(ocd = EAEMJiraWFProcess.Configuration.class)
public class EAEMJiraWFProcess implements WorkflowProcess {
    private static final Logger log = LoggerFactory.getLogger(EAEMJiraWFProcess.class);

    private static final String JIRA_REST_API = "http://localhost:8080/rest/api/2/issue/";

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    @Reference
    private Externalizer externalizer;

    private String jiraProjectKey , jiraUserName, jiraPassword;

    @Activate
    protected void activate(EAEMJiraWFProcess.Configuration configuration) {
        jiraProjectKey = configuration.jiraProjectKey();
        jiraUserName = configuration.jiraUserName();
        jiraPassword = configuration.jiraPassword();
    }

    public void execute(WorkItem workItem, WorkflowSession wfSession, MetaDataMap mapArgs) throws WorkflowException {
        try {
            if(StringUtils.isEmpty(jiraProjectKey) || StringUtils.isEmpty(jiraPassword)
                        || StringUtils.isEmpty(jiraUserName)){
                throw new RuntimeException("Required jira details missing from configuration jiraProjectKey, jiraUserName, jiraPassword");
            }

            Session session = wfSession.getSession();
            WorkflowData wfData = workItem.getWorkflowData();

            String pagePath = null;
            String payLoadType = wfData.getPayloadType();

            if(payLoadType.equals("JCR_PATH") && wfData.getPayload() != null) {
                if(session.itemExists((String)wfData.getPayload())) {
                    pagePath = (String)wfData.getPayload();
                }
            } else if( (wfData.getPayload() != null) && payLoadType.equals("JCR_UUID")) {
                Node metaDataMap = session.getNodeByUUID((String)wfData.getPayload());
                pagePath = metaDataMap.getPath();
            }

            if(StringUtils.isEmpty(pagePath)){
                log.warn("Page path - " + wfData.getPayload() + ", does not exist");
                return;
            }

            String assignee = (String)workItem.getWorkflow().getWorkflowData().getMetaDataMap().get("assignee").toString();

            ResourceResolver resolver = getResourceResolver(session);
            Resource pageResource = resolver.getResource(pagePath);

            createTicket(createTicketBody(resolver, pageResource.adaptTo(Page.class), assignee));
        } catch (Exception e) {
            log.error("Error while creating JIRA ticket", e);
        }
    }

    private String createTicketBody(ResourceResolver resolver, Page page, String user) throws Exception{
        JSONObject request = new JSONObject();
        JSONObject fields = new JSONObject();
        JSONObject project = new JSONObject();
        JSONObject issueType = new JSONObject();

        String authorLink = externalizer.externalLink(resolver, Externalizer.AUTHOR, "/editor.html" + page.getPath() + ".html");

        project.put("key", jiraProjectKey);
        issueType.put("name", "Task");

        fields.put("project", project);
        fields.put("issuetype", issueType);
        fields.put("summary", "Publish page : " + page.getTitle());
        fields.put("description", "Review Approve and Publish page : " + page.getTitle() + "\n\n" + authorLink);

        if(StringUtils.isNotEmpty(user)){
            JSONObject assignee = new JSONObject();

            assignee.put("name", user);
            fields.put("assignee", assignee);
        }

        request.put("fields", fields);

        return request.toString();
    }

    private void createTicket(String requestBody) throws Exception{
        CloseableHttpClient client = null;
        String responseBody = "";

        try {
            SocketConfig sc = SocketConfig.custom().setSoTimeout(180000).build();
            client = HttpClients.custom().setDefaultSocketConfig(sc).build();

            HttpPost post = new HttpPost(JIRA_REST_API);
            StringEntity entity = new StringEntity(requestBody, "UTF-8");

            post.addHeader("Content-Type", "application/json");
            post.setEntity(entity);
            post.setHeader(getAuthorizationHeader());

            HttpResponse response = client.execute(post);

            HttpEntity responseEntity = response.getEntity();

            responseBody = IOUtils.toString(responseEntity.getContent(), "UTF-8");

            log.debug("JIRA create ticket response - " + responseBody);
        }finally{
            if(client != null){
                client.close();
            }
        }
    }

    private Header getAuthorizationHeader(){
        String auth = jiraUserName + ":" + jiraPassword;

        byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.ISO_8859_1));
        String authHeader = "Basic " + new String(encodedAuth);

        return new BasicHeader(HttpHeaders.AUTHORIZATION, authHeader);
    }

    private ResourceResolver getResourceResolver(final Session session) throws LoginException {
        return resourceResolverFactory.getResourceResolver( Collections.<String, Object>
                singletonMap(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session));
    }

    @ObjectClassDefinition(
            name = "Experience AEM JIRA Integration",
            description = "Experience AEM JIRA Integration"
    )
    public @interface Configuration {

        @AttributeDefinition(
                name = "JIRA Project key",
                description = "JIRA project key found in http://<jira server>/secure/BrowseProjects.jspa?selectedCategory=all&selectedProjectType=all",
                type = AttributeType.STRING
        )
        String jiraProjectKey() default "";

        @AttributeDefinition(
                name = "JIRA User name",
                description = "JIRA User name",
                type = AttributeType.STRING
        )
        String jiraUserName() default "";

        @AttributeDefinition(
                name = "JIRA Password",
                description = "JIRA Password",
                type = AttributeType.PASSWORD
        )
        String jiraPassword() default "";
    }
}

5) Add a workflow step Experience AEM - Create Ticket in JIRA with above process in the workflow Experience AEM Request for Activation

                   http://localhost:4502/editor.html/conf/global/settings/workflow/models/request_for_activation.html



6) Add the JIRA Project key, admin Username and Password in workflow step OSGI configuration

                   http://localhost:4502/system/console/configMgr/apps.experienceaem.assets.EAEMJiraWFProcess




3 comments: