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 "Workflow_Rights_Delete"; 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}