001/* 002 * Copyright 2019 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.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024import java.util.concurrent.ExecutionException; 025import java.util.concurrent.Future; 026import java.util.stream.Collectors; 027 028import org.apache.avalon.framework.component.Component; 029import org.apache.avalon.framework.configuration.Configurable; 030import org.apache.avalon.framework.configuration.Configuration; 031import org.apache.avalon.framework.configuration.ConfigurationException; 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.collections4.MapUtils; 036import org.apache.solr.client.solrj.SolrServerException; 037import org.slf4j.Logger; 038 039import org.ametys.cms.ObservationConstants; 040import org.ametys.cms.clientsideelement.content.SmartContentClientSideElementHelper; 041import org.ametys.cms.content.indexing.solr.SolrIndexer; 042import org.ametys.cms.repository.Content; 043import org.ametys.core.observation.Event; 044import org.ametys.core.observation.ObservationManager; 045import org.ametys.core.user.CurrentUserProvider; 046import org.ametys.plugins.repository.AmetysObjectResolver; 047import org.ametys.plugins.repository.ModifiableAmetysObject; 048import org.ametys.plugins.repository.RemovableAmetysObject; 049import org.ametys.plugins.repository.lock.LockableAmetysObject; 050 051/** 052 * Delete UD content component 053 */ 054public abstract class AbstractDeleteUDContentComponent implements Component, Serviceable, Configurable 055{ 056 private static final int _REMOVE_REFERENCE_DEFAULT_ACTION_ID = 200; 057 058 /** The Solr indexer. */ 059 protected SolrIndexer _solrIndexer; 060 061 /** The Ametys object resolver */ 062 protected AmetysObjectResolver _resolver; 063 064 /** The observation manager */ 065 protected ObservationManager _observationManager; 066 067 /** The current user provider */ 068 protected CurrentUserProvider _currentUserProvider; 069 070 /** Helper for smart content client elements */ 071 protected SmartContentClientSideElementHelper _smartHelper; 072 073 /** The action id to call when references are removed */ 074 protected int _removeReferenceActionId; 075 076 @Override 077 public void service(ServiceManager smanager) throws ServiceException 078 { 079 _solrIndexer = (SolrIndexer) smanager.lookup(SolrIndexer.ROLE); 080 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 081 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 082 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 083 _smartHelper = (SmartContentClientSideElementHelper) smanager.lookup(SmartContentClientSideElementHelper.ROLE); 084 } 085 086 @Override 087 public void configure(Configuration configuration) throws ConfigurationException 088 { 089 Configuration conf = configuration.getChild("removeReferenceActionId"); 090 _removeReferenceActionId = conf.getValueAsInteger(_REMOVE_REFERENCE_DEFAULT_ACTION_ID); 091 } 092 093 /** 094 * Delete contents and logs results 095 * @param contentsToRemove the list of contents to remove 096 * @param parameters the additional parameters 097 * @param rights the map of rights id with its prefix 098 * @param logger The logger 099 * @return the number of deleted contents 100 */ 101 @SuppressWarnings("unchecked") 102 public int deleteContentsWithLog(List<Content> contentsToRemove, Map<String, Object> parameters, Map<String, String> rights, Logger logger) 103 { 104 int nbDeletedContents = 0; 105 contentsToRemove.stream().forEach(content -> logger.info("The content '{}' ({}) does not exist anymore in remote source: it will be deleted if possible.", content.getTitle(), content.getId())); 106 107 List<String> contentIds = contentsToRemove.stream() 108 .map(Content::getId) 109 .collect(Collectors.toList()); 110 111 logger.info("Trying to delete contents. This can take a while..."); 112 Map<String, Object> deleteResults = deleteContents(contentIds, MapUtils.EMPTY_SORTED_MAP, MapUtils.EMPTY_SORTED_MAP, logger); 113 logger.info("Contents deleting process ended."); 114 115 for (String contentId : contentIds) 116 { 117 Map<String, Object> result = (Map<String, Object>) deleteResults.get(contentId); 118 List<String> deletedContents = (List<String>) result.get("deleted-contents"); 119 nbDeletedContents += deletedContents.size(); 120 121 List<Content> referencedContents = (List<Content>) result.get("referenced-contents"); 122 if (referencedContents.size() > 0) 123 { 124 logger.info("The following contents cannot be deleted because they are referenced: {}", referencedContents.stream().map(c -> c.getId()).collect(Collectors.toList())); 125 } 126 127 List<Content> lockedContents = (List<Content>) result.get("locked-contents"); 128 if (lockedContents.size() > 0) 129 { 130 logger.info("The following contents cannot be deleted because they are locked: {}", lockedContents.stream().map(c -> c.getId()).collect(Collectors.toList())); 131 } 132 133 List<Content> undeletedContents = (List<Content>) result.get("undeleted-contents"); 134 if (undeletedContents.size() > 0) 135 { 136 logger.info("{} contents were not deleted. See previous logs for more information.", undeletedContents.size()); 137 } 138 } 139 140 return nbDeletedContents; 141 } 142 143 /** 144 * Delete contents 145 * @param contentsId The ids of contents to delete 146 * @param parameters the additional parameters 147 * @param rights the map of rights id with its prefix 148 * @param logger The logger 149 * @return the deleted and undeleted contents 150 */ 151 public Map<String, Object> deleteContents(List<String> contentsId, Map<String, Object> parameters, Map<String, String> rights, Logger logger) 152 { 153 Map<String, Object> results = new HashMap<>(); 154 155 List<String> alreadyDeletedContentIds = new ArrayList<>(); 156 for (String contentId : contentsId) 157 { 158 if (!alreadyDeletedContentIds.contains(contentId)) 159 { 160 Content content = _resolver.resolveById(contentId); 161 162 Map<String, Object> result = new HashMap<>(); 163 result.put("deleted-contents", new ArrayList<String>()); 164 result.put("undeleted-contents", new ArrayList<Content>()); 165 result.put("referenced-contents", new ArrayList<Content>()); 166 result.put("unauthorized-contents", new ArrayList<Content>()); 167 result.put("locked-contents", new ArrayList<Content>()); 168 result.put("initial-content", content.getId()); 169 results.put(contentId, result); 170 171 boolean referenced = isContentReferenced(content, logger); 172 if (referenced || !_checkBeforeDeletion(content, rights, result, logger)) 173 { 174 if (referenced) 175 { 176 // Indicate that the content is referenced. 177 @SuppressWarnings("unchecked") 178 List<Content> referencedContents = (List<Content>) result.get("referenced-contents"); 179 referencedContents.add(content); 180 } 181 result.put("check-before-deletion-failed", true); 182 } 183 else 184 { 185 // Process deletion 186 _deleteContent(content, parameters, rights, result, logger); 187 188 @SuppressWarnings("unchecked") 189 List<String> deletedContents = (List<String>) result.get("deleted-contents"); 190 if (deletedContents != null) 191 { 192 alreadyDeletedContentIds.addAll(deletedContents); 193 194 } 195 } 196 } 197 else 198 { 199 logger.info("Content with id '{}' has been already deleted during its parent deletion", contentId); 200 } 201 } 202 203 return results; 204 } 205 206 /** 207 * Delete one content 208 * @param content the content to delete 209 * @param parameters the additional parameters 210 * @param rights the map of rights id with its prefix 211 * @param results the results map 212 * @param logger The logger 213 */ 214 protected void _deleteContent(Content content, Map<String, Object> parameters, Map<String, String> rights, Map<String, Object> results, Logger logger) 215 { 216 // 1 - First remove relations 217 boolean success = _removeRelations(content, logger); 218 219 // 2 - If succeed, process to deletion 220 if (success) 221 { 222 _processContentDeletion(content, rights, results, logger); 223 } 224 else 225 { 226 @SuppressWarnings("unchecked") 227 List<Content> undeletedContents = (List<Content>) results.get("undeleted-contents"); 228 undeletedContents.add(content); 229 230 logger.warn("Can not delete content {} ('{}') : at least one relation to contents could not be removed", content.getTitle(), content.getId()); 231 } 232 } 233 234 /** 235 * Delete one content 236 * @param content the content to delete 237 * @param rights the map of rights id with its prefix 238 * @param results the results map 239 * @param logger The logger 240 */ 241 @SuppressWarnings("unchecked") 242 protected void _processContentDeletion(Content content, Map<String, String> rights, Map<String, Object> results, Logger logger) 243 { 244 Set<String> toDelete = _getContentIdsToDelete(content, rights, results, logger); 245 246 List<Content> referencedContents = (List<Content>) results.get("referenced-contents"); 247 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 248 List<Content> unauthorizedContents = (List<Content>) results.get("unauthorized-contents"); 249 250 if (referencedContents.size() == 0 && lockedContents.size() == 0 && unauthorizedContents.size() == 0) 251 { 252 _finalizeDeleteContents(toDelete, content.getParent(), results, logger); 253 } 254 } 255 256 /** 257 * Finalize the deletion of contents. Call observers and remove contents 258 * @param contentIdsToDelete the list of content id to delete 259 * @param parent the jcr parent for saving changes 260 * @param results the results map 261 * @param logger The logger 262 */ 263 protected void _finalizeDeleteContents(Set<String> contentIdsToDelete, ModifiableAmetysObject parent, Map<String, Object> results, Logger logger) 264 { 265 @SuppressWarnings("unchecked") 266 List<Content> unauthorizedContents = (List<Content>) results.get("unauthorized-contents"); 267 @SuppressWarnings("unchecked") 268 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 269 270 if (!unauthorizedContents.isEmpty() || !lockedContents.isEmpty()) 271 { 272 //Do Nothing 273 return; 274 } 275 276 try 277 { 278 _observationManager.addArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT, false); 279 280 Map<String, Map<String, Object>> eventParams = new HashMap<>(); 281 for (String id : contentIdsToDelete) 282 { 283 Content content = _resolver.resolveById(id); 284 Map<String, Object> eventParam = _getEventParametersForDeletion(content); 285 eventParams.put(id, eventParam); 286 287 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParam)); 288 } 289 290 for (String id : contentIdsToDelete) 291 { 292 Content content = _resolver.resolveById(id); 293 294 // Remove the content. 295 LockableAmetysObject lockedContent = (LockableAmetysObject) content; 296 if (lockedContent.isLocked()) 297 { 298 lockedContent.unlock(); 299 } 300 301 ((RemovableAmetysObject) content).remove(); 302 } 303 304 parent.saveChanges(); 305 306 for (String id : contentIdsToDelete) 307 { 308 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams.get(id))); 309 310 @SuppressWarnings("unchecked") 311 List<String> deletedContents = (List<String>) results.get("deleted-contents"); 312 deletedContents.add(id); 313 } 314 } 315 finally 316 { 317 _observationManager.removeArgumentForEvents(new String[] {ObservationConstants.EVENT_CONTENT_DELETED}, ObservationConstants.ARGS_CONTENT_COMMIT); 318 _commitAllChanges(logger); 319 } 320 } 321 322 /** 323 * True if we can delete the content (check if removable, rights and if locked) 324 * @param content the content 325 * @param rights the map of rights id with its prefix 326 * @param results the results map 327 * @return true if we can delete the content 328 */ 329 protected boolean _canDeleteContent(Content content, Map<String, String> rights, Map<String, Object> results) 330 { 331 if (!(content instanceof RemovableAmetysObject)) 332 { 333 throw new IllegalArgumentException("The content [" + content.getId() + "] is not a RemovableAmetysObject, it can't be deleted."); 334 } 335 336 if (!_hasRight(content, rights)) 337 { 338 // User has no sufficient right 339 @SuppressWarnings("unchecked") 340 List<Content> norightContents = (List<Content>) results.get("unauthorized-contents"); 341 norightContents.add(content); 342 343 return false; 344 } 345 else if (_isLocked(content)) 346 { 347 @SuppressWarnings("unchecked") 348 List<Content> lockedContents = (List<Content>) results.get("locked-contents"); 349 lockedContents.add(content); 350 351 return false; 352 } 353 354 return true; 355 } 356 357 /** 358 * Get parameters for content deleted {@link Event} 359 * @param content the removed content 360 * @return the event's parameters 361 */ 362 protected Map<String, Object> _getEventParametersForDeletion (Content content) 363 { 364 Map<String, Object> eventParams = new HashMap<>(); 365 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 366 eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName()); 367 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 368 return eventParams; 369 } 370 371 /** 372 * Commit all changes in solr 373 * @param logger The logger 374 */ 375 protected void _commitAllChanges(Logger logger) 376 { 377 // Before trying to commit, be sure all the async observers of the current request are finished 378 for (Future future : _observationManager.getFuturesForRequest()) 379 { 380 try 381 { 382 future.get(); 383 } 384 catch (ExecutionException | InterruptedException e) 385 { 386 logger.info("An exception occured when calling #get() on Future result of an observer." , e); 387 } 388 } 389 390 // Commit all uncommited changes 391 try 392 { 393 _solrIndexer.commit(); 394 395 logger.info("Deleted contents are now committed into Solr."); 396 } 397 catch (IOException | SolrServerException e) 398 { 399 logger.error("Impossible to commit changes", e); 400 } 401 } 402 403 /** 404 * Determines if the content is locked 405 * @param content the content 406 * @return true if the content is locked 407 */ 408 protected boolean _isLocked(Content content) 409 { 410 return _smartHelper.isLocked(content); 411 } 412 413 /** 414 * Determines if the user has sufficient right for the given content 415 * @param content the content 416 * @param rights the map of rights id with its prefix 417 * @return true if user has sufficient right 418 */ 419 protected boolean _hasRight(Content content, Map<String, String> rights) 420 { 421 if (rights.isEmpty()) 422 { 423 return true; 424 } 425 426 return _smartHelper.hasRight(rights, content); 427 } 428 429 /** 430 * True if the content is referenced 431 * @param content the content 432 * @param logger The logger 433 * @return true if the content is referenced 434 */ 435 public abstract boolean isContentReferenced(Content content, Logger logger); 436 437 /** 438 * Check that deletion can be performed without blocking errors 439 * @param content The initial content to delete 440 * @param rights the map of rights id with its prefix 441 * @param results The results 442 * @param logger The logger 443 * @return true if the deletion can be performed 444 */ 445 protected abstract boolean _checkBeforeDeletion(Content content, Map<String, String> rights, Map<String, Object> results, Logger logger); 446 447 /** 448 * Remove relations 449 * @param content the content 450 * @param logger The logger 451 * @return <code>true</code> if all relations have been removed 452 */ 453 protected abstract boolean _removeRelations(Content content, Logger logger); 454 455 /** 456 * Get the id of children to be deleted. 457 * All children shared with other contents which are not part of deletion, will be not deleted. 458 * @param content The content to delete 459 * @param rights the map of rights id with its prefix 460 * @param results The results 461 * @param logger The logger 462 * @return The id of contents to be deleted 463 */ 464 protected abstract Set<String> _getContentIdsToDelete (Content content, Map<String, String> rights, Map<String, Object> results, Logger logger); 465 466}