001/*
002 *  Copyright 2018 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.helper;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.concurrent.ExecutionException;
028import java.util.concurrent.Future;
029import java.util.stream.Collectors;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.avalon.framework.service.Serviceable;
035import org.apache.commons.lang3.ArrayUtils;
036import org.apache.commons.lang3.StringUtils;
037import org.apache.commons.lang3.tuple.Pair;
038import org.apache.solr.client.solrj.SolrServerException;
039
040import org.ametys.cms.ObservationConstants;
041import org.ametys.cms.clientsideelement.content.SmartContentClientSideElementHelper;
042import org.ametys.cms.content.ContentHelper;
043import org.ametys.cms.content.external.ExternalizableMetadataHelper;
044import org.ametys.cms.content.indexing.solr.SolrIndexer;
045import org.ametys.cms.data.ContentDataHelper;
046import org.ametys.cms.repository.Content;
047import org.ametys.cms.repository.WorkflowAwareContent;
048import org.ametys.cms.workflow.ContentWorkflowHelper;
049import org.ametys.core.observation.Event;
050import org.ametys.core.observation.ObservationManager;
051import org.ametys.core.right.RightManager;
052import org.ametys.core.right.RightManager.RightResult;
053import org.ametys.core.user.CurrentUserProvider;
054import org.ametys.odf.ODFHelper;
055import org.ametys.odf.ProgramItem;
056import org.ametys.odf.course.Course;
057import org.ametys.odf.course.ShareableCourseConstants;
058import org.ametys.odf.courselist.CourseList;
059import org.ametys.odf.orgunit.OrgUnit;
060import org.ametys.odf.person.Person;
061import org.ametys.odf.program.Container;
062import org.ametys.odf.program.Program;
063import org.ametys.odf.program.ProgramPart;
064import org.ametys.odf.program.SubProgram;
065import org.ametys.odf.program.TraversableProgramPart;
066import org.ametys.plugins.repository.AmetysObjectResolver;
067import org.ametys.plugins.repository.AmetysRepositoryException;
068import org.ametys.plugins.repository.ModifiableAmetysObject;
069import org.ametys.plugins.repository.RemovableAmetysObject;
070import org.ametys.plugins.repository.lock.LockableAmetysObject;
071import org.ametys.runtime.plugin.component.AbstractLogEnabled;
072
073import com.opensymphony.workflow.WorkflowException;
074
075/**
076 * Helper to delete an ODF content.
077 */
078public class DeleteODFContentHelper extends AbstractLogEnabled implements Component, Serviceable
079{
080    /** Avalon role. */
081    public static final String ROLE = DeleteODFContentHelper.class.getName();
082    
083    /** Ametys object resolver */
084    private AmetysObjectResolver _resolver;
085    
086    /** The ODF helper */
087    private ODFHelper _odfHelper;
088    
089    /** Observer manager. */
090    private ObservationManager _observationManager;
091    
092    /** The Solr indexer. */
093    private SolrIndexer _solrIndexer;
094
095    /** The Content workflow helper */
096    private ContentWorkflowHelper _contentWorkflowHelper;
097    
098    /** The current user provider */
099    private CurrentUserProvider _currentUserProvider;
100    
101    /** The rights manager */
102    private RightManager _rightManager;
103
104    /** The content helper */
105    private ContentHelper _contentHelper;
106    
107    /** Helper for smart content client elements */
108    private SmartContentClientSideElementHelper _smartHelper;
109    
110    public void service(ServiceManager manager) throws ServiceException
111    {
112        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
113        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
114        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
115        _solrIndexer = (SolrIndexer) manager.lookup(SolrIndexer.ROLE);
116        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
117        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
118        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
119        _smartHelper = (SmartContentClientSideElementHelper) manager.lookup(SmartContentClientSideElementHelper.ROLE);
120        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
121    }
122    
123    /**
124     * Enumeration for the mode of deletion
125     *
126     */
127    public enum DeleteMode
128    {
129        /** Delete the content only */
130        SINGLE,
131        /** Delete the content and its structure */
132        STRUCTURE_ONLY,
133        /** Delete the content and its structure and its courses */
134        FULL
135    }
136    
137    /**
138     * Delete ODF contents
139     * @param contentsId The ids of contents to delete
140     * @param modeParam The mode of deletion
141     * @return the deleted and undeleted contents
142     */
143    public Map<String, Object> deleteContents(List<String> contentsId, String modeParam)
144    {
145        return deleteContents(contentsId, modeParam, false);
146    }
147    
148    /**
149     * Delete ODF contents
150     * @param contentsId The ids of contents to delete
151     * @param modeParam The mode of deletion
152     * @param ignoreRights If true, bypass the rights check during the deletion
153     * @return the deleted and undeleted contents
154     */
155    public Map<String, Object> deleteContents(List<String> contentsId, String modeParam, boolean ignoreRights)
156    {
157        Map<String, Object> results = new HashMap<>();
158        
159        List<String> alreadyDeletedContentIds = new ArrayList<>();
160        for (String contentId : contentsId)
161        {
162            Map<String, Object> result = new HashMap<>();
163            result.put("deleted-contents", new HashSet<>());
164            result.put("undeleted-contents", new HashSet<>());
165            result.put("referenced-contents", new HashSet<>());
166            result.put("unauthorized-contents", new HashSet<>());
167            result.put("locked-contents", new HashSet<>());
168
169            if (!alreadyDeletedContentIds.contains(contentId))
170            {
171                Content content = _resolver.resolveById(contentId);
172                
173                result.put("initial-content", content.getId());
174                
175                DeleteMode deleteMode = StringUtils.isNotBlank(modeParam) ? DeleteMode.valueOf(modeParam.toUpperCase()) : DeleteMode.SINGLE;
176                
177                boolean referenced = isContentReferenced(content);
178                if (referenced || !_checkBeforeDeletion(content, deleteMode, ignoreRights, result))
179                {
180                    if (referenced)
181                    {
182                        // Indicate that the content is referenced.
183                        @SuppressWarnings("unchecked")
184                        Set<Content> referencedContents = (Set<Content>) result.get("referenced-contents");
185                        referencedContents.add(content);
186                    }
187                    result.put("check-before-deletion-failed", true);
188                }
189                else
190                {
191                    // Process deletion
192                    _deleteContent(content, deleteMode, ignoreRights, result);
193                    
194                    @SuppressWarnings("unchecked")
195                    Set<String> deletedContents = (Set<String>) result.get("deleted-contents");
196                    if (deletedContents != null)
197                    {
198                        alreadyDeletedContentIds.addAll(deletedContents);
199                    }
200                }
201            }
202            
203            results.put(contentId, result);
204        }
205
206        return results;
207    }
208    
209    /**
210     * Delete ODF contents
211     * @param contentsId The ids of contents to delete
212     * @param modeParam The mode of deletion
213     * @return the deleted and undeleted contents
214     */
215    public Map<String, Object> deleteContentsWithLog(List<String> contentsId, String modeParam)
216    {
217        return deleteContentsWithLog(contentsId, modeParam, false);
218    }
219    
220    /**
221     * Delete ODF contents
222     * @param contentsId The ids of contents to delete
223     * @param modeParam The mode of deletion
224     * @param ignoreRights If true, bypass the rights check during the deletion
225     * @return the deleted and undeleted contents
226     */
227    @SuppressWarnings("unchecked")
228    public Map<String, Object> deleteContentsWithLog(List<String> contentsId, String modeParam, boolean ignoreRights)
229    {
230        Map<String, Object> results = deleteContents(contentsId, modeParam, ignoreRights);
231        for (String contentId : contentsId)
232        {
233            Map<String, Object> result = (Map<String, Object>) results.get(contentId);
234            
235            Set<Content> referencedContents = (Set<Content>) result.get("referenced-contents");
236            if (referencedContents.size() > 0)
237            {
238                getLogger().info("The following contents cannot be deleted because they are referenced: {}", referencedContents.stream().map(m -> m.getId()).collect(Collectors.toList()));
239            }
240            
241            Set<Content> lockedContents = (Set<Content>) result.get("locked-contents");
242            if (lockedContents.size() > 0)
243            {
244                getLogger().info("The following contents cannot be deleted because they are locked: {}", lockedContents.stream().map(m -> m.getId()).collect(Collectors.toList()));
245            }
246            
247            Set<Content> unauthorizedContents = (Set<Content>) result.get("unauthorized-contents");
248            if (unauthorizedContents.size() > 0)
249            {
250                getLogger().info("The following contents cannot be deleted because they are no authorization: {}", unauthorizedContents.stream().map(m -> m.getId()).collect(Collectors.toList()));
251            }
252            
253            Set<Content> undeletedContents = (Set<Content>) result.get("undeleted-contents");
254            if (undeletedContents.size() > 0)
255            {
256                getLogger().info("{} contents were not deleted. See previous logs for more information.", undeletedContents.size());
257            }
258        }
259        
260        return results;
261    }
262    
263    /**
264     * Delete one content
265     * @param content the content to delete
266     * @param deleteMode The deletion mode
267     * @param ignoreRights If true, bypass the rights check during the deletion
268     * @param results the results map
269     */
270    private void _deleteContent(Content content, DeleteMode deleteMode, boolean ignoreRights, Map<String, Object> results)
271    {
272        if (content instanceof OrgUnit)
273        {
274            // 1- First delete relation to parent
275            OrgUnit parentOrgUnit = ((OrgUnit) content).getParentOrgUnit();
276            boolean success = true;
277            if (parentOrgUnit != null)
278            {
279                success = _removeRelation(parentOrgUnit, content, OrgUnit.CHILD_ORGUNITS, 22, results);
280            }
281            
282            // 2 - If succeed, process to deletion
283            if (success)
284            {
285                _deleteOrgUnit((OrgUnit) content, ignoreRights, results);
286            }
287            else
288            {
289                @SuppressWarnings("unchecked")
290                Set<Content> undeletedContents = (Set<Content>) results.get("undeleted-contents");
291                undeletedContents.add(content);
292            }
293        }
294        else if (content instanceof Course)
295        {
296            // 1 - First delete relation to parents
297            List<CourseList> courseLists = ((Course) content).getParentCourseLists();
298            
299            boolean success = _removeRelations(courseLists, content, CourseList.CHILD_COURSES, 22, results);
300            
301            // 2 - If succeed, process to deletion
302            if (success)
303            {
304                _deleteProgramItem((Course) content, deleteMode, ignoreRights, results);
305            }
306            else
307            {
308                @SuppressWarnings("unchecked")
309                Set<Content> undeletedContents = (Set<Content>) results.get("undeleted-contents");
310                undeletedContents.add(content);
311            }
312        }
313        else if (content instanceof CourseList)
314        {
315            // 1 - First delete relation to parents
316            List<ProgramPart> parentProgramItems = ((CourseList) content).getProgramPartParents();
317            List<WorkflowAwareContent> parentContents = parentProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList());
318            
319            boolean success = _removeRelations(parentContents, content, TraversableProgramPart.CHILD_PROGRAM_PARTS, 22, results);
320            if (success)
321            {
322                List<Course> parentCourses = ((CourseList) content).getParentCourses();
323                parentContents = parentCourses.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList());
324                
325                success = _removeRelations(parentCourses, content, Course.CHILD_COURSE_LISTS, 22, results);
326            }
327            
328            // 2 - If succeed, process to deletion
329            if (success)
330            {
331                _deleteProgramItem((CourseList) content, deleteMode, ignoreRights, results);
332            }
333            else
334            {
335                @SuppressWarnings("unchecked")
336                Set<Content> undeletedContents = (Set<Content>) results.get("undeleted-contents");
337                undeletedContents.add(content);
338            }
339        }
340        else if (content instanceof TraversableProgramPart)
341        {
342            // 1 - First delete relation to parents
343            List<ProgramItem> parentProgramItems = _odfHelper.getParentProgramItems((ProgramPart) content);
344            List<WorkflowAwareContent> parentContents = parentProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList());
345            
346            boolean success = _removeRelations(parentContents, content, TraversableProgramPart.CHILD_PROGRAM_PARTS, 22, results);
347            
348            // 2 - If succeed, process to deletion
349            if (success)
350            {
351                _deleteProgramItem((ProgramPart) content, deleteMode, ignoreRights, results);
352            }
353            else
354            {
355                @SuppressWarnings("unchecked")
356                Set<Content> undeletedContents = (Set<Content>) results.get("undeleted-contents");
357                undeletedContents.add(content);
358            }
359        }
360        else
361        {
362            throw new IllegalArgumentException("The content [" + content.getId() + "] is not of the expected type, it can't be deleted.");
363        }
364    }
365
366    /**
367     * Test if content is still referenced before removing it
368     * @param content The content to remove
369     * @return true if content is still referenced
370     */
371    public boolean isContentReferenced(Content content)
372    {
373        return _isContentReferenced(content, null);
374    }
375    
376    /**
377     * Test if content is still referenced before removing it.
378     * @param content The content to remove
379     * @param rootContent the initial content to delete (can be null if checkRoot is false)
380     * @return true if content is still referenced
381     */
382    private boolean _isContentReferenced(Content content, Content rootContent)
383    {
384        if (content instanceof OrgUnit)
385        {
386            return _isReferencedOrgUnit((OrgUnit) content);
387        }
388        else if (content instanceof ProgramItem)
389        {
390            if (rootContent != null)
391            {
392                return _isReferencedContentCheckingRoot((ProgramItem) content, rootContent);
393            }
394            else
395            {
396                List<ProgramItem> ignoredRefContent = _odfHelper.getChildProgramItems((ProgramItem) content);
397                if (!(content instanceof Program)) // TODO remove content instanceof Course
398                {
399                    ignoredRefContent.addAll(_odfHelper.getParentProgramItems((ProgramItem) content));
400                }
401                
402                for (Pair<String, Content> refPair : _contentHelper.getReferencingContents(content))
403                {
404                    Content refContent = refPair.getValue();
405                    String path = refPair.getKey();
406                    
407                    // Ignoring reference from shareable field
408                    if (!(refContent instanceof Course && path.equals(ShareableCourseConstants.PROGRAMS_FIELD_ATTRIBUTE_NAME)))
409                    {
410                        // The ref content is not ignored
411                        if (refContent instanceof ProgramItem && !ignoredRefContent.contains((ProgramItem) refContent))
412                        {
413                            return true;
414                        }
415                    }
416                }
417
418                return false;
419            }
420        }
421        
422        return content.hasReferencingContents();
423    }
424    
425    /**
426     * True if the orgUnit is referenced
427     * @param orgUnit the orgUnit
428     * @return true if the orgUnit is referenced
429     */
430    private boolean _isReferencedOrgUnit(OrgUnit orgUnit)
431    {
432        OrgUnit parentOrgUnit = orgUnit.getParentOrgUnit();
433
434        List<String> relatedOrgUnit = orgUnit.getSubOrgUnits();
435        if (parentOrgUnit != null)
436        {
437            relatedOrgUnit.add(parentOrgUnit.getId());
438        }
439        
440        for (Pair<String, Content> refPair : _contentHelper.getReferencingContents(orgUnit))
441        {
442            Content refContent = refPair.getValue();
443            String path = refPair.getKey();
444            
445            // Ignoring reference from shareable field
446            if (!(refContent instanceof Course && path.equals(ShareableCourseConstants.ORGUNITS_FIELD_ATTRIBUTE_NAME)))
447            {
448                if (!relatedOrgUnit.contains(refContent.getId()))
449                {
450                    return true;
451                }
452            }
453        }
454        
455        return false;
456    }
457
458    /**
459     * Check that deletion can be performed without blocking errors
460     * @param content The initial content to delete
461     * @param mode The deletion mode
462     * @param ignoreRights If true, bypass the rights check during the deletion
463     * @param results The results
464     * @return true if the deletion can be performed
465     */
466    private boolean _checkBeforeDeletion(Content content, DeleteMode mode, boolean ignoreRights, Map<String, Object> results)
467    {
468        // Check right and lock on content it self
469        boolean allRight = _canDeleteContent(content, ignoreRights, results);
470        
471        // Check lock on parent contents
472        allRight = _checkParentsBeforeDeletion(content, results) && allRight;
473        
474        // Check right and lock on children to be deleted or modified
475        allRight = _checkChildrenBeforeDeletion(content, content, mode, ignoreRights, results) && allRight;
476        
477        return allRight;
478    }
479    
480    private boolean _checkParentsBeforeDeletion(Content content,  Map<String, Object> results)
481    {
482        boolean allRight = true;
483        
484        // Check if parents are not locked
485        List< ? extends WorkflowAwareContent> parents = _getParents(content);
486        for (WorkflowAwareContent parent : parents)
487        {
488            if (_smartHelper.isLocked(parent))
489            {
490                @SuppressWarnings("unchecked")
491                Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
492                lockedContents.add(content);
493                
494                allRight = false;
495            }
496        }
497        
498        return allRight;
499    }
500    
501    /**
502     * Browse children to check if deletion could succeed
503     * @param rootContentToDelete The initial content to delete
504     * @param contentToCheck The current content to check
505     * @param mode The deletion mode
506     * @param ignoreRights If true, bypass the rights check during the deletion
507     * @param results The result
508     * @return true if the deletion can be processed
509     */
510    private boolean _checkChildrenBeforeDeletion(Content rootContentToDelete, Content contentToCheck, DeleteMode mode, boolean ignoreRights, Map<String, Object> results)
511    {
512        boolean allRight = true;
513        
514        if (contentToCheck instanceof ProgramItem)
515        {
516            allRight = _checkChildrenBeforeDeletionOfProgramItem(rootContentToDelete, contentToCheck, mode, ignoreRights, results);
517        }
518        else if (contentToCheck instanceof OrgUnit)
519        {
520            allRight = _checkChildrenBeforeDeletionOfOrgUnit(rootContentToDelete, contentToCheck, mode, ignoreRights, results);
521        }
522        
523        return allRight;
524    }
525    
526    private boolean _checkChildrenBeforeDeletionOfProgramItem(Content rootContentToDelete, Content contentToCheck, DeleteMode mode,  boolean ignoreRights, Map<String, Object> results)
527    {
528        boolean allRight = true;
529
530        List<ProgramItem> childProgramItems = _odfHelper.getChildProgramItems((ProgramItem) contentToCheck);
531        for (ProgramItem childProgramItem : childProgramItems)
532        {
533            Content childContent = (Content) childProgramItem;
534            if (_smartHelper.isLocked(childContent))
535            {
536                // Lock should be checked for all children
537                @SuppressWarnings("unchecked")
538                Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
539                lockedContents.add(childContent);
540                
541                allRight = false;
542            }
543
544            // If content is not referenced it should be deleted, so check right
545            if ((mode == DeleteMode.FULL || (mode == DeleteMode.STRUCTURE_ONLY && !(childProgramItem instanceof Course))) && !_isContentReferenced(childContent, rootContentToDelete) && !ignoreRights && !hasRight(childContent))
546            {
547                // User has no sufficient right
548                @SuppressWarnings("unchecked")
549                Set<Content> norightContents = (Set<Content>) results.get("unauthorized-contents");
550                norightContents.add(childContent);
551                
552                allRight = false;
553            }
554            
555            if (mode != DeleteMode.SINGLE && !(mode == DeleteMode.STRUCTURE_ONLY && childProgramItem instanceof Course))
556            {
557                // Browse children recursively
558                allRight = _checkChildrenBeforeDeletion(rootContentToDelete, childContent, mode, ignoreRights, results) && allRight;
559            }
560        }
561        
562        return allRight;
563    }
564    
565    private boolean _checkChildrenBeforeDeletionOfOrgUnit(Content rootContentToDelete, Content contentToCheck, DeleteMode mode, boolean ignoreRights, Map<String, Object> results)
566    {
567        boolean allRight = true;
568
569        List<String> childOrgUnits = ((OrgUnit) contentToCheck).getSubOrgUnits();
570        for (String childOrgUnitId : childOrgUnits)
571        {
572            OrgUnit childOrgUnit = _resolver.resolveById(childOrgUnitId);
573            if (!_canDeleteContent(childOrgUnit, ignoreRights, results))
574            {
575                allRight = false;
576            }
577            else if (_isReferencedOrgUnit(childOrgUnit))
578            {
579                @SuppressWarnings("unchecked")
580                Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents");
581                referencedContents.add(childOrgUnit);
582                
583                allRight = false;
584            }
585            else
586            {
587                // Browse children recursively
588                allRight = _checkChildrenBeforeDeletion(rootContentToDelete, childOrgUnit, mode, ignoreRights, results) && allRight;
589            }
590        }
591        
592        return allRight;
593    }
594
595    /**
596     * Remove the relations between the content and its contents list
597     * @param contentsToEdit the contents to edit
598     * @param refContentToRemove The referenced content to be removed from contents
599     * @param metadataName The name of metadata holding the relationship
600     * @param actionId The id of workflow action to edit the relation
601     * @param results the results map
602     * @return true if remove relation successfully
603     */
604    private boolean _removeRelations(List<? extends WorkflowAwareContent> contentsToEdit, Content refContentToRemove, String metadataName, int actionId, Map<String, Object> results)
605    {
606        boolean success = true;
607        
608        for (Content contentToEdit : contentsToEdit)
609        {
610            success = _removeRelation((WorkflowAwareContent) contentToEdit, refContentToRemove, metadataName, actionId, results) && success;
611        }
612        
613        return success;
614    }
615    
616    /**
617     * Remove the relation parent-child relation on content. 
618     * @param contentToEdit The content to modified
619     * @param refContentToRemove The referenced content to be removed from content
620     * @param metadataName The name of metadata holding the child or parent relationship
621     * @param actionId The id of workflow action to edit the relation
622     * @param results the results map
623     * @return boolean true if remove relation successfully
624     */
625    private boolean _removeRelation(WorkflowAwareContent contentToEdit, Content refContentToRemove, String metadataName, int actionId, Map<String, Object> results)
626    {
627        try
628        {
629            String[] values = ContentDataHelper.getContentIdsArrayFromMultipleContentData(contentToEdit, metadataName);
630            
631            if (ArrayUtils.contains(values, refContentToRemove.getId()))
632            {
633                String[] newValues = ArrayUtils.removeElement(values, refContentToRemove.getId());
634                
635                List<Content> newContents = Arrays.asList(newValues).stream()
636                        .map(id -> (Content) _resolver.resolveById(id))
637                        .collect(Collectors.toList());
638                
639                // Set Jcr content reference if we pass contents in arguments
640                ExternalizableMetadataHelper.setMetadata(contentToEdit.getMetadataHolder(), metadataName, newContents.toArray(new Content[newContents.size()]));
641                _applyChanges(contentToEdit, actionId);
642            }
643            
644            return true;
645        }
646        catch (WorkflowException | AmetysRepositoryException e)
647        {
648            getLogger().error("Unable to remove relationship to content {} ({}) on content {} ({}) for metadata {}", refContentToRemove.getTitle(), refContentToRemove.getId(), contentToEdit.getTitle(), contentToEdit.getId(), metadataName, e);
649            return false;
650        }
651    }
652    
653    /**
654     * Delete a program item
655     * @param item The program item to delete
656     * @param mode The deletion mode
657     * @param ignoreRights If true, bypass the rights check during the deletion
658     * @param results The results
659     */
660    private void _deleteProgramItem(ProgramItem item, DeleteMode mode, boolean ignoreRights, Map<String, Object> results)
661    {
662        if (mode == DeleteMode.SINGLE)
663        {
664            if (_canDeleteContent((Content) item, ignoreRights, results))
665            {
666                // 1 - First remove relations with children
667                String parentMetadataName;
668                if (item instanceof Course)
669                {
670                    parentMetadataName = CourseList.PARENT_COURSES;
671                }
672                else if (item instanceof CourseList)
673                {
674                    parentMetadataName = Course.PARENT_COURSE_LISTS;
675                }
676                else
677                {
678                    parentMetadataName = ProgramPart.PARENT_PROGRAM_PARTS;
679                }
680                
681                List<ProgramItem> childProgramItems = _odfHelper.getChildProgramItems(item);
682                List<WorkflowAwareContent> childContents = childProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList());
683                
684                boolean success = _removeRelations(childContents, (Content) item, parentMetadataName, 22, results);
685                if (success)
686                {
687                    // If success delete course
688                    _finalizeDeleteContents(Collections.singleton(item.getId()), item.getParent(), results);
689                }
690            }
691        }
692        else
693        {
694            Set<String> toDelete = _getChildrenIdToDelete(item, item, results, mode, ignoreRights);
695            _finalizeDeleteContents(toDelete, item.getParent(), results);
696        }
697    }
698    
699   
700    /**
701     * Delete one orgUnit
702     * @param orgUnit the orgUnit to delete
703     * @param ignoreRights If true, bypass the rights check during the deletion
704     * @param results the results map
705     */
706    @SuppressWarnings("unchecked")
707    private void _deleteOrgUnit(OrgUnit orgUnit, boolean ignoreRights, Map<String, Object> results)
708    {
709        Set<String> toDelete = _getChildrenIdToDelete(orgUnit, ignoreRights, results);
710        
711        Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents");
712        Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
713        Set<Content> unauthorizedContents = (Set<Content>) results.get("unauthorized-contents");
714        
715        if (referencedContents.size() == 0 && lockedContents.size() == 0 && unauthorizedContents.size() == 0)
716        {
717            _finalizeDeleteContents(toDelete, orgUnit.getParent(), results);
718        }
719    }
720
721    /**
722     * Finalize the deletion of contents. Call observers and remove contents
723     * @param contentIdsToDelete the list of content id to delete
724     * @param parent the jcr parent for saving changes
725     * @param results the results map
726     */
727    private void _finalizeDeleteContents(Set<String> contentIdsToDelete, ModifiableAmetysObject parent, Map<String, Object> results)
728    {
729        @SuppressWarnings("unchecked")
730        Set<Content> unauthorizedContents = (Set<Content>) results.get("unauthorized-contents");
731        @SuppressWarnings("unchecked")
732        Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
733        
734        if (!unauthorizedContents.isEmpty() || !lockedContents.isEmpty())
735        {
736            //Do Nothing
737            return;
738        }
739        
740        try
741        {
742            _observationManager.addArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT, false);
743            
744            Map<String, Map<String, Object>> eventParams = new HashMap<>();
745            for (String id : contentIdsToDelete)
746            {
747                Content content = _resolver.resolveById(id);
748                Map<String, Object> eventParam = _getEventParametersForDeletion(content);
749                
750                eventParams.put(id, eventParam);
751                
752                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParam));
753                
754                // Remove the content.
755                LockableAmetysObject lockedContent = (LockableAmetysObject) content;
756                if (lockedContent.isLocked())
757                {
758                    lockedContent.unlock();
759                }
760                
761                ((RemovableAmetysObject) content).remove();
762            }
763            
764            
765            parent.saveChanges();
766            
767            for (String id : contentIdsToDelete)
768            {
769                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams.get(id)));
770                
771                @SuppressWarnings("unchecked")
772                Set<String> deletedContents = (Set<String>) results.get("deleted-contents");
773                deletedContents.add(id);
774            }
775        }
776        finally 
777        {
778            _observationManager.removeArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT);
779            _commitAllChanges();
780        }
781    }
782    
783    /**
784     * Commit all changes in solr
785     */
786    private void _commitAllChanges()
787    {
788        // Before trying to commit, be sure all the async observers of the current request are finished
789        for (Future future : _observationManager.getFuturesForRequest())
790        {
791            try
792            {
793                future.get();
794            }
795            catch (ExecutionException | InterruptedException e)
796            {
797                getLogger().info("An exception occured when calling #get() on Future result of an observer." , e);
798            }
799        }
800        
801        // Commit all uncommited changes
802        try
803        {
804            _solrIndexer.commit();
805            
806            getLogger().debug("Deleted contents are now committed into Solr.");
807        }
808        catch (IOException | SolrServerException e)
809        {
810            getLogger().error("Impossible to commit changes", e);
811        }
812    }
813    
814    /**
815     * True if we can delete the content (check if removable, rights and if locked)
816     * @param content the content
817     * @param ignoreRights If true, bypass the rights check during the deletion
818     * @param results the results map
819     * @return true if we can delete the content
820     */
821    private boolean _canDeleteContent(Content content, boolean ignoreRights, Map<String, Object> results)
822    {
823        if (!(content instanceof RemovableAmetysObject))
824        {
825            throw new IllegalArgumentException("The content [" + content.getId() + "] is not a RemovableAmetysObject, it can't be deleted.");
826        }
827        
828        if (!ignoreRights && !hasRight(content))
829        {
830            // User has no sufficient right
831            @SuppressWarnings("unchecked")
832            Set<Content> norightContents = (Set<Content>) results.get("unauthorized-contents");
833            norightContents.add(content);
834            
835            return false;
836        }
837        else if (_smartHelper.isLocked(content))
838        {
839            @SuppressWarnings("unchecked")
840            Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
841            lockedContents.add(content);
842            
843            return false;
844        }
845        
846        return true;
847    }
848    
849    private void _applyChanges(WorkflowAwareContent content, int actionId) throws WorkflowException
850    {
851        // Notify listeners
852        Map<String, Object> eventParams = new HashMap<>();
853        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
854        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
855        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
856       
857        _contentWorkflowHelper.doAction(content, actionId);
858    }
859    
860    /**
861     * Get parameters for content deleted {@link Event}
862     * @param content the removed content
863     * @return the event's parameters
864     */
865    private Map<String, Object> _getEventParametersForDeletion (Content content)
866    {
867        Map<String, Object> eventParams = new HashMap<>();
868        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
869        eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName());
870        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
871        return eventParams;
872    }
873    
874    private List<? extends WorkflowAwareContent> _getParents(Content content)
875    {
876        if (content instanceof OrgUnit)
877        {
878            return Collections.singletonList(((OrgUnit) content).getParentOrgUnit());
879        }
880        else if (content instanceof Course)
881        {
882            return ((Course) content).getParentCourseLists();
883        }
884        else if (content instanceof CourseList)
885        {
886            List<ProgramPart> parentProgramItems = ((CourseList) content).getProgramPartParents();
887            List<WorkflowAwareContent> parents = parentProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList());
888            
889            List<Course> parentCourses = ((CourseList) content).getParentCourses();
890            parents.addAll(parentCourses.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList()));
891            
892            return parents;
893        }
894        else if (content instanceof ProgramPart)
895        {
896            List<ProgramItem> parentProgramItems = _odfHelper.getParentProgramItems((ProgramPart) content);
897            return parentProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList());
898        }
899        
900        return Collections.EMPTY_LIST;
901    }
902    
903    /**
904     * Get the id of children to be deleted. 
905     * All children shared with other contents which are not part of deletion, will be not deleted.
906     * @param orgUnit The orgunit to delete
907     * @param ignoreRights If true, bypass the rights check during the deletion
908     * @param results The results
909     * @return The id of contents to be deleted
910     */
911    private Set<String> _getChildrenIdToDelete (OrgUnit orgUnit, boolean ignoreRights, Map<String, Object> results)
912    {
913        Set<String> toDelete = new HashSet<>();
914        
915        if (_canDeleteContent(orgUnit, ignoreRights, results))
916        {
917            toDelete.add(orgUnit.getId());
918            
919            for (String childId : orgUnit.getSubOrgUnits())
920            {
921                OrgUnit childOrgUnit = _resolver.resolveById(childId);
922                
923                if (!_isReferencedOrgUnit(orgUnit))
924                {
925                    toDelete.addAll(_getChildrenIdToDelete(childOrgUnit, ignoreRights, results));
926                }
927                else
928                {
929                    // The child program item can not be deleted, remove the relation to the parent and stop iteration
930                    @SuppressWarnings("unchecked")
931                    Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents");
932                    referencedContents.add(childOrgUnit);
933                }
934            }
935        }
936        
937        return toDelete;
938    }
939    
940    /**
941     * Get the id of children to be deleted. 
942     * All children shared with other contents which are not part of deletion, will be not deleted.
943     * @param contentToDelete The content to delete in the tree of initial content to delete
944     * @param initialContentToDelete The initial content to delete
945     * @param results The results
946     * @param mode The deletion mode
947     * @param ignoreRights If true, bypass the rights check during the deletion
948     * @return The id of contents to be deleted
949     */
950    private Set<String> _getChildrenIdToDelete (ProgramItem contentToDelete, ProgramItem initialContentToDelete, Map<String, Object> results, DeleteMode mode, boolean ignoreRights)
951    {
952        Set<String> toDelete = new HashSet<>();
953        
954        if (_canDeleteContent((Content) contentToDelete, ignoreRights, results))
955        {
956            toDelete.add(contentToDelete.getId());
957            
958            List<ProgramItem> childProgramItems;
959            if (mode == DeleteMode.STRUCTURE_ONLY && contentToDelete instanceof TraversableProgramPart)
960            {
961                // Get subprogram, container and course list children only
962                childProgramItems = ((TraversableProgramPart) contentToDelete).getProgramPartChildren().stream().map(c -> (ProgramItem) c).collect(Collectors.toList());
963            }
964            else 
965            {
966                childProgramItems = _odfHelper.getChildProgramItems(contentToDelete);
967            }
968            
969            for (ProgramItem childProgramItem : childProgramItems)
970            {
971                if (!_isContentReferenced((Content) childProgramItem, (Content) initialContentToDelete))
972                {
973                    // If all references of this program item is part of the initial content to delete, it can be deleted
974                    if (mode == DeleteMode.STRUCTURE_ONLY && childProgramItem instanceof CourseList)
975                    {
976                        // Remove the relations to the course list to be deleted on all child courses
977                        _removeRelations(((CourseList) childProgramItem).getCourses(), (Content) childProgramItem, Course.PARENT_COURSE_LISTS, 22, results);
978                        toDelete.add(childProgramItem.getId());
979                    }
980                    else
981                    {
982                        // Browse children recursively
983                        toDelete.addAll(_getChildrenIdToDelete(childProgramItem, initialContentToDelete, results, mode, ignoreRights));
984                    }
985                }
986                else
987                {
988                    // The child program item can not be deleted, remove the relation to the parent and stop iteration
989                    String parentMetadataName;
990                    if (childProgramItem instanceof CourseList)
991                    {
992                        parentMetadataName = contentToDelete instanceof Course ? CourseList.PARENT_COURSES : ProgramPart.PARENT_PROGRAM_PARTS;
993                    }
994                    else if (childProgramItem instanceof Course)
995                    {
996                        parentMetadataName = Course.PARENT_COURSE_LISTS;
997                    }
998                    else
999                    {
1000                        parentMetadataName = ProgramPart.PARENT_PROGRAM_PARTS;
1001                    }
1002                    
1003                    _removeRelation((WorkflowAwareContent) childProgramItem, (Content) contentToDelete, parentMetadataName, 22, results);
1004                    
1005                    @SuppressWarnings("unchecked")
1006                    Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents");
1007                    referencedContents.add((Content) childProgramItem);
1008                }
1009            }
1010        }
1011        
1012        return toDelete;
1013    }
1014
1015    /**
1016     * Determines if the user has sufficient right for the given content
1017     * @param content the content
1018     * @return true if user has sufficient right
1019     */
1020    public boolean hasRight(Content content)
1021    {
1022        String rightId = _getRightId(content);
1023        return _rightManager.hasRight(_currentUserProvider.getUser(), rightId, content) == RightResult.RIGHT_ALLOW;
1024    }
1025    
1026    private String _getRightId (Content content)
1027    {
1028        if (content instanceof Course)
1029        {
1030            return "ODF_Rights_Course_Delete";
1031        }
1032        else if (content instanceof SubProgram)
1033        {
1034            return "ODF_Rights_SubProgram_Delete";
1035        }
1036        else if (content instanceof Container)
1037        {
1038            return "ODF_Rights_Container_Delete";
1039        }
1040        else if (content instanceof Program)
1041        {
1042            return "ODF_Rights_Program_Delete";
1043        }
1044        else if (content instanceof Person)
1045        {
1046            return "ODF_Rights_Person_Delete";
1047        }
1048        else if (content instanceof OrgUnit)
1049        {
1050            return "ODF_Rights_OrgUnit_Delete";
1051        }
1052        else if (content instanceof CourseList)
1053        {
1054            return "ODF_Rights_CourseList_Delete";
1055        }
1056        return "Workflow_Rights_Delete";
1057    }
1058    
1059    /**
1060     * True if the content is referenced (we are ignoring parent references if they have same root)
1061     * @param programItem the program item
1062     * @param initialContentToDelete the initial content to delete
1063     * @return true if the content is referenced
1064     */
1065    private boolean _isReferencedContentCheckingRoot(ProgramItem programItem, Content initialContentToDelete)
1066    {
1067        if (programItem.getId().equals(initialContentToDelete.getId()))
1068        {
1069            return false;
1070        }
1071
1072        List<ProgramItem> parentProgramItems = _odfHelper.getParentProgramItems(programItem);
1073        if (parentProgramItems.isEmpty())
1074        {
1075            return true;
1076        }
1077        
1078        for (ProgramItem parentProgramItem : parentProgramItems)
1079        {
1080            if (_isReferencedContentCheckingRoot(parentProgramItem, initialContentToDelete))
1081            {
1082                return true;
1083            }
1084        }
1085        
1086        return false;
1087    }
1088}