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.runtime.model.ElementDefinition; 058import org.ametys.runtime.model.ModelItem; 059import org.ametys.web.filter.SharedContentsHelper; 060import org.ametys.web.repository.content.ModifiableWebContent; 061import org.ametys.web.repository.content.SharedContent; 062import org.ametys.web.repository.content.WebContent; 063import org.ametys.web.repository.content.jcr.DefaultSharedContent; 064import org.ametys.web.repository.content.jcr.DefaultSharedContentFactory; 065import org.ametys.web.repository.content.jcr.DefaultWebContent; 066import org.ametys.web.repository.page.CopySiteComponent; 067import org.ametys.web.repository.page.ModifiableZoneItem; 068import org.ametys.web.repository.page.SitemapElement; 069import org.ametys.web.repository.page.ZoneItem; 070import org.ametys.web.repository.page.ZoneItem.ZoneType; 071import org.ametys.web.repository.site.Site; 072 073/** 074 * Component which provides methods to manage shared contents (creation, validation, and so on). 075 */ 076public class SharedContentManager extends AbstractLogEnabled implements Serviceable, Component 077{ 078 079 /** The avalon role. */ 080 public static final String ROLE = SharedContentManager.class.getName(); 081 082 /** The ametys object resolver. */ 083 protected AmetysObjectResolver _resolver; 084 085 /** The observation manager. */ 086 protected ObservationManager _observationManager; 087 088 /** The current user provider. */ 089 protected CurrentUserProvider _currentUserProvider; 090 091 /** The site copy component. */ 092 protected CopySiteComponent _copySiteComponent; 093 094 @Override 095 public void service(ServiceManager serviceManager) throws ServiceException 096 { 097 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 098 _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE); 099 _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE); 100 _copySiteComponent = (CopySiteComponent) serviceManager.lookup(CopySiteComponent.ROLE); 101 } 102 103 /** 104 * Create a {@link SharedContent} from an original content. 105 * @param site the site in which to create the shared content. 106 * @param originalContent the original content. 107 * @return the created shared content. 108 */ 109 public DefaultSharedContent createSharedContent(Site site, DefaultContent originalContent) 110 { 111 try 112 { 113 ModifiableTraversableAmetysObject contentRoot = site.getRootContents(); 114 115 // Get a reference on the original node. 116 Node originalNode = originalContent.getNode(); 117 118 String copyName = originalContent.getName() + "-shared"; 119 120 DefaultSharedContent content = createContent(copyName, contentRoot); 121 122 // Store the reference to the original content. 123 content.getNode().setProperty(DefaultSharedContent.INITIAL_CONTENT_PROPERTY, originalNode); 124 125 String originalLanguage = originalContent.getLanguage(); 126 if (originalLanguage != null) 127 { 128 content.setLanguage(originalContent.getLanguage()); 129 } 130 131 // Copy standard properties. 132 content.setTypes(originalContent.getTypes()); 133 // Copy title needs model -> types must to be set before 134 SharedContentsHelper.copyTitle(originalContent, content); 135 136 content.setCreator(originalContent.getCreator()); 137 content.setCreationDate(originalContent.getCreationDate()); 138 content.setLastContributor(originalContent.getLastContributor()); 139 content.setLastModified(originalContent.getLastModified()); 140 141 content.saveChanges(); 142 143 // Copy the content data. 144 copyContentData(originalContent, content); 145 146 // Create the first version 147 content.checkpoint(); 148 149 // Validate the shared content if the original content is validated. 150 if (ArrayUtils.contains(originalContent.getAllLabels(), CmsConstants.LIVE_LABEL)) 151 { 152 validateContent(content); 153 } 154 155 return content; 156 } 157 catch (RepositoryException e) 158 { 159 throw new AmetysRepositoryException(e); 160 } 161 } 162 163 /** 164 * Copy the data of a content into a shared content. 165 * @param originalContent the content to copy data from. 166 * @param content the content to copy data to. 167 * @throws AmetysRepositoryException if an error occurs during copy 168 */ 169 public void copyContentData(DefaultContent originalContent, DefaultSharedContent content) throws AmetysRepositoryException 170 { 171 String currentRevision = originalContent.getRevision(); 172 173 try 174 { 175 if (ArrayUtils.contains(originalContent.getAllLabels(), CmsConstants.LIVE_LABEL)) 176 { 177 // Switch the content to its live revision. 178 originalContent.switchToLabel(CmsConstants.LIVE_LABEL); 179 180 // Copy metadata. 181 removeAllModelAwareData(content); 182 originalContent.copyTo(content); 183 184 // Copy unversioned metadata. 185 removeAllModelLessData(content.getUnversionedDataHolder()); 186 originalContent.getUnversionedDataHolder().copyTo(content.getUnversionedDataHolder()); 187 188 // Copy attachments. 189 if (originalContent instanceof WebContent) 190 { 191 WebContent originalWebContent = (WebContent) originalContent; 192 ResourceCollection originalRootAttachments = originalWebContent.getRootAttachments(); 193 if (originalRootAttachments != null && originalRootAttachments instanceof CopiableAmetysObject) 194 { 195 // Remove the attachment root before copying. 196 ModifiableResourceCollection rootAttachments = (ModifiableResourceCollection) content.getRootAttachments(); 197 if (rootAttachments != null) // root attachments can be missing if the content is an (unmodifiable) old version 198 { 199 rootAttachments.remove(); 200 ((CopiableAmetysObject) originalRootAttachments).copyTo(content, originalRootAttachments.getName()); 201 } 202 203 } 204 205 _copySiteComponent.updateSharedContent(originalWebContent, content); 206 } 207 208 // The "site" metadata has been copied: revert it to the real value. 209 content.setSiteName(content.getSite().getName()); 210 211 content.saveChanges(); 212 } 213 } 214 finally 215 { 216 originalContent.switchToRevision(currentRevision); 217 } 218 } 219 220 /** 221 * Validate a shared content. 222 * @param content the content to validate. 223 */ 224 public void validateContent(DefaultSharedContent content) 225 { 226 internalValidateContent(content); 227 228 UserIdentity user = _currentUserProvider.getUser(); 229 230 // Notify observers that the content has been validated. 231 Map<String, Object> eventParams = new HashMap<>(); 232 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 233 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 234 235 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_VALIDATED, user, eventParams)); 236 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) 380 { 381 ((ModifiableWebContent) cContent).setSiteName(siteName); 382 _copySiteComponent.updateSharedContent((DefaultWebContent) content, (ModifiableWebContent) cContent, false); 383 } 384 } 385 386 // Update referenced zone items 387 Set<SitemapElement> refPages = new HashSet<>(); 388 Collection<ZoneItem> refZoneItems = sharedContent.getReferencingZoneItems(); 389 for (ZoneItem zoneItem : refZoneItems) 390 { 391 SitemapElement sitemapElement = zoneItem.getZone().getSitemapElement(); 392 393 if (zoneItem instanceof ModifiableZoneItem) 394 { 395 // Update the zone item with the copied content 396 ((ModifiableZoneItem) zoneItem).setContent(cContent); 397 398 Map<String, Object> eventParams = new HashMap<>(); 399 eventParams.put(org.ametys.web.ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement); 400 eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM, zoneItem); 401 eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId()); 402 eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_TYPE, ZoneType.CONTENT); 403 404 _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_ZONEITEM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 405 } 406 407 refPages.add(sitemapElement); 408 } 409 410 for (SitemapElement sitemapElement : refPages) 411 { 412 Map<String, Object> eventParams = new HashMap<>(); 413 eventParams.put(ObservationConstants.ARGS_CONTENT, cContent); 414 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, cContent.getId()); 415 eventParams.put(org.ametys.web.ObservationConstants.ARGS_SITEMAP_ELEMENT, sitemapElement); 416 417 String eventId = migratedContents.containsKey(siteName) ? ObservationConstants.EVENT_CONTENT_MODIFIED : ObservationConstants.EVENT_CONTENT_ADDED; 418 _observationManager.notify(new Event(eventId, _currentUserProvider.getUser(), eventParams)); 419 } 420 421 migratedContents.put(siteName, cContent); 422 } 423 424 // Remove the shared content 425 sharedContent.remove(); 426 parent.saveChanges(); 427 428 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), sharedEventParams)); 429 } 430 } 431 432 /** 433 * Create a shared content in the given contents root. 434 * @param desiredContentName the desired content name. 435 * @param contentsNode the contents root. 436 * @return the created content. 437 */ 438 protected DefaultSharedContent createContent(String desiredContentName, ModifiableTraversableAmetysObject contentsNode) 439 { 440 DefaultSharedContent content = null; 441 442 String contentName = NameHelper.filterName(desiredContentName); 443 int errorCount = 0; 444 do 445 { 446 if (errorCount != 0) 447 { 448 contentName = NameHelper.filterName(desiredContentName + " " + (errorCount + 1)); 449 } 450 try 451 { 452 content = contentsNode.createChild(contentName, DefaultSharedContentFactory.SHARED_CONTENT_NODETYPE); 453 } 454 catch (RepositoryIntegrityViolationException e) 455 { 456 // Content name is already used 457 errorCount++; 458 } 459 } 460 while (content == null); 461 462 return content; 463 } 464 465 /** 466 * Validate a shared content. 467 * @param content the content to validate. 468 */ 469 protected void internalValidateContent(DefaultSharedContent content) 470 { 471 ZonedDateTime validationDate = ZonedDateTime.now(); 472 473 boolean isValid = Arrays.asList(content.getAllLabels()).contains(CmsConstants.LIVE_LABEL); 474 if (!isValid) 475 { 476 content.setLastMajorValidationDate(validationDate); 477 } 478 479 content.setLastValidationDate(validationDate); 480 if (content.getFirstValidationDate() == null) 481 { 482 content.setFirstValidationDate(validationDate); 483 } 484 485 content.saveChanges(); 486 487 content.checkpoint(); 488 content.addLabel(CmsConstants.LIVE_LABEL, true); 489 } 490 491 /** 492 * Unpublish a shared content. 493 * @param content the content to unpublish. 494 */ 495 protected void internalUnpublishContent(DefaultSharedContent content) 496 { 497 if (ArrayUtils.contains(content.getAllLabels(), CmsConstants.LIVE_LABEL)) 498 { 499 content.removeLabel(CmsConstants.LIVE_LABEL); 500 } 501 502 content.saveChanges(); 503 } 504 505 /** 506 * Remove all data of a model aware data holder. 507 * @param dataHolder the model aware data holder to empty. 508 */ 509 protected void removeAllModelAwareData(ModifiableModelAwareDataHolder dataHolder) 510 { 511 for (String dataName : dataHolder.getDataNames()) 512 { 513 ModelItem modelItem = dataHolder.getDefinition(dataName); 514 if (!(modelItem instanceof ElementDefinition) || ((ElementDefinition) modelItem).isEditable()) 515 { 516 dataHolder.removeValue(dataName); 517 } 518 } 519 } 520 521 /** 522 * Remove all data of a model less data holder. 523 * @param dataHolder the model less data holder to empty. 524 */ 525 protected void removeAllModelLessData(ModifiableModelLessDataHolder dataHolder) 526 { 527 dataHolder.getDataNames() 528 .stream() 529 .forEach(dataHolder::removeValue); 530 } 531 532}