AEM 6 SP2 - Date Time Auto Advancer

Goal


Create a Date Time Auto Advancer extending ootb Absolute Time Auto Advancer to provide the times of day or exact date at which the participant step using this handler should time out.

Demo | Package Install | Source Code


The following screen grab shows Request for Activation workflow's Waiting for activation step using apps.experienceaem.autoadvancer.datetime.DateTimeAutoAdvancer





Selecting Date Time for Timeout shows a dialog with multifields for setting the Times in a day and exact Date & Times




In the above example, for workflow Request for Activation, Waiting for Activation step times out at the earliest time available after step Approve Content is completed. So lets say Approve Content was completed for an asset at 6:30 PM, the handler times out  at 10:30 PM and flow moves to next step Activate Content; asset gets activated at may be 10:31 PM, depending on how big the replication queue is....


Timeout values added in multifield, set in CRX





A sample sling job scheduled for Waiting for Activation




Solution


1) Create a OSGI service apps.experienceaem.autoadvancer.datetime.DateTimeAutoAdvancer extending com.day.cq.workflow.timeout.autoadvance.AbsoluteTimeAutoAdvancer. This is the handler selected for handling date time timeouts. If no time or date is selected for timeout, handler falls back to its super AbsoluteTimeAutoAdvancer for handling timeouts

package apps.experienceaem.autoadvancer.datetime;

import com.day.cq.workflow.exec.WorkItem;
import com.day.cq.workflow.exec.WorkflowProcess;
import com.day.cq.workflow.job.AbsoluteTimeoutHandler;
import com.day.cq.workflow.timeout.autoadvance.AbsoluteTimeAutoAdvancer;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.SimpleDateFormat;
import java.util.*;

@Component(metatype = false)
@Service(value={WorkflowProcess.class, AbsoluteTimeoutHandler.class})
@Properties({
        @Property(name="service.description", value="Experience AEM Date Time Auto Advancer Process"),
        @Property(name="process.label", value = "Experience AEM Date Time Auto Advancer")
})
public class DateTimeAutoAdvancer extends AbsoluteTimeAutoAdvancer {
    protected final Logger log = LoggerFactory.getLogger(DateTimeAutoAdvancer.class);

    private static SimpleDateFormat JS_JAVA_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
    private static String DATE_TIMES_PROP = "eaemTimeoutDateTimes";

    static {
        JS_JAVA_FORMATTER.setTimeZone(TimeZone.getTimeZone("GMT"));
    }

    private void addTimes(Date currentDate, JSONArray times, List<Long> dtNumbers) throws Exception{
        if( (times == null) || (times.length() == 0)){
            return;
        }

        Calendar cal;

        String time[], hhmm[], ampm;
        int hour, min, nextDayHour = -1, nextDayMin = -1;

        for(int i = 0, len = times.length(); i < len; i++){
            time = ((String)times.get(i)).split(" ");

            hhmm = time[0].split(":");
            ampm = time[1];

            cal = Calendar.getInstance();

            hour = NumberUtils.createInteger(hhmm[0]);
            hour = (ampm.equalsIgnoreCase("AM")) ? hour : hour + 12;
            min = NumberUtils.createInteger(hhmm[1]);

            cal.set(Calendar.HOUR_OF_DAY, hour);
            cal.set(Calendar.MINUTE, min);

            //get the earliest time of next day, if a page/asset was approved after the timeout times set
            //in a day, are passed
            if(i == 0){
                nextDayHour = hour;
                nextDayMin = min;
            }else{
                if(hour < nextDayHour){
                    nextDayHour = hour;
                    nextDayMin = min;
                }else if(hour == nextDayHour){
                    if( min < nextDayMin ){
                        nextDayMin = min;
                    }
                }
            }

            //skip past dates
            if(currentDate.getTime() > cal.getTimeInMillis()){
                continue;
            }

            dtNumbers.add(cal.getTimeInMillis());
        }

        if(dtNumbers.isEmpty()){
            cal = Calendar.getInstance();

            cal.add(Calendar.DATE, 1);
            cal.set(Calendar.HOUR_OF_DAY, nextDayHour);
            cal.set(Calendar.MINUTE, nextDayMin);

            dtNumbers.add(cal.getTimeInMillis());
        }
    }

