001/*
002 *  Copyright 2017 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.odf.workflow;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Comparator;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.TreeSet;
027import java.util.stream.Collectors;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.service.ServiceException;
031import org.apache.avalon.framework.service.ServiceManager;
032import org.apache.avalon.framework.service.Serviceable;
033import org.apache.commons.lang3.ArrayUtils;
034import org.apache.commons.lang3.StringUtils;
035
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.repository.WorkflowAwareContent;
038import org.ametys.cms.workflow.ContentWorkflowHelper;
039import org.ametys.core.ui.Callable;
040import org.ametys.odf.ODFHelper;
041import org.ametys.odf.ProgramItem;
042import org.ametys.odf.course.Course;
043import org.ametys.odf.course.CourseFactory;
044import org.ametys.odf.courselist.CourseListFactory;
045import org.ametys.odf.orgunit.OrgUnit;
046import org.ametys.odf.orgunit.OrgUnitFactory;
047import org.ametys.odf.person.PersonFactory;
048import org.ametys.odf.program.AbstractProgram;
049import org.ametys.odf.program.ContainerFactory;
050import org.ametys.odf.program.ProgramFactory;
051import org.ametys.odf.program.SubProgramFactory;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.repository.UnknownAmetysObjectException;
054import org.ametys.plugins.workflow.support.WorkflowProvider;
055import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
056import org.ametys.runtime.plugin.component.AbstractLogEnabled;
057
058import com.opensymphony.workflow.InvalidActionException;
059import com.opensymphony.workflow.WorkflowException;
060import com.opensymphony.workflow.spi.Step;
061
062/**
063 * Helper for ODF contents on their workflow
064 *
065 */
066public class ODFWorkflowHelper extends AbstractLogEnabled implements Component, Serviceable
067{
068    /** The component role. */
069    public static final String ROLE = ODFWorkflowHelper.class.getName();
070    
071    /** The validate step id */
072    public static final int VALIDATED_STEP_ID = 3;
073    
074    /** Constant for storing the result map into the transient variables map. */
075    protected static final String CONTENTS_IN_ERROR_KEY = "contentsInError";
076    
077    /** Constant for storing the result map into the transient variables map. */
078    protected static final String VALIDATED_CONTENTS_KEY = "validatedContents";
079    
080    /** The Ametys object resolver */
081    protected AmetysObjectResolver _resolver;
082    /** The workflow provider */
083    protected WorkflowProvider _workflowProvider;
084    /** The ODF helper */
085    protected ODFHelper _odfHelper;
086    /** The workflow helper for contents */
087    protected ContentWorkflowHelper _contentWorkflowHelper;
088    
089    @Override
090    public void service(ServiceManager manager) throws ServiceException
091    {
092        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
093        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
094        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
095        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
096    }
097    
098    /**
099     * Check if the contents can be validated. A content can be validated only if all its referenced contents are already validated.
100     * @param contentIds The id of contents to check
101     * @return A map with success key to true if all contents can be validated. A map with the invalidated contents otherwise.
102     */
103    @Callable
104    public Map<String, Object> canValidContent(List<String> contentIds)
105    {
106        Map<String, Object> result = new HashMap<>();
107        
108        List<Map<String, Object>> contentsInError = new ArrayList<>();
109        
110        for (String contentId : contentIds)
111        {
112            Set<Content> invalidatedContents = new TreeSet<>(new ContentTypeComparator());
113            
114            WorkflowAwareContent content = _resolver.resolveById(contentId);
115            _checkValidateStep(content, invalidatedContents);
116            
117            // Remove initial content from invalidated contents
118            invalidatedContents.remove(content);
119            
120            if (!invalidatedContents.isEmpty())
121            {
122                List<Map<String, Object>> invalidatedContentsAsJson = invalidatedContents.stream()
123                        .map(c -> _content2Json(c))
124                        .collect(Collectors.toList());
125                
126                Map<String, Object> contentInError = new HashMap<>();
127                contentInError.put("id", content.getId());
128                contentInError.put("code", ((ProgramItem) content).getCode());
129                contentInError.put("title", content.getTitle());
130                contentInError.put("invalidatedContents", invalidatedContentsAsJson);
131                contentsInError.add(contentInError);
132            }
133        }
134        
135        result.put("contentsInError", contentsInError);
136        result.put("success", contentsInError.isEmpty());
137        return result;
138    }
139    
140    /**
141     * Get the global validation status of a content
142     * @param contentId the id of content
143     * @return the result
144     */
145    @Callable
146    public Map<String, Object> getGlobalValidationStatus(String contentId)
147    {
148        Map<String, Object> result = new HashMap<>();
149        
150        WorkflowAwareContent waContent = _resolver.resolveById(contentId);
151        
152        // Order invalidated contents by types
153        Set<Content> invalidatedContents = getInvalidatedContents(waContent);
154        
155        List<Map<String, Object>> invalidatedContentsAsJson = invalidatedContents.stream()
156                .map(c -> _content2Json(c))
157                .collect(Collectors.toList());
158        
159        result.put("invalidatedContents", invalidatedContentsAsJson);
160        result.put("globalValidated", invalidatedContents.isEmpty());
161        
162        return result;
163    }
164    
165    /**
166     * Get the invalidated contents referenced by a ODF content
167     * @param content the initial ODF content
168     * @return the set of referenced invalidated contents
169     */
170    public Set<Content> getInvalidatedContents(WorkflowAwareContent content)
171    {
172        Set<Content> invalidatedContents = new TreeSet<>(new ContentTypeComparator());
173        
174        _checkValidateStep(content, invalidatedContents);
175        
176        return invalidatedContents;
177    }
178    
179    /**
180     * Determines if a content is already in validated step
181     * @param content The content to test
182     * @return true if the content is already validated
183     */
184    public boolean isInValidatedStep (WorkflowAwareContent content)
185    {
186        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
187        long workflowId = content.getWorkflowId();
188        
189        List<Step> steps = workflow.getCurrentSteps(workflowId);
190        for (Step step : steps)
191        {
192            if (step.getStepId() == VALIDATED_STEP_ID)
193            {
194                return true;
195            }
196        }
197        
198        return false;
199    }
200    
201    private void _checkValidateStep (WorkflowAwareContent content, Set<Content> invalidatedContents)
202    {
203        if (!isInValidatedStep(content))
204        {
205            invalidatedContents.add(content);
206        }
207        
208        if (content instanceof ProgramItem)
209        {
210            // Check the structure recursively
211            List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content);
212            for (ProgramItem child : children)
213            {
214                WorkflowAwareContent waChild = (WorkflowAwareContent) child;
215                _checkValidateStep(waChild, invalidatedContents);
216            }
217        }
218        
219        // Validate others referenced contents
220        if (content instanceof AbstractProgram)
221        {
222            _checkReferencedContents(((AbstractProgram) content).getOrgUnits(), invalidatedContents);
223            _checkReferencedContents(((AbstractProgram) content).getContacts(), invalidatedContents);
224        }
225        else if (content instanceof Course)
226        {
227            _checkReferencedContents(((Course) content).getOrgUnits(), invalidatedContents);
228            _checkReferencedContents(((Course) content).getContacts(), invalidatedContents);
229        }
230        else if (content instanceof OrgUnit)
231        {
232            _checkReferencedContents(((OrgUnit) content).getContacts(), invalidatedContents);
233        }
234    }
235    
236    private void _checkReferencedContents (Collection<String> refContentIds, Set<Content> invalidatedContent)
237    {
238        for (String id : refContentIds)
239        {
240            try
241            {
242                if (StringUtils.isNotEmpty(id))
243                {
244                    WorkflowAwareContent refContent = _resolver.resolveById(id);
245                    _checkValidateStep(refContent, invalidatedContent);
246                }
247            }
248            catch (UnknownAmetysObjectException e)
249            {
250                // Nothing
251            }
252        }
253    }
254    
255    /**
256     * Global validation on a contents.
257     * Validate the contents with their whole structure and the others referenced contacts and orgunits.
258     * @param contentIds the id of contents to validation recursively
259     * @return the result for each initial contents
260     */
261    @Callable
262    public Map<String, Object> globalValidate(List<String> contentIds)
263    {
264        Map<String, Object> result = new HashMap<>();
265        
266        for (String contentId : contentIds)
267        {
268            Map<String, Object> contentResult = new HashMap<>();
269            
270            contentResult.put(CONTENTS_IN_ERROR_KEY, new HashSet<Content>());
271            contentResult.put(VALIDATED_CONTENTS_KEY, new HashSet<String>());
272            
273            WorkflowAwareContent waContent = _resolver.resolveById(contentId);
274            
275            _validateRecursively(waContent, contentResult);
276            
277            @SuppressWarnings("unchecked")
278            Set<Content> contentsInError = (Set<Content>) contentResult.get(CONTENTS_IN_ERROR_KEY);
279            List<Map<String, Object>> contentsInErrorAsJson = contentsInError.stream()
280                    .map(c -> _content2Json(c))
281                    .collect(Collectors.toList());
282            
283            contentResult.put(CONTENTS_IN_ERROR_KEY, contentsInErrorAsJson);
284            
285            result.put(contentId, contentResult);
286        }
287        
288        return result;
289    }
290    
291    /**
292     * Get the JSON representation of the content
293     * @param content the content
294     * @return the content properties
295     */
296    protected Map<String, Object> _content2Json(Content content)
297    {
298        Map<String, Object> content2json = new HashMap<>();
299        content2json.put("title", content.getTitle());
300        content2json.put("id", content.getId());
301        
302        if (content instanceof ProgramItem)
303        {
304            content2json.put("code", ((ProgramItem) content).getCode());
305        }
306        else if (content instanceof OrgUnit)
307        {
308            content2json.put("code", ((OrgUnit) content).getUAICode());
309        }
310        
311        return content2json;
312    }
313    
314    /**
315     * Validate the referenced contents recursively
316     * @param content The validated content
317     * @param result the result object to fill during process
318     */
319    protected void _validateRecursively (WorkflowAwareContent content, Map<String, Object> result)
320    {
321        @SuppressWarnings("unchecked")
322        Set<String> validatedContentIds = (Set<String>) result.get(VALIDATED_CONTENTS_KEY);
323        @SuppressWarnings("unchecked")
324        Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY);
325        
326        if (!isInValidatedStep(content))
327        {
328            // Validate content itself
329            if (!_doValidateWorkflowAction(content, ValidateODFContentFunction.VALIDATE_ACTION_ID))
330            {
331                contentsInError.add(content);
332            }
333            else
334            {
335                validatedContentIds.add(content.getId());
336            }
337        }
338        
339        if (content instanceof ProgramItem)
340        {
341            // Validate the structure recursively
342            List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content);
343            for (ProgramItem child : children)
344            {
345                _validateRecursively((WorkflowAwareContent) child, result);
346            }
347        }
348        
349        // Validate others referenced contents
350        if (content instanceof AbstractProgram)
351        {
352            _validateReferencedContents(((AbstractProgram) content).getOrgUnits(), result);
353            _validateReferencedContents(((AbstractProgram) content).getContacts(), result);
354        }
355        else if (content instanceof Course)
356        {
357            _validateReferencedContents(((Course) content).getOrgUnits(), result);
358            _validateReferencedContents(((Course) content).getContacts(), result);
359        }
360        else if (content instanceof OrgUnit)
361        {
362            _validateReferencedContents(((OrgUnit) content).getContacts(), result);
363        }
364    }
365    
366    /**
367     * Validate the list of referenced contents
368     * @param refContentIds The id of contents to validate
369     * @param result the result object to fill during process
370     */
371    protected void _validateReferencedContents (Collection<String> refContentIds, Map<String, Object> result)
372    {
373        @SuppressWarnings("unchecked")
374        Set<String> validatedContentIds = (Set<String>) result.get(VALIDATED_CONTENTS_KEY);
375        @SuppressWarnings("unchecked")
376        Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY);
377        
378        for (String id : refContentIds)
379        {
380            try
381            {
382                if (StringUtils.isNotEmpty(id))
383                {
384                    WorkflowAwareContent content = _resolver.resolveById(id);
385                    if (!isInValidatedStep(content))
386                    {
387                        if (!_doValidateWorkflowAction (content, ValidateODFContentFunction.VALIDATE_ACTION_ID))
388                        {
389                            contentsInError.add(content);
390                        }
391                        else
392                        {
393                            validatedContentIds.add(content.getId());
394                        }
395                    }
396                }
397            }
398            catch (UnknownAmetysObjectException e)
399            {
400                // Nothing
401            }
402        }
403    }
404    
405    /**
406     * Validate a content
407     * @param content The content to validate
408     * @param actionId The id of validate action
409     * @return true if the validation success
410     */
411    protected boolean _doValidateWorkflowAction (WorkflowAwareContent content, int actionId)
412    {
413        try
414        {
415            _contentWorkflowHelper.doAction(content, actionId, new HashMap<>());
416            return true;
417        }
418        catch (InvalidActionException e)
419        {
420            getLogger().warn("Unable to validate content \"{}\" ({}): mandatory metadata are probably missing or the content is locked", content.getTitle(), content.getId(), e);
421            return false;
422        }
423        catch (WorkflowException e)
424        {
425            getLogger().warn("Failed to validate content \"{}\" ({})", content.getTitle(), content.getId(), e);
426            return false;
427        }
428    }
429    
430    class ContentTypeComparator implements Comparator<Content>
431    {
432        String[] _orderedContentTypes = new String[] {
433            ProgramFactory.PROGRAM_CONTENT_TYPE,
434            SubProgramFactory.SUBPROGRAM_CONTENT_TYPE,
435            ContainerFactory.CONTAINER_CONTENT_TYPE,
436            CourseListFactory.COURSE_LIST_CONTENT_TYPE,
437            CourseFactory.COURSE_CONTENT_TYPE,
438            OrgUnitFactory.ORGUNIT_CONTENT_TYPE,
439            PersonFactory.PERSON_CONTENT_TYPE
440        };
441                
442        @Override
443        public int compare(Content c1, Content c2)
444        {
445            if (c1 == c2)
446            {
447                return 0;
448            }
449            
450            String cTypeId1 = c1.getTypes()[0];
451            String cTypeId2 = c2.getTypes()[0];
452            
453            int i1 = ArrayUtils.indexOf(_orderedContentTypes, cTypeId1);
454            int i2 = ArrayUtils.indexOf(_orderedContentTypes, cTypeId2);
455            
456            if (i1 == i2)
457            {
458                // order by title for content of same type
459                int compareTo = c1.getTitle().compareTo(c2.getTitle());
460                if (compareTo == 0)
461                {
462                    // for content of same title, order by id to do not return 0 to add it in TreeSet
463                    // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 
464                    return c1.getId().compareTo(c2.getId());
465                }
466                else
467                {
468                    return compareTo;
469                }
470            }
471            
472            return i1 != -1 && i1 < i2 ? -1 : 1;
473        }
474    }
475    
476}