001/* 002 * Copyright 2011 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.page; 017 018import java.io.InputStream; 019import java.io.OutputStream; 020import java.util.HashMap; 021import java.util.Map; 022import java.util.Properties; 023import java.util.Set; 024 025import javax.jcr.Node; 026import javax.jcr.RepositoryException; 027import javax.xml.transform.OutputKeys; 028import javax.xml.transform.TransformerFactory; 029import javax.xml.transform.sax.SAXTransformerFactory; 030import javax.xml.transform.sax.TransformerHandler; 031import javax.xml.transform.stream.StreamResult; 032 033import org.apache.avalon.framework.component.Component; 034import org.apache.avalon.framework.logger.AbstractLogEnabled; 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.avalon.framework.service.Serviceable; 038import org.apache.excalibur.xml.sax.SAXParser; 039import org.apache.xml.serializer.OutputPropertiesFactory; 040import org.xml.sax.ContentHandler; 041import org.xml.sax.InputSource; 042 043import org.ametys.cms.content.references.OutgoingReferences; 044import org.ametys.cms.content.references.OutgoingReferencesExtractor; 045import org.ametys.cms.contenttype.ContentType; 046import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 047import org.ametys.cms.contenttype.ContentTypesHelper; 048import org.ametys.cms.contenttype.MetadataDefinition; 049import org.ametys.cms.contenttype.RichTextUpdater; 050import org.ametys.cms.repository.Content; 051import org.ametys.cms.repository.ModifiableContent; 052import org.ametys.cms.repository.WorkflowAwareContent; 053import org.ametys.cms.repository.WorkflowAwareContentHelper; 054import org.ametys.plugins.repository.AmetysObject; 055import org.ametys.plugins.repository.AmetysObjectIterable; 056import org.ametys.plugins.repository.AmetysObjectResolver; 057import org.ametys.plugins.repository.AmetysRepositoryException; 058import org.ametys.plugins.repository.ModifiableAmetysObject; 059import org.ametys.plugins.repository.RepositoryConstants; 060import org.ametys.plugins.repository.TraversableAmetysObject; 061import org.ametys.plugins.repository.UnknownAmetysObjectException; 062import org.ametys.plugins.repository.metadata.CompositeMetadata; 063import org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType; 064import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata; 065import org.ametys.plugins.repository.metadata.ModifiableRichText; 066import org.ametys.plugins.repository.version.VersionableAmetysObject; 067import org.ametys.plugins.workflow.support.WorkflowProvider; 068import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 069import org.ametys.web.repository.ModifiableSiteAwareAmetysObject; 070import org.ametys.web.repository.content.WebContent; 071import org.ametys.web.repository.page.ZoneItem.ZoneType; 072import org.ametys.web.repository.site.Site; 073import org.ametys.web.repository.sitemap.Sitemap; 074import org.ametys.web.site.CopyUpdaterExtensionPoint; 075 076import com.opensymphony.workflow.spi.Step; 077 078/** 079 * Component for copying site or pages 080 * 081 */ 082public class CopySiteComponent extends AbstractLogEnabled implements Component, Serviceable 083{ 084 /** Avalon Role */ 085 public static final String ROLE = CopySiteComponent.class.getName(); 086 087 private AmetysObjectResolver _resolver; 088 private WorkflowProvider _workflowProvider; 089 private ContentTypeExtensionPoint _cTypeEP; 090 private ContentTypesHelper _contentTypesHelper; 091 092 private SAXParser _saxParser; 093 private CopyUpdaterExtensionPoint _updaterEP; 094 private OutgoingReferencesExtractor _outgoingReferencesExtractor; 095 096 @Override 097 public void service(ServiceManager serviceManager) throws ServiceException 098 { 099 _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE); 100 _contentTypesHelper = (ContentTypesHelper) serviceManager.lookup(ContentTypesHelper.ROLE); 101 _workflowProvider = (WorkflowProvider) serviceManager.lookup(WorkflowProvider.ROLE); 102 _cTypeEP = (ContentTypeExtensionPoint) serviceManager.lookup(ContentTypeExtensionPoint.ROLE); 103 _saxParser = (SAXParser) serviceManager.lookup(SAXParser.ROLE); 104 _updaterEP = (CopyUpdaterExtensionPoint) serviceManager.lookup(CopyUpdaterExtensionPoint.ROLE); 105 _outgoingReferencesExtractor = (OutgoingReferencesExtractor) serviceManager.lookup(OutgoingReferencesExtractor.ROLE); 106 } 107 108 /** 109 * This methods must be used after calling <code>copyTo</code> on a Page. 110 * Its updates references to ametys objects for metadata of new created pages and contents 111 * @param originalPage the original page 112 * @param createdPage the created page after a copy 113 * @throws AmetysRepositoryException if an error occurs 114 */ 115 public void updateReferencesAfterCopy (Page originalPage, Page createdPage) throws AmetysRepositoryException 116 { 117 // Update references to ametys object on metadata 118 _updateReferencesToAmetysObjects (createdPage.getMetadataHolder(), originalPage, createdPage); 119 120 for (Zone zone : createdPage.getZones()) 121 { 122 _updateReferencesToAmetysObjects (zone.getMetadataHolder(), originalPage, createdPage); 123 124 for (ZoneItem zoneItem : zone.getZoneItems()) 125 { 126 _updateReferencesToAmetysObjects (zoneItem.getMetadataHolder(), originalPage, createdPage); 127 128 if (zoneItem.getType().equals(ZoneType.SERVICE)) 129 { 130 _updateReferencesToAmetysObjects (zoneItem.getServiceParameters(), originalPage, createdPage); 131 } 132 else 133 { 134 Content content = zoneItem.getContent(); 135 _updateReferencesToAmetysObjects (content.getMetadataHolder(), originalPage, createdPage); 136 } 137 } 138 } 139 140 // Browse child pages 141 for (Page childPage : createdPage.getChildrenPages()) 142 { 143 updateReferencesAfterCopy ((Page) originalPage.getChild(childPage.getName()), childPage); 144 } 145 } 146 147 /** 148 * This method must be used after calling <code>copyTo</code> on a Site. 149 * Its updates contents and pages after a site copy 150 * @param originalSite the original site 151 * @param createdSite the created site after copy 152 * @throws AmetysRepositoryException if an error occurs 153 */ 154 public void updateSiteAfterCopy (Site originalSite, Site createdSite) throws AmetysRepositoryException 155 { 156 updateContentsAfterCopy (originalSite, createdSite); 157 updatePagesAfterCopy (originalSite, createdSite); 158 159 Set<String> ids = _updaterEP.getExtensionsIds(); 160 for (String id : ids) 161 { 162 _updaterEP.getExtension(id).updateSite(originalSite, createdSite); 163 } 164 } 165 166 /** 167 * This method re-initializes workflow, updates the site name for web content and updates references to ametys objects on metadata after a site copy 168 * @param initialSite the original site 169 * @param createdSite the created site after copy 170 * @throws AmetysRepositoryException if an error occurs 171 */ 172 public void updateContentsAfterCopy (Site initialSite, Site createdSite) throws AmetysRepositoryException 173 { 174 AmetysObjectIterable<Content> contents = createdSite.getContents(); 175 for (Content content : contents) 176 { 177 String relPath = content.getPath().substring(createdSite.getPath().length() + 1); 178 WebContent initialContent = initialSite.getChild(relPath); 179 180 try 181 { 182 // Re-init workflow 183 if (content instanceof WorkflowAwareContent) 184 { 185 _reinitWorkflow ((WorkflowAwareContent) content); 186 } 187 188 // Update site name 189 if (content instanceof ModifiableSiteAwareAmetysObject) 190 { 191 ((ModifiableSiteAwareAmetysObject) content).setSiteName(createdSite.getName()); 192 } 193 194 // Update references to ametys object on metadata 195 _updateReferencesToAmetysObjects (content.getMetadataHolder(), initialSite, createdSite); 196 197 // Update links in RichText 198 updateLinksInRichText (initialSite, createdSite, initialContent, content); 199 200 // Updaters 201 Set<String> ids = _updaterEP.getExtensionsIds(); 202 for (String id : ids) 203 { 204 _updaterEP.getExtension(id).updateContent(initialSite, createdSite, initialContent, content); 205 } 206 207 // save 208 if (content instanceof ModifiableAmetysObject) 209 { 210 ((ModifiableAmetysObject) content).saveChanges(); 211 } 212 213 // Creates the first version 214 if (content instanceof VersionableAmetysObject) 215 { 216 ((VersionableAmetysObject) content).checkpoint(); 217 } 218 } 219 catch (Exception e) 220 { 221 // Do not make the copy fail. 222 getLogger().warn("[Site copy] An error occured while updating content '" + content.getId() + " after copy from initial content '" + initialContent.getId() + "'", e); 223 } 224 } 225 226 if (createdSite.needsSave()) 227 { 228 createdSite.saveChanges(); 229 } 230 } 231 232 /** 233 * Updates references all references in a content to another one. 234 * @param initialContent the initial content. 235 * @param destContent the destination content. 236 */ 237 public void updateSharedContent(WebContent initialContent, WebContent destContent) 238 { 239 updateSharedContent(initialContent, destContent, true); 240 } 241 242 /** 243 * Updates references all references in a content to another one. 244 * @param initialContent the initial content. 245 * @param destContent the destination content. 246 * @param reinitWorkflow set to 'true' to reinitialize the workflow 247 */ 248 public void updateSharedContent(WebContent initialContent, WebContent destContent, boolean reinitWorkflow) 249 { 250 Site initialSite = initialContent.getSite(); 251 Site createdSite = destContent.getSite(); 252 253 // Re-init workflow 254 if (reinitWorkflow && destContent instanceof WorkflowAwareContent) 255 { 256 _reinitWorkflow((WorkflowAwareContent) destContent); 257 } 258 259 // Update references to ametys object on metadata 260 _updateReferencesToAmetysObjects(destContent.getMetadataHolder(), initialContent, destContent); 261 262 // Update links in RichText 263 updateLinksInRichText(initialContent, destContent, initialContent, destContent); 264 265 // Updaters 266 Set<String> ids = _updaterEP.getExtensionsIds(); 267 for (String id : ids) 268 { 269 _updaterEP.getExtension(id).updateContent(initialSite, createdSite, initialContent, destContent); 270 } 271 272 // save 273 if (destContent instanceof ModifiableAmetysObject) 274 { 275 ((ModifiableAmetysObject) destContent).saveChanges(); 276 } 277 278 // Creates the first version 279 if (destContent instanceof VersionableAmetysObject) 280 { 281 ((VersionableAmetysObject) destContent).checkpoint(); 282 } 283 } 284 285 /** 286 * This method analyzes content rich texts and update links if necessary 287 * @param initialAO The initial object copied 288 * @param createdAO The target object 289 * @param initialContent The initial content 290 * @param createdContent The created content after copy to update 291 * @throws AmetysRepositoryException if an error occurs 292 */ 293 public void updateLinksInRichText (TraversableAmetysObject initialAO, TraversableAmetysObject createdAO, Content initialContent, Content createdContent) throws AmetysRepositoryException 294 { 295 try 296 { 297 Map<String, Object> params = new HashMap<>(); 298 params.put("initialContent", initialContent); 299 params.put("createdContent", createdContent); 300 params.put("initialAO", initialAO); 301 params.put("createdAO", createdAO); 302 303 CompositeMetadata metadataHolder = createdContent.getMetadataHolder(); 304 String[] metadataNames = metadataHolder.getMetadataNames(); 305 for (String metadataName : metadataNames) 306 { 307 if (metadataHolder.hasMetadata(metadataName) && metadataHolder.getType(metadataName).equals(MetadataType.RICHTEXT)) 308 { 309 MetadataDefinition metadataDef = _contentTypesHelper.getMetadataDefinition(metadataName, createdContent); 310 311 ModifiableRichText richText = (ModifiableRichText) metadataHolder.getRichText(metadataName); 312 313 String referenceContentType = metadataDef.getReferenceContentType(); 314 ContentType contentType = _cTypeEP.getExtension(referenceContentType); 315 RichTextUpdater richTextUpdater = contentType.getRichTextUpdater(); 316 317 if (richTextUpdater != null) 318 { 319 try (InputStream is = richText.getInputStream(); OutputStream os = richText.getOutputStream()) 320 { 321 // create a transformer for saving sax into a file 322 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 323 324 // create the result where to write 325 StreamResult result = new StreamResult(os); 326 th.setResult(result); 327 328 // create the format of result 329 Properties format = new Properties(); 330 format.put(OutputKeys.METHOD, "xml"); 331 format.put(OutputKeys.INDENT, "yes"); 332 format.put(OutputKeys.ENCODING, "UTF-8"); 333 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 334 th.getTransformer().setOutputProperties(format); 335 336 ContentHandler richTextHandler = richTextUpdater.getContentHandler(th, th, params); 337 _saxParser.parse(new InputSource(is), richTextHandler); 338 } 339 } 340 341 } 342 } 343 344 // Outgoing references 345 if (createdContent instanceof ModifiableContent) 346 { 347 Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(createdContent); 348 ((ModifiableContent) createdContent).setOutgoingReferences(outgoingReferencesByPath); 349 350 } 351 } 352 catch (Exception e) 353 { 354 // Do not failed the copy 355 getLogger().warn("An error occured while updating links in RichText for content '" + createdContent.getId() + " after copy from initial content '" + initialContent.getId() + "'", e); 356 } 357 } 358 359 /** 360 * This method updates the site name of pages and updates references to ametys objects on page's metadata after a site copy 361 * @param originalSite the original site 362 * @param createdSite the created site after copy 363 * @throws AmetysRepositoryException if an error occurs 364 */ 365 public void updatePagesAfterCopy (Site originalSite, Site createdSite) throws AmetysRepositoryException 366 { 367 AmetysObjectIterable<Sitemap> sitemaps = createdSite.getSitemaps(); 368 369 for (Sitemap sitemap : sitemaps) 370 { 371 for (Page page : sitemap.getChildrenPages()) 372 { 373 _updatePageAfterCopy (originalSite, createdSite, page); 374 } 375 } 376 } 377 378 private void _updatePageAfterCopy (Site originalSite, Site createdSite, Page page) throws AmetysRepositoryException 379 { 380 try 381 { 382 // Update site name 383 if (page instanceof ModifiablePage) 384 { 385 ((ModifiablePage) page).setSiteName(createdSite.getName()); 386 } 387 388 // Update references to ametys object on metadata 389 _updateReferencesToAmetysObjects (page.getMetadataHolder(), originalSite, createdSite); 390 391 for (Zone zone : page.getZones()) 392 { 393 _updateReferencesToAmetysObjects (zone.getMetadataHolder(), originalSite, createdSite); 394 395 for (ZoneItem zoneItem : zone.getZoneItems()) 396 { 397 _updateReferencesToAmetysObjects (zoneItem.getMetadataHolder(), originalSite, createdSite); 398 399 if (zoneItem.getType().equals(ZoneType.SERVICE)) 400 { 401 _updateReferencesToAmetysObjects (zoneItem.getServiceParameters(), originalSite, createdSite); 402 } 403 else if (zoneItem.getType().equals(ZoneType.CONTENT) && zoneItem instanceof ModifiableZoneItem) 404 { 405 Content content = zoneItem.getContent(); 406 String path = content.getPath(); 407 408 String originalPath = originalSite.getPath(); 409 if (path.startsWith(originalPath)) 410 { 411 String relPath = path.substring(originalPath.length() + 1); 412 try 413 { 414 // Find symmetric object on copied sub-tree 415 Content child = createdSite.getChild(relPath); 416 ((ModifiableZoneItem) zoneItem).setContent(child); 417 } 418 catch (UnknownAmetysObjectException e) 419 { 420 // Nothing 421 } 422 } 423 } 424 } 425 } 426 } 427 catch (AmetysRepositoryException e) 428 { 429 // Do not failed the copy 430 getLogger().warn("An error occured while updating page '" + page.getId() + "' (" + page.getPathInSitemap() + ") after copy", e); 431 } 432 433 // Browse child pages 434 for (Page childPage : page.getChildrenPages()) 435 { 436 _updatePageAfterCopy (originalSite, createdSite, childPage); 437 } 438 439 // Updaters 440 Set<String> ids = _updaterEP.getExtensionsIds(); 441 for (String id : ids) 442 { 443 _updaterEP.getExtension(id).updatePage(originalSite, createdSite, page); 444 } 445 } 446 447 private void _updateReferencesToAmetysObjects (CompositeMetadata metadataHolder, TraversableAmetysObject originalAO, TraversableAmetysObject createdAO) 448 { 449 String[] metadataNames = metadataHolder.getMetadataNames(); 450 for (String metadataName : metadataNames) 451 { 452 if (MetadataType.COMPOSITE.equals(metadataHolder.getType(metadataName))) 453 { 454 _updateReferencesToAmetysObjects (metadataHolder.getCompositeMetadata(metadataName), originalAO, createdAO); 455 } 456 else if (_isAmetysObject(metadataHolder, metadataName)) 457 { 458 _updateReferenceToAmetysObject (metadataHolder, metadataName, originalAO, createdAO); 459 } 460 } 461 } 462 463 private void _updateReferenceToAmetysObject (CompositeMetadata metadataHolder, String metadataName, TraversableAmetysObject originalAO, TraversableAmetysObject createdAO) throws AmetysRepositoryException 464 { 465 if (metadataHolder instanceof ModifiableCompositeMetadata) 466 { 467 String id = metadataHolder.getString(metadataName); 468 469 AmetysObject ametysObject = _resolver.resolveById(id); 470 String path = ametysObject.getPath(); 471 472 String originalPath = originalAO.getPath(); 473 if (path.startsWith(originalPath + "/")) 474 { 475 String relPath = path.substring(originalPath.length() + 1); 476 try 477 { 478 // Find symmetric object on copied sub-tree 479 AmetysObject child = createdAO.getChild(relPath); 480 ((ModifiableCompositeMetadata) metadataHolder).setMetadata(metadataName, child.getId()); 481 } 482 catch (UnknownAmetysObjectException e) 483 { 484 getLogger().warn("Object of path " + relPath + " was not found on copied sub-tree " + createdAO.getPath(), e); 485 } 486 catch (AmetysRepositoryException e) 487 { 488 getLogger().error("Unable to retrieve object of path " + relPath + " on copied sub-tree " + createdAO.getPath(), e); 489 } 490 } 491 } 492 } 493 494 private boolean _isAmetysObject (CompositeMetadata metadataHolder, String metadataName) 495 { 496 try 497 { 498 if (metadataHolder.getType(metadataName).equals(MetadataType.STRING)) 499 { 500 if (metadataHolder.isMultiple(metadataName)) 501 { 502 String[] values = metadataHolder.getStringArray(metadataName); 503 for (String value : values) 504 { 505 if (_resolver.hasAmetysObjectForId(value)) 506 { 507 return true; 508 } 509 } 510 } 511 else 512 { 513 String value = metadataHolder.getString(metadataName); 514 if (_resolver.hasAmetysObjectForId(value)) 515 { 516 return true; 517 } 518 } 519 } 520 521 return false; 522 } 523 catch (AmetysRepositoryException e) 524 { 525 return false; 526 } 527 } 528 529 private void _reinitWorkflow (WorkflowAwareContent content) throws AmetysRepositoryException 530 { 531 try 532 { 533 long wId = content.getWorkflowId(); 534 535 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(content); 536 String workflowName = workflow.getWorkflowName(wId); 537 538 // 1 - Delete the cloned workflow 539 WorkflowAwareContentHelper.removeWorkflowId(content); 540 541 // For legacy purpose, delete the workflow reference property if exists (only for contents created on 3.x versions) 542 Node node = content.getNode(); 543 try 544 { 545 if (node.hasProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":workflowRef")) 546 { 547 node.getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":workflowRef").remove(); 548 } 549 } 550 catch (RepositoryException e) 551 { 552 throw new AmetysRepositoryException("Unable to remove workflowId property", e); 553 } 554 555 workflow.removeWorkflow(wId); 556 557 // 2 - Initialize new workflow instance 558 HashMap<String, Object> inputs = new HashMap<>(); 559 long workflowId = workflow.initialize(workflowName, 0, inputs); 560 content.setWorkflowId(workflowId); 561 562 // Update current step property 563 Step currentStep = (Step) workflow.getCurrentSteps(workflowId).iterator().next(); 564 content.setCurrentStepId(currentStep.getStepId()); 565 } 566 catch (Exception e) 567 { 568 throw new AmetysRepositoryException("Unable to initialize workflow for content " + content.getId(), e); 569 } 570 571 } 572}