    private void addDateTimes(Date currentDate, JSONArray dateTimes, List<Long> dtNumbers)
                                    throws Exception{
        if( (dateTimes == null) || (dateTimes.length() == 0)){
            return;
        }

        String dateStr = null; Date date = null;

        for(int i = 0, len = dateTimes.length(); i < len; i++){
            dateStr = (String)dateTimes.get(i);

            date = JS_JAVA_FORMATTER.parse(dateStr);

            //skip past dates
            if(currentDate.getTime() > date.getTime()){
                continue;
            }

            dtNumbers.add(date.getTime());
        }
    }

    public long getTimeoutDate(WorkItem workItem) {
        List<Long> dtNumbers = new ArrayList<Long>();
        Date currentDate = new Date();

        try{
            String dateTimesVal = workItem.getNode().getMetaDataMap().get(DATE_TIMES_PROP, String.class);

            if(StringUtils.isEmpty(dateTimesVal)){
                return super.getTimeoutDate(workItem);
            }

            JSONObject dt = new JSONObject(dateTimesVal);

            JSONArray times = (JSONArray)dt.get("times");
            JSONArray dateTimes = (JSONArray)dt.get("datetimes");

            addTimes(currentDate, times, dtNumbers);
            addDateTimes(currentDate, dateTimes, dtNumbers);

            Collections.sort(dtNumbers);
        }catch(Exception e){
            log.error("Could not calculate timeout", e);
        }

        // get the most recent date&time in future, at which the auto advancer should timeout
        return dtNumbers.isEmpty() ? super.getTimeoutDate(workItem) : dtNumbers.get(0);
    }
}


2) If any time of day or exact date, is in the past, handler simply ignores it. Handler adds all the next timeout dates in a collection, sorts them and gets the lowest long which is the most recent time in future the handler should time out

3) #62, #86, if times are set in dialog, but time of day is already in the past (say user has approved page/asset very late in the night) and no more timeouts are available for the day, handler tries to find earliest time of next day, it should timeout

4) Install the OSGI service Experience AEM Date Time Auto Advancer in CRX folder /apps/classicui-auto-advancer-date-time-selector

5) Login to CRXDE Lite, create clientlib (type cq:ClientLibraryFolder/apps/classicui-auto-advancer-date-time-selector/clientlib and set a property categories of String type to cq.widgets and dependencies type String[] to underscore

6) Create file ( type nt:file ) /apps/classicui-auto-advancer-date-time-selector/clientlib/js.txt, add the following

                         add-date-time.js

7) Create file ( type nt:file ) /apps/classicui-auto-advancer-date-time-selector/clientlib/add-date-time.js, add the following code

