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