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