001/*
002 *  Copyright 2015 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.workflow;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.Comparator;
021import java.util.Date;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import javax.jcr.Node;
030import javax.jcr.RepositoryException;
031import javax.jcr.Session;
032
033import org.apache.avalon.framework.parameters.Parameters;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.cocoon.ProcessingException;
037import org.apache.cocoon.acting.ServiceableAction;
038import org.apache.cocoon.environment.ObjectModelHelper;
039import org.apache.cocoon.environment.Redirector;
040import org.apache.cocoon.environment.Request;
041import org.apache.cocoon.environment.SourceResolver;
042import org.apache.commons.lang.StringUtils;
043
044import org.ametys.cms.repository.Content;
045import org.ametys.cms.repository.WorkflowAwareContent;
046import org.ametys.core.cocoon.JSonReader;
047import org.ametys.core.user.UserIdentity;
048import org.ametys.plugins.core.user.UserHelper;
049import org.ametys.plugins.repository.AmetysObjectResolver;
050import org.ametys.plugins.repository.version.VersionAwareAmetysObject;
051import org.ametys.plugins.workflow.store.AbstractJackrabbitWorkflowStore;
052import org.ametys.plugins.workflow.store.AmetysStep;
053import org.ametys.plugins.workflow.support.WorkflowProvider;
054import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
055import org.ametys.runtime.i18n.I18nizableText;
056import org.ametys.runtime.parameter.ParameterHelper;
057
058import com.opensymphony.workflow.loader.ActionDescriptor;
059import com.opensymphony.workflow.loader.StepDescriptor;
060import com.opensymphony.workflow.loader.WorkflowDescriptor;
061import com.opensymphony.workflow.spi.SimpleStep;
062import com.opensymphony.workflow.spi.Step;
063
064/**
065 * This action returns the workflow history of a content
066 *
067 */
068public class ContentHistoryAction extends ServiceableAction
069{
070    private static final I18nizableText __MESSAGE_NO_STEP = new I18nizableText("plugin.cms", "WORKFLOW_UNKNOWN_STEP");
071
072    private static final I18nizableText __MESSAGE_NO_ACTION = new I18nizableText("plugin.cms", "WORKFLOW_UNKNOWN_ACTION");
073
074    private WorkflowProvider _workflowProvider;
075
076    private UserHelper _userHelper;
077
078    private AmetysObjectResolver _resolver;
079
080
081    @Override
082    public void service(ServiceManager serviceManager) throws ServiceException
083    {
084        super.service(serviceManager);
085        _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE);
086        _userHelper = (UserHelper) serviceManager.lookup(UserHelper.ROLE);
087        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
088    }
089
090    public Map act(Redirector redirector, SourceResolver resolver, Map objectModel, String source, Parameters parameters) throws Exception
091    {
092        Request request = ObjectModelHelper.getRequest(objectModel);
093
094        String id = request.getParameter("contentId");
095        Content content = _resolver.resolveById(id);
096
097        assert content instanceof VersionAwareAmetysObject;
098
099        Map<String, Object> result = new HashMap<>();
100        List<Map<String, Object>> workflowSteps = new ArrayList<>();
101        
102        Set<String> validatedVersions = new HashSet<>();
103        
104        try
105        {
106            List<VersionInformation> versionsInformation = _resolveVersionInformations((VersionAwareAmetysObject) content);
107            workflowSteps = _getContentWorkflowHistory(content, versionsInformation, validatedVersions);
108        }
109        catch (RepositoryException e)
110        {
111            throw new ProcessingException("Unable to access version history", e);
112        }
113        
114        for (Map<String, Object> stepInfo : workflowSteps)
115        {
116            @SuppressWarnings("unchecked")
117            List<Map<String, Object>> versions = (List<Map<String, Object>>) stepInfo.get("versions");
118            
119            for (Map<String, Object> version : versions)
120            {
121                String versionName = (String) version.get("name");
122                if (validatedVersions.contains(versionName))
123                {
124                    version.put("valid", true);
125                }
126            }
127        }
128
129        result.put("workflow", workflowSteps);
130        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
131        return EMPTY_MAP;
132    }
133
134    private List<Map<String, Object>> _getContentWorkflowHistory(Content content, List<VersionInformation> versionsInformation, Set<String> validatedVersions) throws ProcessingException, RepositoryException
135    {
136        List<Map<String, Object>> workflowHistory = new ArrayList<>();
137
138        if (content instanceof WorkflowAwareContent)
139        {
140            WorkflowAwareContent workflowAwareContent = (WorkflowAwareContent) content;
141            
142            long workflowId = workflowAwareContent.getWorkflowId();
143            AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(workflowAwareContent);
144            
145            String workflowName = workflow.getWorkflowName(workflowId);
146
147            if (workflowName == null)
148            {
149                throw new ProcessingException("Unknown workflow name for workflow instance id: " + workflowId);
150            }
151
152            WorkflowDescriptor workflowDescriptor = workflow.getWorkflowDescriptor(workflowName);
153
154            if (workflowDescriptor == null)
155            {
156                throw new ProcessingException("No workflow description for workflow name: " + workflowName);
157            }
158
159            @SuppressWarnings("unchecked")
160            List<Step> allSteps = new ArrayList(workflow.getCurrentSteps(workflowId));
161            allSteps.addAll(workflow.getHistorySteps(workflowId));
162
163            // Sort by start date descendant (only relevant when there is no
164            // split and nor join!)
165            Collections.sort(allSteps, new Comparator<Step>()
166            {
167                public int compare(Step s1, Step s2)
168                {
169                    return -s1.getStartDate().compareTo(s2.getStartDate());
170                }
171            });
172
173            Date startDate = null;
174
175            if (!allSteps.isEmpty())
176            {
177                // Use start date of the first step for the start date
178                // of the unstored "real first step"
179                startDate = allSteps.get(allSteps.size() - 1).getStartDate();
180            }
181
182            // Use a custom step for the unstored "real first step"
183            int initialActionId = (int) _getInitialActionId(workflow, workflowAwareContent);
184            allSteps.add(new SimpleStep(0, 0, 0, initialActionId, null, content.getCreationDate(), startDate, null, "", new long[0], UserIdentity.userIdentityToString(content.getCreator())));
185
186            Iterator<Step> itStep = allSteps.iterator();
187            // We got at least one
188            Step step = itStep.next();
189
190            do
191            {
192                Step previousStep = null;
193
194                if (itStep.hasNext())
195                {
196                    previousStep = itStep.next();
197                }
198
199                workflowHistory.add(_step2Json(content, workflowDescriptor, versionsInformation, validatedVersions, step, previousStep));
200
201                step = previousStep;
202            }
203            while (itStep.hasNext());
204        }
205
206        return workflowHistory;
207    }
208
209    private long _getInitialActionId(AmetysObjectWorkflow workflow, WorkflowAwareContent waContent)
210    {
211        try
212        {
213            Session session = waContent.getNode().getSession();
214            AbstractJackrabbitWorkflowStore workflowStore = (AbstractJackrabbitWorkflowStore) workflow.getConfiguration().getWorkflowStore();
215            Node workflowEntryNode = workflowStore.getEntryNode(session, waContent.getWorkflowId());
216            return workflowEntryNode.getProperty("ametys-internal:initialActionId").getLong();
217        }
218        catch (Exception e)
219        {
220            getLogger().error("Unable to retrieves initial action id for workflow aware content : " + waContent.getId(), e);
221            return 0;
222        }
223    }
224
225    private Map<String, Object> _step2Json(Content content, WorkflowDescriptor workflowDescriptor, List<VersionInformation> versionsInformation, Set<String> validatedVersions, Step step, Step previousStep)
226            throws RepositoryException
227    {
228        Map<String, Object> step2json = new HashMap<>();
229
230        int stepId = step.getStepId();
231        step2json.put("id", stepId);
232        step2json.put("current", step.getFinishDate() == null);
233
234        // We want the caller of the action responsible for being in the step
235        String caller = previousStep != null ? previousStep.getCaller() : null;
236        UserIdentity callerIdentity = UserIdentity.stringToUserIdentity(caller);
237        if (callerIdentity != null)
238        {
239            step2json.put("caller", _userHelper.user2json(callerIdentity));
240        }
241        
242        step2json.putAll(_getDescription2Json(workflowDescriptor, stepId));
243
244        // We want the id of the action responsible for being in the step
245        int actionId = previousStep != null ? previousStep.getActionId() : 0;
246        step2json.putAll(_getWorkfowAction2Json(callerIdentity, content, workflowDescriptor, actionId));
247
248        // Comment
249        if (previousStep != null && previousStep instanceof AmetysStep)
250        {
251            String comments = (String) ((AmetysStep) previousStep).getProperty("comment");
252            if (comments != null)
253            {
254                step2json.put("comment", comments);
255            }
256        }
257
258        /*Date actionStartDate = null;*/
259        Date actionFinishDate = null;
260        if (step instanceof AmetysStep)
261        {
262            /*actionStartDate = (Date) ((AmetysStep) step).getProperty("actionStartDate");*/
263            actionFinishDate = (Date) ((AmetysStep) step).getProperty("actionFinishDate");
264        }
265
266        Date startDate = null;
267        Date finishDate = null;
268        if (actionFinishDate != null)
269        {
270            // New format
271            startDate = step.getStartDate() != null ? step.getStartDate() : null;
272            finishDate = step.getFinishDate() != null ? step.getFinishDate() : null;
273        }
274        else
275        {
276            startDate = previousStep != null ? previousStep.getFinishDate() : null;
277            finishDate = step.getStartDate();
278        }
279
280        boolean isValid = _isValid(step);
281        step2json.put("validation", isValid);
282        step2json.put("date", _getDate(startDate, finishDate, actionFinishDate));
283
284        // Versions
285        List<Map<String, Object>> versions = _getVersionsBetween2Json(versionsInformation, /*stepId, */startDate, finishDate);
286        step2json.put("versions", versions);
287        
288        if (isValid)
289        {
290            for (Map<String, Object> version : versions)
291            {
292                validatedVersions.add((String) version.get("name")); 
293            }
294        }
295
296        return step2json;
297    }
298
299    private Map<String, Object> _getDescription2Json(WorkflowDescriptor workflowDescriptor, int stepId)
300    {
301        Map<String, Object> desc2json = new HashMap<>();
302
303        if (stepId != 0)
304        {
305            StepDescriptor stepDescriptor = workflowDescriptor.getStep(stepId);
306
307            I18nizableText workflowStepName = stepDescriptor == null ? __MESSAGE_NO_STEP : new I18nizableText("application", stepDescriptor.getName());
308            desc2json.put("description", workflowStepName);
309
310            if ("application".equals(workflowStepName.getCatalogue()))
311            {
312                desc2json.put("iconSmall", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-small.png");
313                desc2json.put("iconMedium", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-medium.png");
314                desc2json.put("iconLarge", "/plugins/cms/resources_workflow/" + workflowStepName.getKey() + "-large.png");
315            }
316            else
317            {
318                String pluginName = workflowStepName.getCatalogue().substring("plugin.".length());
319                desc2json.put("iconSmall", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-small.png");
320                desc2json.put("iconMedium", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-medium.png");
321                desc2json.put("iconLarge", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowStepName.getKey() + "-large.png");
322            }
323        }
324        else
325        {
326            desc2json.put("description", "?");
327            desc2json.put("iconSmall", "/plugins/cms/resources/img/history/workflow/step_0_16.png");
328            desc2json.put("iconMedium", "/plugins/cms/resources/img/history/workflow/step_0_32.png");
329        }
330
331        return desc2json;
332    }
333
334    private Map<String, Object> _getWorkfowAction2Json(UserIdentity caller, Content content, WorkflowDescriptor workflowDescriptor, int actionId)
335    {
336        Map<String, Object> action2json = new HashMap<>();
337
338        Map<String, I18nizableText> i18nParams = new HashMap<>();
339        String userFullName = _userHelper.getUserFullName(caller);
340        i18nParams.put("user", StringUtils.isNotEmpty(userFullName) ? new I18nizableText(_userHelper.getUserFullName(caller)) : new I18nizableText("plugin.cms", "PLUGINS_CMS_UITOOL_HISTORY_UNKNOWN_USER"));
341        i18nParams.put("title", new I18nizableText(content.getTitle()));
342
343        action2json.put("actionId", actionId);
344        if (actionId != 0)
345        {
346            ActionDescriptor actionDescriptor = workflowDescriptor.getAction(actionId);
347            
348            I18nizableText workflowActionLabel = actionDescriptor == null ? __MESSAGE_NO_ACTION : new I18nizableText("application", actionDescriptor.getName() + "_ACTION_DESCRIPTION", i18nParams);
349            action2json.put("actionLabel", workflowActionLabel);
350            
351            I18nizableText workflowActionName = actionDescriptor == null ? __MESSAGE_NO_ACTION : new I18nizableText("application", actionDescriptor.getName());
352            if ("application".equals(workflowActionName.getCatalogue()))
353            {
354                action2json.put("actionIconSmall", "/plugins/cms/resources_workflow/" + workflowActionName.getKey() + "-small.png");
355                action2json.put("actionIconMedium", "/plugins/cms/resources_workflow/" + workflowActionName.getKey() + "-medium.png");
356                action2json.put("actionIconLarge", "/plugins/cms/resources_workflow/" + workflowActionName.getKey() + "-large.png");
357            }
358            else
359            {
360                String pluginName = workflowActionName.getCatalogue().substring("plugin.".length());
361                action2json.put("actionIconSmall", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowActionName.getKey() + "-small.png");
362                action2json.put("actionIconMedium", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowActionName.getKey() + "-medium.png");
363                action2json.put("actionIconLarge", "/plugins/" + pluginName + "/resources/img/workflow/" + workflowActionName.getKey() + "-large.png");
364            }
365        }
366        else
367        {
368            action2json.put("actionLabel", new I18nizableText("plugin.cms", "WORKFLOW_ACTION_CREATE_ACTION_DESCRIPTION", i18nParams));
369            action2json.put("actionIconSmall", "/plugins/cms/resources/img/history/workflow/action_0_16.png");
370            action2json.put("actionIconMedium", "/plugins/cms/resources/img/history/workflow/action_0_32.png");
371            action2json.put("actionIconLarge", "/plugins/cms/resources/img/history/workflow/action_0_32.png");
372        }
373
374        return action2json;
375    }
376
377    private List<Map<String, Object>> _getVersionsBetween2Json(List<VersionInformation> versionsInformation, /*int stepId, */Date startDate, Date finishDate) throws RepositoryException
378    {
379        List<Map<String, Object>> versions = new ArrayList<>();
380        List<VersionInformation> versionsInsideStep = _computeVersionsBetween(versionsInformation, startDate, finishDate);
381
382        for (VersionInformation versionInformation : versionsInsideStep)
383        {
384            Map<String, Object> version = new HashMap<>();
385
386            String versionName = versionInformation.getVersionName();
387            version.put("label", versionInformation.getLabels());
388            version.put("name", versionName);
389            if (StringUtils.isNotEmpty(versionInformation.getVersionRawName()))
390            {
391                version.put("rawName", versionInformation.getVersionRawName());
392            }
393
394            version.put("createdAt", ParameterHelper.valueToString(versionInformation.getCreatedAt()));
395
396            versions.add(version);
397        }
398        return versions;
399    }
400
401    private boolean _isValid(Step step)
402    {
403        Boolean validation = false;
404        if (step instanceof AmetysStep)
405        {
406            validation = (Boolean) ((AmetysStep) step).getProperty("validation");
407        }
408
409        return validation == null ? false : validation;
410    }
411    
412    private String _getDate(Date startDate, Date finishDate, Date actionFinishDate)
413    {
414        if (actionFinishDate != null)
415        {
416            return ParameterHelper.valueToString(startDate);
417        }
418        else
419        {
420            return ParameterHelper.valueToString(finishDate);
421        }
422    }
423
424    private List<VersionInformation> _computeVersionsBetween(List<VersionInformation> versionsInformation, Date startDate, Date finishDate) throws RepositoryException
425    {
426        List<VersionInformation> versionsInsideStep = new ArrayList<>();
427
428        for (VersionInformation versionInformation : versionsInformation)
429        {
430            Date versionCreationDate = versionInformation.getCreatedAt();
431
432            if (startDate != null)
433            {
434                if (versionCreationDate.after(startDate))
435                {
436                    if (finishDate == null || versionCreationDate.before(finishDate))
437                    {
438                        versionsInsideStep.add(versionInformation);
439                    }
440                }
441            }
442            else
443            {
444                if (finishDate == null || versionCreationDate.before(finishDate))
445                {
446                    versionsInsideStep.add(versionInformation);
447                }
448            }
449        }
450
451        // If there is no version created inside the step,
452        // retrieve the last one just before the step
453        if (versionsInsideStep.isEmpty())
454        {
455            VersionInformation lastVersionBeforeStep = null;
456
457            for (VersionInformation versionInformation : versionsInformation)
458            {
459                Date versionCreationDate = versionInformation.getCreatedAt();
460
461                if (startDate != null && versionCreationDate.before(startDate))
462                {
463                    lastVersionBeforeStep = versionInformation;
464                    break;
465                }
466                else if (startDate == null)
467                {
468                    // Use the first version
469                    lastVersionBeforeStep = versionInformation;
470                }
471            }
472
473            if (lastVersionBeforeStep != null)
474            {
475                versionsInsideStep.add(lastVersionBeforeStep);
476            }
477        }
478
479        // if there is still no version for this step, then it is version 1
480        // (case of migrations)
481        if (versionsInsideStep.isEmpty())
482        {
483            versionsInsideStep.add(new VersionInformation("1"));
484        }
485
486        return versionsInsideStep;
487    }
488
489    private List<VersionInformation> _resolveVersionInformations(VersionAwareAmetysObject content) throws RepositoryException
490    {
491        List<VersionInformation> versionsInformation = new ArrayList<>();
492
493        for (String revision : content.getAllRevisions())
494        {
495            VersionInformation versionInformation = new VersionInformation(revision, content.getRevisionTimestamp(revision));
496
497            for (String label : content.getLabels(revision))
498            {
499                versionInformation.addLabel(label);
500            }
501
502            versionsInformation.add(versionInformation);
503        }
504
505        // Sort by date descendant
506        Collections.sort(versionsInformation, new Comparator<VersionInformation>()
507        {
508            public int compare(VersionInformation o1, VersionInformation o2)
509            {
510                try
511                {
512                    return -o1.getCreatedAt().compareTo(o2.getCreatedAt());
513                }
514                catch (RepositoryException e)
515                {
516                    throw new RuntimeException("Unable to retrieve a creation date", e);
517                }
518            }
519        });
520
521        // Set the version name
522        int count = versionsInformation.size();
523        for (VersionInformation versionInformation : versionsInformation)
524        {
525            versionInformation.setVersionName(String.valueOf(count--));
526        }
527
528        return versionsInformation;
529    }
530
531    private static class VersionInformation
532    {
533        private String _rawName;
534
535        private String _name;
536
537        private Date _creationDate;
538
539        private Set<String> _labels = new HashSet<>();
540
541        /**
542         * Creates a {@link VersionInformation}.
543         * 
544         * @param rawName the revision name.
545         * @param creationDate the revision creation date.
546         * @throws RepositoryException if an error occurs.
547         */
548        public VersionInformation(String rawName, Date creationDate) throws RepositoryException
549        {
550            _creationDate = creationDate;
551            _rawName = rawName;
552        }
553
554        /**
555         * Creates a named {@link VersionInformation}.
556         * 
557         * @param name the name to use.
558         */
559        public VersionInformation(String name)
560        {
561            _name = name;
562        }
563
564        /**
565         * Set the version name
566         * 
567         * @param name The name
568         */
569        public void setVersionName(String name)
570        {
571            _name = name;
572        }
573
574        /**
575         * Retrieves the version name.
576         * 
577         * @return the version name.
578         */
579        public String getVersionName()
580        {
581            return _name;
582        }
583
584        /**
585         * Retrieves the version raw name.
586         * 
587         * @return the version raw name.
588         */
589        public String getVersionRawName()
590        {
591            return _rawName;
592        }
593
594        /**
595         * Retrieves the creation date.
596         * 
597         * @return the creation date.
598         * @throws RepositoryException if an error occurs.
599         */
600        public Date getCreatedAt() throws RepositoryException
601        {
602            return _creationDate;
603        }
604
605        /**
606         * Retrieves the labels associated with this version.
607         * 
608         * @return the labels.
609         */
610        public Set<String> getLabels()
611        {
612            return _labels;
613        }
614
615        /**
616         * Add a label to this version.
617         * 
618         * @param label the label.
619         */
620        public void addLabel(String label)
621        {
622            _labels.add(label);
623        }
624    }
625}