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.contentio.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.time.LocalDate;
024import java.time.ZoneId;
025import java.time.ZonedDateTime;
026import java.time.format.DateTimeFormatter;
027import java.util.ArrayList;
028import java.util.Calendar;
029import java.util.Date;
030import java.util.GregorianCalendar;
031import java.util.List;
032import java.util.Objects;
033import java.util.Optional;
034import java.util.zip.ZipEntry;
035import java.util.zip.ZipOutputStream;
036
037import javax.jcr.Binary;
038import javax.jcr.Node;
039import javax.jcr.RepositoryException;
040import javax.jcr.Session;
041import javax.xml.parsers.DocumentBuilder;
042import javax.xml.parsers.DocumentBuilderFactory;
043import javax.xml.parsers.ParserConfigurationException;
044import javax.xml.transform.TransformerConfigurationException;
045import javax.xml.transform.TransformerException;
046import javax.xml.transform.sax.TransformerHandler;
047import javax.xml.transform.stream.StreamResult;
048
049import org.apache.avalon.framework.component.Component;
050import org.apache.avalon.framework.context.ContextException;
051import org.apache.avalon.framework.context.Contextualizable;
052import org.apache.avalon.framework.service.ServiceException;
053import org.apache.avalon.framework.service.ServiceManager;
054import org.apache.avalon.framework.service.Serviceable;
055import org.apache.cocoon.Constants;
056import org.apache.cocoon.environment.Context;
057import org.apache.cocoon.xml.XMLUtils;
058import org.apache.commons.io.IOUtils;
059import org.apache.commons.lang3.StringUtils;
060import org.apache.xpath.XPathAPI;
061import org.slf4j.Logger;
062import org.w3c.dom.Document;
063import org.xml.sax.ContentHandler;
064import org.xml.sax.SAXException;
065
066import org.ametys.core.user.UserIdentity;
067import org.ametys.core.util.DateUtils;
068import org.ametys.plugins.contentio.archive.Archivers.AmetysObjectNotImportedException;
069import org.ametys.plugins.explorer.resources.Resource;
070import org.ametys.plugins.explorer.resources.ResourceCollection;
071import org.ametys.plugins.explorer.resources.jcr.JCRResource;
072import org.ametys.plugins.explorer.resources.jcr.JCRResourceFactory;
073import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollection;
074import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory;
075import org.ametys.plugins.repository.AmetysObject;
076import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint;
077import org.ametys.plugins.repository.AmetysObjectIterable;
078import org.ametys.plugins.repository.AmetysRepositoryException;
079import org.ametys.plugins.repository.RepositoryConstants;
080import org.ametys.plugins.repository.dublincore.DublinCoreAwareAmetysObject;
081import org.ametys.plugins.repository.dublincore.ModifiableDublinCoreAwareAmetysObject;
082import org.ametys.plugins.repository.jcr.JCRAmetysObject;
083import org.ametys.runtime.plugin.component.AbstractLogEnabled;
084
085/**
086 * Export a resources collection as individual files.
087 */
088public class ResourcesArchiverHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable
089{
090    /** Avalon role. */
091    public static final String ROLE = ResourcesArchiverHelper.class.getName();
092    
093    private static final String __PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX = "properties.xml";
094    private static final String __DC_METADATA_XML_FILE_NAME_SUFFIX = "dc.xml";
095    
096    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_ROOT = "dublin-core-metadata";
097    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_TITLE = "title";
098    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_CREATOR = "creator";
099    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_SUBJECT = "subject";
100    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_DESCRIPTION = "description";
101    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_PUBLISHER = "publisher";
102    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_CONTRIBUTOR = "contributor";
103    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_DATE = "date";
104    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_TYPE = "type";
105    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_FORMAT = "format";
106    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_IDENTIFIER = "identifier";
107    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_SOURCE = "source";
108    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_LANGUAGE = "language";
109    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_RELATION = "relation";
110    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_COVERAGE = "coverage";
111    private static final String __DC_METADATA_XML_EXPORT_TAG_NAME_RIGHTS = "rights";
112    
113    private JCRResourcesCollectionFactory _jcrResourcesCollectionFactory;
114    private JCRResourceFactory _jcrResourceFactory;
115    
116    private Context _cocoonContext;
117    
118    @Override
119    public void service(ServiceManager manager) throws ServiceException
120    {
121        AmetysObjectFactoryExtensionPoint ametysObjectFactoryEP = (AmetysObjectFactoryExtensionPoint) manager.lookup(AmetysObjectFactoryExtensionPoint.ROLE);
122        _jcrResourcesCollectionFactory = (JCRResourcesCollectionFactory) ametysObjectFactoryEP.getExtension(JCRResourcesCollectionFactory.class.getName());
123        _jcrResourceFactory = (JCRResourceFactory) ametysObjectFactoryEP.getExtension(JCRResourceFactory.class.getName());
124    }
125    
126    @Override
127    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
128    {
129        _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT);
130    }
131    
132    /**
133     * Exports a {@link ResourceCollection} as folders and files inside the ZIP archive.
134     * @param collection the root {@link ResourceCollection}
135     * @param zos the ZIP OutputStream.
136     * @param prefix the prefix for the ZIP archive.
137     * @throws IOException if an error occurs while archiving
138     */
139    public void exportCollection(ResourceCollection collection, ZipOutputStream zos, String prefix) throws IOException
140    {
141        if (collection == null)
142        {
143            return;
144        }
145        
146        zos.putNextEntry(new ZipEntry(StringUtils.appendIfMissing(prefix, "/"))); // even if there is no child, at least export the collection folder
147        
148        try (AmetysObjectIterable<AmetysObject> objects = collection.getChildren();)
149        {
150            for (AmetysObject object : objects)
151            {
152                _exportChild(object, zos, prefix);
153            }
154        }
155        
156        try
157        {
158            // process metadata for the collection
159            _exportCollectionMetadataEntry(collection, zos, prefix);
160            // process ACL for the collection
161            Node collectionNode = ((JCRAmetysObject) collection).getNode();
162            Archivers.exportAcl(collectionNode, zos, ArchiveHandler.METADATA_PREFIX + prefix + "acl.xml");
163        }
164        catch (RepositoryException e)
165        {
166            throw new RuntimeException("Unable to SAX some information for collection '" + collection.getPath() + "' for archiving", e);
167        }
168    }
169    
170    private void _exportCollectionMetadataEntry(ResourceCollection collection, ZipOutputStream zos, String prefix) throws IOException
171    {
172        String metadataFullPrefix = ArchiveHandler.METADATA_PREFIX + prefix;
173        ZipEntry contentEntry = new ZipEntry(metadataFullPrefix + __PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX);
174        zos.putNextEntry(contentEntry);
175        
176        try
177        {
178            TransformerHandler contentHandler = Archivers.newTransformerHandler();
179            contentHandler.setResult(new StreamResult(zos));
180            
181            contentHandler.startDocument();
182            _saxSystemMetadata(collection, contentHandler);
183            contentHandler.endDocument();
184        }
185        catch (SAXException | TransformerConfigurationException e)
186        {
187            throw new RuntimeException("Unable to SAX properties for collection '" + collection.getPath() + "' for archiving", e);
188        }
189    }
190    
191    private void _exportChild(AmetysObject child, ZipOutputStream zos, String prefix) throws IOException
192    {
193        if (child instanceof ResourceCollection)
194        {
195            String newPrefix = prefix + child.getName() + "/";
196            exportCollection((ResourceCollection) child, zos, newPrefix);
197        }
198        else if (child instanceof Resource)
199        {
200            exportResource((Resource) child, zos, prefix);
201        }
202    }
203    
204    /**
205     * Exports a {@link Resource} as  file inside the ZIP archive.
206     * @param resource the {@link Resource}.
207     * @param zos the ZIP OutputStream.
208     * @param prefix the prefix for the ZIP archive.
209     * @throws IOException if an error occurs while archiving
210     */
211    public void exportResource(Resource resource, ZipOutputStream zos, String prefix) throws IOException
212    {
213        ZipEntry resourceEntry = new ZipEntry(prefix + resource.getName());
214        zos.putNextEntry(resourceEntry);
215
216        try (InputStream is = resource.getInputStream())
217        {
218            IOUtils.copy(is, zos);
219        }
220        
221        // dublin core properties
222        _exportResourceMetadataEntry(resource, zos, prefix, __DC_METADATA_XML_FILE_NAME_SUFFIX, this::_saxDublinCoreMetadata, "Dublin Core metadata");
223        
224        // other properties (id, creator...)
225        _exportResourceMetadataEntry(resource, zos, prefix, __PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX, this::_saxSystemMetadata, "properties");
226    }
227    
228    private void _exportResourceMetadataEntry(Resource resource, ZipOutputStream zos, String prefix, String metadataFileNameSuffix, ResourceMetadataSaxer metadataSaxer, String debugName) throws IOException
229    {
230        String metadataFullPrefix = ArchiveHandler.METADATA_PREFIX + prefix + resource.getName() + "_";
231        ZipEntry contentEntry = new ZipEntry(metadataFullPrefix + metadataFileNameSuffix);
232        zos.putNextEntry(contentEntry);
233        
234        try
235        {
236            TransformerHandler contentHandler = Archivers.newTransformerHandler();
237            contentHandler.setResult(new StreamResult(zos));
238            
239            contentHandler.startDocument();
240            metadataSaxer.sax(resource, contentHandler);
241            contentHandler.endDocument();
242        }
243        catch (SAXException | TransformerConfigurationException e)
244        {
245            throw new RuntimeException("Unable to SAX " + debugName + " for resource '" + resource.getPath() + "' for archiving", e);
246        }
247    }
248    
249    @FunctionalInterface
250    private static interface ResourceMetadataSaxer
251    {
252        void sax(Resource resource, ContentHandler contentHandler) throws SAXException;
253    }
254    
255    private void _saxDublinCoreMetadata(DublinCoreAwareAmetysObject dcObject, ContentHandler contentHandler) throws SAXException
256    {
257        XMLUtils.startElement(contentHandler, __DC_METADATA_XML_EXPORT_TAG_NAME_ROOT);
258        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_TITLE, dcObject.getDCTitle(), contentHandler);
259        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_CREATOR, dcObject.getDCCreator(), contentHandler);
260        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_SUBJECT, dcObject.getDCSubject(), contentHandler);
261        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_DESCRIPTION, dcObject.getDCDescription(), contentHandler);
262        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_PUBLISHER, dcObject.getDCPublisher(), contentHandler);
263        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_CONTRIBUTOR, dcObject.getDCContributor(), contentHandler);
264        _saxLocalDateIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_DATE, dcObject.getDCDate(), contentHandler);
265        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_TYPE, dcObject.getDCType(), contentHandler);
266        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_FORMAT, dcObject.getDCFormat(), contentHandler);
267        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_IDENTIFIER, dcObject.getDCIdentifier(), contentHandler);
268        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_SOURCE, dcObject.getDCSource(), contentHandler);
269        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_LANGUAGE, dcObject.getDCLanguage(), contentHandler);
270        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_RELATION, dcObject.getDCRelation(), contentHandler);
271        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_COVERAGE, dcObject.getDCCoverage(), contentHandler);
272        _saxIfNotNull(__DC_METADATA_XML_EXPORT_TAG_NAME_RIGHTS, dcObject.getDCRights(), contentHandler);
273        XMLUtils.endElement(contentHandler, __DC_METADATA_XML_EXPORT_TAG_NAME_ROOT);
274    }
275    
276    private void _saxSystemMetadata(Resource resource, ContentHandler contentHandler) throws SAXException
277    {
278        XMLUtils.startElement(contentHandler, "resource");
279        _saxIfNotNull("id", resource.getId(), contentHandler);
280        _saxIfNotNull("name", resource.getName(), contentHandler);
281        _saxIfNotNull("creator", resource.getCreator(), contentHandler);
282        _saxZonedDateTimeIfNotNull("creationDate", resource.getCreationDate(), contentHandler);
283        _saxIfNotNull("contributor", resource.getLastContributor(), contentHandler);
284        _saxZonedDateTimeIfNotNull("lastModified", resource.getLastModified(), contentHandler);
285        XMLUtils.endElement(contentHandler, "resource");
286    }
287    
288    private void _saxSystemMetadata(ResourceCollection collection, ContentHandler contentHandler) throws SAXException
289    {
290        XMLUtils.startElement(contentHandler, "resource-collection");
291        _saxIfNotNull("id", collection.getId(), contentHandler);
292        _saxIfNotNull("name", collection.getName(), contentHandler);
293        XMLUtils.endElement(contentHandler, "resource-collection");
294    }
295
296    private void _saxIfNotNull(String name, String value, ContentHandler contentHandler) throws SAXException
297    {
298        if (value != null)
299        {
300            XMLUtils.createElement(contentHandler, name, value);
301        }
302    }
303    
304    private void _saxIfNotNull(String name, UserIdentity value, ContentHandler contentHandler) throws SAXException
305    {
306        if (value != null)
307        {
308            XMLUtils.createElement(contentHandler, name, UserIdentity.userIdentityToString(value));
309        }
310    }
311    
312    private void _saxIfNotNull(String name, String[] values, ContentHandler contentHandler) throws SAXException
313    {
314        if (values != null)
315        {
316            for (String value : values)
317            {
318                XMLUtils.createElement(contentHandler, name, value);
319            }
320        }
321    }
322    
323    private void _saxLocalDateIfNotNull(String name, Date value, ContentHandler contentHandler) throws SAXException
324    {
325        if (value != null)
326        {
327            LocalDate ld = DateUtils.asLocalDate(value);
328            XMLUtils.createElement(contentHandler, name, ld.format(DateTimeFormatter.ISO_LOCAL_DATE));
329        }
330    }
331    
332    private void _saxZonedDateTimeIfNotNull(String name, Date value, ContentHandler contentHandler) throws SAXException
333    {
334        if (value != null)
335        {
336            ZonedDateTime zdt = DateUtils.asZonedDateTime(value, ZoneId.systemDefault());
337            XMLUtils.createElement(contentHandler, name, zdt.format(DateUtils.getISODateTimeFormatter()));
338        }
339    }
340    
341    /**
342     * Imports folders and files from the given ZIP archive and path, under the given {@link ResourceCollection}
343     * @param commonPrefix The common prefix in the ZIP archive
344     * @param parentOfRootResources the parent of the root {@link ResourceCollection} (the root will also be created)
345     * @param zipPath the input zip path
346     * @param merger The {@link Merger}
347     * @return The {@link ImportReport}
348     * @throws IOException if an error occurs while importing archive
349     */
350    public ImportReport importCollection(String commonPrefix, Node parentOfRootResources, Path zipPath, Merger merger) throws IOException
351    {
352        Importer importer;
353        List<JCRResource> importedResources;
354        try
355        {
356            importer = new Importer(commonPrefix, parentOfRootResources, zipPath, merger, getLogger());
357            importer.importRoot();
358            importedResources = importer.getImportedResource();
359        }
360        catch (ParserConfigurationException e)
361        {
362            throw new IOException(e);
363        }
364        _saveImported(parentOfRootResources);
365        _checkoutImported(importedResources);
366        return importer._report;
367    }
368    
369    private void _saveImported(Node parentOfRoot)
370    {
371        try
372        {
373            Session session = parentOfRoot.getSession();
374            if (session.hasPendingChanges())
375            {
376                getLogger().warn(Archivers.WARN_MESSAGE_ROOT_HAS_PENDING_CHANGES, parentOfRoot);
377                session.save();
378            }
379        }
380        catch (RepositoryException e)
381        {
382            throw new AmetysRepositoryException("Unable to save changes", e);
383        }
384    }
385    
386    private void _checkoutImported(List<JCRResource> importedResources)
387    {
388        for (JCRResource resource : importedResources)
389        {
390            resource.checkpoint();
391        }
392    }
393    
394    private class Importer
395    {
396        final ImportReport _report = new ImportReport();
397        private final String _commonPrefix;
398        private final Node _parentOfRoot;
399        private final Path _zipArchivePath;
400        private final Merger _merger;
401        private final Logger _logger;
402        private final DocumentBuilder _builder;
403        private final List<JCRResource> _importedResources = new ArrayList<>();
404        private JCRResourcesCollection _root;
405        private final UnitaryCollectionImporter _unitaryCollectionImporter = new UnitaryCollectionImporter();
406        private final UnitaryResourceImporter _unitaryResourceImporter = new UnitaryResourceImporter();
407        
408        Importer(String commonPrefix, Node parentOfRoot, Path zipArchivePath, Merger merger, Logger logger) throws ParserConfigurationException
409        {
410            _commonPrefix = commonPrefix;
411            _parentOfRoot = parentOfRoot;
412            _zipArchivePath = zipArchivePath;
413            _merger = merger;
414            _logger = logger;
415            _builder = DocumentBuilderFactory.newInstance()
416                    .newDocumentBuilder();
417        }
418        
419        void importRoot() throws IOException
420        {
421            if (ZipEntryHelper.zipEntryFolderExists(_zipArchivePath, _commonPrefix))
422            {
423                Path rootFolderToImport = ZipEntryHelper.zipFileRoot(_zipArchivePath)
424                        .resolve(_commonPrefix);
425                
426                _importResourceCollectionAndChildren(rootFolderToImport);
427            }
428        }
429        
430        List<JCRResource> getImportedResource()
431        {
432            return _importedResources;
433        }
434        
435        private void _createResourceCollectionAcl(Node collectionNode, String folderPath) throws IOException
436        {
437            String zipEntryPath = new StringBuilder()
438                    .append(ArchiveHandler.METADATA_PREFIX)
439                    .append(StringUtils.strip(folderPath, "/"))
440                    .append("/acl.xml")
441                    .toString();
442            try
443            {
444                _logger.debug("Trying to import ACL node for ResourcesCollection '{}', from ACL XML file '{}', if it exists", collectionNode, zipEntryPath);
445                Archivers.importAcl(collectionNode, _zipArchivePath, _merger, zipEntryPath, _logger);
446            }
447            catch (RepositoryException e)
448            {
449                throw new IOException(e);
450            }
451        }
452        
453        private void _importChildren(Path folder) throws IOException
454        {
455            String pathPrefix = _relativePath(folder);
456            
457            DirectoryStream<Path> childFiles = _getDirectFileChildren(pathPrefix);
458            try (childFiles)
459            {
460                for (Path childFile : childFiles)
461                {
462                    _importResource(childFile)
463                            .ifPresent(_importedResources::add);
464                }
465            }
466            
467            DirectoryStream<Path> childFolders = _getDirectFolderChildren(pathPrefix);
468            try (childFolders)
469            {
470                for (Path childFolder : childFolders)
471                {
472                    _importResourceCollectionAndChildren(childFolder);
473                }
474            }
475        }
476        
477        private DirectoryStream<Path> _getDirectFolderChildren(String pathPrefix) throws IOException
478        {
479            return ZipEntryHelper.children(
480                _zipArchivePath, 
481                Optional.of(_commonPrefix + pathPrefix), 
482                p -> Files.isDirectory(p));
483        }
484        
485        private DirectoryStream<Path> _getDirectFileChildren(String pathPrefix) throws IOException
486        {
487            return ZipEntryHelper.children(
488                _zipArchivePath, 
489                Optional.of(_commonPrefix + pathPrefix), 
490                p -> !Files.isDirectory(p));
491        }
492        
493        private void _importResourceCollectionAndChildren(Path folder) throws IOException
494        {
495            Optional<Node> optionalCollectionNode = _importResourceCollection(folder);
496            if (optionalCollectionNode.isPresent())
497            {
498                Node collectionNode = optionalCollectionNode.get();
499                _createResourceCollectionAcl(collectionNode, folder.toString());
500                _importChildren(folder);
501            }
502        }
503        
504        private Optional<Node> _importResourceCollection(Path folder) throws ImportGlobalFailException
505        {
506            return _unitaryCollectionImporter.unitaryImport(_zipArchivePath, folder, _merger, _logger);
507        }
508        
509        private Document _getFolderPropertiesXml(Path folder) throws IOException
510        {
511            String zipEntryPath = new StringBuilder()
512                    .append(ArchiveHandler.METADATA_PREFIX)
513                    .append(StringUtils.strip(folder.toString(), "/"))
514                    .append("/")
515                    .append(__PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX)
516                    .toString();
517            try (InputStream stream = ZipEntryHelper.zipEntryFileInputStream(_zipArchivePath, zipEntryPath))
518            {
519                Document doc = _builder.parse(stream);
520                return doc;
521            }
522            catch (SAXException e)
523            {
524                throw new IOException(e);
525            }
526        }
527        
528        private Node _createResourceCollection(Path folder, String id, Document propertiesXml) throws IOException, AmetysObjectNotImportedException, TransformerException
529        {
530            boolean isRoot = _isRoot(folder);
531            Node parentNode = _retrieveParentJcrNode(folder, isRoot);
532            String uuid = StringUtils.substringAfter(id, "://");
533            String collectionName = Archivers.xpathEvalNonEmpty("resource-collection/name", propertiesXml);
534            _logger.info("Creating a ResourcesCollection object for '{}' folder (id={})", folder, id);
535            
536            try
537            {
538                Node resourceCollection = _createChildResourceCollection(parentNode, uuid, collectionName);
539                if (isRoot)
540                {
541                    _root = _jcrResourcesCollectionFactory.getAmetysObject(resourceCollection, null);
542                }
543                return resourceCollection;
544            }
545            catch (RepositoryException e)
546            {
547                throw new IOException(e);
548            }
549        }
550        
551        private boolean _isRoot(Path folder)
552        {
553            String folderPath = StringUtils.strip(folder.toString(), "/");
554            String commonPrefixToCompare = StringUtils.strip(_commonPrefix, "/");
555            return commonPrefixToCompare.equals(folderPath);
556        }
557        
558        private Node _createChildResourceCollection(Node parentNode, String uuid, String collectionName) throws RepositoryException
559        {
560            // Create a Node with JCR primary type "ametys:resources-collection"
561            // But then call 'replaceNodeWithDesiredUuid' to have it with the desired UUID (srcNode will be removed)
562            Node srcNode = parentNode.addNode(collectionName, JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE);
563            Node nodeWithDesiredUuid = Archivers.replaceNodeWithDesiredUuid(srcNode, uuid);
564            return nodeWithDesiredUuid;
565        }
566        
567        private String _relativePath(Path folderOrFile)
568        {
569            // for instance, _commonPrefix="resources/"
570            // relPath=folderOrFile.toString()="/resources/foo/bar"
571            // it should return "foo/bar"
572            
573            // for instance, _commonPrefix="resources/"
574            // relPath=folderOrFile.toString()="/resources"
575            // it should return ""
576            
577            String commonPrefixToRemove = "/" + StringUtils.strip(_commonPrefix, "/");
578            String relPath = folderOrFile.toString();
579            relPath = relPath.startsWith(commonPrefixToRemove)
580                    ? StringUtils.substringAfter(relPath, commonPrefixToRemove)
581                    : relPath;
582            relPath = StringUtils.strip(relPath, "/");
583            return relPath;
584        }
585        
586        private Node _retrieveParentJcrNode(Path fileOrFolder, boolean isRoot)
587        {
588            if (isRoot)
589            {
590                // is the root, thus return the parent of the root
591                return _parentOfRoot;
592            }
593            
594            if (_root == null)
595            {
596                throw new IllegalStateException("Unexpected error, the root must have been created before.");
597            }
598            
599            Path parent = fileOrFolder.getParent();
600            String parentRelPath = _relativePath(parent); 
601            return parentRelPath.isEmpty()
602                    ? _root.getNode()
603                    : _jcrResourcesCollectionFactory.<JCRResourcesCollection>getChild(_root, parentRelPath).getNode();
604        }
605        
606        private Optional<JCRResource> _importResource(Path file) throws ImportGlobalFailException
607        {
608            return _unitaryResourceImporter.unitaryImport(_zipArchivePath, file, _merger, _logger);
609        }
610        
611        private Document _getFilePropertiesXml(Path file) throws IOException
612        {
613            String zipEntryPath = new StringBuilder()
614                    .append(ArchiveHandler.METADATA_PREFIX)
615                    .append(StringUtils.strip(file.toString(), "/"))
616                    .append("_")
617                    .append(__PROPERTIES_METADATA_XML_FILE_NAME_SUFFIX)
618                    .toString();
619            try (InputStream stream = ZipEntryHelper.zipEntryFileInputStream(_zipArchivePath, zipEntryPath))
620            {
621                Document doc = _builder.parse(stream);
622                return doc;
623            }
624            catch (SAXException e)
625            {
626                throw new IOException(e);
627            }
628        }
629        
630        private JCRResource _createdResource(Path file, String id, Document propertiesXml) throws IOException, AmetysObjectNotImportedException
631        {
632            Node parentNode = _retrieveParentJcrNode(file, false);
633            String uuid = StringUtils.substringAfter(id, "://");
634            String resourceName = file.getFileName().toString();
635            _logger.info("Creating a Resource object for '{}' file (id={})", file, id);
636            
637            try
638            {
639                Node resourceNode = _createChildResource(parentNode, uuid, resourceName);
640                _setResourceData(resourceNode, file, propertiesXml);
641                _setResourceProperties(resourceNode, propertiesXml);
642                _setResourceMetadata(resourceNode, file);
643                
644                JCRResource createdResource = _resolveResource(resourceNode);
645                return createdResource;
646            }
647            catch (TransformerException | RepositoryException e)
648            {
649                throw new IOException(e);
650            }
651        }
652        
653        private Node _createChildResource(Node parentNode, String uuid, String resourceName) throws RepositoryException
654        {
655            // Create a Node with JCR primary type "ametys:resource"
656            // But then call 'replaceNodeWithDesiredUuid' to have it with the desired UUID (srcNode will be removed)
657            Node srcNode = parentNode.addNode(resourceName, "ametys:resource");
658            Node nodeWithDesiredUuid = Archivers.replaceNodeWithDesiredUuid(srcNode, uuid);
659            return nodeWithDesiredUuid;
660        }
661        
662        private JCRResource _resolveResource(Node resourceNode)
663        {
664            return _jcrResourceFactory.getAmetysObject(resourceNode, null);
665        }
666        
667        private void _setResourceData(Node resourceNode, Path file, Document propertiesXml) throws RepositoryException, IOException, TransformerException
668        {
669            Node resourceContentNode = resourceNode.addNode("jcr:content", "nt:resource");
670            
671            String mimeType = _getMimeType(file);
672            resourceContentNode.setProperty("jcr:mimeType", mimeType);
673            
674            try (InputStream stream = ZipEntryHelper.zipEntryFileInputStream(_zipArchivePath, file.toString()))
675            {
676                Binary binary = resourceNode.getSession()
677                        .getValueFactory()
678                        .createBinary(stream);
679                resourceContentNode.setProperty("jcr:data", binary);
680            }
681            
682            Date lastModified = Objects.requireNonNull(DomNodeHelper.nullableDatetimeValue(propertiesXml, "resource/lastModified"));
683            Calendar lastModifiedCal = new GregorianCalendar();
684            lastModifiedCal.setTime(lastModified);
685            resourceContentNode.setProperty("jcr:lastModified", lastModifiedCal);
686        }
687        
688        private void _setResourceProperties(Node resourceNode, Document propertiesXml) throws TransformerException, AmetysObjectNotImportedException, RepositoryException
689        {
690            UserIdentity contributor = UserIdentity.stringToUserIdentity(Archivers.xpathEvalNonEmpty("resource/contributor", propertiesXml));
691            Node lastContributorNode = resourceNode.addNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + JCRResource.CONTRIBUTOR_NODE_NAME, RepositoryConstants.USER_NODETYPE);
692            lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", contributor.getLogin());
693            lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", contributor.getPopulationId());
694            
695            UserIdentity creator = UserIdentity.stringToUserIdentity(Archivers.xpathEvalNonEmpty("resource/creator", propertiesXml));
696            Node creatorNode = resourceNode.addNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + JCRResource.CREATOR_NODE_NAME, RepositoryConstants.USER_NODETYPE);
697            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", creator.getLogin());
698            creatorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", creator.getPopulationId());
699            
700            Date creationDate = Objects.requireNonNull(DomNodeHelper.nullableDatetimeValue(propertiesXml, "resource/creationDate"));
701            Calendar creationDateCal = new GregorianCalendar();
702            creationDateCal.setTime(creationDate);
703            resourceNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + JCRResource.CREATION_DATE, creationDateCal);
704        }
705        
706        private void _setResourceMetadata(Node resourceNode, Path file) throws IOException
707        {
708            ModifiableDublinCoreAwareAmetysObject dcObject = _jcrResourceFactory.getAmetysObject(resourceNode, null);
709            _setDublinCoreMetadata(dcObject, file);
710        }
711        
712        private void _setDublinCoreMetadata(ModifiableDublinCoreAwareAmetysObject dcObject, Path file) throws IOException
713        {
714            String zipEntryPath = new StringBuilder()
715                    .append(ArchiveHandler.METADATA_PREFIX)
716                    .append(StringUtils.strip(file.toString(), "/"))
717                    .append("_")
718                    .append(__DC_METADATA_XML_FILE_NAME_SUFFIX)
719                    .toString();
720            try (InputStream stream = ZipEntryHelper.zipEntryFileInputStream(_zipArchivePath, zipEntryPath))
721            {
722                Document doc = _builder.parse(stream);
723                _setDublinCoreMetadata(dcObject, doc);
724            }
725            catch (SAXException | TransformerException e)
726            {
727                throw new IOException(e);
728            }
729        }
730        
731        private void _setDublinCoreMetadata(ModifiableDublinCoreAwareAmetysObject dcObject, Document doc) throws TransformerException
732        {
733            org.w3c.dom.Node dcNode = XPathAPI.selectSingleNode(doc, __DC_METADATA_XML_EXPORT_TAG_NAME_ROOT);
734            dcObject.setDCTitle(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_TITLE));
735            dcObject.setDCCreator(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_CREATOR));
736            dcObject.setDCSubject(DomNodeHelper.stringValues(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_SUBJECT));
737            dcObject.setDCDescription(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_DESCRIPTION));
738            dcObject.setDCPublisher(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_PUBLISHER));
739            dcObject.setDCContributor(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_CONTRIBUTOR));
740            dcObject.setDCDate(DomNodeHelper.nullableDateValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_DATE));
741            dcObject.setDCType(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_TYPE));
742            dcObject.setDCFormat(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_FORMAT));
743            dcObject.setDCIdentifier(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_IDENTIFIER));
744            dcObject.setDCSource(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_SOURCE));
745            dcObject.setDCLanguage(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_LANGUAGE));
746            dcObject.setDCRelation(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_RELATION));
747            dcObject.setDCCoverage(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_COVERAGE));
748            dcObject.setDCRights(DomNodeHelper.nullableStringValue(dcNode, __DC_METADATA_XML_EXPORT_TAG_NAME_RIGHTS));
749        }
750        
751        private String _getMimeType(Path file)
752        {
753            return Optional.of(file)
754                    .map(Path::getFileName)
755                    .map(Path::toString)
756                    .map(String::toLowerCase)
757                    .map(_cocoonContext::getMimeType)
758                    .orElse("application/unknown");
759        }
760        
761        private final class UnitaryCollectionImporter implements UnitaryImporter<Node>
762        {
763            @Override
764            public String objectNameForLogs()
765            {
766                return "Resource collection";
767            }
768
769            @Override
770            public Document getPropertiesXml(Path zipEntryPath) throws Exception
771            {
772                return _getFolderPropertiesXml(zipEntryPath);
773            }
774
775            @Override
776            public String retrieveId(Document propertiesXml) throws Exception
777            {
778                return Archivers.xpathEvalNonEmpty("resource-collection/id", propertiesXml);
779            }
780
781            @Override
782            public Node create(Path zipEntryPath, String id, Document propertiesXml) throws AmetysObjectNotImportedException, Exception
783            {
784                Node node = _createResourceCollection(zipEntryPath, id, propertiesXml);
785                Archivers.unitarySave(node, _logger);
786                return node;
787            }
788            
789            @Override
790            public ImportReport getReport()
791            {
792                return _report;
793            }
794        }
795        
796        private final class UnitaryResourceImporter implements UnitaryImporter<JCRResource>
797        {
798            @Override
799            public String objectNameForLogs()
800            {
801                return "Resource";
802            }
803
804            @Override
805            public Document getPropertiesXml(Path zipEntryPath) throws Exception
806            {
807                return _getFilePropertiesXml(zipEntryPath);
808            }
809
810            @Override
811            public String retrieveId(Document propertiesXml) throws Exception
812            {
813                return Archivers.xpathEvalNonEmpty("resource/id", propertiesXml);
814            }
815
816            @Override
817            public JCRResource create(Path zipEntryPath, String id, Document propertiesXml) throws AmetysObjectNotImportedException, Exception
818            {
819                JCRResource createdResource = _createdResource(zipEntryPath, id, propertiesXml);
820                Archivers.unitarySave(createdResource.getNode(), _logger);
821                return createdResource;
822            }
823            
824            @Override
825            public ImportReport getReport()
826            {
827                return _report;
828            }
829        }
830    }
831}