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