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