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