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