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