001/* 002 * Copyright 2019 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.plugins.webcontentio.archive; 017 018import java.io.IOException; 019import java.nio.file.Path; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.HashMap; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Map; 026import java.util.Optional; 027import java.util.Set; 028import java.util.stream.Collectors; 029import java.util.stream.Stream; 030import java.util.zip.ZipEntry; 031import java.util.zip.ZipOutputStream; 032 033import javax.jcr.Node; 034import javax.jcr.NodeIterator; 035import javax.jcr.Property; 036import javax.jcr.RepositoryException; 037import javax.jcr.Session; 038import javax.jcr.Value; 039import javax.xml.parsers.ParserConfigurationException; 040import javax.xml.transform.TransformerConfigurationException; 041import javax.xml.transform.sax.TransformerHandler; 042import javax.xml.transform.stream.StreamResult; 043 044import org.apache.avalon.framework.service.ServiceException; 045import org.apache.avalon.framework.service.ServiceManager; 046import org.apache.avalon.framework.service.Serviceable; 047import org.apache.cocoon.xml.AttributesImpl; 048import org.apache.cocoon.xml.XMLUtils; 049import org.apache.commons.lang3.StringUtils; 050import org.apache.commons.math3.util.IntegerSequence.Incrementor; 051import org.xml.sax.Attributes; 052import org.xml.sax.SAXException; 053 054import org.ametys.cms.tag.Tag; 055import org.ametys.cms.tag.TagProvider; 056import org.ametys.cms.tag.TagProviderExtensionPoint; 057import org.ametys.core.observation.ObservationManager; 058import org.ametys.core.user.CurrentUserProvider; 059import org.ametys.core.util.LambdaUtils; 060import org.ametys.plugins.contentio.archive.ArchiveHandler; 061import org.ametys.plugins.contentio.archive.Archiver; 062import org.ametys.plugins.contentio.archive.Archivers; 063import org.ametys.plugins.contentio.archive.ContentsArchiverHelper; 064import org.ametys.plugins.contentio.archive.DefaultPluginArchiver; 065import org.ametys.plugins.contentio.archive.ImportReport; 066import org.ametys.plugins.contentio.archive.ManifestReaderWriter; 067import org.ametys.plugins.contentio.archive.Merger; 068import org.ametys.plugins.contentio.archive.PartialImport; 069import org.ametys.plugins.contentio.archive.ResourcesArchiverHelper; 070import org.ametys.plugins.explorer.resources.ResourceCollection; 071import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint; 072import org.ametys.plugins.repository.AmetysObjectIterable; 073import org.ametys.plugins.repository.AmetysObjectResolver; 074import org.ametys.plugins.repository.ModifiableAmetysObject; 075import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 076import org.ametys.plugins.repository.RepositoryConstants; 077import org.ametys.plugins.repository.TraversableAmetysObject; 078import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 079import org.ametys.plugins.repository.jcr.JCRAmetysObject; 080import org.ametys.runtime.i18n.I18nizableText; 081import org.ametys.runtime.plugin.component.AbstractLogEnabled; 082import org.ametys.web.data.type.ModelItemTypeExtensionPoint; 083import org.ametys.web.parameters.view.ViewParametersManager; 084import org.ametys.web.repository.page.Page; 085import org.ametys.web.repository.page.Page.PageType; 086import org.ametys.web.repository.page.SitemapElement; 087import org.ametys.web.repository.page.Zone; 088import org.ametys.web.repository.page.ZoneItem; 089import org.ametys.web.repository.page.ZoneItem.ZoneType; 090import org.ametys.web.repository.page.jcr.DefaultPageFactory; 091import org.ametys.web.repository.site.Site; 092import org.ametys.web.repository.site.SiteManager; 093import org.ametys.web.repository.site.SiteType; 094import org.ametys.web.repository.site.SiteTypesExtensionPoint; 095import org.ametys.web.repository.sitemap.Sitemap; 096import org.ametys.web.repository.sitemap.SitemapFactory; 097import org.ametys.web.service.ServiceExtensionPoint; 098 099import com.google.common.collect.Streams; 100 101/** 102 * {@link Archiver} for all sites. 103 */ 104public class SitesArchiver extends AbstractLogEnabled implements Archiver, Serviceable 105{ 106 /** Archiver id. */ 107 public static final String ID = "sites"; 108 109 static final String __SITE_RESOURCES_JCR_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources"; 110 static final String __SITE_CONTENTS_JCR_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":contents"; 111 static final String __SITE_PLUGINS_JCR_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":plugins"; 112 113 static final String __COMMON_PREFIX = ID + "/"; 114 static final String __ACL_ZIP_ENTRY_FILENAME = "acl.xml"; 115 116 private static final String __PARTIAL_IMPORT_PREFIX = ID + "/"; 117 118 /** The site manager */ 119 protected SiteManager _siteManager; 120 /** The helper for archiving contents */ 121 protected ContentsArchiverHelper _contentsArchiverHelper; 122 /** The helper for archiving resources */ 123 protected ResourcesArchiverHelper _resourcesArchiverHelper; 124 /** The extension point for {@link SitePluginArchiver}s */ 125 protected SitePluginArchiverExtensionPoint _pluginArchiverExtensionPoint; 126 /** The extension point for {@link TagProvider}s */ 127 protected TagProviderExtensionPoint _tagProviderEP; 128 /** The Ametys Object Resolver */ 129 protected AmetysObjectResolver _resolver; 130 /** The default page factory */ 131 protected DefaultPageFactory _defaultPageFactory; 132 /** The sitemap factory */ 133 protected SitemapFactory _sitemapFactory; 134 /** The extension point for Services */ 135 protected ServiceExtensionPoint _serviceExtensionPoint; 136 /** The extension point for {@link SiteType}s */ 137 protected SiteTypesExtensionPoint _siteTypeEP; 138 /** The page data type extension point */ 139 protected ModelItemTypeExtensionPoint _pageDataTypeExtensionPoint; 140 /** The observation manager */ 141 protected ObservationManager _observationManager; 142 /** The current user provider */ 143 protected CurrentUserProvider _currentUserProvider; 144 145 private ManifestReaderWriter _manifestReaderWriter = new SitesArchiverManifestReaderWriter(); 146 147 @Override 148 public void service(ServiceManager manager) throws ServiceException 149 { 150 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 151 _contentsArchiverHelper = (ContentsArchiverHelper) manager.lookup(ContentsArchiverHelper.ROLE); 152 _resourcesArchiverHelper = (ResourcesArchiverHelper) manager.lookup(ResourcesArchiverHelper.ROLE); 153 _pluginArchiverExtensionPoint = (SitePluginArchiverExtensionPoint) manager.lookup(SitePluginArchiverExtensionPoint.ROLE); 154 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 155 _tagProviderEP = (TagProviderExtensionPoint) manager.lookup(TagProviderExtensionPoint.ROLE); 156 AmetysObjectFactoryExtensionPoint ametysObjectFactoryEP = (AmetysObjectFactoryExtensionPoint) manager.lookup(AmetysObjectFactoryExtensionPoint.ROLE); 157 _defaultPageFactory = (DefaultPageFactory) ametysObjectFactoryEP.getExtension(DefaultPageFactory.class.getName()); 158 _sitemapFactory = (SitemapFactory) ametysObjectFactoryEP.getExtension(SitemapFactory.class.getName()); 159 _serviceExtensionPoint = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE); 160 _siteTypeEP = (SiteTypesExtensionPoint) manager.lookup(SiteTypesExtensionPoint.ROLE); 161 _pageDataTypeExtensionPoint = (ModelItemTypeExtensionPoint) manager.lookup(ModelItemTypeExtensionPoint.ROLE_PAGE_DATA); 162 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 163 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 164 } 165 166 @Override 167 public ManifestReaderWriter getManifestReaderWriter() 168 { 169 return _manifestReaderWriter; 170 } 171 172 @Override 173 public void export(ZipOutputStream zos) throws IOException 174 { 175 zos.putNextEntry(new ZipEntry(__COMMON_PREFIX)); // even if there is no site, at least export the sites folder 176 177 try (AmetysObjectIterable<Site> sites = _siteManager.getSites()) 178 { 179 for (Site site : sites) 180 { 181 getLogger().info("Processing site '{}' for archiving", site.getName()); 182 _exportSite(site, zos); 183 } 184 } 185 } 186 187 private void _exportSite(Site site, ZipOutputStream zos) throws IOException 188 { 189 String siteName = site.getName(); 190 String path = Archivers.getHashedPath(siteName); 191 String prefix = __COMMON_PREFIX + path + "/" + siteName; 192 193 // export site's own data 194 ZipEntry siteEntry = new ZipEntry(prefix + "/" + siteName + ".xml"); 195 zos.putNextEntry(siteEntry); 196 197 try 198 { 199 TransformerHandler contentHandler = Archivers.newTransformerHandler(); 200 contentHandler.setResult(new StreamResult(zos)); 201 202 contentHandler.startDocument(); 203 Attributes attrs = _getSiteAttributes(site); 204 XMLUtils.startElement(contentHandler, "site", attrs); 205 site.dataToSAX(contentHandler); 206 XMLUtils.endElement(contentHandler, "site"); 207 contentHandler.endDocument(); 208 } 209 catch (SAXException | TransformerConfigurationException e) 210 { 211 throw new RuntimeException("Unable to SAX site '" + site.getPath() + "' for archiving", e); 212 } 213 214 // export site's resources 215 ResourceCollection rootResources = site.getChild(__SITE_RESOURCES_JCR_NODE_NAME); 216 _resourcesArchiverHelper.exportCollection(rootResources, zos, prefix + "/resources/"); 217 218 // export site's binaries 219 Archivers.exportBinaries(site, zos, prefix + "/"); 220 221 // export all site's contents 222 TraversableAmetysObject rootContents = site.getChild(__SITE_CONTENTS_JCR_NODE_NAME); 223 _contentsArchiverHelper.exportContents(prefix + "/contents/", rootContents, zos); 224 225 // export site's plugins data 226 try 227 { 228 _getPluginNodes(site) 229 .forEachRemaining(LambdaUtils.wrapConsumer(pluginNode -> _exportPlugin(site, pluginNode, zos, prefix))); 230 } 231 catch (RepositoryException e) 232 { 233 throw new IllegalArgumentException("Unable to archive plugins for site " + siteName, e); 234 } 235 236 _exportSitemaps(site, zos, prefix, siteName); 237 } 238 239 private Attributes _getSiteAttributes(Site site) 240 { 241 AttributesImpl attrs = new AttributesImpl(); 242 attrs.addCDATAAttribute("id", site.getId()); 243 attrs.addCDATAAttribute("type", site.getType()); 244 return attrs; 245 } 246 247 static Node _getAllPluginsNode(Site site) throws RepositoryException 248 { 249 return site.getNode() 250 .getNode(__SITE_PLUGINS_JCR_NODE_NAME); 251 } 252 253 private static Iterator<Node> _getPluginNodes(Site site) throws RepositoryException 254 { 255 return _getAllPluginsNode(site) 256 .getNodes(); 257 } 258 259 SitePluginArchiver _retrieveSitePluginArchiver(String pluginName) 260 { 261 SitePluginArchiver sitePluginArchiver = _pluginArchiverExtensionPoint.getExtension(pluginName); 262 if (sitePluginArchiver == null) 263 { 264 // there's no specific exporter for this plugin, let's fallback to the default export 265 sitePluginArchiver = _pluginArchiverExtensionPoint.getExtension(DefaultPluginArchiver.EXTENSION_ID); 266 267 if (sitePluginArchiver == null) 268 { 269 throw new IllegalStateException("There sould be a '__default' extension to SitePluginArchiverExtensionPoint. Please check your excluded features."); 270 } 271 } 272 273 return sitePluginArchiver; 274 } 275 276 private void _exportPlugin(Site site, Node pluginNode, ZipOutputStream zos, String prefix) throws Exception 277 { 278 String pluginName = pluginNode.getName(); 279 SitePluginArchiver sitePluginArchiver = _retrieveSitePluginArchiver(pluginName); 280 getLogger().info("Processing plugin '{}' for site '{}' for archiving at {} with archiver '{}'", pluginName, site.getName(), pluginNode, sitePluginArchiver.getClass().getName()); 281 sitePluginArchiver.export(site, pluginName, pluginNode, zos, prefix + "/plugins/" + pluginName); 282 } 283 284 private void _exportSitemaps(Site site, ZipOutputStream zos, String prefix, String siteName) throws IOException 285 { 286 // export all stored pages 287 try (AmetysObjectIterable<Sitemap> sitemaps = site.getSitemaps()) 288 { 289 for (Sitemap sitemap : sitemaps) 290 { 291 _exportSitemap(sitemap, zos, prefix); 292 } 293 } 294 catch (RepositoryException e) 295 { 296 throw new IllegalArgumentException("Unable to archive pages for site " + siteName, e); 297 } 298 } 299 300 private void _exportSitemap(Sitemap sitemap, ZipOutputStream zos, String prefix) throws IOException, RepositoryException 301 { 302 String sitemapPrefix = prefix + "/pages/" + sitemap.getName(); 303 zos.putNextEntry(new ZipEntry(sitemapPrefix + "/")); // even if there is no page, at least export the sitemap folder 304 _exportSitemapData(sitemap, zos, sitemapPrefix); 305 306 Stream<Node> nodes = Streams.stream(_getSitemapChildrenNodes(sitemap)); 307 Incrementor orderIncrementor = Incrementor.create() 308 .withStart(0) 309 .withMaximalCount(Integer.MAX_VALUE); 310 nodes.filter(LambdaUtils.wrapPredicate(node -> node.isNodeType("ametys:page"))) 311 .forEachOrdered(LambdaUtils.wrapConsumer(pageNode -> 312 { 313 _exportPage(pageNode, zos, sitemapPrefix, orderIncrementor.getCount()); 314 orderIncrementor.increment(); 315 })); 316 } 317 318 private void _exportSitemapData(Sitemap sitemap, ZipOutputStream zos, String sitemapPrefix) throws RepositoryException, IOException 319 { 320 ZipEntry siteEntry = new ZipEntry(sitemapPrefix + ".xml"); 321 zos.putNextEntry(siteEntry); 322 323 try 324 { 325 TransformerHandler contentHandler = Archivers.newTransformerHandler(); 326 contentHandler.setResult(new StreamResult(zos)); 327 328 contentHandler.startDocument(); 329 Attributes attrs = _getSitemapAttributes(sitemap); 330 XMLUtils.startElement(contentHandler, "sitemap", attrs); 331 332 // internal properties (virtual, ...) 333 _saxAmetysInternalProperties(sitemap, contentHandler); 334 335 XMLUtils.startElement(contentHandler, "attributes"); 336 sitemap.dataToSAX(contentHandler); 337 XMLUtils.endElement(contentHandler, "attributes"); 338 XMLUtils.endElement(contentHandler, "sitemap"); 339 contentHandler.endDocument(); 340 } 341 catch (SAXException | TransformerConfigurationException e) 342 { 343 throw new RuntimeException("Unable to SAX sitemap '" + sitemap.getPath() + "' for archiving", e); 344 } 345 346 Archivers.exportAcl(sitemap.getNode(), zos, ArchiveHandler.METADATA_PREFIX + sitemapPrefix + "/" + __ACL_ZIP_ENTRY_FILENAME); 347 } 348 349 private Attributes _getSitemapAttributes(Sitemap sitemap) 350 { 351 AttributesImpl attrs = new AttributesImpl(); 352 attrs.addCDATAAttribute("name", sitemap.getName()); 353 attrs.addCDATAAttribute("id", sitemap.getId()); 354 return attrs; 355 } 356 357 private static Iterator<Node> _getSitemapChildrenNodes(Sitemap sitemap) throws RepositoryException 358 { 359 return sitemap.getNode().getNodes(); 360 } 361 362 private void _exportPage(Node pageNode, ZipOutputStream zos, String prefix, int order) throws RepositoryException, IOException 363 { 364 String pageName = pageNode.getName(); 365 Page page = _resolver.resolve(pageNode, false); 366 367 String pagePrefix = prefix + "/" + pageName; 368 369 ZipEntry siteEntry = new ZipEntry(pagePrefix + ".xml"); 370 zos.putNextEntry(siteEntry); 371 372 try 373 { 374 TransformerHandler contentHandler = Archivers.newTransformerHandler(); 375 contentHandler.setResult(new StreamResult(zos)); 376 377 contentHandler.startDocument(); 378 Attributes attrs = _getPageAttributes(page, order); 379 380 XMLUtils.startElement(contentHandler, "page", attrs); 381 382 // internal properties (virtual, ...) 383 _saxAmetysInternalProperties(page, contentHandler); 384 385 XMLUtils.startElement(contentHandler, "attributes"); 386 page.dataToSAX(contentHandler); 387 XMLUtils.endElement(contentHandler, "attributes"); 388 389 // Tags 390 _saxTags(page, contentHandler); 391 392 XMLUtils.startElement(contentHandler, "templateParameters"); 393 page.getTemplateParametersHolder().dataToSAX(contentHandler); 394 XMLUtils.endElement(contentHandler, "templateParameters"); 395 396 XMLUtils.startElement(contentHandler, "pageContents"); 397 398 _saxZones(page, contentHandler); 399 400 XMLUtils.endElement(contentHandler, "pageContents"); 401 402 XMLUtils.endElement(contentHandler, "page"); 403 contentHandler.endDocument(); 404 } 405 catch (SAXException | TransformerConfigurationException e) 406 { 407 throw new RuntimeException("Unable to SAX page '" + page.getPath() + "' for archiving", e); 408 } 409 catch (RuntimeException e) 410 { 411 throw new RuntimeException("Unable to process Page for archiving: " + page.getId(), e); 412 } 413 414 _saxAttachments(page, pagePrefix, zos); 415 416 Archivers.exportAcl(pageNode, zos, ArchiveHandler.METADATA_PREFIX + pagePrefix + "/" + __ACL_ZIP_ENTRY_FILENAME); 417 418 Stream<Node> nodes = Streams.stream(_getPageChildrenNodes(pageNode)); 419 Incrementor orderIncrementor = Incrementor.create() 420 .withStart(0) 421 .withMaximalCount(Integer.MAX_VALUE); 422 nodes.filter(LambdaUtils.wrapPredicate(node -> node.isNodeType("ametys:page"))) 423 .forEachOrdered(LambdaUtils.wrapConsumer(node -> 424 { 425 _exportPage(node, zos, pagePrefix, orderIncrementor.getCount()); 426 orderIncrementor.increment(); 427 })); 428 } 429 430 private Attributes _getPageAttributes(Page page, int order) 431 { 432 AttributesImpl attrs = new AttributesImpl(); 433 attrs.addCDATAAttribute("title", page.getTitle()); 434 attrs.addCDATAAttribute("long-title", page.getLongTitle()); 435 attrs.addCDATAAttribute("id", page.getId()); 436 attrs.addCDATAAttribute("type", page.getType().toString()); 437 attrs.addCDATAAttribute("order", Integer.toString(order)); 438 439 if (page.getType() == PageType.LINK) 440 { 441 attrs.addCDATAAttribute("url", page.getURL()); 442 attrs.addCDATAAttribute("urlType", page.getURLType().toString()); 443 } 444 else if (page.getType() == PageType.CONTAINER) 445 { 446 attrs.addCDATAAttribute("template", page.getTemplate()); 447 } 448 449 return attrs; 450 } 451 452 private void _saxAmetysInternalProperties(SitemapElement sitemapElement, TransformerHandler contentHandler) throws SAXException 453 { 454 XMLUtils.startElement(contentHandler, "internal"); 455 if (sitemapElement instanceof JCRAmetysObject) 456 { 457 Node node = ((JCRAmetysObject) sitemapElement).getNode(); 458 try 459 { 460 _saxInternalPropeties(node, contentHandler, AmetysObjectResolver.VIRTUAL_PROPERTY); 461 } 462 catch (RepositoryException e) 463 { 464 throw new SAXException(e); 465 } 466 } 467 XMLUtils.endElement(contentHandler, "internal"); 468 } 469 470 private void _saxInternalPropeties(Node node, TransformerHandler contentHandler, String... properties) throws RepositoryException, SAXException 471 { 472 for (String propertyName : properties) 473 { 474 if (node.hasProperty(propertyName)) 475 { 476 Property property = node.getProperty(propertyName); 477 for (Value value : property.getValues()) 478 { 479 String propertyValue = value.getString(); 480 XMLUtils.createElement(contentHandler, propertyName, propertyValue); 481 } 482 } 483 } 484 } 485 486 private void _saxTags(Page page, TransformerHandler contentHandler) throws SAXException 487 { 488 XMLUtils.startElement(contentHandler, "tags"); 489 Set<String> tags = page.getTags(); 490 for (String tagName : tags) 491 { 492 Map<String, Object> contextParameters = new HashMap<>(); 493 contextParameters.put("siteName", page.getSiteName()); 494 495 Tag tag = _tagProviderEP.getTag(tagName, contextParameters); 496 497 if (tag != null) 498 { 499 // tag may be null if it has been registered on the page and then removed from the application 500 XMLUtils.createElement(contentHandler, tagName); 501 } 502 } 503 XMLUtils.endElement(contentHandler, "tags"); 504 } 505 506 private void _saxAttachments(Page page, String pagePrefix, ZipOutputStream zos) throws IOException 507 { 508 // Put page attachments under /_metadata 509 // It means that metadata of the attachments will be under /_metadata/_metadata 510 String prefix = ArchiveHandler.METADATA_PREFIX + pagePrefix + "/_attachments/"; 511 _resourcesArchiverHelper.exportCollection(page.getRootAttachments(), zos, prefix); 512 } 513 514 private void _saxZones(Page page, TransformerHandler contentHandler) throws SAXException 515 { 516 for (Zone zone : page.getZones()) 517 { 518 _saxZone(zone, contentHandler); 519 } 520 } 521 522 private void _saxZone(Zone zone, TransformerHandler contentHandler) throws SAXException 523 { 524 try 525 { 526 String zoneName = zone.getName(); 527 528 AttributesImpl zoneAttrs = new AttributesImpl(); 529 zoneAttrs.addCDATAAttribute("name", zoneName); 530 XMLUtils.startElement(contentHandler, "zone", zoneAttrs); 531 532 XMLUtils.startElement(contentHandler, "attributes"); 533 zone.dataToSAX(contentHandler); 534 XMLUtils.endElement(contentHandler, "attributes"); 535 536 XMLUtils.startElement(contentHandler, "zoneParameters"); 537 zone.getZoneParametersHolder().dataToSAX(contentHandler); 538 XMLUtils.endElement(contentHandler, "zoneParameters"); 539 540 AmetysObjectIterable<? extends ZoneItem> zoneItems = zone.getZoneItems(); 541 _saxZoneItems(zoneItems, contentHandler); 542 543 XMLUtils.endElement(contentHandler, "zone"); 544 } 545 catch (RuntimeException e) 546 { 547 throw new RuntimeException("Unable to process Zone for archiving: " + zone.getId(), e); 548 } 549 } 550 551 private void _saxZoneItems(AmetysObjectIterable<? extends ZoneItem> zoneItems, TransformerHandler contentHandler) throws SAXException 552 { 553 for (ZoneItem zoneItem : zoneItems) 554 { 555 try 556 { 557 _saxZoneItem(zoneItem, contentHandler); 558 } 559 catch (RuntimeException e) 560 { 561 throw new RuntimeException("Unable to process ZoneItem for archiving: " + zoneItem.getId(), e); 562 } 563 } 564 } 565 566 private void _saxZoneItem(ZoneItem zoneItem, TransformerHandler contentHandler) throws SAXException 567 { 568 ZoneType zoneType = zoneItem.getType(); 569 570 Attributes zoneItemAttrs = _getZoneItemAttributes(zoneItem, zoneType); 571 572 XMLUtils.startElement(contentHandler, "zoneItem", zoneItemAttrs); 573 574 XMLUtils.startElement(contentHandler, "attributes"); 575 zoneItem.dataToSAX(contentHandler); 576 XMLUtils.endElement(contentHandler, "attributes"); 577 578 XMLUtils.startElement(contentHandler, "zoneItemParameters"); 579 zoneItem.getZoneItemParametersHolder().dataToSAX(contentHandler); 580 XMLUtils.endElement(contentHandler, "zoneItemParameters"); 581 582 if (zoneType == ZoneType.SERVICE) 583 { 584 ModelAwareDataHolder serviceParameters = _getServiceParameters(zoneItem); 585 if (serviceParameters != null) 586 { 587 XMLUtils.startElement(contentHandler, "serviceParameters"); 588 serviceParameters.dataToSAX(contentHandler); 589 XMLUtils.endElement(contentHandler, "serviceParameters"); 590 591 XMLUtils.startElement(contentHandler, "serviceViewParameters"); 592 if (serviceParameters.hasDefinition(ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME)) 593 { 594 String viewName = serviceParameters.getValue(ViewParametersManager.SERVICE_VIEW_DEFAULT_MODEL_ITEM_NAME); 595 if (StringUtils.isNotBlank(viewName)) 596 { 597 ModelAwareDataHolder serviceViewParameters = zoneItem.getServiceViewParametersHolder(viewName); 598 if (serviceViewParameters != null) 599 { 600 serviceViewParameters.dataToSAX(contentHandler); 601 } 602 } 603 } 604 XMLUtils.endElement(contentHandler, "serviceViewParameters"); 605 } 606 } 607 else if (zoneType == ZoneType.CONTENT) 608 { 609 String viewName = StringUtils.defaultIfBlank(zoneItem.getViewName(), "main"); 610 ModelAwareDataHolder contentViewParameters = zoneItem.getContentViewParametersHolder(viewName); 611 612 XMLUtils.startElement(contentHandler, "contentViewParameters"); 613 contentViewParameters.dataToSAX(contentHandler); 614 XMLUtils.endElement(contentHandler, "contentViewParameters"); 615 } 616 617 XMLUtils.endElement(contentHandler, "zoneItem"); 618 } 619 620 private ModelAwareDataHolder _getServiceParameters(ZoneItem zoneItem) 621 { 622 try 623 { 624 return zoneItem.getServiceParameters(); 625 } 626 catch (Exception e) 627 { 628 getLogger().error("Cannot get service parameters for ZoneItem \"{}\"", zoneItem, e); 629 return null; 630 } 631 } 632 633 private Attributes _getZoneItemAttributes(ZoneItem zoneItem, ZoneType zoneType) 634 { 635 AttributesImpl zoneItemAttrs = new AttributesImpl(); 636 zoneItemAttrs.addCDATAAttribute("type", zoneType.toString()); 637 638 if (zoneType == ZoneType.CONTENT) 639 { 640 zoneItemAttrs.addCDATAAttribute("contentId", zoneItem.getContent().getId()); 641 642 String viewName = zoneItem.getViewName(); 643 if (viewName != null) 644 { 645 zoneItemAttrs.addCDATAAttribute("viewName", viewName); 646 } 647 } 648 else if (zoneType == ZoneType.SERVICE) 649 { 650 zoneItemAttrs.addCDATAAttribute("serviceId", zoneItem.getServiceId()); 651 } 652 653 return zoneItemAttrs; 654 } 655 656 private static Iterator<Node> _getPageChildrenNodes(Node pageNode) throws RepositoryException 657 { 658 return pageNode.getNodes(); 659 } 660 661 @Override 662 public List<I18nizableText> additionalSuccessImportMail() 663 { 664 return List.of( 665 new I18nizableText("plugin.web-contentio", "PLUGINS_WEB_CONTENTIO_ARCHIVE_IMPORT_SITE_ARCHIVER_MAIL_ADDITIONAL_BODY_REBUILD_LIVE"), 666 new I18nizableText("plugin.web-contentio", "PLUGINS_WEB_CONTENTIO_ARCHIVE_IMPORT_SITE_ARCHIVER_MAIL_ADDITIONAL_BODY_SITE_TOOL") 667 ); 668 } 669 670 @Override 671 public Collection<String> managedPartialImports(Collection<String> partialImports) 672 { 673 if (partialImports.contains(ID)) 674 { 675 // All sites 676 return Collections.singletonList(ID); 677 } 678 else 679 { 680 // Return a Collection (can be empty) of sites to be imported 681 return partialImports.stream() 682 .filter(partialImport -> partialImport.startsWith(__PARTIAL_IMPORT_PREFIX)) 683 .collect(Collectors.toList()); 684 } 685 } 686 687 @Override 688 public ImportReport partialImport(Path zipPath, Collection<String> partialImports, Merger merger, boolean deleteBefore) throws IOException 689 { 690 ModifiableTraversableAmetysObject siteRoot = _siteManager.getRoot(); 691 if (deleteBefore && siteRoot instanceof JCRAmetysObject) 692 { 693 _deleteBeforePartialImport(siteRoot, partialImports); 694 } 695 696 ImportReport result = _partialImport(zipPath, partialImports, merger, siteRoot); 697 _saveImported(siteRoot); 698 return result; 699 } 700 701 private void _deleteBeforePartialImport(ModifiableTraversableAmetysObject siteRoot, Collection<String> partialImports) throws IOException 702 { 703 if (!(siteRoot instanceof JCRAmetysObject)) 704 { 705 return; 706 } 707 708 JCRAmetysObject jcrSiteRoot = (JCRAmetysObject) siteRoot; 709 if (partialImports.contains(ID)) 710 { 711 _deleteSiteRootBeforePartialImport(jcrSiteRoot); 712 } 713 else 714 { 715 for (String siteName : _retrieveSiteNames(partialImports)) 716 { 717 _deleteSiteBeforePartialImport(siteName); 718 } 719 } 720 } 721 722 private void _deleteSiteRootBeforePartialImport(JCRAmetysObject siteRoot) throws IOException 723 { 724 try 725 { 726 Node rootNode = siteRoot.getNode(); 727 NodeIterator rootChildren = rootNode.getNodes(); 728 while (rootChildren.hasNext()) 729 { 730 rootChildren.nextNode().remove(); 731 } 732 rootNode.getSession().save(); 733 } 734 catch (RepositoryException e) 735 { 736 throw new IOException(e); 737 } 738 } 739 740 private void _deleteSiteBeforePartialImport(String siteName) throws IOException 741 { 742 if (_siteManager.hasSite(siteName)) 743 { 744 Node siteNode = _siteManager.getSite(siteName).getNode(); 745 try 746 { 747 Session parentNodeSession = siteNode.getParent().getSession(); 748 siteNode.remove(); 749 parentNodeSession.save(); 750 } 751 catch (RepositoryException e) 752 { 753 throw new IOException(e); 754 } 755 } 756 } 757 758 private Collection<String> _retrieveSiteNames(Collection<String> partialImports) 759 { 760 return partialImports.stream() 761 .map(partialImport -> StringUtils.substringAfter(partialImport, __PARTIAL_IMPORT_PREFIX)) 762 .collect(Collectors.toList()); 763 } 764 765 private ImportReport _partialImport(Path zipPath, Collection<String> partialImports, Merger merger, ModifiableTraversableAmetysObject siteRoot) throws IOException 766 { 767 try 768 { 769 var importer = new SiteImporter(this, siteRoot, zipPath, merger, getLogger()); 770 if (partialImports.contains(ID)) 771 { 772 importer.importSites(); 773 } 774 else 775 { 776 for (String siteName : _retrieveSiteNames(partialImports)) 777 { 778 importer.importSite(siteName); 779 } 780 } 781 return importer._report; 782 } 783 catch (ParserConfigurationException e) 784 { 785 throw new IOException(e); 786 } 787 } 788 789 private void _saveImported(ModifiableAmetysObject siteRoot) 790 { 791 if (siteRoot.needsSave()) 792 { 793 getLogger().warn(Archivers.WARN_MESSAGE_ROOT_HAS_PENDING_CHANGES, siteRoot); 794 siteRoot.saveChanges(); 795 } 796 } 797 798 private final class SitesArchiverManifestReaderWriter implements ManifestReaderWriter 799 { 800 @Override 801 public Object getData() 802 { 803 return _siteManager.getSiteNames(); 804 } 805 806 @Override 807 public Stream<PartialImport> toPartialImports(Object data) 808 { 809 return Stream.concat( 810 _allSitesToPartialImports(), 811 _sitesDataToPartialImports(data)); 812 } 813 814 private Stream<PartialImport> _allSitesToPartialImports() 815 { 816 return Stream.of(PartialImport.of(ID, new I18nizableText("plugin.web-contentio", "PLUGINS_WEB_CONTENTIO_ARCHIVE_IMPORT_SITE_ARCHIVER_OPTION_ALL"))); // for importing all sites 817 } 818 819 @SuppressWarnings("synthetic-access") 820 private Stream<PartialImport> _sitesDataToPartialImports(Object data) 821 { 822 return Optional.ofNullable(data) 823 .filter(Collection.class::isInstance) 824 .map(this::_castToCollection) 825 .orElseGet(() -> 826 { 827 getLogger().warn("Unexpected manifest data '{}', we would expect an array representing the site names. The ZIP archive probably comes from a different version of Ametys.", data); 828 return Collections.emptySet(); 829 }) 830 .stream() 831 .sorted() // sort by site name 832 .map(this::_toPartialImport); 833 } 834 835 private Collection<String> _castToCollection(Object data) 836 { 837 return Collection.class.cast(data); 838 } 839 840 private PartialImport _toPartialImport(String siteName) 841 { 842 String key = __PARTIAL_IMPORT_PREFIX + siteName; 843 return PartialImport.of(key, _toPartialImportLabel(siteName)); 844 } 845 846 private I18nizableText _toPartialImportLabel(String siteName) 847 { 848 // "Site 'wwww'" 849 return new I18nizableText("plugin.web-contentio", "PLUGINS_WEB_CONTENTIO_ARCHIVE_IMPORT_SITE_ARCHIVER_OPTION_ONE_SITE", Map.of("siteName", new I18nizableText(siteName))); 850 } 851 } 852}