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