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