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