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}