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