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