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.Objects;
027import java.util.Optional;
028import java.util.Set;
029import java.util.function.Function;
030import java.util.function.Predicate;
031import java.util.stream.Collectors;
032import java.util.stream.Stream;
033
034import org.apache.avalon.framework.activity.Initializable;
035import org.apache.avalon.framework.component.Component;
036import org.apache.avalon.framework.context.Context;
037import org.apache.avalon.framework.context.ContextException;
038import org.apache.avalon.framework.context.Contextualizable;
039import org.apache.avalon.framework.service.ServiceException;
040import org.apache.avalon.framework.service.ServiceManager;
041import org.apache.avalon.framework.service.Serviceable;
042import org.apache.cocoon.components.ContextHelper;
043import org.apache.cocoon.environment.Request;
044import org.apache.commons.lang3.ArrayUtils;
045import org.apache.commons.lang3.StringUtils;
046import org.apache.commons.lang3.tuple.Pair;
047
048import org.ametys.cms.CmsConstants;
049import org.ametys.cms.content.ContentHelper;
050import org.ametys.cms.content.references.OutgoingReferences;
051import org.ametys.cms.content.references.OutgoingReferencesExtractor;
052import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
053import org.ametys.cms.data.ContentDataHelper;
054import org.ametys.cms.repository.Content;
055import org.ametys.cms.repository.ContentDAO;
056import org.ametys.cms.repository.ContentQueryHelper;
057import org.ametys.cms.repository.ContentTypeExpression;
058import org.ametys.cms.repository.DefaultContent;
059import org.ametys.cms.repository.LanguageExpression;
060import org.ametys.cms.repository.ModifiableContent;
061import org.ametys.cms.repository.ModifiableWorkflowAwareContent;
062import org.ametys.cms.rights.ContentRightAssignmentContext;
063import org.ametys.core.cache.AbstractCacheManager;
064import org.ametys.core.cache.Cache;
065import org.ametys.core.ui.Callable;
066import org.ametys.odf.content.code.UniqueCodeGenerator;
067import org.ametys.odf.content.code.UniqueCodeGeneratorExtensionPoint;
068import org.ametys.odf.course.Course;
069import org.ametys.odf.course.CourseContainer;
070import org.ametys.odf.course.ShareableCourseHelper;
071import org.ametys.odf.courselist.CourseList;
072import org.ametys.odf.courselist.CourseList.ChoiceType;
073import org.ametys.odf.courselist.CourseListContainer;
074import org.ametys.odf.coursepart.CoursePart;
075import org.ametys.odf.coursepart.CoursePartFactory;
076import org.ametys.odf.data.EducationalPath;
077import org.ametys.odf.data.type.EducationalPathRepositoryElementType;
078import org.ametys.odf.orgunit.OrgUnit;
079import org.ametys.odf.orgunit.OrgUnitFactory;
080import org.ametys.odf.orgunit.RootOrgUnitProvider;
081import org.ametys.odf.person.Person;
082import org.ametys.odf.program.AbstractProgram;
083import org.ametys.odf.program.AbstractTraversableProgramPart;
084import org.ametys.odf.program.Container;
085import org.ametys.odf.program.Program;
086import org.ametys.odf.program.ProgramFactory;
087import org.ametys.odf.program.ProgramPart;
088import org.ametys.odf.program.SubProgram;
089import org.ametys.odf.program.TraversableProgramPart;
090import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
091import org.ametys.plugins.repository.AmetysObject;
092import org.ametys.plugins.repository.AmetysObjectExistsException;
093import org.ametys.plugins.repository.AmetysObjectIterable;
094import org.ametys.plugins.repository.AmetysObjectIterator;
095import org.ametys.plugins.repository.AmetysObjectResolver;
096import org.ametys.plugins.repository.AmetysRepositoryException;
097import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
098import org.ametys.plugins.repository.RepositoryConstants;
099import org.ametys.plugins.repository.UnknownAmetysObjectException;
100import org.ametys.plugins.repository.collection.AmetysObjectCollection;
101import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeater;
102import org.ametys.plugins.repository.data.holder.group.ModelAwareRepeaterEntry;
103import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
104import org.ametys.plugins.repository.model.RepositoryDataContext;
105import org.ametys.plugins.repository.query.QueryHelper;
106import org.ametys.plugins.repository.query.SortCriteria;
107import org.ametys.plugins.repository.query.expression.AndExpression;
108import org.ametys.plugins.repository.query.expression.Expression;
109import org.ametys.plugins.repository.query.expression.Expression.Operator;
110import org.ametys.plugins.repository.query.expression.OrExpression;
111import org.ametys.plugins.repository.query.expression.StringExpression;
112import org.ametys.runtime.config.Config;
113import org.ametys.runtime.i18n.I18nizableText;
114import org.ametys.runtime.model.ModelHelper;
115import org.ametys.runtime.model.ModelItem;
116import org.ametys.runtime.model.type.DataContext;
117import org.ametys.runtime.plugin.component.AbstractLogEnabled;
118
119import com.opensymphony.workflow.WorkflowException;
120
121/**
122 * Helper for ODF contents
123 *
124 */
125public class ODFHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable, Initializable
126{
127    /** The component role. */
128    public static final String ROLE = ODFHelper.class.getName();
129    
130    /** Request attribute to get the "Live" version of contents */
131    public static final String REQUEST_ATTRIBUTE_VALID_LABEL = "live-version";
132    
133    /** The default id of initial workflow action */
134    protected static final int __INITIAL_WORKFLOW_ACTION_ID = 0;
135    
136    private static final String __ANCESTORS_CACHE = ODFHelper.class.getName() + "$ancestors";
137    
138    private static final String __PARENT_PROGRAM_ITEMS_CACHE = ODFHelper.class.getName() + "$parentProgramItems";
139    
140    private static final String __ODF_PLUGIN_NAME = "odf";
141    
142    /** Ametys object resolver */
143    protected AmetysObjectResolver _resolver;
144    /** The content types manager */
145    protected ContentTypeExtensionPoint _cTypeEP;
146    /** Root orgunit */
147    protected RootOrgUnitProvider _ouRootProvider;
148    /** Helper for shareable course */
149    protected ShareableCourseHelper _shareableCourseHelper;
150    /** The Avalon context */
151    protected Context _context;
152    /** The cache manager */
153    protected AbstractCacheManager _cacheManager;
154    
155    /** The content helper */
156    protected ContentHelper _contentHelper;
157    
158    /** The outgoing references extractor */
159    protected OutgoingReferencesExtractor _outgoingReferencesExtractor;
160    
161    /** The content DAO **/
162    protected ContentDAO _contentDAO;
163    
164    private UniqueCodeGeneratorExtensionPoint _codeGeneratorEP;
165
166    public void contextualize(Context context) throws ContextException
167    {
168        _context = context;
169    }
170    
171    @Override
172    public void service(ServiceManager manager) throws ServiceException
173    {
174        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
175        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
176        _ouRootProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE);
177        _shareableCourseHelper = (ShareableCourseHelper) manager.lookup(ShareableCourseHelper.ROLE);
178        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
179        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
180        _outgoingReferencesExtractor = (OutgoingReferencesExtractor) manager.lookup(OutgoingReferencesExtractor.ROLE);
181        _contentDAO = (ContentDAO) manager.lookup(ContentDAO.ROLE);
182        _codeGeneratorEP = (UniqueCodeGeneratorExtensionPoint) manager.lookup(UniqueCodeGeneratorExtensionPoint.ROLE);
183    }
184    
185    public void initialize() throws Exception
186    {
187        _cacheManager.createRequestCache(__ANCESTORS_CACHE,
188                new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_ODF_ANCESTORS_LABEL"),
189                new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_ODF_ANCESTORS_DESCRIPTION"),
190                false);
191        
192        _cacheManager.createRequestCache(__PARENT_PROGRAM_ITEMS_CACHE,
193                new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_ODF_PARENT_PROGRAM_ITEMS_LABEL"),
194                new I18nizableText("plugin.odf", "PLUGINS_ODF_CACHE_ODF_PARENT_PROGRAM_ITEMS_DESCRIPTION"),
195                false);
196    }
197    
198    /**
199     * Determines if legacy sax of program structure should be used
200     * @param programItem the program item beeing rendering
201     * @return true if legacy sax of program structure should be used
202     */
203    public boolean useLegacyProgramStructure(ProgramItem programItem)
204    {
205        return true;
206    }
207    
208    /**
209     * Generate an unique code for a given ODF content type
210     * @param cTypeId The id of ODF content type
211     * @return The unique code
212     */
213    public String generateUniqueCode(String cTypeId)
214    {
215        String codeGenId = Config.getInstance().getValue("odf.code.generator", true, "odf-code-incremental");
216        UniqueCodeGenerator codeGen = _codeGeneratorEP.getExtension(codeGenId);
217        
218        return codeGen.generateUniqueCode(cTypeId);
219    }
220    
221    /**
222     * Gets the root for ODF contents
223     * @return the root for ODF contents
224     */
225    public AmetysObjectCollection getRootContent()
226    {
227        return getRootContent(false);
228    }
229    
230    /**
231     * Gets the root for ODF contents
232     * @param create <code>true</code> to create automatically the root when missing.
233     * @return the root for ODF contents
234     */
235    public AmetysObjectCollection getRootContent(boolean create)
236    {
237        ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/");
238        
239        boolean needSave = false;
240        if (!pluginsNode.hasChild(__ODF_PLUGIN_NAME))
241        {
242            if (create)
243            {
244                pluginsNode.createChild(__ODF_PLUGIN_NAME, "ametys:unstructured");
245                needSave = true;
246            }
247            else
248            {
249                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + __ODF_PLUGIN_NAME + "' is missing");
250            }
251        }
252        
253        ModifiableTraversableAmetysObject pluginNode = pluginsNode.getChild(__ODF_PLUGIN_NAME);
254        if (!pluginNode.hasChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents"))
255        {
256            if (create)
257            {
258                pluginNode.createChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents", "ametys:collection");
259                needSave = true;
260            }
261            else
262            {
263                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + __ODF_PLUGIN_NAME + "/ametys:contents' is missing");
264            }
265        }
266        
267        if (needSave)
268        {
269            pluginsNode.saveChanges();
270        }
271        
272        return pluginNode.getChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents");
273    }
274    
275    /**
276     * Get the {@link ProgramItem}s matching the given arguments
277     * @param cTypeId The id of content type. Can be null to get program's items whatever their content type.
278     * @param code The code. Can be null to get program's items regardless of their code
279     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
280     * @param lang The search language. Can be null to get program's items regardless of their language
281     * @param <C> The content return type
282     * @return The matching program items
283     */
284    public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang)
285    {
286        return getProgramItems(cTypeId, code, catalogName, lang, null, null);
287    }
288    
289    /**
290     * Get the {@link ProgramItem}s matching the given arguments
291     * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type.
292     * @param code The code. Can be null to get program's items regardless of their code
293     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
294     * @param lang The search language. Can be null to get program's items regardless of their language
295     * @param <C> The content return type
296     * @return The matching program items
297     */
298    public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang)
299    {
300        return getProgramItems(cTypeIds, code, catalogName, lang, null, null);
301    }
302    
303    /**
304     * Get the {@link ProgramItem}s matching the given arguments
305     * @param cTypeId The id of content type. Can be null to get program's items whatever their content type.
306     * @param code The code. Can be null to get program's items regardless of their code
307     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
308     * @param lang The search language. Can be null to get program's items regardless of their language
309     * @param additionnalExpr An additional expression for filtering result. Can be null
310     * @param sortCriteria criteria for sorting results
311     * @param <C> The content return type
312     * @return The matching program items
313     */
314    public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria)
315    {
316        return getProgramItems(cTypeId != null ? Collections.singletonList(cTypeId) : Collections.EMPTY_LIST, code, catalogName, lang, additionnalExpr, sortCriteria);
317    }
318    
319    /**
320     * Get the {@link ProgramItem}s matching the given arguments
321     * @param cTypeIds The id of content types. Can be empty to get program's items whatever their content type.
322     * @param code The code. Can be null to get program's items regardless of their code
323     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
324     * @param lang The search language. Can be null to get program's items regardless of their language
325     * @param additionnalExpr An additional expression for filtering result. Can be null
326     * @param sortCriteria criteria for sorting results
327     * @param <C> The content return type
328     * @return The matching program items
329     */
330    public <C extends Content> AmetysObjectIterable<C> getProgramItems(Collection<String> cTypeIds, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria)
331    {
332        List<Expression> exprs = new ArrayList<>();
333        
334        if (!cTypeIds.isEmpty())
335        {
336            exprs.add(new ContentTypeExpression(Operator.EQ, cTypeIds.toArray(new String[cTypeIds.size()])));
337        }
338        if (StringUtils.isNotEmpty(code))
339        {
340            exprs.add(new StringExpression(ProgramItem.CODE, Operator.EQ, code));
341        }
342        if (StringUtils.isNotEmpty(catalogName))
343        {
344            exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName));
345        }
346        if (StringUtils.isNotEmpty(lang))
347        {
348            exprs.add(new LanguageExpression(Operator.EQ, lang));
349        }
350        if (additionnalExpr != null)
351        {
352            exprs.add(additionnalExpr);
353        }
354        
355        Expression expr = new AndExpression(exprs.toArray(new Expression[exprs.size()]));
356        
357        String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr, sortCriteria);
358        return _resolver.query(xpathQuery);
359    }
360    
361    /**
362     * Get the equivalent {@link CoursePart} of the source {@link CoursePart} in given catalog and language
363     * @param srcCoursePart The source course part
364     * @param catalogName The name of catalog to search into
365     * @param lang The search language
366     * @return The equivalent program item or <code>null</code> if not exists
367     */
368    public CoursePart getCoursePart(CoursePart srcCoursePart, String catalogName, String lang)
369    {
370        return getODFContent(CoursePartFactory.COURSE_PART_CONTENT_TYPE, srcCoursePart.getCode(), catalogName, lang);
371    }
372    
373    /**
374     * Get the equivalent {@link ProgramItem} of the source {@link ProgramItem} in given catalog and language
375     * @param <T> The type of returned object, it have to be a subclass of {@link ProgramItem}
376     * @param srcProgramItem The source program item
377     * @param catalogName The name of catalog to search into
378     * @param lang The search language
379     * @return The equivalent program item or <code>null</code> if not exists
380     */
381    public <T extends ProgramItem> T getProgramItem(T srcProgramItem, String catalogName, String lang)
382    {
383        return getODFContent(((Content) srcProgramItem).getTypes()[0], srcProgramItem.getCode(), catalogName, lang);
384    }
385    
386    /**
387     * Get the equivalent {@link Content} having the same code in given catalog and language
388     * @param <T> The type of returned object, it have to be a subclass of {@link AmetysObject}
389     * @param contentType The content type to search for
390     * @param odfContentCode The code of the ODF content
391     * @param catalogName The name of catalog to search into
392     * @param lang The search language
393     * @return The equivalent content or <code>null</code> if not exists
394     */
395    public <T extends AmetysObject> T getODFContent(String contentType, String odfContentCode, String catalogName, String lang)
396    {
397        Expression contentTypeExpr = new ContentTypeExpression(Operator.EQ, contentType);
398        Expression langExpr = new LanguageExpression(Operator.EQ, lang);
399        Expression catalogExpr = new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalogName);
400        Expression codeExpr = new StringExpression(ProgramItem.CODE, Operator.EQ, odfContentCode);
401        
402        Expression expr = new AndExpression(contentTypeExpr, langExpr, catalogExpr, codeExpr);
403        
404        String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr);
405        AmetysObjectIterable<T> contents = _resolver.query(xpathQuery);
406        AmetysObjectIterator<T> contentsIt = contents.iterator();
407        if (contentsIt.hasNext())
408        {
409            return contentsIt.next();
410        }
411        
412        return null;
413    }
414    
415    /**
416     * Get the child program items of a {@link ProgramItem}
417     * @param programItem The program item
418     * @return The child program items
419     */
420    public List<ProgramItem> getChildProgramItems(ProgramItem programItem)
421    {
422        List<ProgramItem> children = new ArrayList<>();
423        
424        if (programItem instanceof TraversableProgramPart programPart)
425        {
426            children.addAll(programPart.getProgramPartChildren());
427        }
428        
429        if (programItem instanceof CourseContainer courseContainer)
430        {
431            children.addAll(courseContainer.getCourses());
432        }
433        
434        if (programItem instanceof Course course)
435        {
436            children.addAll(course.getCourseLists());
437        }
438        
439        return children;
440    }
441
442    /**
443     * Get the child subprograms of a {@link ProgramPart}
444     * @param programPart The program part
445     * @return The child subprograms
446     */
447    public Set<SubProgram> getChildSubPrograms(ProgramPart programPart)
448    {
449        Set<SubProgram> subPrograms = new HashSet<>();
450        
451        if (programPart instanceof TraversableProgramPart traversableProgram)
452        {
453            if (programPart instanceof SubProgram subProgram)
454            {
455                subPrograms.add(subProgram);
456            }
457            traversableProgram.getProgramPartChildren().forEach(child -> subPrograms.addAll(getChildSubPrograms(child)));
458        }
459
460        return subPrograms;
461    }
462    
463    /**
464     * Gets (recursively) parent containers of this program item.
465     * @param programItem The program item
466     * @return parent containers of this program item.
467     */
468    public Set<Container> getParentContainers(ProgramItem programItem)
469    {
470        return getParentContainers(programItem, false);
471    }
472    
473    /**
474     * Gets (recursively) parent containers of this program item.
475     * @param programItem The program item
476     * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned.
477     * @return parent containers of this program item.
478     */
479    public Set<Container> getParentContainers(ProgramItem programItem, boolean continueIfFound)
480    {
481        return _getParentsOfType(programItem, Container.class, continueIfFound);
482    }
483
484    /**
485     * Gets (recursively) parent programs of this course part.
486     * @param coursePart The course part
487     * @return parent programs of this course part.
488     */
489    public Set<Program> getParentPrograms(CoursePart coursePart)
490    {
491        Set<Program> programs = new HashSet<>();
492        for (Course course : coursePart.getCourses())
493        {
494            programs.addAll(getParentPrograms(course));
495        }
496        return programs;
497    }
498    
499    /**
500     * Gets (recursively) parent programs of this program item.
501     * @param programItem The program item
502     * @return parent programs of this program item.
503     */
504    public Set<Program> getParentPrograms(ProgramItem programItem)
505    {
506        return getParentPrograms(programItem, false);
507    }
508    
509    /**
510     * Gets (recursively) parent programs of this program item.
511     * @param programItem The program item
512     * @param includingItself <code>true</code> to return the program item if it's a {@link Program}.
513     * @return parent programs of this program item.
514     */
515    public Set<Program> getParentPrograms(ProgramItem programItem, boolean includingItself)
516    {
517        if (includingItself && programItem instanceof Program program)
518        {
519            return Set.of(program);
520        }
521        
522        return _getParentsOfType(programItem, Program.class, false);
523    }
524
525    /**
526     * Gets (recursively) parent subprograms of this course part.
527     * @param coursePart The course part
528     * @return parent subprograms of this course part.
529     */
530    public Set<SubProgram> getParentSubPrograms(CoursePart coursePart)
531    {
532        return getParentSubPrograms(coursePart, false);
533    }
534
535    /**
536     * Gets (recursively) parent subprograms of this course part.
537     * @param coursePart The course part
538     * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned.
539     * @return parent subprograms of this course part.
540     */
541    public Set<SubProgram> getParentSubPrograms(CoursePart coursePart, boolean continueIfFound)
542    {
543        Set<SubProgram> abstractPrograms = new HashSet<>();
544        for (Course course : coursePart.getCourses())
545        {
546            abstractPrograms.addAll(getParentSubPrograms(course, continueIfFound));
547        }
548        return abstractPrograms;
549    }
550    
551    /**
552     * Gets (recursively) parent subprograms of this program item.
553     * @param programItem The program item
554     * @return parent subprograms of this program item.
555     */
556    public Set<SubProgram> getParentSubPrograms(ProgramItem programItem)
557    {
558        return getParentSubPrograms(programItem, false);
559    }
560    
561    /**
562     * Gets (recursively) parent subprograms of this program item.
563     * @param programItem The program item
564     * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned.
565     * @return parent subprograms of this program item.
566     */
567    public Set<SubProgram> getParentSubPrograms(ProgramItem programItem, boolean continueIfFound)
568    {
569        return _getParentsOfType(programItem, SubProgram.class, continueIfFound);
570    }
571
572    /**
573     * Gets (recursively) parent abstract programs of this course part.
574     * @param coursePart The course part
575     * @return parent abstract programs of this course part.
576     */
577    public Set<AbstractProgram> getParentAbstractPrograms(CoursePart coursePart)
578    {
579        return getParentAbstractPrograms(coursePart, false);
580    }
581
582    /**
583     * Gets (recursively) parent abstract programs of this course part.
584     * @param coursePart The course part
585     * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned.
586     * @return parent abstract programs of this course part.
587     */
588    public Set<AbstractProgram> getParentAbstractPrograms(CoursePart coursePart, boolean continueIfFound)
589    {
590        Set<AbstractProgram> abstractPrograms = new HashSet<>();
591        for (Course course : coursePart.getCourses())
592        {
593            abstractPrograms.addAll(getParentAbstractPrograms(course, continueIfFound));
594        }
595        return abstractPrograms;
596    }
597    
598    /**
599     * Gets (recursively) parent abstract programs of this program item.
600     * @param programItem The program item
601     * @return parent abstract programs of this program item.
602     */
603    public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem)
604    {
605        return getParentAbstractPrograms(programItem, false);
606    }
607    
608    /**
609     * Gets (recursively) parent abstract programs of this program item.
610     * @param programItem The program item
611     * @param continueIfFound If <code>true</code> continue searching corresponding parents in the parent structure, otherwise only closest items are returned.
612     * @return parent abstract programs of this program item.
613     */
614    public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem, boolean continueIfFound)
615    {
616        return _getParentsOfType(programItem, AbstractProgram.class, continueIfFound);
617    }
618
619    private <T> Set<T> _getParentsOfType(ProgramItem programItem, Class<T> classToTest, boolean continueIfFound)
620    {
621        Set<ProgramItem> visitedProgramItems = new HashSet<>();
622        visitedProgramItems.add(programItem);
623        return _getParentsOfType(programItem, visitedProgramItems, classToTest, continueIfFound);
624    }
625    
626    @SuppressWarnings("unchecked")
627    private <T> Set<T> _getParentsOfType(ProgramItem programItem, Set<ProgramItem> visitedProgramItems, Class<T> classToTest, boolean continueIfFound)
628    {
629        Set<T> parentsOfType = new HashSet<>();
630        List<ProgramItem> parents = getParentProgramItems(programItem);
631        
632        for (ProgramItem parent : parents)
633        {
634            // Only parents not already visited
635            if (visitedProgramItems.add(parent))
636            {
637                // Cast to Content if instance of Content instead of another type (for structures containing both Container and SubProgram)
638                boolean found = false;
639                if (classToTest.isInstance(parent))
640                {
641                    parentsOfType.add((T) parent);
642                    found = true;
643                }
644                
645                if (!found || continueIfFound)
646                {
647                    parentsOfType.addAll(_getParentsOfType(parent, visitedProgramItems, classToTest, continueIfFound));
648                }
649            }
650        }
651        
652        return parentsOfType;
653    }
654    
655    /**
656     * Get the programs that belong to a given {@link OrgUnit} or a child orgunit
657     * @param orgUnit The orgUnit. Can be null
658     * @param catalog The catalog. Can be null.
659     * @param lang The lang. Can be null.
660     * @return The child programs as unmodifiable list
661     */
662    public List<Program> getProgramsFromOrgUnit(OrgUnit orgUnit, String catalog, String lang)
663    {
664        return getProgramsFromOrgUnit(orgUnit, catalog, lang, true);
665    }
666    
667    /**
668     * Get the child programs of an {@link OrgUnit}
669     * @param orgUnit The orgUnit. Can be null
670     * @param catalog The catalog. Can be null.
671     * @param lang The lang. Can be null.
672     * @param browseChildOrgunits Set to true to get programs among child orgunits recursively, false to get only programs directly linked to the given orgunit.
673     * @return The child programs as unmodifiable list
674     */
675    public List<Program> getProgramsFromOrgUnit(OrgUnit orgUnit, String catalog, String lang, boolean browseChildOrgunits)
676    {
677        Expression ouExpr = null;
678        if (orgUnit != null)
679        {
680            if (!browseChildOrgunits)
681            {
682                ouExpr = new StringExpression(ProgramItem.ORG_UNITS_REFERENCES, Operator.EQ, orgUnit.getId());
683            }
684            else
685            {
686                ouExpr = new OrExpression();
687                for (String orgUnitId : getSubOrgUnitIds(orgUnit))
688                {
689                    ((OrExpression) ouExpr).add(new StringExpression(ProgramItem.ORG_UNITS_REFERENCES, Operator.EQ, orgUnitId));
690                }
691            }
692        }
693        
694        AmetysObjectIterable<Program> programs = getProgramItems(ProgramFactory.PROGRAM_CONTENT_TYPE, null, catalog, lang, ouExpr, null);
695        return programs.stream().toList();
696    }
697    
698    /**
699     * Get the current orgunit and its suborgunits recursively identifiers.
700     * @param orgUnit The orgunit at the top
701     * @return A {@link List} of {@link OrgUnit} ids
702     */
703    public List<String> getSubOrgUnitIds(OrgUnit orgUnit)
704    {
705        List<String> orgUnitIds = new ArrayList<>();
706        orgUnitIds.add(orgUnit.getId());
707        for (String id : orgUnit.getSubOrgUnits())
708        {
709            OrgUnit childOrgUnit = _resolver.resolveById(id);
710            orgUnitIds.addAll(getSubOrgUnitIds(childOrgUnit));
711        }
712        
713        return orgUnitIds;
714    }
715    
716    /**
717     * Get all program item linked to the given orgUnit
718     * @param orgUnit the given orgUnit
719     * @return the set of program item
720     */
721    public Set<Program> getProgramsReferencingOrgunit(OrgUnit orgUnit)
722    {
723        StringExpression stringExpression = new StringExpression(ProgramItem.ORG_UNITS_REFERENCES, Operator.EQ, orgUnit.getId());
724        String contentXPathQuery = ContentQueryHelper.getContentXPathQuery(stringExpression);
725        
726        return _resolver.<Content>query(contentXPathQuery)
727            .stream()
728            .filter(ProgramItem.class::isInstance)
729            .map(ProgramItem.class::cast)
730            .map(p -> getParentPrograms(p, true))
731            .flatMap(Set::stream)
732            .collect(Collectors.toSet());
733    }
734    
735    /**
736     * Get the linked program items to the given person
737     * @param person the person
738     * @return the set of program items
739     */
740    public Set<Program> getProgramsReferencingPerson(Person person)
741    {
742        return _contentHelper.getReferencingContents(person)
743            .stream()
744            .map(Pair::getValue)
745            .map(this::_getProgramsReferencingContent)
746            .flatMap(Set::stream)
747            .collect(Collectors.toSet());
748    }
749    
750    private Set<Program> _getProgramsReferencingContent(Content content)
751    {
752        if (content instanceof ProgramItem programItem)
753        {
754            return getParentPrograms(programItem, true);
755        }
756        else if (content instanceof OrgUnit orgUnit)
757        {
758            return getProgramsReferencingOrgunit(orgUnit);
759        }
760        
761        return Set.of();
762    }
763    
764    /**
765     * Determines if the {@link ProgramItem} has parent program items
766     * @param programItem The program item
767     * @return true if has parent program items
768     */
769    public boolean hasParentProgramItems(ProgramItem programItem)
770    {
771        boolean hasParent = false;
772        
773        if (programItem instanceof ProgramPart programPart)
774        {
775            hasParent = !programPart.getProgramPartParents().isEmpty() || hasParent;
776        }
777        
778        if (programItem instanceof CourseList courseList)
779        {
780            hasParent = !courseList.getParentCourses().isEmpty() || hasParent;
781        }
782        
783        if (programItem instanceof Course course)
784        {
785            hasParent = !course.getParentCourseLists().isEmpty() || hasParent;
786        }
787        
788        return hasParent;
789    }
790    
791    /**
792     * Get the parent program items of a {@link ProgramItem}
793     * @param programItem The program item
794     * @return The parent program items
795     */
796    public List<ProgramItem> getParentProgramItems(ProgramItem programItem)
797    {
798        return getParentProgramItems(programItem, null);
799    }
800    
801    /**
802     * Get the program item parents into the given ancestor {@link ProgramPart}
803     * @param programItem The program item
804     * @param parentProgramPart The parent program, subprogram or container. If null, all parents program items will be returned.
805     * @return The parent program items which have given parent program part has an ancestor
806     */
807    public List<ProgramItem> getParentProgramItems(ProgramItem programItem, ProgramPart parentProgramPart)
808    {
809        Cache<ParentProgramItemsCacheKey, List<ProgramItem>> cache = _cacheManager.get(__PARENT_PROGRAM_ITEMS_CACHE);
810        
811        return cache.get(ParentProgramItemsCacheKey.of(programItem.getId(), parentProgramPart != null ? parentProgramPart.getId() : "__NOPARENT"), item -> {
812            List<ProgramItem> parents = new ArrayList<>();
813            
814            if (programItem instanceof Program)
815            {
816                return parents;
817            }
818            
819            if (programItem instanceof ProgramPart programPart)
820            {
821                List<ProgramPart> allParents = programPart.getProgramPartParents();
822                
823                for (ProgramPart parent : allParents)
824                {
825                    if (parentProgramPart == null || parent.equals(parentProgramPart))
826                    {
827                        parents.add(parent);
828                    }
829                    else if (!getParentProgramItems(parent, parentProgramPart).isEmpty())
830                    {
831                        parents.add(parent);
832                    }
833                }
834            }
835            
836            if (programItem instanceof CourseList courseList)
837            {
838                for (Course parentCourse : courseList.getParentCourses())
839                {
840                    if (!getParentProgramItems(parentCourse, parentProgramPart).isEmpty())
841                    {
842                        parents.add(parentCourse);
843                    }
844                }
845            }
846            
847            if (programItem instanceof Course course)
848            {
849                for (CourseList cl : course.getParentCourseLists())
850                {
851                    if (!getParentProgramItems(cl, parentProgramPart).isEmpty())
852                    {
853                        parents.add(cl);
854                    }
855                    
856                }
857            }
858            
859            return parents;
860        });
861    }
862    
863    private static class ParentProgramItemsCacheKey extends AbstractCacheKey
864    {
865        public ParentProgramItemsCacheKey(String programItemId, String parentProgramPartId)
866        {
867            super(programItemId, parentProgramPartId);
868        }
869        
870        public static ParentProgramItemsCacheKey of(String programItemId, String parentProgramPartId)
871        {
872            return new ParentProgramItemsCacheKey(programItemId, parentProgramPartId);
873        }
874    }
875    
876    /**
877     * Get the first nearest program item parent into the given parent {@link AbstractProgram}
878     * @param programItem The program item
879     * @param parentProgram The parent program or subprogram. If null, the nearest abstract program will be returned.
880     * @return The parent program item or null if not found.
881     */
882    public ProgramItem getParentProgramItem(ProgramItem programItem, AbstractProgram parentProgram)
883    {
884        List<ProgramItem> parentProgramItems = getParentProgramItems(programItem, parentProgram);
885        return parentProgramItems.isEmpty() ? null : parentProgramItems.get(0);
886    }
887    
888    /**
889     * Get information of the program item
890     * @param programItemId the program item id
891     * @param programItemPathIds the list of program item ids containing in the path of the program item ... starting with itself. Can be null or empty
892     * @return a map of information
893     */
894    @Callable
895    public Map<String, Object> getProgramItemInfo(String programItemId, List<String> programItemPathIds)
896    {
897        Map<String, Object> results = new HashMap<>();
898        ProgramItem programItem = _resolver.resolveById(programItemId);
899
900        // Get catalog
901        String catalog = programItem.getCatalog();
902        if (StringUtils.isNotBlank(catalog))
903        {
904            results.put("catalog", catalog);
905        }
906        
907        // Get the orgunits
908        List<String> orgUnits = programItem.getOrgUnits();
909        if (programItemPathIds == null || programItemPathIds.isEmpty())
910        {
911            // The programItemPathIds is null or empty because we do not know the program item context.
912            // so get the information in the parent structure if unique.
913            while (programItem != null && orgUnits.isEmpty())
914            {
915                orgUnits = programItem.getOrgUnits();
916                List<ProgramItem> parentProgramItems = getParentProgramItems(programItem);
917                programItem = parentProgramItems.size() == 1 ? parentProgramItems.get(0) : null;
918            }
919        }
920        else // We have the program item context: parent structure is known ...
921        {
922            // ... the first element of the programItemPathIds is the programItem itself, so begin to index 1
923            int position = 1;
924            int size = programItemPathIds.size();
925            while (position < size && orgUnits.isEmpty())
926            {
927                programItem = _resolver.resolveById(programItemPathIds.get(position));
928                orgUnits = programItem.getOrgUnits();
929                position++;
930            }
931        }
932        results.put("orgUnits", orgUnits);
933        
934        return results;
935    }
936    
937    /**
938     * Get information of the program item structure (type, if program has children) or orgunit (no structure for now)
939     * @param contentId the content id
940     * @return a map of information
941     */
942    @Callable
943    public Map<String, Object> getStructureInfo(String contentId)
944    {
945        Map<String, Object> results = new HashMap<>();
946        
947        if (StringUtils.isNotBlank(contentId))
948        {
949            Content content = _resolver.resolveById(contentId);
950            if (content instanceof ProgramItem programItem)
951            {
952                results.put("id", contentId);
953                results.put("title", content.getTitle());
954                results.put("code", programItem.getDisplayCode());
955                
956                List<ProgramItem> childProgramItems = getChildProgramItems(programItem);
957                results.put("hasChildren", !childProgramItems.isEmpty());
958                
959                List<ProgramItem> parentProgramItems = getParentProgramItems(programItem);
960                results.put("hasParent", !parentProgramItems.isEmpty());
961                
962                results.put("paths", getPaths(programItem, " > "));
963            }
964            else if (content instanceof OrgUnit orgunit)
965            {
966                results.put("id", contentId);
967                results.put("title", content.getTitle());
968                results.put("code", orgunit.getUAICode());
969                
970                // Always to false, we don't manage complete copy with children
971                results.put("hasChildren", false);
972                
973                results.put("hasParent", orgunit.getParentOrgUnit() != null);
974                
975                results.put("paths", List.of(getOrgUnitPath(orgunit, " > ")));
976            }
977        }
978        
979        return results;
980    }
981    
982    /**
983     * Get information of the program item structure (type, if program has children)
984     * @param programItemIds the list of program item id
985     * @return a map of information
986     */
987    @Callable
988    public Map<String, Map<String, Object>> getStructureInfo(List<String> programItemIds)
989    {
990        Map<String,  Map<String, Object>> results = new HashMap<>();
991        
992        for (String programItemId : programItemIds)
993        {
994            results.put(programItemId, getStructureInfo(programItemId));
995        }
996        
997        return results;
998    }
999    
1000    /**
1001     * Get all the path of the orgunit.<br>
1002     * The path is built with the contents' title and code
1003     * @param orgunit The orgunit
1004     * @param separator The path separator
1005     * @return the path in parent orgunit
1006     */
1007    public String getOrgUnitPath(OrgUnit orgunit, String separator)
1008    {
1009        String path = orgunit.getTitle() + " (" + orgunit.getUAICode() + ")";
1010        OrgUnit parent = orgunit.getParentOrgUnit();
1011        if (parent != null)
1012        {
1013            path = getOrgUnitPath(parent, separator) + separator + path;
1014        }
1015        return path;
1016    }
1017    
1018    /**
1019     * Get all {@link EducationalPath} of a {@link ProgramItem} as readable values
1020     * The path is built with the contents' title and code
1021     * @param item The program item
1022     * @param separator The path separator
1023     * @return the paths in parent program items
1024     */
1025    public List<String> getPaths(ProgramItem item, String separator)
1026    {
1027        Function<ProgramItem, String> mapper = c -> ((Content) c).getTitle() + " (" + c.getDisplayCode() + ")";
1028        return getPaths(item, separator, mapper, true);
1029    }
1030    
1031    /**
1032     * Get all {@link EducationalPath} of a {@link ProgramItem} as readable values
1033     * The path is built with the mapper function.
1034     * @param item The program item
1035     * @param separator The path separator
1036     * @param mapper the function to apply to each program item to build the path
1037     * @param includeItself set to false to not include final item in path
1038     * @return the paths in parent program items
1039     */
1040    public List<String> getPaths(ProgramItem item, String separator, Function<ProgramItem, String> mapper, boolean includeItself)
1041    {
1042        return getPaths(item, separator, mapper, x -> true, includeItself, false);
1043    }
1044    
1045    /**
1046     * Get all {@link EducationalPath} of a {@link ProgramItem} as readable values
1047     * The path is built with the mapper function.
1048     * @param item The program item
1049     * @param separator The path separator
1050     * @param mapper the function to apply to each program item to build the path
1051     * @param filterPathSegment predicate to exclude some program item of path
1052     * @param includeItself set to false to not include final item in path
1053     * @param ignoreOrphanPath set to true to ignore paths that is not part of a Program
1054     * @return the paths in parent program items
1055     */
1056    public List<String> getPaths(ProgramItem item, String separator, Function<ProgramItem, String> mapper, Predicate<ProgramItem> filterPathSegment, boolean includeItself, boolean ignoreOrphanPath)
1057    {
1058        List<EducationalPath> educationalPaths = getEducationalPaths(item, includeItself, ignoreOrphanPath);
1059        
1060        return educationalPaths.stream()
1061            .map(p -> getEducationalPathAsString(p, mapper, separator, filterPathSegment))
1062            .collect(Collectors.toList());
1063    }
1064    
1065    /**
1066     * Get the value of an attribute for a given educational path. The attribute is necessarily in a repeater composed of at least one educational path attribute and the attribute to be retrieved.
1067     * @param <T> The type of the value returned by the path
1068     * @param programItem The program item
1069     * @param path Full or partial educational path. Cannot be null. In case of partial educational path (ie., no root program) the full possible paths will be computed.
1070     * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName)
1071     * @return the value for this educational path
1072     */
1073    public <T> Optional<T> getValueForPath(ProgramItem programItem, String dataPath, EducationalPath path)
1074    {
1075        return getValueForPath(programItem, dataPath, List.of(path));
1076    }
1077    
1078    
1079    /**
1080     * Get the value of an attribute for a given educational path. The attribute is necessarily in a repeater composed of at least one educational path attribute and the attribute to be retrieved.
1081     * @param <T> The type of the value returned by the path
1082     * @param programItem The program item
1083     * @param paths Full educational paths (from root program). Cannot be null nor empty.
1084     * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName)
1085     * @return the value for this educational path
1086     */
1087    public <T> Optional<T> getValueForPath(ProgramItem programItem, String dataPath, List<EducationalPath> paths)
1088    {
1089        String repeaterPath = StringUtils.substringBeforeLast(dataPath, "/");
1090        String attributeName = StringUtils.substringAfterLast(dataPath, "/");
1091        
1092        Content content = (Content) programItem;
1093        if (!content.hasValue(repeaterPath))
1094        {
1095            // Repeater is not defined
1096            getLogger().warn("There is no repeater '{}' defined for content '{}'", repeaterPath, programItem.getId());
1097            return Optional.empty();
1098        }
1099        
1100        // Get the repeater
1101        ModelAwareRepeater repeater = content.getRepeater(repeaterPath);
1102        
1103        // Get a value for each value in paths because if it is multiple, we want to check all paths, keep null values
1104        List<T> values = paths.stream()
1105             .map(path ->
1106                 getRepeaterEntriesByPath(repeater, List.of(path))
1107                     .findFirst()
1108                     .map(e -> e.<T>getValue(attributeName))
1109                     .orElse(null)
1110             )
1111             .toList();
1112        
1113        boolean isSameValues = values.stream().distinct().count() == 1;
1114        if (!isSameValues)
1115        {
1116            // No same values for each path
1117            getLogger().warn("Unable to determine value for '{}' attribute of content '{}'. Multiple educational paths are available for requested context with no same values", dataPath, programItem.getId());
1118            return Optional.empty();
1119        }
1120
1121        // Same value for each available paths => return this common value
1122        return values.stream().filter(Objects::nonNull).findFirst();
1123    }
1124    
1125    /**
1126     * Get the value of an attribute for each available educational paths
1127     * @param <T> The type of the values returned by the path
1128     * @param programItem The program item
1129     * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName)
1130     * @param defaultValue The default value to use if the repeater contains no entry for this educational path. Can be null.
1131     * @return the values for each educational paths
1132     */
1133    public <T> Map<EducationalPath, T> getValuesForPaths(ProgramItem programItem, String dataPath, T defaultValue)
1134    {
1135        return getValuesForPaths(programItem, dataPath, getEducationalPaths(programItem), defaultValue);
1136    }
1137    
1138    /**
1139     * Get the value of an attribute for each given educational paths
1140     * @param <T> The type of the value returned by the path
1141     * @param programItem The program item
1142     * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName)
1143     * @param paths The full educational paths (from root programs)
1144     * @param defaultValue The default value to use if the repeater contains no entry for this educational path. Can be null.
1145     * @return the values for each educational paths
1146     */
1147    @SuppressWarnings("unchecked")
1148    public <T> Map<EducationalPath, T> getValuesForPaths(ProgramItem programItem, String dataPath, List<EducationalPath> paths, T defaultValue)
1149    {
1150        Map<EducationalPath, T> valuesByPath = new HashMap<>();
1151        
1152        paths.stream().forEach(path -> {
1153            valuesByPath.put(path, (T) getValueForPath(programItem, dataPath, path).orElse(defaultValue));
1154        });
1155        
1156        return valuesByPath;
1157    }
1158    
1159    /**
1160     * Determines if the values of an attribute depending of a educational path is the same for all available educational paths
1161     * @param programItem The program item
1162     * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName)
1163     * @return true if the value is the same for all available educational paths
1164     */
1165    public boolean isSameValueForAllPaths(ProgramItem programItem, String dataPath)
1166    {
1167        return isSameValueForPaths(programItem, dataPath, getEducationalPaths(programItem));
1168    }
1169    
1170    /**
1171     * Determines if the values of an attribute depending of a educational path is the same for all available educational paths
1172     * @param programItem The program item
1173     * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName)
1174     * @param paths The full educational paths (from root programs)
1175     * @return true if the value is the same for all available educational paths
1176     */
1177    public boolean isSameValueForPaths(ProgramItem programItem, String dataPath, List<EducationalPath> paths)
1178    {
1179        String repeaterPath = StringUtils.substringBeforeLast(dataPath, "/");
1180        if (!((Content) programItem).hasValue(repeaterPath))
1181        {
1182            // Repeater is empty, the value is the default value for all educational paths
1183            return true;
1184        }
1185        
1186        return getValuesForPaths(programItem, dataPath, paths, null).values().stream().distinct().count() == 1;
1187    }
1188    
1189    /**
1190     * Get a position of repeater entry that match the given educational path
1191     * @param programItem The program item
1192     * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName)
1193     * @param path The full educational path (from root program). Cannot be null.
1194     * @return the index position of the entry matching the educational path with a non-empty value for requested attribute, -1 otherwise
1195     */
1196    public int getRepeaterEntryPositionForPath(ProgramItem programItem, String dataPath, EducationalPath path)
1197    {
1198        return getRepeaterEntryPositionForPath(programItem, dataPath, List.of(path));
1199    }
1200    
1201    /**
1202     * Get a position of repeater entry that match the given educational path
1203     * @param programItem The program item
1204     * @param dataPath The full path of attribute to retrieve. The path must contain the path of repeater holding the value (ex: path/to/repeater/attributeName)
1205     * @param paths The full educational paths (from root program). Cannot be null.
1206     * @return the index position of the entry matching the educational path with a non-empty value for requested attribute, -1 otherwise
1207     */
1208    public int getRepeaterEntryPositionForPath(ProgramItem programItem, String dataPath, List<EducationalPath> paths)
1209    {
1210        String repeaterPath = StringUtils.substringBeforeLast(dataPath, "/");
1211        String attributeName = StringUtils.substringAfterLast(dataPath, "/");
1212        
1213        Content content = (Content) programItem;
1214        if (!content.hasValue(repeaterPath))
1215        {
1216            // Repeater is not defined
1217            getLogger().debug("There is no repeater '{}' defined for content '{}'", repeaterPath, programItem.getId());
1218            return -1;
1219        }
1220        
1221        // Get the repeater
1222        ModelAwareRepeater repeater = content.getRepeater(repeaterPath);
1223        
1224        // Get a value for each value in paths because if it is multiple, we want to check all paths
1225        List<Pair<Integer, Object>> values = paths.stream()
1226             .map(path ->
1227                 getRepeaterEntriesByPath(repeater, List.of(path))
1228                     .findFirst()
1229                     .map(e -> Pair.of(e.getPosition(), e.getValue(attributeName)))
1230                     .orElseGet(() -> Pair.of(-1, null))
1231             )
1232             .toList();
1233        
1234        boolean isSameValues = values.stream().map(Pair::getRight).distinct().count() == 1;
1235        if (!isSameValues)
1236        {
1237            // No same values for each path
1238            getLogger().warn("Unable to determine repeater entry for '{}' attribute of content '{}'. Multiple educational paths are available for requested context with no same values", dataPath, programItem.getId());
1239            return -1;
1240        }
1241        
1242        // Same value for each available paths => return position of any entry
1243        return values.stream().findFirst().map(Pair::getLeft).orElse(-1);
1244    }
1245    
1246    /**
1247     * Get the full educations paths (from a root {@link Program}) from a full or partial path
1248     * @param path A full or partial path composed by program item ancestors
1249     * @return the full educational paths
1250     */
1251    public List<EducationalPath> getEducationPathFromPath(EducationalPath path)
1252    {
1253        return getEducationPathFromPath(path.getProgramItems(_resolver));
1254    }
1255    
1256    /**
1257     * Get the full educations paths (from a root {@link Program}) from a full or partial path
1258     * @param path A full or partial path composed by program item ancestors
1259     * @return the full educational paths
1260     */
1261    public List<EducationalPath> getEducationPathFromPath(List<ProgramItem> path)
1262    {
1263        return getEducationPathFromPaths(List.of(path));
1264    }
1265    
1266    /**
1267     * Get the full educations paths (from a root {@link Program}) from full or partial paths
1268     * @param paths full or partial paths composed by program item ancestors
1269     * @return the full educational paths
1270     */
1271    public List<EducationalPath> getEducationPathFromPaths(List<List<ProgramItem>> paths)
1272    {
1273        return getEducationPathFromPaths(paths, null);
1274    }
1275    
1276    /**
1277     * Get the full educations paths (from a root {@link Program})from full or partial paths
1278     * @param paths full or partial paths composed by program item ancestors
1279     * @param withAncestor filter the educational paths that contains this ancestor. Can be null.
1280     * @return the full educational paths
1281     */
1282    public List<EducationalPath> getEducationPathFromPaths(List<List<ProgramItem>> paths, ProgramItem withAncestor)
1283    {
1284        List<EducationalPath> fullPaths = new ArrayList<>();
1285        
1286        for (List<ProgramItem> partialPath : paths)
1287        {
1288            ProgramItem firstProgramItem = partialPath.get(0);
1289            if (!(firstProgramItem instanceof Program))
1290            {
1291                // First program item of path is not a root program => computed the available full paths from this first item ancestors
1292                List<EducationalPath> parentEducationalPaths = getEducationalPaths(firstProgramItem, false);
1293                
1294                fullPaths.addAll(parentEducationalPaths.stream()
1295                    .filter(p -> withAncestor == null || p.getProgramItems(_resolver).contains(withAncestor)) // filter educational paths that is not composed by the required ancestor
1296                    .map(p -> EducationalPath.of(p, partialPath.toArray(ProgramItem[]::new))) // concat path
1297                    .toList());
1298            }
1299            else if (withAncestor == null || partialPath.contains(withAncestor))
1300            {
1301                // The path is already a full path
1302                fullPaths.add(EducationalPath.of(partialPath.toArray(ProgramItem[]::new)));
1303            }
1304        }
1305        
1306        return fullPaths;
1307    }
1308    
1309    /**
1310     * Get all {@link EducationalPath} of a {@link ProgramItem}
1311     * The path is built with the mapper function.
1312     * @param programItem The program item
1313     * @return the paths in parent program items
1314     */
1315    public List<EducationalPath> getEducationalPaths(ProgramItem programItem)
1316    {
1317        return getEducationalPaths(programItem, true);
1318    }
1319    
1320    /**
1321     * Get all {@link EducationalPath} of a {@link ProgramItem}
1322     * The path is built with the mapper function.
1323     * @param programItem The program item
1324     * @param includeItself set to false to not include final item in path
1325     * @return the paths in parent program items
1326     */
1327    public List<EducationalPath> getEducationalPaths(ProgramItem programItem, boolean includeItself)
1328    {
1329        return getEducationalPaths(programItem, includeItself, false);
1330    }
1331    
1332    /**
1333     * Get all {@link EducationalPath} of a {@link ProgramItem}
1334     * The path is built with the mapper function.
1335     * @param programItem The program item
1336     * @param includeItself set to false to not include final item in path
1337     * @param ignoreOrphanPath set to true to ignore paths that is not part of a Program
1338     * @return the paths in parent program items
1339     */
1340    public List<EducationalPath> getEducationalPaths(ProgramItem programItem, boolean includeItself, boolean ignoreOrphanPath)
1341    {
1342        List<EducationalPath> paths = new ArrayList<>();
1343        
1344        List<List<ProgramItem>> ancestorPaths = getPathOfAncestors(programItem);
1345        for (List<ProgramItem> ancestorPath : ancestorPaths)
1346        {
1347            if (!ignoreOrphanPath || ancestorPath.get(0) instanceof Program) // ignore paths that is not part of a Program if ignoreOrphanPath is true
1348            {
1349                // Don't modify directly the returned value, it may be immutable and cause side effects
1350                List<ProgramItem> ancestorPathCopy = new ArrayList<>(ancestorPath);
1351                if (!includeItself)
1352                {
1353                    ancestorPathCopy.remove(programItem);
1354                }
1355                
1356                if (!ancestorPathCopy.isEmpty())
1357                {
1358                    paths.add(EducationalPath.of(ancestorPathCopy.toArray(ProgramItem[]::new)));
1359                }
1360            }
1361        }
1362        
1363        return paths;
1364    }
1365    
1366    /**
1367     * Get a readable value of a {@link EducationalPath}
1368     * @param path the educational path
1369     * @return a String representing the path with program item's title separated by '>'
1370     */
1371    public String getEducationalPathAsString(EducationalPath path)
1372    {
1373        return getEducationalPathAsString(path, pi -> ((Content) pi).getTitle(), " > ");
1374    }
1375    
1376    /**
1377     * Get a readable value of a {@link EducationalPath}
1378     * @param path the educational path
1379     * @param mapper the function to use for the readable value of a program item
1380     * @param separator the separator to use
1381     * @return a String representing the path with program item's readable value given by mapper function and separated by given separator
1382     */
1383    public String getEducationalPathAsString(EducationalPath path, Function<ProgramItem, String> mapper, CharSequence separator)
1384    {
1385        return getEducationalPathAsString(path, mapper, separator, x -> true);
1386    }
1387    
1388    /**
1389     * Get a readable value of a {@link EducationalPath}
1390     * @param path the educational path
1391     * @param mapper the function to use for the readable value of a program item
1392     * @param separator the separator to use
1393     * @param filterPathSegment predicate to exclude some program item of path
1394     * @return a String representing the path with program item's readable value given by mapper function and separated by given separator
1395     */
1396    public String getEducationalPathAsString(EducationalPath path, Function<ProgramItem, String> mapper, CharSequence separator, Predicate<ProgramItem> filterPathSegment)
1397    {
1398        return path.resolveProgramItems(_resolver)
1399                .filter(filterPathSegment)
1400                .map(mapper)
1401                .collect(Collectors.joining(separator));
1402    }
1403    
1404    /**
1405     * Determines if a educational path is valid ODF path
1406     * @param path the educational path
1407     * @return <code>true</code> if path represents a valid ODF path
1408     */
1409    public boolean isValid(EducationalPath path)
1410    {
1411        // Get leaf program item of this educational path
1412        String leafProgramItemId = path.getProgramItemIds().getLast();
1413        if (_resolver.hasAmetysObjectForId(leafProgramItemId))
1414        {
1415            ProgramItem leafProgramItem = _resolver.resolveById(leafProgramItemId);
1416            return isValid(path, leafProgramItem, false);
1417        }
1418        return false;
1419    }
1420    
1421    
1422    /**
1423     * Determines if the given educational path is a valid path for the program item
1424     * @param path the educational path. The path must include the program item itself
1425     * @param programItem the program item
1426     * @param ignoreOrphanPath set to true to ignore paths that is not part of a Program
1427     * @return <code>true</code> if path is valid for the given program item
1428     */
1429    public boolean isValid(EducationalPath path, ProgramItem programItem, boolean ignoreOrphanPath)
1430    {
1431        return getEducationalPaths(programItem, true, ignoreOrphanPath).contains(path);
1432    }
1433    
1434    /**
1435     * Get the full path to program item for highest ancestors. The path includes this final item.
1436     * @param programItem the program item
1437     * @return a list for each highest ancestors found. Each item of the list contains the program items to the path to this program item.
1438     */
1439    protected List<List<ProgramItem>> getPathOfAncestors(ProgramItem programItem)
1440    {
1441        Cache<ProgramItem, List<List<ProgramItem>>> cache = _cacheManager.get(__ANCESTORS_CACHE);
1442        
1443        return cache.get(programItem, item -> {
1444            List<ProgramItem> parentProgramItems = getParentProgramItems(item);
1445            
1446            // There is no more parents, the only path is the item itself
1447            if (parentProgramItems.isEmpty())
1448            {
1449                return List.of(List.of(item));
1450            }
1451            
1452            List<List<ProgramItem>> ancestors = new ArrayList<>();
1453            
1454            // Compute the path for each parent
1455            for (ProgramItem parentProgramItem : parentProgramItems)
1456            {
1457                for (List<ProgramItem> ancestorPaths : getPathOfAncestors(parentProgramItem))
1458                {
1459                    List<ProgramItem> ancestorPathsCopy = new ArrayList<>(ancestorPaths);
1460                    ancestorPathsCopy.add(item);
1461                    // Add an immutable list to avoid unvoluntary modifications in cache
1462                    ancestors.add(Collections.unmodifiableList(ancestorPathsCopy));
1463                }
1464            }
1465
1466            // Add an immutable list to avoid unvoluntary modifications in cache
1467            return Collections.unmodifiableList(ancestors);
1468        });
1469    }
1470    
1471    /**
1472     * Get the enumeration of educational paths for a program item for highest ancestors. The paths does not includes this final item.
1473     * @param programItemId the id of program item
1474     * @return a list of educational paths with paths' label (composed by ancestors' title separated by '>') and paths' id (composed by ancestors' id seperated by coma)
1475     */
1476    @Callable (rights = Callable.READ_ACCESS, rightContext = ContentRightAssignmentContext.ID, paramIndex = 0)
1477    public List<Map<String, String>> getEducationalPathsEnumeration(String programItemId)
1478    {
1479        List<Map<String, String>> paths = new ArrayList<>();
1480        
1481        ProgramItem programItem = _resolver.resolveById(programItemId);
1482        
1483        getEducationalPaths(programItem)
1484            .stream()
1485            .forEach(p -> paths.add(Map.of("id", p.toString(), "title", getEducationalPathAsString(p, c -> ((Content) c).getTitle(), " > "))));
1486        
1487        return paths;
1488    }
1489    
1490    
1491    /**
1492     * Get the path of a {@link ProgramItem} into a {@link Program}<br>
1493     * The path is construct with the contents' names and the used separator is '/'.
1494     * @param programItemId The id of the program item
1495     * @param programId The id of program. Can not be null.
1496     * @return the path into the parent program or null if the item is not part of this program.
1497     */
1498    @Callable
1499    public String getPathInProgram (String programItemId, String programId)
1500    {
1501        ProgramItem item = _resolver.resolveById(programItemId);
1502        Program program = _resolver.resolveById(programId);
1503        
1504        return getPathInProgram(item, program);
1505    }
1506    
1507    /**
1508     * Get the path of a ODF content into a {@link Program}.<br>
1509     * The path is construct with the contents' names and the used separator is '/'.
1510     * @param item The program item
1511     * @param parentProgram The parent root (sub)program. Can not be null.
1512     * @return the path from the parent program
1513     */
1514    public String getPathInProgram (ProgramItem item, Program parentProgram)
1515    {
1516        if (item instanceof Program)
1517        {
1518            // The program item is already the program it self or another program
1519            return item.equals(parentProgram) ? "" : null;
1520        }
1521        
1522        List<EducationalPath> paths = getEducationalPaths(item, true, true);
1523        
1524        for (EducationalPath path : paths)
1525        {
1526            if (path.getProgramItemIds().contains(parentProgram.getId()))
1527            {
1528                // Find a path that match the given parent program
1529                Stream<ProgramItem> resolvedPath = path.resolveProgramItems(_resolver);
1530                return resolvedPath.map(ProgramItem::getName).collect(Collectors.joining("/"));
1531            }
1532        }
1533        
1534        return null;
1535    }
1536    
1537    /**
1538     * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br>
1539     * The path is construct with the contents' names and the used separator is '/'.
1540     * @param contentId The id of the content
1541     * @param parentCourseId The id of parent course. Can not be null.
1542     * @return the path into the parent course or null if the item is not part of this course.
1543     */
1544    @Callable
1545    public String getPathInCourse (String contentId, String parentCourseId)
1546    {
1547        Content content = _resolver.resolveById(contentId);
1548        Course parentCourse = _resolver.resolveById(parentCourseId);
1549        
1550        return getPathInCourse(content, parentCourse);
1551    }
1552    
1553    /**
1554     * Get the path of a {@link Course}  or a {@link CourseList} into a {@link Course}<br>
1555     * The path is construct with the contents' names and the used separator is '/'.
1556     * @param courseOrList The course or the course list
1557     * @param parentCourse The parent course. Can not be null.
1558     * @return the path into the parent course or null if the item is not part of this course.
1559     */
1560    public String getPathInCourse(Content courseOrList, Course parentCourse)
1561    {
1562        if (courseOrList.equals(parentCourse))
1563        {
1564            return "";
1565        }
1566        
1567        String path = _getPathInCourse(courseOrList, parentCourse);
1568        
1569        return path;
1570    }
1571    
1572    private String _getPathInCourse(Content content, Content parentContent)
1573    {
1574        if (content.equals(parentContent))
1575        {
1576            return content.getName();
1577        }
1578
1579        List<? extends Content> parents;
1580        
1581        if (content instanceof Course course)
1582        {
1583            parents = course.getParentCourseLists();
1584        }
1585        else if (content instanceof CourseList courseList)
1586        {
1587            parents = courseList.getParentCourses();
1588        }
1589        else
1590        {
1591            throw new IllegalStateException();
1592        }
1593        
1594        for (Content parent : parents)
1595        {
1596            String path = _getPathInCourse(parent, parentContent);
1597            if (path != null)
1598            {
1599                return path + '/' + content.getName();
1600            }
1601        }
1602        return null;
1603    }
1604    
1605    /**
1606     * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br>
1607     * The path is construct with the contents' names and the used separator is '/'.
1608     * @param orgUnitId The id of the orgunit
1609     * @param rootOrgUnitId The root orgunit id
1610     * @return the path into the parent program or null if the item is not part of this program.
1611     */
1612    @Callable
1613    public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId)
1614    {
1615        OrgUnit rootOU = null;
1616        if (StringUtils.isNotBlank(rootOrgUnitId))
1617        {
1618            rootOU = _resolver.resolveById(rootOrgUnitId);
1619        }
1620        else
1621        {
1622            rootOU = _ouRootProvider.getRoot();
1623        }
1624        
1625        if (orgUnitId.equals(rootOU.getId()))
1626        {
1627            // The orgunit is already the root orgunit
1628            return rootOU.getName();
1629        }
1630        
1631        OrgUnit ou = _resolver.resolveById(orgUnitId);
1632        
1633        List<String> paths = new ArrayList<>();
1634        paths.add(ou.getName());
1635        
1636        OrgUnit parent = ou.getParentOrgUnit();
1637        while (parent != null && !parent.getId().equals(rootOU.getId()))
1638        {
1639            paths.add(parent.getName());
1640            parent = parent.getParentOrgUnit();
1641        }
1642        
1643        if (parent != null)
1644        {
1645            paths.add(rootOU.getName());
1646            Collections.reverse(paths);
1647            return StringUtils.join(paths, "/");
1648        }
1649        
1650        return null;
1651    }
1652    
1653    /**
1654     * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br>
1655     * The path is construct with the contents' names and the used separator is '/'.
1656     * @param orgUnitId The id of the orgunit
1657     * @return the path into the parent program or null if the item is not part of this program.
1658     */
1659    @Callable
1660    public String getOrgUnitPath(String orgUnitId)
1661    {
1662        return getOrgUnitPath(orgUnitId, null);
1663    }
1664    
1665    /**
1666     * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id
1667     * @param part The program part
1668     * @param parentId The ancestor id
1669     * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id
1670     */
1671    public boolean hasAncestor (ProgramPart part, String parentId)
1672    {
1673        List<ProgramPart> parents = part.getProgramPartParents();
1674        
1675        for (ProgramPart parent : parents)
1676        {
1677            if (parent.getId().equals(parentId))
1678            {
1679                return true;
1680            }
1681            else if (hasAncestor(parent, parentId))
1682            {
1683                return true;
1684            }
1685        }
1686        
1687        return false;
1688    }
1689    
1690    /**
1691     * Determines if a program item is shared
1692     * @param programItem the program item
1693     * @return true if the program item is shared
1694     */
1695    public boolean isShared(ProgramItem programItem)
1696    {
1697        List<ProgramItem> parents = getParentProgramItems(programItem);
1698        if (parents.size() > 1)
1699        {
1700            return true;
1701        }
1702        else
1703        {
1704            return parents.isEmpty() ? false : isShared(parents.get(0));
1705        }
1706    }
1707    
1708    /**
1709     * Check if a relation can be establish between two ODF contents
1710     * @param srcContent The source content (copied or moved)
1711     * @param targetContent The target content
1712     * @param errors The list of error messages
1713     * @param contextualParameters the contextual parameters
1714     * @return true if the relation is valid, false otherwise
1715     */
1716    public boolean isRelationCompatible(Content srcContent, Content targetContent, List<I18nizableText> errors, Map<String, Object> contextualParameters)
1717    {
1718        boolean isCompatible = true;
1719        
1720        if (targetContent instanceof ProgramItem || targetContent instanceof OrgUnit)
1721        {
1722            if (!_isContentTypeCompatible(srcContent, targetContent))
1723            {
1724                // Invalid relations between content types
1725                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CONTENT_TYPES", _getContentParameters(srcContent, targetContent)));
1726                isCompatible = false;
1727            }
1728            else if (!_isCatalogCompatible(srcContent, targetContent))
1729            {
1730                // Catalog is invalid
1731                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_CATALOG", _getContentParameters(srcContent, targetContent)));
1732                isCompatible = false;
1733            }
1734            else if (!_isLanguageCompatible(srcContent, targetContent))
1735            {
1736                // Language is invalid
1737                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_LANGUAGE", _getContentParameters(srcContent, targetContent)));
1738                isCompatible = false;
1739            }
1740            else if (!_areShareableFieldsCompatibles(srcContent, targetContent, contextualParameters))
1741            {
1742                // Shareable fields don't match
1743                errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_SHAREABLE_COURSE", _getContentParameters(srcContent, targetContent)));
1744                isCompatible = false;
1745            }
1746        }
1747        else if (srcContent instanceof ProgramItem || srcContent instanceof OrgUnit)
1748        {
1749            // If the target isn't ODF related but the source is, the relation is not compatible.
1750            errors.add(new I18nizableText("plugin.odf", "PLUGINS_ODF_RELATIONS_SETCONTENTATTRIBUTE_REFERENCE_ERROR_NO_PROGRAM_ITEM", _getContentParameters(srcContent, targetContent)));
1751            isCompatible = false;
1752        }
1753        
1754        return isCompatible;
1755    }
1756    
1757    /**
1758     * Get the name of attribute holding the relation between a parent content and its children
1759     * @param parentProgramItem the parent content
1760     * @param childProgramItem the child content
1761     * @return the name of attribute the child relation
1762     */
1763    public String getDescendantRelationAttributeName(ProgramItem parentProgramItem, ProgramItem childProgramItem)
1764    {
1765        if (parentProgramItem instanceof CourseList && childProgramItem instanceof Course)
1766        {
1767            return CourseList.CHILD_COURSES;
1768        }
1769        else if (parentProgramItem instanceof Course && childProgramItem instanceof CourseList)
1770        {
1771            return Course.CHILD_COURSE_LISTS;
1772        }
1773        else if (parentProgramItem instanceof Course && childProgramItem instanceof CoursePart)
1774        {
1775            return Course.CHILD_COURSE_PARTS;
1776        }
1777        else if (parentProgramItem instanceof TraversableProgramPart && childProgramItem instanceof ProgramPart)
1778        {
1779            return TraversableProgramPart.CHILD_PROGRAM_PARTS;
1780        }
1781        
1782        return null;
1783    }
1784    
1785    private boolean _isCourseAlreadyBelongToCourseList(Course course, CourseList courseList)
1786    {
1787        return courseList.getCourses().contains(course);
1788    }
1789    
1790    private boolean _isContentTypeCompatible(Content srcContent, Content targetContent)
1791    {
1792        if (srcContent instanceof Container || srcContent instanceof SubProgram)
1793        {
1794            return targetContent instanceof AbstractTraversableProgramPart;
1795        }
1796        else if (srcContent instanceof CourseList)
1797        {
1798            return targetContent instanceof CourseListContainer;
1799        }
1800        else if (srcContent instanceof Course)
1801        {
1802            return targetContent instanceof CourseList;
1803        }
1804        else if (srcContent instanceof OrgUnit)
1805        {
1806            return targetContent instanceof OrgUnit;
1807        }
1808        
1809        return false;
1810    }
1811    
1812    private boolean _isCatalogCompatible(Content srcContent, Content targetContent)
1813    {
1814        if (srcContent instanceof ProgramItem srcProgramItem && targetContent instanceof ProgramItem targetProgramItem)
1815        {
1816            return srcProgramItem.getCatalog().equals(targetProgramItem.getCatalog());
1817        }
1818        return true;
1819    }
1820    
1821    private boolean _isLanguageCompatible(Content srcContent, Content targetContent)
1822    {
1823        return srcContent.getLanguage().equals(targetContent.getLanguage());
1824    }
1825    
1826    private boolean _areShareableFieldsCompatibles(Content srcContent, Content targetContent, Map<String, Object> contextualParameters)
1827    {
1828        // We check shareable fields only if the course content is not created (or created by copy) and not moved
1829        if (srcContent instanceof Course srcCourse
1830                && targetContent instanceof CourseList targetCourseList
1831                && _shareableCourseHelper.handleShareableCourse()
1832                && !"create".equals(contextualParameters.get("mode"))
1833                && !"copy".equals(contextualParameters.get("mode"))
1834                && !"move".equals(contextualParameters.get("mode"))
1835                // In this case, it means that we try to change the position of the course in the courseList, so don't check shareable fields
1836                && !_isCourseAlreadyBelongToCourseList(srcCourse, targetCourseList))
1837        {
1838            return _shareableCourseHelper.isShareableFieldsMatch(srcCourse, targetCourseList);
1839        }
1840        
1841        return true;
1842    }
1843    
1844    private List<String> _getContentParameters(Content srcContent, Content targetContent)
1845    {
1846        List<String> parameters = new ArrayList<>();
1847        parameters.add(srcContent.getTitle());
1848        parameters.add(srcContent.getId());
1849        parameters.add(targetContent.getTitle());
1850        parameters.add(targetContent.getId());
1851        return parameters;
1852    }
1853    /**
1854     * Copy a {@link ProgramItem}
1855     * @param srcContent The program item to copy
1856     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1857     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1858     * @param copiedContents the initial contents with their copied content
1859     * @return The created content
1860     * @param <C> The modifiable content return type
1861     * @throws AmetysRepositoryException If an error occurred during copy
1862     * @throws WorkflowException If an error occurred during copy
1863     */
1864    public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException
1865    {
1866        return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedContents);
1867    }
1868    
1869    /**
1870     * Copy a {@link ProgramItem}
1871     * @param srcContent The program item to copy
1872     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1873     * @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.
1874     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1875     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1876     * @param copiedContents the initial contents with their copied content
1877     * @param <C> The modifiable content return type
1878     * @return The created content
1879     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1880     * @throws AmetysRepositoryException If an error occurred
1881     * @throws WorkflowException If an error occurred
1882     */
1883    public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException
1884    {
1885        return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedContents);
1886    }
1887    
1888    /**
1889     * Copy a {@link CoursePart}
1890     * @param srcContent The course part to copy
1891     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1892     * @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.
1893     * @param initWorkflowActionId The initial workflow action id
1894     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1895     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1896     * @param copiedContents the initial contents with their copied content
1897     * @param <C> The modifiable content return type
1898     * @return The created content
1899     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1900     * @throws AmetysRepositoryException If an error occurred
1901     * @throws WorkflowException If an error occurred
1902     */
1903    public <C extends ModifiableContent> C copyCoursePart(CoursePart srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException
1904    {
1905        return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedContents);
1906    }
1907    
1908    /**
1909     * Copy a {@link ProgramItem}
1910     * @param srcContent The program item to copy
1911     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1912     * @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.
1913     * @param initWorkflowActionId The initial workflow action id
1914     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1915     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1916     * @param copiedContents the initial contents with their copied content
1917     * @param <C> The modifiable content return type
1918     * @return The created content
1919     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1920     * @throws AmetysRepositoryException If an error occurred
1921     * @throws WorkflowException If an error occurred
1922     */
1923    public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException
1924    {
1925        return _copyODFContent((Content) srcContent, srcContent.getCatalog(), srcContent.getCode(), targetContentName, targetContentLanguage, initWorkflowActionId, targetCatalog, fullCopy, copiedContents);
1926    }
1927    
1928    /**
1929     * Copy a {@link ProgramItem}. Also copy the synchronization metadata (status and alternative value)
1930     * @param srcContent The program item to copy
1931     * @param catalog The catalog
1932     * @param code The odf content code
1933     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
1934     * @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.
1935     * @param initWorkflowActionId The initial workflow action id
1936     * @param fullCopy Set to <code>true</code> to copy the sub-structure
1937     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
1938     * @param copiedContents the initial contents with their copied content
1939     * @param <C> The modifiable content return type
1940     * @return The created content
1941     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
1942     * @throws AmetysRepositoryException If an error occurred
1943     * @throws WorkflowException If an error occurred
1944     */
1945    @SuppressWarnings("unchecked")
1946    private <C extends ModifiableContent> C _copyODFContent(Content srcContent, String catalog, String code, String targetContentName, String targetContentLanguage, int initWorkflowActionId, String targetCatalog, boolean fullCopy, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException
1947    {
1948        String computedTargetLanguage = targetContentLanguage;
1949        if (computedTargetLanguage == null)
1950        {
1951            computedTargetLanguage = srcContent.getLanguage();
1952        }
1953        
1954        String computeTargetName = targetContentName;
1955        if (computeTargetName == null)
1956        {
1957            // Compute content name from source content and requested language
1958            computeTargetName = srcContent.getName() + (targetContentLanguage != null && !targetContentLanguage.equals(srcContent.getName()) ? "-" + targetContentLanguage : "");
1959        }
1960        
1961        String computeTargetCatalog = targetCatalog;
1962        if (computeTargetCatalog == null)
1963        {
1964            computeTargetCatalog = catalog;
1965        }
1966        
1967        String principalContentType = srcContent.getTypes()[0];
1968        ModifiableContent createdContent = getODFContent(principalContentType, code, computeTargetCatalog, computedTargetLanguage);
1969        if (createdContent != null)
1970        {
1971            getLogger().info("A program item already exists with the same type, code, catalog and language [{}, {}, {}, {}]", principalContentType, code, computeTargetCatalog, targetContentLanguage);
1972        }
1973        else
1974        {
1975            // Copy content without notifying observers (done later) and copying ACL
1976            DataContext context = RepositoryDataContext.newInstance()
1977                                                       .withExternalMetadataInCopy(true);
1978            createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, false, true, false, true, context);
1979            
1980            if (fullCopy)
1981            {
1982                _cleanContentMetadata(createdContent);
1983                
1984                if (targetCatalog != null)
1985                {
1986                    if (createdContent instanceof ProgramItem programItem)
1987                    {
1988                        programItem.setCatalog(targetCatalog);
1989                    }
1990                    else if (createdContent instanceof CoursePart coursePart)
1991                    {
1992                        coursePart.setCatalog(targetCatalog);
1993                    }
1994                    
1995                }
1996                
1997                if (srcContent instanceof ProgramItem programItem)
1998                {
1999                    copyProgramItemStructure(programItem, createdContent, computedTargetLanguage, initWorkflowActionId, computeTargetCatalog, copiedContents);
2000                }
2001                
2002                _extractOutgoingReferences(createdContent);
2003                
2004                createdContent.saveChanges();
2005            }
2006            
2007            // Notify observers after all structure has been copied
2008            _contentDAO.notifyContentCopied(createdContent, false);
2009            
2010            copiedContents.put(srcContent, createdContent);
2011        }
2012        
2013        return (C) createdContent;
2014    }
2015    
2016    /**
2017     * Copy the structure of a {@link ProgramItem}
2018     * @param srcContent the content to copy
2019     * @param targetContent the target content
2020     * @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.
2021     * @param initWorkflowActionId The initial workflow action id
2022     * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object.
2023     * @param copiedContents the initial contents with their copied content
2024     * @throws AmetysRepositoryException If an error occurred during copy
2025     * @throws WorkflowException If an error occurred during copy
2026     */
2027    protected void copyProgramItemStructure(ProgramItem srcContent, ModifiableContent targetContent, String targetContentLanguage, int initWorkflowActionId, String targetCatalogName, Map<Content, Content> copiedContents) throws AmetysRepositoryException, WorkflowException
2028    {
2029        List<ProgramItem> srcChildContents = new ArrayList<>();
2030        Map<Pair<String, String>, List<String>> values = new HashMap<>();
2031        
2032        String childMetadataPath = null;
2033        String parentMetadataPath = null;
2034        
2035        if (srcContent instanceof TraversableProgramPart programPart)
2036        {
2037            childMetadataPath = TraversableProgramPart.CHILD_PROGRAM_PARTS;
2038            parentMetadataPath = ProgramPart.PARENT_PROGRAM_PARTS;
2039            srcChildContents.addAll(programPart.getProgramPartChildren());
2040        }
2041        else if (srcContent instanceof CourseList courseList)
2042        {
2043            childMetadataPath = CourseList.CHILD_COURSES;
2044            parentMetadataPath = Course.PARENT_COURSE_LISTS;
2045            srcChildContents.addAll(courseList.getCourses());
2046        }
2047        else if (srcContent instanceof Course course)
2048        {
2049            childMetadataPath = Course.CHILD_COURSE_LISTS;
2050            parentMetadataPath = CourseList.PARENT_COURSES;
2051            srcChildContents.addAll(course.getCourseLists());
2052
2053            List<String> refCoursePartIds = new ArrayList<>();
2054            for (CoursePart srcChildContent : course.getCourseParts())
2055            {
2056                CoursePart targetChildContent = copyCoursePart(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedContents);
2057                refCoursePartIds.add(targetChildContent.getId());
2058            }
2059            _addFormValues(values, Course.CHILD_COURSE_PARTS, CoursePart.PARENT_COURSES, refCoursePartIds);
2060        }
2061
2062        List<String> refChildIds = new ArrayList<>();
2063        for (ProgramItem srcChildContent : srcChildContents)
2064        {
2065            ProgramItem targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, targetCatalogName, true, copiedContents);
2066            refChildIds.add(targetChildContent.getId());
2067        }
2068
2069        _addFormValues(values, childMetadataPath, parentMetadataPath, refChildIds);
2070
2071        _editChildRelation((ModifiableWorkflowAwareContent) targetContent, values);
2072    }
2073    
2074    private void _addFormValues(Map<Pair<String, String>, List<String>> values, String childMetadataPath, String parentMetadataPath, List<String> refChildIds)
2075    {
2076        if (!refChildIds.isEmpty())
2077        {
2078            values.put(Pair.of(childMetadataPath, parentMetadataPath), refChildIds);
2079        }
2080    }
2081    
2082    private void _editChildRelation(ModifiableWorkflowAwareContent parentContent, Map<Pair<String, String>, List<String>> values) throws AmetysRepositoryException
2083    {
2084        if (!values.isEmpty())
2085        {
2086            for (Map.Entry<Pair<String, String>, List<String>> entry : values.entrySet())
2087            {
2088                String childMetadataName = entry.getKey().getLeft();
2089                String parentMetadataName = entry.getKey().getRight();
2090                List<String> childContents = entry.getValue();
2091                
2092                parentContent.setValue(childMetadataName, childContents.toArray(new String[childContents.size()]));
2093                
2094                for (String childContentId : childContents)
2095                {
2096                    ModifiableContent content = _resolver.resolveById(childContentId);
2097                    String[] parentContentIds = ContentDataHelper.getContentIdsArrayFromMultipleContentData(content, parentMetadataName);
2098                    content.setValue(parentMetadataName, ArrayUtils.add(parentContentIds, parentContent.getId()));
2099                    content.saveChanges();
2100                }
2101            }
2102        }
2103    }
2104    
2105    /**
2106     * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure
2107     * @param createdContent The created content to clean
2108     */
2109    protected void _cleanContentMetadata(ModifiableContent createdContent)
2110    {
2111        if (createdContent instanceof ProgramPart)
2112        {
2113            _removeFullValue(createdContent, ProgramPart.PARENT_PROGRAM_PARTS);
2114        }
2115        
2116        if (createdContent instanceof TraversableProgramPart)
2117        {
2118            _removeFullValue(createdContent, TraversableProgramPart.CHILD_PROGRAM_PARTS);
2119        }
2120        
2121        if (createdContent instanceof CourseList)
2122        {
2123            _removeFullValue(createdContent, CourseList.CHILD_COURSES);
2124            _removeFullValue(createdContent, CourseList.PARENT_COURSES);
2125        }
2126        
2127        if (createdContent instanceof Course)
2128        {
2129            _removeFullValue(createdContent, Course.CHILD_COURSE_LISTS);
2130            _removeFullValue(createdContent, Course.PARENT_COURSE_LISTS);
2131            _removeFullValue(createdContent, Course.CHILD_COURSE_PARTS);
2132        }
2133        
2134        if (createdContent instanceof CoursePart)
2135        {
2136            _removeFullValue(createdContent, CoursePart.PARENT_COURSES);
2137        }
2138    }
2139    
2140    private void _removeFullValue(ModifiableContent content, String attributeName)
2141    {
2142        content.removeValue(attributeName);
2143        content.removeExternalizableMetadataIfExists(attributeName);
2144    }
2145    
2146    private void _extractOutgoingReferences(ModifiableContent content)
2147    {
2148        Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(content);
2149        content.setOutgoingReferences(outgoingReferencesByPath);
2150    }
2151    
2152    /**
2153     * Switch the ametys object to Live version if it has one
2154     * @param ao the Ametys object
2155     * @throws NoLiveVersionException if the content has no live version
2156     */
2157    public void switchToLiveVersion(DefaultAmetysObject ao) throws NoLiveVersionException
2158    {
2159        // Switch to the Live label if exists
2160        String[] allLabels = ao.getAllLabels();
2161        String[] currentLabels = ao.getLabels();
2162        
2163        boolean hasLiveVersion = Arrays.asList(allLabels).contains(CmsConstants.LIVE_LABEL);
2164        boolean currentVersionIsLive = Arrays.asList(currentLabels).contains(CmsConstants.LIVE_LABEL);
2165        
2166        if (hasLiveVersion && !currentVersionIsLive)
2167        {
2168            ao.switchToLabel(CmsConstants.LIVE_LABEL);
2169        }
2170        else if (!hasLiveVersion)
2171        {
2172            throw new NoLiveVersionException("The ametys object '" + ao.getId() + "' has no live version");
2173        }
2174    }
2175    
2176    /**
2177     * Switch to Live version if is required
2178     * @param ao the Ametys object
2179     * @throws NoLiveVersionException if the Live version is required but not exist
2180     */
2181    public void switchToLiveVersionIfNeeded(DefaultAmetysObject ao) throws NoLiveVersionException
2182    {
2183        Request request = _getRequest();
2184        if (request != null && request.getAttribute(REQUEST_ATTRIBUTE_VALID_LABEL) != null)
2185        {
2186            switchToLiveVersion(ao);
2187        }
2188    }
2189    
2190    /**
2191     * Count the hours accumulation in the {@link ProgramItem}
2192     * @param programItem The program item on which we compute the total number of hours
2193     * @return The hours accumulation
2194     */
2195    public Double getCumulatedHours(ProgramItem programItem)
2196    {
2197        // Ignore optional course list and avoid useless expensive calls
2198        if (programItem instanceof CourseList courseList && ChoiceType.OPTIONAL.equals(courseList.getType()))
2199        {
2200            return 0.0;
2201        }
2202
2203        List<ProgramItem> children = getChildProgramItems(programItem);
2204
2205        Double coef = 1.0;
2206        Double countNbHours = 0.0;
2207
2208        // If the program item is a course list, compute the coef (mandatory: 1, optional: 0, optional: min / total)
2209        if (programItem instanceof CourseList courseList)
2210        {
2211            // If there is no children, compute the coef is useless
2212            // Also choice list can throw an exception while dividing by zero
2213            if (children.isEmpty())
2214            {
2215                return 0.0;
2216            }
2217            
2218            switch (courseList.getType())
2219            {
2220                case CHOICE:
2221                    // Apply the average of number of EC from children multiply by the minimum ELP to select
2222                    coef = ((double) courseList.getMinNumberOfCourses()) / children.size();
2223                    break;
2224                case MANDATORY:
2225                default:
2226                    // Add all ECTS from children
2227                    break;
2228            }
2229        }
2230
2231        // If it's a course and we have a value for the number of hours
2232        // Then get the value
2233        if (programItem instanceof Course course && course.hasValue(Course.NUMBER_OF_HOURS))
2234        {
2235            countNbHours += course.<Double>getValue(Course.NUMBER_OF_HOURS);
2236        }
2237        // Else if there are program item children on the item
2238        // Then compute on children
2239        else if (children.size() > 0)
2240        {
2241            for (ProgramItem child : children)
2242            {
2243                countNbHours += getCumulatedHours(child);
2244            }
2245        }
2246        // Else, it's a course but there is no value for the number of hours and we don't have program item children
2247        // Then compute on course parts
2248        else if (programItem instanceof Course course)
2249        {
2250            countNbHours += course.getCourseParts()
2251                .stream()
2252                .mapToDouble(CoursePart::getNumberOfHours)
2253                .sum();
2254        }
2255        
2256        return coef * countNbHours;
2257    }
2258    
2259    /**
2260     * Get the request
2261     * @return the request
2262     */
2263    protected Request _getRequest()
2264    {
2265        return ContextHelper.getRequest(_context);
2266    }
2267    
2268    /**
2269     * Get the first orgunit matching the given UAI code
2270     * @param uaiCode the UAI code
2271     * @return the orgunit or null if not found
2272     */
2273    public OrgUnit getOrgUnitByUAICode(String uaiCode)
2274    {
2275        Expression expr = new AndExpression(
2276                new ContentTypeExpression(Operator.EQ, OrgUnitFactory.ORGUNIT_CONTENT_TYPE),
2277                new StringExpression(OrgUnit.CODE_UAI, Operator.EQ, uaiCode)
2278        );
2279        
2280        String xPathQuery = QueryHelper.getXPathQuery(null, OrgUnitFactory.ORGUNIT_NODETYPE, expr);
2281        AmetysObjectIterable<OrgUnit> orgUnits = _resolver.query(xPathQuery);
2282        
2283        return orgUnits.stream()
2284            .findFirst()
2285            .orElse(null);
2286    }
2287    
2288    /**
2289     * Get the repeater entries filtered by path, it takes care of "common" attribute if exists and find itself the educational-path attribute.
2290     * @param repeater The repeater to filter
2291     * @param educationalPaths List of full educational paths
2292     * @return a {@link Stream} of filtered repeater entries
2293     */
2294    public Stream<? extends ModelAwareRepeaterEntry> getRepeaterEntriesByPath(ModelAwareRepeater repeater, List<EducationalPath> educationalPaths)
2295    {
2296        // Build the filter to apply on repeater entries
2297        Predicate<ModelAwareRepeaterEntry> filterRepeaterEntries =  _buildRepeaterEntryByPathPredicate(repeater, educationalPaths);
2298        
2299        // If predicate is null, an error has been logged
2300        if (filterRepeaterEntries == null)
2301        {
2302            return Stream.empty();
2303        }
2304        
2305        // For each entry, check if the entry is common (if attribute exists) or the path correspond to one of the retrieved full educational paths
2306        return repeater.getEntries()
2307            .stream()
2308            .filter(filterRepeaterEntries);
2309    }
2310    
2311    private Predicate<ModelAwareRepeaterEntry> _buildRepeaterEntryByPathPredicate(ModelAwareRepeater repeater, List<EducationalPath> educationalPaths)
2312    {
2313        List<ModelItem> educationalPathModelItem = ModelHelper.findModelItemsByType(repeater.getModel(), EducationalPathRepositoryElementType.EDUCATIONAL_PATH_ELEMENT_TYPE_ID);
2314        if (educationalPathModelItem.size() != 1)
2315        {
2316            getLogger().error("Unable to determine repeater entry matching an education path. No attribute or several attributes of type '{}' found.", EducationalPathRepositoryElementType.EDUCATIONAL_PATH_ELEMENT_TYPE_ID, repeater.getModel().getPath());
2317            return null;
2318        }
2319        
2320        // Prepare the predicate for educational path attribute
2321        String pathAttributeName = educationalPathModelItem.get(0).getName();
2322        Predicate<ModelAwareRepeaterEntry> repeaterEntriesFilter = e -> educationalPaths.contains(e.getValue(pathAttributeName, false, null));
2323        
2324        // Complete predicate with common attribute if exists
2325        if (repeater.getModel().hasModelItem("common"))
2326        {
2327            return ((Predicate<ModelAwareRepeaterEntry>) e -> e.getValue("common", true, true)).or(repeaterEntriesFilter);
2328        }
2329        
2330        return repeaterEntriesFilter;
2331    }
2332}