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