diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index aae4b282b654..1c6879a5d701 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. 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 eef8a859157d..0682082e03f8 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/main/java/org/dspace/discovery/SolrServiceBestMatchIndexingPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceBestMatchIndexingPlugin.java index 39130e9224d2..a1830a3931c7 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceBestMatchIndexingPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceBestMatchIndexingPlugin.java @@ -68,7 +68,7 @@ protected void addIndexValueForPersonItem(Item item, SolrInputDocument document) String lastName = getMetadataValue(item, LASTNAME_FIELD); List fullNames = getMetadataValues(item, FULLNAME_FIELDS); - getAllNameVariants(firstName, lastName, fullNames) + getAllNameVariants(firstName, lastName, fullNames, item.getID().toString()) .forEach(variant -> addIndexValue(document, variant)); } diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidAffiliationFactory.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidAffiliationFactory.java index d74f05bcaf50..5b325a44a37a 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidAffiliationFactory.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidAffiliationFactory.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -56,9 +57,9 @@ public OrcidAffiliationFactory(OrcidProfileSectionType sectionType, OrcidProfile @Override public List getMetadataFields() { - return List.of(organizationField, roleField, startDateField, endDateField).stream() - .filter(StringUtils::isNotBlank) - .collect(Collectors.toList()); + return Stream.of(organizationField, roleField, startDateField, endDateField) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); } @Override @@ -80,14 +81,17 @@ public Object create(Context context, List metadataValues) { orcidCommonObjectFactory.createFuzzyDate(endDate).ifPresent(affiliation::setEndDate); affiliation.setRoleTitle(isUnprocessableValue(role) ? null : role.getValue()); - orcidCommonObjectFactory.createOrganization(context, organization).ifPresent(affiliation::setOrganization); + orcidCommonObjectFactory.createOrganization(context, organization).ifPresent(org -> { + affiliation.setOrganization(org); + affiliation.setDepartmentName(org.getName()); + }); return affiliation; } @Override public List getMetadataSignatures(Context context, Item item) { - List signatures = new ArrayList(); + List signatures = new ArrayList<>(); Map> metadataGroups = getMetadataGroups(item); int groupSize = metadataGroups.getOrDefault(organizationField, Collections.emptyList()).size(); @@ -95,7 +99,7 @@ public List getMetadataSignatures(Context context, Item item) { List metadataValues = getMetadataValueByPlace(metadataGroups, currentGroupIndex); //only "visible" metadatavalues within this group metadataValues = metadataValues.stream() - .filter(metadataValue -> isAllowedMetadataByVisibility(metadataValue)) + .filter(this::isAllowedMetadataByVisibility) .collect(Collectors.toList()); if (!metadataValues.isEmpty()) { signatures.add(metadataSignatureGenerator.generate(context, metadataValues)); @@ -168,7 +172,7 @@ private Map> getMetadataGroups(Item item) { } private List getMetadataValueByPlace(Map> metadataGroups, int place) { - List metadataValues = new ArrayList(); + List metadataValues = new ArrayList<>(); for (String metadataField : metadataGroups.keySet()) { List nestedMetadataValues = metadataGroups.get(metadataField); if (nestedMetadataValues.size() > place) { diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidCommonObjectFactoryImpl.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidCommonObjectFactoryImpl.java index 48cda3b3da20..8d92e72b80c0 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidCommonObjectFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidCommonObjectFactoryImpl.java @@ -121,7 +121,9 @@ public Optional createOrganization(Context context, MetadataValue Item organizationItem = findRelatedItem(context, metadataValue); if (organizationItem != null) { organization.setAddress(createOrganizationAddress(organizationItem)); - organization.setDisambiguatedOrganization(createDisambiguatedOrganization(organizationItem)); + organization.setDisambiguatedOrganization( + createDisambiguatedOrganization(context, organizationItem) + ); } return of(organization); @@ -156,7 +158,7 @@ public Optional createFundingContributor(Context context, Me FundingContributor contributor = new FundingContributor(); contributor.setCreditName(new CreditName(metadataValue.getValue())); - contributor.setContributorAttributes(getFundingContributorAttributes(metadataValue, role)); + contributor.setContributorAttributes(getFundingContributorAttributes(role)); Item authorItem = findItem(context, UUIDUtils.fromString(metadataValue.getAuthority())); if (authorItem != null) { @@ -190,7 +192,7 @@ public Optional createCountry(Context context, MetadataValue metadataVa throw new OrcidValidationException(OrcidValidationError.INVALID_COUNTRY); } - return country.map(isoCountry -> new Country(isoCountry)); + return country.map(Country::new); } private ContributorAttributes getContributorAttributes(MetadataValue metadataValue, ContributorRole role) { @@ -211,8 +213,7 @@ private OrganizationAddress createOrganizationAddress(Item organizationItem) { return address; } - private FundingContributorAttributes getFundingContributorAttributes(MetadataValue metadataValue, - FundingContributorRole role) { + private FundingContributorAttributes getFundingContributorAttributes(FundingContributorRole role) { FundingContributorAttributes attributes = new FundingContributorAttributes(); attributes.setContributorRole(role != null ? role.value() : null); return attributes; @@ -237,11 +238,23 @@ private DisambiguatedOrganization createDisambiguatedOrganization(Item organizat return null; } + private DisambiguatedOrganization createDisambiguatedOrganization(Context context, Item organizationItem) { + DisambiguatedOrganization disambiguatedOrganization = createDisambiguatedOrganization(organizationItem); + Item parentOrganization = findParentOrganization(context, organizationItem); + + while (disambiguatedOrganization == null && parentOrganization != null) { + disambiguatedOrganization = createDisambiguatedOrganization(parentOrganization); + parentOrganization = findParentOrganization(context, parentOrganization); + } + + return disambiguatedOrganization; + } + private Optional convertToIso3166Country(String countryValue) { return ofNullable(countryValue) .map(value -> countryConverter != null ? countryConverter.getValue(value) : value) .filter(value -> isValidEnum(Iso3166Country.class, value)) - .map(value -> Iso3166Country.fromValue(value)); + .map(Iso3166Country::fromValue); } private boolean isUnprocessableValue(MetadataValue value) { @@ -249,6 +262,19 @@ private boolean isUnprocessableValue(MetadataValue value) { || value.getValue().equals(PLACEHOLDER_PARENT_METADATA_VALUE); } + private Item findParentOrganization(Context context, Item item) { + try { + Optional metadataValue = + itemService.getMetadataByMetadataString(item, "organization.parentOrganization") + .stream().findFirst(); + return metadataValue.isPresent() + ? itemService.find(context, UUIDUtils.fromString(metadataValue.get().getAuthority())) + : null; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + private Item findRelatedItem(Context context, MetadataValue metadataValue) { try { return itemService.find(context, UUIDUtils.fromString(metadataValue.getAuthority())); diff --git a/dspace-api/src/main/java/org/dspace/util/PersonNameUtil.java b/dspace-api/src/main/java/org/dspace/util/PersonNameUtil.java index 0e88a0a9cdf5..cea02c76990b 100644 --- a/dspace-api/src/main/java/org/dspace/util/PersonNameUtil.java +++ b/dspace-api/src/main/java/org/dspace/util/PersonNameUtil.java @@ -16,6 +16,7 @@ import org.apache.commons.collections4.iterators.PermutationIterator; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; /** * Utility class that handle person names. @@ -24,6 +25,7 @@ * */ public final class PersonNameUtil { + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(PersonNameUtil.class); private PersonNameUtil() { @@ -35,12 +37,14 @@ private PersonNameUtil() { * @param firstName the first name * @param lastName the last name * @param fullNames the full names + * @param uuid the uuid * @return all the variants of the given names */ - public static Set getAllNameVariants(String firstName, String lastName, List fullNames) { + public static Set getAllNameVariants(String firstName, String lastName, List fullNames, + String uuid) { Set variants = new HashSet(); variants.addAll(getNameVariants(firstName, lastName)); - variants.addAll(getNameVariants(fullNames)); + variants.addAll(getNameVariants(fullNames, uuid)); return variants; } @@ -95,24 +99,30 @@ private static List getNameVariants(String[] firstNames, String lastName return variants; } - private static List getNameVariants(List fullNames) { + private static List getNameVariants(List fullNames, String uuid) { return fullNames.stream() .filter(Objects::nonNull) .map(name -> removeComma(name)) .distinct() - .flatMap(name -> getAllNamePermutations(name).stream()) + .flatMap(name -> getAllNamePermutations(name, uuid).stream()) .distinct() .collect(Collectors.toList()); } - private static List getAllNamePermutations(String name) { + private static List getAllNamePermutations(String name, String uuid) { List namePermutations = new ArrayList(); - PermutationIterator permutationIterator = new PermutationIterator(List.of(name.split(" "))); + List names = List.of(name.split(" ")); + if (names.size() < 5) { + PermutationIterator permutationIterator = new PermutationIterator(names); - while (permutationIterator.hasNext()) { - namePermutations.add(String.join(" ", permutationIterator.next())); + while (permutationIterator.hasNext()) { + namePermutations.add(String.join(" ", permutationIterator.next())); + } + } else { + log.warn(String.format("Cannot retrieve variants on the Person with UUID %s because the name is too long", + uuid)); } return namePermutations; 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-api/src/test/java/org/dspace/orcid/service/OrcidProfileSectionFactoryServiceIT.java b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidProfileSectionFactoryServiceIT.java index 4d2e85a5b99e..60122ba5dbde 100644 --- a/dspace-api/src/test/java/org/dspace/orcid/service/OrcidProfileSectionFactoryServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidProfileSectionFactoryServiceIT.java @@ -242,6 +242,184 @@ public void testFullEmploymentCreation() { } + @Test + public void testDisambiguationFromOrgUnitHierarchyOnEmploymentCreation() { + + context.turnOffAuthorisationSystem(); + + Item orgUnitWithRinId = ItemBuilder.createItem(context, orgUnits) + .withTitle("4Science with rin") + .withOrgUnitCountry("IT") + .withOrgUnitLocality("Milan") + .withOrgUnitRinggoldIdentifier("12345") + .build(); + + Item orgUnit = ItemBuilder.createItem(context, orgUnits) + .withTitle("4Science") + .withOrgUnitCountry("IT") + .withOrgUnitLocality("Milan") + .withParentOrganization("4Science with rin", orgUnitWithRinId.getID().toString()) + .build(); + + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Test profile") + .withPersonAffiliation("4Science", orgUnit.getID().toString()) + .withPersonAffiliationStartDate("2020-02") + .withPersonAffiliationEndDate(PLACEHOLDER_PARENT_METADATA_VALUE) + .withPersonAffiliationRole("Researcher") + .build(); + + context.restoreAuthSystemState(); + + List values = new ArrayList<>(); + values.add(getMetadata(item, "oairecerif.person.affiliation", 0)); + values.add(getMetadata(item, "oairecerif.affiliation.startDate", 0)); + values.add(getMetadata(item, "oairecerif.affiliation.endDate", 0)); + values.add(getMetadata(item, "oairecerif.affiliation.role", 0)); + + Object firstOrcidObject = profileSectionFactoryService.createOrcidObject(context, values, AFFILIATION); + assertThat(firstOrcidObject, instanceOf(Employment.class)); + Employment qualification = (Employment) firstOrcidObject; + assertThat(qualification.getStartDate(), notNullValue()); + assertThat(qualification.getStartDate().getYear().getValue(), is("2020")); + assertThat(qualification.getStartDate().getMonth().getValue(), is("02")); + assertThat(qualification.getStartDate().getDay().getValue(), is("01")); + assertThat(qualification.getEndDate(), nullValue()); + assertThat(qualification.getRoleTitle(), is("Researcher")); + assertThat(qualification.getDepartmentName(), is("4Science")); + + Organization organization = qualification.getOrganization(); + assertThat(organization, notNullValue()); + assertThat(organization.getName(), is("4Science")); + assertThat(organization.getAddress(), notNullValue()); + assertThat(organization.getAddress().getCountry(), is(Iso3166Country.IT)); + assertThat(organization.getAddress().getCity(), is("Milan")); + assertThat(organization.getDisambiguatedOrganization(), notNullValue()); + assertThat(organization.getDisambiguatedOrganization().getDisambiguatedOrganizationIdentifier(), is("12345")); + assertThat(organization.getDisambiguatedOrganization().getDisambiguationSource(), is("RINGGOLD")); + } + + @Test + public void testDisambiguationFromOrgUnitHierarchyOnEmploymentCreationWithAncestor() { + + context.turnOffAuthorisationSystem(); + + Item orgUnitGranfather = ItemBuilder.createItem(context, orgUnits) + .withTitle("4Science with rin") + .withOrgUnitCountry("IT") + .withOrgUnitLocality("Milan") + .withOrgUnitRinggoldIdentifier("12345") + .build(); + + Item orgUnitFather = ItemBuilder.createItem(context, orgUnits) + .withTitle("4Science without rin") + .withOrgUnitCountry("IT") + .withOrgUnitLocality("Milan") + .withParentOrganization("4Science with rin", orgUnitGranfather.getID().toString()) + .build(); + + + Item orgUnit = ItemBuilder.createItem(context, orgUnits) + .withTitle("4Science") + .withOrgUnitCountry("IT") + .withOrgUnitLocality("Milan") + .withParentOrganization("4Science without rin", orgUnitFather.getID().toString()) + .build(); + + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Test profile") + .withPersonAffiliation("4Science", orgUnit.getID().toString()) + .withPersonAffiliationStartDate("2020-02") + .withPersonAffiliationEndDate(PLACEHOLDER_PARENT_METADATA_VALUE) + .withPersonAffiliationRole("Researcher") + .build(); + + context.restoreAuthSystemState(); + + List values = new ArrayList<>(); + values.add(getMetadata(item, "oairecerif.person.affiliation", 0)); + values.add(getMetadata(item, "oairecerif.affiliation.startDate", 0)); + values.add(getMetadata(item, "oairecerif.affiliation.endDate", 0)); + values.add(getMetadata(item, "oairecerif.affiliation.role", 0)); + + Object firstOrcidObject = profileSectionFactoryService.createOrcidObject(context, values, AFFILIATION); + assertThat(firstOrcidObject, instanceOf(Employment.class)); + Employment qualification = (Employment) firstOrcidObject; + assertThat(qualification.getStartDate(), notNullValue()); + assertThat(qualification.getStartDate().getYear().getValue(), is("2020")); + assertThat(qualification.getStartDate().getMonth().getValue(), is("02")); + assertThat(qualification.getStartDate().getDay().getValue(), is("01")); + assertThat(qualification.getEndDate(), nullValue()); + assertThat(qualification.getRoleTitle(), is("Researcher")); + assertThat(qualification.getDepartmentName(), is("4Science")); + + Organization organization = qualification.getOrganization(); + assertThat(organization, notNullValue()); + assertThat(organization.getName(), is("4Science")); + assertThat(organization.getAddress(), notNullValue()); + assertThat(organization.getAddress().getCountry(), is(Iso3166Country.IT)); + assertThat(organization.getAddress().getCity(), is("Milan")); + assertThat(organization.getDisambiguatedOrganization(), notNullValue()); + assertThat(organization.getDisambiguatedOrganization().getDisambiguatedOrganizationIdentifier(), is("12345")); + assertThat(organization.getDisambiguatedOrganization().getDisambiguationSource(), is("RINGGOLD")); + } + + @Test + public void testDisambiguationFromOrgUnitHierarchyOnEmploymentCreationWithNoId() { + + context.turnOffAuthorisationSystem(); + + Item orgUnitWithRinId = ItemBuilder.createItem(context, orgUnits) + .withTitle("4Science with rin") + .withOrgUnitCountry("IT") + .withOrgUnitLocality("Milan") + .build(); + + Item orgUnit = ItemBuilder.createItem(context, orgUnits) + .withTitle("4Science") + .withOrgUnitCountry("IT") + .withOrgUnitLocality("Milan") + .withParentOrganization("4Science with rin", orgUnitWithRinId.getID().toString()) + .build(); + + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Test profile") + .withPersonAffiliation("4Science", orgUnit.getID().toString()) + .withPersonAffiliationStartDate("2020-02") + .withPersonAffiliationEndDate(PLACEHOLDER_PARENT_METADATA_VALUE) + .withPersonAffiliationRole("Researcher") + .build(); + + context.restoreAuthSystemState(); + + List values = new ArrayList<>(); + values.add(getMetadata(item, "oairecerif.person.affiliation", 0)); + values.add(getMetadata(item, "oairecerif.affiliation.startDate", 0)); + values.add(getMetadata(item, "oairecerif.affiliation.endDate", 0)); + values.add(getMetadata(item, "oairecerif.affiliation.role", 0)); + + Object firstOrcidObject = profileSectionFactoryService.createOrcidObject(context, values, AFFILIATION); + assertThat(firstOrcidObject, instanceOf(Employment.class)); + Employment qualification = (Employment) firstOrcidObject; + assertThat(qualification.getStartDate(), notNullValue()); + assertThat(qualification.getStartDate().getYear().getValue(), is("2020")); + assertThat(qualification.getStartDate().getMonth().getValue(), is("02")); + assertThat(qualification.getStartDate().getDay().getValue(), is("01")); + assertThat(qualification.getEndDate(), nullValue()); + assertThat(qualification.getRoleTitle(), is("Researcher")); + assertThat(qualification.getDepartmentName(), is("4Science")); + + Organization organization = qualification.getOrganization(); + assertThat(organization, notNullValue()); + assertThat(organization.getName(), is("4Science")); + assertThat(organization.getAddress(), notNullValue()); + assertThat(organization.getAddress().getCountry(), is(Iso3166Country.IT)); + assertThat(organization.getAddress().getCity(), is("Milan")); + assertThat(organization.getDisambiguatedOrganization(), nullValue()); + } + + + @Test public void testQualificationCreation() { context.turnOffAuthorisationSystem(); diff --git a/dspace-api/src/test/java/org/dspace/util/PersonNameUtilTest.java b/dspace-api/src/test/java/org/dspace/util/PersonNameUtilTest.java index fe80bf143756..c0c5a0c02194 100644 --- a/dspace-api/src/test/java/org/dspace/util/PersonNameUtilTest.java +++ b/dspace-api/src/test/java/org/dspace/util/PersonNameUtilTest.java @@ -27,7 +27,8 @@ public class PersonNameUtilTest { @Test public void testWithAllNames() { - Set variants = getAllNameVariants("Luca", "Giamminonni", List.of("Giamminonni, Luca", "Luke Giammo")); + Set variants = getAllNameVariants("Luca", "Giamminonni", List.of("Giamminonni, Luca", + "Luke Giammo"), "uuid"); assertThat(variants, containsInAnyOrder("Giamminonni Luca", "Luca Giamminonni", "Giamminonni L.", "L. Giamminonni", "Giamminonni L", "L Giamminonni", "Luke Giammo", "Giammo Luke")); @@ -37,7 +38,7 @@ public void testWithAllNames() { public void testWithFirstNameComposedByTwoNames() { Set variants = getAllNameVariants("Luca Paolo", "Giamminonni", - List.of("Giamminonni, Luca", "Luke Giammo")); + List.of("Giamminonni, Luca", "Luke Giammo"), "uuid"); assertThat(variants, containsInAnyOrder("Giamminonni Luca Paolo", "Luca Paolo Giamminonni", "Giamminonni Luca", "Luca Giamminonni", "Giamminonni Paolo", "Paolo Giamminonni", @@ -51,7 +52,7 @@ public void testWithFirstNameComposedByTwoNames() { public void testWithFirstNameComposedByThreeNames() { Set variants = getAllNameVariants("Luca Paolo Claudio", "Giamminonni", - List.of("Giamminonni, Luca", "Luke Giammo")); + List.of("Giamminonni, Luca", "Luke Giammo"), "uuid"); assertThat(variants, containsInAnyOrder("Giamminonni Luca Paolo Claudio", "Luca Paolo Claudio Giamminonni", "Giamminonni Luca Claudio", "Luca Claudio Giamminonni", "Giamminonni Paolo Claudio", @@ -69,7 +70,8 @@ public void testWithFirstNameComposedByThreeNames() { @Test public void testWithoutFirstAndLastName() { - Set variants = getAllNameVariants(null, null, List.of("Giamminonni, Luca Fabio", "Luke Giammo")); + Set variants = getAllNameVariants(null, null, List.of("Giamminonni, Luca Fabio", "Luke Giammo"), + "uuid"); assertThat(variants, containsInAnyOrder("Giamminonni Luca Fabio", "Fabio Luca Giamminonni", "Giamminonni Fabio Luca", "Luca Fabio Giamminonni", "Luca Giamminonni Fabio", @@ -80,12 +82,13 @@ public void testWithoutFirstAndLastName() { @Test public void testWithAlreadyTruncatedName() { - Set variants = getAllNameVariants("L.", "Giamminonni", List.of("Giamminonni, Luca")); + Set variants = getAllNameVariants("L.", "Giamminonni", List.of("Giamminonni, Luca"), + "uuid"); assertThat(variants, containsInAnyOrder("Giamminonni Luca", "Luca Giamminonni", "Giamminonni L.", "L. Giamminonni", "Giamminonni L", "L Giamminonni")); - variants = getAllNameVariants("L. P.", "Giamminonni", List.of("Giamminonni, Luca")); + variants = getAllNameVariants("L. P.", "Giamminonni", List.of("Giamminonni, Luca"), "uuid"); assertThat(variants, containsInAnyOrder("Giamminonni Luca", "Luca Giamminonni", "L. Giamminonni", "Giamminonni L.", "P. Giamminonni", "Giamminonni P.", "Giamminonni L. P.", "L. P. Giamminonni", @@ -97,7 +100,8 @@ public void testWithAlreadyTruncatedName() { @Test public void testWithAlreadyTruncatedNameOnFullName() { - Set variants = getAllNameVariants("Luca", "Giamminonni", List.of("Giamminonni, L.")); + Set variants = getAllNameVariants("Luca", "Giamminonni", List.of("Giamminonni, L."), + "uuid"); assertThat(variants, containsInAnyOrder("Giamminonni Luca", "Luca Giamminonni", "Giamminonni L.", "L. Giamminonni", "Giamminonni L", "L Giamminonni")); diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 08c2fe062265..3b56ba53e832 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index f3b7e23bf4e5..76718f44ba3c 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index f5acab4dfc70..eb63a67e4579 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index e1ade345ba72..257d0b3a91f8 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index a2add8447d8b..c94bfaeed6ae 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditMetadataFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditMetadataFeature.java index 820de57b7246..80a052224fb8 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditMetadataFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EditMetadataFeature.java @@ -60,12 +60,12 @@ public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLEx ) { String defaultGroupUUID = configurationService.getProperty("edit.metadata.allowed-group"); if (StringUtils.isBlank(defaultGroupUUID)) { - return authorizeServiceRestUtil.authorizeActionBoolean(context, object,DSpaceRestPermission.WRITE); + return authorizeServiceRestUtil.authorizeActionBoolean(context, object, DSpaceRestPermission.WRITE); } Group defaultGroup = StringUtils.isNotBlank(defaultGroupUUID) ? groupService.find(context, UUID.fromString(defaultGroupUUID)) : null; if (Objects.nonNull(defaultGroup) && groupService.isMember(context, defaultGroup)) { - return authorizeServiceRestUtil.authorizeActionBoolean(context, object,DSpaceRestPermission.WRITE); + return authorizeServiceRestUtil.authorizeActionBoolean(context, object, DSpaceRestPermission.WRITE); } } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java index b64da66af9cf..8c7e89565371 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java @@ -40,6 +40,7 @@ import org.dspace.app.rest.model.patch.Patch; import org.dspace.app.rest.repository.handler.service.UriListHandlerService; import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Bundle; import org.dspace.content.Collection; import org.dspace.content.Item; @@ -106,6 +107,9 @@ public class ItemRestRepository extends DSpaceObjectRestRepository findAll(Context context, Pageable pageable) { protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID id, Patch patch) throws AuthorizeException, SQLException { Item item = itemService.find(context, id); - if (!editMetadataFeature.isAuthorized(context, converter.toRest(item, utils.obtainProjection()))) { + if (!authorizeService.isAdmin(context) && + !editMetadataFeature.isAuthorized(context, converter.toRest(item, utils.obtainProjection()))) { throw new AccessDeniedException("Current user not authorized for this operation"); } patchDSpaceObject(apiCategory, model, id, patch); @@ -350,13 +355,13 @@ protected ItemRest put(Context context, HttpServletRequest request, String apiCa } catch (IOException e1) { throw new UnprocessableEntityException("Error parsing request body", e1); } - if (!editMetadataFeature.isAuthorized(context, itemRest)) { - throw new AccessDeniedException("Current user not authorized for this operation"); - } Item item = itemService.find(context, uuid); if (item == null) { throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + uuid + " not found"); } + if (!authorizeService.isAdmin(context) && !editMetadataFeature.isAuthorized(context, itemRest)) { + throw new AccessDeniedException("Current user not authorized for this operation"); + } if (StringUtils.equals(uuid.toString(), itemRest.getId())) { metadataConverter.setMetadata(context, item, itemRest.getMetadata()); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java index 29caf8def121..7e0edf08bac9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java @@ -4552,8 +4552,8 @@ public void putItemMetadataWithUserNotPartOfGroupConfigured() throws Exception { itemRest.setInArchive(true); itemRest.setDiscoverable(true); itemRest.setWithdrawn(false); - String token = getAuthToken(admin.getEmail(), password); - MvcResult mvcResult = getClient(token).perform(post("/api/core/items?owningCollection=" + + String adminToken = getAuthToken(admin.getEmail(), password); + MvcResult mvcResult = getClient(adminToken).perform(post("/api/core/items?owningCollection=" + col1.getID().toString()) .content(mapper.writeValueAsBytes(itemRest)) .contentType(contentType)) @@ -4569,12 +4569,25 @@ public void putItemMetadataWithUserNotPartOfGroupConfigured() throws Exception { itemRest.setHandle(itemHandleString); Group group = GroupBuilder.createGroup(context).build(); configurationService.setProperty("edit.metadata.allowed-group", group.getID()); + // add write rights to the user + ResourcePolicyBuilder.createResourcePolicy(context) + .withUser(eperson) + .withAction(WRITE) + .withDspaceObject(itemService.find(context, UUID.fromString(itemUuidString))) + .build(); + context.restoreAuthSystemState(); + String token = getAuthToken(eperson.getEmail(), password); // expect forbidden, the user is not part of the group set in property {{edit.metadata.allowed-group}} getClient(token).perform(put("/api/core/items/" + itemUuidString) .content(mapper.writeValueAsBytes(itemRest)) .contentType(contentType)) .andExpect(status().isForbidden()); + // admins should still be able to use put + getClient(adminToken).perform(put("/api/core/items/" + itemUuidString) + .content(mapper.writeValueAsBytes(itemRest)) + .contentType(contentType)) + .andExpect(status().isOk()); } finally { ItemBuilder.deleteItem(UUID.fromString(itemUuidString)); } @@ -4635,7 +4648,7 @@ public void putItemMetadataWithUserPartOfGroupConfigured() throws Exception { context.restoreAuthSystemState(); token = getAuthToken(eperson.getEmail(), password); configurationService.setProperty("edit.metadata.allowed-group", group.getID()); - // expect forbidden, the user is not part of the group set in property {{edit.metadata.allowed-group}} + // expect ok, the user is part of the group set in property {{edit.metadata.allowed-group}} getClient(token).perform(put("/api/core/items/" + itemUuidString) .content(mapper.writeValueAsBytes(itemRest)) .contentType(contentType)) @@ -4907,7 +4920,7 @@ public void findVersionForItemWithoutVersionsWithVersioningDisabledTest() throws public void patchItemMetadataWithUserPartOfGroupConfigured() throws Exception { context.turnOffAuthorisationSystem(); // add admin person as member to the group - Group group = GroupBuilder.createGroup(context).addMember(admin).build(); + Group group = GroupBuilder.createGroup(context).addMember(eperson).build(); groupService.update(context, group); context.commit(); // ** GIVEN ** @@ -4930,15 +4943,19 @@ public void patchItemMetadataWithUserPartOfGroupConfigured() throws Exception { .build(); // add write permission to the user admin ResourcePolicyBuilder.createResourcePolicy(context) - .withUser(admin) + .withUser(eperson) .withAction(WRITE) .withDspaceObject(itemService.find(context, item.getID())) .build(); context.restoreAuthSystemState(); configurationService.setProperty("edit.metadata.allowed-group", group.getID()); - String token = getAuthToken(admin.getEmail(), password); + String token = getAuthToken(eperson.getEmail(), password); List ops = new ArrayList(); - ReplaceOperation replaceOperation = new ReplaceOperation("/withdrawn", true); + List> titleValue = new ArrayList<>(); + Map value = new HashMap(); + value.put("value", "New title"); + titleValue.add(value); + ReplaceOperation replaceOperation = new ReplaceOperation("/metadata/dc.title", titleValue); ops.add(replaceOperation); String patchBody = getPatchContent(ops); // withdraw item @@ -4948,8 +4965,7 @@ public void patchItemMetadataWithUserPartOfGroupConfigured() throws Exception { .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.uuid", Matchers.is(item.getID().toString()))) - .andExpect(jsonPath("$.withdrawn", Matchers.is(true))) - .andExpect(jsonPath("$.inArchive", Matchers.is(false))); + .andExpect(jsonPath("$.metadata['dc.title'][0].value", Matchers.is("New title"))); } @Test @@ -4974,7 +4990,7 @@ public void patchItemMetadataWithUserNotPartOfGroupConfigured() throws Exception .build(); // add write rights to the user admin ResourcePolicyBuilder.createResourcePolicy(context) - .withUser(admin) + .withUser(eperson) .withAction(WRITE) .withDspaceObject(itemService.find(context, item.getID())) .build(); @@ -4984,9 +5000,13 @@ public void patchItemMetadataWithUserNotPartOfGroupConfigured() throws Exception context.commit(); context.restoreAuthSystemState(); configurationService.setProperty("edit.metadata.allowed-group", group.getID()); - String token = getAuthToken(admin.getEmail(), password); + String token = getAuthToken(eperson.getEmail(), password); List ops = new ArrayList(); - ReplaceOperation replaceOperation = new ReplaceOperation("/withdrawn", true); + List> titleValue = new ArrayList<>(); + Map value = new HashMap(); + value.put("value", "New title"); + titleValue.add(value); + ReplaceOperation replaceOperation = new ReplaceOperation("/metadata/dc.title", titleValue); ops.add(replaceOperation); String patchBody = getPatchContent(ops); // withdraw item @@ -4995,6 +5015,14 @@ public void patchItemMetadataWithUserNotPartOfGroupConfigured() throws Exception .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) .andExpect(status().isForbidden()); + token = getAuthToken(admin.getEmail(), password); + //expect ok as admin + getClient(token).perform(patch("/api/core/items/" + item.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.uuid", Matchers.is(item.getID().toString()))) + .andExpect(jsonPath("$.metadata['dc.title'][0].value", Matchers.is("New title"))); } @Test diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 2caba151f403..867ac1dc1a7e 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 716361a5406c..10a44f3615f6 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index 8393d311b4ca..e76bfca65b9f 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. 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 + + + + + + diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml index ddf410abec3b..ac57d73923b5 100644 --- a/dspace/config/submission-forms.xml +++ b/dspace/config/submission-forms.xml @@ -37,6 +37,17 @@ + + + bitstream + hide + + dropdown + false + + + +
diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 9dae87e503ba..7de65e9ca49e 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 6e9daa881871..af44a7efc2e7 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index fa3083bda8d7..4dfa2939bf90 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index eeb283d96ea2..e8a714dfd25e 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT .. diff --git a/dspace/pom.xml b/dspace/pom.xml index 8e9226149995..9d21239d1034 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index a06395b3c0b3..7cfaf08e0011 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT DSpace Parent Project DSpace-CRIS is an open source extension of DSpace (http://www.dspace.org) providing out of box support for the CRIS / RIMS and moder Institution Repository use cases with advanced features and optimized configurations @@ -958,14 +958,14 @@ org.dspace dspace-rest - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT jar classes org.dspace dspace-rest - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT war @@ -1116,69 +1116,69 @@ org.dspace dspace-api - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT org.dspace dspace-api test-jar - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT test org.dspace.modules additions - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT org.dspace dspace-sword - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT org.dspace dspace-swordv2 - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT org.dspace dspace-oai - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT org.dspace dspace-services - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT org.dspace dspace-server-webapp test-jar - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT test org.dspace dspace-rdf - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT org.dspace dspace-iiif - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT org.dspace dspace-server-webapp - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT jar classes org.dspace dspace-server-webapp - cris-2023.02.00 + cris-2023.02.02-SNAPSHOT war @@ -2024,7 +2024,7 @@ scm:git:git@github.com:4Science/DSpace.git scm:git:git@github.com:4Science/DSpace.git git@github.com:4Science/DSpace.git - dspace-cris-2023.02.00 + dspace-cris-2023.02.02-SNAPSHOT