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 Catalog _defaultCatalog;
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 (_defaultCatalog == null)
199        {
200            updateDefaultCatalog();
201        }
202        return _defaultCatalog;
203    }
204    
205    /**
206     * Updates the default catalog (if it's null or if the user has updated it).
207     */
208    void updateDefaultCatalog()
209    {
210        List<Catalog> catalogs = getCatalogs();
211        for (Catalog catalog : catalogs)
212        {
213            if (catalog.isDefault())
214            {
215                _defaultCatalog = catalog;
216                break;
217            }
218        }
219    }
220    
221    /**
222     * Get the name of the catalog of a ODF content
223     * @param contentId The id of content
224     * @return The catalog's name
225     */
226    @Callable
227    public String getContentCatalog(String contentId)
228    {
229        Content content = _resolver.resolveById(contentId);
230        
231        if (content instanceof ProgramItem)
232        {
233            return ((ProgramItem) content).getCatalog();
234        }
235        
236        // Get catalog from its parents (unecessary ?)
237        AmetysObject parent = content.getParent();
238        while (parent != null)
239        {
240            if (parent instanceof ProgramItem)
241            {
242                return ((ProgramItem) parent).getCatalog();
243            }
244            parent = parent.getParent();
245        }
246        
247        return null;
248    }
249    
250    /**
251     * Determines if the catalog can be modified from the given content
252     * @param contentId The content id
253     * @return A map with success=false if the catalog cannot be edited
254     */
255    @Callable
256    public Map<String, Object> canEditCatalog(String contentId)
257    {
258        Map<String, Object> result = new HashMap<>();
259        
260        Content content = _resolver.resolveById(contentId);
261        
262        if (content instanceof ProgramItem)
263        {
264            if (_isReferenced(content))
265            {
266                result.put("success", false);
267                result.put("error", "referenced");
268            }
269            else if (_hasSharedContent((ProgramItem) content, (ProgramItem) content))
270            {
271                result.put("success", false);
272                result.put("error", "hasSharedContent");
273            }
274            else
275            {
276                result.put("success", true);
277            }
278            
279        }
280        else
281        {
282            result.put("success", false);
283            result.put("error", "typeError");
284        }
285        
286        return result;
287    }
288    
289    private boolean _isReferenced (Content content)
290    {
291        return !_odfHelper.getParentProgramItems((ProgramItem) content).isEmpty();
292    }
293    
294    private boolean _hasSharedContent (ProgramItem rootProgramItem, ProgramItem programItem)
295    {
296        List<ProgramItem> children = _odfHelper.getChildProgramItems(programItem);
297        
298        for (ProgramItem child : children)
299        {
300            if (_isShared(rootProgramItem, child))
301            {
302                return true;
303            }
304        }
305        
306        if (programItem instanceof Course)
307        {
308            List<CoursePart> courseParts = ((Course) programItem).getCourseParts();
309            for (CoursePart coursePart : courseParts)
310            {
311                List<ProgramItem> parentCourses = coursePart.getCourses()
312                        .stream()
313                        .map(ProgramItem.class::cast)
314                        .collect(Collectors.toList());
315                if (parentCourses.size() > 1 && !_isPartOfSameStructure(rootProgramItem, parentCourses))
316                {
317                    return true;
318                }
319            }
320        }
321        
322        return false;
323    }
324    
325    private boolean _isShared(ProgramItem rootProgramItem, ProgramItem programItem)
326    {
327        try
328        {
329            List<ProgramItem> parents = _odfHelper.getParentProgramItems(programItem);
330            if ((parents.size() > 1 && !_isPartOfSameStructure(rootProgramItem, parents)) || _hasSharedContent(rootProgramItem, programItem))
331            {
332                return true;
333            }
334        }
335        catch (UnknownAmetysObjectException e)
336        {
337            // Nothing
338        }
339        
340        return false;
341    }
342    
343    private boolean _isPartOfSameStructure(ProgramItem rootProgramItem, List<ProgramItem> programItems)
344    {
345        for (ProgramItem programItem : programItems)
346        {
347            List<List<ProgramItem>> ancestorPaths = _odfHelper.getPathOfAncestors(programItem);
348            
349            boolean isPartOfInitalStructure = false;
350            for (List<ProgramItem> ancestorPath : ancestorPaths)
351            {
352                for (ProgramItem pathSegment : ancestorPath)
353                {
354                    if (pathSegment.equals(rootProgramItem)) 
355                    {
356                        isPartOfInitalStructure = true;
357                        break;
358                    }
359                }
360            }
361            
362            if (!isPartOfInitalStructure)
363            {
364                // The content is shared outside the program item to edit
365                return false;
366            }
367        }
368        
369        return true;
370    }
371    
372    /**
373     * Set the catalog of a content. This will modify recursively the catalog of referenced children
374     * @param catalog The catalog
375     * @param contentId The id of content to edit
376     * @throws WorkflowException if an error occurred
377     */
378    @Callable
379    public void setContentCatalog(String catalog, String contentId) throws WorkflowException
380    {
381        Content content = _resolver.resolveById(contentId);
382        
383        if (content instanceof ProgramItem)
384        {
385            _setCatalog(content, catalog);
386        }
387        else
388        {
389            throw new IllegalArgumentException("You can not edit the catalog of the content " + contentId);
390        }
391    }
392    
393    private void _setCatalog (Content content, String catalogName) throws WorkflowException
394    {
395        if (content instanceof ProgramItem)
396        {
397            String oldCatalog = ((ProgramItem) content).getCatalog();
398            if (!catalogName.equals(oldCatalog))
399            {
400                ((ProgramItem) content).setCatalog(catalogName);
401                
402                if (content instanceof WorkflowAwareContent)
403                {
404                    _applyChanges((WorkflowAwareContent) content);
405                }
406            }
407        }
408        else if (content instanceof CoursePart)
409        {
410            String oldCatalog = ((CoursePart) content).getCatalog();
411            if (!catalogName.equals(oldCatalog))
412            {
413                ((CoursePart) content).setCatalog(catalogName);
414                
415                if (content instanceof WorkflowAwareContent)
416                {
417                    _applyChanges((WorkflowAwareContent) content);
418                }
419            }
420        }
421        
422        _setCatalogToChildren(content, catalogName);
423    }
424    
425    private void _setCatalogToChildren (Content content, String catalogName) throws WorkflowException
426    {
427        if (content instanceof TraversableProgramPart)
428        {
429            ContentValue[] children = content.getValue(TraversableProgramPart.CHILD_PROGRAM_PARTS, false, new ContentValue[0]);
430            for (ContentValue child : children)
431            {
432                try
433                {
434                    _setCatalog(child.getContent(), catalogName);
435                }
436                catch (UnknownAmetysObjectException e)
437                {
438                    // Nothing
439                }
440            }
441        }
442        
443        if (content instanceof CourseContainer)
444        {
445            for (Course course : ((CourseContainer) content).getCourses())
446            {
447                _setCatalog(course, catalogName);
448            }
449        }
450        
451        if (content instanceof CourseListContainer)
452        {
453            for (CourseList cl : ((CourseListContainer) content).getCourseLists())
454            {
455                _setCatalog(cl, catalogName);
456            }
457        }
458        
459        if (content instanceof Course)
460        {
461            for (CoursePart coursePart : ((Course) content).getCourseParts())
462            {
463                _setCatalog(coursePart, catalogName);
464            }
465        }
466    }
467    
468    private void _applyChanges(WorkflowAwareContent content) throws WorkflowException
469    {
470        ((ModifiableDefaultContent) content).setLastContributor(_userProvider.getUser());
471        ((ModifiableDefaultContent) content).setLastModified(new Date());
472        
473        // Remove the proposal date.
474        content.setProposalDate(null);
475        
476        // Save changes
477        content.saveChanges();
478        
479        // Notify listeners
480        Map<String, Object> eventParams = new HashMap<>();
481        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
482        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
483        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _userProvider.getUser(), eventParams));
484       
485        _contentWorkflowHelper.doAction(content, 22);
486    }
487    
488    /**
489     * Get the root catalogs storage object.
490     * @return the root catalogs node
491     * @throws AmetysRepositoryException if a repository error occurs.
492     */
493    public ModifiableTraversableAmetysObject getCatalogsRootNode() throws AmetysRepositoryException
494    {
495        String originalWorkspace = null;
496        Request request = ContextHelper.getRequest(_context);
497        try
498        {
499            originalWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request);
500            if (ArchiveConstants.ARCHIVE_WORKSPACE.equals(originalWorkspace))
501            {
502                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, RepositoryConstants.DEFAULT_WORKSPACE);
503            }
504            
505            ModifiableTraversableAmetysObject rootNode = _resolver.resolveByPath("/");
506            ModifiableTraversableAmetysObject pluginsNode = _getOrCreateNode(rootNode, "ametys:plugins", "ametys:unstructured");
507            ModifiableTraversableAmetysObject pluginNode = _getOrCreateNode(pluginsNode, _pluginName, "ametys:unstructured");
508            
509            return _getOrCreateNode(pluginNode, "catalogs", "ametys:unstructured");
510        }
511        catch (AmetysRepositoryException e)
512        {
513            throw new AmetysRepositoryException("Unable to get the ODF catalogs root node", e);
514        }
515        finally
516        {
517            if (ArchiveConstants.ARCHIVE_WORKSPACE.equals(originalWorkspace))
518            {
519                RequestAttributeWorkspaceSelector.setForcedWorkspace(request, originalWorkspace);
520            }
521        }
522    }
523    
524    /**
525     * Create a new catalog
526     * @param name The unique name
527     * @param title The title of catalog
528     * @return the created catalog
529     */
530    public Catalog createCatalog(String name, String title)
531    {
532        Catalog newCatalog = null;
533        
534        ModifiableTraversableAmetysObject catalogsNode = getCatalogsRootNode();
535        
536        newCatalog = catalogsNode.createChild(name, "ametys:catalog");
537        newCatalog.setTitle(title);
538        
539        if (getCatalogs().size() == 1)
540        {
541            newCatalog.setDefault(true);
542        }
543        return newCatalog;
544    }
545    
546    /**
547     * Get the programs of a catalog for all languages
548     * @param catalog The code of catalog
549     * @return The programs
550     */
551    public AmetysObjectIterable<Program> getPrograms (String catalog)
552    {
553        return getPrograms(catalog, null);
554    }
555    
556    /**
557     * Get the program's items of a catalog for all languages
558     * @param catalog The code of catalog
559     * @return The {@link ProgramItem}
560     */
561    public AmetysObjectIterable<ProgramItem> getProgramItems(String catalog)
562    {
563        List<Expression> exprs = new ArrayList<>();
564        
565        exprs.add(_cTypeEP.createHierarchicalCTExpression(ProgramItem.PROGRAM_ITEM_CONTENT_TYPE));
566        exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
567        
568        Expression programItemsExpression = new AndExpression(exprs.toArray(new Expression[exprs.size()]));
569        
570        // Add sort criteria to get size
571        SortCriteria sortCriteria = new SortCriteria();
572        sortCriteria.addCriterion(Content.ATTRIBUTE_TITLE, true, true);
573        
574        String query = ContentQueryHelper.getContentXPathQuery(programItemsExpression, sortCriteria);
575        return _resolver.query(query);
576    }
577    
578    /**
579     * Get the programs of a catalog
580     * @param catalog The code of catalog
581     * @param lang The language. Can be null to get programs for all languages
582     * @return The programs
583     */
584    public AmetysObjectIterable<Program> getPrograms (String catalog, String lang)
585    {
586        List<Expression> exprs = new ArrayList<>();
587        exprs.add(new ContentTypeExpression(Operator.EQ, ProgramFactory.PROGRAM_CONTENT_TYPE));
588        exprs.add(new StringExpression(ProgramItem.CATALOG, Operator.EQ, catalog));
589        if (lang != null)
590        {
591            exprs.add(new LanguageExpression(Operator.EQ, lang));
592        }
593        
594        Expression programsExpression = new AndExpression(exprs.toArray(new Expression[exprs.size()]));
595        
596        // Add sort criteria to get size
597        SortCriteria sortCriteria = new SortCriteria();
598        sortCriteria.addCriterion(Content.ATTRIBUTE_TITLE, true, true);
599        
600        String programsQuery = QueryHelper.getXPathQuery(null, ProgramFactory.PROGRAM_NODETYPE, programsExpression, sortCriteria);
601        return _resolver.query(programsQuery);
602    }
603    
604    /**
605     * Copy the programs and its hierarchy from a catalog to another.
606     * The referenced courses are NOT copied.
607     * @param catalog The new catalog to populate
608     * @param catalogToCopy The catalog from which we copy the programs.
609     * @throws ProcessingException If an error occurred during copy
610     */
611    public void copyCatalog(Catalog catalog, Catalog catalogToCopy) throws ProcessingException
612    {
613        String catalogToCopyName = catalogToCopy.getName();
614        String catalogName = catalog.getName();
615        String [] handledEventIds = new String[] {ObservationConstants.EVENT_CONTENT_ADDED, ObservationConstants.EVENT_CONTENT_MODIFIED,  ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED};
616        try
617        {
618            Map<String, String> copiedPrograms = new HashMap<>();
619            Map<String, String> copiedSubPrograms = new HashMap<>();
620            Map<String, String> copiedContainers = new HashMap<>();
621            Map<String, String> copiedCourseLists = new HashMap<>();
622            Map<String, String> copiedCourses = new HashMap<>();
623            Map<String, String> copiedCourseParts = new HashMap<>();
624            
625            Set<String> copyUpdaters = _copyUpdaterEP.getExtensionsIds();
626            
627            AmetysObjectIterable<Program> programs = getPrograms(catalogToCopyName);
628            
629            // Do NOT commit yet to Solr in order to improve perfs
630            _observationManager.addArgumentForEvents(handledEventIds, ObservationConstants.ARGS_CONTENT_COMMIT, false);
631
632            long start = System.currentTimeMillis();
633            
634            getLogger().debug("Begin to iterate over programs for copying them");
635            
636            for (Program program : programs)
637            {
638                if (getLogger().isDebugEnabled())
639                {
640                    getLogger().debug("Start copying program '{}' (name: '{}', title: '{}')...", program.getId(), program.getName(), program.getTitle());
641                }
642                
643                Program newProgram = _odfHelper.copyProgramItem(program, catalogName, true, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
644                
645                for (String updaterId : copyUpdaters)
646                {
647                    // Call updaters after copy of program
648                    CopyCatalogUpdater updater = _copyUpdaterEP.getExtension(updaterId);
649                    updater.updateContent(catalogToCopyName, catalogName, program, newProgram);
650                }
651            }
652
653            for (String updaterId : copyUpdaters)
654            {
655                // Call updaters after full copy of catalog
656                CopyCatalogUpdater updater = _copyUpdaterEP.getExtension(updaterId);
657                updater.updateContents(catalogToCopyName, catalogName, copiedPrograms, copiedSubPrograms, copiedContainers, copiedCourseLists, copiedCourses, copiedCourseParts);
658            }
659            
660            // Workflow
661            _addCopyStep(copiedPrograms.values());
662            _addCopyStep(copiedSubPrograms.values());
663            _addCopyStep(copiedContainers.values());
664            _addCopyStep(copiedCourseLists.values());
665            _addCopyStep(copiedCourses.values());
666            _addCopyStep(copiedCourseParts.values());
667
668            if (getLogger().isDebugEnabled())
669            {
670                getLogger().debug("End of iteration over programs for copying them ({})", Duration.of((System.currentTimeMillis() - start) / 1000, ChronoUnit.SECONDS));
671            }
672            
673        }
674        catch (AmetysRepositoryException | WorkflowException e)
675        {
676            getLogger().error("Copy of items of catalog {} into catalog {} has failed", catalogToCopyName, catalogName);
677            throw new ProcessingException("Failed to copy catalog", e);
678        }
679        finally
680        {
681            _observationManager.removeArgumentForEvents(handledEventIds, ObservationConstants.ARGS_CONTENT_COMMIT);
682            
683            // Before trying to commit, be sure all the async observers of the current request are finished
684            for (Future future : _observationManager.getFuturesForRequest())
685            {
686                try
687                {
688                    future.get();
689                }
690                catch (ExecutionException | InterruptedException e)
691                {
692                    getLogger().info("An exception occured when calling #get() on Future result of an observer." , e);
693                }
694            }
695            
696            // Commit all uncommited changes
697            try
698            {
699                _solrIndexer.commit();
700                
701                getLogger().debug("Copied contents are now committed into Solr.");
702            }
703            catch (IOException | SolrServerException e)
704            {
705                getLogger().error("Impossible to commit changes", e);
706            }
707        }
708    }
709    
710    private void _addCopyStep(Collection<String> contentIds) throws AmetysRepositoryException, WorkflowException
711    {
712        for (String contentId : contentIds)
713        {
714            WorkflowAwareContent content = _resolver.resolveById(contentId);
715            _contentWorkflowHelper.doAction(content, getCopyActionId());
716        }
717    }
718    
719    /**
720     * Get the workflow action id for copy.
721     * @return The workflow action id
722     */
723    protected int getCopyActionId()
724    {
725        return 210;
726    }
727    
728    /**
729     * Delete catalog
730     * @param id the id of catalog
731     */
732    public void deleteCatalog (String id)
733    {
734        try
735        {
736            Catalog catalog = _resolver.resolveById(id);
737            if (catalog != null)
738            {
739                catalog.remove();
740                catalog.saveChanges();
741            }
742        }
743        catch (UnknownAmetysObjectException e)
744        {
745            // Nothing
746        }
747        
748    }
749    
750    private ModifiableTraversableAmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException
751    {
752        ModifiableTraversableAmetysObject definitionsNode;
753        if (parentNode.hasChild(nodeName))
754        {
755            definitionsNode = parentNode.getChild(nodeName);
756        }
757        else
758        {
759            definitionsNode = parentNode.createChild(nodeName, nodeType);
760            parentNode.saveChanges();
761        }
762        return definitionsNode;
763    }
764}