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