001/*
002 *  Copyright 2014 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.catalog;
017
018import java.time.Duration;
019import java.time.temporal.ChronoUnit;
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.stream.Collectors;
028import java.util.stream.Stream;
029
030import org.apache.avalon.framework.component.Component;
031import org.apache.avalon.framework.context.Context;
032import org.apache.avalon.framework.context.ContextException;
033import org.apache.avalon.framework.context.Contextualizable;
034import org.apache.avalon.framework.service.ServiceException;
035import org.apache.avalon.framework.service.ServiceManager;
036import org.apache.avalon.framework.service.Serviceable;
037import org.apache.cocoon.ProcessingException;
038import org.apache.cocoon.components.ContextHelper;
039import org.apache.cocoon.environment.Request;
040import org.apache.commons.lang.StringUtils;
041
042import org.ametys.cms.ObservationConstants;
043import org.ametys.cms.content.archive.ArchiveConstants;
044import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
045import org.ametys.cms.data.ContentValue;
046import org.ametys.cms.indexing.solr.SolrIndexHelper;
047import org.ametys.cms.repository.Content;
048import org.ametys.cms.repository.ContentQueryHelper;
049import org.ametys.cms.repository.ContentTypeExpression;
050import org.ametys.cms.repository.LanguageExpression;
051import org.ametys.cms.repository.ModifiableDefaultContent;
052import org.ametys.cms.repository.WorkflowAwareContent;
053import org.ametys.cms.workflow.ContentWorkflowHelper;
054import org.ametys.core.observation.Event;
055import org.ametys.core.observation.ObservationManager;
056import org.ametys.core.ui.Callable;
057import org.ametys.core.user.CurrentUserProvider;
058import org.ametys.odf.ODFHelper;
059import org.ametys.odf.ProgramItem;
060import org.ametys.odf.course.Course;
061import org.ametys.odf.course.CourseContainer;
062import org.ametys.odf.courselist.CourseList;
063import org.ametys.odf.courselist.CourseListContainer;
064import org.ametys.odf.coursepart.CoursePart;
065import org.ametys.odf.coursepart.CoursePartFactory;
066import org.ametys.odf.program.Program;
067import org.ametys.odf.program.ProgramFactory;
068import org.ametys.odf.program.TraversableProgramPart;
069import org.ametys.plugins.repository.AmetysObject;
070import org.ametys.plugins.repository.AmetysObjectIterable;
071import org.ametys.plugins.repository.AmetysObjectResolver;
072import org.ametys.plugins.repository.AmetysRepositoryException;
073import org.ametys.plugins.repository.ModifiableAmetysObject;
074import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
075import org.ametys.plugins.repository.RemovableAmetysObject;
076import org.ametys.plugins.repository.RepositoryConstants;
077import org.ametys.plugins.repository.TraversableAmetysObject;
078import org.ametys.plugins.repository.UnknownAmetysObjectException;
079import org.ametys.plugins.repository.lock.LockableAmetysObject;
080import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector;
081import org.ametys.plugins.repository.query.QueryHelper;
082import org.ametys.plugins.repository.query.SortCriteria;
083import org.ametys.plugins.repository.query.expression.AndExpression;
084import org.ametys.plugins.repository.query.expression.Expression;
085import org.ametys.plugins.repository.query.expression.Expression.Operator;
086import org.ametys.plugins.repository.query.expression.StringExpression;
087import org.ametys.runtime.plugin.component.AbstractLogEnabled;
088import org.ametys.runtime.plugin.component.PluginAware;
089
090import com.opensymphony.workflow.WorkflowException;
091
092/**
093 * Component to handle ODF catalogs
094 */
095public class CatalogsManager extends AbstractLogEnabled implements Serviceable, Component, PluginAware, Contextualizable
096{
097    /** Avalon Role */
098    public static final String ROLE = CatalogsManager.class.getName();
099
100    private AmetysObjectResolver _resolver;
101
102    private CopyCatalogUpdaterExtensionPoint _copyUpdaterEP;
103
104    private ObservationManager _observationManager;
105
106    private CurrentUserProvider _userProvider;
107
108    private ContentWorkflowHelper _contentWorkflowHelper;
109
110    private String _pluginName;
111
112    private ODFHelper _odfHelper;
113
114    private ContentTypeExtensionPoint _cTypeEP;
115    
116    private Context _context;
117
118    private SolrIndexHelper _solrIndexHelper;
119    
120    private String _defaultCatalogId;
121
122    private CurrentUserProvider _currentUserProvider;
123
124    public void contextualize(Context context) throws ContextException
125    {
126        _context = context;
127    }
128    
129    @Override
130    public void service(ServiceManager manager) throws ServiceException
131    {
132        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
133        _copyUpdaterEP = (CopyCatalogUpdaterExtensionPoint) manager.lookup(CopyCatalogUpdaterExtensionPoint.ROLE);
134        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
135        _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
136        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
137        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
138        _cTypeEP = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
139        _solrIndexHelper = (SolrIndexHelper) manager.lookup(SolrIndexHelper.ROLE);
140        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
141    }
142    
143    public void setPluginInfo(String pluginName, String featureName, String id)
144    {
145        _pluginName = pluginName;
146    }
147    
148    /**
149     * Get the list of catalogs
150     * @return the catalogs
151     */
152    public List<Catalog> getCatalogs()
153    {
154        List<Catalog> result = new ArrayList<>();
155
156        TraversableAmetysObject catalogsNode = getCatalogsRootNode();
157        
158        AmetysObjectIterable<Catalog> catalogs = catalogsNode.getChildren();
159        for (Catalog catalog : catalogs)
160        {
161            result.add(catalog);
162        }
163        
164        return result;
165    }
166
167    /**
168     * Get a catalog matching with the given name
169     * @param name The name
170     * @return a catalog, or null if not found
171     */
172    public Catalog getCatalog(String name)
173    {
174        ModifiableTraversableAmetysObject catalogsNode = getCatalogsRootNode();
175        
176        if (StringUtils.isNotEmpty(name) && catalogsNode.hasChild(name))
177        {
178            return catalogsNode.getChild(name);
179        }
180        
181        // Not found
182        return null;
183    }
184    
185    /**
186     * Returns the name of the default catalog
187     * @return the name of the default catalog
188     */
189    @Callable
190    public String getDefaultCatalogName()
191    {
192        Catalog defaultCatalog = getDefaultCatalog();
193        return defaultCatalog != null ? defaultCatalog.getName() : null;
194    }
195    
196    /**
197     * Returns the default catalog
198     * @return the default catalog or null if no default catalog was defined.
199     */
200    public synchronized Catalog getDefaultCatalog()
201    {
202        if (_defaultCatalogId == null)
203        {
204            updateDefaultCatalog();
205        }
206        
207        return _resolver.resolveById(_defaultCatalogId);
208    }
209    
210    /**
211     * Updates the default catalog (if it's null or if the user has updated it).
212     */
213    void updateDefaultCatalog()
214    {
215        List<Catalog> catalogs = getCatalogs();
216        for (Catalog catalog : catalogs)
217        {
218            if (catalog.isDefault())
219            {
220                _defaultCatalogId = catalog.getId();
221                return;
222            }
223        }
224        
225        // If no default catalog found, get the only catalog if it exists
226        if (catalogs.size() == 1)
227        {
228            _defaultCatalogId = catalogs.get(0).getId();
229        }
230    }
231    
232    /**
233     * Get the name of the catalog of a ODF content
234     * @param contentId The id of content
235     * @return The catalog's name
236     */
237    @Callable
238    public String getContentCatalog(String contentId)
239    {
240        Content content = _resolver.resolveById(contentId);
241        
242        if (content instanceof ProgramItem)
243        {
244            return ((ProgramItem) content).getCatalog();
245        }
246        
247        // Get catalog from its parents (unecessary ?)
248        AmetysObject parent = content.getParent();
249        while (parent != null)
250        {
251            if (parent instanceof ProgramItem)
252            {
253                return ((ProgramItem) parent).getCatalog();
254            }
255            parent = parent.getParent();
256        }
257        
258        return null;
259    }
260    
261    /**
262     * Determines if the catalog can be modified from the given content
263     * @param contentId The content id
264     * @return A map with success=false if the catalog cannot be edited
265     */
266    @Callable
267    public Map<String, Object> canEditCatalog(String contentId)
268    {
269        Map<String, Object> result = new HashMap<>();
270        
271        Content content = _resolver.resolveById(contentId);
272        
273        if (content instanceof ProgramItem)
274        {
275            if (_isReferenced(content))
276            {
277                result.put("success", false);
278                result.put("error", "referenced");
279            }
280            else if (_hasSharedContent((ProgramItem) content, (ProgramItem) content))
281            {
282                result.put("success", false);
283                result.put("error", "hasSharedContent");
284            }
285            else
286            {
287                result.put("success", true);
288            }
289            
290        }
291        else
292        {
293            result.put("success", false);
294            result.put("error", "typeError");
295        }
296        
297        return result;
298    }
299    
300    private boolean _isReferenced (Content content)
301    {
302        return !_odfHelper.getParentProgramItems((ProgramItem) content).isEmpty();
303    }
304    
305    private boolean _hasSharedContent (ProgramItem rootProgramItem, ProgramItem programItem)
306    {
307        List<ProgramItem> children = _odfHelper.getChildProgramItems(programItem);
308        
309        for (ProgramItem child : children)
310        {
311            if (_isShared(rootProgramItem, child))
312            {
313                return true;
314            }
315        }
316        
317        if (programItem instanceof Course)
318        {
319            List<CoursePart> courseParts = ((Course) programItem).getCourseParts();
320            for (CoursePart coursePart : courseParts)
321            {
322                List<ProgramItem> parentCourses = coursePart.getCourses()
323                        .stream()
324                        .map(ProgramItem.class::cast)
325                        .collect(Collectors.toList());
326                if (parentCourses.size() > 1 && !_isPartOfSameStructure(rootProgramItem, parentCourses))
327                {
328                    return true;
329                }
330            }
331        }
332        
333        return false;
334    }
335    
336    private boolean _isShared(ProgramItem rootProgramItem, ProgramItem programItem)
337    {
338        try
339        {
340            List<ProgramItem> parents = _odfHelper.getParentProgramItems(programItem);
341            if (parents.size() > 1 && !_isPartOfSameStructure(rootProgramItem, parents)
342                || _hasSharedContent(rootProgramItem, programItem))
343            {
344                return true;
345            }
346        }
347        catch (UnknownAmetysObjectException e)
348        {
349            // Nothing
350        }
351        
352        return false;
353    }
354    
355    private boolean _isPartOfSameStructure(ProgramItem rootProgramItem, List<ProgramItem> programItems)
356    {
357        for (ProgramItem programItem : programItems)
358        {
359            List<List<ProgramItem>> ancestorPaths = _odfHelper.getPathOfAncestors(programItem);
360            
361            boolean isPartOfInitalStructure = false;
362            for (List<ProgramItem> ancestorPath : ancestorPaths)
363            {
364                for (ProgramItem pathSegment : ancestorPath)
365                {
366                    if (pathSegment.equals(rootProgramItem)) 
367                    {
368                        isPartOfInitalStructure = true;
369                        break;
370                    }
371                }
372            }
373            
374            if (!isPartOfInitalStructure)
375            {
376                // The content is shared outside the program item to edit
377                return false;
378            }
379        }
380        
381        return true;
382    }
383    
384    /**
385     * Set the catalog of a content. This will modify recursively the catalog of referenced children
386     * @param catalog The catalog
387     * @param contentId The id of content to edit
388     * @throws WorkflowException if an error occurred
389     */
390    @Callable
391    public void setContentCatalog(String catalog, String contentId) throws WorkflowException
392    {
393        Content content = _resolver.resolveById(contentId);
394        
395        if (content instanceof ProgramItem)
396        {
397            _setCatalog(content, catalog);
398        }
399        else
400        {
401            throw new IllegalArgumentException("You can not edit the catalog of the content " + contentId);
402        }
403    }
404    
405    private void _setCatalog (Content content, String catalogName) throws WorkflowException
406    {
407        if (content instanceof ProgramItem)
408        {
409            String oldCatalog = ((ProgramItem) content).getCatalog();
410            if (!catalogName.equals(oldCatalog))
411            {
412                ((ProgramItem) content).setCatalog(catalogName);
413                
414                if (content instanceof WorkflowAwareContent)
415                {
416                    _applyChanges((WorkflowAwareContent) content);
417                }
418            }
419        }
420        else if (content instanceof CoursePart)
421        {
422            String oldCatalog = ((CoursePart) content).getCatalog();
423            if (!catalogName.equals(oldCatalog))
424            {
425                ((CoursePart) content).setCatalog(catalogName);
426                
427                if (content instanceof WorkflowAwareContent)
428                {
429                    _applyChanges((WorkflowAwareContent) content);
430                }
431            }
432        }
433        
434        _setCatalogToChildren(content, catalogName);
435    }
436    
437    private void _setCatalogToChildren (Content content, String catalogName) throws WorkflowException
438    {
439        if (content instanceof TraversableProgramPart)
440        {
441            ContentValue[] children = content.getValue(TraversableProgramPart.CHILD_PROGRAM_PARTS, false, new ContentValue[0]);
442            for (ContentValue child : children)
443            {
444                try
445                {
446                    _setCatalog(child.getContent(), catalogName);
447                }
448                catch (UnknownAmetysObjectException e)
449                {
450                    // Nothing
451                }
452            }
453        }
454        
455        if (content instanceof CourseContainer)
456        {
457            for (Course course : ((CourseContainer) content).getCourses())
458            {
459                _setCatalog(course, catalogName);
460            }
461        }
462        
463        if (content instanceof CourseListContainer)
464        {
465            for (CourseList cl : ((CourseListContainer) content).getCourseLists())
466            {
467                _setCatalog(cl, catalogName);
468            }
469        }
470        
471        if (content instanceof Course)
472        {
473            for (CoursePart coursePart : ((Course) content).getCourseParts())
474            {
475                _setCatalog(coursePart, catalogName);
476            }
477        }
478    }
479    
480    private void _applyChanges(WorkflowAwareContent content) throws WorkflowException
481    {
482        ((ModifiableDefaultContent) content).setLastContributor(_userProvider.getUser());
483        ((ModifiableDefaultContent) content).setLastModified(new Date());
484        
485        // Remove the proposal date.
486        content.setProposalDate(null);
487        
488        // Save changes
489        content.saveChanges();
490        
491        // Notify listeners
492        Map<String, Object> eventParams = new HashMap<>();
493        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
494        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
495        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _userProvider.getUser(), eventParams));
496       
497        _contentWorkflowHelper.doAction(content, 22);
498    }
499    
500    /**
501     * Get the root catalogs storage object.
502     * @return the root catalogs node
503     * @throws AmetysRepositoryException if a repository error occurs.
504     */
505    public ModifiableTraversableAmetysObject getCatalogsRootNode() throws AmetysRepositoryException
506    {
507        String originalWorkspace = null;
508        Request request = ContextHelper.getRequest(_context);
509        try
510        {
511            originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
512            if (ArchiveConstants.ARCHIVE_WORKSPACE.equals(originalWorkspace))
513            {
514                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
515            }
516            
517            ModifiableTraversableAmetysObject rootNode = _resolver.resolveByPath("/");
518            ModifiableTraversableAmetysObject pluginsNode = _getOrCreateNode(rootNode, "ametys:plugins", "ametys:unstructured");
519            ModifiableTraversableAmetysObject pluginNode = _getOrCreateNode(pluginsNode, _pluginName, "ametys:unstructured");
520            
521            return _getOrCreateNode(pluginNode, "catalogs", "ametys:unstructured");
522        }
523        catch (AmetysRepositoryException e)
524        {
525            throw new AmetysRepositoryException("Unable to get the ODF catalogs root node", e);
526        }
527        finally
528        {
529            if (ArchiveConstants.ARCHIVE_WORKSPACE.equals(originalWorkspace))
530            {
531                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace);
532            }
533        }
534    }
535    
536    /**
537     * Create a new catalog
538     * @param name The unique name
539     * @param title The title of catalog
540     * @return the created catalog
541     */
542    public Catalog createCatalog(String name, String title)
543    {
544        Catalog newCatalog = null;
545        
546        ModifiableTraversableAmetysObject catalogsNode = getCatalogsRootNode();
547        
548        newCatalog = catalogsNode.createChild(name, "ametys:catalog");
549        newCatalog.setTitle(title);
550        
551        if (getCatalogs().size() == 1)
552        {
553            newCatalog.setDefault(true);
554        }
555        return newCatalog;
556    }
557    
558    /**
559     * Get the programs of a catalog for all languages
560     * @param catalog The code of catalog
561     * @return The programs
562     */
563    public AmetysObjectIterable<Program> getPrograms (String catalog)
564    {
565        return getPrograms(catalog, null);
566    }
567    
568    /**
569     * Get the program's items of a catalog for all languages
570     * @param catalog The code of catalog
571     * @return The {@link ProgramItem}
572     */
573    public AmetysObjectIterable<Content> getProgramItems(String catalog)
574    {
575        return _getContentsInCatalog(catalog, ProgramItem.PROGRAM_ITEM_CONTENT_TYPE);
576    }
577
578    private <T extends Content> AmetysObjectIterable<T> _getContentsInCatalog(String catalog, String contentTypeId)
579    {
580        List<Expression> exprs = new ArrayList<>();
581        
582        exprs.add(_cTypeEP.createHierarchicalCTExpression(contentTypeId));
583        exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
584        
585        Expression expression = new AndExpression(exprs.toArray(new Expression[exprs.size()]));
586        
587        String query = ContentQueryHelper.getContentXPathQuery(expression);
588        return _resolver.query(query);
589    }
590    
591    /**
592     * Get the programs of a catalog
593     * @param catalog The code of catalog
594     * @param lang The language. Can be null to get programs for all languages
595     * @return The programs
596     */
597    public AmetysObjectIterable<Program> getPrograms (String catalog, String lang)
598    {
599        List<Expression> exprs = new ArrayList<>();
600        exprs.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE));
601        exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
602        if (lang != null)
603        {
604            exprs.add(new LanguageExpression(Operator.EQ, lang));
605        }
606        
607        Expression programsExpression = new AndExpression(exprs.toArray(new Expression[exprs.size()]));
608        
609        // Add sort criteria to get size
610        SortCriteria sortCriteria = new SortCriteria();
611        sortCriteria.addCriterion(Content.ATTRIBUTE_TITLE, true, true);
612        
613        String programsQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, programsExpression, sortCriteria);
614        return _resolver.query(programsQuery);
615    }
616    
617    /**
618     * Copy the programs and its hierarchy from a catalog to another.
619     * The referenced courses are NOT copied.
620     * @param catalog The new catalog to populate
621     * @param catalogToCopy The catalog from which we copy the programs.
622     * @throws ProcessingException If an error occurred during copy
623     */
624    public void copyCatalog(Catalog catalog, Catalog catalogToCopy) throws ProcessingException
625    {
626        String catalogToCopyName = catalogToCopy.getName();
627        String catalogName = catalog.getName();
628        String [] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_ADDED, ObservationConstants.EVENT_CONTENT_MODIFIED,  ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED};
629        try
630        {
631            Map<String, String> copiedPrograms = new HashMap<>();
632            Map<String, String> copiedSubPrograms = new HashMap<>();
633            Map<String, String> copiedContainers = new HashMap<>();
634            Map<String, String> copiedCourseLists = new HashMap<>();
635            Map<String, String> copiedCourses = new HashMap<>();
636            Map<String, String> copiedCourseParts = new HashMap<>();
637            
638            Set<String> copyUpdaters = _copyUpdaterEP.getExtensionsIds();
639            
640            AmetysObjectIterable<Program> programs = getPrograms(catalogToCopyName);
641            
642            // Do NOT commit yet to Solr in order to improve perfs
643            _solrIndexHelper.pauseSolrCommitForEvents(handledEventIds);
644            long start = System.currentTimeMillis();
645            
646            getLogger().debug("Begin to iterate over programs for copying them");
647            
648            for (Program program : programs)
649            {
650                if (getLogger().isDebugEnabled())
651                {
652                    getLogger().debug("Start copying program '{}' (name: '{}', title: '{}')...", program.getId(), program.getName(), program.getTitle());
653                }
654                
655                Program newProgram = _odfHelper.copyProgramItem(program, catalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
656                
657                for (String updaterId : copyUpdaters)
658                {
659                    // Call updaters after copy of program
660                    CopyCatalogUpdater updater = _copyUpdaterEP.getExtension(updaterId);
661                    updater.updateContent(catalogToCopyName, catalogName, program, newProgram);
662                }
663            }
664
665            for (String updaterId : copyUpdaters)
666            {
667                // Call updaters after full copy of catalog
668                CopyCatalogUpdater updater = _copyUpdaterEP.getExtension(updaterId);
669                updater.updateContents(catalogToCopyName, catalogName, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
670            }
671            
672            // Workflow
673            _addCopyStep(copiedPrograms.values());
674            _addCopyStep(copiedSubPrograms.values());
675            _addCopyStep(copiedContainers.values());
676            _addCopyStep(copiedCourseLists.values());
677            _addCopyStep(copiedCourses.values());
678            _addCopyStep(copiedCourseParts.values());
679
680            if (getLogger().isDebugEnabled())
681            {
682                getLogger().debug("End of iteration over programs for copying them ({})", Duration.of((System.currentTimeMillis() - start) / 1000, ChronoUnit.SECONDS));
683            }
684            
685        }
686        catch (AmetysRepositoryException | WorkflowException e)
687        {
688            getLogger().error("Copy of items of catalog {} into catalog {} has failed", catalogToCopyName, catalogName);
689            throw new ProcessingException("Failed to copy catalog", e);
690        }
691        finally
692        {
693            _solrIndexHelper.restartSolrCommitForEvents(handledEventIds);
694        }
695    }
696
697    private void _addCopyStep(Collection<String> contentIds) throws AmetysRepositoryException, WorkflowException
698    {
699        for (String contentId : contentIds)
700        {
701            WorkflowAwareContent content = _resolver.resolveById(contentId);
702            _contentWorkflowHelper.doAction(content, getCopyActionId());
703        }
704    }
705    
706    /**
707     * Get the workflow action id for copy.
708     * @return The workflow action id
709     */
710    protected int getCopyActionId()
711    {
712        return 210;
713    }
714    
715    /**
716     * Delete catalog
717     * @param catalog the catalog to delete
718     * @return the result map
719     */
720    public Map<String, Object> deleteCatalog(Catalog catalog)
721    {
722        Map<String, Object> result = new HashMap<>();
723        result.put("id", catalog.getId());
724        
725        // Before deleting anything, we have to make sure that it's safe to delete the catalog and its programItems
726        String catalogName = catalog.getName();
727        AmetysObjectIterable<Content> programItems = getProgramItems(catalogName);
728        AmetysObjectIterable<CoursePart> courseParts = _getCourseParts(catalogName);
729        
730        List<Content> contentsToDelete = Stream.concat(courseParts.stream(), programItems.stream()).collect(Collectors.toUnmodifiableList());
731        
732        List<Content> referencingContents = _getExternalReferencingContents(contentsToDelete);
733        
734        if (!referencingContents.isEmpty())
735        {
736            for (Content content : referencingContents)
737            {
738                if (content instanceof ProgramItem || content instanceof CoursePart)
739                {
740                    getLogger().error("{} '{}' ({}) is referencing a content of the catalog {} while being itself in the catalog {}. There is an inconsistency.",
741                            content.getClass().getName(),
742                            content.getTitle(),
743                            content.getValue("code"),
744                            catalogName,
745                            content.getValue("catalog", false, StringUtils.EMPTY));
746                }
747                else
748                {
749                    getLogger().warn("Content {} ({}) is referencing a content of the catalog {}. There is an inconsistency.",
750                            content.getTitle(),
751                            content.getId(),
752                            catalogName);
753                }
754            }
755            result.put("error", "referencing-contents");
756            result.put("referencingContents", referencingContents);
757            return result;
758        }
759        
760        // Everything is fine, we can delete the courseParts, the programItems and the catalog
761        String[] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_DELETED};
762        try
763        {
764            _solrIndexHelper.pauseSolrCommitForEvents(handledEventIds);
765            contentsToDelete.forEach(this::_deleteContent);
766        }
767        finally
768        {
769            _solrIndexHelper.restartSolrCommitForEvents(handledEventIds);
770        }
771        
772        ModifiableAmetysObject parent = catalog.getParent();
773        catalog.remove();
774        parent.saveChanges();
775        
776        return result;
777    }
778
779    /**
780     * Get the course part of a catalog for all languages
781     * @param catalog The code of catalog
782     * @return The {@link CoursePart}s
783     */
784    private AmetysObjectIterable<CoursePart> _getCourseParts(String catalog)
785    {
786        return _getContentsInCatalog(catalog, CoursePartFactory.COURSE_PART_CONTENT_TYPE);
787    }
788
789    /**
790     * Get the list of contents referencing one of the {@code ProgramItem}s in the set.
791     * This method will ignore content that are included in the set and any {@code CoursePart} belonging to a {@code Course} of the set
792     * @param contentsToDelete the contents to be tested
793     * @return the contents referencing those program items excluding the course parts id
794     */
795    private List<Content> _getExternalReferencingContents(List<Content> contentsToDelete)
796    {
797        
798        // Get all the Contents referencing one of the content to delete but not one of the content to delete
799        List<Content> referencingContents = contentsToDelete.stream()
800            .map(Content::getReferencingContents)   // get the referencing contents
801            .flatMap(Collection::stream)            // flatten the Collection
802            .distinct()                             // remove all duplicates
803            .filter(content -> !contentsToDelete.contains(content))
804            .collect(Collectors.toUnmodifiableList());          // collect it into a list
805        return referencingContents;
806    }
807    
808    private void _deleteContent(Content content)
809    {
810        
811        Map<String, Object> eventParams = new HashMap<>();
812        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
813        eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName());
814        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
815        ModifiableAmetysObject parent = content.getParent();
816        
817        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParams));
818        
819        // Remove the content.
820        LockableAmetysObject lockedContent = (LockableAmetysObject) content;
821        if (lockedContent.isLocked())
822        {
823            lockedContent.unlock();
824        }
825        
826        ((RemovableAmetysObject) content).remove();
827        
828        
829        parent.saveChanges();
830        
831        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams));
832    }
833    
834    private ModifiableTraversableAmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException
835    {
836        ModifiableTraversableAmetysObject definitionsNode;
837        if (parentNode.hasChild(nodeName))
838        {
839            definitionsNode = parentNode.getChild(nodeName);
840        }
841        else
842        {
843            definitionsNode = parentNode.createChild(nodeName, nodeType);
844            parentNode.saveChanges();
845        }
846        return definitionsNode;
847    }
848}