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