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 if (content instanceof Person)
361        {
362            // 1 - Process to deletion
363            _finalizeDeleteContents(Collections.singleton(content.getId()), content.getParent(), results);
364        }
365        else
366        {
367            throw new IllegalArgumentException("The content [" + content.getId() + "] is not of the expected type, it can't be deleted.");
368        }
369    }
370
371    /**
372     * Test if content is still referenced before removing it
373     * @param content The content to remove
374     * @return true if content is still referenced
375     */
376    public boolean isContentReferenced(Content content)
377    {
378        return _isContentReferenced(content, null);
379    }
380    
381    /**
382     * Test if content is still referenced before removing it.
383     * @param content The content to remove
384     * @param rootContent the initial content to delete (can be null if checkRoot is false)
385     * @return true if content is still referenced
386     */
387    private boolean _isContentReferenced(Content content, Content rootContent)
388    {
389        if (content instanceof OrgUnit)
390        {
391            return _isReferencedOrgUnit((OrgUnit) content);
392        }
393        else if (content instanceof ProgramItem)
394        {
395            if (rootContent != null)
396            {
397                return _isReferencedContentCheckingRoot((ProgramItem) content, rootContent);
398            }
399            else
400            {
401                List<ProgramItem> ignoredRefContent = _odfHelper.getChildProgramItems((ProgramItem) content);
402                if (!(content instanceof Program)) // TODO remove content instanceof Course
403                {
404                    ignoredRefContent.addAll(_odfHelper.getParentProgramItems((ProgramItem) content));
405                }
406                
407                for (Pair<String, Content> refPair : _contentHelper.getReferencingContents(content))
408                {
409                    Content refContent = refPair.getValue();
410                    String path = refPair.getKey();
411                    
412                    // Ignoring reference from shareable field
413                    if (!(refContent instanceof Course && path.equals(ShareableCourseConstants.PROGRAMS_FIELD_ATTRIBUTE_NAME)))
414                    {
415                        // The ref content is not ignored
416                        if (refContent instanceof ProgramItem && !ignoredRefContent.contains((ProgramItem) refContent))
417                        {
418                            return true;
419                        }
420                    }
421                }
422
423                return false;
424            }
425        }
426        
427        return content.hasReferencingContents();
428    }
429    
430    /**
431     * True if the orgUnit is referenced
432     * @param orgUnit the orgUnit
433     * @return true if the orgUnit is referenced
434     */
435    private boolean _isReferencedOrgUnit(OrgUnit orgUnit)
436    {
437        OrgUnit parentOrgUnit = orgUnit.getParentOrgUnit();
438
439        List<String> relatedOrgUnit = orgUnit.getSubOrgUnits();
440        if (parentOrgUnit != null)
441        {
442            relatedOrgUnit.add(parentOrgUnit.getId());
443        }
444        
445        for (Pair<String, Content> refPair : _contentHelper.getReferencingContents(orgUnit))
446        {
447            Content refContent = refPair.getValue();
448            String path = refPair.getKey();
449            
450            // Ignoring reference from shareable field
451            if (!(refContent instanceof Course && path.equals(ShareableCourseConstants.ORGUNITS_FIELD_ATTRIBUTE_NAME)))
452            {
453                if (!relatedOrgUnit.contains(refContent.getId()))
454                {
455                    return true;
456                }
457            }
458        }
459        
460        return false;
461    }
462
463    /**
464     * Check that deletion can be performed without blocking errors
465     * @param content The initial content to delete
466     * @param mode The deletion mode
467     * @param ignoreRights If true, bypass the rights check during the deletion
468     * @param results The results
469     * @return true if the deletion can be performed
470     */
471    private boolean _checkBeforeDeletion(Content content, DeleteMode mode, boolean ignoreRights, Map<String, Object> results)
472    {
473        // Check right and lock on content it self
474        boolean allRight = _canDeleteContent(content, ignoreRights, results);
475        
476        // Check lock on parent contents
477        allRight = _checkParentsBeforeDeletion(content, results) && allRight;
478        
479        // Check right and lock on children to be deleted or modified
480        allRight = _checkChildrenBeforeDeletion(content, content, mode, ignoreRights, results) && allRight;
481        
482        return allRight;
483    }
484    
485    private boolean _checkParentsBeforeDeletion(Content content,  Map<String, Object> results)
486    {
487        boolean allRight = true;
488        
489        // Check if parents are not locked
490        List< ? extends WorkflowAwareContent> parents = _getParents(content);
491        for (WorkflowAwareContent parent : parents)
492        {
493            if (_smartHelper.isLocked(parent))
494            {
495                @SuppressWarnings("unchecked")
496                Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
497                lockedContents.add(content);
498                
499                allRight = false;
500            }
501        }
502        
503        return allRight;
504    }
505    
506    /**
507     * Browse children to check if deletion could succeed
508     * @param rootContentToDelete The initial content to delete
509     * @param contentToCheck The current content to check
510     * @param mode The deletion mode
511     * @param ignoreRights If true, bypass the rights check during the deletion
512     * @param results The result
513     * @return true if the deletion can be processed
514     */
515    private boolean _checkChildrenBeforeDeletion(Content rootContentToDelete, Content contentToCheck, DeleteMode mode, boolean ignoreRights, Map<String, Object> results)
516    {
517        boolean allRight = true;
518        
519        if (contentToCheck instanceof ProgramItem)
520        {
521            allRight = _checkChildrenBeforeDeletionOfProgramItem(rootContentToDelete, contentToCheck, mode, ignoreRights, results);
522        }
523        else if (contentToCheck instanceof OrgUnit)
524        {
525            allRight = _checkChildrenBeforeDeletionOfOrgUnit(rootContentToDelete, contentToCheck, mode, ignoreRights, results);
526        }
527        
528        return allRight;
529    }
530    
531    private boolean _checkChildrenBeforeDeletionOfProgramItem(Content rootContentToDelete, Content contentToCheck, DeleteMode mode,  boolean ignoreRights, Map<String, Object> results)
532    {
533        boolean allRight = true;
534
535        List<ProgramItem> childProgramItems = _odfHelper.getChildProgramItems((ProgramItem) contentToCheck);
536        for (ProgramItem childProgramItem : childProgramItems)
537        {
538            Content childContent = (Content) childProgramItem;
539            if (_smartHelper.isLocked(childContent))
540            {
541                // Lock should be checked for all children
542                @SuppressWarnings("unchecked")
543                Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
544                lockedContents.add(childContent);
545                
546                allRight = false;
547            }
548
549            // If content is not referenced it should be deleted, so check right
550            if ((mode == DeleteMode.FULL || (mode == DeleteMode.STRUCTURE_ONLY && !(childProgramItem instanceof Course))) && !_isContentReferenced(childContent, rootContentToDelete) && !ignoreRights && !hasRight(childContent))
551            {
552                // User has no sufficient right
553                @SuppressWarnings("unchecked")
554                Set<Content> norightContents = (Set<Content>) results.get("unauthorized-contents");
555                norightContents.add(childContent);
556                
557                allRight = false;
558            }
559            
560            if (mode != DeleteMode.SINGLE && !(mode == DeleteMode.STRUCTURE_ONLY && childProgramItem instanceof Course))
561            {
562                // Browse children recursively
563                allRight = _checkChildrenBeforeDeletion(rootContentToDelete, childContent, mode, ignoreRights, results) && allRight;
564            }
565        }
566        
567        return allRight;
568    }
569    
570    private boolean _checkChildrenBeforeDeletionOfOrgUnit(Content rootContentToDelete, Content contentToCheck, DeleteMode mode, boolean ignoreRights, Map<String, Object> results)
571    {
572        boolean allRight = true;
573
574        List<String> childOrgUnits = ((OrgUnit) contentToCheck).getSubOrgUnits();
575        for (String childOrgUnitId : childOrgUnits)
576        {
577            OrgUnit childOrgUnit = _resolver.resolveById(childOrgUnitId);
578            if (!_canDeleteContent(childOrgUnit, ignoreRights, results))
579            {
580                allRight = false;
581            }
582            else if (_isReferencedOrgUnit(childOrgUnit))
583            {
584                @SuppressWarnings("unchecked")
585                Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents");
586                referencedContents.add(childOrgUnit);
587                
588                allRight = false;
589            }
590            else
591            {
592                // Browse children recursively
593                allRight = _checkChildrenBeforeDeletion(rootContentToDelete, childOrgUnit, mode, ignoreRights, results) && allRight;
594            }
595        }
596        
597        return allRight;
598    }
599
600    /**
601     * Remove the relations between the content and its contents list
602     * @param contentsToEdit the contents to edit
603     * @param refContentToRemove The referenced content to be removed from contents
604     * @param metadataName The name of metadata holding the relationship
605     * @param actionId The id of workflow action to edit the relation
606     * @param results the results map
607     * @return true if remove relation successfully
608     */
609    private boolean _removeRelations(List<? extends WorkflowAwareContent> contentsToEdit, Content refContentToRemove, String metadataName, int actionId, Map<String, Object> results)
610    {
611        boolean success = true;
612        
613        for (Content contentToEdit : contentsToEdit)
614        {
615            success = _removeRelation((WorkflowAwareContent) contentToEdit, refContentToRemove, metadataName, actionId, results) && success;
616        }
617        
618        return success;
619    }
620    
621    /**
622     * Remove the relation parent-child relation on content. 
623     * @param contentToEdit The content to modified
624     * @param refContentToRemove The referenced content to be removed from content
625     * @param metadataName The name of metadata holding the child or parent relationship
626     * @param actionId The id of workflow action to edit the relation
627     * @param results the results map
628     * @return boolean true if remove relation successfully
629     */
630    private boolean _removeRelation(WorkflowAwareContent contentToEdit, Content refContentToRemove, String metadataName, int actionId, Map<String, Object> results)
631    {
632        try
633        {
634            String[] values = ContentDataHelper.getContentIdsArrayFromMultipleContentData(contentToEdit, metadataName);
635            
636            if (ArrayUtils.contains(values, refContentToRemove.getId()))
637            {
638                String[] newValues = ArrayUtils.removeElement(values, refContentToRemove.getId());
639                
640                List<Content> newContents = Arrays.asList(newValues).stream()
641                        .map(id -> (Content) _resolver.resolveById(id))
642                        .collect(Collectors.toList());
643                
644                // Set Jcr content reference if we pass contents in arguments
645                ExternalizableMetadataHelper.setMetadata(contentToEdit.getMetadataHolder(), metadataName, newContents.toArray(new Content[newContents.size()]));
646                _applyChanges(contentToEdit, actionId);
647            }
648            
649            return true;
650        }
651        catch (WorkflowException | AmetysRepositoryException e)
652        {
653            getLogger().error("Unable to remove relationship to content {} ({}) on content {} ({}) for metadata {}", refContentToRemove.getTitle(), refContentToRemove.getId(), contentToEdit.getTitle(), contentToEdit.getId(), metadataName, e);
654            return false;
655        }
656    }
657    
658    /**
659     * Delete a program item
660     * @param item The program item to delete
661     * @param mode The deletion mode
662     * @param ignoreRights If true, bypass the rights check during the deletion
663     * @param results The results
664     */
665    private void _deleteProgramItem(ProgramItem item, DeleteMode mode, boolean ignoreRights, Map<String, Object> results)
666    {
667        if (mode == DeleteMode.SINGLE)
668        {
669            if (_canDeleteContent((Content) item, ignoreRights, results))
670            {
671                // 1 - First remove relations with children
672                String parentMetadataName;
673                if (item instanceof Course)
674                {
675                    parentMetadataName = CourseList.PARENT_COURSES;
676                }
677                else if (item instanceof CourseList)
678                {
679                    parentMetadataName = Course.PARENT_COURSE_LISTS;
680                }
681                else
682                {
683                    parentMetadataName = ProgramPart.PARENT_PROGRAM_PARTS;
684                }
685                
686                List<ProgramItem> childProgramItems = _odfHelper.getChildProgramItems(item);
687                List<WorkflowAwareContent> childContents = childProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList());
688                
689                boolean success = _removeRelations(childContents, (Content) item, parentMetadataName, 22, results);
690                if (success)
691                {
692                    // If success delete course
693                    _finalizeDeleteContents(Collections.singleton(item.getId()), item.getParent(), results);
694                }
695            }
696        }
697        else
698        {
699            Set<String> toDelete = _getChildrenIdToDelete(item, item, results, mode, ignoreRights);
700            _finalizeDeleteContents(toDelete, item.getParent(), results);
701        }
702    }
703    
704   
705    /**
706     * Delete one orgUnit
707     * @param orgUnit the orgUnit to delete
708     * @param ignoreRights If true, bypass the rights check during the deletion
709     * @param results the results map
710     */
711    @SuppressWarnings("unchecked")
712    private void _deleteOrgUnit(OrgUnit orgUnit, boolean ignoreRights, Map<String, Object> results)
713    {
714        Set<String> toDelete = _getChildrenIdToDelete(orgUnit, ignoreRights, results);
715        
716        Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents");
717        Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
718        Set<Content> unauthorizedContents = (Set<Content>) results.get("unauthorized-contents");
719        
720        if (referencedContents.size() == 0 && lockedContents.size() == 0 && unauthorizedContents.size() == 0)
721        {
722            _finalizeDeleteContents(toDelete, orgUnit.getParent(), results);
723        }
724    }
725
726    /**
727     * Finalize the deletion of contents. Call observers and remove contents
728     * @param contentIdsToDelete the list of content id to delete
729     * @param parent the jcr parent for saving changes
730     * @param results the results map
731     */
732    private void _finalizeDeleteContents(Set<String> contentIdsToDelete, ModifiableAmetysObject parent, Map<String, Object> results)
733    {
734        @SuppressWarnings("unchecked")
735        Set<Content> unauthorizedContents = (Set<Content>) results.get("unauthorized-contents");
736        @SuppressWarnings("unchecked")
737        Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
738        
739        if (!unauthorizedContents.isEmpty() || !lockedContents.isEmpty())
740        {
741            //Do Nothing
742            return;
743        }
744        
745        try
746        {
747            _observationManager.addArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT, false);
748            
749            Map<String, Map<String, Object>> eventParams = new HashMap<>();
750            for (String id : contentIdsToDelete)
751            {
752                Content content = _resolver.resolveById(id);
753                Map<String, Object> eventParam = _getEventParametersForDeletion(content);
754                
755                eventParams.put(id, eventParam);
756                
757                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParam));
758                
759                // Remove the content.
760                LockableAmetysObject lockedContent = (LockableAmetysObject) content;
761                if (lockedContent.isLocked())
762                {
763                    lockedContent.unlock();
764                }
765                
766                ((RemovableAmetysObject) content).remove();
767            }
768            
769            
770            parent.saveChanges();
771            
772            for (String id : contentIdsToDelete)
773            {
774                _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams.get(id)));
775                
776                @SuppressWarnings("unchecked")
777                Set<String> deletedContents = (Set<String>) results.get("deleted-contents");
778                deletedContents.add(id);
779            }
780        }
781        finally 
782        {
783            _observationManager.removeArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT);
784            _commitAllChanges();
785        }
786    }
787    
788    /**
789     * Commit all changes in solr
790     */
791    private void _commitAllChanges()
792    {
793        // Before trying to commit, be sure all the async observers of the current request are finished
794        for (Future future : _observationManager.getFuturesForRequest())
795        {
796            try
797            {
798                future.get();
799            }
800            catch (ExecutionException | InterruptedException e)
801            {
802                getLogger().info("An exception occured when calling #get() on Future result of an observer." , e);
803            }
804        }
805        
806        // Commit all uncommited changes
807        try
808        {
809            _solrIndexer.commit();
810            
811            getLogger().debug("Deleted contents are now committed into Solr.");
812        }
813        catch (IOException | SolrServerException e)
814        {
815            getLogger().error("Impossible to commit changes", e);
816        }
817    }
818    
819    /**
820     * True if we can delete the content (check if removable, rights and if locked)
821     * @param content the content
822     * @param ignoreRights If true, bypass the rights check during the deletion
823     * @param results the results map
824     * @return true if we can delete the content
825     */
826    private boolean _canDeleteContent(Content content, boolean ignoreRights, Map<String, Object> results)
827    {
828        if (!(content instanceof RemovableAmetysObject))
829        {
830            throw new IllegalArgumentException("The content [" + content.getId() + "] is not a RemovableAmetysObject, it can't be deleted.");
831        }
832        
833        if (!ignoreRights && !hasRight(content))
834        {
835            // User has no sufficient right
836            @SuppressWarnings("unchecked")
837            Set<Content> norightContents = (Set<Content>) results.get("unauthorized-contents");
838            norightContents.add(content);
839            
840            return false;
841        }
842        else if (_smartHelper.isLocked(content))
843        {
844            @SuppressWarnings("unchecked")
845            Set<Content> lockedContents = (Set<Content>) results.get("locked-contents");
846            lockedContents.add(content);
847            
848            return false;
849        }
850        
851        return true;
852    }
853    
854    private void _applyChanges(WorkflowAwareContent content, int actionId) throws WorkflowException
855    {
856        // Notify listeners
857        Map<String, Object> eventParams = new HashMap<>();
858        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
859        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
860        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams));
861       
862        _contentWorkflowHelper.doAction(content, actionId);
863    }
864    
865    /**
866     * Get parameters for content deleted {@link Event}
867     * @param content the removed content
868     * @return the event's parameters
869     */
870    private Map<String, Object> _getEventParametersForDeletion (Content content)
871    {
872        Map<String, Object> eventParams = new HashMap<>();
873        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
874        eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName());
875        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
876        return eventParams;
877    }
878    
879    private List<? extends WorkflowAwareContent> _getParents(Content content)
880    {
881        if (content instanceof OrgUnit)
882        {
883            return Collections.singletonList(((OrgUnit) content).getParentOrgUnit());
884        }
885        else if (content instanceof Course)
886        {
887            return ((Course) content).getParentCourseLists();
888        }
889        else if (content instanceof CourseList)
890        {
891            List<ProgramPart> parentProgramItems = ((CourseList) content).getProgramPartParents();
892            List<WorkflowAwareContent> parents = parentProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList());
893            
894            List<Course> parentCourses = ((CourseList) content).getParentCourses();
895            parents.addAll(parentCourses.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList()));
896            
897            return parents;
898        }
899        else if (content instanceof ProgramPart)
900        {
901            List<ProgramItem> parentProgramItems = _odfHelper.getParentProgramItems((ProgramPart) content);
902            return parentProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList());
903        }
904        
905        return Collections.EMPTY_LIST;
906    }
907    
908    /**
909     * Get the id of children to be deleted. 
910     * All children shared with other contents which are not part of deletion, will be not deleted.
911     * @param orgUnit The orgunit to delete
912     * @param ignoreRights If true, bypass the rights check during the deletion
913     * @param results The results
914     * @return The id of contents to be deleted
915     */
916    private Set<String> _getChildrenIdToDelete (OrgUnit orgUnit, boolean ignoreRights, Map<String, Object> results)
917    {
918        Set<String> toDelete = new HashSet<>();
919        
920        if (_canDeleteContent(orgUnit, ignoreRights, results))
921        {
922            toDelete.add(orgUnit.getId());
923            
924            for (String childId : orgUnit.getSubOrgUnits())
925            {
926                OrgUnit childOrgUnit = _resolver.resolveById(childId);
927                
928                if (!_isReferencedOrgUnit(orgUnit))
929                {
930                    toDelete.addAll(_getChildrenIdToDelete(childOrgUnit, ignoreRights, results));
931                }
932                else
933                {
934                    // The child program item can not be deleted, remove the relation to the parent and stop iteration
935                    @SuppressWarnings("unchecked")
936                    Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents");
937                    referencedContents.add(childOrgUnit);
938                }
939            }
940        }
941        
942        return toDelete;
943    }
944    
945    /**
946     * Get the id of children to be deleted. 
947     * All children shared with other contents which are not part of deletion, will be not deleted.
948     * @param contentToDelete The content to delete in the tree of initial content to delete
949     * @param initialContentToDelete The initial content to delete
950     * @param results The results
951     * @param mode The deletion mode
952     * @param ignoreRights If true, bypass the rights check during the deletion
953     * @return The id of contents to be deleted
954     */
955    private Set<String> _getChildrenIdToDelete (ProgramItem contentToDelete, ProgramItem initialContentToDelete, Map<String, Object> results, DeleteMode mode, boolean ignoreRights)
956    {
957        Set<String> toDelete = new HashSet<>();
958        
959        if (_canDeleteContent((Content) contentToDelete, ignoreRights, results))
960        {
961            toDelete.add(contentToDelete.getId());
962            
963            List<ProgramItem> childProgramItems;
964            if (mode == DeleteMode.STRUCTURE_ONLY && contentToDelete instanceof TraversableProgramPart)
965            {
966                // Get subprogram, container and course list children only
967                childProgramItems = ((TraversableProgramPart) contentToDelete).getProgramPartChildren().stream().map(c -> (ProgramItem) c).collect(Collectors.toList());
968            }
969            else 
970            {
971                childProgramItems = _odfHelper.getChildProgramItems(contentToDelete);
972            }
973            
974            for (ProgramItem childProgramItem : childProgramItems)
975            {
976                if (!_isContentReferenced((Content) childProgramItem, (Content) initialContentToDelete))
977                {
978                    // If all references of this program item is part of the initial content to delete, it can be deleted
979                    if (mode == DeleteMode.STRUCTURE_ONLY && childProgramItem instanceof CourseList)
980                    {
981                        // Remove the relations to the course list to be deleted on all child courses
982                        _removeRelations(((CourseList) childProgramItem).getCourses(), (Content) childProgramItem, Course.PARENT_COURSE_LISTS, 22, results);
983                        toDelete.add(childProgramItem.getId());
984                    }
985                    else
986                    {
987                        // Browse children recursively
988                        toDelete.addAll(_getChildrenIdToDelete(childProgramItem, initialContentToDelete, results, mode, ignoreRights));
989                    }
990                }
991                else
992                {
993                    // The child program item can not be deleted, remove the relation to the parent and stop iteration
994                    String parentMetadataName;
995                    if (childProgramItem instanceof CourseList)
996                    {
997                        parentMetadataName = contentToDelete instanceof Course ? CourseList.PARENT_COURSES : ProgramPart.PARENT_PROGRAM_PARTS;
998                    }
999                    else if (childProgramItem instanceof Course)
1000                    {
1001                        parentMetadataName = Course.PARENT_COURSE_LISTS;
1002                    }
1003                    else
1004                    {
1005                        parentMetadataName = ProgramPart.PARENT_PROGRAM_PARTS;
1006                    }
1007                    
1008                    _removeRelation((WorkflowAwareContent) childProgramItem, (Content) contentToDelete, parentMetadataName, 22, results);
1009                    
1010                    @SuppressWarnings("unchecked")
1011                    Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents");
1012                    referencedContents.add((Content) childProgramItem);
1013                }
1014            }
1015        }
1016        
1017        return toDelete;
1018    }
1019
1020    /**
1021     * Determines if the user has sufficient right for the given content
1022     * @param content the content
1023     * @return true if user has sufficient right
1024     */
1025    public boolean hasRight(Content content)
1026    {
1027        String rightId = _getRightId(content);
1028        return _rightManager.hasRight(_currentUserProvider.getUser(), rightId, content) == RightResult.RIGHT_ALLOW;
1029    }
1030    
1031    private String _getRightId (Content content)
1032    {
1033        if (content instanceof Course)
1034        {
1035            return "ODF_Rights_Course_Delete";
1036        }
1037        else if (content instanceof SubProgram)
1038        {
1039            return "ODF_Rights_SubProgram_Delete";
1040        }
1041        else if (content instanceof Container)
1042        {
1043            return "ODF_Rights_Container_Delete";
1044        }
1045        else if (content instanceof Program)
1046        {
1047            return "ODF_Rights_Program_Delete";
1048        }
1049        else if (content instanceof Person)
1050        {
1051            return "ODF_Rights_Person_Delete";
1052        }
1053        else if (content instanceof OrgUnit)
1054        {
1055            return "ODF_Rights_OrgUnit_Delete";
1056        }
1057        else if (content instanceof CourseList)
1058        {
1059            return "ODF_Rights_CourseList_Delete";
1060        }
1061        return "Workflow_Rights_Delete";
1062    }
1063    
1064    /**
1065     * True if the content is referenced (we are ignoring parent references if they have same root)
1066     * @param programItem the program item
1067     * @param initialContentToDelete the initial content to delete
1068     * @return true if the content is referenced
1069     */
1070    private boolean _isReferencedContentCheckingRoot(ProgramItem programItem, Content initialContentToDelete)
1071    {
1072        if (programItem.getId().equals(initialContentToDelete.getId()))
1073        {
1074            return false;
1075        }
1076
1077        List<ProgramItem> parentProgramItems = _odfHelper.getParentProgramItems(programItem);
1078        if (parentProgramItems.isEmpty())
1079        {
1080            return true;
1081        }
1082        
1083        for (ProgramItem parentProgramItem : parentProgramItems)
1084        {
1085            if (_isReferencedContentCheckingRoot(parentProgramItem, initialContentToDelete))
1086            {
1087                return true;
1088            }
1089        }
1090        
1091        return false;
1092    }
1093}