From e87c9f8e578ecf6bbf1684a1b93c7256ac83db4e Mon Sep 17 00:00:00 2001 From: mohamed eskander Date: Wed, 29 Nov 2023 17:07:42 +0200 Subject: [PATCH] [DSC-737] Restrict export formats by groups and Bulk Item export with metadata and bitstream --- .../dspace/content/BitstreamServiceImpl.java | 7 + .../crosswalk/METSDisseminationCrosswalk.java | 23 +- .../crosswalks/ItemExportCrosswalk.java | 5 + .../METSStreamDisseminationCrosswalk.java | 63 ++++ .../crosswalks/ReferCrosswalk.java | 48 +++ .../crosswalks/ZipItemExportCrosswalk.java | 325 ++++++++++++++++++ .../service/ItemExportFormatServiceImpl.java | 2 + .../content/service/BitstreamService.java | 2 + .../crosswalks/ZipItemExportCrosswalkIT.java | 256 ++++++++++++++ dspace/config/spring/api/crosswalks.xml | 18 +- 10 files changed, 744 insertions(+), 5 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/content/integration/crosswalks/METSStreamDisseminationCrosswalk.java create mode 100644 dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ZipItemExportCrosswalk.java create mode 100644 dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ZipItemExportCrosswalkIT.java diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index 2233bedd034e..7cc346ff8da9 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -407,6 +407,13 @@ public Bitstream getBitstreamByName(Item item, String bundleName, String bitstre return null; } + @Override + public List getBitstreamByBundleName(Item item, String bundleName) throws SQLException { + return itemService.getBundles(item, bundleName).stream() + .flatMap(bundle -> bundle.getBitstreams().stream()) + .collect(Collectors.toList()); + } + @Override public Bitstream getFirstBitstream(Item item, String bundleName) throws SQLException { List bundles = itemService.getBundles(item, bundleName); diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java index b8a4a8aef390..e6156e78d295 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/METSDisseminationCrosswalk.java @@ -72,6 +72,16 @@ public class METSDisseminationCrosswalk private static final String schemaLocation = METS_NS.getURI() + " " + METS_XSD; + private String metsPackagerPlugin; + + public METSDisseminationCrosswalk() { + this.metsPackagerPlugin = METS_PACKAGER_PLUGIN; + } + + public METSDisseminationCrosswalk(String metsPackagerPlugin) { + this.metsPackagerPlugin = metsPackagerPlugin; + } + @Override public Namespace[] getNamespaces() { return (Namespace[]) ArrayUtils.clone(namespaces); @@ -103,10 +113,10 @@ public Element disseminateElement(Context context, DSpaceObject dso) PackageDisseminator dip = (PackageDisseminator) CoreServiceFactory.getInstance().getPluginService() - .getNamedPlugin(PackageDisseminator.class, METS_PACKAGER_PLUGIN); + .getNamedPlugin(PackageDisseminator.class, metsPackagerPlugin); if (dip == null) { throw new CrosswalkInternalException( - "Cannot find a disseminate plugin for package=" + METS_PACKAGER_PLUGIN); + "Cannot find a disseminate plugin for package=" + metsPackagerPlugin); } try { @@ -117,11 +127,16 @@ public Element disseminateElement(Context context, DSpaceObject dso) // Create a temporary file to disseminate into ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - String tempDirectory = (configurationService.hasProperty("upload.temp.dir")) + String tempDirectoryPath = (configurationService.hasProperty("upload.temp.dir")) ? configurationService.getProperty("upload.temp.dir") : System.getProperty("java.io.tmpdir"); - File tempFile = File.createTempFile("METSDissemination" + dso.hashCode(), null, new File(tempDirectory)); + File tempDirectory = new File(tempDirectoryPath); + if (!tempDirectory.exists()) { + tempDirectory.mkdirs(); + } + + File tempFile = File.createTempFile("METSDissemination" + dso.hashCode(), null, tempDirectory); tempFile.deleteOnExit(); // Disseminate METS to temp file diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ItemExportCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ItemExportCrosswalk.java index 3a8b5a1524d1..dba686198e8a 100644 --- a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ItemExportCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ItemExportCrosswalk.java @@ -11,6 +11,7 @@ import org.dspace.content.crosswalk.CrosswalkMode; import org.dspace.content.crosswalk.StreamDisseminationCrosswalk; +import org.dspace.core.Context; /** * Implementation of {@link StreamDisseminationCrosswalk} related to item @@ -40,4 +41,8 @@ public default Optional getEntityType() { public default CrosswalkMode getCrosswalkMode() { return CrosswalkMode.SINGLE; } + + public default boolean isAuthorized(Context context) { + return true; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/METSStreamDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/METSStreamDisseminationCrosswalk.java new file mode 100644 index 000000000000..292a1e14f946 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/METSStreamDisseminationCrosswalk.java @@ -0,0 +1,63 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.integration.crosswalks; + +import java.io.IOException; +import java.io.OutputStream; +import java.sql.SQLException; +import javax.annotation.PostConstruct; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; +import org.dspace.content.crosswalk.CrosswalkException; +import org.dspace.content.crosswalk.METSDisseminationCrosswalk; +import org.dspace.content.crosswalk.StreamDisseminationCrosswalk; +import org.dspace.core.Context; +import org.jdom2.Element; +import org.jdom2.output.Format; +import org.jdom2.output.XMLOutputter; + +/** + * Implementation of {@link StreamDisseminationCrosswalk} that produces a METS + * manifest for the DSpace item as a metadata description, using + * {@link METSDisseminationCrosswalk}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class METSStreamDisseminationCrosswalk implements StreamDisseminationCrosswalk { + + private METSDisseminationCrosswalk metsDisseminationCrosswalk; + + @PostConstruct + public void setup() { + metsDisseminationCrosswalk = new METSDisseminationCrosswalk("AIP"); + } + + @Override + public boolean canDisseminate(Context context, DSpaceObject dso) { + return metsDisseminationCrosswalk.canDisseminate(dso); + } + + @Override + public void disseminate(Context context, DSpaceObject dso, OutputStream out) + throws CrosswalkException, IOException, SQLException, AuthorizeException { + + Element element = metsDisseminationCrosswalk.disseminateElement(context, dso); + + XMLOutputter xmlOutputter = new XMLOutputter(Format.getPrettyFormat()); + xmlOutputter.output(element, out); + + } + + @Override + public String getMIMEType() { + return "application/xml"; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ReferCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ReferCrosswalk.java index d54fef41ee68..519d9531cb71 100644 --- a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ReferCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ReferCrosswalk.java @@ -58,6 +58,9 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.discovery.configuration.DiscoveryConfigurationUtilsService; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.service.GroupService; import org.dspace.services.ConfigurationService; import org.dspace.util.UUIDUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -94,6 +97,9 @@ public class ReferCrosswalk implements ItemExportCrosswalk { @Autowired private MetadataSecurityService metadataSecurityService; + @Autowired + private GroupService groupService; + private Converter converter; private Consumer> linesPostProcessor; @@ -116,6 +122,8 @@ public class ReferCrosswalk implements ItemExportCrosswalk { private CrosswalkMode crosswalkMode; + private List allowedGroups; + @PostConstruct private void postConstruct() throws IOException { String parent = configurationService.getProperty("dspace.dir") + File.separator + "config" + File.separator; @@ -128,6 +136,21 @@ private void postConstruct() throws IOException { } } + @Override + public boolean isAuthorized(Context context) { + if (CollectionUtils.isEmpty(allowedGroups)) { + return true; + } + + EPerson ePerson = context.getCurrentUser(); + if (ePerson == null) { + return allowedGroups.contains(Group.ANONYMOUS); + } + + return allowedGroups.stream() + .anyMatch(groupName -> isMemberOfGroupNamed(context, ePerson, groupName)); + } + @Override public void disseminate(Context context, DSpaceObject dso, OutputStream out) throws CrosswalkException, IOException, SQLException, AuthorizeException { @@ -136,6 +159,10 @@ public void disseminate(Context context, DSpaceObject dso, OutputStream out) throw new CrosswalkObjectNotSupported("Can only crosswalk an Item with the configured type: " + entityType); } + if (!isAuthorized(context)) { + throw new AuthorizeException("The current user is not allowed to perform a zip item export"); + } + List lines = getItemLines(context, dso, true); if (linesPostProcessor != null) { @@ -154,6 +181,10 @@ public void disseminate(Context context, Iterator dsoIte throw new UnsupportedOperationException("No template defined for multiple items"); } + if (!isAuthorized(context)) { + throw new AuthorizeException("The current user is not allowed to perform a zip item export"); + } + List lines = new ArrayList(); for (TemplateLine line : multipleItemsTemplateLines) { @@ -466,6 +497,15 @@ private boolean hasExpectedEntityType(Item item) { return Objects.equals(itemEntityType, entityType); } + private boolean isMemberOfGroupNamed(Context context, EPerson ePerson, String groupName) { + try { + Group group = groupService.findByName(context, groupName); + return groupService.isMember(context, ePerson, group); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + public void setConverter(Converter converter) { this.converter = converter; } @@ -525,4 +565,12 @@ public void setPubliclyReadable(boolean isPubliclyReadable) { this.publiclyReadable = isPubliclyReadable; } + public List getAllowedGroups() { + return allowedGroups; + } + + public void setAllowedGroups(List allowedGroups) { + this.allowedGroups = allowedGroups; + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ZipItemExportCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ZipItemExportCrosswalk.java new file mode 100644 index 000000000000..2096fa037273 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/ZipItemExportCrosswalk.java @@ -0,0 +1,325 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.integration.crosswalks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Bitstream; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.crosswalk.CrosswalkException; +import org.dspace.content.crosswalk.CrosswalkMode; +import org.dspace.content.crosswalk.CrosswalkObjectNotSupported; +import org.dspace.content.crosswalk.StreamDisseminationCrosswalk; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.core.exception.SQLRuntimeException; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.service.GroupService; +import org.dspace.storage.bitstore.service.BitstreamStorageService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; + +/** + * Implementation of {@link ItemExportCrosswalk} that export all the given items + * creating a zip. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class ZipItemExportCrosswalk implements ItemExportCrosswalk { + + private static final Logger LOGGER = LoggerFactory.getLogger(ZipItemExportCrosswalk.class); + + @Autowired + private ItemService itemService; + + @Autowired + private BitstreamService bitstreamService; + + @Autowired + private BitstreamStorageService bitstreamStorageService; + + @Autowired + private GroupService groupService; + + private String zipName = "items.zip"; + + private String entityType; + + private String bitstreamBundle = "ORIGINAL"; + + private String metadataFileName; + + private StreamDisseminationCrosswalk crosswalk; + + private CrosswalkMode crosswalkMode = CrosswalkMode.MULTIPLE; + + private List allowedGroups; + + @Override + public boolean isAuthorized(Context context) { + if (CollectionUtils.isEmpty(allowedGroups)) { + return true; + } + + EPerson ePerson = context.getCurrentUser(); + if (ePerson == null) { + return allowedGroups.contains(Group.ANONYMOUS); + } + + return allowedGroups.stream() + .anyMatch(groupName -> isMemberOfGroupNamed(context, ePerson, groupName)); + } + + @Override + public boolean canDisseminate(Context context, DSpaceObject dso) { + return dso.getType() == Constants.ITEM && hasExpectedEntityType((Item) dso); + } + + @Override + public void disseminate(Context context, DSpaceObject dso, OutputStream out) + throws CrosswalkException, IOException, SQLException, AuthorizeException { + this.disseminate(context, Arrays.asList(dso).iterator(), out); + } + + @Override + public void disseminate(Context context, Iterator dsoIterator, OutputStream out) + throws CrosswalkException, IOException, SQLException, AuthorizeException { + + Assert.notNull(metadataFileName, "The name of the metadata file is required to perform a bulk item export"); + Assert.notNull(crosswalk, "An instance of DisseminationCrosswalk is required to perform a bulk item export"); + Assert.notNull(zipName, "The name of the zip to be generated is required to perform a bulk item export"); + + if (!isAuthorized(context)) { + throw new AuthorizeException("The current user is not allowed to perform a zip item export"); + } + + createZip(context, dsoIterator, out); + + } + + private void createZip(Context context, Iterator dsoIterator, OutputStream out) + throws CrosswalkObjectNotSupported, IOException { + + try (ZipOutputStream zos = new ZipOutputStream(out)) { + + while (dsoIterator.hasNext()) { + + DSpaceObject dso = dsoIterator.next(); + if (!canDisseminate(context, dso)) { + throw new CrosswalkObjectNotSupported( + "Can only crosswalk an Item with the configured type: " + entityType); + } + + try { + createFolder(context, (Item) dso, zos); + } catch (Exception ex) { + LOGGER.error("An error occurs creating folder for item " + dso.getID(), ex); + } + + } + + } + + } + + private void createFolder(Context context, Item item, ZipOutputStream zos) throws IOException { + + createMetadataEntry(context, item, zos); + + List bitstreams = getBitstreamToExport(item); + for (Bitstream bitstream : bitstreams) { + try { + addBitstreamEntry(context, item, bitstream, zos); + } catch (Exception ex) { + LOGGER.error("An error occurs adding bitstream " + bitstream.getID() + + " to the folder of item " + item.getID(), ex); + } + } + + } + + private void createMetadataEntry(Context context, Item item, ZipOutputStream zos) throws IOException { + ZipEntry metadataEntry = new ZipEntry(getFolderName(item) + "/" + getMetadataFileName()); + zos.putNextEntry(metadataEntry); + zos.write(getMetadataFileNameContent(context, item)); + zos.closeEntry(); + } + + private byte[] getMetadataFileNameContent(Context context, Item item) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + crosswalk.disseminate(context, item, out); + return out.toByteArray(); + } catch (CrosswalkException | IOException | SQLException | AuthorizeException e) { + throw new RuntimeException(e); + } + } + + private List getBitstreamToExport(Item item) { + try { + return bitstreamService.getBitstreamByBundleName(item, bitstreamBundle); + } catch (SQLException e) { + throw new SQLRuntimeException(e); + } + } + + private void addBitstreamEntry(Context context, Item item, Bitstream bitstream, ZipOutputStream zos) + throws IOException { + + InputStream bitstreamContent = retrieveContent(context, bitstream); + + ZipEntry bitstreamEntry = new ZipEntry(getFolderName(item) + "/" + getBitstreamFileName(context, bitstream)); + zos.putNextEntry(bitstreamEntry); + + try { + writeBitstreamContent(bitstreamContent, zos); + } finally { + zos.closeEntry(); + } + + } + + private void writeBitstreamContent(InputStream content, ZipOutputStream zos) throws IOException { + byte[] bytes = new byte[1024]; + int length; + while ((length = content.read(bytes)) >= 0) { + zos.write(bytes, 0, length); + } + } + + private String getBitstreamFileName(Context context, Bitstream bitstream) { + String name = "bitstream_" + bitstream.getID().toString(); + return getBitstreamExtension(context, bitstream) + .map(extension -> name + "." + extension) + .orElse(name); + } + + private Optional getBitstreamExtension(Context context, Bitstream bitstream) { + try { + return bitstream.getFormat(context).getExtensions().stream().findFirst(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private InputStream retrieveContent(Context context, Bitstream bitstream) { + try { + return bitstreamStorageService.retrieve(context, bitstream); + } catch (SQLException | IOException e) { + throw new RuntimeException(e); + } + } + + private String getMetadataFileName() { + return metadataFileName; + } + + private String getFolderName(Item item) { + return item.getID().toString(); + } + + private boolean isMemberOfGroupNamed(Context context, EPerson ePerson, String groupName) { + try { + Group group = groupService.findByName(context, groupName); + return groupService.isMember(context, ePerson, group); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getMIMEType() { + return "application/octet-stream"; + } + + public void setCrosswalkMode(CrosswalkMode crosswalkMode) { + this.crosswalkMode = crosswalkMode; + } + + @Override + public CrosswalkMode getCrosswalkMode() { + return Optional.ofNullable(this.crosswalkMode).orElse(CrosswalkMode.MULTIPLE); + } + + private boolean hasExpectedEntityType(Item item) { + if (StringUtils.isBlank(entityType)) { + return true; + } + return entityType.equals(itemService.getEntityType(item)); + } + + @Override + public String getFileName() { + return getZipName(); + } + + public String getZipName() { + return zipName; + } + + public void setZipName(String zipName) { + this.zipName = zipName; + } + + public Optional getEntityType() { + return Optional.ofNullable(entityType); + } + + public void setEntityType(String entityType) { + this.entityType = entityType; + } + + public StreamDisseminationCrosswalk getCrosswalk() { + return crosswalk; + } + + public void setCrosswalk(StreamDisseminationCrosswalk crosswalk) { + this.crosswalk = crosswalk; + } + + public String getBitstreamBundle() { + return bitstreamBundle; + } + + public void setBitstreamBundle(String bitstreamBundle) { + this.bitstreamBundle = bitstreamBundle; + } + + public void setMetadataFileName(String metadataFileName) { + this.metadataFileName = metadataFileName; + } + + public List getAllowedGroups() { + return allowedGroups; + } + + public void setAllowedGroups(List allowedGroups) { + this.allowedGroups = allowedGroups; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/service/ItemExportFormatServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/service/ItemExportFormatServiceImpl.java index 4d33ba35c5e8..5745ec3e8ce8 100644 --- a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/service/ItemExportFormatServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/service/ItemExportFormatServiceImpl.java @@ -45,6 +45,7 @@ public ItemExportFormat get(Context context, String id) { public List getAll(Context context) { return this.streamDissiminatorCrosswalkMapper.getAllItemExportCrosswalks().entrySet().stream() + .filter(entry -> entry.getValue().isAuthorized(context)) .map(entry -> buildItemExportFormat(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); @@ -58,6 +59,7 @@ public List byEntityTypeAndMolteplicity(Context context, Strin .entrySet().stream() .filter(entry -> hasSameMolteplicity(entry.getValue(), molteplicity)) .filter(entry -> hasSameEntityType(entry.getValue(), entityType)) + .filter(entry -> entry.getValue().isAuthorized(context)) .map(entry -> buildItemExportFormat(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); diff --git a/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java b/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java index 85a4fd140e9a..fa1cbc38beae 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java @@ -210,6 +210,8 @@ public InputStream retrieve(Context context, Bitstream bitstream) public Bitstream getBitstreamByName(Item item, String bundleName, String bitstreamName) throws SQLException; + List getBitstreamByBundleName(Item item, String bundleName) throws SQLException; + public Bitstream getFirstBitstream(Item item, String bundleName) throws SQLException; public Bitstream getThumbnail(Context context, Bitstream bitstream) throws SQLException; diff --git a/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ZipItemExportCrosswalkIT.java b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ZipItemExportCrosswalkIT.java new file mode 100644 index 000000000000..e824fef5a9b1 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ZipItemExportCrosswalkIT.java @@ -0,0 +1,256 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.integration.crosswalks; + +import static org.dspace.builder.CollectionBuilder.createCollection; +import static org.dspace.builder.CommunityBuilder.createCommunity; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.apache.commons.collections.IteratorUtils; +import org.apache.commons.io.IOUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.GroupBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.utils.DSpace; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class ZipItemExportCrosswalkIT extends AbstractIntegrationTestWithDatabase { + + private ZipItemExportCrosswalk zipItemExportCrosswalk; + + private Community community; + + private Collection collection; + + @Before + public void setup() throws SQLException, AuthorizeException { + + zipItemExportCrosswalk = new DSpace().getServiceManager() + .getServicesByType(ZipItemExportCrosswalk.class).get(0); + + context.turnOffAuthorisationSystem(); + community = createCommunity(context).build(); + collection = createCollection(context, community).build(); + context.restoreAuthSystemState(); + + } + + @Test + public void testItemsExportWithAdmin() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item item1 = createItem("Test Item 1", "2022-01-01", "Luca Giamminonni"); + Item item2 = createItem("Test Item 2", "2022-03-01", "Walter White"); + Item item3 = createItem("Test Item 3", "2020-01-01", "Andrea Bollini"); + + Bitstream bitstream1 = createBitstream(item1, "test.txt", "This is a test"); + Bitstream bitstream2 = createBitstream(item3, "test.pdf", "Last test", "6 months"); + + String expectedEmbargo = LocalDate.now().plus(6, ChronoUnit.MONTHS).format(DateTimeFormatter.ISO_DATE); + + context.restoreAuthSystemState(); + + context.setCurrentUser(admin); + + File tempZip = File.createTempFile("test", "zip"); + tempZip.deleteOnExit(); + + try (FileOutputStream fos = new FileOutputStream(tempZip)) { + zipItemExportCrosswalk.disseminate(context, List.of(item1, item2, item3).iterator(), fos); + } + + try (ZipFile zipFile = new ZipFile(tempZip)) { + + ZipEntry zipEntry = zipFile.getEntry(item1.getID().toString() + "/mets.xml"); + assertThat(zipEntry, notNullValue()); + + String metsContent = getZipEntryContent(zipFile, zipEntry); + + assertThat(metsContent, containsString( + "2022-01-01")); + assertThat(metsContent, + containsString("Test Item 1")); + assertThat(metsContent, containsString("Luca Giamminonni")); + assertThat(metsContent, + containsString("test@email.com")); + assertThat(metsContent, + containsString("test.txt")); + + zipEntry = zipFile.getEntry(item1.getID().toString() + "/bitstream_" + bitstream1.getID().toString()); + assertThat(zipEntry, notNullValue()); + assertThat(getZipEntryContent(zipFile, zipEntry), is("This is a test")); + + zipEntry = zipFile.getEntry(item2.getID().toString() + "/mets.xml"); + assertThat(zipEntry, notNullValue()); + + metsContent = getZipEntryContent(zipFile, zipEntry); + + assertThat(metsContent, containsString( + "2022-03-01")); + assertThat(metsContent, + containsString("Test Item 2")); + assertThat(metsContent, containsString("Walter White")); + assertThat(metsContent, + containsString("test@email.com")); + + zipEntry = zipFile.getEntry(item3.getID().toString() + "/mets.xml"); + assertThat(zipEntry, notNullValue()); + + metsContent = getZipEntryContent(zipFile, zipEntry); + + assertThat(metsContent, containsString( + "2020-01-01")); + assertThat(metsContent, + containsString("Test Item 3")); + assertThat(metsContent, containsString("Andrea Bollini")); + assertThat(metsContent, + containsString("test@email.com")); + assertThat(metsContent, containsString("")); + assertThat(metsContent, + containsString("test.pdf")); + + zipEntry = zipFile.getEntry(item3.getID().toString() + "/bitstream_" + bitstream2.getID().toString()); + assertThat(zipEntry, notNullValue()); + assertThat(getZipEntryContent(zipFile, zipEntry), is("Last test")); + + assertThat(getAllEntries(zipFile), hasSize(5)); + + } + + } + + @Test + public void testItemsExportWithCurators() throws Exception { + + context.turnOffAuthorisationSystem(); + + Group curators = GroupBuilder.createGroup(context) + .withName("Curators") + .build(); + + EPerson user = EPersonBuilder.createEPerson(context) + .withEmail("user@test.com") + .withGroupMembership(curators) + .build(); + + Item item1 = createItem("Test Item 1", "2022-01-01", "Luca Giamminonni"); + Item item2 = createItem("Test Item 2", "2022-03-01", "Walter White"); + Item item3 = createItem("Test Item 3", "2020-01-01", "Andrea Bollini"); + + context.restoreAuthSystemState(); + + context.setCurrentUser(user); + + File tempZip = File.createTempFile("test", "zip"); + tempZip.deleteOnExit(); + + try (FileOutputStream fos = new FileOutputStream(tempZip)) { + zipItemExportCrosswalk.disseminate(context, List.of(item1, item2, item3).iterator(), fos); + } + + try (ZipFile zipFile = new ZipFile(tempZip)) { + assertThat(getAllEntries(zipFile), hasSize(3)); + } + + } + + @Test + public void testItemsExportWithNotAuthorizedUser() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item item1 = createItem("Test Item 1", "2022-01-01", "Luca Giamminonni"); + Item item2 = createItem("Test Item 2", "2022-03-01", "Walter White"); + Item item3 = createItem("Test Item 3", "2020-01-01", "Andrea Bollini"); + + context.restoreAuthSystemState(); + + context.setCurrentUser(eperson); + + File tempZip = File.createTempFile("test", "zip"); + tempZip.deleteOnExit(); + + try (FileOutputStream fos = new FileOutputStream(tempZip)) { + + AuthorizeException authorizeException = Assert.assertThrows(AuthorizeException.class, + () -> zipItemExportCrosswalk.disseminate(context, List.of(item1, item2, item3).iterator(), fos)); + + assertThat(authorizeException.getMessage(), + is("The current user is not allowed to perform a zip item export")); + } + + } + + private Item createItem(String title, String issueDate, String author) { + return ItemBuilder.createItem(context, collection) + .withTitle(title) + .withIssueDate(issueDate) + .withAuthor(author) + .build(); + } + + private Bitstream createBitstream(Item item, String name, String content) throws Exception { + return BitstreamBuilder.createBitstream(context, item, getInputStream(content)) + .withName(name) + .build(); + } + + private Bitstream createBitstream(Item item, String name, String content, String embargoPeriod) throws Exception { + return BitstreamBuilder.createBitstream(context, item, getInputStream(content)) + .withName(name) + .withEmbargoPeriod(embargoPeriod) + .build(); + } + + private String getZipEntryContent(ZipFile zipFile, ZipEntry zipEntry) throws IOException { + return IOUtils.toString(zipFile.getInputStream(zipEntry), StandardCharsets.UTF_8); + } + + private InputStream getInputStream(String str) { + return IOUtils.toInputStream(str, StandardCharsets.UTF_8); + } + + @SuppressWarnings("unchecked") + private List getAllEntries(ZipFile zipFile) { + return IteratorUtils.toList(zipFile.entries().asIterator()); + } + +} diff --git a/dspace/config/spring/api/crosswalks.xml b/dspace/config/spring/api/crosswalks.xml index 34941fe7b0d4..9184a56482da 100644 --- a/dspace/config/spring/api/crosswalks.xml +++ b/dspace/config/spring/api/crosswalks.xml @@ -68,6 +68,7 @@ + @@ -509,7 +510,22 @@ - + + + + + + + + + Administrator + Curators + + + + + +