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