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.plugins.userdirectory; 017 018import java.io.IOException; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.concurrent.ExecutionException; 027import java.util.concurrent.Future; 028import java.util.stream.Collectors; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.commons.lang3.ArrayUtils; 035import org.apache.solr.client.solrj.SolrServerException; 036 037import org.ametys.cms.ObservationConstants; 038import org.ametys.cms.clientsideelement.content.SmartContentClientSideElementHelper; 039import org.ametys.cms.content.external.ExternalizableMetadataHelper; 040import org.ametys.cms.content.indexing.solr.SolrIndexer; 041import org.ametys.cms.repository.Content; 042import org.ametys.cms.repository.WorkflowAwareContent; 043import org.ametys.cms.workflow.ContentWorkflowHelper; 044import org.ametys.core.observation.Event; 045import org.ametys.core.observation.ObservationManager; 046import org.ametys.core.user.CurrentUserProvider; 047import org.ametys.plugins.repository.AmetysObjectResolver; 048import org.ametys.plugins.repository.AmetysRepositoryException; 049import org.ametys.plugins.repository.ModifiableAmetysObject; 050import org.ametys.plugins.repository.RemovableAmetysObject; 051import org.ametys.plugins.repository.lock.LockableAmetysObject; 052import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 053import org.ametys.runtime.plugin.component.AbstractLogEnabled; 054 055import com.opensymphony.workflow.WorkflowException; 056 057/** 058 * Delete orgunit component 059 */ 060public class DeleteOrgUnitComponent extends AbstractLogEnabled implements Component, Serviceable 061{ 062 /** The avalon role. */ 063 public static final String ROLE = DeleteOrgUnitComponent.class.getName(); 064 065 private static final int _REMOVE_REFERENCE_DEFAULT_ACTION_ID = 22; 066 067 /** The organisation chart page handler */ 068 protected OrganisationChartPageHandler _oCPageHandler; 069 070 /** The Content workflow helper */ 071 protected ContentWorkflowHelper _contentWorkflowHelper; 072 073 /** The Solr indexer. */ 074 protected SolrIndexer _solrIndexer; 075 076 /** The Ametys object resolver */ 077 protected AmetysObjectResolver _resolver; 078 079 /** The observation manager */ 080 protected ObservationManager _observationManager; 081 082 /** The current user provider */ 083 protected CurrentUserProvider _currentUserProvider; 084 085 /** Helper for smart content client elements */ 086 protected SmartContentClientSideElementHelper _smartHelper; 087 088 @Override 089 public void service(ServiceManager smanager) throws ServiceException 090 { 091 _oCPageHandler = (OrganisationChartPageHandler) smanager.lookup(OrganisationChartPageHandler.ROLE); 092 _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE); 093 _solrIndexer = (SolrIndexer) smanager.lookup(SolrIndexer.ROLE); 094 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 095 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 096 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 097 _smartHelper = (SmartContentClientSideElementHelper) smanager.lookup(SmartContentClientSideElementHelper.ROLE); 098 } 099 100 /** 101 * Delete orgUnits contents 102 * @param contentsId The ids of contents to delete 103 * @param parameters the additional parameters 104 * @param rights the map of rights id with its prefix 105 * @return the deleted and undeleted contents 106 */ 107 public Map<String, Object> deleteContents(List<String> contentsId, Map<String, Object> parameters, Map<String, String> rights) 108 { 109 Map<String, Object> results = new HashMap<>(); 110 111 List<String> alreadyDeletedContentIds = new ArrayList<>(); 112 for (String contentId : contentsId) 113 { 114 if (!alreadyDeletedContentIds.contains(contentId)) 115 { 116 Content content = _resolver.resolveById(contentId); 117 118 Map<String, Object> result = new HashMap<>(); 119 result.put("deleted-contents", new ArrayList<String>()); 120 result.put("undeleted-contents", new ArrayList<Content>()); 121 result.put("referenced-contents", new ArrayList<Content>()); 122 result.put("unauthorized-contents", new ArrayList<Content>()); 123 result.put("locked-contents", new ArrayList<Content>()); 124 result.put("initial-content", content.getId()); 125 results.put(contentId, result); 126 127 boolean referenced = _oCPageHandler.isReferencedOrgUnit(content); 128 if (referenced || !checkBeforeDeletion(content, result, rights)) 129 { 130 if (referenced) 131 { 132 // Indicate that the content is referenced. 133 @SuppressWarnings("unchecked") 134 List<Content> referencedContents = (List<Content>) result.get("referenced-contents"); 135 referencedContents.add(content); 136 } 137 result.put("check-before-deletion-failed", true); 138 } 139 else 140 { 141 // Process deletion 142 deleteContent(content, result, parameters, rights); 143 144 @SuppressWarnings("unchecked") 145 List<String> deletedContents = (List<String>) result.get("deleted-contents"); 146 if (deletedContents != null) 147 { 148 alreadyDeletedContentIds.addAll(deletedContents); 149 150 } 151 } 152 } 153 else 154 { 155 getLogger().info("Content with id '{}' has been already deleted during its parent deletion", contentId); 156 } 157 } 158 159 return results; 160 } 161 162 /** 163 * Delete one content 164 * @param content the content to delete 165 * @param results the results map 166 * @param rights the map of rights id with its prefix 167 * @param parameters the additional parameters 168 */ 169 protected void deleteContent(Content content, Map<String, Object> results, Map<String, Object> parameters, Map<String, String> rights) 170 { 171 // 1 - First delete relation to parent 172 Content parentOrgUnit = _oCPageHandler.getParentContent(content); 173 boolean success = true; 174 if (parentOrgUnit != null) 175 { 176 success = _removeRelation((WorkflowAwareContent) parentOrgUnit, content, OrganisationChartPageHandler.METADATA_CHILD_ORGUNIT, _REMOVE_REFERENCE_DEFAULT_ACTION_ID, results); 177 } 178 179 // 2 - If succeed, process to deletion 180 if (success) 181 { 182 deleteOrgUnit(content, results, rights); 183 } 184 else 185 { 186 @SuppressWarnings("unchecked") 187 List<Content> undeletedContents = (List<Content>) results.get("undeleted-contents"); 188 undeletedContents.add(content); 189 } 190 } 191 192 /** 193 * Delete one orgUnit 194 * @param orgUnit the orgUnit to delete 195 * @param rights the map of rights id with its prefix 196 * @param results the results map 197 */ 198 @SuppressWarnings("unchecked") 199 protected void deleteOrgUnit(Content orgUnit, Map<String, Object> results, Map<String, String> rights) 200 { 201 Set<String> toDelete = _getChildrenIdToDelete(orgUnit, results, rights); 202 203 List<Content> referencedContents = (List<Content>) results.get("referenced-contents"); 204 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 205 List<Content> unauthorizedContents = (List<Content>) results.get("unauthorized-contents"); 206 207 if (referencedContents.size() == 0 && lockedContents.size() == 0 && unauthorizedContents.size() == 0) 208 { 209 _finalizeDeleteContents(toDelete, orgUnit.getParent(), results); 210 } 211 } 212 213 /** 214 * Get the id of children to be deleted. 215 * All children shared with other contents which are not part of deletion, will be not deleted. 216 * @param orgUnit The orgunit to delete 217 * @param results The results 218 * @param rights the map of rights id with its prefix 219 * @return The id of contents to be deleted 220 */ 221 protected Set<String> _getChildrenIdToDelete (Content orgUnit, Map<String, Object> results, Map<String, String> rights) 222 { 223 Set<String> toDelete = new HashSet<>(); 224 225 if (_canDeleteContent(orgUnit, results, rights)) 226 { 227 toDelete.add(orgUnit.getId()); 228 229 for (Content childOrgUnit : _oCPageHandler.getChildContents(orgUnit)) 230 { 231 if (!_oCPageHandler.isReferencedOrgUnit(orgUnit)) 232 { 233 toDelete.addAll(_getChildrenIdToDelete(childOrgUnit, results, rights)); 234 } 235 else 236 { 237 // The child program item can not be deleted, remove the relation to the parent and stop iteration 238 @SuppressWarnings("unchecked") 239 List<Content> referencedContents = (List<Content>) results.get("referenced-contents"); 240 referencedContents.add(childOrgUnit); 241 } 242 } 243 } 244 245 return toDelete; 246 } 247 248 /** 249 * Finalize the deletion of contents. Call observers and remove contents 250 * @param contentIdsToDelete the list of content id to delete 251 * @param parent the jcr parent for saving changes 252 * @param results the results map 253 */ 254 private void _finalizeDeleteContents(Set<String> contentIdsToDelete, ModifiableAmetysObject parent, Map<String, Object> results) 255 { 256 @SuppressWarnings("unchecked") 257 List<Content> unauthorizedContents = (List<Content>) results.get("unauthorized-contents"); 258 @SuppressWarnings("unchecked") 259 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 260 261 if (!unauthorizedContents.isEmpty() || !lockedContents.isEmpty()) 262 { 263 //Do Nothing 264 return; 265 } 266 267 try 268 { 269 _observationManager.addArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT, false); 270 271 Map<String, Map<String, Object>> eventParams = new HashMap<>(); 272 for (String id : contentIdsToDelete) 273 { 274 Content content = _resolver.resolveById(id); 275 Map<String, Object> eventParam = _getEventParametersForDeletion(content); 276 eventParams.put(id, eventParam); 277 278 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParam)); 279 280 // Remove the content. 281 LockableAmetysObject lockedContent = (LockableAmetysObject) content; 282 if (lockedContent.isLocked()) 283 { 284 lockedContent.unlock(); 285 } 286 287 ((RemovableAmetysObject) content).remove(); 288 } 289 290 291 parent.saveChanges(); 292 293 for (String id : contentIdsToDelete) 294 { 295 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams.get(id))); 296 297 @SuppressWarnings("unchecked") 298 List<String> deletedContents = (List<String>) results.get("deleted-contents"); 299 deletedContents.add(id); 300 } 301 } 302 finally 303 { 304 _observationManager.removeArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT); 305 _commitAllChanges(); 306 } 307 } 308 309 /** 310 * Check that deletion can be performed without blocking errors 311 * @param content The initial content to delete 312 * @param results The results 313 * @param rights the map of rights id with its prefix 314 * @return true if the deletion can be performed 315 */ 316 protected boolean checkBeforeDeletion(Content content, Map<String, Object> results, Map<String, String> rights) 317 { 318 // Check right and lock on content it self 319 boolean allRight = _canDeleteContent(content, results, rights); 320 321 // Check lock on parent contents 322 allRight = _checkParentBeforeDeletion(content, results) && allRight; 323 324 // Check right and lock on children to be deleted or modified 325 allRight = _checkChildrenBeforeDeletion(content, results, rights) && allRight; 326 327 return allRight; 328 } 329 330 /** 331 * True if we can delete the content (check if removable, rights and if locked) 332 * @param content the content 333 * @param results the results map 334 * @param rights the map of rights id with its prefix 335 * @return true if we can delete the content 336 */ 337 protected boolean _canDeleteContent(Content content, Map<String, Object> results, Map<String, String> rights) 338 { 339 if (!(content instanceof RemovableAmetysObject)) 340 { 341 throw new IllegalArgumentException("The content [" + content.getId() + "] is not a RemovableAmetysObject, it can't be deleted."); 342 } 343 344 if (!_hasRight(content, rights)) 345 { 346 // User has no sufficient right 347 @SuppressWarnings("unchecked") 348 List<Content> norightContents = (List<Content>) results.get("unauthorized-contents"); 349 norightContents.add(content); 350 351 return false; 352 } 353 else if (_isLocked(content)) 354 { 355 @SuppressWarnings("unchecked") 356 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 357 lockedContents.add(content); 358 359 return false; 360 } 361 362 return true; 363 } 364 365 /** 366 * True if the parent content is not locked 367 * @param content the content 368 * @param results the results map 369 * @return true if the parent content is not locked 370 */ 371 protected boolean _checkParentBeforeDeletion(Content content, Map<String, Object> results) 372 { 373 boolean allRight = true; 374 375 // Check if parents are not locked 376 Content parentContent = _oCPageHandler.getParentContent(content); 377 if (_isLocked(parentContent)) 378 { 379 @SuppressWarnings("unchecked") 380 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 381 lockedContents.add(parentContent); 382 383 allRight = false; 384 } 385 386 return allRight; 387 } 388 389 /** 390 * Browse children to check if deletion could succeed 391 * @param contentToCheck The current content to check 392 * @param results The result 393 * @param rights the map of rights id with its prefix 394 * @return true if the deletion can be processed 395 */ 396 private boolean _checkChildrenBeforeDeletion(Content contentToCheck, Map<String, Object> results, Map<String, String> rights) 397 { 398 boolean allRight = true; 399 400 List<Content> childOrgUnits = _oCPageHandler.getChildContents(contentToCheck); 401 for (Content childOrgUnit : childOrgUnits) 402 { 403 if (!_canDeleteContent(childOrgUnit, results, rights)) 404 { 405 allRight = false; 406 } 407 else if (_oCPageHandler.isReferencedOrgUnit(childOrgUnit)) 408 { 409 @SuppressWarnings("unchecked") 410 List<Content> referencedContents = (List<Content>) results.get("referenced-contents"); 411 referencedContents.add(childOrgUnit); 412 413 allRight = false; 414 } 415 else 416 { 417 // Browse children recursively 418 allRight = _checkChildrenBeforeDeletion(childOrgUnit, results, rights) && allRight; 419 } 420 } 421 422 return allRight; 423 } 424 425 /** 426 * Remove the relation parent-child relation on content. 427 * @param contentToEdit The content to modified 428 * @param refContentToRemove The referenced content to be removed from content 429 * @param metadataName The name of metadata holding the child or parent relationship 430 * @param actionId The id of workflow action to edit the relation 431 * @param results the results map 432 * @return boolean true if remove relation successfully 433 */ 434 protected boolean _removeRelation(WorkflowAwareContent contentToEdit, Content refContentToRemove, String metadataName, int actionId, Map<String, Object> results) 435 { 436 try 437 { 438 ModifiableCompositeMetadata metadataHolder = contentToEdit.getMetadataHolder(); 439 440 String[] values = metadataHolder.getStringArray(metadataName, new String[0]); 441 442 if (ArrayUtils.contains(values, refContentToRemove.getId())) 443 { 444 String[] newValues = ArrayUtils.removeElement(values, refContentToRemove.getId()); 445 446 List<Content> newContents = Arrays.asList(newValues).stream() 447 .map(id -> (Content) _resolver.resolveById(id)) 448 .collect(Collectors.toList()); 449 450 ExternalizableMetadataHelper.setMetadata(metadataHolder, metadataName, newContents.toArray(new Content[newContents.size()])); 451 _applyChanges(contentToEdit, actionId); 452 } 453 454 return true; 455 } 456 catch (WorkflowException | AmetysRepositoryException e) 457 { 458 getLogger().error("Unable to remove relationship to content {} ({}) on content {} ({}) for metadata {}", refContentToRemove.getTitle(), refContentToRemove.getId(), contentToEdit.getTitle(), contentToEdit.getId(), metadataName, e); 459 return false; 460 } 461 } 462 463 private void _applyChanges(WorkflowAwareContent content, int actionId) throws WorkflowException 464 { 465 // Notify listeners 466 Map<String, Object> eventParams = new HashMap<>(); 467 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 468 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 469 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams)); 470 471 _contentWorkflowHelper.doAction(content, actionId); 472 } 473 474 /** 475 * Get parameters for content deleted {@link Event} 476 * @param content the removed content 477 * @return the event's parameters 478 */ 479 protected Map<String, Object> _getEventParametersForDeletion (Content content) 480 { 481 Map<String, Object> eventParams = new HashMap<>(); 482 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 483 eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName()); 484 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 485 return eventParams; 486 } 487 488 /** 489 * Commit all changes in solr 490 */ 491 protected void _commitAllChanges() 492 { 493 // Before trying to commit, be sure all the async observers of the current request are finished 494 for (Future future : _observationManager.getFuturesForRequest()) 495 { 496 try 497 { 498 future.get(); 499 } 500 catch (ExecutionException | InterruptedException e) 501 { 502 getLogger().info("An exception occured when calling #get() on Future result of an observer." , e); 503 } 504 } 505 506 // Commit all uncommited changes 507 try 508 { 509 _solrIndexer.commit(); 510 511 if (getLogger().isDebugEnabled()) 512 { 513 getLogger().info("Deleted contents are now committed into Solr."); 514 } 515 } 516 catch (IOException | SolrServerException e) 517 { 518 getLogger().error("Impossible to commit changes", e); 519 } 520 } 521 522 /** 523 * Determines if the content is locked 524 * @param content the content 525 * @return true if the content is locked 526 */ 527 protected boolean _isLocked(Content content) 528 { 529 return _smartHelper.isLocked(content); 530 } 531 532 /** 533 * Determines if the user has sufficient right for the given content 534 * @param content the content 535 * @param rights the map of rights id with its prefix 536 * @return true if user has sufficient right 537 */ 538 protected boolean _hasRight(Content content, Map<String, String> rights) 539 { 540 if (rights.isEmpty()) 541 { 542 return true; 543 } 544 545 return _smartHelper.hasRight(rights, content); 546 } 547}