001/*
002 *  Copyright 2020 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.io.InputStream;
020import java.nio.file.DirectoryStream;
021import java.nio.file.Files;
022import java.nio.file.Path;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.List;
027import java.util.Map;
028import java.util.Optional;
029import java.util.function.Function;
030import java.util.stream.Collectors;
031import java.util.stream.Stream;
032import java.util.stream.StreamSupport;
033
034import javax.jcr.Node;
035import javax.jcr.RepositoryException;
036import javax.xml.parsers.DocumentBuilder;
037import javax.xml.parsers.DocumentBuilderFactory;
038import javax.xml.parsers.ParserConfigurationException;
039import javax.xml.transform.TransformerException;
040
041import org.apache.commons.lang3.StringUtils;
042import org.apache.xpath.XPathAPI;
043import org.slf4j.Logger;
044import org.w3c.dom.Document;
045import org.w3c.dom.Element;
046import org.xml.sax.SAXException;
047
048import org.ametys.cms.repository.DefaultContent;
049import org.ametys.core.util.LambdaUtils;
050import org.ametys.plugins.contentio.archive.Archivers;
051import org.ametys.plugins.contentio.archive.Archivers.AmetysObjectNotImportedException;
052import org.ametys.plugins.contentio.archive.ContentsArchiverHelper.ContentFiller;
053import org.ametys.plugins.contentio.archive.ImportGlobalFailException;
054import org.ametys.plugins.contentio.archive.ImportReport;
055import org.ametys.plugins.contentio.archive.Merger;
056import org.ametys.plugins.contentio.archive.ResourcesAdditionalDataGetter;
057import org.ametys.plugins.contentio.archive.UnitaryImporter;
058import org.ametys.plugins.contentio.archive.ZipEntryHelper;
059import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
060import org.ametys.plugins.repository.collection.AmetysObjectCollection;
061import org.ametys.plugins.repository.data.extractor.xml.ModelAwareXMLValuesExtractor;
062import org.ametys.plugins.repository.data.extractor.xml.XMLValuesExtractorAdditionalDataGetter;
063import org.ametys.runtime.model.View;
064import org.ametys.web.repository.ModifiableSiteAwareAmetysObject;
065import org.ametys.web.repository.site.Site;
066import org.ametys.web.repository.site.SiteType;
067
068class SiteImporter
069{
070    final ImportReport _report = new ImportReport();
071    private final SitesArchiver _siteArchiver;
072    private final ModifiableTraversableAmetysObject _root;
073    private final Path _zipArchivePath;
074    private final Merger _merger;
075    private final Logger _logger;
076    private final DocumentBuilder _builder;
077    
078    SiteImporter(SitesArchiver siteArchiver, ModifiableTraversableAmetysObject root, Path zipArchivePath, Merger merger, Logger logger) throws ParserConfigurationException
079    {
080        _siteArchiver = siteArchiver;
081        _root = root;
082        _zipArchivePath = zipArchivePath;
083        _merger = merger;
084        _logger = logger;
085        _builder = DocumentBuilderFactory.newInstance()
086                .newDocumentBuilder();
087    }
088    
089    private class UnitarySiteImporter implements UnitaryImporter<Site>
090    {
091        private String _siteName;
092        
093        UnitarySiteImporter(String siteName)
094        {
095            _siteName = siteName;
096        }
097        
098        @Override
099        public String objectNameForLogs()
100        {
101            return "Site";
102        }
103
104        @Override
105        public Document getPropertiesXml(Path zipEntryPath) throws Exception
106        {
107            return _getSitePropertiesXml(zipEntryPath);
108        }
109
110        @Override
111        public String retrieveId(Document propertiesXml) throws Exception
112        {
113            return Archivers.xpathEvalNonEmpty("site/@id", propertiesXml);
114        }
115
116        @Override
117        public Site create(Path zipEntryPath, String id, Document propertiesXml) throws AmetysObjectNotImportedException, Exception
118        {
119            return _createSite(zipEntryPath, id, _siteName, propertiesXml);
120        }
121        
122        @Override
123        public ImportReport getReport()
124        {
125            return _report;
126        }
127        
128        @Override
129        public void unitaryImportFinalize()
130        {
131            _siteArchiver._siteManager.clearCache();
132            UnitaryImporter.super.unitaryImportFinalize();
133        }
134    }
135    
136    void importSites() throws IOException
137    {
138        Collection<Path> sitePaths = _retrieveSites();
139        for (Path sitePath : sitePaths)
140        {
141            String siteName = sitePath.getFileName().toString();
142            _importSite(sitePath, siteName);
143        }
144    }
145    
146    void importSite(String siteName) throws IOException
147    {
148        String path = Archivers.getHashedPath(siteName);
149        String prefix = SitesArchiver.__COMMON_PREFIX + path + "/" + siteName;
150        Path sitePath = ZipEntryHelper.zipFileRoot(_zipArchivePath)
151                .resolve(prefix);
152        _importSite(sitePath, siteName);
153    }
154    
155    private Collection<Path> _retrieveSites() throws IOException
156    {
157        Collection<Path> sitePaths = new SiteRetriever().getThreeDepthPaths();
158        return sitePaths;
159    }
160    
161    /**
162     * Inner class to retrieve from:
163     * 
164     * sites
165     * |_45
166     * __|_7d
167     * ____|_www
168     * |_7b
169     * __|_d7
170     * ____|_site1
171     * 
172     * the result paths:
173     * # /sites/45/7d/www
174     * # /sites/7b/d7/site1
175     * 
176     */
177    private class SiteRetriever
178    {
179        Collection<Path> getThreeDepthPaths() throws IOException
180        {
181            return _getThreeDepthPaths(SitesArchiver.__COMMON_PREFIX, 0)
182                    .collect(Collectors.toList());
183        }
184        
185        private Stream<Path> _getThreeDepthPaths(String currentPath, int count) throws IOException
186        {
187            DirectoryStream<Path> nextLevels = ZipEntryHelper.children(
188                _zipArchivePath, 
189                Optional.of(currentPath), 
190                Files::isDirectory);
191            return _getNextLevelPathDirectories(nextLevels, count);
192        }
193        
194        private Stream<Path> _getThreeDepthPaths(Path currentPath, int count) throws IOException
195        {
196            if (count == 3)
197            {
198                return Stream.of(currentPath);
199            }
200            else
201            {
202                return _getThreeDepthPaths(currentPath.toString(), count);
203            }
204        }
205        
206        private Stream<Path> _getNextLevelPathDirectories(DirectoryStream<Path> currentLevels, int count)
207        {
208            return StreamSupport.stream(currentLevels.spliterator(), false)
209                    .map(LambdaUtils.wrap(currentLevel -> _getThreeDepthPaths(currentLevel, count + 1)))
210                    .flatMap(Function.identity());
211        }
212    }
213    
214    private void _importSite(Path sitePath, String siteName) throws IOException
215    {
216        new UnitarySiteImporter(siteName)
217                .unitaryImport(_zipArchivePath, sitePath, _merger, _logger);
218    }
219    
220    private Document _getSitePropertiesXml(Path sitePath) throws IOException
221    {
222        String propertiesFileName = sitePath.getFileName() + ".xml";
223        String zipEntryPath = sitePath.resolve(propertiesFileName).toString();
224        try (InputStream stream = ZipEntryHelper.zipEntryFileInputStream(_zipArchivePath, zipEntryPath))
225        {
226            Document doc = _builder.parse(stream);
227            return doc;
228        }
229        catch (SAXException e)
230        {
231            throw new IOException(e);
232        }
233    }
234    
235    private Site _createSite(Path sitePath, String id, String siteName, Document propertiesXml) throws ImportGlobalFailException, AmetysObjectNotImportedException, RepositoryException, TransformerException, IOException, Exception
236    {
237        _logger.info("Importing site from '{}!{}' ...", _zipArchivePath, sitePath);
238        Site site = _createChildSite(id, siteName);
239        site.setType(Archivers.xpathEvalNonEmpty("site/@type", propertiesXml));
240        
241        XMLValuesExtractorAdditionalDataGetter additionalDataGetter = new ResourcesAdditionalDataGetter(_zipArchivePath, sitePath);
242        _setSiteProperties(site, propertiesXml, additionalDataGetter);
243        
244        Archivers.unitarySave(site.getNode(), _logger);
245        
246        ImportReport importResourceReport = _importResources(site, sitePath);
247        _report.addFrom(importResourceReport);
248        ImportReport importContentReport = _importContents(site, sitePath, siteName);
249        _report.addFrom(importContentReport);
250        ImportReport importPluginReport = _importPlugins(site, sitePath);
251        _report.addFrom(importPluginReport);
252        ImportReport importSitemapReport = _importSitemaps(site, sitePath);
253        _report.addFrom(importSitemapReport);
254        
255        return site;
256    }
257    
258    private Site _createChildSite(String id, String siteName) throws RepositoryException
259    {
260        String uuid = StringUtils.substringAfter(id, "://");
261        // Create a Node with JCR primary type "ametys:site"
262        // But then call '_replaceNodeWithDesiredUuid' to have it with the desired UUID (srcNode will be removed)
263        Node srcNode = ((Site) _root.createChild(siteName, "ametys:site")).getNode();
264        Node siteNode = Archivers.replaceNodeWithDesiredUuid(srcNode, uuid);
265        return _siteArchiver._resolver.resolve(_root.getPath(), siteNode, null, false);
266    }
267    
268    private void _setSiteProperties(Site site, Document propertiesXml, XMLValuesExtractorAdditionalDataGetter additionalDataGetter) throws TransformerException, Exception
269    {
270        Element siteElement = (Element) XPathAPI.selectSingleNode(propertiesXml, "site");
271        SiteType siteType = _siteArchiver._siteTypeEP.getExtension(site.getType());
272        View view = View.of(siteType);
273        Map<String, Object> values = new ModelAwareXMLValuesExtractor(siteElement, additionalDataGetter, siteType)
274                .extractValues(view);
275        
276        site.getDataHolder().synchronizeValues(view, values);
277    }
278    
279    private ImportReport _importResources(Site site, Path sitePath) throws IOException
280    {
281        String baseImportResourcePath = sitePath.resolve("resources")
282                .toString();
283        if (ZipEntryHelper.zipEntryFolderExists(_zipArchivePath, baseImportResourcePath))
284        {
285            Node parentOfRootResources = site.getNode();
286            return _siteArchiver._resourcesArchiverHelper.importCollection(baseImportResourcePath + "/", parentOfRootResources, _zipArchivePath, _merger);
287        }
288        else
289        {
290            _logger.info("No resource to be imported for Site '{}', the path '{}!{}' does not exist", site, _zipArchivePath, baseImportResourcePath);
291            return new ImportReport();
292        }
293    }
294    
295    private ImportReport _importContents(Site site, Path sitePath, String siteName) throws IOException
296    {
297        AmetysObjectCollection rootContents = site.hasChild(SitesArchiver.__SITE_CONTENTS_JCR_NODE_NAME)
298                ? site.getChild(SitesArchiver.__SITE_CONTENTS_JCR_NODE_NAME)
299                : site.createChild(SitesArchiver.__SITE_CONTENTS_JCR_NODE_NAME, "ametys:collection");
300        String baseImportContentPath = sitePath.resolve("contents")
301                .toString();
302        if (ZipEntryHelper.zipEntryFolderExists(_zipArchivePath, baseImportContentPath))
303        {
304            Collection<ContentFiller> contentFillers = Collections.singleton(content -> _setWebAttributes(content, siteName));
305            return _siteArchiver._contentsArchiverHelper.importContents(baseImportContentPath + "/", rootContents, _zipArchivePath, _merger, contentFillers);
306        }
307        else
308        {
309            _logger.info("No content to be imported for Site '{}', the path '{}!{}' does not exist", site, _zipArchivePath, baseImportContentPath);
310            return new ImportReport();
311        }
312    }
313    
314    private void _setWebAttributes(DefaultContent content, String siteName)
315    {
316        if (content instanceof ModifiableSiteAwareAmetysObject)
317        {
318            _logger.debug("Setting site name '{}' to freshly imported content {}", siteName, content);
319            ((ModifiableSiteAwareAmetysObject) content).setSiteName(siteName);
320        }
321    }
322    
323    private ImportReport _importPlugins(Site site, Path sitePath) throws IOException, RepositoryException
324    {
325        String baseImportPluginPath = sitePath.resolve("plugins")
326                .toString();
327        if (!ZipEntryHelper.zipEntryFolderExists(_zipArchivePath, baseImportPluginPath))
328        {
329            _logger.info("No plugin to be imported for Site '{}', the path '{}!{}' does not exist", site, _zipArchivePath, baseImportPluginPath);
330            return new ImportReport();
331        }
332        
333        List<ImportReport> reports = new ArrayList<>();
334        
335        DirectoryStream<Path> childPlugins = ZipEntryHelper.children(
336                _zipArchivePath, 
337                Optional.of(baseImportPluginPath), 
338                Files::isDirectory);
339        
340        try (childPlugins)
341        {
342            for (Path childPlugin : childPlugins)
343            {
344                ImportReport importPluginReport = _importPlugin(site, childPlugin);
345                reports.add(importPluginReport);
346            }
347        }
348        
349        return ImportReport.union(reports);
350    }
351    
352    private ImportReport _importPlugin(Site site, Path zipPluginEntryPath) throws IOException, RepositoryException
353    {
354        String pluginName = zipPluginEntryPath.getFileName().toString();
355        SitePluginArchiver sitePluginArchiver = _siteArchiver._retrieveSitePluginArchiver(pluginName);
356        Node allPluginsNode = SitesArchiver._getAllPluginsNode(site);
357        _logger.info("Importing site plugin '{}' ({}!{}) from archive with archiver '{}'", pluginName, _zipArchivePath, zipPluginEntryPath, sitePluginArchiver.getClass().getName());
358        String zipPluginEntryPathStr = zipPluginEntryPath.toString();
359        return sitePluginArchiver.partialImport(site, pluginName, allPluginsNode, _zipArchivePath, zipPluginEntryPathStr, _merger);
360    }
361    
362    private ImportReport _importSitemaps(Site site, Path sitePath) throws RepositoryException, IOException
363    {
364        String baseImportSitemapPath = sitePath.resolve("pages")
365                .toString();
366        if (!ZipEntryHelper.zipEntryFolderExists(_zipArchivePath, baseImportSitemapPath))
367        {
368            _logger.info("No sitemap to be imported for Site '{}', the path '{}!{}' does not exist", site, _zipArchivePath, baseImportSitemapPath);
369            return new ImportReport();
370        }
371        
372        List<ImportReport> reports = new ArrayList<>();
373        
374        DirectoryStream<Path> sitemaps = ZipEntryHelper.children(
375                _zipArchivePath, 
376                Optional.of(baseImportSitemapPath), 
377                Files::isDirectory);
378        
379        try (sitemaps)
380        {
381            for (Path sitemap : sitemaps)
382            {
383                var sitemapImporter = new SitemapImporter(_siteArchiver, site, sitemap, _zipArchivePath, _merger, _logger, _builder);
384                sitemapImporter.importSitemap();
385                reports.add(sitemapImporter._report);
386            }
387        }
388        
389        return ImportReport.union(reports);
390    }
391}