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