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