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.CmsConstants;
037import org.ametys.cms.repository.Content;
038import org.ametys.cms.repository.WorkflowAwareContent;
039import org.ametys.cms.workflow.ContentWorkflowHelper;
040import org.ametys.core.ui.Callable;
041import org.ametys.odf.ODFHelper;
042import org.ametys.odf.ProgramItem;
043import org.ametys.odf.course.Course;
044import org.ametys.odf.course.CourseFactory;
045import org.ametys.odf.courselist.CourseListFactory;
046import org.ametys.odf.orgunit.OrgUnit;
047import org.ametys.odf.orgunit.OrgUnitFactory;
048import org.ametys.odf.person.PersonFactory;
049import org.ametys.odf.program.AbstractProgram;
050import org.ametys.odf.program.ContainerFactory;
051import org.ametys.odf.program.ProgramFactory;
052import org.ametys.odf.program.SubProgramFactory;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.repository.UnknownAmetysObjectException;
055import org.ametys.plugins.repository.version.VersionAwareAmetysObject;
056import org.ametys.plugins.workflow.AbstractWorkflowComponent;
057import org.ametys.plugins.workflow.AbstractWorkflowComponent.ConditionFailure;
058import org.ametys.plugins.workflow.component.CheckRightsCondition;
059import org.ametys.plugins.workflow.support.WorkflowProvider;
060import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
061import org.ametys.runtime.plugin.component.AbstractLogEnabled;
062
063import com.opensymphony.workflow.InvalidActionException;
064import com.opensymphony.workflow.WorkflowException;
065import com.opensymphony.workflow.spi.Step;
066
067/**
068 * Helper for ODF contents on their workflow
069 *
070 */
071public class ODFWorkflowHelper extends AbstractLogEnabled implements Component, Serviceable
072{
073    /** The component role. */
074    public static final String ROLE = ODFWorkflowHelper.class.getName();
075    
076    /** The validate step id */
077    public static final int VALIDATED_STEP_ID = 3;
078    
079    /** The action id of global validation */
080    public static final int VALIDATE_ACTION_ID = 4;
081    
082    /** The action id of global unpublishment */
083    public static final int UNPUBLISH_ACTION_ID = 10;
084    
085    /** Constant for storing the result map into the transient variables map. */
086    protected static final String CONTENTS_IN_ERROR_KEY = "contentsInError";
087    
088    /** Constant for storing the content with right error into the transient variables map. */
089    protected static final String CONTENTS_WITH_RIGHT_ERROR_KEY = "contentsWithRightError";
090    
091    /** Constant for storing the result map into the transient variables map. */
092    protected static final String VALIDATED_CONTENTS_KEY = "validatedContents";
093    
094    /** Constant for storing the unpublish result map into the transient variables map. */
095    protected static final String UNPUBLISHED_CONTENTS_KEY = "unpublishedContents";
096    
097    /** The Ametys object resolver */
098    protected AmetysObjectResolver _resolver;
099    /** The workflow provider */
100    protected WorkflowProvider _workflowProvider;
101    /** The ODF helper */
102    protected ODFHelper _odfHelper;
103    /** The workflow helper for contents */
104    protected ContentWorkflowHelper _contentWorkflowHelper;
105    
106    private enum WorkflowActionStatus 
107    {
108        SUCCESS,
109        RIGHT_ERROR,
110        OTHER_ERROR
111    }
112    
113    @Override
114    public void service(ServiceManager manager) throws ServiceException
115    {
116        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
117        _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE);
118        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
119        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
120    }
121    
122    /**
123     * Check if the contents has referenced contents that are not already validated (children excluded)
124     * @param contentIds The id of contents to check
125     * @return A map with success key to true if referenced contents are validated. A map with the invalidated contents otherwise.
126     */
127    @Callable
128    public Map<String, Object> checkReferences(List<String> contentIds)
129    {
130        Map<String, Object> result = new HashMap<>();
131        
132        List<Map<String, Object>> contentsInError = new ArrayList<>();
133        
134        for (String contentId : contentIds)
135        {
136            Set<Content> invalidatedContents = new TreeSet<>(new ContentTypeComparator());
137            
138            WorkflowAwareContent content = _resolver.resolveById(contentId);
139            _checkValidateStep(content, invalidatedContents, false);
140            
141            // Remove initial content from invalidated contents
142            invalidatedContents.remove(content);
143            
144            if (!invalidatedContents.isEmpty())
145            {
146                List<Map<String, Object>> invalidatedContentsAsJson = invalidatedContents.stream()
147                        .map(c -> _content2Json(c))
148                        .collect(Collectors.toList());
149                
150                Map<String, Object> contentInError = new HashMap<>();
151                contentInError.put("id", content.getId());
152                contentInError.put("code", ((ProgramItem) content).getCode());
153                contentInError.put("title", content.getTitle());
154                contentInError.put("invalidatedContents", invalidatedContentsAsJson);
155                contentsInError.add(contentInError);
156            }
157        }
158        
159        result.put("contentsInError", contentsInError);
160        result.put("success", contentsInError.isEmpty());
161        return result;
162    }
163    
164    /**
165     * Get the global validation status of a content
166     * @param contentId the id of content
167     * @return the result
168     */
169    @Callable
170    public Map<String, Object> getGlobalValidationStatus(String contentId)
171    {
172        Map<String, Object> result = new HashMap<>();
173        
174        WorkflowAwareContent waContent = _resolver.resolveById(contentId);
175        
176        // Order invalidated contents by types
177        Set<Content> invalidatedContents = getInvalidatedContents(waContent);
178        
179        List<Map<String, Object>> invalidatedContentsAsJson = invalidatedContents.stream()
180                .map(c -> _content2Json(c))
181                .collect(Collectors.toList());
182        
183        result.put("invalidatedContents", invalidatedContentsAsJson);
184        result.put("globalValidated", invalidatedContents.isEmpty());
185        
186        return result;
187    }
188    
189    /**
190     * Get the invalidated contents referenced by a ODF content
191     * @param content the initial ODF content
192     * @return the set of referenced invalidated contents
193     */
194    public Set<Content> getInvalidatedContents(WorkflowAwareContent content)
195    {
196        Set<Content> invalidatedContents = new TreeSet<>(new ContentTypeComparator());
197        
198        _checkValidateStep(content, invalidatedContents, true);
199        
200        return invalidatedContents;
201    }
202    
203    /**
204     * Determines if a content is already in validated step
205     * @param content The content to test
206     * @return true if the content is already validated
207     */
208    public boolean isInValidatedStep (WorkflowAwareContent content)
209    {
210        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content);
211        long workflowId = content.getWorkflowId();
212        
213        List<Step> steps = workflow.getCurrentSteps(workflowId);
214        for (Step step : steps)
215        {
216            if (step.getStepId() == VALIDATED_STEP_ID)
217            {
218                return true;
219            }
220        }
221        
222        return false;
223    }
224    
225    private void _checkValidateStep (WorkflowAwareContent content, Set<Content> invalidatedContents, boolean checkChildren)
226    {
227        if (!isInValidatedStep(content))
228        {
229            invalidatedContents.add(content);
230        }
231        
232        if (checkChildren && content instanceof ProgramItem)
233        {
234            // Check the structure recursively
235            List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content)
236                    .stream()
237                    .filter(ProgramItem::isPublishable)
238                    .toList();
239            for (ProgramItem child : children)
240            {
241                WorkflowAwareContent waChild = (WorkflowAwareContent) child;
242                _checkValidateStep(waChild, invalidatedContents, checkChildren);
243            }
244        }
245        
246        // Validate others referenced contents
247        if (content instanceof AbstractProgram)
248        {
249            _checkReferencedContents(((AbstractProgram) content).getOrgUnits(), invalidatedContents, checkChildren);
250            _checkReferencedContents(((AbstractProgram) content).getContacts(), invalidatedContents, checkChildren);
251        }
252        else if (content instanceof Course)
253        {
254            _checkReferencedContents(((Course) content).getOrgUnits(), invalidatedContents, checkChildren);
255            _checkReferencedContents(((Course) content).getContacts(), invalidatedContents, checkChildren);
256        }
257        else if (content instanceof OrgUnit)
258        {
259            _checkReferencedContents(((OrgUnit) content).getContacts(), invalidatedContents, checkChildren);
260        }
261    }
262    
263    private void _checkReferencedContents(Collection<String> refContentIds, Set<Content> invalidatedContents, boolean recursively)
264    {
265        for (String id : refContentIds)
266        {
267            try
268            {
269                if (StringUtils.isNotEmpty(id))
270                {
271                    WorkflowAwareContent refContent = _resolver.resolveById(id);
272                    if (recursively)
273                    {
274                        _checkValidateStep(refContent, invalidatedContents, recursively);
275                    }
276                    else if (!isInValidatedStep(refContent))
277                    {
278                        invalidatedContents.add(refContent);
279                    }
280                }
281            }
282            catch (UnknownAmetysObjectException e)
283            {
284                // Nothing
285            }
286        }
287    }
288    
289    /**
290     * Global validation on a contents.
291     * Validate the contents with their whole structure and the others referenced contacts and orgunits.
292     * @param contentIds the id of contents to validation recursively
293     * @return the result for each initial contents
294     */
295    @Callable
296    public Map<String, Object> globalValidate(List<String> contentIds)
297    {
298        Map<String, Object> result = new HashMap<>();
299        
300        for (String contentId : contentIds)
301        {
302            Map<String, Object> contentResult = new HashMap<>();
303            
304            contentResult.put(CONTENTS_IN_ERROR_KEY, new HashSet<>());
305            contentResult.put(CONTENTS_WITH_RIGHT_ERROR_KEY, new HashSet<>());
306            contentResult.put(VALIDATED_CONTENTS_KEY, new HashSet<>());
307            
308            ProgramItem programItem = _resolver.resolveById(contentId);
309            if (programItem.isPublishable())
310            {
311                _validateRecursively((WorkflowAwareContent) programItem, contentResult);
312            }
313            
314            @SuppressWarnings("unchecked")
315            Set<Content> contentsInError = (Set<Content>) contentResult.get(CONTENTS_IN_ERROR_KEY);
316            List<Map<String, Object>> contentsInErrorAsJson = contentsInError.stream()
317                    .map(c -> _content2Json(c))
318                    .toList();
319            
320            contentResult.put(CONTENTS_IN_ERROR_KEY, contentsInErrorAsJson);
321            
322            @SuppressWarnings("unchecked")
323            Set<Content> contentsWithRightError = (Set<Content>) contentResult.get(CONTENTS_WITH_RIGHT_ERROR_KEY);
324            List<Map<String, Object>> contentsWithRightErrorAsJson = contentsWithRightError.stream()
325                    .map(c -> _content2Json(c))
326                    .toList();
327            
328            contentResult.put(CONTENTS_WITH_RIGHT_ERROR_KEY, contentsWithRightErrorAsJson);
329            
330            
331            result.put(contentId, contentResult);
332        }
333        
334        return result;
335    }
336    
337    /**
338     * Get the JSON representation of the content
339     * @param content the content
340     * @return the content properties
341     */
342    protected Map<String, Object> _content2Json(Content content)
343    {
344        Map<String, Object> content2json = new HashMap<>();
345        content2json.put("title", content.getTitle());
346        content2json.put("id", content.getId());
347        
348        if (content instanceof ProgramItem)
349        {
350            content2json.put("code", ((ProgramItem) content).getCode());
351        }
352        else if (content instanceof OrgUnit)
353        {
354            content2json.put("code", ((OrgUnit) content).getUAICode());
355        }
356        
357        return content2json;
358    }
359    
360    /**
361     * Validate the referenced contents recursively
362     * @param content The validated content
363     * @param result the result object to fill during process
364     */
365    protected void _validateRecursively (WorkflowAwareContent content, Map<String, Object> result)
366    {
367        @SuppressWarnings("unchecked")
368        Set<String> validatedContentIds = (Set<String>) result.get(VALIDATED_CONTENTS_KEY);
369        @SuppressWarnings("unchecked")
370        Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY);
371        @SuppressWarnings("unchecked")
372        Set<Content> contentsWithRightError = (Set<Content>) result.get(CONTENTS_WITH_RIGHT_ERROR_KEY);
373        
374        if (!isInValidatedStep(content))
375        {
376            // Validate content itself
377            WorkflowActionStatus status = _doValidateWorkflowAction (content, VALIDATE_ACTION_ID);
378            if (status == WorkflowActionStatus.RIGHT_ERROR)
379            {
380                contentsWithRightError.add(content);
381            }
382            else if (status == WorkflowActionStatus.OTHER_ERROR)
383            {
384                contentsInError.add(content);
385            }
386            else
387            {
388                validatedContentIds.add(content.getId());
389            }
390        }
391        
392        if (content instanceof ProgramItem)
393        {
394            // Validate the structure recursively
395            List<ProgramItem> children = _odfHelper.getChildProgramItems((ProgramItem) content)
396                    .stream()
397                    .filter(ProgramItem::isPublishable)
398                    .toList();
399            for (ProgramItem child : children)
400            {
401                _validateRecursively((WorkflowAwareContent) child, result);
402            }
403        }
404        
405        // Validate others referenced contents
406        if (content instanceof AbstractProgram)
407        {
408            _validateReferencedContents(((AbstractProgram) content).getOrgUnits(), result);
409            _validateReferencedContents(((AbstractProgram) content).getContacts(), result);
410        }
411        else if (content instanceof Course)
412        {
413            _validateReferencedContents(((Course) content).getOrgUnits(), result);
414            _validateReferencedContents(((Course) content).getContacts(), result);
415        }
416        else if (content instanceof OrgUnit)
417        {
418            _validateReferencedContents(((OrgUnit) content).getContacts(), result);
419        }
420    }
421    
422    /**
423     * Validate the list of referenced contents
424     * @param refContentIds The id of contents to validate
425     * @param result the result object to fill during process
426     */
427    protected void _validateReferencedContents (Collection<String> refContentIds, Map<String, Object> result)
428    {
429        @SuppressWarnings("unchecked")
430        Set<String> validatedContentIds = (Set<String>) result.get(VALIDATED_CONTENTS_KEY);
431        @SuppressWarnings("unchecked")
432        Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY);
433        @SuppressWarnings("unchecked")
434        Set<Content> contentsWithRightError = (Set<Content>) result.get(CONTENTS_WITH_RIGHT_ERROR_KEY);
435        
436        for (String id : refContentIds)
437        {
438            try
439            {
440                if (StringUtils.isNotEmpty(id))
441                {
442                    WorkflowAwareContent content = _resolver.resolveById(id);
443                    if (!isInValidatedStep(content))
444                    {
445                        WorkflowActionStatus status = _doValidateWorkflowAction (content, VALIDATE_ACTION_ID);
446                        if (status == WorkflowActionStatus.RIGHT_ERROR)
447                        {
448                            contentsWithRightError.add(content);
449                        }
450                        else if (status == WorkflowActionStatus.OTHER_ERROR)
451                        {
452                            contentsInError.add(content);
453                        }
454                        else
455                        {
456                            validatedContentIds.add(content.getId());
457                        }
458                    }
459                }
460            }
461            catch (UnknownAmetysObjectException e)
462            {
463                // Nothing
464            }
465        }
466    }
467    
468    /**
469     * Validate a content
470     * @param content The content to validate
471     * @param actionId The id of validate action
472     * @return the validate workflow status
473     */
474    protected WorkflowActionStatus _doValidateWorkflowAction (WorkflowAwareContent content, int actionId)
475    {
476        Map<String, Object> inputs = new HashMap<>();
477        try
478        {
479            _contentWorkflowHelper.doAction(content, actionId, inputs, false);
480            return WorkflowActionStatus.SUCCESS;
481        }
482        catch (InvalidActionException | WorkflowException e)
483        {
484            String failureString = _getActionFailuresAsString(inputs);
485            if (e instanceof InvalidActionException)
486            {
487                getLogger().warn("Unable to validate content \"{}\" ({}): mandatory metadata are probably missing or the content is locked{}", content.getTitle(), content.getId(), failureString, e);
488            }
489            else
490            {
491                getLogger().warn("Failed to validate content \"{}\" ({}){}", content.getTitle(), content.getId(), failureString, e);
492            }
493            return _isRightConditionFailure(inputs) ? WorkflowActionStatus.RIGHT_ERROR : WorkflowActionStatus.OTHER_ERROR;
494        }
495    }
496    
497    private boolean _isRightConditionFailure(Map<String, Object> actionInputs)
498    {
499        if (actionInputs.containsKey(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY))
500        {
501            @SuppressWarnings("unchecked")
502            List<ConditionFailure> failures = (List<ConditionFailure>) actionInputs.getOrDefault(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<>());
503            return failures.stream()
504                        .filter(c -> c.type().equals(CheckRightsCondition.class.getName()))
505                        .findFirst()
506                        .isPresent();
507        }
508        
509        return false;
510    }
511    
512    private String _getActionFailuresAsString(Map<String, Object> actionInputs)
513    {
514        String failuresAsString = "";
515        if (actionInputs.containsKey(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY))
516        {
517            @SuppressWarnings("unchecked")
518            List<ConditionFailure> failures = (List<ConditionFailure>) actionInputs.getOrDefault(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<>());
519            if (!failures.isEmpty())
520            {
521                failuresAsString = ", due to the following error(s):\n" + String.join("\n", failures.stream().map(ConditionFailure::text).toList());
522            }
523        }
524        
525        return failuresAsString;
526    }
527    
528    /**
529     * Set the publishable state of contents
530     * @param contentIds The id of contents
531     * @param isPublishable <code>true</code> to set content as publishable, <code>false</code> otherwise
532     * @return The result map
533     */
534    @Callable
535    public Map<String, Object> setPublishableState (List<String> contentIds, boolean isPublishable)
536    {
537        Map<String, Object> result = new HashMap<>();
538        
539        for (String id : contentIds)
540        {
541            Map<String, Object> contentResult = new HashMap<>();
542            Set<Content> contentsInError = new HashSet<>();
543            contentResult.put(CONTENTS_IN_ERROR_KEY, contentsInError);
544            Set<Content> contentsWithRightError = new HashSet<>();
545            contentResult.put(CONTENTS_WITH_RIGHT_ERROR_KEY, contentsWithRightError);
546            contentResult.put(UNPUBLISHED_CONTENTS_KEY, new HashSet<>());
547            
548            WorkflowAwareContent content = _resolver.resolveById(id);
549            if (content instanceof ProgramItem programItem)
550            {
551                try
552                {
553                    programItem.setPublishable(isPublishable);
554                    content.saveChanges();
555
556                    if (!isPublishable)
557                    {
558                        _unpublishRecursively(programItem, contentResult);
559                    }
560                }
561                catch (Exception e)
562                {
563                    getLogger().error("Unable to set publishable property for content '{}' with id '{}'", content.getTitle(), content.getId(), e);
564                    contentsInError.add(content);
565                }
566
567                List<Map<String, Object>> contentsInErrorAsJson = contentsInError.stream()
568                        .map(c -> _content2Json(c))
569                        .collect(Collectors.toList());
570                contentResult.put(CONTENTS_IN_ERROR_KEY, contentsInErrorAsJson);
571                
572                List<Map<String, Object>> contentsWithRightErrorAsJson = contentsWithRightError.stream()
573                        .map(c -> _content2Json(c))
574                        .collect(Collectors.toList());
575                contentResult.put(CONTENTS_WITH_RIGHT_ERROR_KEY, contentsWithRightErrorAsJson);
576                
577                result.put(id, contentResult);
578            }
579        } 
580        
581        return result;
582    }
583    
584    /**
585     * Unpublish the referenced contents recursively
586     * @param programItem The content to unpublish
587     * @param result the result object to fill during process
588     */
589    protected void _unpublishRecursively (ProgramItem programItem, Map<String, Object> result)
590    {
591        @SuppressWarnings("unchecked")
592        Set<String> unpublishedContentIds = (Set<String>) result.get(UNPUBLISHED_CONTENTS_KEY);
593        @SuppressWarnings("unchecked")
594        Set<Content> contentsInError = (Set<Content>) result.get(CONTENTS_IN_ERROR_KEY);
595        @SuppressWarnings("unchecked")
596        Set<Content> contentsWithRightError = (Set<Content>) result.get(CONTENTS_WITH_RIGHT_ERROR_KEY);
597        
598        if (_isPublished(programItem))
599        {
600            // Unpublish content itself
601            WorkflowActionStatus status = _doUnpublishWorkflowAction((WorkflowAwareContent) programItem, UNPUBLISH_ACTION_ID);
602            if (status == WorkflowActionStatus.RIGHT_ERROR)
603            {
604                contentsWithRightError.add((Content) programItem);
605            }
606            else if (status == WorkflowActionStatus.OTHER_ERROR)
607            {
608                contentsInError.add((Content) programItem);
609            }
610            else
611            {
612                unpublishedContentIds.add(programItem.getId());
613            }
614        }
615
616        // Unpublish the structure recursively
617        List<ProgramItem> children = _odfHelper.getChildProgramItems(programItem);
618        for (ProgramItem child : children)
619        {
620            boolean hasOtherPublishedParent = _odfHelper.getParentProgramItems(child)
621                .stream()
622                .filter(p -> !p.getId().equals(programItem.getId()))
623                .filter(this::_isPublished)
624                .findFirst()
625                .isPresent();
626                
627            // Don't unpublish the content if it has an other published parent
628            if (!hasOtherPublishedParent)
629            {
630                _unpublishRecursively(child, result);
631            }
632        }
633    }
634    
635    /**
636     * Unpublish a content
637     * @param content The content to unpublish
638     * @param actionId The id of unpublish action
639     * @return the workflow action status
640     */
641    protected WorkflowActionStatus _doUnpublishWorkflowAction (WorkflowAwareContent content, int actionId)
642    {
643        Map<String, Object> inputs = new HashMap<>();
644        try
645        {
646            _contentWorkflowHelper.doAction(content, actionId, inputs, false);
647            return WorkflowActionStatus.SUCCESS;
648        }
649        catch (Exception e)
650        {
651            String failureString = _getActionFailuresAsString(inputs);
652            getLogger().warn("Failed to unpublish content \"{}\" ({}){}", content.getTitle(), content.getId(), failureString, e);
653            return _isRightConditionFailure(inputs) ? WorkflowActionStatus.RIGHT_ERROR : WorkflowActionStatus.OTHER_ERROR;
654        }
655    }
656    
657    /**
658     * <code>true</code> if the parent is publishable
659     * @param content the content
660     * @return <code>true</code> if the parent is publishable
661     */
662    public boolean isParentPublishable(ProgramItem content)
663    {
664        List<ProgramItem> parents = _odfHelper.getParentProgramItems(content);
665        if (parents.isEmpty())
666        {
667            return true;
668        }
669        
670        return parents.stream()
671                .filter(c -> c.isPublishable() && isParentPublishable(c))
672                .findAny()
673                .isPresent();
674    }
675    
676    private boolean _isPublished(ProgramItem content)
677    {
678        return content instanceof VersionAwareAmetysObject versionAAO ? ArrayUtils.contains(versionAAO.getAllLabels(), CmsConstants.LIVE_LABEL) : false;
679    }
680    
681    class ContentTypeComparator implements Comparator<Content>
682    {
683        String[] _orderedContentTypes = new String[] {
684            ProgramFactory.PROGRAM_CONTENT_TYPE,
685            SubProgramFactory.SUBPROGRAM_CONTENT_TYPE,
686            ContainerFactory.CONTAINER_CONTENT_TYPE,
687            CourseListFactory.COURSE_LIST_CONTENT_TYPE,
688            CourseFactory.COURSE_CONTENT_TYPE,
689            OrgUnitFactory.ORGUNIT_CONTENT_TYPE,
690            PersonFactory.PERSON_CONTENT_TYPE
691        };
692                
693        @Override
694        public int compare(Content c1, Content c2)
695        {
696            if (c1 == c2)
697            {
698                return 0;
699            }
700            
701            String cTypeId1 = c1.getTypes()[0];
702            String cTypeId2 = c2.getTypes()[0];
703            
704            int i1 = ArrayUtils.indexOf(_orderedContentTypes, cTypeId1);
705            int i2 = ArrayUtils.indexOf(_orderedContentTypes, cTypeId2);
706            
707            if (i1 == i2)
708            {
709                // order by title for content of same type
710                int compareTo = c1.getTitle().compareTo(c2.getTitle());
711                if (compareTo == 0)
712                {
713                    // for content of same title, order by id to do not return 0 to add it in TreeSet
714                    // Indeed, in a TreeSet implementation two elements that are equal by the method compareTo are, from the standpoint of the set, equal 
715                    return c1.getId().compareTo(c2.getId());
716                }
717                else
718                {
719                    return compareTo;
720                }
721            }
722            
723            return i1 != -1 && i1 < i2 ? -1 : 1;
724        }
725    }
726    
727}