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.stream.Collectors;
028
029import org.apache.avalon.framework.component.Component;
030import org.apache.avalon.framework.context.Context;
031import org.apache.avalon.framework.context.ContextException;
032import org.apache.avalon.framework.context.Contextualizable;
033import org.apache.avalon.framework.service.ServiceException;
034import org.apache.avalon.framework.service.ServiceManager;
035import org.apache.avalon.framework.service.Serviceable;
036import org.apache.cocoon.components.ContextHelper;
037import org.apache.cocoon.environment.Request;
038import org.apache.commons.lang.StringUtils;
039import org.apache.commons.lang3.ArrayUtils;
040import org.apache.commons.lang3.tuple.Pair;
041
042import org.ametys.cms.content.external.ExternalizableMetadataHelper;
043import org.ametys.cms.content.external.ExternalizableMetadataProviderExtensionPoint;
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.RootOrgUnitProvider;
067import org.ametys.odf.program.AbstractProgram;
068import org.ametys.odf.program.AbstractTraversableProgramPart;
069import org.ametys.odf.program.Container;
070import org.ametys.odf.program.Program;
071import org.ametys.odf.program.ProgramFactory;
072import org.ametys.odf.program.ProgramPart;
073import org.ametys.odf.program.SubProgram;
074import org.ametys.odf.program.TraversableProgramPart;
075import org.ametys.odf.workflow.ValidateODFContentFunction;
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.jcr.DefaultAmetysObject;
087import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
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 ExternalizableMetadataProviderExtensionPoint _externalizableMetadataProviderEP;
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        _externalizableMetadataProviderEP = (ExternalizableMetadataProviderExtensionPoint) manager.lookup(ExternalizableMetadataProviderExtensionPoint.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
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        List<Expression> expressions = new ArrayList<>();
483        for (String orgUnitId : getSubOrgUnitIds(orgUnit))
484        {
485            expressions.add(new StringExpression(AbstractProgram.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId));
486        }
487        
488        programExpressions.add(new OrExpression(expressions.toArray(new Expression[0])));
489        String programQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, new AndExpression(programExpressions.toArray(new Expression[0])));
490        AmetysObjectIterable<Program> programsIterable = _resolver.query(programQuery);
491        AmetysObjectIterator<Program> programsIterator = programsIterable.iterator();
492        while (programsIterator.hasNext())
493        {
494            programs.add(programsIterator.next());
495        }
496        return programs;
497    }
498    
499    /**
500     * Get the current orgunit and its suborgunits recursively identifiers.
501     * @param orgUnit The orgunit at the top
502     * @return A {@link List} of {@link OrgUnit} ids
503     */
504    public List<String> getSubOrgUnitIds(OrgUnit orgUnit)
505    {
506        List<String> orgUnitIds = new ArrayList<>();
507        orgUnitIds.add(orgUnit.getId());
508        for (String id : orgUnit.getSubOrgUnits())
509        {
510            OrgUnit childOrgUnit = _resolver.resolveById(id);
511            orgUnitIds.addAll(getSubOrgUnitIds(childOrgUnit));
512        }
513        
514        return orgUnitIds;
515    }
516    
517    /**
518     * Determines if the {@link ProgramItem} has parent program items 
519     * @param programItem The program item
520     * @return true if has parent program items 
521     */
522    public boolean hasParentProgramItems(ProgramItem programItem)
523    {
524        boolean hasParent = false;
525        
526        if (programItem instanceof ProgramPart)
527        {
528            hasParent = !((ProgramPart) programItem).getProgramPartParents().isEmpty() || hasParent;
529        }
530        
531        if (programItem instanceof CourseList)
532        {
533            hasParent = !((CourseList) programItem).getParentCourses().isEmpty() || hasParent;
534        }
535        
536        if (programItem instanceof Course)
537        {
538            hasParent = !((Course) programItem).getParentCourseLists().isEmpty() || hasParent;
539        }
540        
541        return hasParent;
542    }
543    
544    /**
545     * Get the parent program items of a {@link ProgramItem}
546     * @param programItem The program item
547     * @return The parent program items 
548     */
549    public List<ProgramItem> getParentProgramItems(ProgramItem programItem)
550    {
551        List<ProgramItem> parents = new ArrayList<>();
552        
553        if (programItem instanceof ProgramPart)
554        {
555            parents.addAll(((ProgramPart) programItem).getProgramPartParents());
556        }
557        
558        if (programItem instanceof CourseList)
559        {
560            parents.addAll(((CourseList) programItem).getParentCourses());
561        }
562        
563        if (programItem instanceof Course)
564        {
565            parents.addAll(((Course) programItem).getParentCourseLists());
566        }
567        
568        return parents;
569    }
570    
571    /**
572     * Get the nearest program item parent into the given parent {@link AbstractProgram}
573     * @param programItem The program item
574     * @param parentProgram The parent program or subprogram. If null, the nearest abstract program will be returned.
575     * @return The parent program item or null if not found.
576     */
577    public ProgramItem getParentProgramItem (ProgramItem programItem, AbstractProgram parentProgram)
578    {
579        if (programItem instanceof Program)
580        {
581            return null;
582        }
583        
584        if (programItem instanceof ProgramPart)
585        {
586            List<ProgramPart> parents = ((ProgramPart) programItem).getProgramPartParents();
587            
588            for (ProgramPart parent : parents)
589            {
590                if (parent instanceof AbstractProgram && (parentProgram == null || parent.equals(parentProgram)))
591                {
592                    return parent;
593                }
594                else
595                {
596                    ProgramItem ancestor = getParentProgramItem(parent, parentProgram);
597                    if (ancestor != null)
598                    {
599                        return parent;
600                    }
601                }
602            }
603        }
604        
605        if (programItem instanceof CourseList)
606        {
607            for (Course parentCourse : ((CourseList) programItem).getParentCourses())
608            {
609                ProgramItem ancestor = getParentProgramItem(parentCourse, parentProgram);
610                if (ancestor != null)
611                {
612                    return parentCourse;
613                }
614            }
615        }
616        
617        if (programItem instanceof Course)
618        {
619            for (CourseList cl : ((Course) programItem).getParentCourseLists())
620            {
621                ProgramItem ancestor = getParentProgramItem(cl, parentProgram);
622                if (ancestor != null)
623                {
624                    return cl;
625                }
626            }
627        }
628        
629        return null;
630    }
631    
632    /**
633     * Get information of the program item structure (type, if program has children)
634     * @param programItemId the program item id
635     * @return a map of information
636     */
637    @Callable
638    public Map<String, Object> getStructureInfo(String programItemId)
639    {
640        Map<String, Object> results = new HashMap<>();
641        
642        if (StringUtils.isNotBlank(programItemId))
643        {
644            Content content = _resolver.resolveById(programItemId);
645            if (content instanceof ProgramItem)
646            {
647                results.put("id", programItemId);
648                results.put("title", content.getTitle());
649                results.put("code", ((ProgramItem) content).getCode());
650                
651                List<ProgramItem> childProgramItems = getChildProgramItems((ProgramItem) content);
652                results.put("hasChildren", !childProgramItems.isEmpty());
653                
654                List<ProgramItem> parentProgramItems = getParentProgramItems((ProgramItem) content);
655                results.put("hasParent", !parentProgramItems.isEmpty());
656                
657                results.put("paths", getPaths((ProgramItem) content, " > "));
658            }
659        }
660        
661        return results;
662    }
663    
664    /**
665     * Get information of the program item structure (type, if program has children)
666     * @param programItemIds the list of program item id
667     * @return a map of information
668     */
669    @Callable
670    public Map<String, Map<String, Object>> getStructureInfo(List<String> programItemIds)
671    {
672        Map<String,  Map<String, Object>> results = new HashMap<>();
673        
674        for (String programItemId : programItemIds)
675        {
676            results.put(programItemId, getStructureInfo(programItemId));
677        }
678        
679        return results;
680    }
681    
682    /**
683     * Get all the paths of a ODF content.<br>
684     * The path is construct with the contents' title
685     * @param separator The path separator
686     * @param item The program item
687     * @return the paths in parent program items
688     */
689    protected List<String> getPaths(ProgramItem item, String separator)
690    {
691        List<String> paths = new ArrayList<>();
692        
693        List<List<ProgramItem>> ancestorPaths = getPathOfAncestors(item);
694        for (List<ProgramItem> ancestorPath : ancestorPaths)
695        {
696            List<String> titles = ancestorPath.stream().map(p -> ((Content) p).getTitle() + " (" + p.getCode() + ")").collect(Collectors.toList());
697            paths.add(String.join(separator, titles));
698        }
699        
700        return paths;
701    }
702    
703    /**
704     * Get the full path to program item for highest ancestors. The path includes this final item.
705     * @param item the program item
706     * @return a list for each highest ancestors found. Each item of the list contains the program items to the path to this program item.
707     */
708    public List<List<ProgramItem>> getPathOfAncestors(ProgramItem item)
709    {
710        List<List<ProgramItem>> ancestors = new ArrayList<>();
711        
712        List<ProgramItem> parentProgramItems = getParentProgramItems(item);
713        if (parentProgramItems.isEmpty())
714        {
715            List<ProgramItem> items = new ArrayList<>();
716            items.add(item);
717            ancestors.add(items);
718            return ancestors;
719        }
720        
721        for (ProgramItem parentProgramItem : parentProgramItems)
722        {
723            for (List<ProgramItem> ancestorPaths : getPathOfAncestors(parentProgramItem))
724            {
725                ancestorPaths.add(item);
726                ancestors.add(ancestorPaths);
727            }
728        }
729        
730        return ancestors;
731    }
732    
733    /**
734     * Get the path of a {@link ProgramItem} into a {@link Program}<br>
735     * The path is construct with the contents' names and the used separator is '/'.
736     * @param programItemId The id of the program item
737     * @param programId The id of program. Can not be null.
738     * @return the path into the parent program or null if the item is not part of this program.
739     */
740    @Callable
741    public String getPathInProgram (String programItemId, String programId)
742    {
743        ProgramItem item = _resolver.resolveById(programItemId);
744        Program program = _resolver.resolveById(programId);
745        
746        return getPathInProgram(item, program);
747    }
748    
749    /**
750     * Get the path of a ODF content into a {@link Program}.<br>
751     * The path is construct with the contents' names and the used separator is '/'.
752     * @param item The program item
753     * @param parentProgram The parent root (sub)program. Can not be null.
754     * @return the path from the parent program
755     */
756    public String getPathInProgram (ProgramItem item, Program parentProgram)
757    {
758        if (item instanceof Program)
759        {
760            // The program item is already the program it self or another program
761            return item.equals(parentProgram) ? "" : null;
762        }
763        
764        List<String> paths = new ArrayList<>();
765        paths.add(item.getName());
766        
767        ProgramItem parent = getParentProgramItem(item, parentProgram);
768        while (parent != null && !(parent instanceof Program))
769        {
770            paths.add(parent.getName());
771            parent = getParentProgramItem(parent, parentProgram);
772        }
773        
774        if (parent != null)
775        {
776            paths.add(parent.getName());
777            Collections.reverse(paths);
778            return org.apache.commons.lang3.StringUtils.join(paths, "/");
779        }
780        
781        return null;
782    }
783    
784    /**
785     * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br>
786     * The path is construct with the contents' names and the used separator is '/'.
787     * @param contentId The id of the content
788     * @param parentCourseId The id of parent course. Can not be null.
789     * @return the path into the parent course or null if the item is not part of this course.
790     */
791    @Callable
792    public String getPathInCourse (String contentId, String parentCourseId)
793    {
794        Content content = _resolver.resolveById(contentId);
795        Course parentCourse = _resolver.resolveById(parentCourseId);
796        
797        return getPathInCourse(content, parentCourse);
798    }
799    
800    /**
801     * Get the path of a {@link Course}  or a {@link CourseList} into a {@link Course}<br>
802     * The path is construct with the contents' names and the used separator is '/'.
803     * @param courseOrList The course or the course list
804     * @param parentCourse The parent course. Can not be null.
805     * @return the path into the parent course or null if the item is not part of this course.
806     */
807    public String getPathInCourse(Content courseOrList, Course parentCourse)
808    {
809        if (courseOrList.equals(parentCourse))
810        {
811            return "";
812        }
813        
814        String path = _getPathInCourse(courseOrList, parentCourse);
815        
816        return path;
817    }
818    
819    private String _getPathInCourse(Content content, Content parentContent)
820    {
821        if (content.equals(parentContent))
822        {
823            return content.getName();
824        }
825
826        List<? extends Content> parents;
827        
828        if (content instanceof Course)
829        {
830            parents = ((Course) content).getParentCourseLists();
831        }
832        else if (content instanceof CourseList)
833        {
834            parents = ((CourseList) content).getParentCourses();
835        }
836        else
837        {
838            throw new IllegalStateException();
839        }
840        
841        for (Content parent : parents)
842        {
843            String path = _getPathInCourse(parent, parentContent);
844            if (path != null)
845            {
846                return path + '/' + content.getName(); 
847            }
848        }
849        return null;
850    }
851    
852    /**
853     * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br>
854     * The path is construct with the contents' names and the used separator is '/'.
855     * @param orgUnitId The id of the orgunit
856     * @param rootOrgUnitId The root orgunit id
857     * @return the path into the parent program or null if the item is not part of this program.
858     */
859    @Callable
860    public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId)
861    {
862        OrgUnit rootOU = null;
863        if (StringUtils.isNotBlank(rootOrgUnitId))
864        {
865            rootOU = _resolver.resolveById(rootOrgUnitId);
866        }
867        else
868        {
869            rootOU = _ouRootProvider.getRoot();
870        }
871        
872        if (orgUnitId.equals(rootOU.getId()))
873        {
874            // The orgunit is already the root orgunit
875            return rootOU.getName();
876        }
877        
878        OrgUnit ou = _resolver.resolveById(orgUnitId);
879        
880        List<String> paths = new ArrayList<>();
881        paths.add(ou.getName());
882        
883        OrgUnit parent = ou.getParentOrgUnit();
884        while (parent != null && !parent.getId().equals(rootOU.getId()))
885        {
886            paths.add(parent.getName());
887            parent = parent.getParentOrgUnit();
888        }
889        
890        if (parent != null)
891        {
892            paths.add(rootOU.getName());
893            Collections.reverse(paths);
894            return org.apache.commons.lang3.StringUtils.join(paths, "/");
895        }
896        
897        return null;
898    }
899    
900    /**
901     * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br>
902     * The path is construct with the contents' names and the used separator is '/'.
903     * @param orgUnitId The id of the orgunit
904     * @return the path into the parent program or null if the item is not part of this program.
905     */
906    @Callable
907    public String getOrgUnitPath(String orgUnitId)
908    {
909        return getOrgUnitPath(orgUnitId, null);
910    }
911    
912    /**
913     * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id
914     * @param part The program part
915     * @param parentId The ancestor id
916     * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id
917     */
918    public boolean hasAncestor (ProgramPart part, String parentId)
919    {
920        List<ProgramPart> parents = part.getProgramPartParents();
921        
922        for (ProgramPart parent : parents)
923        {
924            if (parent.getId().equals(parentId))
925            {
926                return true;
927            }
928            else if (hasAncestor(parent, parentId))
929            {
930                return true;
931            }
932        }
933        
934        return false;
935    }
936    
937    /**
938     * Check if a relation can be establish between two ODF contents
939     * @param srcContent The source content (copied or moved)
940     * @param targetContent The target content
941     * @param errors The list of error messages
942     * @param contextualParameters the contextual parameters
943     * @return true if the relation is valid, false otherwise
944     */
945    public boolean isRelationCompatible(Content srcContent, Content targetContent, List<I18nizableText> errors, Map<String, Object> contextualParameters)
946    {
947        boolean isCompatible = true;
948        
949        if (targetContent instanceof ProgramItem || targetContent instanceof OrgUnit)
950        {
951            if (!_isContentTypeCompatible(srcContent, targetContent))
952            {
953                // Invalid relations between content types
954                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CONTENT_TYPES", _getContentParameters(srcContent, targetContent)));
955                isCompatible = false;
956            }
957            else if (!_isCatalogCompatible(srcContent, targetContent))
958            {
959                // Catalog is invalid
960                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CATALOG", _getContentParameters(srcContent, targetContent)));
961                isCompatible = false;
962            }
963            else if (!srcContent.getLanguage().equals(targetContent.getLanguage()))
964            {
965                // Language is invalid
966                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_LANGUAGE", _getContentParameters(srcContent, targetContent)));
967                isCompatible = false;
968            }
969            // We check shareable fields only if the course content is not created (or created by copy) and not moved
970            else if (srcContent instanceof Course 
971                    && targetContent instanceof CourseList 
972                    && _shareableCourseHelper.handleShareableCourse() 
973                    && !"create".equals(contextualParameters.get("mode")) 
974                    && !"copy".equals(contextualParameters.get("mode"))
975                    && !"move".equals(contextualParameters.get("mode"))
976                    // In this case, it means that we try to change the position of the course in the courseList, so don't check shareable fields
977                    && !_isCourseAlreadyBelongToCourseList((Course) srcContent, (CourseList) targetContent))
978            {
979                if (!_shareableCourseHelper.isShareableFieldsMatch((Course) srcContent, (CourseList) targetContent))
980                {
981                    // Shareable fields don't match
982                    errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_SHAREABLE_COURSE", _getContentParameters(srcContent, targetContent)));
983                    isCompatible = false;
984                }
985            }
986        }
987        else if (srcContent instanceof ProgramItem || srcContent instanceof OrgUnit)
988        {
989            // If the target isn't ODF related but the source is, the relation is not compatible.
990            errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_NO_PROGRAM_ITEM", _getContentParameters(srcContent, targetContent)));
991            isCompatible = false;
992        }
993        
994        return isCompatible;
995    }
996    
997    private boolean _isCourseAlreadyBelongToCourseList(Course course, CourseList courseList)
998    {
999        return courseList.getCourses().contains(course);
1000    }
1001    
1002    private boolean _isContentTypeCompatible(Content srcContent, Content targetContent)
1003    {
1004        if (srcContent instanceof Container || srcContent instanceof SubProgram)
1005        {
1006            return targetContent instanceof AbstractTraversableProgramPart;
1007        }
1008        else if (srcContent instanceof CourseList)
1009        {
1010            return targetContent instanceof CourseListContainer;
1011        }
1012        else if (srcContent instanceof Course)
1013        {
1014            return targetContent instanceof CourseList;
1015        }
1016        else if (srcContent instanceof OrgUnit)
1017        {
1018            return targetContent instanceof OrgUnit;
1019        }
1020        
1021        return false;
1022    }
1023    
1024    private boolean _isCatalogCompatible(Content srcContent, Content targetContent)
1025    {
1026        if (srcContent instanceof ProgramItem && targetContent instanceof ProgramItem)
1027        {
1028            return ((ProgramItem) srcContent).getCatalog().equals(((ProgramItem) targetContent).getCatalog());
1029        }
1030        return true;
1031    }
1032    
1033    private List<String> _getContentParameters(Content srcContent, Content targetContent)
1034    {
1035        List<String> parameters = new ArrayList<>();
1036        parameters.add(srcContent.getTitle());
1037        parameters.add(srcContent.getId());
1038        parameters.add(targetContent.getTitle());
1039        parameters.add(targetContent.getId());
1040        return parameters;
1041    }
1042    /**
1043     * Copy a {@link ProgramItem} 
1044     * @param srcContent The program item to copy
1045     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1046     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1047     * @param copiedPrograms the id of initial programs with their copied content
1048     * @param copiedSubPrograms the id of initial subprograms with their copied content
1049     * @param copiedContainers the id of initial containers with their copied content
1050     * @param copiedCourseLists the id of initial course lists with their copied content
1051     * @param copiedCourses the id of initial courses with their copied content
1052     * @param copiedCourseParts the id of initial course parts with their copied content
1053     * @return The created content
1054     * @param <C> The modifiable content return type 
1055     * @throws AmetysRepositoryException If an error occurred during copy
1056     * @throws WorkflowException If an error occurred during copy
1057     */
1058    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
1059    {
1060        return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1061    }
1062    
1063    /**
1064     * Copy a {@link ProgramItem}
1065     * @param srcContent The program item to copy
1066     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1067     * @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.
1068     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1069     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1070     * @param copiedPrograms the id of initial programs with their copied content
1071     * @param copiedSubPrograms the id of initial subprograms with their copied content
1072     * @param copiedContainers the id of initial containers with their copied content
1073     * @param copiedCourseLists the id of initial course lists with their copied content
1074     * @param copiedCourses the id of initial courses with their copied content
1075     * @param copiedCourseParts the id of initial course parts with their copied content
1076     * @param <C> The modifiable content return type 
1077     * @return The created content
1078     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1079     * @throws AmetysRepositoryException If an error occurred
1080     * @throws WorkflowException If an error occurred
1081     */
1082    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
1083    {
1084        return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1085    }
1086    
1087    /**
1088     * Copy a {@link CoursePart}
1089     * @param srcContent The course part to copy
1090     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1091     * @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.
1092     * @param initWorkflowActionId The initial workflow action id
1093     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1094     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1095     * @param copiedPrograms the id of initial programs with their copied content
1096     * @param copiedSubPrograms the id of initial subprograms with their copied content
1097     * @param copiedContainers the id of initial containers with their copied content
1098     * @param copiedCourseLists the id of initial course lists with their copied content
1099     * @param copiedCourses the id of initial courses with their copied content
1100     * @param copiedCourseParts the id of initial course parts with their copied content
1101     * @param <C> The modifiable content return type 
1102     * @return The created content
1103     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1104     * @throws AmetysRepositoryException If an error occurred
1105     * @throws WorkflowException If an error occurred
1106     */
1107    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
1108    {
1109        return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1110    }
1111    
1112    /**
1113     * Copy a {@link ProgramItem}
1114     * @param srcContent The program item to copy
1115     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1116     * @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.
1117     * @param initWorkflowActionId The initial workflow action id
1118     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1119     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1120     * @param copiedPrograms the id of initial programs with their copied content
1121     * @param copiedSubPrograms the id of initial subprograms with their copied content
1122     * @param copiedContainers the id of initial containers with their copied content
1123     * @param copiedCourseLists the id of initial course lists with their copied content
1124     * @param copiedCourses the id of initial courses with their copied content
1125     * @param copiedCourseParts the id of initial course parts with their copied content
1126     * @param <C> The modifiable content return type 
1127     * @return The created content
1128     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1129     * @throws AmetysRepositoryException If an error occurred
1130     * @throws WorkflowException If an error occurred
1131     */
1132    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
1133    {
1134        return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1135    }
1136    
1137    /**
1138     * Copy a {@link ProgramItem}
1139     * @param srcContent The program item to copy
1140     * @param catalog The catalog
1141     * @param code The odf content code
1142     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1143     * @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.
1144     * @param initWorkflowActionId The initial workflow action id
1145     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1146     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1147     * @param copiedPrograms the id of initial programs with their copied content
1148     * @param copiedSubPrograms the id of initial subprograms with their copied content
1149     * @param copiedContainers the id of initial containers with their copied content
1150     * @param copiedCourseLists the id of initial course lists with their copied content
1151     * @param copiedCourses the id of initial courses with their copied content
1152     * @param copiedCourseParts the id of initial course parts with their copied content
1153     * @param <C> The modifiable content return type 
1154     * @return The created content
1155     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1156     * @throws AmetysRepositoryException If an error occurred
1157     * @throws WorkflowException If an error occurred
1158     */
1159    @SuppressWarnings("unchecked")
1160    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
1161    {
1162        String computedTargetLanguage = targetContentLanguage;
1163        if (computedTargetLanguage == null)
1164        {
1165            computedTargetLanguage = srcContent.getLanguage();
1166        }
1167        
1168        String computeTargetName = targetContentName;
1169        if (computeTargetName == null)
1170        {
1171            // Compute content name from source content and requested language
1172            computeTargetName = srcContent.getName() + (targetContentLanguage != null && !targetContentLanguage.equals(srcContent.getName()) ? "-" + targetContentLanguage : "");
1173        }
1174        
1175        String computeTargetCatalog = targetCatalog;
1176        if (computeTargetCatalog == null)
1177        {
1178            computeTargetCatalog = catalog;
1179        }
1180        
1181        String principalContentType = srcContent.getTypes()[0];
1182        ModifiableContent createdContent = getODFContent(principalContentType, code, computeTargetCatalog, computedTargetLanguage);
1183        if (createdContent != null)
1184        {
1185            getLogger().info("A program item already exists with the same type, code, catalog and language [{}, {}, {}, {}]", principalContentType, code, computeTargetCatalog, targetContentLanguage);
1186        }
1187        else
1188        {
1189            // Copy content waiting for observers to be completed and copying ACL
1190            createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, false, true);
1191            
1192            if (fullCopy)
1193            {
1194                _cleanContentMetadata(createdContent);
1195                
1196                if (targetCatalog != null)
1197                {
1198                    boolean hasChanges = false;
1199                    if (createdContent instanceof ProgramItem)
1200                    {
1201                        ((ProgramItem) createdContent).setCatalog(targetCatalog);
1202                        hasChanges = true;
1203                    }
1204                    else if (createdContent instanceof CoursePart)
1205                    {
1206                        ((CoursePart) createdContent).setCatalog(targetCatalog);
1207                        hasChanges = true;
1208                    }
1209                    
1210                    if (hasChanges)
1211                    {
1212                        createdContent.saveChanges();
1213                    }
1214                }
1215                
1216                if (srcContent instanceof ProgramItem)
1217                {
1218                    copyProgramItemStructure((ProgramItem) srcContent, createdContent, computedTargetLanguage, initWorkflowActionId, computeTargetCatalog, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1219                }
1220            }
1221
1222            _putInCopiedMap(srcContent, createdContent, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1223        }
1224        
1225        return (C) createdContent;
1226    }
1227    
1228    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)
1229    {
1230        if (createdContent instanceof Program)
1231        {
1232            copiedPrograms.put(srcContent.getId(), createdContent.getId());
1233        }
1234        else if (createdContent instanceof SubProgram)
1235        {
1236            copiedSubPrograms.put(srcContent.getId(), createdContent.getId());
1237        }
1238        else if (createdContent instanceof Container)
1239        {
1240            copiedContainers.put(srcContent.getId(), createdContent.getId());
1241        }
1242        else if (createdContent instanceof CourseList)
1243        {
1244            copiedCourseLists.put(srcContent.getId(), createdContent.getId());
1245        }
1246        else if (createdContent instanceof Course)
1247        {
1248            copiedCourses.put(srcContent.getId(), createdContent.getId());
1249        }
1250        else if (createdContent instanceof CoursePart)
1251        {
1252            copiedCourseParts.put(srcContent.getId(), createdContent.getId());
1253        }
1254    }
1255    
1256    /**
1257     * Copy the structure of a {@link ProgramItem}
1258     * @param srcContent the content to copy
1259     * @param targetContent the target content
1260     * @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.
1261     * @param initWorkflowActionId The initial workflow action id
1262     * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object.
1263     * @param copiedPrograms the id of initial programs with their copied content
1264     * @param copiedSubPrograms the id of initial subprograms with their copied content
1265     * @param copiedContainers the id of initial containers with their copied content
1266     * @param copiedCourseLists the id of initial course lists with their copied content
1267     * @param copiedCourses the id of initial courses with their copied content
1268     * @param copiedCourseParts the id of initial course parts with their copied content
1269     * @throws AmetysRepositoryException If an error occurred during copy
1270     * @throws WorkflowException If an error occurred during copy
1271     */
1272    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
1273    {
1274        List<ProgramItem> srcChildContents = new ArrayList<>();
1275        Map<Pair<String, String>, List<String>> values = new HashMap<>();
1276        
1277        String childMetadataPath = null;
1278        String parentMetadataPath = null;
1279        
1280        if (srcContent instanceof TraversableProgramPart)
1281        {
1282            childMetadataPath = TraversableProgramPart.CHILD_PROGRAM_PARTS;
1283            parentMetadataPath = ProgramPart.PARENT_PROGRAM_PARTS;
1284            srcChildContents.addAll(((TraversableProgramPart) srcContent).getProgramPartChildren());
1285        }
1286        else if (srcContent instanceof CourseList)
1287        {
1288            childMetadataPath = CourseList.CHILD_COURSES;
1289            parentMetadataPath = Course.PARENT_COURSE_LISTS;
1290            srcChildContents.addAll(((CourseList) srcContent).getCourses());
1291        }
1292        else if (srcContent instanceof Course)
1293        {
1294            childMetadataPath = Course.CHILD_COURSE_LISTS;
1295            parentMetadataPath = CourseList.PARENT_COURSES;
1296            srcChildContents.addAll(((Course) srcContent).getCourseLists());
1297
1298            List<String> refCoursePartIds = new ArrayList<>();
1299            for (CoursePart srcChildContent : ((Course) srcContent).getCourseParts())
1300            {
1301                CoursePart targetChildContent = copyCoursePart(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1302                refCoursePartIds.add(targetChildContent.getId());
1303            }
1304            _addFormValues(values, Course.CHILD_COURSE_PARTS, CoursePart.PARENT_COURSES, refCoursePartIds);
1305        }
1306
1307        List<String> refChildIds = new ArrayList<>();
1308        for (ProgramItem srcChildContent : srcChildContents)
1309        {
1310            ProgramItem targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
1311            refChildIds.add(targetChildContent.getId());
1312        }
1313
1314        _addFormValues(values, childMetadataPath, parentMetadataPath, refChildIds);
1315
1316        _editChildRelation((ModifiableWorkflowAwareContent) targetContent, values);
1317        
1318    }
1319    
1320    private void _addFormValues(Map<Pair<String, String>, List<String>> values, String childMetadataPath, String parentMetadataPath, List<String> refChildIds)
1321    {
1322        if (!refChildIds.isEmpty())
1323        {
1324            values.put(Pair.of(childMetadataPath, parentMetadataPath), refChildIds);
1325        }
1326    }
1327    
1328    private void _editChildRelation(ModifiableWorkflowAwareContent parentContent, Map<Pair<String, String>, List<String>> values) throws AmetysRepositoryException
1329    {
1330        if (!values.isEmpty())
1331        {
1332            for (Map.Entry<Pair<String, String>, List<String>> entry : values.entrySet())
1333            {
1334                String childMetadataName = entry.getKey().getLeft();
1335                String parentMetadataName = entry.getKey().getRight();
1336                List<String> childContents = entry.getValue();
1337                
1338                parentContent.setValue(childMetadataName, childContents.toArray(new String[childContents.size()]));
1339                
1340                for (String childContentId : childContents)
1341                {
1342                    ModifiableContent content = _resolver.resolveById(childContentId);
1343                    String[] parentContentIds = ContentDataHelper.getContentIdsArrayFromMultipleContentData(content, parentMetadataName);
1344                    content.setValue(parentMetadataName, ArrayUtils.add(parentContentIds, parentContent.getId()));
1345                }
1346            }
1347        }
1348    }
1349    
1350    /**
1351     * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure
1352     * @param createdContent The created content to clean
1353     */
1354    protected void _cleanContentMetadata(ModifiableContent createdContent)
1355    {
1356        ModifiableCompositeMetadata metadataHolder = createdContent.getMetadataHolder();
1357        if (createdContent instanceof ProgramPart)
1358        {
1359            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, ProgramPart.PARENT_PROGRAM_PARTS);
1360        }
1361        
1362        if (createdContent instanceof TraversableProgramPart)
1363        {
1364            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, TraversableProgramPart.CHILD_PROGRAM_PARTS);
1365        }
1366        
1367        if (createdContent instanceof CourseList)
1368        {
1369            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.CHILD_COURSES);
1370            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.PARENT_COURSES);
1371        }
1372        
1373        if (createdContent instanceof Course)
1374        {
1375            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.CHILD_COURSE_LISTS);
1376            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.PARENT_COURSE_LISTS);
1377            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.CHILD_COURSE_PARTS);
1378        }
1379        
1380        if (createdContent instanceof CoursePart)
1381        {
1382            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CoursePart.PARENT_COURSES);
1383        }
1384    }
1385    
1386    /**
1387     * Switch the ametys object to Live version if it has one
1388     * @param ao the Ametys object
1389     * @throws NoLiveVersionException if the content has no live version
1390     */
1391    public void switchToLiveVersion(DefaultAmetysObject ao) throws NoLiveVersionException
1392    {
1393        // Switch to the Live label if exists
1394        String[] allLabels = ao.getAllLabels();
1395        String[] currentLabels = ao.getLabels();
1396        
1397        boolean hasLiveVersion = Arrays.asList(allLabels).contains(ValidateODFContentFunction.VALID_LABEL);
1398        boolean currentVersionIsLive = Arrays.asList(currentLabels).contains(ValidateODFContentFunction.VALID_LABEL);
1399        
1400        if (hasLiveVersion && !currentVersionIsLive)
1401        {
1402            ao.switchToLabel(ValidateODFContentFunction.VALID_LABEL);
1403        }
1404        else if (!hasLiveVersion)
1405        {
1406            throw new NoLiveVersionException("The ametys object '" + ao.getId() + "' has no live version");
1407        }
1408    }
1409    
1410    /**
1411     * Switch to Live version if is required
1412     * @param ao the Ametys object
1413     * @throws NoLiveVersionException if the Live version is required but not exist
1414     */
1415    public void switchToLiveVersionIfNeeded(DefaultAmetysObject ao) throws NoLiveVersionException
1416    {
1417        Request request = _getRequest();
1418        if (request != null && request.getAttribute(REQUEST_ATTRIBUTE_VALID_LABEL) != null)
1419        {
1420            switchToLiveVersion(ao);
1421        }
1422    }
1423    
1424    /**
1425     * Count the hours accumulation in the {@link ProgramItem}
1426     * @param programItem The program item on which we compute the total number of hours
1427     * @return The hours accumulation
1428     */
1429    public Double getCumulatedHours(ProgramItem programItem)
1430    {
1431        // Ignore optional course list and avoid useless expensive calls
1432        if (programItem instanceof CourseList && ChoiceType.OPTIONAL.equals(((CourseList) programItem).getType()))
1433        {
1434            return 0.0;
1435        }
1436
1437        List<ProgramItem> children = getChildProgramItems(programItem);
1438
1439        Double coef = 1.0;
1440        Double countNbHours = 0.0;
1441
1442        // If the program item is a course list, compute the coef (mandatory: 1, optional: 0, optional: min / total)
1443        if (programItem instanceof CourseList)
1444        {
1445            // If there is no children, compute the coef is useless
1446            // Also choice list can throw an exception while dividing by zero
1447            if (children.isEmpty())
1448            {
1449                return 0.0;
1450            }
1451            
1452            CourseList courseList = (CourseList) programItem;
1453            switch (courseList.getType())
1454            {
1455                case CHOICE:
1456                    // Apply the average of number of EC from children multiply by the minimum ELP to select
1457                    coef = ((double) courseList.getMinNumberOfCourses()) / children.size();
1458                    break;
1459                case MANDATORY:
1460                default:
1461                    // Add all ECTS from children
1462                    break;
1463            }
1464        }
1465
1466        // If it's a course and we have a value for the number of hours
1467        // Then get the value
1468        if (programItem instanceof Course && ((Course) programItem).hasNonEmptyValue(Course.NUMBER_OF_HOURS))
1469        {
1470            countNbHours += ((Course) programItem).<Double>getValue(Course.NUMBER_OF_HOURS);
1471        }
1472        // Else if there are program item children on the item
1473        // Then compute on children
1474        else if (children.size() > 0)
1475        {
1476            for (ProgramItem child : children)
1477            {
1478                countNbHours += getCumulatedHours(child);
1479            }
1480        }
1481        // Else, it's a course but there is no value for the number of hours and we don't have program item children
1482        // Then compute on course parts
1483        else if (programItem instanceof Course)
1484        {
1485            countNbHours += ((Course) programItem).getCourseParts()
1486                .stream()
1487                .mapToDouble(CoursePart::getNumberOfHours)
1488                .sum();
1489        }
1490        
1491        return coef * countNbHours;
1492    }
1493    
1494    /**
1495     * Get the request
1496     * @return the request
1497     */
1498    protected Request _getRequest()
1499    {
1500        return ContextHelper.getRequest(_context);
1501    }
1502}