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