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;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.function.Function;
028import java.util.stream.Collectors;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.context.Context;
032import org.apache.avalon.framework.context.ContextException;
033import org.apache.avalon.framework.context.Contextualizable;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037import org.apache.cocoon.components.ContextHelper;
038import org.apache.cocoon.environment.Request;
039import org.apache.commons.lang.StringUtils;
040import org.apache.commons.lang3.ArrayUtils;
041import org.apache.commons.lang3.tuple.Pair;
042
043import org.ametys.cms.CmsConstants;
044import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
045import org.ametys.cms.data.ContentDataHelper;
046import org.ametys.cms.repository.Content;
047import org.ametys.cms.repository.ContentQueryHelper;
048import org.ametys.cms.repository.ContentTypeExpression;
049import org.ametys.cms.repository.DefaultContent;
050import org.ametys.cms.repository.LanguageExpression;
051import org.ametys.cms.repository.ModifiableContent;
052import org.ametys.cms.repository.ModifiableWorkflowAwareContent;
053import org.ametys.cms.workflow.ContentWorkflowHelper;
054import org.ametys.core.observation.ObservationManager;
055import org.ametys.core.ui.Callable;
056import org.ametys.core.user.CurrentUserProvider;
057import org.ametys.odf.course.Course;
058import org.ametys.odf.course.CourseContainer;
059import org.ametys.odf.course.ShareableCourseHelper;
060import org.ametys.odf.courselist.CourseList;
061import org.ametys.odf.courselist.CourseList.ChoiceType;
062import org.ametys.odf.courselist.CourseListContainer;
063import org.ametys.odf.coursepart.CoursePart;
064import org.ametys.odf.coursepart.CoursePartFactory;
065import org.ametys.odf.orgunit.OrgUnit;
066import org.ametys.odf.orgunit.OrgUnitFactory;
067import org.ametys.odf.orgunit.RootOrgUnitProvider;
068import org.ametys.odf.program.AbstractProgram;
069import org.ametys.odf.program.AbstractTraversableProgramPart;
070import org.ametys.odf.program.Container;
071import org.ametys.odf.program.Program;
072import org.ametys.odf.program.ProgramFactory;
073import org.ametys.odf.program.ProgramPart;
074import org.ametys.odf.program.SubProgram;
075import org.ametys.odf.program.TraversableProgramPart;
076import org.ametys.plugins.repository.AmetysObject;
077import org.ametys.plugins.repository.AmetysObjectExistsException;
078import org.ametys.plugins.repository.AmetysObjectIterable;
079import org.ametys.plugins.repository.AmetysObjectIterator;
080import org.ametys.plugins.repository.AmetysObjectResolver;
081import org.ametys.plugins.repository.AmetysRepositoryException;
082import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
083import org.ametys.plugins.repository.RepositoryConstants;
084import org.ametys.plugins.repository.UnknownAmetysObjectException;
085import org.ametys.plugins.repository.collection.AmetysObjectCollection;
086import org.ametys.plugins.repository.data.external.ExternalizableDataProviderExtensionPoint;
087import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
088import org.ametys.plugins.repository.query.QueryHelper;
089import org.ametys.plugins.repository.query.SortCriteria;
090import org.ametys.plugins.repository.query.expression.AndExpression;
091import org.ametys.plugins.repository.query.expression.Expression;
092import org.ametys.plugins.repository.query.expression.Expression.Operator;
093import org.ametys.plugins.repository.query.expression.OrExpression;
094import org.ametys.plugins.repository.query.expression.StringExpression;
095import org.ametys.runtime.i18n.I18nizableText;
096import org.ametys.runtime.plugin.component.AbstractLogEnabled;
097import org.ametys.runtime.plugin.component.PluginAware;
098
099import com.opensymphony.workflow.WorkflowException;
100
101/**
102 * Helper for ODF contents
103 *
104 */
105public class ODFHelper extends AbstractLogEnabled implements Component, Serviceable, PluginAware, Contextualizable
106{
107    /** The component role. */
108    public static final String ROLE = ODFHelper.class.getName();
109    
110    /** Request attribute to get the "Live" version of contents */
111    public static final String REQUEST_ATTRIBUTE_VALID_LABEL = "live-version";
112    
113    /** The default id of initial workflow action */
114    protected static final int __INITIAL_WORKFLOW_ACTION_ID = 0;
115    
116    /** Ametys object resolver */
117    protected AmetysObjectResolver _resolver;
118    /** The content workflow helper */
119    protected ContentWorkflowHelper _contentWorkflowHelper;
120    /** The content types manager */
121    protected ContentTypeExtensionPoint _cTypeEP;
122    /** The observation manager */
123    protected ObservationManager _observationManager;
124    /** The current user provider */
125    protected CurrentUserProvider _currentUserProvider;
126    /** Root orgunit */
127    protected RootOrgUnitProvider _ouRootProvider;
128    /** Provider for externalizable metadata */
129    protected ExternalizableDataProviderExtensionPoint _externalizableDataProviderEP;
130    /** Helper for shareable course */
131    protected ShareableCourseHelper _shareableCourseHelper;
132    /** The Avalon context */
133    protected Context _context;
134    
135    private String _pluginName;
136
137    public void contextualize(Context context) throws ContextException
138    {
139        _context = context;
140    }
141    
142    @Override
143    public void service(ServiceManager manager) throws ServiceException
144    {
145        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
146        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
147        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
148        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
149        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
150        _ouRootProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE);
151        _externalizableDataProviderEP = (ExternalizableDataProviderExtensionPoint) manager.lookup(ExternalizableDataProviderExtensionPoint.ROLE);
152        _shareableCourseHelper = (ShareableCourseHelper) manager.lookup(ShareableCourseHelper.ROLE);
153    }
154    
155    @Override
156    public void setPluginInfo(String pluginName, String featureName, String id)
157    {
158        _pluginName = pluginName;
159    }
160    
161    /**
162     * Gets the root for ODF contents
163     * @return the root for ODF contents
164     */
165    public AmetysObjectCollection getRootContent()
166    {
167        return getRootContent(false);
168    }
169    
170    /**
171     * Gets the root for ODF contents
172     * @param create <code>true</code> to create automatically the root when missing.
173     * @return the root for ODF contents
174     */
175    public AmetysObjectCollection getRootContent(boolean create)
176    {
177        ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/");
178        
179        boolean needSave = false;
180        if (!pluginsNode.hasChild(_pluginName))
181        {
182            if (create)
183            {
184                pluginsNode.createChild(_pluginName, "ametys:unstructured");
185                needSave = true;
186            }
187            else
188            {
189                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "' is missing");
190            }
191        }
192        
193        ModifiableTraversableAmetysObject pluginNode = pluginsNode.getChild(_pluginName);
194        if (!pluginNode.hasChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents"))
195        {
196            if (create)
197            {
198                pluginNode.createChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents", "ametys:collection");
199                needSave = true;
200            }
201            else
202            {
203                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "/ametys:contents' is missing");
204            }
205        }
206        
207        if (needSave)
208        {
209            pluginsNode.saveChanges();
210        }
211        
212        return pluginNode.getChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents");
213    }
214    
215    /**
216     * Get the {@link ProgramItem}s matching the given arguments
217     * @param cTypeId The id of content type. Can be null to get program's items whatever their content type.
218     * @param code The code. Can be null to get program's items regardless of their code
219     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
220     * @param lang The search language. Can be null to get program's items regardless of their language
221     * @param <C> The content return type 
222     * @return The matching program items
223     */
224    public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang)
225    {
226        return getProgramItems(cTypeId, code, catalogName, lang, null, null);
227    }
228    
229    /**
230     * Get the {@link ProgramItem}s matching the given arguments
231     * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type.
232     * @param code The code. Can be null to get program's items regardless of their code
233     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
234     * @param lang The search language. Can be null to get program's items regardless of their language
235     * @param <C> The content return type 
236     * @return The matching program items
237     */
238    public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang)
239    {
240        return getProgramItems(cTypeIds, code, catalogName, lang, null, null);
241    }
242    
243    /**
244     * Get the {@link ProgramItem}s matching the given arguments
245     * @param cTypeId The id of content type. Can be null to get program's items whatever their content type.
246     * @param code The code. Can be null to get program's items regardless of their code
247     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
248     * @param lang The search language. Can be null to get program's items regardless of their language
249     * @param additionnalExpr An additional expression for filtering result. Can be null
250     * @param sortCriteria criteria for sorting results
251     * @param <C> The content return type 
252     * @return The matching program items
253     */
254    public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria)
255    {
256        return getProgramItems(cTypeId != null ? Collections.singletonList(cTypeId) : Collections.EMPTY_LIST, code, catalogName, lang, additionnalExpr, sortCriteria);
257    }
258    
259    /**
260     * Get the {@link ProgramItem}s matching the given arguments
261     * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type.
262     * @param code The code. Can be null to get program's items regardless of their code
263     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
264     * @param lang The search language. Can be null to get program's items regardless of their language
265     * @param additionnalExpr An additional expression for filtering result. Can be null
266     * @param sortCriteria criteria for sorting results
267     * @param <C> The content return type 
268     * @return The matching program items
269     */
270    public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria)
271    {
272        List<Expression> exprs = new ArrayList<>();
273        
274        if (!cTypeIds.isEmpty())
275        {
276            exprs.add(new ContentTypeExpression(Operator.EQ, cTypeIds.toArray(new String[cTypeIds.size()])));
277        }
278        if (StringUtils.isNotEmpty(code))
279        {
280            exprs.add(new StringExpression(ProgramItem.CODE, Operator.EQ, code));
281        }
282        if (StringUtils.isNotEmpty(catalogName))
283        {
284            exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName));
285        }
286        if (StringUtils.isNotEmpty(lang))
287        {
288            exprs.add(new LanguageExpression(Operator.EQ, lang));
289        }
290        if (additionnalExpr != null)
291        {
292            exprs.add(additionnalExpr);
293        }
294        
295        Expression expr = new AndExpression(exprs.toArray(new Expression[exprs.size()]));
296        
297        String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr, sortCriteria);
298        return _resolver.query(xpathQuery);
299    }
300    
301    /**
302     * Get the equivalent {@link CoursePart} of the source {@link CoursePart} in given catalog and language 
303     * @param srcCoursePart The source course part
304     * @param catalogName The name of catalog to search into
305     * @param lang The search language
306     * @return The equivalent program item or <code>null</code> if not exists
307     */
308    public CoursePart getCoursePart(CoursePart srcCoursePart, String catalogName, String lang)
309    {
310        return getODFContent(CoursePartFactory.COURSE_PART_CONTENT_TYPE, srcCoursePart.getCode(), catalogName, lang);
311    }
312    
313    /**
314     * Get the equivalent {@link ProgramItem} of the source {@link ProgramItem} in given catalog and language 
315     * @param <T> The type of returned object, it have to be a subclass of {@link ProgramItem}
316     * @param srcProgramItem The source program item
317     * @param catalogName The name of catalog to search into
318     * @param lang The search language
319     * @return The equivalent program item or <code>null</code> if not exists
320     */
321    public <T extends ProgramItem> T getProgramItem(T srcProgramItem, String catalogName, String lang)
322    {
323        return getODFContent(((Content) srcProgramItem).getTypes()[0], srcProgramItem.getCode(), catalogName, lang);
324    }
325    
326    /**
327     * Get the equivalent {@link Content} having the same code in given catalog and language 
328     * @param <T> The type of returned object, it have to be a subclass of {@link AmetysObject}
329     * @param contentType The content type to search for
330     * @param odfContentCode The code of the ODF content
331     * @param catalogName The name of catalog to search into
332     * @param lang The search language
333     * @return The equivalent content or <code>null</code> if not exists
334     */
335    public <T extends AmetysObject> T getODFContent(String contentType, String odfContentCode, String catalogName, String lang)
336    {
337        Expression contentTypeExpr = new ContentTypeExpression(Operator.EQ, contentType);
338        Expression langExpr = new LanguageExpression(Operator.EQ, lang);
339        Expression catalogExpr = new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName);
340        Expression codeExpr = new StringExpression(ProgramItem.CODE, Operator.EQ, odfContentCode);
341        
342        Expression expr = new AndExpression(contentTypeExpr, langExpr, catalogExpr, codeExpr);
343        
344        String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr);
345        AmetysObjectIterable<T> contents = _resolver.query(xpathQuery);
346        AmetysObjectIterator<T> contentsIt = contents.iterator();
347        if (contentsIt.hasNext())
348        {
349            return contentsIt.next();
350        }
351        
352        return null;
353    }
354    
355    /**
356     * Get the child program items of a {@link ProgramItem}
357     * @param programItem The program item
358     * @return The child program items 
359     */
360    public List<ProgramItem> getChildProgramItems(ProgramItem programItem)
361    {
362        List<ProgramItem> children = new ArrayList<>();
363        
364        if (programItem instanceof TraversableProgramPart)
365        {
366            children.addAll(((TraversableProgramPart) programItem).getProgramPartChildren());
367        }
368        
369        if (programItem instanceof CourseContainer)
370        {
371            children.addAll(((CourseContainer) programItem).getCourses());
372        }
373        
374        if (programItem instanceof Course)
375        {
376            children.addAll(((Course) programItem).getCourseLists());
377        }
378        
379        return children;
380    }
381
382    /**
383     * Get the child subprograms of a {@link ProgramPart}
384     * @param programPart The program part
385     * @return The child subprograms
386     */
387    public Set<SubProgram> getChildSubPrograms(ProgramPart programPart)
388    {
389        Set<SubProgram> subPrograms = new HashSet<>();
390        
391        if (programPart instanceof TraversableProgramPart)
392        {
393            if (programPart instanceof SubProgram)
394            {
395                subPrograms.add((SubProgram) programPart);
396            }
397            ((TraversableProgramPart) programPart).getProgramPartChildren().forEach(child -> subPrograms.addAll(getChildSubPrograms(child)));
398        }
399
400        return subPrograms;
401    }
402    
403    /**
404     * Gets (recursively) parent containers of this program item.
405     * @param programItem The program item
406     * @return parent containers of this program item.
407     */
408    public Set<Container> getParentContainers(ProgramItem programItem)
409    {
410        return _getParentsOfType(programItem, Container.class);
411    }
412    
413    /**
414     * Gets (recursively) parent programs of this program item.
415     * @param programItem The program item
416     * @return parent programs of this program item.
417     */
418    public Set<Program> getParentPrograms(ProgramItem programItem)
419    {
420        return _getParentsOfType(programItem, Program.class);
421    }
422    
423    /**
424     * Gets (recursively) parent abstract programs of this program item.
425     * @param programItem The program item
426     * @return parent abstract programs of this program item.
427     */
428    public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem)
429    {
430        return _getParentsOfType(programItem, AbstractProgram.class);
431    }
432
433    private <T> Set<T> _getParentsOfType(ProgramItem programItem, Class<T> classToTest)
434    {
435        Set<ProgramItem> visitedProgramItems = new HashSet<>();
436        visitedProgramItems.add(programItem);
437        return _getParentsOfType(programItem, visitedProgramItems, classToTest);
438    }
439    
440    @SuppressWarnings("unchecked")
441    private <T> Set<T> _getParentsOfType(ProgramItem programItem, Set<ProgramItem> visitedProgramItems, Class<T> classToTest)
442    {
443        Set<T> parentsOfType = new HashSet<>();
444        List<ProgramItem> parents = getParentProgramItems(programItem);
445        
446        for (ProgramItem parent : parents)
447        {
448            // Only parents not already visited
449            if (visitedProgramItems.add(parent))
450            {
451                // Cast to Content if instance of Content instead of another type (for structures containing both Container and SubProgram)
452                if (classToTest.isInstance(parent))
453                {
454                    parentsOfType.add((T) parent);
455                }
456                else
457                {
458                    parentsOfType.addAll(_getParentsOfType(parent, visitedProgramItems, classToTest));
459                }
460            }
461        }
462        
463        return parentsOfType;
464    }
465    
466    /**
467     * Get the child programs of an {@link OrgUnit}
468     * @param orgUnit the orgUnit, can be null
469     * @param catalog the catalog
470     * @param lang the lang
471     * @return The child programs
472     */
473    public List<Program> getProgramsFromOrgUnit(OrgUnit orgUnit, String catalog, String lang)
474    {
475        List<Program> programs = new ArrayList<>();
476        List<Expression> programExpressions = new ArrayList<>();
477        programExpressions.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE));
478       
479        programExpressions.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
480        programExpressions.add(new LanguageExpression(Operator.EQ, lang));
481        
482        // Can be null, it means that all programs for catalog and lang are selected
483        if (orgUnit != null)
484        {
485            List<Expression> expressions = new ArrayList<>();
486            for (String orgUnitId : getSubOrgUnitIds(orgUnit))
487            {
488                expressions.add(new StringExpression(AbstractProgram.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId));
489            }
490            
491            programExpressions.add(new OrExpression(expressions.toArray(new Expression[0])));
492        }
493        
494        String programQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, new AndExpression(programExpressions.toArray(new Expression[0])));
495        AmetysObjectIterable<Program> programsIterable = _resolver.query(programQuery);
496        AmetysObjectIterator<Program> programsIterator = programsIterable.iterator();
497        while (programsIterator.hasNext())
498        {
499            programs.add(programsIterator.next());
500        }
501        return programs;
502    }
503    
504    /**
505     * Get the current orgunit and its suborgunits recursively identifiers.
506     * @param orgUnit The orgunit at the top
507     * @return A {@link List} of {@link OrgUnit} ids
508     */
509    public List<String> getSubOrgUnitIds(OrgUnit orgUnit)
510    {
511        List<String> orgUnitIds = new ArrayList<>();
512        orgUnitIds.add(orgUnit.getId());
513        for (String id : orgUnit.getSubOrgUnits())
514        {
515            OrgUnit childOrgUnit = _resolver.resolveById(id);
516            orgUnitIds.addAll(getSubOrgUnitIds(childOrgUnit));
517        }
518        
519        return orgUnitIds;
520    }
521    
522    /**
523     * Determines if the {@link ProgramItem} has parent program items 
524     * @param programItem The program item
525     * @return true if has parent program items 
526     */
527    public boolean hasParentProgramItems(ProgramItem programItem)
528    {
529        boolean hasParent = false;
530        
531        if (programItem instanceof ProgramPart)
532        {
533            hasParent = !((ProgramPart) programItem).getProgramPartParents().isEmpty() || hasParent;
534        }
535        
536        if (programItem instanceof CourseList)
537        {
538            hasParent = !((CourseList) programItem).getParentCourses().isEmpty() || hasParent;
539        }
540        
541        if (programItem instanceof Course)
542        {
543            hasParent = !((Course) programItem).getParentCourseLists().isEmpty() || hasParent;
544        }
545        
546        return hasParent;
547    }
548    
549    /**
550     * Get the parent program items of a {@link ProgramItem}
551     * @param programItem The program item
552     * @return The parent program items 
553     */
554    public List<ProgramItem> getParentProgramItems(ProgramItem programItem)
555    {
556        List<ProgramItem> parents = new ArrayList<>();
557        
558        if (programItem instanceof ProgramPart)
559        {
560            parents.addAll(((ProgramPart) programItem).getProgramPartParents());
561        }
562        
563        if (programItem instanceof CourseList)
564        {
565            parents.addAll(((CourseList) programItem).getParentCourses());
566        }
567        
568        if (programItem instanceof Course)
569        {
570            parents.addAll(((Course) programItem).getParentCourseLists());
571        }
572        
573        return parents;
574    }
575    
576    /**
577     * Get the nearest program item parent into the given parent {@link AbstractProgram}
578     * @param programItem The program item
579     * @param parentProgram The parent program or subprogram. If null, the nearest abstract program will be returned.
580     * @return The parent program item or null if not found.
581     */
582    public ProgramItem getParentProgramItem (ProgramItem programItem, AbstractProgram parentProgram)
583    {
584        if (programItem instanceof Program)
585        {
586            return null;
587        }
588        
589        if (programItem instanceof ProgramPart)
590        {
591            List<ProgramPart> parents = ((ProgramPart) programItem).getProgramPartParents();
592            
593            for (ProgramPart parent : parents)
594            {
595                if (parent instanceof AbstractProgram && (parentProgram == null || parent.equals(parentProgram)))
596                {
597                    return parent;
598                }
599                else
600                {
601                    ProgramItem ancestor = getParentProgramItem(parent, parentProgram);
602                    if (ancestor != null)
603                    {
604                        return parent;
605                    }
606                }
607            }
608        }
609        
610        if (programItem instanceof CourseList)
611        {
612            for (Course parentCourse : ((CourseList) programItem).getParentCourses())
613            {
614                ProgramItem ancestor = getParentProgramItem(parentCourse, parentProgram);
615                if (ancestor != null)
616                {
617                    return parentCourse;
618                }
619            }
620        }
621        
622        if (programItem instanceof Course)
623        {
624            for (CourseList cl : ((Course) programItem).getParentCourseLists())
625            {
626                ProgramItem ancestor = getParentProgramItem(cl, parentProgram);
627                if (ancestor != null)
628                {
629                    return cl;
630                }
631            }
632        }
633        
634        return null;
635    }
636    
637    /**
638     * Get information of the program item structure (type, if program has children)
639     * @param programItemId the program item id
640     * @return a map of information
641     */
642    @Callable
643    public Map<String, Object> getStructureInfo(String programItemId)
644    {
645        Map<String, Object> results = new HashMap<>();
646        
647        if (StringUtils.isNotBlank(programItemId))
648        {
649            Content content = _resolver.resolveById(programItemId);
650            if (content instanceof ProgramItem)
651            {
652                results.put("id", programItemId);
653                results.put("title", content.getTitle());
654                results.put("code", ((ProgramItem) content).getCode());
655                
656                List<ProgramItem> childProgramItems = getChildProgramItems((ProgramItem) content);
657                results.put("hasChildren", !childProgramItems.isEmpty());
658                
659                List<ProgramItem> parentProgramItems = getParentProgramItems((ProgramItem) content);
660                results.put("hasParent", !parentProgramItems.isEmpty());
661                
662                results.put("paths", getPaths((ProgramItem) content, " > "));
663            }
664        }
665        
666        return results;
667    }
668    
669    /**
670     * Get information of the program item structure (type, if program has children)
671     * @param programItemIds the list of program item id
672     * @return a map of information
673     */
674    @Callable
675    public Map<String, Map<String, Object>> getStructureInfo(List<String> programItemIds)
676    {
677        Map<String,  Map<String, Object>> results = new HashMap<>();
678        
679        for (String programItemId : programItemIds)
680        {
681            results.put(programItemId, getStructureInfo(programItemId));
682        }
683        
684        return results;
685    }
686    
687    /**
688     * Get all the paths of a ODF content.<br>
689     * The path is built with the contents' title and code
690     * @param item The program item
691     * @param separator The path separator
692     * @return the paths in parent program items
693     */
694    public List<String> getPaths(ProgramItem item, String separator)
695    {
696        Function<ProgramItem, String> mapper = c -> ((Content) c).getTitle() + " (" + c.getCode() + ")";
697        return getPaths(item, separator, mapper, true);
698    }
699    
700    /**
701     * Get all the paths of a ODF content.<br>
702     * The path is built with the mapper function.
703     * @param item The program item
704     * @param separator The path separator
705     * @param mapper the function to apply to each program item to build the path
706     * @param includeItseft set to false to not include final item in path
707     * @return the paths in parent program items
708     */
709    public List<String> getPaths(ProgramItem item, String separator, Function<ProgramItem, String> mapper, boolean includeItseft)
710    {
711        List<String> paths = new ArrayList<>();
712        
713        List<List<ProgramItem>> ancestorPaths = getPathOfAncestors(item);
714        for (List<ProgramItem> ancestorPath : ancestorPaths)
715        {
716            if (!includeItseft)
717            {
718                ancestorPath.remove(item);
719            }
720            if (!ancestorPath.isEmpty())
721            {
722                List<String> titles = ancestorPath.stream().map(mapper).collect(Collectors.toList());
723                paths.add(String.join(separator, titles));
724            }
725        }
726        
727        return paths;
728    }
729    
730    
731    /**
732     * Get the full path to program item for highest ancestors. The path includes this final item.
733     * @param item the program item
734     * @return a list for each highest ancestors found. Each item of the list contains the program items to the path to this program item.
735     */
736    public List<List<ProgramItem>> getPathOfAncestors(ProgramItem item)
737    {
738        List<List<ProgramItem>> ancestors = new ArrayList<>();
739        
740        List<ProgramItem> parentProgramItems = getParentProgramItems(item);
741        if (parentProgramItems.isEmpty())
742        {
743            List<ProgramItem> items = new ArrayList<>();
744            items.add(item);
745            ancestors.add(items);
746            return ancestors;
747        }
748        
749        for (ProgramItem parentProgramItem : parentProgramItems)
750        {
751            for (List<ProgramItem> ancestorPaths : getPathOfAncestors(parentProgramItem))
752            {
753                ancestorPaths.add(item);
754                ancestors.add(ancestorPaths);
755            }
756        }
757        
758        return ancestors;
759    }
760    
761    /**
762     * Get the path of a {@link ProgramItem} into a {@link Program}<br>
763     * The path is construct with the contents' names and the used separator is '/'.
764     * @param programItemId The id of the program item
765     * @param programId The id of program. Can not be null.
766     * @return the path into the parent program or null if the item is not part of this program.
767     */
768    @Callable
769    public String getPathInProgram (String programItemId, String programId)
770    {
771        ProgramItem item = _resolver.resolveById(programItemId);
772        Program program = _resolver.resolveById(programId);
773        
774        return getPathInProgram(item, program);
775    }
776    
777    /**
778     * Get the path of a ODF content into a {@link Program}.<br>
779     * The path is construct with the contents' names and the used separator is '/'.
780     * @param item The program item
781     * @param parentProgram The parent root (sub)program. Can not be null.
782     * @return the path from the parent program
783     */
784    public String getPathInProgram (ProgramItem item, Program parentProgram)
785    {
786        if (item instanceof Program)
787        {
788            // The program item is already the program it self or another program
789            return item.equals(parentProgram) ? "" : null;
790        }
791        
792        List<String> paths = new ArrayList<>();
793        paths.add(item.getName());
794        
795        ProgramItem parent = getParentProgramItem(item, parentProgram);
796        while (parent != null && !(parent instanceof Program))
797        {
798            paths.add(parent.getName());
799            parent = getParentProgramItem(parent, parentProgram);
800        }
801        
802        if (parent != null)
803        {
804            paths.add(parent.getName());
805            Collections.reverse(paths);
806            return org.apache.commons.lang3.StringUtils.join(paths, "/");
807        }
808        
809        return null;
810    }
811    
812    /**
813     * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br>
814     * The path is construct with the contents' names and the used separator is '/'.
815     * @param contentId The id of the content
816     * @param parentCourseId The id of parent course. Can not be null.
817     * @return the path into the parent course or null if the item is not part of this course.
818     */
819    @Callable
820    public String getPathInCourse (String contentId, String parentCourseId)
821    {
822        Content content = _resolver.resolveById(contentId);
823        Course parentCourse = _resolver.resolveById(parentCourseId);
824        
825        return getPathInCourse(content, parentCourse);
826    }
827    
828    /**
829     * Get the path of a {@link Course}  or a {@link CourseList} into a {@link Course}<br>
830     * The path is construct with the contents' names and the used separator is '/'.
831     * @param courseOrList The course or the course list
832     * @param parentCourse The parent course. Can not be null.
833     * @return the path into the parent course or null if the item is not part of this course.
834     */
835    public String getPathInCourse(Content courseOrList, Course parentCourse)
836    {
837        if (courseOrList.equals(parentCourse))
838        {
839            return "";
840        }
841        
842        String path = _getPathInCourse(courseOrList, parentCourse);
843        
844        return path;
845    }
846    
847    private String _getPathInCourse(Content content, Content parentContent)
848    {
849        if (content.equals(parentContent))
850        {
851            return content.getName();
852        }
853
854        List<? extends Content> parents;
855        
856        if (content instanceof Course)
857        {
858            parents = ((Course) content).getParentCourseLists();
859        }
860        else if (content instanceof CourseList)
861        {
862            parents = ((CourseList) content).getParentCourses();
863        }
864        else
865        {
866            throw new IllegalStateException();
867        }
868        
869        for (Content parent : parents)
870        {
871            String path = _getPathInCourse(parent, parentContent);
872            if (path != null)
873            {
874                return path + '/' + content.getName(); 
875            }
876        }
877        return null;
878    }
879    
880    /**
881     * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br>
882     * The path is construct with the contents' names and the used separator is '/'.
883     * @param orgUnitId The id of the orgunit
884     * @param rootOrgUnitId The root orgunit id
885     * @return the path into the parent program or null if the item is not part of this program.
886     */
887    @Callable
888    public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId)
889    {
890        OrgUnit rootOU = null;
891        if (StringUtils.isNotBlank(rootOrgUnitId))
892        {
893            rootOU = _resolver.resolveById(rootOrgUnitId);
894        }
895        else
896        {
897            rootOU = _ouRootProvider.getRoot();
898        }
899        
900        if (orgUnitId.equals(rootOU.getId()))
901        {
902            // The orgunit is already the root orgunit
903            return rootOU.getName();
904        }
905        
906        OrgUnit ou = _resolver.resolveById(orgUnitId);
907        
908        List<String> paths = new ArrayList<>();
909        paths.add(ou.getName());
910        
911        OrgUnit parent = ou.getParentOrgUnit();
912        while (parent != null && !parent.getId().equals(rootOU.getId()))
913        {
914            paths.add(parent.getName());
915            parent = parent.getParentOrgUnit();
916        }
917        
918        if (parent != null)
919        {
920            paths.add(rootOU.getName());
921            Collections.reverse(paths);
922            return org.apache.commons.lang3.StringUtils.join(paths, "/");
923        }
924        
925        return null;
926    }
927    
928    /**
929     * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br>
930     * The path is construct with the contents' names and the used separator is '/'.
931     * @param orgUnitId The id of the orgunit
932     * @return the path into the parent program or null if the item is not part of this program.
933     */
934    @Callable
935    public String getOrgUnitPath(String orgUnitId)
936    {
937        return getOrgUnitPath(orgUnitId, null);
938    }
939    
940    /**
941     * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id
942     * @param part The program part
943     * @param parentId The ancestor id
944     * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id
945     */
946    public boolean hasAncestor (ProgramPart part, String parentId)
947    {
948        List<ProgramPart> parents = part.getProgramPartParents();
949        
950        for (ProgramPart parent : parents)
951        {
952            if (parent.getId().equals(parentId))
953            {
954                return true;
955            }
956            else if (hasAncestor(parent, parentId))
957            {
958                return true;
959            }
960        }
961        
962        return false;
963    }
964    
965    /**
966     * Check if a relation can be establish between two ODF contents
967     * @param srcContent The source content (copied or moved)
968     * @param targetContent The target content
969     * @param errors The list of error messages
970     * @param contextualParameters the contextual parameters
971     * @return true if the relation is valid, false otherwise
972     */
973    public boolean isRelationCompatible(Content srcContent, Content targetContent, List<I18nizableText> errors, Map<String, Object> contextualParameters)
974    {
975        boolean isCompatible = true;
976        
977        if (targetContent instanceof ProgramItem || targetContent instanceof OrgUnit)
978        {
979            if (!_isContentTypeCompatible(srcContent, targetContent))
980            {
981                // Invalid relations between content types
982                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CONTENT_TYPES", _getContentParameters(srcContent, targetContent)));
983                isCompatible = false;
984            }
985            else if (!_isCatalogCompatible(srcContent, targetContent))
986            {
987                // Catalog is invalid
988                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CATALOG", _getContentParameters(srcContent, targetContent)));
989                isCompatible = false;
990            }
991            else if (!_isLanguageCompatible(srcContent, targetContent))
992            {
993                // Language is invalid
994                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_LANGUAGE", _getContentParameters(srcContent, targetContent)));
995                isCompatible = false;
996            }
997            else if (!_areShareableFieldsCompatibles(srcContent, targetContent, contextualParameters))
998            {
999                // Shareable fields don't match
1000                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_SHAREABLE_COURSE", _getContentParameters(srcContent, targetContent)));
1001                isCompatible = false;
1002            }
1003        }
1004        else if (srcContent instanceof ProgramItem || srcContent instanceof OrgUnit)
1005        {
1006            // If the target isn't ODF related but the source is, the relation is not compatible.
1007            errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_NO_PROGRAM_ITEM", _getContentParameters(srcContent, targetContent)));
1008            isCompatible = false;
1009        }
1010        
1011        return isCompatible;
1012    }
1013    
1014    private boolean _isCourseAlreadyBelongToCourseList(Course course, CourseList courseList)
1015    {
1016        return courseList.getCourses().contains(course);
1017    }
1018    
1019    private boolean _isContentTypeCompatible(Content srcContent, Content targetContent)
1020    {
1021        if (srcContent instanceof Container || srcContent instanceof SubProgram)
1022        {
1023            return targetContent instanceof AbstractTraversableProgramPart;
1024        }
1025        else if (srcContent instanceof CourseList)
1026        {
1027            return targetContent instanceof CourseListContainer;
1028        }
1029        else if (srcContent instanceof Course)
1030        {
1031            return targetContent instanceof CourseList;
1032        }
1033        else if (srcContent instanceof OrgUnit)
1034        {
1035            return targetContent instanceof OrgUnit;
1036        }
1037        
1038        return false;
1039    }
1040    
1041    private boolean _isCatalogCompatible(Content srcContent, Content targetContent)
1042    {
1043        if (srcContent instanceof ProgramItem && targetContent instanceof ProgramItem)
1044        {
1045            return ((ProgramItem) srcContent).getCatalog().equals(((ProgramItem) targetContent).getCatalog());
1046        }
1047        return true;
1048    }
1049    
1050    private boolean _isLanguageCompatible(Content srcContent, Content targetContent)
1051    {
1052        return srcContent.getLanguage().equals(targetContent.getLanguage());
1053    }
1054    
1055    private boolean _areShareableFieldsCompatibles(Content srcContent, Content targetContent, Map<String, Object> contextualParameters)
1056    {
1057        // We check shareable fields only if the course content is not created (or created by copy) and not moved
1058        if (srcContent instanceof Course 
1059                && targetContent instanceof CourseList 
1060                && _shareableCourseHelper.handleShareableCourse() 
1061                && !"create".equals(contextualParameters.get("mode")) 
1062                && !"copy".equals(contextualParameters.get("mode"))
1063                && !"move".equals(contextualParameters.get("mode"))
1064                // In this case, it means that we try to change the position of the course in the courseList, so don't check shareable fields
1065                && !_isCourseAlreadyBelongToCourseList((Course) srcContent, (CourseList) targetContent))
1066        {
1067            return _shareableCourseHelper.isShareableFieldsMatch((Course) srcContent, (CourseList) targetContent);
1068        }
1069        
1070        return true;
1071    }
1072    
1073    private List<String> _getContentParameters(Content srcContent, Content targetContent)
1074    {
1075        List<String> parameters = new ArrayList<>();
1076        parameters.add(srcContent.getTitle());
1077        parameters.add(srcContent.getId());
1078        parameters.add(targetContent.getTitle());
1079        parameters.add(targetContent.getId());
1080        return parameters;
1081    }
1082    /**
1083     * Copy a {@link ProgramItem} 
1084     * @param srcContent The program item to copy
1085     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1086     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1087     * @param copiedPrograms the id of initial programs with their copied content
1088     * @param copiedSubPrograms the id of initial subprograms with their copied content
1089     * @param copiedContainers the id of initial containers with their copied content
1090     * @param copiedCourseLists the id of initial course lists with their copied content
1091     * @param copiedCourses the id of initial courses with their copied content
1092     * @param copiedCourseParts the id of initial course parts with their copied content
1093     * @return The created content
1094     * @param <C> The modifiable content return type 
1095     * @throws AmetysRepositoryException If an error occurred during copy
1096     * @throws WorkflowException If an error occurred during copy
1097     */
1098    public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException
1099    {
1100        return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1101    }
1102    
1103    /**
1104     * Copy a {@link ProgramItem}
1105     * @param srcContent The program item to copy
1106     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1107     * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object.
1108     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1109     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1110     * @param copiedPrograms the id of initial programs with their copied content
1111     * @param copiedSubPrograms the id of initial subprograms with their copied content
1112     * @param copiedContainers the id of initial containers with their copied content
1113     * @param copiedCourseLists the id of initial course lists with their copied content
1114     * @param copiedCourses the id of initial courses with their copied content
1115     * @param copiedCourseParts the id of initial course parts with their copied content
1116     * @param <C> The modifiable content return type 
1117     * @return The created content
1118     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1119     * @throws AmetysRepositoryException If an error occurred
1120     * @throws WorkflowException If an error occurred
1121     */
1122    public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException
1123    {
1124        return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1125    }
1126    
1127    /**
1128     * Copy a {@link CoursePart}
1129     * @param srcContent The course part to copy
1130     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1131     * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object.
1132     * @param initWorkflowActionId The initial workflow action id
1133     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1134     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1135     * @param copiedPrograms the id of initial programs with their copied content
1136     * @param copiedSubPrograms the id of initial subprograms with their copied content
1137     * @param copiedContainers the id of initial containers with their copied content
1138     * @param copiedCourseLists the id of initial course lists with their copied content
1139     * @param copiedCourses the id of initial courses with their copied content
1140     * @param copiedCourseParts the id of initial course parts with their copied content
1141     * @param <C> The modifiable content return type 
1142     * @return The created content
1143     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1144     * @throws AmetysRepositoryException If an error occurred
1145     * @throws WorkflowException If an error occurred
1146     */
1147    public <C extends ModifiableContent> C copyCoursePart(CoursePart srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException
1148    {
1149        return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1150    }
1151    
1152    /**
1153     * Copy a {@link ProgramItem}
1154     * @param srcContent The program item to copy
1155     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1156     * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object.
1157     * @param initWorkflowActionId The initial workflow action id
1158     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1159     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1160     * @param copiedPrograms the id of initial programs with their copied content
1161     * @param copiedSubPrograms the id of initial subprograms with their copied content
1162     * @param copiedContainers the id of initial containers with their copied content
1163     * @param copiedCourseLists the id of initial course lists with their copied content
1164     * @param copiedCourses the id of initial courses with their copied content
1165     * @param copiedCourseParts the id of initial course parts with their copied content
1166     * @param <C> The modifiable content return type 
1167     * @return The created content
1168     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1169     * @throws AmetysRepositoryException If an error occurred
1170     * @throws WorkflowException If an error occurred
1171     */
1172    public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException
1173    {
1174        return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1175    }
1176    
1177    /**
1178     * Copy a {@link ProgramItem}
1179     * @param srcContent The program item to copy
1180     * @param catalog The catalog
1181     * @param code The odf content code
1182     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1183     * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object.
1184     * @param initWorkflowActionId The initial workflow action id
1185     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1186     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1187     * @param copiedPrograms the id of initial programs with their copied content
1188     * @param copiedSubPrograms the id of initial subprograms with their copied content
1189     * @param copiedContainers the id of initial containers with their copied content
1190     * @param copiedCourseLists the id of initial course lists with their copied content
1191     * @param copiedCourses the id of initial courses with their copied content
1192     * @param copiedCourseParts the id of initial course parts with their copied content
1193     * @param <C> The modifiable content return type 
1194     * @return The created content
1195     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1196     * @throws AmetysRepositoryException If an error occurred
1197     * @throws WorkflowException If an error occurred
1198     */
1199    @SuppressWarnings("unchecked")
1200    private <C extends ModifiableContent> C _copyODFContent(Content srcContent, String catalog, String code, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException
1201    {
1202        String computedTargetLanguage = targetContentLanguage;
1203        if (computedTargetLanguage == null)
1204        {
1205            computedTargetLanguage = srcContent.getLanguage();
1206        }
1207        
1208        String computeTargetName = targetContentName;
1209        if (computeTargetName == null)
1210        {
1211            // Compute content name from source content and requested language
1212            computeTargetName = srcContent.getName() + (targetContentLanguage != null && !targetContentLanguage.equals(srcContent.getName()) ? "-" + targetContentLanguage : "");
1213        }
1214        
1215        String computeTargetCatalog = targetCatalog;
1216        if (computeTargetCatalog == null)
1217        {
1218            computeTargetCatalog = catalog;
1219        }
1220        
1221        String principalContentType = srcContent.getTypes()[0];
1222        ModifiableContent createdContent = getODFContent(principalContentType, code, computeTargetCatalog, computedTargetLanguage);
1223        if (createdContent != null)
1224        {
1225            getLogger().info("A program item already exists with the same type, code, catalog and language [{}, {}, {}, {}]", principalContentType, code, computeTargetCatalog, targetContentLanguage);
1226        }
1227        else
1228        {
1229            // Copy content waiting for observers to be completed and copying ACL
1230            createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, false, true);
1231            
1232            if (fullCopy)
1233            {
1234                _cleanContentMetadata(createdContent);
1235                
1236                if (targetCatalog != null)
1237                {
1238                    if (createdContent instanceof ProgramItem)
1239                    {
1240                        ((ProgramItem) createdContent).setCatalog(targetCatalog);
1241                    }
1242                    else if (createdContent instanceof CoursePart)
1243                    {
1244                        ((CoursePart) createdContent).setCatalog(targetCatalog);
1245                    }
1246                    
1247                }
1248                
1249                if (srcContent instanceof ProgramItem)
1250                {
1251                    copyProgramItemStructure((ProgramItem) srcContent, createdContent, computedTargetLanguage, initWorkflowActionId, computeTargetCatalog, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1252                }
1253                
1254                createdContent.saveChanges();
1255            }
1256            
1257            _putInCopiedMap(srcContent, createdContent, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1258        }
1259        
1260        return (C) createdContent;
1261    }
1262    
1263    private void _putInCopiedMap(Content srcContent, ModifiableContent createdContent, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts)
1264    {
1265        if (createdContent instanceof Program)
1266        {
1267            copiedPrograms.put(srcContent.getId(), createdContent.getId());
1268        }
1269        else if (createdContent instanceof SubProgram)
1270        {
1271            copiedSubPrograms.put(srcContent.getId(), createdContent.getId());
1272        }
1273        else if (createdContent instanceof Container)
1274        {
1275            copiedContainers.put(srcContent.getId(), createdContent.getId());
1276        }
1277        else if (createdContent instanceof CourseList)
1278        {
1279            copiedCourseLists.put(srcContent.getId(), createdContent.getId());
1280        }
1281        else if (createdContent instanceof Course)
1282        {
1283            copiedCourses.put(srcContent.getId(), createdContent.getId());
1284        }
1285        else if (createdContent instanceof CoursePart)
1286        {
1287            copiedCourseParts.put(srcContent.getId(), createdContent.getId());
1288        }
1289    }
1290    
1291    /**
1292     * Copy the structure of a {@link ProgramItem}
1293     * @param srcContent the content to copy
1294     * @param targetContent the target content
1295     * @param targetContentLanguage The name of content to created. Can be null. If null, the language of target content will be the same as source object.
1296     * @param initWorkflowActionId The initial workflow action id
1297     * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object.
1298     * @param copiedPrograms the id of initial programs with their copied content
1299     * @param copiedSubPrograms the id of initial subprograms with their copied content
1300     * @param copiedContainers the id of initial containers with their copied content
1301     * @param copiedCourseLists the id of initial course lists with their copied content
1302     * @param copiedCourses the id of initial courses with their copied content
1303     * @param copiedCourseParts the id of initial course parts with their copied content
1304     * @throws AmetysRepositoryException If an error occurred during copy
1305     * @throws WorkflowException If an error occurred during copy
1306     */
1307    protected void copyProgramItemStructure(ProgramItem srcContent, ModifiableContent targetContent, String targetContentLanguage, int initWorkflowActionId, String targetCatalogName, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses, Map<String, String> copiedCourseParts) throws AmetysRepositoryException, WorkflowException
1308    {
1309        List<ProgramItem> srcChildContents = new ArrayList<>();
1310        Map<Pair<String, String>, List<String>> values = new HashMap<>();
1311        
1312        String childMetadataPath = null;
1313        String parentMetadataPath = null;
1314        
1315        if (srcContent instanceof TraversableProgramPart)
1316        {
1317            childMetadataPath = TraversableProgramPart.CHILD_PROGRAM_PARTS;
1318            parentMetadataPath = ProgramPart.PARENT_PROGRAM_PARTS;
1319            srcChildContents.addAll(((TraversableProgramPart) srcContent).getProgramPartChildren());
1320        }
1321        else if (srcContent instanceof CourseList)
1322        {
1323            childMetadataPath = CourseList.CHILD_COURSES;
1324            parentMetadataPath = Course.PARENT_COURSE_LISTS;
1325            srcChildContents.addAll(((CourseList) srcContent).getCourses());
1326        }
1327        else if (srcContent instanceof Course)
1328        {
1329            childMetadataPath = Course.CHILD_COURSE_LISTS;
1330            parentMetadataPath = CourseList.PARENT_COURSES;
1331            srcChildContents.addAll(((Course) srcContent).getCourseLists());
1332
1333            List<String> refCoursePartIds = new ArrayList<>();
1334            for (CoursePart srcChildContent : ((Course) srcContent).getCourseParts())
1335            {
1336                CoursePart targetChildContent = copyCoursePart(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1337                refCoursePartIds.add(targetChildContent.getId());
1338            }
1339            _addFormValues(values, Course.CHILD_COURSE_PARTS, CoursePart.PARENT_COURSES, refCoursePartIds);
1340        }
1341
1342        List<String> refChildIds = new ArrayList<>();
1343        for (ProgramItem srcChildContent : srcChildContents)
1344        {
1345            ProgramItem targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1346            refChildIds.add(targetChildContent.getId());
1347        }
1348
1349        _addFormValues(values, childMetadataPath, parentMetadataPath, refChildIds);
1350
1351        _editChildRelation((ModifiableWorkflowAwareContent) targetContent, values);
1352    }
1353    
1354    private void _addFormValues(Map<Pair<String, String>, List<String>> values, String childMetadataPath, String parentMetadataPath, List<String> refChildIds)
1355    {
1356        if (!refChildIds.isEmpty())
1357        {
1358            values.put(Pair.of(childMetadataPath, parentMetadataPath), refChildIds);
1359        }
1360    }
1361    
1362    private void _editChildRelation(ModifiableWorkflowAwareContent parentContent, Map<Pair<String, String>, List<String>> values) throws AmetysRepositoryException
1363    {
1364        if (!values.isEmpty())
1365        {
1366            for (Map.Entry<Pair<String, String>, List<String>> entry : values.entrySet())
1367            {
1368                String childMetadataName = entry.getKey().getLeft();
1369                String parentMetadataName = entry.getKey().getRight();
1370                List<String> childContents = entry.getValue();
1371                
1372                parentContent.setValue(childMetadataName, childContents.toArray(new String[childContents.size()]));
1373                
1374                for (String childContentId : childContents)
1375                {
1376                    ModifiableContent content = _resolver.resolveById(childContentId);
1377                    String[] parentContentIds = ContentDataHelper.getContentIdsArrayFromMultipleContentData(content, parentMetadataName);
1378                    content.setValue(parentMetadataName, ArrayUtils.add(parentContentIds, parentContent.getId()));
1379                    content.saveChanges();
1380                }
1381            }
1382        }
1383    }
1384    
1385    /**
1386     * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure
1387     * @param createdContent The created content to clean
1388     */
1389    protected void _cleanContentMetadata(ModifiableContent createdContent)
1390    {
1391        if (createdContent instanceof ProgramPart)
1392        {
1393            _removeFullValue(createdContent, ProgramPart.PARENT_PROGRAM_PARTS);
1394        }
1395        
1396        if (createdContent instanceof TraversableProgramPart)
1397        {
1398            _removeFullValue(createdContent, TraversableProgramPart.CHILD_PROGRAM_PARTS);
1399        }
1400        
1401        if (createdContent instanceof CourseList)
1402        {
1403            _removeFullValue(createdContent, CourseList.CHILD_COURSES);
1404            _removeFullValue(createdContent, CourseList.PARENT_COURSES);
1405        }
1406        
1407        if (createdContent instanceof Course)
1408        {
1409            _removeFullValue(createdContent, Course.CHILD_COURSE_LISTS);
1410            _removeFullValue(createdContent, Course.PARENT_COURSE_LISTS);
1411            _removeFullValue(createdContent, Course.CHILD_COURSE_PARTS);
1412        }
1413        
1414        if (createdContent instanceof CoursePart)
1415        {
1416            _removeFullValue(createdContent, CoursePart.PARENT_COURSES);
1417        }
1418    }
1419    
1420    private void _removeFullValue(ModifiableContent content, String attributeName)
1421    {
1422        content.removeValue(attributeName);
1423        content.removeExternalizableMetadataIfExists(attributeName);
1424    }
1425    
1426    /**
1427     * Switch the ametys object to Live version if it has one
1428     * @param ao the Ametys object
1429     * @throws NoLiveVersionException if the content has no live version
1430     */
1431    public void switchToLiveVersion(DefaultAmetysObject ao) throws NoLiveVersionException
1432    {
1433        // Switch to the Live label if exists
1434        String[] allLabels = ao.getAllLabels();
1435        String[] currentLabels = ao.getLabels();
1436        
1437        boolean hasLiveVersion = Arrays.asList(allLabels).contains(CmsConstants.LIVE_LABEL);
1438        boolean currentVersionIsLive = Arrays.asList(currentLabels).contains(CmsConstants.LIVE_LABEL);
1439        
1440        if (hasLiveVersion && !currentVersionIsLive)
1441        {
1442            ao.switchToLabel(CmsConstants.LIVE_LABEL);
1443        }
1444        else if (!hasLiveVersion)
1445        {
1446            throw new NoLiveVersionException("The ametys object '" + ao.getId() + "' has no live version");
1447        }
1448    }
1449    
1450    /**
1451     * Switch to Live version if is required
1452     * @param ao the Ametys object
1453     * @throws NoLiveVersionException if the Live version is required but not exist
1454     */
1455    public void switchToLiveVersionIfNeeded(DefaultAmetysObject ao) throws NoLiveVersionException
1456    {
1457        Request request = _getRequest();
1458        if (request != null && request.getAttribute(REQUEST_ATTRIBUTE_VALID_LABEL) != null)
1459        {
1460            switchToLiveVersion(ao);
1461        }
1462    }
1463    
1464    /**
1465     * Count the hours accumulation in the {@link ProgramItem}
1466     * @param programItem The program item on which we compute the total number of hours
1467     * @return The hours accumulation
1468     */
1469    public Double getCumulatedHours(ProgramItem programItem)
1470    {
1471        // Ignore optional course list and avoid useless expensive calls
1472        if (programItem instanceof CourseList && ChoiceType.OPTIONAL.equals(((CourseList) programItem).getType()))
1473        {
1474            return 0.0;
1475        }
1476
1477        List<ProgramItem> children = getChildProgramItems(programItem);
1478
1479        Double coef = 1.0;
1480        Double countNbHours = 0.0;
1481
1482        // If the program item is a course list, compute the coef (mandatory: 1, optional: 0, optional: min / total)
1483        if (programItem instanceof CourseList)
1484        {
1485            // If there is no children, compute the coef is useless
1486            // Also choice list can throw an exception while dividing by zero
1487            if (children.isEmpty())
1488            {
1489                return 0.0;
1490            }
1491            
1492            CourseList courseList = (CourseList) programItem;
1493            switch (courseList.getType())
1494            {
1495                case CHOICE:
1496                    // Apply the average of number of EC from children multiply by the minimum ELP to select
1497                    coef = ((double) courseList.getMinNumberOfCourses()) / children.size();
1498                    break;
1499                case MANDATORY:
1500                default:
1501                    // Add all ECTS from children
1502                    break;
1503            }
1504        }
1505
1506        // If it's a course and we have a value for the number of hours
1507        // Then get the value
1508        if (programItem instanceof Course && ((Course) programItem).hasValue(Course.NUMBER_OF_HOURS))
1509        {
1510            countNbHours += ((Course) programItem).<Double>getValue(Course.NUMBER_OF_HOURS);
1511        }
1512        // Else if there are program item children on the item
1513        // Then compute on children
1514        else if (children.size() > 0)
1515        {
1516            for (ProgramItem child : children)
1517            {
1518                countNbHours += getCumulatedHours(child);
1519            }
1520        }
1521        // Else, it's a course but there is no value for the number of hours and we don't have program item children
1522        // Then compute on course parts
1523        else if (programItem instanceof Course)
1524        {
1525            countNbHours += ((Course) programItem).getCourseParts()
1526                .stream()
1527                .mapToDouble(CoursePart::getNumberOfHours)
1528                .sum();
1529        }
1530        
1531        return coef * countNbHours;
1532    }
1533    
1534    /**
1535     * Get the request
1536     * @return the request
1537     */
1538    protected Request _getRequest()
1539    {
1540        return ContextHelper.getRequest(_context);
1541    }
1542    
1543    /**
1544     * Get the first orgunit matching the given UAI code
1545     * @param uaiCode the UAI code
1546     * @return the orgunit or null if not found
1547     */
1548    public OrgUnit getOrgUnitByUAICode(String uaiCode)
1549    {
1550        Expression expr = new AndExpression(
1551                new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE),
1552                new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode)
1553        );
1554        
1555        String xPathQuery = QueryHelper.getXPathQuery(null, OrgUnitFactory.ORGUNIT_NODETYPE, expr);
1556        AmetysObjectIterable<OrgUnit> orgUnits = _resolver.query(xPathQuery);
1557        
1558        return orgUnits.stream()
1559            .findFirst()
1560            .orElse(null);
1561    }
1562}