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) content.getRootAttachments()).remove(); 196 197 ((CopiableAmetysObject) originalRootAttachments).copyTo(content, originalRootAttachments.getName()); 198 } 199 200 _copySiteComponent.updateSharedContent(originalWebContent, content); 201 } 202 203 // The "site" metadata has been copied: revert it to the real value. 204 content.setSiteName(content.getSite().getName()); 205 206 content.saveChanges(); 207 } 208 } 209 finally 210 { 211 originalContent.switchToRevision(currentRevision); 212 } 213 } 214 215 /** 216 * Validate a shared content. 217 * @param content the content to validate. 218 */ 219 public void validateContent(DefaultSharedContent content) 220 { 221 internalValidateContent(content); 222 223 UserIdentity user = _currentUserProvider.getUser(); 224 225 // Notify observers that the content has been validated. 226 Map<String, Object> eventParams = new HashMap<>(); 227 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 228 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 229 230 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_VALIDATED, user, eventParams)); 231 232 } 233 234 /** 235 * Invalidate a shared content. 236 * @param content the content to invalidate. 237 */ 238 public void invalidateSharedContent(DefaultSharedContent content) 239 { 240 internalUnpublishContent(content); 241 242 UserIdentity user = _currentUserProvider.getUser(); 243 244 // Notify observers that the content has been unpublished. 245 Map<String, Object> eventParams = new HashMap<>(); 246 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 247 eventParams.put(org.ametys.web.ObservationConstants.ARGS_SITE_NAME, content.getSiteName()); 248 249 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_UNTAG_LIVE, user, eventParams)); 250 } 251 252 /** 253 * Test if there are shared contents created from the given content. 254 * @param content the content to test. 255 * @return true if at least one shared content was created from the given content, false otherwise. 256 */ 257 public boolean hasSharedContents(Content content) 258 { 259 try 260 { 261 if (content instanceof DefaultContent) 262 { 263 DefaultContent originalContent = (DefaultContent) content; 264 PropertyIterator references = originalContent.getNode().getReferences(DefaultSharedContent.INITIAL_CONTENT_PROPERTY); 265 266 return references.hasNext(); 267 } 268 269 return false; 270 } 271 catch (RepositoryException e) 272 { 273 throw new AmetysRepositoryException(e); 274 } 275 } 276 277 /** 278 * Get the list of shared contents created from the given content. 279 * @param content the content of which to get referencing shared contents. 280 * @return the shared contents created from the given content. 281 */ 282 public Set<SharedContent> getSharedContents(Content content) 283 { 284 try 285 { 286 Set<SharedContent> sharedContents = new HashSet<>(); 287 288 if (content instanceof DefaultContent) 289 { 290 // Resolve the content to switch its revision without impacting the original object. 291 DefaultContent originalContent = (DefaultContent) content; 292 PropertyIterator references = originalContent.getNode().getReferences(DefaultSharedContent.INITIAL_CONTENT_PROPERTY); 293 while (references.hasNext()) 294 { 295 Node referer = references.nextProperty().getParent(); 296 DefaultSharedContent sharedContent = _resolver.resolve(referer, true); 297 if (sharedContent != null) 298 { 299 sharedContents.add(sharedContent); 300 } 301 } 302 } 303 304 return sharedContents; 305 } 306 catch (RepositoryException e) 307 { 308 throw new AmetysRepositoryException(e); 309 } 310 } 311 312 /** 313 * Remove the list of shared contents created from the given content. 314 * @param content the content of which to remove referencing shared content references. 315 */ 316 public void removeSharedContentReferences(Content content) 317 { 318 try 319 { 320 // Get shared contents which reference the content. 321 Set<SharedContent> sharedContents = getSharedContents(content); 322 for (SharedContent sharedContent : sharedContents) 323 { 324 if (sharedContent instanceof JCRAmetysObject) 325 { 326 Node node = ((JCRAmetysObject) sharedContent).getNode(); 327 node.getProperty(DefaultSharedContent.INITIAL_CONTENT_PROPERTY).remove(); 328 } 329 } 330 } 331 catch (RepositoryException e) 332 { 333 throw new AmetysRepositoryException(e); 334 } 335 } 336 337 /** 338 * Switch all shared contents created from the given content into default contents 339 * @param content the initial content with shared content references. 340 */ 341 public void switchSharedContentReferences (Content content) 342 { 343 // Get shared contents which reference the content. 344 Set<SharedContent> sharedContents = getSharedContents(content); 345 346 // Store migrated contents per site 347 Map<String, Content> migratedContents = new HashMap<>(); 348 349 for (SharedContent sharedContent : sharedContents) 350 { 351 String siteName = sharedContent.getSiteName(); 352 String sharedContentName = sharedContent.getName(); 353 String sharedContentId = sharedContent.getId(); 354 String contentName = StringUtils.removeEnd(sharedContentName, "-shared"); 355 356 ModifiableTraversableAmetysObject parent = sharedContent.getParent(); 357 358 // Prepare deletion of shared content 359 Map<String, Object> sharedEventParams = new HashMap<>(); 360 sharedEventParams.put(ObservationConstants.ARGS_CONTENT, sharedContent); 361 sharedEventParams.put(ObservationConstants.ARGS_CONTENT_NAME, sharedContentName); 362 sharedEventParams.put(ObservationConstants.ARGS_CONTENT_ID, sharedContentId); 363 sharedEventParams.put(org.ametys.web.ObservationConstants.ARGS_SITE_NAME, siteName); 364 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), sharedEventParams)); 365 366 if (content instanceof DefaultWebContent) 367 { 368 Content cContent = migratedContents.get(siteName); 369 370 if (cContent == null) 371 { 372 // Do a full copy of initial content - only one per site 373 cContent = ((DefaultWebContent) content).copyTo(parent, contentName); 374 if (cContent instanceof ModifiableWebContent) 375 { 376 ((ModifiableWebContent) cContent).setSiteName(siteName); 377 _copySiteComponent.updateSharedContent((DefaultWebContent) content, (ModifiableWebContent) cContent, false); 378 } 379 } 380 381 // Update referenced zone items 382 Set<Page> refPages = new HashSet<>(); 383 Collection<ZoneItem> refZoneItems = sharedContent.getReferencingZoneItems(); 384 for (ZoneItem zoneItem : refZoneItems) 385 { 386 Page page = zoneItem.getZone().getPage(); 387 388 if (zoneItem instanceof ModifiableZoneItem) 389 { 390 // Update the zone item with the copied content 391 ((ModifiableZoneItem) zoneItem).setContent(cContent); 392 393 Map<String, Object> eventParams = new HashMap<>(); 394 eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page); 395 eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM, zoneItem); 396 eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_ITEM_ID, zoneItem.getId()); 397 eventParams.put(org.ametys.web.ObservationConstants.ARGS_ZONE_TYPE, ZoneType.CONTENT); 398 399 _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_ZONEITEM_MODIFIED, _currentUserProvider.getUser(), eventParams)); 400 } 401 402 refPages.add(page); 403 } 404 405 for (Page page : refPages) 406 { 407 Map<String, Object> eventParams = new HashMap<>(); 408 eventParams.put(ObservationConstants.ARGS_CONTENT, cContent); 409 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, cContent.getId()); 410 eventParams.put(org.ametys.web.ObservationConstants.ARGS_PAGE, page); 411 412 String eventId = migratedContents.containsKey(siteName) ? ObservationConstants.EVENT_CONTENT_MODIFIED : ObservationConstants.EVENT_CONTENT_ADDED; 413 _observationManager.notify(new Event(eventId, _currentUserProvider.getUser(), eventParams)); 414 } 415 416 migratedContents.put(siteName, cContent); 417 } 418 419 // Remove the shared content 420 sharedContent.remove(); 421 parent.saveChanges(); 422 423 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), sharedEventParams)); 424 } 425 } 426 427 /** 428 * Create a shared content in the given contents root. 429 * @param desiredContentName the desired content name. 430 * @param contentsNode the contents root. 431 * @return the created content. 432 */ 433 protected DefaultSharedContent createContent(String desiredContentName, ModifiableTraversableAmetysObject contentsNode) 434 { 435 DefaultSharedContent content = null; 436 437 String contentName = FilterNameHelper.filterName(desiredContentName); 438 int errorCount = 0; 439 do 440 { 441 if (errorCount != 0) 442 { 443 contentName = FilterNameHelper.filterName(desiredContentName + " " + (errorCount + 1)); 444 } 445 try 446 { 447 content = contentsNode.createChild(contentName, DefaultSharedContentFactory.SHARED_CONTENT_NODETYPE); 448 } 449 catch (RepositoryIntegrityViolationException e) 450 { 451 // Content name is already used 452 errorCount++; 453 } 454 } 455 while (content == null); 456 457 return content; 458 } 459 460 /** 461 * Validate a shared content. 462 * @param content the content to validate. 463 */ 464 protected void internalValidateContent(DefaultSharedContent content) 465 { 466 Date validationDate = new Date(); 467 468 boolean isValid = Arrays.asList(content.getAllLabels()).contains(CmsConstants.LIVE_LABEL); 469 if (!isValid) 470 { 471 content.setLastMajorValidationDate(validationDate); 472 } 473 474 content.setLastValidationDate(validationDate); 475 if (content.getFirstValidationDate() == null) 476 { 477 content.setFirstValidationDate(validationDate); 478 } 479 480 content.saveChanges(); 481 482 content.checkpoint(); 483 content.addLabel(CmsConstants.LIVE_LABEL, true); 484 } 485 486 /** 487 * Unpublish a shared content. 488 * @param content the content to unpublish. 489 */ 490 protected void internalUnpublishContent(DefaultSharedContent content) 491 { 492 if (ArrayUtils.contains(content.getAllLabels(), CmsConstants.LIVE_LABEL)) 493 { 494 content.removeLabel(CmsConstants.LIVE_LABEL); 495 } 496 497 content.saveChanges(); 498 } 499 500 /** 501 * Remove all children of a {@link ModifiableTraversableAmetysObject}. 502 * @param rootObject the traversable ametys object to empty. 503 */ 504 protected void removeAllChildren(ModifiableTraversableAmetysObject rootObject) 505 { 506 for (AmetysObject object : rootObject.getChildren()) 507 { 508 if (object instanceof RemovableAmetysObject) 509 { 510 ((RemovableAmetysObject) object).remove(); 511 } 512 } 513 514 rootObject.saveChanges(); 515 } 516 517 /** 518 * Remove all data of a data holder. 519 * @param dataHolder the data holder to empty. 520 */ 521 protected void removeAllData(ModifiableDataHolder dataHolder) 522 { 523 for (String dataName : dataHolder.getDataNames()) 524 { 525 dataHolder.removeValue(dataName); 526 } 527 } 528 529}