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