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