001/* 002 * Copyright 2012 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.web.repository.content.shared; 017 018import java.time.ZonedDateTime; 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.Map; 024import java.util.Set; 025 026import javax.jcr.Node; 027import javax.jcr.PropertyIterator; 028import javax.jcr.RepositoryException; 029 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.logger.AbstractLogEnabled; 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.lang.ArrayUtils; 036import org.apache.commons.lang.StringUtils; 037 038import org.ametys.cms.CmsConstants; 039import org.ametys.cms.ObservationConstants; 040import org.ametys.cms.repository.Content; 041import org.ametys.cms.repository.DefaultContent; 042import org.ametys.core.observation.Event; 043import org.ametys.core.observation.ObservationManager; 044import org.ametys.core.user.CurrentUserProvider; 045import org.ametys.core.user.UserIdentity; 046import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 047import org.ametys.plugins.explorer.resources.ResourceCollection; 048import org.ametys.plugins.repository.AmetysObjectResolver; 049import org.ametys.plugins.repository.AmetysRepositoryException; 050import org.ametys.plugins.repository.CopiableAmetysObject; 051import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 052import org.ametys.plugins.repository.RepositoryIntegrityViolationException; 053import org.ametys.plugins.repository.data.holder.ModifiableModelAwareDataHolder; 054import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 055import org.ametys.plugins.repository.jcr.JCRAmetysObject; 056import org.ametys.plugins.repository.jcr.NameHelper; 057import org.ametys.plugins.repository.version.VersionableAmetysObject; 058import org.ametys.runtime.model.ElementDefinition; 059import org.ametys.runtime.model.ModelItem; 060import org.ametys.web.filter.SharedContentsHelper; 061import org.ametys.web.repository.content.ModifiableWebContent; 062import org.ametys.web.repository.content.SharedContent; 063import org.ametys.web.repository.content.WebContent; 064import org.ametys.web.repository.content.jcr.DefaultSharedContent; 065import org.ametys.web.repository.content.jcr.DefaultSharedContentFactory; 066import org.ametys.web.repository.content.jcr.DefaultWebContent; 067import org.ametys.web.repository.page.CopySiteComponent; 068import org.ametys.web.repository.page.ModifiableZoneItem; 069import org.ametys.web.repository.page.SitemapElement; 070import org.ametys.web.repository.page.ZoneItem; 071import org.ametys.web.repository.page.ZoneItem.ZoneType; 072import org.ametys.web.repository.site.Site; 073 074/** 075 * Component which provides methods to manage shared contents (creation, validation, and so on). 076 */ 077public class SharedContentManager extends AbstractLogEnabled implements Serviceable, Component 078{ 079 080 /** The avalon role. */ 081 public static final String ROLE = SharedContentManager.class.getName(); 082 083 /** The ametys object resolver. */ 084 protected AmetysObjectResolver _resolver; 085 086 /** The observation manager. */ 087 protected ObservationManager _observationManager; 088 089 /** The current user provider. */ 090 protected CurrentUserProvider _currentUserProvider; 091 092 /** The site copy component. */ 093 protected CopySiteComponent _copySiteComponent; 094 095 @Override 096 public void service(ServiceManager serviceManager) throws ServiceException 097 { 098 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 099 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 100 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 101 _copySiteComponent = (CopySiteComponent) serviceManager.lookup(CopySiteComponent.ROLE); 102 } 103 104 /** 105 * Create a {@link SharedContent} from an original content. 106 * @param site the site in which to create the shared content. 107 * @param originalContent the original content. 108 * @return the created shared content. 109 */ 110 public DefaultSharedContent createSharedContent(Site site, DefaultContent originalContent) 111 { 112 try 113 { 114 ModifiableTraversableAmetysObject contentRoot = site.getRootContents(); 115 116 // Get a reference on the original node. 117 Node originalNode = originalContent.getNode(); 118 119 String copyName = originalContent.getName() + "-shared"; 120 121 DefaultSharedContent content = createContent(copyName, contentRoot); 122 123 // Store the reference to the original content. 124 content.getNode().setProperty(DefaultSharedContent.INITIAL_CONTENT_PROPERTY, originalNode); 125 126 String originalLanguage = originalContent.getLanguage(); 127 if (originalLanguage != null) 128 { 129 content.setLanguage(originalContent.getLanguage()); 130 } 131 132 // Copy standard properties. 133 content.setTypes(originalContent.getTypes()); 134 // Copy title needs model -> types must to be set before 135 SharedContentsHelper.copyTitle(originalContent, content); 136 137 content.setCreator(originalContent.getCreator()); 138 content.setCreationDate(originalContent.getCreationDate()); 139 content.setLastContributor(originalContent.getLastContributor()); 140 content.setLastModified(originalContent.getLastModified()); 141 content.setSiteName(site.getName()); 142 143 content.saveChanges(); 144 145 // Copy the content data. 146 copyContentData(originalContent, content); 147 148 return content; 149 } 150 catch (RepositoryException e) 151 { 152 throw new AmetysRepositoryException(e); 153 } 154 } 155 156 /** 157 * Copy the data of a content into a shared content. 158 * @param originalContent the content to copy data from. 159 * @param content the content to copy data to. 160 * @throws AmetysRepositoryException if an error occurs during copy 161 */ 162 public void copyContentData(DefaultContent originalContent, DefaultSharedContent content) throws AmetysRepositoryException 163 { 164 String currentRevision = originalContent.getRevision(); 165 166 try 167 { 168 if (ArrayUtils.contains(originalContent.getAllLabels(), CmsConstants.LIVE_LABEL)) 169 { 170 // Switch the content to its live revision. 171 originalContent.switchToLabel(CmsConstants.LIVE_LABEL); 172 173 // Copy metadata. 174 removeAllModelAwareData(content); 175 originalContent.copyTo(content); 176 177 // Copy unversioned metadata. 178 removeAllModelLessData(content.getUnversionedDataHolder()); 179 originalContent.getUnversionedDataHolder().copyTo(content.getUnversionedDataHolder()); 180 181 // Copy attachments. 182 ResourceCollection originalRootAttachments = originalContent.getRootAttachments(); 183 if (originalRootAttachments != null && originalRootAttachments instanceof CopiableAmetysObject) 184 { 185 // Remove the attachment root before copying. 186 ModifiableResourceCollection rootAttachments = (ModifiableResourceCollection) content.getRootAttachments(); 187 if (rootAttachments != null) // root attachments can be missing if the content is an (unmodifiable) old version 188 { 189 rootAttachments.remove(); 190 ((CopiableAmetysObject) originalRootAttachments).copyTo(content, originalRootAttachments.getName()); 191 } 192 } 193 194 if (originalContent instanceof WebContent originalWebContent) 195 { 196 _copySiteComponent.updateSharedContent(originalWebContent, content); 197 } 198 199 content.saveChanges(); 200 content.checkpoint(); 201 202 Map<String, Object> eventParams = new HashMap<>(); 203 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 204 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 205 206 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, _currentUserProvider.getUser(), eventParams)); 207 208 // Validate the shared content if the original content is validated, and only create a new version otherwise. 209 if (ArrayUtils.contains(originalContent.getAllLabels(), CmsConstants.LIVE_LABEL)) 210 { 211 validateContent(content); 212 } 213 } 214 } 215 finally 216 { 217 originalContent.switchToRevision(currentRevision); 218 } 219 } 220 221 /** 222 * Validate a shared content. 223 * @param content the content to validate. 224 */ 225 public void validateContent(DefaultSharedContent content) 226 { 227 internalValidateContent(content); 228 229 UserIdentity user = _currentUserProvider.getUser(); 230 231 // Notify observers that the content has been validated. 232 Map<String, Object> eventParams = new HashMap<>(); 233 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 234 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 235 236 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_VALIDATED, user, eventParams)); 237 } 238 239 /** 240 * Invalidate a shared content. 241 * @param content the content to invalidate. 242 */ 243 public void invalidateSharedContent(DefaultSharedContent content) 244 { 245 internalUnpublishContent(content); 246 247 UserIdentity user = _currentUserProvider.getUser(); 248 249 // Notify observers that the content has been unpublished. 250 Map<String, Object> eventParams = new HashMap<>(); 251 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 252 eventParams.put(org.ametys.web.ObservationConstants.ARGS_SITE_NAME, content.getSiteName()); 253 254 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_UNTAG_LIVE, user, eventParams)); 255 } 256 257 /** 258 * Test if there are shared contents created from the given content. 259 * @param content the content to test. 260 * @return true if at least one shared content was created from the given content, false otherwise. 261 */ 262 public boolean hasSharedContents(Content content) 263 { 264 try 265 { 266 if (content instanceof DefaultContent) 267 { 268 DefaultContent originalContent = (DefaultContent) content; 269 PropertyIterator references = originalContent.getNode().getReferences(DefaultSharedContent.INITIAL_CONTENT_PROPERTY); 270 271 return references.hasNext(); 272 } 273 274 return false; 275 } 276 catch (RepositoryException e) 277 { 278 throw new AmetysRepositoryException(e); 279 } 280 } 281 282 /** 283 * Get the list of shared contents created from the given content. 284 * @param content the content of which to get referencing shared contents. 285 * @return the shared contents created from the given content. 286 */ 287 public Set<SharedContent> getSharedContents(Content content) 288 { 289 try 290 { 291 Set<SharedContent> sharedContents = new HashSet<>(); 292 293 if (content instanceof DefaultContent) 294 { 295 // Resolve the content to switch its revision without impacting the original object. 296 DefaultContent originalContent = (DefaultContent) content; 297 PropertyIterator references = originalContent.getNode().getReferences(DefaultSharedContent.INITIAL_CONTENT_PROPERTY); 298 while (references.hasNext()) 299 { 300 Node referer = references.nextProperty().getParent(); 301 DefaultSharedContent sharedContent = _resolver.resolve(referer, true); 302 if (sharedContent != null) 303 { 304 sharedContents.add(sharedContent); 305 } 306 } 307 } 308 309 return sharedContents; 310 } 311 catch (RepositoryException e) 312 { 313 throw new AmetysRepositoryException(e); 314 } 315 } 316 317 /** 318 * Remove the list of shared contents created from the given content. 319 * @param content the content of which to remove referencing shared content references. 320 */ 321 public void removeSharedContentReferences(Content content) 322 { 323 try 324 { 325 // Get shared contents which reference the content. 326 Set<SharedContent> sharedContents = getSharedContents(content); 327 for (SharedContent sharedContent : sharedContents) 328 { 329 if (sharedContent instanceof JCRAmetysObject) 330 { 331 Node node = ((JCRAmetysObject) sharedContent).getNode(); 332 node.getProperty(DefaultSharedContent.INITIAL_CONTENT_PROPERTY).remove(); 333 } 334 } 335 } 336 catch (RepositoryException e) 337 { 338 throw new AmetysRepositoryException(e); 339 } 340 } 341 342 /** 343 * Switch all shared contents created from the given content into default contents 344 * @param content the initial content with shared content references. 345 */ 346 public void switchSharedContentReferences (Content content) 347 { 348 // Get shared contents which reference the content. 349 Set<SharedContent> sharedContents = getSharedContents(content); 350 351 // Store migrated contents per site 352 Map<String, Content> migratedContents = new HashMap<>(); 353 354 for (SharedContent sharedContent : sharedContents) 355 { 356 String siteName = sharedContent.getSiteName(); 357 String sharedContentName = sharedContent.getName(); 358 String sharedContentId = sharedContent.getId(); 359 String contentName = StringUtils.removeEnd(sharedContentName, "-shared"); 360 361 ModifiableTraversableAmetysObject parent = sharedContent.getParent(); 362 363 // Prepare deletion of shared content 364 Map<String, Object> sharedEventParams = new HashMap<>(); 365 sharedEventParams.put(ObservationConstants.ARGS_CONTENT, sharedContent); 366 sharedEventParams.put(ObservationConstants.ARGS_CONTENT_NAME, sharedContentName); 367 sharedEventParams.put(ObservationConstants.ARGS_CONTENT_ID, sharedContentId); 368 sharedEventParams.put(org.ametys.web.ObservationConstants.ARGS_SITE_NAME, siteName); 369 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), sharedEventParams)); 370 371 if (content instanceof DefaultWebContent) 372 { 373 Content cContent = migratedContents.get(siteName); 374 375 if (cContent == null) 376 { 377 // Do a full copy of initial content - only one per site 378 cContent = ((DefaultWebContent) content).copyTo(parent, contentName); 379 if (cContent instanceof ModifiableWebContent modifiableWebContent) 380 { 381 ((ModifiableWebContent) cContent).setSiteName(siteName); 382 _copySiteComponent.updateSharedContent((DefaultWebContent) content, modifiableWebContent, false); 383 384 modifiableWebContent.saveChanges(); 385 386 if (modifiableWebContent instanceof VersionableAmetysObject versionableWebContent) 387 { 388 versionableWebContent.checkpoint(); 389 } 390 } 391 } 392 393 // Update referenced zone items 394 Set<SitemapElement> refPages = new HashSet<>(); 395 Collection<ZoneItem> refZoneItems = sharedContent.getReferencingZoneItems(); 396 for (ZoneItem zoneItem : refZoneItems) 397 { 398 SitemapElement sitemapElement = zoneItem.getZone().getSitemapElement(); 399 400 if (zoneItem instanceof ModifiableZoneItem) 401 { 402 // Update the zone item with the copied content 403 ((ModifiableZoneItem) zoneItem).setContent(cContent); 404 405 Map<String, Object> eventParams = new HashMap<>(); 406 eventParams.put(org.ametys.web.ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement); 407 eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM, zoneItem); 408 eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId()); 409 eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_TYPE, ZoneType.CONTENT); 410 411 _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_ZONEITEM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 412 } 413 414 refPages.add(sitemapElement); 415 } 416 417 for (SitemapElement sitemapElement : refPages) 418 { 419 Map<String, Object> eventParams = new HashMap<>(); 420 eventParams.put(ObservationConstants.ARGS_CONTENT, cContent); 421 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, cContent.getId()); 422 eventParams.put(org.ametys.web.ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement); 423 424 String eventId = migratedContents.containsKey(siteName) ? ObservationConstants.EVENT_CONTENT_MODIFIED : ObservationConstants.EVENT_CONTENT_ADDED; 425 _observationManager.notify(new Event(eventId, _currentUserProvider.getUser(), eventParams)); 426 } 427 428 migratedContents.put(siteName, cContent); 429 } 430 431 // Remove the shared content 432 sharedContent.remove(); 433 parent.saveChanges(); 434 435 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), sharedEventParams)); 436 } 437 } 438 439 /** 440 * Create a shared content in the given contents root. 441 * @param desiredContentName the desired content name. 442 * @param contentsNode the contents root. 443 * @return the created content. 444 */ 445 protected DefaultSharedContent createContent(String desiredContentName, ModifiableTraversableAmetysObject contentsNode) 446 { 447 DefaultSharedContent content = null; 448 449 String contentName = NameHelper.filterName(desiredContentName); 450 int errorCount = 0; 451 do 452 { 453 if (errorCount != 0) 454 { 455 contentName = NameHelper.filterName(desiredContentName + " " + (errorCount + 1)); 456 } 457 try 458 { 459 content = contentsNode.createChild(contentName, DefaultSharedContentFactory.SHARED_CONTENT_NODETYPE); 460 } 461 catch (RepositoryIntegrityViolationException e) 462 { 463 // Content name is already used 464 errorCount++; 465 } 466 } 467 while (content == null); 468 469 return content; 470 } 471 472 /** 473 * Validate a shared content. 474 * @param content the content to validate. 475 */ 476 protected void internalValidateContent(DefaultSharedContent content) 477 { 478 ZonedDateTime validationDate = ZonedDateTime.now(); 479 480 boolean isValid = Arrays.asList(content.getAllLabels()).contains(CmsConstants.LIVE_LABEL); 481 if (!isValid) 482 { 483 content.setLastMajorValidationDate(validationDate); 484 } 485 486 content.setLastValidationDate(validationDate); 487 if (content.getFirstValidationDate() == null) 488 { 489 content.setFirstValidationDate(validationDate); 490 } 491 492 content.saveChanges(); 493 494 content.checkpoint(); 495 content.addLabel(CmsConstants.LIVE_LABEL, true); 496 } 497 498 /** 499 * Unpublish a shared content. 500 * @param content the content to unpublish. 501 */ 502 protected void internalUnpublishContent(DefaultSharedContent content) 503 { 504 if (ArrayUtils.contains(content.getAllLabels(), CmsConstants.LIVE_LABEL)) 505 { 506 content.removeLabel(CmsConstants.LIVE_LABEL); 507 } 508 509 content.saveChanges(); 510 } 511 512 /** 513 * Remove all data of a model aware data holder. 514 * @param dataHolder the model aware data holder to empty. 515 */ 516 protected void removeAllModelAwareData(ModifiableModelAwareDataHolder dataHolder) 517 { 518 for (String dataName : dataHolder.getDataNames()) 519 { 520 ModelItem modelItem = dataHolder.getDefinition(dataName); 521 if (!(modelItem instanceof ElementDefinition) || ((ElementDefinition) modelItem).isEditable()) 522 { 523 dataHolder.removeValue(dataName); 524 } 525 } 526 } 527 528 /** 529 * Remove all data of a model less data holder. 530 * @param dataHolder the model less data holder to empty. 531 */ 532 protected void removeAllModelLessData(ModifiableModelLessDataHolder dataHolder) 533 { 534 dataHolder.getDataNames() 535 .stream() 536 .forEach(dataHolder::removeValue); 537 } 538 539}