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