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