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