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;
031
032import org.ametys.cms.ObservationConstants;
033import org.ametys.cms.content.external.ExternalizableMetadataHelper;
034import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
035import org.ametys.cms.form.AbstractField.MODE;
036import org.ametys.cms.repository.Content;
037import org.ametys.cms.repository.ContentQueryHelper;
038import org.ametys.cms.repository.ContentTypeExpression;
039import org.ametys.cms.repository.DefaultContent;
040import org.ametys.cms.repository.LanguageExpression;
041import org.ametys.cms.repository.ModifiableContent;
042import org.ametys.cms.repository.WorkflowAwareContent;
043import org.ametys.cms.workflow.ContentWorkflowHelper;
044import org.ametys.cms.workflow.EditContentFunction;
045import org.ametys.core.observation.Event;
046import org.ametys.core.observation.ObservationManager;
047import org.ametys.core.ui.Callable;
048import org.ametys.core.user.CurrentUserProvider;
049import org.ametys.odf.course.Course;
050import org.ametys.odf.course.CourseContainer;
051import org.ametys.odf.courselist.CourseList;
052import org.ametys.odf.orgunit.OrgUnit;
053import org.ametys.odf.orgunit.RootOrgUnitProvider;
054import org.ametys.odf.program.AbstractProgram;
055import org.ametys.odf.program.Container;
056import org.ametys.odf.program.Program;
057import org.ametys.odf.program.ProgramPart;
058import org.ametys.odf.program.SubProgram;
059import org.ametys.odf.program.TraversableProgramPart;
060import org.ametys.plugins.repository.AmetysObjectExistsException;
061import org.ametys.plugins.repository.AmetysObjectIterable;
062import org.ametys.plugins.repository.AmetysObjectIterator;
063import org.ametys.plugins.repository.AmetysObjectResolver;
064import org.ametys.plugins.repository.AmetysRepositoryException;
065import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
066import org.ametys.plugins.repository.RepositoryConstants;
067import org.ametys.plugins.repository.UnknownAmetysObjectException;
068import org.ametys.plugins.repository.collection.AmetysObjectCollection;
069import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
070import org.ametys.plugins.repository.query.SortCriteria;
071import org.ametys.plugins.repository.query.expression.AndExpression;
072import org.ametys.plugins.repository.query.expression.Expression;
073import org.ametys.plugins.repository.query.expression.Expression.Operator;
074import org.ametys.plugins.repository.query.expression.StringExpression;
075import org.ametys.plugins.workflow.AbstractWorkflowComponent;
076import org.ametys.runtime.plugin.component.AbstractLogEnabled;
077import org.ametys.runtime.plugin.component.PluginAware;
078
079import com.opensymphony.workflow.WorkflowException;
080
081/**
082 * Helper for ODF contents
083 *
084 */
085public class ODFHelper extends AbstractLogEnabled implements Component, Serviceable, PluginAware
086{
087    /** The component role. */
088    public static final String ROLE = ODFHelper.class.getName();
089    
090    /** The default id of initial workflow action */
091    protected static final int __INITIAL_WORKFLOW_ACTION_ID = 0;
092    /** The default id of edit workflow action */
093    protected static final int __EDIT_WORKFLOW_ACTION_ID = 2;
094    
095    /** Ametys object resolver */
096    protected AmetysObjectResolver _resolver;
097    /** The content workflow helper */
098    protected ContentWorkflowHelper _contentWorkflowHelper;
099    /** The content types manager */
100    protected ContentTypeExtensionPoint _cTypeEP;
101    /** The observation manager */
102    protected ObservationManager _observationManager;
103    /** The current user provider */
104    protected CurrentUserProvider _currentUserProvider;
105    /** Root orgunit */
106    protected RootOrgUnitProvider _ouRootProvider;
107    
108    private String _pluginName;
109    
110    @Override
111    public void service(ServiceManager manager) throws ServiceException
112    {
113        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
114        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
115        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
116        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
117        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
118        _ouRootProvider = (RootOrgUnitProvider) manager.lookup(RootOrgUnitProvider.ROLE);
119    }
120    
121    @Override
122    public void setPluginInfo(String pluginName, String featureName, String id)
123    {
124        _pluginName = pluginName;
125    }
126    
127    /**
128     * Gets the root for ODF contents
129     * @return the root for ODF contents
130     */
131    public AmetysObjectCollection getRootContent()
132    {
133        return getRootContent(false);
134    }
135    
136    /**
137     * Gets the root for ODF contents
138     * @param create <code>true</code> to create automatically the root when missing.
139     * @return the root for ODF contents
140     */
141    public AmetysObjectCollection getRootContent(boolean create)
142    {
143        ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins/");
144        
145        boolean needSave = false;
146        if (!pluginsNode.hasChild(_pluginName))
147        {
148            if (create)
149            {
150                pluginsNode.createChild(_pluginName, "ametys:unstructured");
151                needSave = true;
152            }
153            else
154            {
155                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "' is missing");
156            }
157        }
158        
159        ModifiableTraversableAmetysObject pluginNode = pluginsNode.getChild(_pluginName);
160        if (!pluginNode.hasChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents"))
161        {
162            if (create)
163            {
164                pluginNode.createChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents", "ametys:collection");
165                needSave = true;
166            }
167            else
168            {
169                throw new UnknownAmetysObjectException("Node '/ametys:plugins/" + _pluginName + "/ametys:contents' is missing");
170            }
171        }
172        
173        if (needSave)
174        {
175            pluginsNode.saveChanges();
176        }
177        
178        return pluginNode.getChild(RepositoryConstants.NAMESPACE_PREFIX + ":contents");
179    }
180    
181    /**
182     * Get the {@link ProgramItem}s matching the given arguments
183     * @param cTypeId The id of content type. Can be null to get program's items whatever their content type.
184     * @param code The code. Can be null to get program's items regardless of their code
185     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
186     * @param lang The search language. Can be null to get program's items regardless of their language
187     * @param <C> The content return type 
188     * @return The matching program items
189     */
190    public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang)
191    {
192        return getProgramItems(cTypeId, code, catalogName, lang, null, null);
193    }
194    
195    /**
196     * Get the {@link ProgramItem}s matching the given arguments
197     * @param cTypeId The id of content type. Can be null to get program's items whatever their content type.
198     * @param code The code. Can be null to get program's items regardless of their code
199     * @param catalogName The search catalog. Can be null to get program's items regardless the catalog they belong to.
200     * @param lang The search language. Can be null to get program's items regardless of their language
201     * @param additionnalExpr An additional expression for filtering result. Can be null
202     * @param sortCriteria criteria for sorting results
203     * @param <C> The content return type 
204     * @return The matching program items
205     */
206    public <C extends Content> AmetysObjectIterable<C> getProgramItems(String cTypeId, String code, String catalogName, String lang, Expression additionnalExpr, SortCriteria sortCriteria)
207    {
208        List<Expression> exprs = new ArrayList<>();
209        
210        if (StringUtils.isNotEmpty(cTypeId))
211        {
212            exprs.add(new ContentTypeExpression(Operator.EQ, cTypeId));
213        }
214        if (StringUtils.isNotEmpty(code))
215        {
216            exprs.add(new StringExpression(ProgramItem.METADATA_CODE, Operator.EQ, code));
217        }
218        if (StringUtils.isNotEmpty(catalogName))
219        {
220            exprs.add(new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalogName));
221        }
222        if (StringUtils.isNotEmpty(lang))
223        {
224            exprs.add(new LanguageExpression(Operator.EQ, lang));
225        }
226        if (additionnalExpr != null)
227        {
228            exprs.add(additionnalExpr);
229        }
230        
231        Expression expr = new AndExpression(exprs.toArray(new Expression[exprs.size()]));
232        
233        String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr, sortCriteria);
234        return _resolver.query(xpathQuery);
235    }
236    
237    /**
238     * Get the equivalent {@link ProgramItem} of the source {@link ProgramItem} in given catalog and language 
239     * @param srcProgramItem The source program item
240     * @param catalogName The name of catalog to search into
241     * @param lang The search language
242     * @return The equivalent program item or <code>null</code> if not exists
243     */
244    public Content getProgramItem(ProgramItem srcProgramItem, String catalogName, String lang)
245    {
246        Expression langExpr = new LanguageExpression(Operator.EQ, lang);
247        Expression catalogExpr = new StringExpression(ProgramItem.METADATA_CATALOG, Operator.EQ, catalogName);
248        Expression codeExpr = new StringExpression(ProgramItem.METADATA_CODE, Operator.EQ, srcProgramItem.getCode());
249        
250        Expression expr = new AndExpression(langExpr, catalogExpr, codeExpr);
251        
252        String xpathQuery = ContentQueryHelper.getContentXPathQuery(expr);
253        AmetysObjectIterable<Content> contents = _resolver.query(xpathQuery);
254        AmetysObjectIterator<Content> contentsIt = contents.iterator();
255        if (contentsIt.hasNext())
256        {
257            return contentsIt.next();
258        }
259        
260        return null;
261    }
262    
263    /**
264     * Get the child program items of a {@link ProgramItem}
265     * @param programItem The program item
266     * @return The child program items 
267     */
268    public List<ProgramItem> getChildProgramItems(ProgramItem programItem)
269    {
270        List<ProgramItem> children = new ArrayList<>();
271        
272        if (programItem instanceof TraversableProgramPart)
273        {
274            children.addAll(((TraversableProgramPart) programItem).getProgramPartChildren());
275        }
276        
277        if (programItem instanceof CourseContainer)
278        {
279            children.addAll(((CourseContainer) programItem).getCourses());
280        }
281        
282        if (programItem instanceof Course)
283        {
284            children.addAll(((Course) programItem).getCourseLists());
285        }
286        
287        return children;
288    }
289    
290    /**
291     * Gets (recursively) parent abstract programs of this program item.
292     * @param programItem The program item
293     * @return parent abstract programs of this program item.
294     */
295    public Set<AbstractProgram> getParentAbstractPrograms(ProgramItem programItem)
296    {
297        Set<ProgramItem> visitedProgramItems = new HashSet<>();
298        visitedProgramItems.add(programItem);
299        return _getParentAbstractPrograms(programItem, visitedProgramItems);
300    }
301    
302    private Set<AbstractProgram> _getParentAbstractPrograms(ProgramItem programItem, Set<ProgramItem> visitedProgramItems)
303    {
304        Set<AbstractProgram> parentAbstractPrograms = new HashSet<>();
305        List<ProgramItem> parents = getParentProgramItems(programItem);
306        
307        for (ProgramItem parent : parents)
308        {
309            // Only parents not already visited
310            if (visitedProgramItems.add(parent))
311            {
312                if (parent instanceof AbstractProgram)
313                {
314                    parentAbstractPrograms.add((AbstractProgram) parent);
315                }
316                else
317                {
318                    parentAbstractPrograms.addAll(_getParentAbstractPrograms(parent, visitedProgramItems));
319                }
320            }
321        }
322        
323        return parentAbstractPrograms;
324    }
325    
326    /**
327     * Get the parent program items of a {@link ProgramItem}
328     * @param programItem The program item
329     * @return The parent program items 
330     */
331    public List<ProgramItem> getParentProgramItems(ProgramItem programItem)
332    {
333        List<ProgramItem> parents = new ArrayList<>();
334        
335        if (programItem instanceof ProgramPart)
336        {
337            parents.addAll(((ProgramPart) programItem).getProgramPartParents());
338        }
339        
340        if (programItem instanceof CourseList)
341        {
342            parents.addAll(((CourseList) programItem).getParentCourses());
343        }
344        
345        if (programItem instanceof Course)
346        {
347            parents.addAll(((Course) programItem).getParentCourseLists());
348        }
349        
350        return parents;
351    }
352    
353    /**
354     * Get the nearest program item parent into the given parent {@link AbstractProgram}
355     * @param programItem The program item
356     * @param parentProgram The parent program or subprogram. If null, the nearest abstract program will be returned.
357     * @return The parent program item or null if not found.
358     */
359    public ProgramItem getParentProgramItem (ProgramItem programItem, AbstractProgram parentProgram)
360    {
361        if (programItem instanceof Program)
362        {
363            return null;
364        }
365        
366        if (programItem instanceof ProgramPart)
367        {
368            List<ProgramPart> parents = ((ProgramPart) programItem).getProgramPartParents();
369            
370            for (ProgramPart parent : parents)
371            {
372                if (parent instanceof AbstractProgram && (parentProgram == null || parent.equals(parentProgram)))
373                {
374                    return parent;
375                }
376                else
377                {
378                    ProgramItem ancestor = getParentProgramItem(parent, parentProgram);
379                    if (ancestor != null)
380                    {
381                        return parent;
382                    }
383                }
384            }
385        }
386        
387        if (programItem instanceof CourseList)
388        {
389            for (Course parentCourse : ((CourseList) programItem).getParentCourses())
390            {
391                ProgramItem ancestor = getParentProgramItem(parentCourse, parentProgram);
392                if (ancestor != null)
393                {
394                    return parentCourse;
395                }
396            }
397        }
398        
399        if (programItem instanceof Course)
400        {
401            for (CourseList cl : ((Course) programItem).getParentCourseLists())
402            {
403                ProgramItem ancestor = getParentProgramItem(cl, parentProgram);
404                if (ancestor != null)
405                {
406                    return cl;
407                }
408            }
409        }
410        
411        return null;
412    }
413    
414    /**
415     * Get information of the program item structure (type, if program has children)
416     * @param programItemId the program item id
417     * @return a map of information
418     */
419    @Callable
420    public Map<String, Object> getStructureInfo(String programItemId)
421    {
422        Map<String, Object> results = new HashMap<>();
423        
424        if (StringUtils.isNotBlank(programItemId))
425        {
426            Content content = _resolver.resolveById(programItemId);
427            if (content instanceof ProgramItem)
428            {
429                List<ProgramItem> childProgramItems = getChildProgramItems((ProgramItem) content);
430                results.put("hasChildren", !childProgramItems.isEmpty());
431                
432                List<ProgramItem> parentProgramItems = getParentProgramItems((ProgramItem) content);
433                results.put("hasParent", !parentProgramItems.isEmpty());
434                
435                results.put("paths", getPaths((ProgramItem) content, " > "));
436            }
437        }
438        
439        return results;
440    }
441    
442    /**
443     * Get all the paths of a ODF content.<br>
444     * The path is construct with the contents' title
445     * @param separator The path separator
446     * @param item The program item
447     * @return the paths in parent program items
448     */
449    protected List<String> getPaths (ProgramItem item, String separator)
450    {
451        List<String> paths = new ArrayList<>();
452        
453        String title = ((Content) item).getTitle();
454
455        List<ProgramItem> parentProgramItems = getParentProgramItems(item);
456        if (parentProgramItems.isEmpty())
457        {
458            paths.add(title);
459            return paths;
460        }
461        
462        for (ProgramItem parentProgramItem : parentProgramItems)
463        {
464            for (String path : getPaths(parentProgramItem, separator))
465            {
466                paths.add(path + separator + title);
467            }
468        }
469        
470        return paths;
471    }
472    
473    /**
474     * Get the path of a {@link ProgramItem} into a {@link Program}<br>
475     * The path is construct with the contents' names and the used separator is '/'.
476     * @param programItemId The id of the program item
477     * @param programId The id of program. Can not be null.
478     * @return the path into the parent program or null if the item is not part of this program.
479     */
480    @Callable
481    public String getPathInProgram (String programItemId, String programId)
482    {
483        ProgramItem item = _resolver.resolveById(programItemId);
484        Program program = _resolver.resolveById(programId);
485        
486        return getPathInProgram(item, program);
487    }
488    
489    /**
490     * Get the path of a ODF content into a {@link Program}.<br>
491     * The path is construct with the contents' names and the used separator is '/'.
492     * @param item The program item
493     * @param parentProgram The parent root (sub)program. Can not be null.
494     * @return the path from the parent program
495     */
496    public String getPathInProgram (ProgramItem item, Program parentProgram)
497    {
498        if (item instanceof Program)
499        {
500            // The program item is already the program it self or another program
501            return item.equals(parentProgram) ? "" : null;
502        }
503        
504        List<String> paths = new ArrayList<>();
505        paths.add(item.getName());
506        
507        ProgramItem parent = getParentProgramItem(item, parentProgram);
508        while (parent != null && !(parent instanceof Program))
509        {
510            paths.add(parent.getName());
511            parent = getParentProgramItem(parent, parentProgram);
512        }
513        
514        if (parent != null)
515        {
516            paths.add(parent.getName());
517            Collections.reverse(paths);
518            return org.apache.commons.lang3.StringUtils.join(paths, "/");
519        }
520        
521        return null;
522    }
523    
524    /**
525     * Get the path of a {@link Course} or a {@link CourseList} into a {@link Course}<br>
526     * The path is construct with the contents' names and the used separator is '/'.
527     * @param contentId The id of the content
528     * @param parentCourseId The id of parent course. Can not be null.
529     * @return the path into the parent course or null if the item is not part of this course.
530     */
531    @Callable
532    public String getPathInCourse (String contentId, String parentCourseId)
533    {
534        Content content = _resolver.resolveById(contentId);
535        Course parentCourse = _resolver.resolveById(parentCourseId);
536        
537        return getPathInCourse(content, parentCourse);
538    }
539    
540    /**
541     * Get the path of a {@link Course}  or a {@link CourseList} into a {@link Course}<br>
542     * The path is construct with the contents' names and the used separator is '/'.
543     * @param courseOrList The course or the course list
544     * @param parentCourse The parent course. Can not be null.
545     * @return the path into the parent course or null if the item is not part of this course.
546     */
547    public String getPathInCourse(Content courseOrList, Course parentCourse)
548    {
549        if (courseOrList.equals(parentCourse))
550        {
551            return "";
552        }
553        
554        String path = _getPathInCourse(courseOrList, parentCourse);
555        
556        return path;
557    }
558    
559    private String _getPathInCourse(Content content, Content parentContent)
560    {
561        if (content.equals(parentContent))
562        {
563            return content.getName();
564        }
565
566        List<? extends Content> parents;
567        
568        if (content instanceof Course)
569        {
570            parents = ((Course) content).getParentCourseLists();
571        }
572        else if (content instanceof CourseList)
573        {
574            parents = ((CourseList) content).getParentCourses();
575        }
576        else
577        {
578            throw new IllegalStateException();
579        }
580        
581        for (Content parent : parents)
582        {
583            String path = _getPathInCourse(parent, parentContent);
584            if (path != null)
585            {
586                return path + '/' + content.getName(); 
587            }
588        }
589        return null;
590    }
591    
592    /**
593     * Get the hierarchical path of a {@link OrgUnit} from the root orgunit id.<br>
594     * The path is construct with the contents' names and the used separator is '/'.
595     * @param orgUnitId The id of the orgunit
596     * @param rootOrgUnitId The root orgunit id
597     * @return the path into the parent program or null if the item is not part of this program.
598     */
599    @Callable
600    public String getOrgUnitPath(String orgUnitId, String rootOrgUnitId)
601    {
602        OrgUnit rootOU = null;
603        if (StringUtils.isNotBlank(rootOrgUnitId))
604        {
605            rootOU = _resolver.resolveById(rootOrgUnitId);
606        }
607        else
608        {
609            rootOU = _ouRootProvider.getRoot();
610        }
611        
612        if (orgUnitId.equals(rootOU.getId()))
613        {
614            // The orgunit is already the root orgunit
615            return rootOU.getName();
616        }
617        
618        OrgUnit ou = _resolver.resolveById(orgUnitId);
619        
620        List<String> paths = new ArrayList<>();
621        paths.add(ou.getName());
622        
623        OrgUnit parent = ou.getParentOrgUnit();
624        while (parent != null && !parent.getId().equals(rootOU.getId()))
625        {
626            paths.add(parent.getName());
627            parent = parent.getParentOrgUnit();
628        }
629        
630        if (parent != null)
631        {
632            paths.add(rootOU.getName());
633            Collections.reverse(paths);
634            return org.apache.commons.lang3.StringUtils.join(paths, "/");
635        }
636        
637        return null;
638    }
639    
640    /**
641     * Get the hierarchical path of a {@link OrgUnit} from the root orgunit.<br>
642     * The path is construct with the contents' names and the used separator is '/'.
643     * @param orgUnitId The id of the orgunit
644     * @return the path into the parent program or null if the item is not part of this program.
645     */
646    @Callable
647    public String getOrgUnitPath(String orgUnitId)
648    {
649        return getOrgUnitPath(orgUnitId, null);
650    }
651    
652    /**
653     * Return true if the given {@link ProgramPart} has in its hierarchy a parent of given id
654     * @param part The program part
655     * @param parentId The ancestor id
656     * @return true if the given {@link ProgramPart} has in its hierarchy a parent of given id
657     */
658    public boolean hasAncestor (ProgramPart part, String parentId)
659    {
660        List<ProgramPart> parents = part.getProgramPartParents();
661        
662        for (ProgramPart parent : parents)
663        {
664            if (parent.getId().equals(parentId))
665            {
666                return true;
667            }
668            else if (hasAncestor(parent, parentId))
669            {
670                return true;
671            }
672        }
673        
674        return false;
675    }
676    
677    /**
678     * Copy a {@link ProgramItem} 
679     * @param srcContent The program item to copy
680     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
681     * @param fullCopy Set to <code>true</code> to copy the sub-structure
682     * @param copiedPrograms the id of initial programs with their copied content
683     * @param copiedSubPrograms the id of initial subprograms with their copied content
684     * @param copiedContainers the id of initial containers with their copied content
685     * @param copiedCourseLists the id of initial course lists with their copied content
686     * @param copiedCourses the id of initial courses with their copied content
687     * @return The created content
688     * @param <C> The modifiable content return type 
689     * @throws AmetysRepositoryException If an error occurred during copy
690     * @throws WorkflowException If an error occurred during copy
691     */
692    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) throws AmetysRepositoryException, WorkflowException
693    {
694        return copyProgramItem(srcContent, null, null, __INITIAL_WORKFLOW_ACTION_ID, __EDIT_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses);
695    }
696    
697    /**
698     * Copy a {@link ProgramItem}
699     * @param srcContent The program item to copy
700     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
701     * @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.
702     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
703     * @param fullCopy Set to <code>true</code> to copy the sub-structure
704     * @param copiedPrograms the id of initial programs with their copied content
705     * @param copiedSubPrograms the id of initial subprograms with their copied content
706     * @param copiedContainers the id of initial containers with their copied content
707     * @param copiedCourseLists the id of initial course lists with their copied content
708     * @param copiedCourses the id of initial courses with their copied content
709     * @param <C> The modifiable content return type 
710     * @return The created content
711     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
712     * @throws AmetysRepositoryException If an error occurred
713     * @throws WorkflowException If an error occurred
714     */
715    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) throws AmetysRepositoryException, WorkflowException
716    {
717        return copyProgramItem(srcContent, targetContentName, targetContentLanguage, __INITIAL_WORKFLOW_ACTION_ID, __EDIT_WORKFLOW_ACTION_ID, targetCatalog, fullCopy, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses);
718    }
719    
720    
721    /**
722     * Copy a {@link ProgramItem}
723     * @param srcContent The program item to copy
724     * @param targetContentName The name of content to created. Can be null. If null, the new name will be get from the source object.
725     * @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.
726     * @param initWorkflowActionId The initial workflow action id
727     * @param editWorkflowActionId The workflow action id to edit the relationship
728     * @param fullCopy Set to <code>true</code> to copy the sub-structure
729     * @param targetCatalog The target catalog. Can be null. The target catalog will be the catalog of the source object.
730     * @param copiedPrograms the id of initial programs with their copied content
731     * @param copiedSubPrograms the id of initial subprograms with their copied content
732     * @param copiedContainers the id of initial containers with their copied content
733     * @param copiedCourseLists the id of initial course lists with their copied content
734     * @param copiedCourses the id of initial courses with their copied content
735     * @param <C> The modifiable content return type 
736     * @return The created content
737     * @throws AmetysObjectExistsException If a program item with same code, catalog and language already exists
738     * @throws AmetysRepositoryException If an error occurred
739     * @throws WorkflowException If an error occurred
740     */
741    @SuppressWarnings("unchecked")
742    public <C extends ModifiableContent> C copyProgramItem(ProgramItem srcContent, String targetContentName, String targetContentLanguage, int initWorkflowActionId, int editWorkflowActionId, String targetCatalog, boolean fullCopy, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses) throws AmetysRepositoryException, WorkflowException
743    {
744        String computedTargetLanguage = targetContentLanguage;
745        if (computedTargetLanguage == null)
746        {
747            computedTargetLanguage = ((Content) srcContent).getLanguage();
748        }
749        
750        String computeTargetName = targetContentName;
751        if (computeTargetName == null)
752        {
753            // Compute content name from source content and requested language
754            computeTargetName = ((Content) srcContent).getName() + (targetContentLanguage != null && !targetContentLanguage.equals(((Content) srcContent).getName()) ? "-" + targetContentLanguage : "");
755        }
756        
757        String computeTargetCatalog = targetCatalog;
758        if (computeTargetCatalog == null)
759        {
760            computeTargetCatalog = srcContent.getCatalog();
761        }
762        
763        if (getProgramItem(srcContent, computeTargetCatalog, computedTargetLanguage) != null)
764        {
765            throw new AmetysObjectExistsException("A program item already exists with same code, catalog and language [" + srcContent.getCode() + ", " + computeTargetCatalog + ", " + targetContentLanguage + "]");
766        }
767        
768        // Copy content waiting for observers to be completed and copying ACL
769        ModifiableContent createdContent = ((DefaultContent) srcContent).copyTo(getRootContent(true), computeTargetName, targetContentLanguage, initWorkflowActionId, true, true);
770        String createdContentId = createdContent.getId();
771        
772        if (fullCopy)
773        {
774            _cleanContentMetadata(createdContent);
775            
776            if (targetCatalog != null && createdContent instanceof ProgramItem)
777            {
778                ((ProgramItem) createdContent).setCatalog(targetCatalog);
779                createdContent.saveChanges();
780                // Content is modified as we changed the value of its catalog, notify it
781                Map<String, Object> eventParams = new HashMap<>();
782                eventParams.put(ObservationConstants.ARGS_CONTENT, createdContent);
783                eventParams.put(ObservationConstants.ARGS_CONTENT_ID, createdContentId);
784                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
785            }
786            
787            copyProgramItemStructure(srcContent, createdContent, computedTargetLanguage, initWorkflowActionId, editWorkflowActionId, computeTargetCatalog, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses);
788        }
789        
790        if (createdContent instanceof Program)
791        {
792            copiedPrograms.put(((Content) srcContent).getId(), createdContentId);
793        }
794        else if (createdContent instanceof SubProgram)
795        {
796            copiedSubPrograms.put(((Content) srcContent).getId(), createdContentId);
797        }
798        else if (createdContent instanceof Container)
799        {
800            copiedContainers.put(((Content) srcContent).getId(), createdContentId);
801        }
802        else if (createdContent instanceof CourseList)
803        {
804            copiedCourseLists.put(((Content) srcContent).getId(), createdContentId);
805        }
806        else if (createdContent instanceof Course)
807        {
808            copiedCourses.put(((Content) srcContent).getId(), createdContentId);
809        }
810        
811        return (C) createdContent;
812    }
813    
814    /**
815     * Copy the structure of a {@link ProgramItem}
816     * @param srcContent the content to copy
817     * @param targetContent the target content
818     * @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.
819     * @param initWorkflowActionId The initial workflow action id
820     * @param editWorkflowActionId The workflow action id to edit the relationship
821     * @param targetCatalogName The target catalog. Can be null. The target catalog will be the catalog of the source object.
822     * @param copiedPrograms the id of initial programs with their copied content
823     * @param copiedSubPrograms the id of initial subprograms with their copied content
824     * @param copiedContainers the id of initial containers with their copied content
825     * @param copiedCourseLists the id of initial course lists with their copied content
826     * @param copiedCourses the id of initial courses with their copied content
827     * @throws AmetysRepositoryException If an error occurred during copy
828     * @throws WorkflowException If an error occurred during copy
829     */
830    protected void copyProgramItemStructure(ProgramItem srcContent, ModifiableContent targetContent, String targetContentLanguage, int initWorkflowActionId, int editWorkflowActionId, String targetCatalogName, Map<String, String> copiedPrograms, Map<String, String> copiedSubPrograms, Map<String, String> copiedContainers, Map<String, String> copiedCourseLists, Map<String, String> copiedCourses) throws AmetysRepositoryException, WorkflowException
831    {
832        List<String> refChildIds = new ArrayList<>();
833        
834        List<ProgramItem> srcChildContents = new ArrayList<>();
835        
836        String childMetadataPath = null;
837        
838        if (srcContent instanceof TraversableProgramPart)
839        {
840            childMetadataPath = TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS;
841            srcChildContents.addAll(((TraversableProgramPart) srcContent).getProgramPartChildren());
842        }
843        else if (srcContent instanceof CourseList)
844        {
845            childMetadataPath = CourseList.METADATA_CHILD_COURSES;
846            srcChildContents.addAll(((CourseList) srcContent).getCourses());
847        }
848        else if (srcContent instanceof Course)
849        {
850            childMetadataPath = Course.METADATA_CHILD_COURSE_LISTS;
851            srcChildContents.addAll(((Course) srcContent).getCourseLists());
852        }
853        
854        for (ProgramItem srcChildContent : srcChildContents)
855        {
856            Content targetChildContent = getProgramItem(srcChildContent, targetCatalogName, targetContentLanguage);
857            if (targetChildContent == null)
858            {
859                targetChildContent = copyProgramItem(srcChildContent, null, targetContentLanguage, initWorkflowActionId, editWorkflowActionId, targetCatalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses);
860            }
861            refChildIds.add(targetChildContent.getId());
862        }
863        
864        _editChildRelation((WorkflowAwareContent) targetContent, refChildIds, editWorkflowActionId, childMetadataPath);
865    }
866    
867    private void _editChildRelation(WorkflowAwareContent parentContent, List<String> refChildIds, int actionId, String childMetadataPath) throws AmetysRepositoryException, WorkflowException
868    {
869        if (refChildIds.size() > 0)
870        {
871            Map<String, Object> values = new HashMap<>();
872            
873            values.put(EditContentFunction.FORM_ELEMENTS_PREFIX + childMetadataPath, refChildIds);
874            values.put(EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + childMetadataPath + ".mode", MODE.REPLACE.name());
875            
876            Map<String, Object> contextParameters = new HashMap<>();
877            contextParameters.put("quit", true);
878            contextParameters.put("values", values);
879            
880            Map<String, Object> inputs = new HashMap<>();
881            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
882            
883            _contentWorkflowHelper.doAction(parentContent, actionId, inputs);
884        }
885    }
886    
887    /**
888     * Clean the CONTENT metadata created after a copy but whose values reference the initial content' structure
889     * @param createdContent The created content to clean
890     */
891    protected void _cleanContentMetadata(ModifiableContent createdContent)
892    {
893        ModifiableCompositeMetadata metadataHolder = createdContent.getMetadataHolder();
894        if (createdContent instanceof ProgramPart)
895        {
896            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, ProgramPart.METADATA_PARENT_PROGRAM_PARTS);
897        }
898        
899        if (createdContent instanceof TraversableProgramPart)
900        {
901            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, TraversableProgramPart.METADATA_CHILD_PROGRAM_PARTS);
902        }
903        
904        if (createdContent instanceof CourseList)
905        {
906            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.METADATA_CHILD_COURSES);
907            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, CourseList.METADATA_PARENT_COURSES);
908        }
909        
910        if (createdContent instanceof Course)
911        {
912            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.METADATA_CHILD_COURSE_LISTS);
913            ExternalizableMetadataHelper.removeMetadataIfExists(metadataHolder, Course.METADATA_PARENT_COURSE_LISTS);
914        }
915    }
916}