(function(){
    var DATE_TIME_AA_CLASS = "apps.experienceaem.autoadvancer.datetime.DateTimeAutoAdvancer";
    var DATE_TIME_SEL_VALUE = -1;
    var DATE_TIMES_PROP = "./metaData/eaemTimeoutDateTimes";

    var pathName = window.location.pathname;

    if( pathName.indexOf("/etc/workflow") != 0 ){
        return;
    }

    function getDialog(dateTimesHidden){
        var datetimes = new  CQ.form.MultiField({
            border: false,
            fieldLabel: "Date and Time",
            fieldConfig: {
                "xtype": "datetime"
            }
        });

        var times = new CQ.form.MultiField({
            border: false,
            fieldLabel: "Time",
            fieldConfig: {
                "xtype": "timefield"
            }
        });

        var text = "Time: The times in a day, handler should timeout - Date and Time: The exact date&time, handler should timeout" +
                   "Example: Workflow - Request for Activation, Handler Step: Waiting for Activation, Previous Step - Approve Content" +
                   "Scenario 1: 'Time' is set to '4:00 PM', '5:00 PM', '6:00 PM' and 'Approve Content' step is completed at '4:30 PM', " +
                   "'Waiting for Activation' step is guaranteed to timeout at '5:00 PM'" +
                   "Scenario 2: 'Time' is set to '4:00 PM', '5:00 PM', '6:00 PM' and " +
                   "'Date and Time' is set to 'May 04, 2015, 5:10 PM', 'June 13, 2015, 4:30 PM'; " +
                   "If 'Approve Content' step completes on 'May 04, 2015, 4:20 PM', the 'Waiting for Activation' timesout at " +
                   "'5:00 PM' and not '5:10 PM'. 'Approve Content' step completed on 'June 13, 2015, 4:20 PM' will timeout " +
                   "'Waiting for Activation' at '4:30 PM'";

        var config = {
            "jcr:primaryType": "cq:Dialog",
            width: 600,
            height: 400,
            title: "Date Time",
            items: {
                "jcr:primaryType": "cq:Panel",
                bodyStyle: "padding: 10px",
                html: text,
                items: {
                    "jcr:primaryType": "cq:WidgetCollection",
                    times: times,
                    datetimes: datetimes
                }
            },
            ok: function(){
                var value = {
                    times: times.getValue(),
                    datetimes: datetimes.getValue()
                };

                dateTimesHidden.setValue(JSON.stringify(value));

                this.close();
            },
            cancel: function(){
                this.close();
            }
        };

        var dateTimeValues = dateTimesHidden.getValue();

        if(!_.isEmpty(dateTimeValues)){
            dateTimeValues = JSON.parse(dateTimeValues);

            datetimes.setValue(dateTimeValues.datetimes);
            times.setValue(dateTimeValues.times);
        }

        return CQ.WCM.getDialog(config);
    }

    function initTimeoutSelection(dialog, timeoutType){
        var dateTimeOption = {
            text: "Date Time",
            value: DATE_TIME_SEL_VALUE
        };

        var dateTimesHidden = dialog.addHidden({ "./metaData/eaemTimeoutDateTimes" : ""})[DATE_TIMES_PROP];

        $.getJSON(dialog.form.url + ".infinity.json").done(function(data){
            if(_.isEmpty(data.metaData) || _.isEmpty(data.metaData.eaemTimeoutDateTimes)){
                return;
            }
            dateTimesHidden.setValue(data.metaData.eaemTimeoutDateTimes);
        });

        timeoutType.options.push(dateTimeOption);

        timeoutType.setOptions(timeoutType.options);

        timeoutType.reset();

        return dateTimesHidden;
    }

    function handleTimeoutChange(timesDialog, handlerType, dateTimesHidden, timeoutValue){
        if(timeoutValue != DATE_TIME_SEL_VALUE){
            return timesDialog;
        }

        if(handlerType.getValue() != DATE_TIME_AA_CLASS){
            CQ.Ext.Msg.alert("Invalid", "Handler selected cannot handle datetime");

            this.setValue("Off");

            return timesDialog;
        }

        //to handle the change event fired by combo in timeout selection
        if(timesDialog && timesDialog.isVisible()){
            return timesDialog;
        }

        timesDialog = getDialog(dateTimesHidden);

        timesDialog.show();

        return timesDialog;
    }

    function handleTimeoutHandlerChange(timeoutType, handlerValue){
        if(handlerValue != DATE_TIME_AA_CLASS){
            timeoutType.setValue("Off");
        }
    }

    function registerDateTime(dialog, handlerType, timeoutType){
        if(dialog.eaemListenersAdded){
            return;
        }

        dialog.eaemListenersAdded = true;

        var dateTimesHidden = initTimeoutSelection(dialog, timeoutType);

        var timesDialog;

        handlerType.on("selectionchanged", function(t, value){
            handleTimeoutHandlerChange(timeoutType, value);
        });

        timeoutType.on("selectionchanged", function(t, value){
            timesDialog = handleTimeoutChange.call(this, timesDialog, handlerType, dateTimesHidden, value);
        });
    }

    function findTimeoutHandler(editable){
        function handler(){
            var dialog = editable.dialogs[CQ.wcm.EditBase.EDIT];

            var selTypes = dialog.findByType("selection");

            if(_.isEmpty(selTypes)){
                return;
            }

            var handlerType, timeoutType;

            _.each(selTypes, function(selType){
                if(selType.name == "./metaData/timeoutHandler"){
                    handlerType = selType;
                }else if(selType.name == "./metaData/timeoutMillis"){
                    timeoutType = selType;
                }
            });

            //wait until the dialog gets opened by ootb handlers & initialized
            if(!handlerType || !timeoutType){
                return;
            }

            clearInterval(INTERVAL);

            registerDateTime(dialog, handlerType, timeoutType);
        }

        var INTERVAL = setInterval(handler, 250);
    }

    var INTERVAL = setInterval(function(){
        var editables = CQ.WCM.getEditables();

        if(_.isEmpty(editables)){
            return;
        }

        clearInterval(INTERVAL);

        _.each(editables, function(editable){
            editable.el.on('dblclick', function(e){
                findTimeoutHandler(editable);
            }, this);
        });
    }, 250);
})();


8) The JS extension waits until Handler and Timeout select widgets are available, adds the Date Time option and multifields dialog




No comments:

Post a Comment