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 if (content instanceof Person) 361 { 362 // 1 - Process to deletion 363 _finalizeDeleteContents(Collections.singleton(content.getId()), content.getParent(), results); 364 } 365 else 366 { 367 throw new IllegalArgumentException("The content [" + content.getId() + "] is not of the expected type, it can't be deleted."); 368 } 369 } 370 371 /** 372 * Test if content is still referenced before removing it 373 * @param content The content to remove 374 * @return true if content is still referenced 375 */ 376 public boolean isContentReferenced(Content content) 377 { 378 return _isContentReferenced(content, null); 379 } 380 381 /** 382 * Test if content is still referenced before removing it. 383 * @param content The content to remove 384 * @param rootContent the initial content to delete (can be null if checkRoot is false) 385 * @return true if content is still referenced 386 */ 387 private boolean _isContentReferenced(Content content, Content rootContent) 388 { 389 if (content instanceof OrgUnit) 390 { 391 return _isReferencedOrgUnit((OrgUnit) content); 392 } 393 else if (content instanceof ProgramItem) 394 { 395 if (rootContent != null) 396 { 397 return _isReferencedContentCheckingRoot((ProgramItem) content, rootContent); 398 } 399 else 400 { 401 List<ProgramItem> ignoredRefContent = _odfHelper.getChildProgramItems((ProgramItem) content); 402 if (!(content instanceof Program)) // TODO remove content instanceof Course 403 { 404 ignoredRefContent.addAll(_odfHelper.getParentProgramItems((ProgramItem) content)); 405 } 406 407 for (Pair<String, Content> refPair : _contentHelper.getReferencingContents(content)) 408 { 409 Content refContent = refPair.getValue(); 410 String path = refPair.getKey(); 411 412 // Ignoring reference from shareable field 413 if (!(refContent instanceof Course && path.equals(ShareableCourseConstants.PROGRAMS_FIELD_ATTRIBUTE_NAME))) 414 { 415 // The ref content is not ignored 416 if (refContent instanceof ProgramItem && !ignoredRefContent.contains((ProgramItem) refContent)) 417 { 418 return true; 419 } 420 } 421 } 422 423 return false; 424 } 425 } 426 427 return content.hasReferencingContents(); 428 } 429 430 /** 431 * True if the orgUnit is referenced 432 * @param orgUnit the orgUnit 433 * @return true if the orgUnit is referenced 434 */ 435 private boolean _isReferencedOrgUnit(OrgUnit orgUnit) 436 { 437 OrgUnit parentOrgUnit = orgUnit.getParentOrgUnit(); 438 439 List<String> relatedOrgUnit = orgUnit.getSubOrgUnits(); 440 if (parentOrgUnit != null) 441 { 442 relatedOrgUnit.add(parentOrgUnit.getId()); 443 } 444 445 for (Pair<String, Content> refPair : _contentHelper.getReferencingContents(orgUnit)) 446 { 447 Content refContent = refPair.getValue(); 448 String path = refPair.getKey(); 449 450 // Ignoring reference from shareable field 451 if (!(refContent instanceof Course && path.equals(ShareableCourseConstants.ORGUNITS_FIELD_ATTRIBUTE_NAME))) 452 { 453 if (!relatedOrgUnit.contains(refContent.getId())) 454 { 455 return true; 456 } 457 } 458 } 459 460 return false; 461 } 462 463 /** 464 * Check that deletion can be performed without blocking errors 465 * @param content The initial content to delete 466 * @param mode The deletion mode 467 * @param ignoreRights If true, bypass the rights check during the deletion 468 * @param results The results 469 * @return true if the deletion can be performed 470 */ 471 private boolean _checkBeforeDeletion(Content content, DeleteMode mode, boolean ignoreRights, Map<String, Object> results) 472 { 473 // Check right and lock on content it self 474 boolean allRight = _canDeleteContent(content, ignoreRights, results); 475 476 // Check lock on parent contents 477 allRight = _checkParentsBeforeDeletion(content, results) && allRight; 478 479 // Check right and lock on children to be deleted or modified 480 allRight = _checkChildrenBeforeDeletion(content, content, mode, ignoreRights, results) && allRight; 481 482 return allRight; 483 } 484 485 private boolean _checkParentsBeforeDeletion(Content content, Map<String, Object> results) 486 { 487 boolean allRight = true; 488 489 // Check if parents are not locked 490 List< ? extends WorkflowAwareContent> parents = _getParents(content); 491 for (WorkflowAwareContent parent : parents) 492 { 493 if (_smartHelper.isLocked(parent)) 494 { 495 @SuppressWarnings("unchecked") 496 Set<Content> lockedContents = (Set<Content>) results.get("locked-contents"); 497 lockedContents.add(content); 498 499 allRight = false; 500 } 501 } 502 503 return allRight; 504 } 505 506 /** 507 * Browse children to check if deletion could succeed 508 * @param rootContentToDelete The initial content to delete 509 * @param contentToCheck The current content to check 510 * @param mode The deletion mode 511 * @param ignoreRights If true, bypass the rights check during the deletion 512 * @param results The result 513 * @return true if the deletion can be processed 514 */ 515 private boolean _checkChildrenBeforeDeletion(Content rootContentToDelete, Content contentToCheck, DeleteMode mode, boolean ignoreRights, Map<String, Object> results) 516 { 517 boolean allRight = true; 518 519 if (contentToCheck instanceof ProgramItem) 520 { 521 allRight = _checkChildrenBeforeDeletionOfProgramItem(rootContentToDelete, contentToCheck, mode, ignoreRights, results); 522 } 523 else if (contentToCheck instanceof OrgUnit) 524 { 525 allRight = _checkChildrenBeforeDeletionOfOrgUnit(rootContentToDelete, contentToCheck, mode, ignoreRights, results); 526 } 527 528 return allRight; 529 } 530 531 private boolean _checkChildrenBeforeDeletionOfProgramItem(Content rootContentToDelete, Content contentToCheck, DeleteMode mode, boolean ignoreRights, Map<String, Object> results) 532 { 533 boolean allRight = true; 534 535 List<ProgramItem> childProgramItems = _odfHelper.getChildProgramItems((ProgramItem) contentToCheck); 536 for (ProgramItem childProgramItem : childProgramItems) 537 { 538 Content childContent = (Content) childProgramItem; 539 if (_smartHelper.isLocked(childContent)) 540 { 541 // Lock should be checked for all children 542 @SuppressWarnings("unchecked") 543 Set<Content> lockedContents = (Set<Content>) results.get("locked-contents"); 544 lockedContents.add(childContent); 545 546 allRight = false; 547 } 548 549 // If content is not referenced it should be deleted, so check right 550 if ((mode == DeleteMode.FULL || (mode == DeleteMode.STRUCTURE_ONLY && !(childProgramItem instanceof Course))) && !_isContentReferenced(childContent, rootContentToDelete) && !ignoreRights && !hasRight(childContent)) 551 { 552 // User has no sufficient right 553 @SuppressWarnings("unchecked") 554 Set<Content> norightContents = (Set<Content>) results.get("unauthorized-contents"); 555 norightContents.add(childContent); 556 557 allRight = false; 558 } 559 560 if (mode != DeleteMode.SINGLE && !(mode == DeleteMode.STRUCTURE_ONLY && childProgramItem instanceof Course)) 561 { 562 // Browse children recursively 563 allRight = _checkChildrenBeforeDeletion(rootContentToDelete, childContent, mode, ignoreRights, results) && allRight; 564 } 565 } 566 567 return allRight; 568 } 569 570 private boolean _checkChildrenBeforeDeletionOfOrgUnit(Content rootContentToDelete, Content contentToCheck, DeleteMode mode, boolean ignoreRights, Map<String, Object> results) 571 { 572 boolean allRight = true; 573 574 List<String> childOrgUnits = ((OrgUnit) contentToCheck).getSubOrgUnits(); 575 for (String childOrgUnitId : childOrgUnits) 576 { 577 OrgUnit childOrgUnit = _resolver.resolveById(childOrgUnitId); 578 if (!_canDeleteContent(childOrgUnit, ignoreRights, results)) 579 { 580 allRight = false; 581 } 582 else if (_isReferencedOrgUnit(childOrgUnit)) 583 { 584 @SuppressWarnings("unchecked") 585 Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents"); 586 referencedContents.add(childOrgUnit); 587 588 allRight = false; 589 } 590 else 591 { 592 // Browse children recursively 593 allRight = _checkChildrenBeforeDeletion(rootContentToDelete, childOrgUnit, mode, ignoreRights, results) && allRight; 594 } 595 } 596 597 return allRight; 598 } 599 600 /** 601 * Remove the relations between the content and its contents list 602 * @param contentsToEdit the contents to edit 603 * @param refContentToRemove The referenced content to be removed from contents 604 * @param metadataName The name of metadata holding the relationship 605 * @param actionId The id of workflow action to edit the relation 606 * @param results the results map 607 * @return true if remove relation successfully 608 */ 609 private boolean _removeRelations(List<? extends WorkflowAwareContent> contentsToEdit, Content refContentToRemove, String metadataName, int actionId, Map<String, Object> results) 610 { 611 boolean success = true; 612 613 for (Content contentToEdit : contentsToEdit) 614 { 615 success = _removeRelation((WorkflowAwareContent) contentToEdit, refContentToRemove, metadataName, actionId, results) && success; 616 } 617 618 return success; 619 } 620 621 /** 622 * Remove the relation parent-child relation on content. 623 * @param contentToEdit The content to modified 624 * @param refContentToRemove The referenced content to be removed from content 625 * @param metadataName The name of metadata holding the child or parent relationship 626 * @param actionId The id of workflow action to edit the relation 627 * @param results the results map 628 * @return boolean true if remove relation successfully 629 */ 630 private boolean _removeRelation(WorkflowAwareContent contentToEdit, Content refContentToRemove, String metadataName, int actionId, Map<String, Object> results) 631 { 632 try 633 { 634 String[] values = ContentDataHelper.getContentIdsArrayFromMultipleContentData(contentToEdit, metadataName); 635 636 if (ArrayUtils.contains(values, refContentToRemove.getId())) 637 { 638 String[] newValues = ArrayUtils.removeElement(values, refContentToRemove.getId()); 639 640 List<Content> newContents = Arrays.asList(newValues).stream() 641 .map(id -> (Content) _resolver.resolveById(id)) 642 .collect(Collectors.toList()); 643 644 // Set Jcr content reference if we pass contents in arguments 645 ExternalizableMetadataHelper.setMetadata(contentToEdit.getMetadataHolder(), metadataName, newContents.toArray(new Content[newContents.size()])); 646 _applyChanges(contentToEdit, actionId); 647 } 648 649 return true; 650 } 651 catch (WorkflowException | AmetysRepositoryException e) 652 { 653 getLogger().error("Unable to remove relationship to content {} ({}) on content {} ({}) for metadata {}", refContentToRemove.getTitle(), refContentToRemove.getId(), contentToEdit.getTitle(), contentToEdit.getId(), metadataName, e); 654 return false; 655 } 656 } 657 658 /** 659 * Delete a program item 660 * @param item The program item to delete 661 * @param mode The deletion mode 662 * @param ignoreRights If true, bypass the rights check during the deletion 663 * @param results The results 664 */ 665 private void _deleteProgramItem(ProgramItem item, DeleteMode mode, boolean ignoreRights, Map<String, Object> results) 666 { 667 if (mode == DeleteMode.SINGLE) 668 { 669 if (_canDeleteContent((Content) item, ignoreRights, results)) 670 { 671 // 1 - First remove relations with children 672 String parentMetadataName; 673 if (item instanceof Course) 674 { 675 parentMetadataName = CourseList.PARENT_COURSES; 676 } 677 else if (item instanceof CourseList) 678 { 679 parentMetadataName = Course.PARENT_COURSE_LISTS; 680 } 681 else 682 { 683 parentMetadataName = ProgramPart.PARENT_PROGRAM_PARTS; 684 } 685 686 List<ProgramItem> childProgramItems = _odfHelper.getChildProgramItems(item); 687 List<WorkflowAwareContent> childContents = childProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList()); 688 689 boolean success = _removeRelations(childContents, (Content) item, parentMetadataName, 22, results); 690 if (success) 691 { 692 // If success delete course 693 _finalizeDeleteContents(Collections.singleton(item.getId()), item.getParent(), results); 694 } 695 } 696 } 697 else 698 { 699 Set<String> toDelete = _getChildrenIdToDelete(item, item, results, mode, ignoreRights); 700 _finalizeDeleteContents(toDelete, item.getParent(), results); 701 } 702 } 703 704 705 /** 706 * Delete one orgUnit 707 * @param orgUnit the orgUnit to delete 708 * @param ignoreRights If true, bypass the rights check during the deletion 709 * @param results the results map 710 */ 711 @SuppressWarnings("unchecked") 712 private void _deleteOrgUnit(OrgUnit orgUnit, boolean ignoreRights, Map<String, Object> results) 713 { 714 Set<String> toDelete = _getChildrenIdToDelete(orgUnit, ignoreRights, results); 715 716 Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents"); 717 Set<Content> lockedContents = (Set<Content>) results.get("locked-contents"); 718 Set<Content> unauthorizedContents = (Set<Content>) results.get("unauthorized-contents"); 719 720 if (referencedContents.size() == 0 && lockedContents.size() == 0 && unauthorizedContents.size() == 0) 721 { 722 _finalizeDeleteContents(toDelete, orgUnit.getParent(), results); 723 } 724 } 725 726 /** 727 * Finalize the deletion of contents. Call observers and remove contents 728 * @param contentIdsToDelete the list of content id to delete 729 * @param parent the jcr parent for saving changes 730 * @param results the results map 731 */ 732 private void _finalizeDeleteContents(Set<String> contentIdsToDelete, ModifiableAmetysObject parent, Map<String, Object> results) 733 { 734 @SuppressWarnings("unchecked") 735 Set<Content> unauthorizedContents = (Set<Content>) results.get("unauthorized-contents"); 736 @SuppressWarnings("unchecked") 737 Set<Content> lockedContents = (Set<Content>) results.get("locked-contents"); 738 739 if (!unauthorizedContents.isEmpty() || !lockedContents.isEmpty()) 740 { 741 //Do Nothing 742 return; 743 } 744 745 try 746 { 747 _observationManager.addArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT, false); 748 749 Map<String, Map<String, Object>> eventParams = new HashMap<>(); 750 for (String id : contentIdsToDelete) 751 { 752 Content content = _resolver.resolveById(id); 753 Map<String, Object> eventParam = _getEventParametersForDeletion(content); 754 755 eventParams.put(id, eventParam); 756 757 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParam)); 758 759 // Remove the content. 760 LockableAmetysObject lockedContent = (LockableAmetysObject) content; 761 if (lockedContent.isLocked()) 762 { 763 lockedContent.unlock(); 764 } 765 766 ((RemovableAmetysObject) content).remove(); 767 } 768 769 770 parent.saveChanges(); 771 772 for (String id : contentIdsToDelete) 773 { 774 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams.get(id))); 775 776 @SuppressWarnings("unchecked") 777 Set<String> deletedContents = (Set<String>) results.get("deleted-contents"); 778 deletedContents.add(id); 779 } 780 } 781 finally 782 { 783 _observationManager.removeArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT); 784 _commitAllChanges(); 785 } 786 } 787 788 /** 789 * Commit all changes in solr 790 */ 791 private void _commitAllChanges() 792 { 793 // Before trying to commit, be sure all the async observers of the current request are finished 794 for (Future future : _observationManager.getFuturesForRequest()) 795 { 796 try 797 { 798 future.get(); 799 } 800 catch (ExecutionException | InterruptedException e) 801 { 802 getLogger().info("An exception occured when calling #get() on Future result of an observer." , e); 803 } 804 } 805 806 // Commit all uncommited changes 807 try 808 { 809 _solrIndexer.commit(); 810 811 getLogger().debug("Deleted contents are now committed into Solr."); 812 } 813 catch (IOException | SolrServerException e) 814 { 815 getLogger().error("Impossible to commit changes", e); 816 } 817 } 818 819 /** 820 * True if we can delete the content (check if removable, rights and if locked) 821 * @param content the content 822 * @param ignoreRights If true, bypass the rights check during the deletion 823 * @param results the results map 824 * @return true if we can delete the content 825 */ 826 private boolean _canDeleteContent(Content content, boolean ignoreRights, Map<String, Object> results) 827 { 828 if (!(content instanceof RemovableAmetysObject)) 829 { 830 throw new IllegalArgumentException("The content [" + content.getId() + "] is not a RemovableAmetysObject, it can't be deleted."); 831 } 832 833 if (!ignoreRights && !hasRight(content)) 834 { 835 // User has no sufficient right 836 @SuppressWarnings("unchecked") 837 Set<Content> norightContents = (Set<Content>) results.get("unauthorized-contents"); 838 norightContents.add(content); 839 840 return false; 841 } 842 else if (_smartHelper.isLocked(content)) 843 { 844 @SuppressWarnings("unchecked") 845 Set<Content> lockedContents = (Set<Content>) results.get("locked-contents"); 846 lockedContents.add(content); 847 848 return false; 849 } 850 851 return true; 852 } 853 854 private void _applyChanges(WorkflowAwareContent content, int actionId) throws WorkflowException 855 { 856 // Notify listeners 857 Map<String, Object> eventParams = new HashMap<>(); 858 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 859 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 860 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams)); 861 862 _contentWorkflowHelper.doAction(content, actionId); 863 } 864 865 /** 866 * Get parameters for content deleted {@link Event} 867 * @param content the removed content 868 * @return the event's parameters 869 */ 870 private Map<String, Object> _getEventParametersForDeletion (Content content) 871 { 872 Map<String, Object> eventParams = new HashMap<>(); 873 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 874 eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName()); 875 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 876 return eventParams; 877 } 878 879 private List<? extends WorkflowAwareContent> _getParents(Content content) 880 { 881 if (content instanceof OrgUnit) 882 { 883 return Collections.singletonList(((OrgUnit) content).getParentOrgUnit()); 884 } 885 else if (content instanceof Course) 886 { 887 return ((Course) content).getParentCourseLists(); 888 } 889 else if (content instanceof CourseList) 890 { 891 List<ProgramPart> parentProgramItems = ((CourseList) content).getProgramPartParents(); 892 List<WorkflowAwareContent> parents = parentProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList()); 893 894 List<Course> parentCourses = ((CourseList) content).getParentCourses(); 895 parents.addAll(parentCourses.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList())); 896 897 return parents; 898 } 899 else if (content instanceof ProgramPart) 900 { 901 List<ProgramItem> parentProgramItems = _odfHelper.getParentProgramItems((ProgramPart) content); 902 return parentProgramItems.stream().map(p -> (WorkflowAwareContent) p).collect(Collectors.toList()); 903 } 904 905 return Collections.EMPTY_LIST; 906 } 907 908 /** 909 * Get the id of children to be deleted. 910 * All children shared with other contents which are not part of deletion, will be not deleted. 911 * @param orgUnit The orgunit to delete 912 * @param ignoreRights If true, bypass the rights check during the deletion 913 * @param results The results 914 * @return The id of contents to be deleted 915 */ 916 private Set<String> _getChildrenIdToDelete (OrgUnit orgUnit, boolean ignoreRights, Map<String, Object> results) 917 { 918 Set<String> toDelete = new HashSet<>(); 919 920 if (_canDeleteContent(orgUnit, ignoreRights, results)) 921 { 922 toDelete.add(orgUnit.getId()); 923 924 for (String childId : orgUnit.getSubOrgUnits()) 925 { 926 OrgUnit childOrgUnit = _resolver.resolveById(childId); 927 928 if (!_isReferencedOrgUnit(orgUnit)) 929 { 930 toDelete.addAll(_getChildrenIdToDelete(childOrgUnit, ignoreRights, results)); 931 } 932 else 933 { 934 // The child program item can not be deleted, remove the relation to the parent and stop iteration 935 @SuppressWarnings("unchecked") 936 Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents"); 937 referencedContents.add(childOrgUnit); 938 } 939 } 940 } 941 942 return toDelete; 943 } 944 945 /** 946 * Get the id of children to be deleted. 947 * All children shared with other contents which are not part of deletion, will be not deleted. 948 * @param contentToDelete The content to delete in the tree of initial content to delete 949 * @param initialContentToDelete The initial content to delete 950 * @param results The results 951 * @param mode The deletion mode 952 * @param ignoreRights If true, bypass the rights check during the deletion 953 * @return The id of contents to be deleted 954 */ 955 private Set<String> _getChildrenIdToDelete (ProgramItem contentToDelete, ProgramItem initialContentToDelete, Map<String, Object> results, DeleteMode mode, boolean ignoreRights) 956 { 957 Set<String> toDelete = new HashSet<>(); 958 959 if (_canDeleteContent((Content) contentToDelete, ignoreRights, results)) 960 { 961 toDelete.add(contentToDelete.getId()); 962 963 List<ProgramItem> childProgramItems; 964 if (mode == DeleteMode.STRUCTURE_ONLY && contentToDelete instanceof TraversableProgramPart) 965 { 966 // Get subprogram, container and course list children only 967 childProgramItems = ((TraversableProgramPart) contentToDelete).getProgramPartChildren().stream().map(c -> (ProgramItem) c).collect(Collectors.toList()); 968 } 969 else 970 { 971 childProgramItems = _odfHelper.getChildProgramItems(contentToDelete); 972 } 973 974 for (ProgramItem childProgramItem : childProgramItems) 975 { 976 if (!_isContentReferenced((Content) childProgramItem, (Content) initialContentToDelete)) 977 { 978 // If all references of this program item is part of the initial content to delete, it can be deleted 979 if (mode == DeleteMode.STRUCTURE_ONLY && childProgramItem instanceof CourseList) 980 { 981 // Remove the relations to the course list to be deleted on all child courses 982 _removeRelations(((CourseList) childProgramItem).getCourses(), (Content) childProgramItem, Course.PARENT_COURSE_LISTS, 22, results); 983 toDelete.add(childProgramItem.getId()); 984 } 985 else 986 { 987 // Browse children recursively 988 toDelete.addAll(_getChildrenIdToDelete(childProgramItem, initialContentToDelete, results, mode, ignoreRights)); 989 } 990 } 991 else 992 { 993 // The child program item can not be deleted, remove the relation to the parent and stop iteration 994 String parentMetadataName; 995 if (childProgramItem instanceof CourseList) 996 { 997 parentMetadataName = contentToDelete instanceof Course ? CourseList.PARENT_COURSES : ProgramPart.PARENT_PROGRAM_PARTS; 998 } 999 else if (childProgramItem instanceof Course) 1000 { 1001 parentMetadataName = Course.PARENT_COURSE_LISTS; 1002 } 1003 else 1004 { 1005 parentMetadataName = ProgramPart.PARENT_PROGRAM_PARTS; 1006 } 1007 1008 _removeRelation((WorkflowAwareContent) childProgramItem, (Content) contentToDelete, parentMetadataName, 22, results); 1009 1010 @SuppressWarnings("unchecked") 1011 Set<Content> referencedContents = (Set<Content>) results.get("referenced-contents"); 1012 referencedContents.add((Content) childProgramItem); 1013 } 1014 } 1015 } 1016 1017 return toDelete; 1018 } 1019 1020 /** 1021 * Determines if the user has sufficient right for the given content 1022 * @param content the content 1023 * @return true if user has sufficient right 1024 */ 1025 public boolean hasRight(Content content) 1026 { 1027 String rightId = _getRightId(content); 1028 return _rightManager.hasRight(_currentUserProvider.getUser(), rightId, content) == RightResult.RIGHT_ALLOW; 1029 } 1030 1031 private String _getRightId (Content content) 1032 { 1033 if (content instanceof Course) 1034 { 1035 return "ODF_Rights_Course_Delete"; 1036 } 1037 else if (content instanceof SubProgram) 1038 { 1039 return "ODF_Rights_SubProgram_Delete"; 1040 } 1041 else if (content instanceof Container) 1042 { 1043 return "ODF_Rights_Container_Delete"; 1044 } 1045 else if (content instanceof Program) 1046 { 1047 return "ODF_Rights_Program_Delete"; 1048 } 1049 else if (content instanceof Person) 1050 { 1051 return "ODF_Rights_Person_Delete"; 1052 } 1053 else if (content instanceof OrgUnit) 1054 { 1055 return "ODF_Rights_OrgUnit_Delete"; 1056 } 1057 else if (content instanceof CourseList) 1058 { 1059 return "ODF_Rights_CourseList_Delete"; 1060 } 1061 return "Workflow_Rights_Delete"; 1062 } 1063 1064 /** 1065 * True if the content is referenced (we are ignoring parent references if they have same root) 1066 * @param programItem the program item 1067 * @param initialContentToDelete the initial content to delete 1068 * @return true if the content is referenced 1069 */ 1070 private boolean _isReferencedContentCheckingRoot(ProgramItem programItem, Content initialContentToDelete) 1071 { 1072 if (programItem.getId().equals(initialContentToDelete.getId())) 1073 { 1074 return false; 1075 } 1076 1077 List<ProgramItem> parentProgramItems = _odfHelper.getParentProgramItems(programItem); 1078 if (parentProgramItems.isEmpty()) 1079 { 1080 return true; 1081 } 1082 1083 for (ProgramItem parentProgramItem : parentProgramItems) 1084 { 1085 if (_isReferencedContentCheckingRoot(parentProgramItem, initialContentToDelete)) 1086 { 1087 return true; 1088 } 1089 } 1090 1091 return false; 1092 } 1093}