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