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}