001/* 002 * Copyright 2010 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.synchronization; 017 018import java.io.IOException; 019import java.time.ZonedDateTime; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025 026import javax.jcr.ItemNotFoundException; 027import javax.jcr.Node; 028import javax.jcr.NodeIterator; 029import javax.jcr.Property; 030import javax.jcr.PropertyIterator; 031import javax.jcr.RepositoryException; 032import javax.jcr.Session; 033import javax.jcr.version.VersionHistory; 034 035import org.apache.avalon.framework.activity.Initializable; 036import org.apache.avalon.framework.component.Component; 037import org.apache.avalon.framework.logger.AbstractLogEnabled; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.commons.collections.Predicate; 042import org.apache.commons.collections.PredicateUtils; 043import org.apache.commons.lang.StringUtils; 044 045import org.ametys.cms.CmsConstants; 046import org.ametys.cms.repository.CloneComponent; 047import org.ametys.cms.repository.Content; 048import org.ametys.cms.support.AmetysPredicateUtils; 049import org.ametys.core.schedule.progression.ProgressionTrackerFactory; 050import org.ametys.core.schedule.progression.SimpleProgressionTracker; 051import org.ametys.core.util.AvalonLoggerAdapter; 052import org.ametys.core.util.I18nUtils; 053import org.ametys.core.util.mail.SendMailHelper; 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.UnknownAmetysObjectException; 059import org.ametys.plugins.repository.jcr.JCRAmetysObject; 060import org.ametys.plugins.repository.version.VersionableAmetysObject; 061import org.ametys.runtime.config.Config; 062import org.ametys.runtime.i18n.I18nizableText; 063import org.ametys.web.repository.page.Page; 064import org.ametys.web.repository.page.Page.LinkType; 065import org.ametys.web.repository.page.Page.PageType; 066import org.ametys.web.repository.page.SitemapElement; 067import org.ametys.web.repository.page.Zone; 068import org.ametys.web.repository.page.ZoneItem; 069import org.ametys.web.repository.page.jcr.DefaultPage; 070import org.ametys.web.repository.sitemap.Sitemap; 071import org.ametys.web.skin.Skin; 072import org.ametys.web.skin.SkinTemplate; 073import org.ametys.web.skin.SkinTemplateZone; 074import org.ametys.web.skin.SkinsManager; 075 076import jakarta.mail.MessagingException; 077 078/** 079 * Helper for common processing used while synchronizing. 080 */ 081public class SynchronizeComponent extends AbstractLogEnabled implements Component, Serviceable, Initializable 082{ 083 /** Avalon Role */ 084 public static final String ROLE = SynchronizeComponent.class.getName(); 085 086 private static SynchronizeComponent _instance; 087 088 private CloneComponent _cloneComponent; 089 private AmetysObjectResolver _resolver; 090 private SkinsManager _skinsManager; 091 private I18nUtils _i18nUtils; 092 093 @Override 094 public void service(ServiceManager smanager) throws ServiceException 095 { 096 _cloneComponent = (CloneComponent) smanager.lookup(CloneComponent.ROLE); 097 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 098 _skinsManager = (SkinsManager) smanager.lookup(SkinsManager.ROLE); 099 _i18nUtils = (I18nUtils) smanager.lookup(I18nUtils.ROLE); 100 } 101 102 @Override 103 public void initialize() throws Exception 104 { 105 _instance = this; 106 } 107 108 /** 109 * Get the unique instance 110 * @return the unique instance 111 */ 112 @Deprecated 113 public static SynchronizeComponent getInstance() 114 { 115 return _instance; 116 } 117 118 /** 119 * Returns true if the hierarchy of the given Page is valid, ie if all its ancestors are valid and synchronized in the live workspace. 120 * @param page the source page. 121 * @param liveSession the live Session. 122 * @return the hierarchy validity status. 123 * @throws RepositoryException if failed to check page hierarchy 124 */ 125 public boolean isHierarchyValid(Page page, Session liveSession) throws RepositoryException 126 { 127 AmetysObject parent = page.getParent(); 128 if (parent instanceof Sitemap) 129 { 130 return true; 131 } 132 133 Page parentPage = (Page) parent; 134 if (!(parentPage instanceof JCRAmetysObject)) 135 { 136 return isPageValid(parentPage, _skinsManager.getSkin(page.getSite().getSkinId())) && isHierarchyValid(parentPage, liveSession); 137 } 138 else if (parentPage.getType() == PageType.NODE) 139 { 140 return isDateValid(parentPage) && isHierarchyValid(parentPage, liveSession); 141 } 142 else 143 { 144 return liveSession.itemExists(((JCRAmetysObject) parentPage).getNode().getPath()); 145 } 146 } 147 148 /** 149 * Returns true if the given page should be synchronized in the live workspace. 150 * @param page the page to test. 151 * @param skin the skin of the page's site. 152 * @return true if the page is valid 153 */ 154 public boolean isPageValid(Page page, Skin skin) 155 { 156 if (!isDateValid(page)) 157 { 158 return false; 159 } 160 161 switch (page.getType()) 162 { 163 case LINK: 164 return _isLinkPageValid(page); 165 case NODE: 166 return _isNodePageValid(page, skin); 167 case CONTAINER: 168 return _isContainerPageValid(page, skin); 169 default: 170 return false; 171 } 172 } 173 174 /** 175 * Returns true if the publication date of the given page are valid. 176 * @param page the page to test. 177 * @return true if the publication dates are valid 178 */ 179 public boolean isDateValid (Page page) 180 { 181 ZonedDateTime startDate = page.getValue(DefaultPage.METADATA_PUBLICATION_START_DATE); 182 ZonedDateTime endDate = page.getValue(DefaultPage.METADATA_PUBLICATION_END_DATE); 183 184 if (startDate != null && startDate.isAfter(ZonedDateTime.now())) 185 { 186 return false; 187 } 188 189 if (endDate != null && endDate.isBefore(ZonedDateTime.now())) 190 { 191 return false; 192 } 193 194 return true; 195 } 196 197 private boolean _isInfiniteRedirection (Page page, List<String> pagesSequence) 198 { 199 Page redirectPage = _getPageRedirection (page); 200 if (redirectPage == null) 201 { 202 return false; 203 } 204 205 if (pagesSequence.contains(redirectPage.getId())) 206 { 207 return true; 208 } 209 210 pagesSequence.add(redirectPage.getId()); 211 return _isInfiniteRedirection (redirectPage, pagesSequence); 212 } 213 214 private Page _getPageRedirection (Page page) 215 { 216 if (PageType.LINK.equals(page.getType()) && LinkType.PAGE.equals(page.getURLType())) 217 { 218 try 219 { 220 String pageId = page.getURL(); 221 return _resolver.resolveById(pageId); 222 } 223 catch (AmetysRepositoryException e) 224 { 225 return null; 226 } 227 } 228 else if (PageType.NODE.equals(page.getType())) 229 { 230 AmetysObjectIterable<? extends Page> childPages = page.getChildrenPages(); 231 Iterator<? extends Page> it = childPages.iterator(); 232 if (it.hasNext()) 233 { 234 return it.next(); 235 } 236 } 237 238 return null; 239 } 240 241 private boolean _isLinkPageValid(Page page) 242 { 243 if (LinkType.WEB.equals(page.getURLType())) 244 { 245 return true; 246 } 247 248 // Check for infinitive loop redirection 249 List<String> pagesSequence = new ArrayList<>(); 250 pagesSequence.add(page.getId()); 251 if (_isInfiniteRedirection (page, pagesSequence)) 252 { 253 getLogger().error("An infinite loop redirection was detected for page '" + page.getSiteName() + "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "' (" + page.getId() + ") in the sequence " + pagesSequence); 254 _sendErrorMailForInfiniteRedirection (page); 255 return false; 256 } 257 258 try 259 { 260 String pageId = page.getURL(); 261 Page linkedPage = _resolver.resolveById(pageId); 262 263 Skin linkedPageSkin = _skinsManager.getSkin(linkedPage.getSite().getSkinId()); 264 return isPageValid(linkedPage, linkedPageSkin); 265 } 266 catch (UnknownAmetysObjectException e) 267 { 268 getLogger().error("Page '" + page.getSiteName() + "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "' (" + page.getId() + ") redirects to an unexisting page '" + page.getURL() + "'", e); 269 return false; 270 } 271 catch (AmetysRepositoryException e) 272 { 273 getLogger().error("Unable to check page validity for page link '" + page.getId() + "'", e); 274 return false; 275 } 276 } 277 278 private boolean _isNodePageValid(Page page, Skin skin) 279 { 280 // Check for infinitive loop redirection 281 List<String> pagesSequence = new ArrayList<>(); 282 pagesSequence.add(page.getId()); 283 if (_isInfiniteRedirection (page, pagesSequence)) 284 { 285 getLogger().error("An infinite loop redirection was detected for page '" + page.getSiteName() + "/" + page.getSitemapName() + "/" + page.getPathInSitemap() + "' (" + page.getId() + ") in the sequence " + pagesSequence); 286 _sendErrorMailForInfiniteRedirection (page); 287 return false; 288 } 289 290 // a node page is valid if at least one of its child pages is valid 291 AmetysObjectIterable<? extends Page> childPages = page.getChildrenPages(); 292 boolean hasOneConcreteChildPage = false; 293 Iterator<? extends Page> it = childPages.iterator(); 294 295 while (!hasOneConcreteChildPage && it.hasNext()) 296 { 297 Page childPage = it.next(); 298 if (isPageValid(childPage, skin)) 299 { 300 hasOneConcreteChildPage = true; 301 } 302 } 303 304 return hasOneConcreteChildPage; 305 } 306 307 private boolean _isContainerPageValid(Page page, Skin skin) 308 { 309 // a container page is valid if it has no zones or at least one zone is valid 310 if (skin == null) 311 { 312 return false; 313 } 314 else 315 { 316 SkinTemplate template = skin.getTemplate(page.getTemplate()); 317 if (template == null) 318 { 319 return false; 320 } 321 322 Map<String, SkinTemplateZone> modelZones = template.getZones(); 323 Iterator<String> zoneNames = modelZones.keySet().iterator(); 324 325 boolean pageIsValid = modelZones.size() == 0; 326 327 while (!pageIsValid && zoneNames.hasNext()) 328 { 329 // a zone is valid if it is not empty and if at least one ZoneItem is valid 330 String zoneName = zoneNames.next(); 331 332 if (page.hasZone(zoneName)) 333 { 334 Zone zone = page.getZone(zoneName); 335 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 336 Iterator<? extends ZoneItem> it = zoneItems.iterator(); 337 boolean zoneIsValid = false; 338 339 while (!zoneIsValid && it.hasNext()) 340 { 341 ZoneItem zoneItem = it.next(); 342 zoneIsValid = _isZoneItemValid(zoneItem); 343 } 344 345 pageIsValid = zoneIsValid; 346 } 347 } 348 349 return pageIsValid; 350 } 351 } 352 353 private boolean _isZoneItemValid(ZoneItem zoneItem) 354 { 355 switch (zoneItem.getType()) 356 { 357 case SERVICE: 358 // a service is always valid 359 return true; 360 case CONTENT: 361 try 362 { 363 // a content is valid if it has been validated at least once 364 Content content = zoneItem.getContent(); 365 if (content instanceof VersionableAmetysObject) 366 { 367 return Arrays.asList(((VersionableAmetysObject) content).getAllLabels()).contains(CmsConstants.LIVE_LABEL); 368 } 369 else 370 { 371 return false; 372 } 373 } 374 catch (AmetysRepositoryException e) 375 { 376 getLogger().error("Unable to get content property", e); 377 return false; 378 } 379 default: 380 throw new IllegalArgumentException("A zoneItem must be either a service or a content."); 381 } 382 } 383 384 /** 385 * Adds a node to a parent node using a source node for name, type 386 * and potential UUID. 387 * @param srcNode the source node. 388 * @param parentNode the parent node for the newly created node. 389 * @param nodeName the node name to use. 390 * @return the created node. 391 * @throws RepositoryException if an error occurs. 392 */ 393 public Node addNodeWithUUID(Node srcNode, Node parentNode, String nodeName) throws RepositoryException 394 { 395 Node node = null; 396 397 if (AmetysPredicateUtils.isAllowedForLiveContent().evaluate(srcNode)) 398 { 399 node = _cloneComponent.addNodeWithUUID(srcNode, parentNode, nodeName); 400 } 401 402 return node; 403 } 404 405 /** 406 * Clones properties of a node 407 * @param srcNode the source node. 408 * @param clonedNode the cloned node. 409 * @param propertyPredicate the property selector. 410 * @throws RepositoryException if an error occurs. 411 */ 412 public void cloneProperties(Node srcNode, Node clonedNode, Predicate propertyPredicate) throws RepositoryException 413 { 414 cloneProperties(srcNode, clonedNode, propertyPredicate, ProgressionTrackerFactory.createSimpleProgressionTracker("Clone properties", new AvalonLoggerAdapter(getLogger()))); 415 } 416 417 /** 418 * Clones properties of a node 419 * @param srcNode the source node. 420 * @param clonedNode the cloned node. 421 * @param propertyPredicate the property selector. 422 * @param progressionTracker The progression tracker 423 * @throws RepositoryException if an error occurs. 424 */ 425 public void cloneProperties(Node srcNode, Node clonedNode, Predicate propertyPredicate, SimpleProgressionTracker progressionTracker) throws RepositoryException 426 { 427 if (srcNode == null || clonedNode == null) 428 { 429 return; 430 } 431 432 // Ignore protected properties + filter node/property for live 433 final Predicate predicate = AmetysPredicateUtils.ignoreProtectedProperties(propertyPredicate); 434 435 // First remove existing matching properties from cloned Node 436 PropertyIterator clonedProperties = clonedNode.getProperties(); 437 while (clonedProperties.hasNext()) 438 { 439 Property property = clonedProperties.nextProperty(); 440 if (predicate.evaluate(property)) 441 { 442 property.remove(); 443 } 444 } 445 446 long nbOfProperties = srcNode.getProperties().getSize(); 447 progressionTracker.setSize(nbOfProperties); 448 449 // Then copy properties 450 PropertyIterator itProperties = srcNode.getProperties(); 451 452 while (itProperties.hasNext()) 453 { 454 Property property = itProperties.nextProperty(); 455 456 457 if (predicate.evaluate(property)) 458 { 459 boolean propertyHandled = false; 460 /* 461 if (property.getType() == PropertyType.REFERENCE) 462 { 463 try 464 { 465 Node referencedNode = property.getNode(); 466 if (property.getName().equals("ametys-internal:initial-content")) 467 { 468 // Do not clone the initial-content reference. 469 propertyHandled = true; 470 } 471 472 } 473 catch (ItemNotFoundException e) 474 { 475 // the target node does not exist anymore, this could be due to workflow having been deleted 476 propertyHandled = true; 477 } 478 }*/ 479 480 if (!propertyHandled) 481 { 482 if (property.getDefinition().isMultiple()) 483 { 484 clonedNode.setProperty(property.getName(), property.getValues(), property.getType()); 485 } 486 else 487 { 488 clonedNode.setProperty(property.getName(), property.getValue(), property.getType()); 489 } 490 } 491 } 492 493 progressionTracker.increment(); 494 } 495 } 496 497 /** 498 * Clones a node by preserving the source node UUID for a content. 499 * @param srcNode the source node. 500 * @param clonedNode the cloned node. 501 * @param propertyPredicate the property selector. 502 * @param nodePredicate the node selector. 503 * @throws RepositoryException if an error occurs. 504 */ 505 public void cloneContentNodeAndPreserveUUID(Node srcNode, Node clonedNode, Predicate propertyPredicate, Predicate nodePredicate) throws RepositoryException 506 { 507 Predicate finalNodePredicate = PredicateUtils.andPredicate(AmetysPredicateUtils.isAllowedForLiveContent(), nodePredicate); 508 Predicate finalPropertiesPredicate = PredicateUtils.andPredicate(AmetysPredicateUtils.isAllowedForLiveContent(), propertyPredicate); 509 510 cloneNodeAndPreserveUUID(srcNode, clonedNode, finalPropertiesPredicate, finalNodePredicate); 511 } 512 513 /** 514 * Clones properties of a content node 515 * @param srcNode the source node. 516 * @param clonedNode the cloned node. 517 * @param propertyPredicate the property selector. 518 * @throws RepositoryException if an error occurs. 519 */ 520 public void cloneContentProperties(Node srcNode, Node clonedNode, Predicate propertyPredicate) throws RepositoryException 521 { 522 Predicate finalPropertiesPredicate = PredicateUtils.andPredicate(AmetysPredicateUtils.isAllowedForLiveContent(), propertyPredicate); 523 524 cloneProperties(srcNode, clonedNode, finalPropertiesPredicate); 525 } 526 527 /** 528 * Clones a node by preserving the source node UUID. 529 * @param srcNode the source node. 530 * @param clonedNode the cloned node. 531 * @param propertyPredicate the property selector. 532 * @param nodePredicate the node selector. 533 * @throws RepositoryException if an error occurs. 534 */ 535 public void cloneNodeAndPreserveUUID(Node srcNode, Node clonedNode, Predicate propertyPredicate, Predicate nodePredicate) throws RepositoryException 536 { 537 if (srcNode == null || clonedNode == null) 538 { 539 return; 540 } 541 542 // Clone properties 543 cloneProperties(srcNode, clonedNode, propertyPredicate); 544 545 // Remove all matching subNodes before cloning, for better handling of same name siblings 546 NodeIterator subNodes = clonedNode.getNodes(); 547 while (subNodes.hasNext()) 548 { 549 Node subNode = subNodes.nextNode(); 550 551 if (nodePredicate.evaluate(subNode)) 552 { 553 subNode.remove(); 554 } 555 } 556 557 // Then copy sub nodes 558 NodeIterator itNodes = srcNode.getNodes(); 559 while (itNodes.hasNext()) 560 { 561 Node subNode = itNodes.nextNode(); 562 563 if (nodePredicate.evaluate(subNode)) 564 { 565 Node clonedSubNode = addNodeWithUUID(subNode, clonedNode, subNode.getName()); 566 cloneNodeAndPreserveUUID(subNode, clonedSubNode, propertyPredicate, nodePredicate); 567 } 568 } 569 } 570 571 /** 572 * Will copy the LIVE version of the content into the LIVE workspace. Works only with JCRAmetysObject. 573 * This method DO NOT save the liveSession. 574 * @param content The content to copy 575 * @param liveSession The session for live 576 * @throws RepositoryException If an error occurred 577 */ 578 public void synchronizeContent(Content content, Session liveSession) throws RepositoryException 579 { 580 if (getLogger().isDebugEnabled()) 581 { 582 getLogger().debug("Synchronizing content " + content.getId()); 583 } 584 585 if (!(content instanceof JCRAmetysObject)) 586 { 587 return; 588 } 589 590 Node node = ((JCRAmetysObject) content).getNode(); 591 VersionHistory versionHistory = node.getSession().getWorkspace().getVersionManager().getVersionHistory(node.getPath()); 592 if (Arrays.asList(versionHistory.getVersionLabels()).contains(CmsConstants.LIVE_LABEL)) 593 { 594 Node validatedContentNode = versionHistory.getVersionByLabel(CmsConstants.LIVE_LABEL).getFrozenNode(); 595 596 Node clonedNode; 597 try 598 { 599 // content already exists in the live workspace 600 clonedNode = liveSession.getNodeByIdentifier(node.getIdentifier()); 601 } 602 catch (ItemNotFoundException e) 603 { 604 // content does not exist in the live workspace 605 606 // clone content itself 607 Node clonedContentParentNode = cloneAncestorsAndPreserveUUID(node, liveSession); 608 609 clonedNode = addNodeWithUUID(validatedContentNode, clonedContentParentNode, node.getName()); 610 611 } 612 613 // First, let's remove all child node and all properties (versionned or not) 614 // Second, clone all versionned node and properties 615 cloneContentNodeAndPreserveUUID(validatedContentNode, clonedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate()); 616 617 // Third, clone all unversionned node and properties (such as acl, tags or all unversionned metadata but except initial-content or workflow) 618 cloneContentNodeAndPreserveUUID(node, clonedNode, AmetysPredicateUtils.isNonVersionned(node), AmetysPredicateUtils.isNonVersionned(node)); 619 620 // Clone validation metadata from the current version. 621 cloneContentProperties(node, clonedNode, AmetysPredicateUtils.propertyNamesPredicate("ametys:lastValidationDate", "ametys:lastMajorValidationDate", "ametys:privacy")); // FIXME CMS-7630 privacy should be not versionned 622 } 623 else 624 { 625 try 626 { 627 // content already exists in the live workspace 628 Node clonedNode = liveSession.getNodeByIdentifier(node.getIdentifier()); 629 clonedNode.remove(); 630 } 631 catch (ItemNotFoundException e) 632 { 633 // Ok, the content was not in live 634 } 635 } 636 } 637 638 /** 639 * Clones ancestors of a node by preserving the source node UUID. 640 * @param srcNode the source node. 641 * @param liveSession the session to the live workspace. 642 * @return the parent node in the live workspace. 643 * @throws RepositoryException if an error occurs. 644 */ 645 public Node cloneAncestorsAndPreserveUUID(Node srcNode, Session liveSession) throws RepositoryException 646 { 647 if (srcNode.getName().length() == 0) 648 { 649 // We are on the root node which already exists 650 return liveSession.getRootNode(); 651 } 652 else 653 { 654 Node liveRootNode = liveSession.getRootNode(); 655 Node parentNode = srcNode.getParent(); 656 String parentNodePath = parentNode.getPath().substring(1); 657 658 if (liveRootNode.hasNode(parentNodePath)) 659 { 660 // Found existing parent 661 return liveRootNode.getNode(parentNodePath); 662 663 } 664 else 665 { 666 Node clonedAncestorNode = cloneAncestorsAndPreserveUUID(parentNode, liveSession); 667 668 Node clonedParentNode = null; 669 670 if (clonedAncestorNode.hasNode(parentNode.getName())) 671 { 672 // Possible with autocreated children 673 clonedParentNode = clonedAncestorNode.getNode(parentNode.getName()); 674 } 675 else 676 { 677 clonedParentNode = addNodeWithUUID(parentNode, clonedAncestorNode, parentNode.getName()); 678 679 } 680 681 // reorder node when possible 682 if (parentNode.getParent().getPrimaryNodeType().hasOrderableChildNodes()) 683 { 684 orderNode(parentNode.getParent(), parentNode.getName(), clonedParentNode); 685 } 686 687 // update existing acl 688 synchronizeACL(parentNode, liveSession); 689 690 // Copy only properties 691 cloneNodeAndPreserveUUID(parentNode, clonedParentNode, PredicateUtils.truePredicate(), PredicateUtils.falsePredicate()); 692 693 return clonedParentNode; 694 } 695 } 696 } 697 698 /** 699 * Clones a page and its eligible child pages, recursively. 700 * It is assumed that parent page already exist in the live workspace 701 * @param page the page to clone. 702 * @param skin the skin of the page's site. 703 * @param liveSession the session to the live workspace. 704 * @throws RepositoryException if an error occurs. 705 */ 706 public void cloneEligiblePage(Page page, Skin skin, Session liveSession) throws RepositoryException 707 { 708 cloneEligiblePage(page, skin, liveSession, null); 709 } 710 711 /** 712 * Clones a page and its eligible child pages, recursively. 713 * It is assumed that parent page already exist in the live workspace 714 * @param page the page to clone. 715 * @param skin the skin of the page's site. 716 * @param liveSession the session to the live workspace. 717 * @param progressionTracker The page progression tracker for sub pages. The size should already been set. Can be null. 718 * @throws RepositoryException if an error occurs. 719 */ 720 public void cloneEligiblePage(Page page, Skin skin, Session liveSession, SimpleProgressionTracker progressionTracker) throws RepositoryException 721 { 722 if (!(page instanceof JCRAmetysObject)) 723 { 724 return; 725 } 726 727 Node pageNode = ((JCRAmetysObject) page).getNode(); 728 String pagePathInJcr = pageNode.getPath().substring(1); 729 Node rootNode = liveSession.getRootNode(); 730 731 boolean isNew = false; 732 Node liveNode = null; 733 734 if (!rootNode.hasNode(pagePathInJcr)) 735 { 736 isNew = true; 737 Node parentNode = pageNode.getParent(); 738 String parentPath = parentNode.getPath().substring(1); 739 String pageName = pageNode.getName(); 740 741 // We assume that the parent Node exists. 742 Node liveParentNode = rootNode.getNode(parentPath); 743 liveNode = addNodeWithUUID(pageNode, liveParentNode, pageNode.getName()); 744 745 // reorder correctly 746 orderNode(parentNode, pageName, liveNode); 747 } 748 else 749 { 750 liveNode = rootNode.getNode(pagePathInJcr); 751 } 752 753 // Clone all but child pages and zones 754 Predicate childPredicate = PredicateUtils.andPredicate(PredicateUtils.notPredicate(AmetysPredicateUtils.nodeTypePredicate("ametys:page")), 755 PredicateUtils.notPredicate(AmetysPredicateUtils.nodeTypePredicate("ametys:zones"))); 756 757 cloneNodeAndPreserveUUID(pageNode, liveNode, PredicateUtils.truePredicate(), childPredicate); 758 759 // Clone zones, if relevant 760 cloneZones(pageNode, page, liveNode); 761 762 if (progressionTracker != null) 763 { 764 progressionTracker.increment(); 765 } 766 767 // In case of a new page , there may be valid children pages 768 if (isNew) 769 { 770 // Clone each child page 771 cloneEligibleChildrenPages(page, skin, liveSession, progressionTracker); 772 } 773 } 774 775 /** 776 * Clones the child pages of a given page, recursively, if eligible 777 * It is assumed that parent page already exist in the live workspace 778 * @param page the page to clone. 779 * @param skin the skin of the page's site. 780 * @param liveSession the session to the live workspace. 781 * @param progressionTracker The page progression tracker for sub pages. The size should already been set. Can be null. 782 * @throws RepositoryException if an error occurs. 783 */ 784 public void cloneEligibleChildrenPages(SitemapElement page, Skin skin, Session liveSession, SimpleProgressionTracker progressionTracker) throws RepositoryException 785 { 786 // Clone each child page 787 for (Page childPage : page.getChildrenPages()) 788 { 789 if (childPage instanceof JCRAmetysObject) 790 { 791 if (isPageValid(childPage, skin)) 792 { 793 cloneEligiblePage(childPage, skin, liveSession, progressionTracker); 794 } 795 else if (progressionTracker != null) 796 { 797 _increment(childPage, progressionTracker); 798 } 799 } 800 } 801 } 802 803 private void _increment(Page page, SimpleProgressionTracker progressionTracker) 804 { 805 if (page instanceof JCRAmetysObject) 806 { 807 progressionTracker.increment(); 808 for (Page childPage : page.getChildrenPages()) 809 { 810 _increment(childPage, progressionTracker); 811 } 812 } 813 } 814 815 /** 816 * Reorder a node, mirroring the order in the default workspace. 817 * @param parentNode the parent of the source Node in the default workspace. 818 * @param nodeName the node name. 819 * @param liveNode the node in the live workspace to be reordered. 820 * @throws RepositoryException if an error occurs. 821 */ 822 public void orderNode(Node parentNode, String nodeName, Node liveNode) throws RepositoryException 823 { 824 _cloneComponent.orderNode(parentNode, nodeName, liveNode); 825 } 826 827 /** 828 * Clones the zones of a page. 829 * @param pageNode the JCR Node of the page. 830 * @param sitemapElement the page. 831 * @param liveNode the node in the live workspace. 832 * @throws RepositoryException if an error occurs. 833 */ 834 public void cloneZones(Node pageNode, SitemapElement sitemapElement, Node liveNode) throws RepositoryException 835 { 836 if (pageNode.hasNode("ametys-internal:zones")) 837 { 838 if (liveNode.hasNode("ametys-internal:zones")) 839 { 840 liveNode.getNode("ametys-internal:zones").remove(); 841 } 842 843 Node zonesNode = addNodeWithUUID(pageNode.getNode("ametys-internal:zones"), liveNode, "ametys-internal:zones"); 844 845 for (Zone zone : sitemapElement.getZones()) 846 { 847 Node srcZoneNode = ((JCRAmetysObject) zone).getNode(); 848 Node zoneNode = addNodeWithUUID(srcZoneNode, zonesNode, zone.getName()); 849 850 // Clone properties and children except zone items 851 cloneNodeAndPreserveUUID(srcZoneNode, zoneNode, PredicateUtils.truePredicate(), PredicateUtils.notPredicate(AmetysPredicateUtils.nodeTypePredicate("ametys:zoneItems"))); 852 853 // Clone zone items 854 Node zoneItemsNode = zoneNode.getNode("ametys-internal:zoneItems"); 855 for (ZoneItem zoneItem : zone.getZoneItems()) 856 { 857 if (_isZoneItemValid(zoneItem)) 858 { 859 Node srcNode = ((JCRAmetysObject) zoneItem).getNode(); 860 Node zoneItemNode = addNodeWithUUID(srcNode, zoneItemsNode, "ametys:zoneItem"); 861 cloneNodeAndPreserveUUID(srcNode, zoneItemNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate()); 862 } 863 } 864 } 865 } 866 } 867 868 /** 869 * Invalidates the hierarchy of a page if needed 870 * @param page the page. 871 * @param skin the skin of the page's site. 872 * @param liveSession the session to the live workspace. 873 * @throws RepositoryException if an error occurs. 874 */ 875 public void invalidateHierarchy(Page page, Skin skin, Session liveSession) throws RepositoryException 876 { 877 String jcrPath = ((JCRAmetysObject) page).getNode().getPath(); 878 if (liveSession.itemExists(jcrPath)) 879 { 880 liveSession.getItem(jcrPath).remove(); 881 } 882 883 AmetysObject parent = page.getParent(); 884 885 if (parent instanceof Page) 886 { 887 if (!isPageValid((Page) parent, skin)) 888 { 889 invalidateHierarchy((Page) parent, skin, liveSession); 890 } 891 } 892 } 893 894 /** 895 * Synchronizes a page with the live workspace. Also synchronizes hierarchy. 896 * @param page the page. 897 * @param skin the skin of the page's site. 898 * @param liveSession the session to the live workspace. 899 * @throws RepositoryException if an error occurs. 900 */ 901 public void synchronizePage(Page page, Skin skin, Session liveSession) throws RepositoryException 902 { 903 if (isHierarchyValid(page, liveSession)) 904 { 905 if (isPageValid(page, skin)) 906 { 907 //FIXME clone ancestor pages, not only ancestor nodes 908 cloneAncestorsAndPreserveUUID(((JCRAmetysObject) page).getNode(), liveSession); 909 910 // clone page and valid children 911 cloneEligiblePage(page, skin, liveSession); 912 } 913 else 914 { 915 // page is invalid, remove it from live if it was previously valid, then potentially invalidate hierarchy 916 invalidateHierarchy(page, skin, liveSession); 917 } 918 } 919 } 920 921 private void _sendErrorMailForInfiniteRedirection (Page page) 922 { 923 String recipient = Config.getInstance().getValue("smtp.mail.sysadminto"); 924 try 925 { 926 List<String> i18nParams = new ArrayList<>(); 927 i18nParams.add(page.getSite().getTitle()); 928 929 I18nizableText i18nSubject = new I18nizableText("plugin.web", "PLUGINS_WEB_SYNCHRONIZE_INFINITE_REDIRECTION_MAIL_SUBJECT", i18nParams); 930 String subject = _i18nUtils.translate(i18nSubject); 931 932 i18nParams.add(page.getSitemapName() + "/" + page.getPathInSitemap()); 933 i18nParams.add(StringUtils.stripEnd(StringUtils.removeEndIgnoreCase(Config.getInstance().getValue("cms.url"), "index.html"), "/")); 934 String body = _i18nUtils.translate(new I18nizableText("plugin.web", "PLUGINS_WEB_SYNCHRONIZE_INFINITE_REDIRECTION_MAIL_BODY", i18nParams)); 935 936 SendMailHelper.newMail() 937 .withSubject(subject) 938 .withTextBody(body) 939 .withRecipient(recipient) 940 .sendMail(); 941 } 942 catch (MessagingException | IOException e) 943 { 944 if (getLogger().isWarnEnabled()) 945 { 946 getLogger().warn("Could not send an alert e-mail to " + recipient, e); 947 } 948 } 949 } 950 951 /** 952 * Synchronize a node ACL information with node in live session if available 953 * This method does NOT save live session 954 * @param node The trunk node in default workspace 955 * @param liveSession The live session 956 */ 957 public void synchronizeACL(Node node, Session liveSession) 958 { 959 try 960 { 961 if (getLogger().isDebugEnabled()) 962 { 963 getLogger().debug("Synchronizing ACL for node " + node.getIdentifier()); 964 } 965 966 try 967 { 968 // content already exists in the live workspace 969 Node clonedNode = liveSession.getNodeByIdentifier(node.getIdentifier()); 970 971 // Clone acl 972 String aclNodeNode = "ametys-internal:acl"; 973 if (node.hasNode(aclNodeNode)) 974 { 975 Node aclNode = node.getNode(aclNodeNode); 976 977 Node aclClonedNode; 978 if (clonedNode.hasNode(aclNodeNode)) 979 { 980 aclClonedNode = clonedNode.getNode(aclNodeNode); 981 } 982 else 983 { 984 aclClonedNode = clonedNode.addNode(aclNodeNode, aclNode.getPrimaryNodeType().getName()); 985 } 986 987 cloneNodeAndPreserveUUID(aclNode, aclClonedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate()); 988 } 989 } 990 catch (ItemNotFoundException e) 991 { 992 // Nothing to synchronize: the content is not in live 993 } 994 } 995 catch (RepositoryException e) 996 { 997 throw new RuntimeException("Can not copy ACL for node", e); 998 } 999 } 1000 1001 /** 1002 * Synchronize a list of content to the targeted session 1003 * @param contents the set of contents 1004 * @param liveSession the target session 1005 * @param progressionTracker The progression tracker 1006 * @throws AmetysRepositoryException if an error occurred while synchronizing 1007 */ 1008 public void synchronizeContents(AmetysObjectIterable<Content> contents, Session liveSession, SimpleProgressionTracker progressionTracker) throws AmetysRepositoryException 1009 { 1010 long nbContents = contents.getSize(); 1011 1012 progressionTracker.setSize(nbContents); 1013 for (Content content : contents) 1014 { 1015 try 1016 { 1017 synchronizeContent(content, liveSession); 1018 liveSession.save(); 1019 } 1020 catch (RepositoryException e) 1021 { 1022 throw new AmetysRepositoryException("Synchronization failed for content " + content.getId() + ". See below for more information.", e); 1023 } 1024 1025 progressionTracker.increment(); 1026 } 1027 } 1028}