001/*
002 *  Copyright 2018 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.plugins.odfpilotage.helper;
017
018import java.time.LocalDate;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024import java.util.stream.Collectors;
025
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.commons.lang3.ArrayUtils;
031
032import org.ametys.cms.CmsConstants;
033import org.ametys.cms.ObservationConstants;
034import org.ametys.cms.repository.Content;
035import org.ametys.cms.repository.ModifiableDefaultContent;
036import org.ametys.core.observation.Event;
037import org.ametys.core.observation.ObservationManager;
038import org.ametys.core.right.RightManager;
039import org.ametys.core.right.RightManager.RightResult;
040import org.ametys.core.user.CurrentUserProvider;
041import org.ametys.core.user.UserIdentity;
042import org.ametys.odf.ODFHelper;
043import org.ametys.odf.ProgramItem;
044import org.ametys.odf.coursepart.CoursePart;
045import org.ametys.odf.program.Program;
046import org.ametys.plugins.repository.AmetysObjectResolver;
047import org.ametys.plugins.repository.data.holder.group.ModifiableModelAwareComposite;
048import org.ametys.runtime.model.ModelItem;
049import org.ametys.runtime.plugin.component.AbstractLogEnabled;
050
051/**
052 * Helper for ODF pilotage status
053 */
054public class PilotageStatusHelper extends AbstractLogEnabled implements Component, Serviceable
055{
056    /** The component role. */
057    public static final String ROLE = PilotageStatusHelper.class.getName();
058    
059    /** The super right for mention validation state */
060    public static final String MENTION_VALIDATION_SUPER_RIGHT_ID = "ODF_Pilotage_Mention_Validated_Super_Rights";
061    
062    /** The super right for orgunit validation state */
063    public static final String ORGUNIT_VALIDATION_SUPER_RIGHT_ID = "ODF_Pilotage_OrgUnit_Validated_Super_Rights";
064    
065    /** The attribute name for the pilotage composite */
066    private static final String __PILOTAGE_COMPOSITE = "pilotage";
067    
068    /** The attribute name for the pilotage status */
069    private static final String __PILOTAGE_STATUS = "pilotage_status";
070    
071    /** The attribute name for the date of the mention validation */
072    private static final String __MENTION_VALIDATION_DATE = "mention_validation_date";
073
074    /** The attribute name for the author of the mention validation */
075    private static final String __MENTION_VALIDATION_AUTHOR = "mention_validation_author";
076
077    /** The attribute name for the comment of the mention validation */
078    private static final String __MENTION_VALIDATION_COMMENT = "mention_validation_comment";
079    
080    /** The attribute name for the date of the orgUnit validation */
081    private static final String __ORGUNIT_VALIDATION_DATE = "orgunit_validation_date";
082
083    /** The attribute name for the author of the orgUnit validation */
084    private static final String __ORGUNIT_VALIDATION_AUTHOR = "orgunit_validation_author";
085
086    /** The attribute name for the comment of the orgUnit validation */
087    private static final String __ORGUNIT_VALIDATION_COMMENT = "orgunit_validation_comment";
088    
089    /** The attribute name for the date of the CFVU validation */
090    private static final String __CFVU_VALIDATION_DATE = "cfvu_validation_date";
091
092    /** The attribute name for the author of the CFVU validation */
093    private static final String __CFVU_VALIDATION_AUTHOR = "cfvu_validation_author";
094
095    /** The attribute name for the comment of the CFVU validation */
096    private static final String __CFVU_VALIDATION_COMMENT = "cfvu_validation_comment";
097    
098    /** The attribute name for the date of the CFVU MCC validation */
099    private static final String __CFVU_MCC_VALIDATION_DATE = "cfvu_mcc_validation_date";
100
101    /** The attribute name for the author of the CFVU MCC validation */
102    private static final String __CFVU_MCC_VALIDATION_AUTHOR = "cfvu_mcc_validation_author";
103
104    /** The attribute name for the comment of the CFVU MCC validation */
105    private static final String __CFVU_MCC_VALIDATION_COMMENT = "cfvu_mcc_validation_comment";
106    
107    /** The odf helper */
108    protected ODFHelper _odfHelper;
109    
110    /** The Ametys object resolver */
111    protected AmetysObjectResolver _resolver;
112    
113    /** The right manager */
114    protected RightManager _rightManager;
115    
116    /** The current user provider */
117    protected CurrentUserProvider _currentUserProvider;
118    
119    /** The observation manager */
120    protected ObservationManager _observationManager;
121    
122    /**
123     * Enumeration for the pilotage status
124     */
125    public enum PilotageStatus
126    {
127        /** State 0 : No status */
128        NONE,
129        /** State 1 : Mention validated */
130        MENTION_VALIDATED,
131        /** State 2 : OrgUnit validated */
132        ORGUNIT_VALIDATED,
133        /** State 3 : CFVU validated */
134        CFVU_VALIDATED,
135        /** State 4 : CFVU MCC validated */
136        CFVU_MCC_VALIDATED
137    }
138    
139    @Override
140    public void service(ServiceManager manager) throws ServiceException
141    {
142        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
143        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
144        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
145        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
146        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
147    }
148    
149    /**
150     * Get all program parent from the program item 
151     * @param programItem the program item
152     * @return the set of program parent
153     */
154    public Set<Program> getParentPrograms(ProgramItem programItem)
155    {
156        Set<Program> parentPrograms = new HashSet<>();
157        if (programItem instanceof Program)
158        {
159            parentPrograms.add((Program) programItem);
160            return parentPrograms;
161        }
162        
163        List<ProgramItem> parents = _odfHelper.getParentProgramItems(programItem);
164        for (ProgramItem parent : parents)
165        {
166            if (parent instanceof Program)
167            {
168                parentPrograms.add((Program) parent);
169            }
170            else
171            {
172                parentPrograms.addAll(getParentPrograms(parent));
173            }
174        }
175        
176        return parentPrograms;
177    }
178    
179    /**
180     * Get all program parent with a pilotage status from the program item 
181     * @param programItem the program item
182     * @return the set of program parent
183     */
184    public Set<Program> getParentProgramsWithPilotageStatus(ProgramItem programItem)
185    {
186        Set<Program> parentPrograms = new HashSet<>();
187        for (Program parent : getParentPrograms(programItem))
188        {
189            PilotageStatus pilotageStatus = getPilotageStatus(parent);
190            if (!pilotageStatus.equals(PilotageStatus.NONE))
191            {
192                parentPrograms.add(parent);
193            }
194        }
195        
196        return parentPrograms;
197    }
198    
199    /**
200     * Return true if the current user has the edit super right depend on the pilotage status of the program
201     * @param program the program
202     * @return true if the user has the super right
203     */
204    public boolean hasEditSuperRight(Program program)
205    {
206        PilotageStatus pilotageStatus = getPilotageStatus(program);
207        
208        UserIdentity user = _currentUserProvider.getUser();
209        switch (pilotageStatus)
210        {
211            case NONE:
212                return true;
213            case MENTION_VALIDATED:
214                return _rightManager.hasRight(user, MENTION_VALIDATION_SUPER_RIGHT_ID, program).equals(RightResult.RIGHT_ALLOW);
215            case ORGUNIT_VALIDATED:
216                return _rightManager.hasRight(user, ORGUNIT_VALIDATION_SUPER_RIGHT_ID, program).equals(RightResult.RIGHT_ALLOW);
217            case CFVU_MCC_VALIDATED:
218            case CFVU_VALIDATED:
219            default:
220                return false;
221        }
222    }
223    
224    /**
225     * Compare two programs depends on their pilotage status
226     * Pilotage status order : NONE lower than MENTION_VALIDATED lower than ORGUNIT_VALIDATED lower than CFVU_VALIDATED lower than CFVU_VALIDATED lower than CFVU_MCC_VALIDATED
227     * -1 if pilotage status of program 1 is lower than pilotage status of program 2
228     * 0 if they have the same pilotage status
229     * 1 if pilotage status of program 1 is higher than pilotage status of program 2
230     * -2 if we don't know
231     * @param program1 program 1
232     * @param program2 progam 2
233     * @return the int compare number
234     */
235    public int comparePilotageStatus(Program program1, Program program2)
236    {
237        PilotageStatus pilotageStatus1 = getPilotageStatus(program1);
238        PilotageStatus pilotageStatus2 = getPilotageStatus(program2);
239        
240        switch (pilotageStatus1)
241        {
242            case NONE:
243                return PilotageStatus.NONE.equals(pilotageStatus2) ? 0 : 1;
244            case MENTION_VALIDATED:
245                if (PilotageStatus.NONE.equals(pilotageStatus2))
246                {
247                    return -1;
248                }
249                else
250                {
251                    return PilotageStatus.MENTION_VALIDATED.equals(pilotageStatus2) ? 0 : 1;
252                }
253            case ORGUNIT_VALIDATED:
254                if (PilotageStatus.NONE.equals(pilotageStatus2) || PilotageStatus.MENTION_VALIDATED.equals(pilotageStatus2))
255                {
256                    return -1;
257                }
258                else
259                {
260                    return PilotageStatus.ORGUNIT_VALIDATED.equals(pilotageStatus2) ? 0 : 1;
261                }
262            case CFVU_VALIDATED:
263                if (PilotageStatus.CFVU_MCC_VALIDATED.equals(pilotageStatus2))
264                {
265                    return 1;
266                }
267                else
268                {
269                    return PilotageStatus.CFVU_VALIDATED.equals(pilotageStatus2) ? 0 : -1;
270                }
271            case CFVU_MCC_VALIDATED:
272                return PilotageStatus.CFVU_MCC_VALIDATED.equals(pilotageStatus2) ? 0 : -1;
273            default:
274                break;
275        }
276        
277        return -2;
278    }
279
280    /**
281     * Get parent program or it self from program item with the higher pilotage status.
282     * Program with pilotage status NONE are ignored, so can be null if there are no program with active pilotage status.
283     * @param coursePart the course part
284     * @return the program parent
285     */
286    public Program getParentProgramWithHigherPilotageStatus(CoursePart coursePart)
287    {
288        return _getParentProgramWithHigherPilotageStatus(
289            coursePart.getCourses()
290                .stream()
291                .map(this::getParentProgramsWithPilotageStatus)
292                .flatMap(Set::stream)
293                .collect(Collectors.toSet())
294        );
295    }
296    
297    /**
298     * Get parent program or it self from program item with the higher pilotage status.
299     * Program with pilotage status NONE are ignored, so can be null if there are no program with active pilotage status.
300     * @param programItem the program item
301     * @return the program parent
302     */
303    public Program getParentProgramWithHigherPilotageStatus(ProgramItem programItem)
304    {
305        return _getParentProgramWithHigherPilotageStatus(getParentProgramsWithPilotageStatus(programItem));
306    }
307    
308    private Program _getParentProgramWithHigherPilotageStatus(Set<Program> parentProgramsWithPilotageStatus)
309    {
310        Program parentProgram = null;
311        for (Program program : parentProgramsWithPilotageStatus)
312        {
313            if (parentProgram == null || comparePilotageStatus(parentProgram, program) == 1)
314            {
315                parentProgram = program;
316            }
317        }
318        
319        return parentProgram;
320    }
321    
322    /**
323     * Get the pilotage information status for a client side element.
324     * The button is enabled if the pilotage status of root programs is NONE or if the current user has super edit right on root program of higher status
325     * @param contentId the content id
326     * @return the pilotage information 
327     */
328    public Map<String, Object> getPilotageButtonInfo(String contentId)
329    {
330        Map<String, Object> contentParams = new HashMap<>();
331        Content content = _resolver.resolveById(contentId);
332        if (content instanceof ProgramItem)
333        {
334            ProgramItem programItem = (ProgramItem) content;
335            
336            // Get parent program (or itself) with the higher pilotage status (program with no pilotage status are ignored)
337            Program parentProgramWithHigherPilotageStatus = getParentProgramWithHigherPilotageStatus(programItem);
338            boolean noParentProgramWithActivePilotageStatus = parentProgramWithHigherPilotageStatus == null;
339            
340            contentParams.put("isEnabled", noParentProgramWithActivePilotageStatus || hasEditSuperRight(parentProgramWithHigherPilotageStatus));
341            
342            if (!noParentProgramWithActivePilotageStatus)
343            {
344                // Get parent programs (or itself) which have an active pilotage status (!= NONE)
345                Set<Program> parentPrograms = getParentProgramsWithPilotageStatus(programItem);
346                List<String> programTitles = parentPrograms.stream()
347                              .map(p -> p.getTitle())
348                              .collect(Collectors.toList());
349                
350                contentParams.put("programTitles", programTitles);
351            }
352        }
353        else
354        {
355            contentParams.put("isEnabled", true);
356        }
357        
358        return contentParams;
359    }
360    
361    /**
362     * Get the pilotage status of the content
363     * @param content the content
364     * @return the pilotage status
365     */
366    public PilotageStatus getPilotageStatus(Content content)
367    {
368        String pilotageStatusDataPath = __PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __PILOTAGE_STATUS;
369        String status = content.getValue(pilotageStatusDataPath, false, PilotageStatus.NONE.name());
370        return PilotageStatus.valueOf(status);
371    }
372    
373    /**
374     * Send a notification with the content modified event.
375     * @param content The content to notify on
376     */
377    protected void _notifyPilotageWorkflowModification(Content content)
378    {
379        Map<String, Object> eventParams = new HashMap<>();
380        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
381        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
382        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
383    }
384    
385    /**
386     * Set the validation attribute (date, login, comment) to the content 
387     * @param content the content
388     * @param validationDate the validation date
389     * @param user the user
390     * @param comment the comment
391     * @param status the pilotage status
392     */
393    public void setValidationAttribute(ModifiableDefaultContent content, LocalDate validationDate, UserIdentity user, String comment, PilotageStatus status)
394    {
395        boolean hasChanges = false;
396        switch (status)
397        {
398            case NONE :
399                // Do nothing
400                break;
401            case MENTION_VALIDATED :
402                hasChanges = setMentionValidationAttribute(content, validationDate, user, comment);
403                break;
404            case ORGUNIT_VALIDATED :
405                hasChanges = setOrgUnitValidationAttribute(content, validationDate, user, comment);
406                break;
407            case CFVU_VALIDATED :
408                hasChanges = setCFVUValidationAttribute(content, validationDate, user, comment);
409                break;
410            case CFVU_MCC_VALIDATED :
411                hasChanges = setCFVUMCCValidationAttribute(content, validationDate, user, comment);
412                break;
413            default :
414                getLogger().error("{} is an unknown pilotage status", status);
415        }
416        
417        if (hasChanges)
418        {
419            _notifyPilotageWorkflowModification(content);
420        }
421    }
422    
423    /**
424     * Remove the validation attribute from the content
425     * @param content the content
426     * @param status the pilotage status
427     */
428    public void removePilotageStatus(ModifiableDefaultContent content, PilotageStatus status)
429    {
430        boolean hasChanges = false;
431        switch (status)
432        {
433            case NONE :
434                // Do nothing
435                break;
436            case MENTION_VALIDATED :
437                hasChanges = removeMentionValidationAttribute(content);
438                break;
439            case ORGUNIT_VALIDATED :
440                hasChanges = removeOrgUnitValidationAttribute(content);
441                break;
442            case CFVU_VALIDATED :
443                hasChanges = removeCFVUValidationAttribute(content);
444                break;
445            case CFVU_MCC_VALIDATED :
446                hasChanges = removeCFVUMCCValidationAttribute(content);
447                break;
448            default :
449                getLogger().error("{} is an unknown pilotage status", status);
450        }
451        
452        if (hasChanges)
453        {
454            _notifyPilotageWorkflowModification(content);
455        }
456    }
457    
458    /**
459     * Set the validation attribute for 'mention validated' state
460     * @param content the content
461     * @param validationDate the validation date
462     * @param user the user
463     * @param comment the comment
464     * @return <code>true</code> if the content has changed.
465     */
466    public boolean setMentionValidationAttribute(ModifiableDefaultContent content, LocalDate validationDate, UserIdentity user, String comment)
467    {
468        ModifiableModelAwareComposite composite = content.getComposite(__PILOTAGE_COMPOSITE, true);
469        composite.setValue(__MENTION_VALIDATION_DATE, validationDate);
470        composite.setValue(__MENTION_VALIDATION_AUTHOR, user);
471        composite.setValue(__MENTION_VALIDATION_COMMENT, comment);
472        
473        composite.setValue(__PILOTAGE_STATUS, PilotageStatus.MENTION_VALIDATED.name());
474        
475        return saveContent(content);
476    }
477    
478    
479    /**
480     * Remove validation attribute for 'mention validated' state
481     * @param content the content
482     * @return <code>true</code> if the content has changed.
483     */
484    public boolean removeMentionValidationAttribute(ModifiableDefaultContent content)
485    {
486        // Remove the first step
487        return removePilotageWorkflow(content);
488    }
489    
490    /**
491     * Set the validation attribute for 'orgunit validated' state
492     * @param content the content
493     * @param validationDate the validation date
494     * @param user the user
495     * @param comment the comment
496     * @return <code>true</code> if the content has changed.
497     */
498    public boolean setOrgUnitValidationAttribute(ModifiableDefaultContent content, LocalDate validationDate, UserIdentity user, String comment)
499    {
500        ModifiableModelAwareComposite composite = content.getComposite(__PILOTAGE_COMPOSITE, true);
501        composite.setValue(__ORGUNIT_VALIDATION_DATE, validationDate);
502        composite.setValue(__ORGUNIT_VALIDATION_AUTHOR, user);
503        composite.setValue(__ORGUNIT_VALIDATION_COMMENT, comment);
504        
505        composite.setValue(__PILOTAGE_STATUS, PilotageStatus.ORGUNIT_VALIDATED.name());
506
507        return saveContent(content);
508    }
509    
510    /**
511     * Remove validation attribute for 'orgunit validated' state
512     * @param content the content
513     * @return <code>true</code> if the content has changed.
514     */
515    public boolean removeOrgUnitValidationAttribute(ModifiableDefaultContent content)
516    {
517        ModifiableModelAwareComposite compositeMetadata = content.getComposite(__PILOTAGE_COMPOSITE, true);
518        compositeMetadata.removeValue(__ORGUNIT_VALIDATION_DATE);
519        compositeMetadata.removeValue(__ORGUNIT_VALIDATION_AUTHOR);
520        compositeMetadata.removeValue(__ORGUNIT_VALIDATION_COMMENT);
521        
522        compositeMetadata.setValue(__PILOTAGE_STATUS, PilotageStatus.MENTION_VALIDATED.name());
523
524        return saveContent(content);
525    }
526    
527    /**
528     * Set the validation attribute for 'CFVU validated' state
529     * @param content the content
530     * @param validationDate the validation date
531     * @param user the login
532     * @param comment the comment
533     * @return <code>true</code> if the content has changed.
534     */
535    public boolean setCFVUValidationAttribute(ModifiableDefaultContent content, LocalDate validationDate, UserIdentity user, String comment)
536    {
537        ModifiableModelAwareComposite composite = content.getComposite(__PILOTAGE_COMPOSITE, true);
538        composite.setValue(__CFVU_VALIDATION_DATE, validationDate);
539        composite.setValue(__CFVU_VALIDATION_AUTHOR, user);
540        composite.setValue(__CFVU_VALIDATION_COMMENT, comment);
541        
542        composite.setValue(__PILOTAGE_STATUS, PilotageStatus.CFVU_VALIDATED.name());
543
544        return saveContent(content);
545    }
546    
547    /**
548     * Remove validation attribute for 'CFVU validated' state
549     * @param content the content
550     * @return <code>true</code> if the content has changed.
551     */
552    public boolean removeCFVUValidationAttribute(ModifiableDefaultContent content)
553    {
554        ModifiableModelAwareComposite composite = content.getComposite(__PILOTAGE_COMPOSITE, true);
555        composite.removeValue(__CFVU_VALIDATION_DATE);
556        composite.removeValue(__CFVU_VALIDATION_AUTHOR);
557        composite.removeValue(__CFVU_VALIDATION_COMMENT);
558        
559        composite.setValue(__PILOTAGE_STATUS, PilotageStatus.ORGUNIT_VALIDATED.name());
560
561        return saveContent(content);
562    }
563    
564    /**
565     * Set the validation attribute for 'CFVU MCC validated' state
566     * @param content the content
567     * @param validationDate the validation date
568     * @param user the user
569     * @param comment the comment
570     * @return <code>true</code> if the content has changed.
571     */
572    public boolean setCFVUMCCValidationAttribute(ModifiableDefaultContent content, LocalDate validationDate, UserIdentity user, String comment)
573    {
574        ModifiableModelAwareComposite composite = content.getComposite(__PILOTAGE_COMPOSITE, true);
575        composite.setValue(__CFVU_MCC_VALIDATION_DATE, validationDate);
576        composite.setValue(__CFVU_MCC_VALIDATION_AUTHOR, user);
577        composite.setValue(__CFVU_MCC_VALIDATION_COMMENT, comment);
578        
579        composite.setValue(__PILOTAGE_STATUS, PilotageStatus.CFVU_MCC_VALIDATED.name());
580
581        return saveContent(content);
582    }
583    
584    /**
585     * Remove validation attribute for 'CFVU MCC validated' state
586     * @param content the content
587     * @return <code>true</code> if the content has changed.
588     */
589    public boolean removeCFVUMCCValidationAttribute(ModifiableDefaultContent content)
590    {
591        ModifiableModelAwareComposite composite = content.getComposite(__PILOTAGE_COMPOSITE, true);
592        composite.removeValue(__CFVU_MCC_VALIDATION_DATE);
593        composite.removeValue(__CFVU_MCC_VALIDATION_AUTHOR);
594        composite.removeValue(__CFVU_MCC_VALIDATION_COMMENT);
595        
596        composite.setValue(__PILOTAGE_STATUS, PilotageStatus.CFVU_VALIDATED.name());
597
598        return saveContent(content);
599    }
600    
601    /**
602     * Get mention validation date
603     * @param content the content
604     * @return the validation date
605     */
606    public LocalDate getMentionValidationDate(Content content)
607    {
608        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __MENTION_VALIDATION_DATE);
609    }
610    
611    /**
612     * Get mention validation comment
613     * @param content the content
614     * @return the validation comment
615     */
616    public String getMentionValidationComment(Content content)
617    {
618        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __MENTION_VALIDATION_COMMENT);
619    }
620    
621    /**
622     * Get mention validation author
623     * @param content the content
624     * @return the validation author
625     */
626    public UserIdentity getMentionValidationAuthor(Content content)
627    {
628        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __MENTION_VALIDATION_AUTHOR);
629    }
630    
631    /**
632     * Get orgUnit validation date
633     * @param content the content
634     * @return the validation date
635     */
636    public LocalDate getOrgUnitValidationDate(Content content)
637    {
638        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __ORGUNIT_VALIDATION_DATE);
639    }
640    
641    /**
642     * Get orgUnit validation comment
643     * @param content the content
644     * @return the validation comment
645     */
646    public String getOrgUnitValidationComment(Content content)
647    {
648        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __ORGUNIT_VALIDATION_COMMENT);
649    }
650    
651    /**
652     * Get orgUnit validation author
653     * @param content the content
654     * @return the validation author
655     */
656    public UserIdentity getOrgUnitValidationAuthor(Content content)
657    {
658        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __ORGUNIT_VALIDATION_AUTHOR);
659    }
660    
661    /**
662     * Get CFVU validation date
663     * @param content the content
664     * @return the validation date
665     */
666    public LocalDate getCFVUValidationDate(Content content)
667    {
668        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __CFVU_VALIDATION_DATE);
669    }
670    
671    /**
672     * Get CFVU validation comment
673     * @param content the content
674     * @return the validation comment
675     */
676    public String getCFVUValidationComment(Content content)
677    {
678        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __CFVU_VALIDATION_COMMENT);
679    }
680    
681    /**
682     * Get CFVU validation author
683     * @param content the content
684     * @return the validation author
685     */
686    public UserIdentity getCFVUValidationAuthor(Content content)
687    {
688        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __CFVU_VALIDATION_AUTHOR);
689    }
690    
691    /**
692     * Get CFVU MMC validation date
693     * @param content the content
694     * @return the validation date
695     */
696    public LocalDate getCFVUMCCValidationDate(Content content)
697    {
698        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __CFVU_MCC_VALIDATION_DATE);
699    }
700    
701    /**
702     * Get CFVU MCC validation comment
703     * @param content the content
704     * @return the validation comment
705     */
706    public String getCFVUMCCValidationComment(Content content)
707    {
708        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __CFVU_MCC_VALIDATION_COMMENT);
709    }
710    
711    /**
712     * Get CFVU MCC validation author
713     * @param content the content
714     * @return the validation author
715     */
716    public UserIdentity getCFVUMCCValidationAuthor(Content content)
717    {
718        return content.getValue(__PILOTAGE_COMPOSITE + ModelItem.ITEM_PATH_SEPARATOR + __CFVU_MCC_VALIDATION_AUTHOR);
719    }
720    
721    /**
722     * Remove the pilotage workflow metadata.
723     * @param content The content to clean
724     * @return <code>true</code> if the content has changed
725     */
726    public boolean removePilotageWorkflow(ModifiableDefaultContent content)
727    {
728        content.removeValue(__PILOTAGE_COMPOSITE);
729        return saveContent(content);
730    }
731    
732    /**
733     * Save the content if needed, add a version (checkpoint) and move the Live label if the last version was validated.
734     * @param content The content to save
735     * @return <code>true</code> if the content has changed
736     */
737    protected boolean saveContent(ModifiableDefaultContent content)
738    {
739        if (content.needsSave())
740        {
741            boolean currentVersionIsLive = ArrayUtils.contains(content.getLabels(), CmsConstants.LIVE_LABEL);
742            
743            content.saveChanges();
744            content.checkpoint();
745            
746            // Move the Live label if the last version was validated.
747            if (currentVersionIsLive)
748            {
749                content.addLabel(CmsConstants.LIVE_LABEL, true);
750            }
751            
752            return true;
753        }
754        
755        return false;
756    }
757}