From 69e0accecea2fc8c696c53fab962ca918cff95d4 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 25 May 2023 11:43:25 +0200 Subject: [PATCH 01/81] [DSC-1101] Porting of CST-10226 --- .../content/authority/DCInputAuthority.java | 10 +- .../crosswalks/script/ItemExport.java | 12 + .../VirtualFieldVocabularyI18nValuePair.java | 193 ++++++++++++++ ...n-publication-with-vocabulary-xml.template | 7 + ...ld-vocabulary_18n-publication-xml.template | 6 + .../config/spring/api/test-beans.xml | 18 ++ .../crosswalks/ReferCrosswalkIT.java | 239 ++++++++++++++++++ dspace/config/spring/api/crosswalks.xml | 2 + 8 files changed, 483 insertions(+), 4 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldVocabularyI18nValuePair.java create mode 100644 dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-with-vocabulary-xml.template create mode 100644 dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-xml.template diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java index 9695f9c32552..ca9f42f13a3d 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java @@ -48,6 +48,8 @@ * fields. */ public class DCInputAuthority extends SelfNamedPlugin implements ChoiceAuthority { + public static final String UNKNOWN_KEY = "UNKNOWN KEY "; + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(DCInputAuthority.class); /** @@ -92,7 +94,7 @@ public static String[] getPluginNames() { initPluginNames(); } - return (String[]) ArrayUtils.clone(pluginNames); + return ArrayUtils.clone(pluginNames); } private static synchronized void initPluginNames() { @@ -205,17 +207,17 @@ public String getLabel(String key, String locale) { String[] labelsLocale = labels.get(locale); int pos = -1; // search in the values to return the label - for (int i = 0; i < valuesLocale.length; i++) { + for (int i = 0; valuesLocale != null && i < valuesLocale.length; i++) { if (valuesLocale[i].equals(key)) { pos = i; break; } } - if (pos != -1) { + if (pos != -1 && labelsLocale != null) { // return the label in the same position where we found the value return labelsLocale[pos]; } else { - return "UNKNOWN KEY " + key; + return UNKNOWN_KEY + key; } } diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/script/ItemExport.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/script/ItemExport.java index d52287ad3631..43b5d0b0971e 100644 --- a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/script/ItemExport.java +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/script/ItemExport.java @@ -10,6 +10,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.sql.SQLException; +import java.util.Objects; import java.util.UUID; import org.apache.commons.cli.ParseException; @@ -67,6 +68,7 @@ public void internalRun() throws Exception { context = new Context(Context.Mode.READ_ONLY); assignCurrentUserInContext(); + assignHandlerLocaleInContext(); assignSpecialGroupsInContext(); if (exportFormat == null) { @@ -140,6 +142,16 @@ private void assignSpecialGroupsInContext() throws SQLException { } } + private void assignHandlerLocaleInContext() { + if (Objects.nonNull(this.handler) && + Objects.nonNull(this.context) && + Objects.nonNull(this.handler.getLocale()) && + !this.handler.getLocale().equals(this.context.getCurrentLocale()) + ) { + this.context.setCurrentLocale(this.handler.getLocale()); + } + } + private StreamDisseminationCrosswalk getCrosswalkByType(String type) { return new DSpace().getSingletonService(StreamDisseminationCrosswalkMapper.class).getByType(type); } diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldVocabularyI18nValuePair.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldVocabularyI18nValuePair.java new file mode 100644 index 000000000000..89d9181c20bc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldVocabularyI18nValuePair.java @@ -0,0 +1,193 @@ +/** + * 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.virtualfields; + +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.authority.ChoiceAuthority; +import org.dspace.content.authority.DCInputAuthority; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.core.I18nUtil; +import org.dspace.core.factory.CoreServiceFactory; +import org.dspace.core.service.PluginService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link VirtualField} that translates {@code value-pair} + * and {@code vocabulary-fields} into displayable labels. + * Internally uses the {@link ChoiceAuthorityService} to translate them. + *
+ *
+ * (Example: {@code @virtual.vocabulary_18n.metadataField@}) + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class VirtualFieldVocabularyI18nValuePair implements VirtualField { + + private final static Logger LOGGER = LoggerFactory.getLogger(VirtualFieldVocabularyI18nValuePair.class); + + @Autowired + private ItemService itemService; + @Autowired + private ChoiceAuthorityService choiceAuthorityService; + + private PluginService pluginService = CoreServiceFactory.getInstance().getPluginService(); + + @Override + public String[] getMetadata(Context context, Item item, String fieldName) { + String[] virtualFieldName = fieldName.split("\\.", 4); + + if (virtualFieldName.length < 3 || virtualFieldName.length > 4) { + LOGGER.warn("Invalid value-pairs virtual field: " + fieldName); + return new String[] {}; + } + String vocabularyName = getVocabularyName(virtualFieldName); + String metadataField = virtualFieldName[2].replaceAll("-", "."); + Locale locale = getLocale(context); + + return itemService.getMetadataByMetadataString(item, metadataField) + .stream() + .map(metadataValue -> + getLabelForVocabulary(vocabularyName, metadataValue, locale) + .orElse(getDisplayableLabel(item, metadataValue, locale.getLanguage())) + ) + .toArray(String[]::new); + } + + protected Optional getLabelForVocabulary( + String vocabularyName, MetadataValue metadataValue, Locale locale + ) { + return Optional.ofNullable(vocabularyName) + .map(vocabulary -> (ChoiceAuthority) pluginService.getNamedPlugin(ChoiceAuthority.class, vocabulary)) + .filter(Objects::nonNull) + .flatMap(choiceAuthority -> Optional.ofNullable(metadataValue.getAuthority()) + .flatMap( + authority -> getLabelWithFallback(choiceAuthority, authority, locale, I18nUtil.getDefaultLocale()) + ) + .or( + () -> getLabelWithFallback( + choiceAuthority, metadataValue.getValue(), + locale, I18nUtil.getDefaultLocale() + ) + ) + ); + } + + private Optional getLabelWithFallback( + ChoiceAuthority choiceAuthority, String authKey, Locale locale, Locale fallbackLocale + ) { + return getValidLabel( + Optional.ofNullable(choiceAuthority.getLabel(authKey, locale.getLanguage())) + ) + .or( + () -> getValidLabel( + Optional.ofNullable( + choiceAuthority.getLabel( + authKey, + fallbackLocale.getLanguage() + ) + ) + ) + ); + } + + protected String getDisplayableLabel(Item item, MetadataValue metadataValue, String language) { + return getLabelForCurrentLanguage(item, metadataValue, language) + .or(() -> getLabelForDefaultLanguage(item, metadataValue)) + .orElse(metadataValue.getValue()); + } + + protected Optional getLabelForDefaultLanguage(Item item, MetadataValue metadataValue) { + return getLabelForVocabulary(item, metadataValue, I18nUtil.getDefaultLocale().getLanguage()) + .or(() -> getLabelForValuePair(item, metadataValue, I18nUtil.getDefaultLocale().getLanguage())); + } + + protected Optional getLabelForCurrentLanguage(Item item, MetadataValue metadataValue, String language) { + return getLabelForVocabulary(item, metadataValue, language) + .or(() -> getLabelForValuePair(item, metadataValue, language)); + } + + private Optional getLabelForVocabulary(Item item, MetadataValue metadataValue, String language) { + return getValidLabel( + Optional.ofNullable(metadataValue) + .filter(mv -> StringUtils.isNotBlank(mv.getAuthority())) + .map(mv -> getVocabulary(item, mv, language)) + ); + } + + private Optional getLabelForValuePair(Item item, MetadataValue metadataValue, String language) { + return getValidLabel( + Optional.ofNullable(metadataValue) + .filter(mv -> StringUtils.isNotBlank(mv.getValue())) + .map(mv -> getValuePair(item, mv, language)) + ); + } + + private String getVocabulary(Item item, MetadataValue metadataValue, String language) { + try { + return this.choiceAuthorityService + .getLabel( + metadataValue, item.getType(), + item.getOwningCollection(), language + ); + } catch (Exception e) { + LOGGER.warn("Error while retrieving the vocabulary for: " + + metadataValue.getMetadataField().toString(), e + ); + } + return null; + } + + + private String getValuePair(Item item, MetadataValue metadataValue, String language) { + try { + return this.choiceAuthorityService + .getLabel( + metadataValue.getMetadataField().toString(), item.getType(), + item.getOwningCollection(), metadataValue.getValue(), language + ); + } catch (Exception e) { + LOGGER.warn( + "Error while retrievingthe value-pair for: " + + metadataValue.getMetadataField().toString(), + e + ); + } + return null; + } + + private String getVocabularyName(String[] virtualFieldName) { + return Optional.of(virtualFieldName.length) + .filter(l -> l == 4) + .map(l -> virtualFieldName[l - 1]) + .orElse(null); + } + + private Optional getValidLabel(Optional label) { + return label.filter(this::isValidLabel); + } + + private boolean isValidLabel(String s) { + return s != null && !s.contains(DCInputAuthority.UNKNOWN_KEY); + } + + private Locale getLocale(Context context) { + return Optional.ofNullable(context.getCurrentLocale()) + .orElse(I18nUtil.getDefaultLocale()); + } + +} diff --git a/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-with-vocabulary-xml.template b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-with-vocabulary-xml.template new file mode 100644 index 000000000000..a8ca0b6f5b82 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-with-vocabulary-xml.template @@ -0,0 +1,7 @@ + + @dspace.entity.type@ + @dc.title@ + @virtual.vocabulary_i18n.dc-type.publication-coar-types@ + @virtual.vocabulary_i18n.dc-language-iso.common_iso_languages@ + @virtual.vocabulary_i18n.organization-address-addressCountry.common_iso_countries@ + \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-xml.template b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-xml.template new file mode 100644 index 000000000000..099f285a32e2 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-xml.template @@ -0,0 +1,6 @@ + + @dspace.entity.type@ + @dc.title@ + @virtual.vocabulary_i18n.dc-type@ + @virtual.vocabulary_i18n.dc-language-iso@ + \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml index 87cc17de18a9..89a2c1949dfe 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml @@ -75,6 +75,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java index b4ecc73a0c46..fe28c10e5fd7 100644 --- a/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java +++ b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java @@ -34,6 +34,7 @@ import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; +import java.util.Locale; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -59,6 +60,11 @@ import org.dspace.content.MetadataField; import org.dspace.content.MetadataFieldServiceImpl; import org.dspace.content.RelationshipType; +import org.dspace.content.authority.Choices; +import org.dspace.content.authority.DCInputAuthority; +import org.dspace.content.authority.factory.ContentAuthorityServiceFactory; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.content.authority.service.MetadataAuthorityService; import org.dspace.content.crosswalk.StreamDisseminationCrosswalk; import org.dspace.content.integration.crosswalks.virtualfields.VirtualField; import org.dspace.content.integration.crosswalks.virtualfields.VirtualFieldMapper; @@ -69,6 +75,8 @@ import org.dspace.eperson.EPerson; import org.dspace.layout.CrisLayoutBox; import org.dspace.layout.LayoutSecurity; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.utils.DSpace; import org.json.JSONObject; import org.junit.After; @@ -99,6 +107,12 @@ public class ReferCrosswalkIT extends AbstractIntegrationTestWithDatabase { private VirtualField virtualFieldId; + private ConfigurationService configurationService; + + private MetadataAuthorityService metadataAuthorityService; + + private ChoiceAuthorityService choiceAuthorityService; + @Before public void setup() throws SQLException, AuthorizeException { @@ -111,6 +125,10 @@ public void setup() throws SQLException, AuthorizeException { this.itemService = new DSpace().getSingletonService(ItemServiceImpl.class); this.mfss = new DSpace().getSingletonService(MetadataFieldServiceImpl.class); + this.configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + this.metadataAuthorityService = ContentAuthorityServiceFactory.getInstance().getMetadataAuthorityService(); + this.choiceAuthorityService = ContentAuthorityServiceFactory.getInstance().getChoiceAuthorityService(); + this.virtualFieldId = this.virtualFieldMapper.getVirtualField("id"); VirtualField mockedVirtualFieldId = mock(VirtualField.class); @@ -2530,6 +2548,227 @@ public void testVirtualBitstreamFieldWithProject() throws Exception { assertThat(resultLines[54].trim(), equalTo("")); } + @Test + public void testPublicationVirtualFieldWithVocabularyValuePairList() throws Exception { + + Locale defaultLocale = context.getCurrentLocale(); + String[] defaultLocales = this.configurationService.getArrayProperty("webui.supported.locales"); + + try { + + Locale ukranian = new Locale("uk"); + + context.turnOffAuthorisationSystem(); + // reset supported locales + this.configurationService.setProperty( + "webui.supported.locales", + new String[] {Locale.ENGLISH.getLanguage(), Locale.ITALIAN.getLanguage(), ukranian.getLanguage()} + ); + this.metadataAuthorityService.clearCache(); + this.choiceAuthorityService.clearCache(); + // reload plugin + DCInputAuthority.reset(); + DCInputAuthority.getPluginNames(); + // set italian locale + context.setCurrentLocale(Locale.ITALIAN); + + String vocabularyName = "publication-coar-types"; + Collection publicationCollection = + createCollection(context, community) + .withEntityType("Publication") + .withSubmissionDefinition("publication") + .withAdminGroup(eperson) + .build(); + + Item publicationItem = createItem(context, publicationCollection) + .withEntityType("Publication") + .withTitle("Publication title") + .withType("not translated", vocabularyName + ":c_7bab") + .withLanguage("en_US") + .build(); + + context.restoreAuthSystemState(); + + ReferCrosswalk referCrosswalk = + new DSpace().getServiceManager() + .getServiceByName( + "referCrosswalkPublicationVirtualVocabularyI18nFieldWithVocabulary", ReferCrosswalk.class + ); + assertThat(referCrosswalk, notNullValue()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publicationItem, out); + + String[] resultLines = out.toString().split("\n"); + assertThat(resultLines.length, is(7)); + assertThat(resultLines[0].trim(), equalTo("")); + assertThat(resultLines[4].trim(), equalTo("software paper")); + assertThat(resultLines[5].trim(), equalTo("Inglese (USA)")); + assertThat(resultLines[6].trim(), equalTo("")); + + context.setCurrentLocale(ukranian); + out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publicationItem, out); + + resultLines = out.toString().split("\n"); + assertThat(resultLines.length, is(7)); + assertThat(resultLines[0].trim(), equalTo("")); + assertThat(resultLines[4].trim(), equalTo("software paper")); + assertThat(resultLines[5].trim(), equalTo("Американська (USA)")); + assertThat(resultLines[6].trim(), equalTo("")); + + } finally { + context.setCurrentLocale(defaultLocale); + this.configurationService.setProperty("webui.supported.locales",defaultLocales); + } + } + + @Test + public void testPublicationVirtualFieldValuePairList() throws Exception { + + context.turnOffAuthorisationSystem(); + String vocabularyName = "publication-coar-types"; + Collection publicationCollection = + createCollection(context, community) + .withEntityType("Publication") + .withSubmissionDefinition("publication") + .withAdminGroup(eperson) + .build(); + + Item publicationItem = createItem(context, publicationCollection) + .withEntityType("Publication") + .withTitle("Publication title") + .withType("not translated", vocabularyName + ":c_7bab") + .withLanguage("en_US") + .build(); + + context.restoreAuthSystemState(); + + ReferCrosswalk referCrosswalk = + new DSpace().getServiceManager() + .getServiceByName("referCrosswalkPublicationVirtualVocabularyI18nField", ReferCrosswalk.class); + assertThat(referCrosswalk, notNullValue()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publicationItem, out); + + String[] resultLines = out.toString().split("\n"); + assertThat(resultLines.length, is(7)); + assertThat(resultLines[0].trim(), equalTo("")); + assertThat(resultLines[4].trim(), equalTo("software paper")); + assertThat(resultLines[5].trim(), equalTo("English (United States)")); + assertThat(resultLines[6].trim(), equalTo("")); + } + + @Test + public void testPublicationMultilanguageVirtualFieldValuePairList() throws Exception { + + Locale defaultLocale = context.getCurrentLocale(); + String[] defaultLocales = this.configurationService.getArrayProperty("webui.supported.locales"); + try { + + Locale ukranian = new Locale("uk"); + + context.turnOffAuthorisationSystem(); + // reset supported locales + this.configurationService.setProperty( + "webui.supported.locales", + new String[] {Locale.ENGLISH.getLanguage(), Locale.ITALIAN.getLanguage(), ukranian.getLanguage()} + ); + this.metadataAuthorityService.clearCache(); + this.choiceAuthorityService.clearCache(); + // reload plugin + DCInputAuthority.reset(); + DCInputAuthority.getPluginNames(); + // set italian locale + context.setCurrentLocale(Locale.ITALIAN); + + String subjectVocabularyName = "srsc"; + Collection publicationCollection = + createCollection(context, community) + .withEntityType("Publication") + .withSubmissionDefinition("languagetestprocess") + .withAdminGroup(eperson) + .build(); + + Item publicationItem = createItem(context, publicationCollection) + .withTitle("Publication title") + .withType("not translated", subjectVocabularyName + ":SCB16") + .withLanguage("en_US") + .build(); + + this.itemService.addMetadata( + context, publicationItem, + "organization", "address", "addressCountry", + Item.ANY, "IT", null, Choices.CF_UNSET, 0 + ); + + context.restoreAuthSystemState(); + + ReferCrosswalk referCrosswalk = + new DSpace().getServiceManager() + .getServiceByName( + "referCrosswalkPublicationVirtualVocabularyI18nFieldWithVocabulary", ReferCrosswalk.class + ); + assertThat(referCrosswalk, notNullValue()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publicationItem, out); + + String[] resultLines = out.toString().split("\n"); + assertThat(resultLines.length, is(7)); + assertThat(resultLines[0].trim(), equalTo("")); + assertThat(resultLines[3].trim(), equalTo("TECNOLOGIA")); + assertThat(resultLines[4].trim(), equalTo("Inglese (USA)")); + assertThat(resultLines[5].trim(), equalTo("Italia")); + assertThat(resultLines[6].trim(), equalTo("")); + + context.turnOffAuthorisationSystem(); + // set uk locale + context.setCurrentLocale(ukranian); + context.restoreAuthSystemState(); + + out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publicationItem, out); + + resultLines = out.toString().split("\n"); + assertThat(resultLines.length, is(7)); + assertThat(resultLines[0].trim(), equalTo("")); + assertThat(resultLines[3].trim(), equalTo("ТЕХНОЛОГІЯ")); + assertThat(resultLines[4].trim(), equalTo("Американська (USA)")); + // take value from submission_forms (_uk doesn't have the value-pair) + assertThat(resultLines[5].trim(), equalTo("Italia")); + assertThat(resultLines[6].trim(), equalTo("")); + + context.turnOffAuthorisationSystem(); + // set uknown locale + context.setCurrentLocale(new Locale("ru")); + context.restoreAuthSystemState(); + + out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publicationItem, out); + + // it uses the default locale (en) + resultLines = out.toString().split("\n"); + assertThat(resultLines.length, is(7)); + // takes the value from default (_ru doesn't exist) + assertThat(resultLines[0].trim(), equalTo("")); + assertThat(resultLines[3].trim(), equalTo("TECHNOLOGY")); + assertThat( + resultLines[4].trim(), equalTo("English (United States)") + ); + // takes the value from submission_forms (_ru doesn't exist) + assertThat(resultLines[5].trim(), equalTo("Italia")); + assertThat(resultLines[6].trim(), equalTo("")); + + } finally { + context.setCurrentLocale(defaultLocale); + configurationService.setProperty("webui.supported.locales", defaultLocales); + DCInputAuthority.reset(); + DCInputAuthority.getPluginNames(); + } + } + private void createSelectedRelationship(Item author, Item publication, RelationshipType selectedRelationshipType) { createRelationshipBuilder(context, publication, author, selectedRelationshipType, -1, -1).build(); diff --git a/dspace/config/spring/api/crosswalks.xml b/dspace/config/spring/api/crosswalks.xml index 18d49f3433f4..2ff96907efe7 100644 --- a/dspace/config/spring/api/crosswalks.xml +++ b/dspace/config/spring/api/crosswalks.xml @@ -500,6 +500,7 @@ + @@ -549,6 +550,7 @@ + From 4f4fe192763791c79f2ec0ddd04003cd16774fa9 Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Tue, 13 Jun 2023 15:25:29 +0200 Subject: [PATCH 02/81] [DSC-1124] included inline-group fields for indexing --- .../authority/ChoiceAuthorityServiceImpl.java | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java index 6e0800457397..cbc92c3be5f6 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java @@ -7,6 +7,7 @@ */ package org.dspace.content.authority; +import static java.lang.Integer.MAX_VALUE; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.ArrayList; @@ -21,6 +22,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.util.DCInput; import org.dspace.app.util.DCInputSet; @@ -64,7 +66,7 @@ * @see ChoiceAuthority */ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService { - private Logger log = org.apache.logging.log4j.LogManager.getLogger(ChoiceAuthorityServiceImpl.class); + private Logger log = LogManager.getLogger(ChoiceAuthorityServiceImpl.class); // map of field key to authority plugin protected Map controller = new HashMap(); @@ -343,16 +345,26 @@ private void loadChoiceAuthorityConfigurations() { */ private void autoRegisterChoiceAuthorityFromInputReader() { try { - List submissionConfigs = itemSubmissionConfigReader - .getAllSubmissionConfigs(Integer.MAX_VALUE, 0); + List submissionConfigs = itemSubmissionConfigReader.getAllSubmissionConfigs(MAX_VALUE, 0); DCInputsReader dcInputsReader = new DCInputsReader(); // loop over all the defined item submission configuration for (SubmissionConfig subCfg : submissionConfigs) { String submissionName = subCfg.getSubmissionName(); List inputsBySubmissionName = dcInputsReader.getInputsBySubmissionName(submissionName); - autoRegisterChoiceAuthorityFromSubmissionForms(Constants.ITEM, submissionName, - inputsBySubmissionName); + List inputsByGroupOfAllSteps = new ArrayList(); + try { + List inputsByGroup = dcInputsReader.getInputsByGroup(submissionName); + inputsByGroupOfAllSteps.addAll(inputsByGroup); + for (DCInputSet step : inputsBySubmissionName) { + List inputsByGroupOfStep = dcInputsReader.getInputsByGroup(step.getFormName()); + inputsByGroupOfAllSteps.addAll(inputsByGroupOfStep); + } + } catch (DCInputsReaderException e) { + log.warn("Cannot load the groups of the submission: " + submissionName, e); + } + inputsBySubmissionName.addAll(inputsByGroupOfAllSteps); + autoRegisterChoiceAuthorityFromSubmissionForms(Constants.ITEM, submissionName, inputsBySubmissionName); } // loop over all the defined bitstream metadata submission configuration for (UploadConfiguration uploadCfg : uploadConfigurationService.getMap().values()) { @@ -363,8 +375,7 @@ private void autoRegisterChoiceAuthorityFromInputReader() { } } catch (DCInputsReaderException e) { // the system is in an illegal state as the submission definition is not valid - throw new IllegalStateException("Error reading the item submission configuration: " + e.getMessage(), - e); + throw new IllegalStateException("Error reading the item submission configuration: " + e.getMessage(), e); } } From 3db67538b12652e2d0ac790c1f85c56eb5f15b7e Mon Sep 17 00:00:00 2001 From: Mykhaylo Date: Wed, 14 Jun 2023 12:45:47 +0200 Subject: [PATCH 03/81] [DSC-1124] added missing check for empty collection --- .../src/main/java/org/dspace/app/util/DCInputsReader.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java index ac8031880233..86b45c367941 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java @@ -316,6 +316,9 @@ public List getInputsByGroup(String formName) // cache miss - construct new DCInputSet List>> pages = formDefns.get(formName); + if (pages == null) { + return results; + } Iterator>> iterator = pages.iterator(); From 0691ad26adace58fa1f7b34ab9ae79b0fd96939d Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 19 Jun 2023 16:36:25 +0200 Subject: [PATCH 04/81] [DSC-782][CST-6963] New metadata and solr index for file type feat: - new consumer for bitstream to copy metadatas as file type - new solr index for child bitstream type - new discovery configuration - new dspace types. --- .../FileTypeMetadataEnhancerConsumer.java | 274 +++++++++++ .../discovery/SolrServiceFileInfoPlugin.java | 202 ++++++-- .../FileTypeMetadataEnhancerConsumerIT.java | 432 ++++++++++++++++++ dspace/config/dspace.cfg | 6 +- dspace/config/registries/dspace-types.xml | 7 + dspace/config/spring/api/discovery.xml | 51 +++ 6 files changed, 944 insertions(+), 28 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumer.java create mode 100644 dspace-api/src/test/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumerIT.java diff --git a/dspace-api/src/main/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumer.java b/dspace-api/src/main/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumer.java new file mode 100644 index 000000000000..b5c51e93e766 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumer.java @@ -0,0 +1,274 @@ +/** + * 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.app.filetype.consumer; + +import static org.dspace.util.FunctionalUtils.throwingConsumerWrapper; +import static org.dspace.util.FunctionalUtils.throwingMapperWrapper; + +import java.sql.SQLException; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.codec.binary.StringUtils; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataFieldName; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +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.event.Consumer; +import org.dspace.event.Event; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FileTypeMetadataEnhancerConsumer implements Consumer { + + private static final Logger logger = LoggerFactory.getLogger(FileTypeMetadataEnhancerConsumer.class); + + protected static final MetadataFieldName entityTypeMetadata = new MetadataFieldName("dc", "type"); + protected static final MetadataFieldName fileTypeMetadata = new MetadataFieldName("dspace", "file", "type"); + private static final List itemMetadatas = List.of(fileTypeMetadata); + private static final List bitstreamMetadatas = List.of(entityTypeMetadata); + private static final Map bitstreamToItemMetadatasMap = Map.of( + entityTypeMetadata.toString(), fileTypeMetadata + ); + + private BitstreamService bitstreamService; + private ItemService itemService; + + private Set bitstreamAlreadyProcessed = new HashSet<>(); + private Set itemsToProcess = new HashSet<>(); + + @Override + public void initialize() throws Exception { + this.bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); + this.itemService = ContentServiceFactory.getInstance().getItemService(); + } + + @Override + public void consume(Context ctx, Event event) throws Exception { + if (Constants.BITSTREAM == event.getSubjectType()) { + this.handleBitStreamConsumer( + ctx, + Optional.ofNullable((Bitstream) event.getObject(ctx)) + .orElse(this.loadBitstream(ctx, event)), + event + ); + } else if (Constants.ITEM == event.getSubjectType() && Event.CREATE == event.getEventType()) { + this.handleItemConsumer( + ctx, + Optional.ofNullable((Item) event.getObject(ctx)) + .orElse(this.loadItem(ctx, event)) + ); + } else { + logger.warn( + "Can't consume the DSPaceObject with id {}, only BITSTREAM and ITEMS'CREATION events are consumable!", + event.getSubjectID() + ); + } + } + + @Override + public void end(Context ctx) throws Exception { + bitstreamAlreadyProcessed.clear(); + this.itemsToProcess + .stream() + .forEach(item -> this.handleItemConsumer(ctx, item)); + itemsToProcess.clear(); + } + + @Override + public void finish(Context ctx) throws Exception {} + + private Bitstream loadBitstream(Context ctx, Event event) { + Bitstream found = null; + try { + found = this.bitstreamService.find(ctx, event.getSubjectID()); + } catch (SQLException e) { + logger.error("Error while retrieving the bitstream with ID: " + event.getSubjectID(), e); + throw new SQLRuntimeException("Error while retrieving the bitstream with ID: " + event.getSubjectID(), e); + } + return found; + } + + private Item loadItem(Context ctx, Event event) { + Item found = null; + try { + found = this.itemService.find(ctx, event.getSubjectID()); + } catch (SQLException e) { + logger.error("Error while retrieving the bitstream with ID: " + event.getSubjectID(), e); + throw new SQLRuntimeException("Error while retrieving the bitstream with ID: " + event.getSubjectID(), e); + } + return found; + } + + private void handleBitStreamConsumer(Context ctx, Bitstream bitstream, Event event) { + + if (bitstream == null || this.alreadyProcessed(bitstream)) { + return; + } + List bitstreamItems = List.of(); + try { + bitstreamItems = bitstream.getBundles() + .stream() + .filter(bundle -> "ORIGINAL".equals(bundle.getName())) + .map(Bundle::getItems) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + bitstreamAlreadyProcessed.add(bitstream); + bitstreamItems + .stream() + .forEach(item -> this.itemsToProcess.add(item)); + } + } + + private void handleItemConsumer(Context ctx, Item item) { + + if (item == null) { + return; + } + + try { + Item loadedItem = this.itemService.find(ctx, item.getID()); + Map> grouped = + Optional.ofNullable(loadedItem) + .map(i -> i.getBundles("ORIGINAL")) + .filter(bundles -> !bundles.isEmpty()) + .map(bundles -> bundles.get(0)) + .map(Bundle::getBitstreams) + .filter(bitstreams -> !bitstreams.isEmpty()) + .map(bitstreams -> getMetadatasForItem(ctx, bitstreams).collect(Collectors.toList())) + .map(metadatas -> groupByMetadataField(metadatas)) + .filter(metadatas -> !metadatas.isEmpty()) + .orElse(Map.of()); + + this.itemService.removeMetadataValues(ctx, loadedItem, getRemovableMetadatas(loadedItem)); + + grouped + .entrySet() + .stream() + .map(entry -> + Map.entry(bitstreamToItemMetadatasMap.get(entry.getKey().toString('.')), entry.getValue()) + ) + .filter(entry -> entry.getKey() != null) + .forEach( + throwingConsumerWrapper(entry -> + this.addMetadata( + ctx, + loadedItem, + entry.getKey(), + entry.getValue() + ) + ) + ); + + } catch (SQLException e) { + logger.error(MessageFormat.format("Error while processing item {}!", item.getID().toString()), e); + throw new SQLRuntimeException(e); + } + + } + + private void addMetadata(Context ctx, Item loadedItem, MetadataFieldName metadata, List value) + throws SQLException { + this.itemService.addMetadata( + ctx, + loadedItem, + metadata.schema, + metadata.element, + metadata.qualifier, + null, + value + ); + } + + private Stream getMetadatasForItem(Context ctx, List bitstreams) { + return bitstreams + .stream() + .map( + throwingMapperWrapper(bitstream -> + this.bitstreamService.find(ctx, bitstream.getID()), + null + ) + ) + .filter(Objects::nonNull) + .flatMap(bitstream -> filterBitstreamMetadatasForItem(bitstream)); + } + + private Stream filterBitstreamMetadatasForItem(Bitstream bitstream) { + return bitstream.getMetadata() + .stream() + .filter( + metadataFilter( + bitstreamMetadatas + ) + ); + } + + private Map> groupByMetadataField(List metadatas) { + return this.collectByGroupingMetadataFieldMappingValue(metadatas.stream()); + } + + private Map> collectByGroupingMetadataFieldMappingValue(Stream stream) { + return stream + .collect( + Collectors.groupingBy( + MetadataValue::getMetadataField, + Collectors.mapping(MetadataValue::getValue, Collectors.toList()) + ) + ); + } + + private boolean alreadyProcessed(Bitstream bitstream) { + return bitstreamAlreadyProcessed.contains(bitstream); + } + + private List getRemovableMetadatas(DSpaceObject dspaceObject) { + return dspaceObject + .getMetadata() + .stream() + .filter( + metadataFilter( + itemMetadatas + ) + ) + .collect(Collectors.toList()); + } + + private Predicate metadataFilter(List metadataFields) { + return metadata -> + metadataFields + .stream() + .filter(field -> + StringUtils.equals(field.schema, metadata.getSchema()) && + StringUtils.equals(field.element, metadata.getElement()) && + StringUtils.equals(field.qualifier, metadata.getQualifier()) + ) + .findFirst() + .isPresent(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java index ab56e4692e39..b4305dc3dd0b 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -8,15 +8,24 @@ package org.dspace.discovery; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import org.apache.commons.lang3.StringUtils; import org.apache.solr.common.SolrInputDocument; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; -import org.dspace.content.Item; +import org.dspace.content.MetadataFieldName; +import org.dspace.content.MetadataValue; import org.dspace.core.Context; import org.dspace.discovery.indexobject.IndexableItem; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; /** *

@@ -36,41 +45,180 @@ * * * @author Martin Walk + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + * */ public class SolrServiceFileInfoPlugin implements SolrServiceIndexPlugin { - private static final Logger log = LogManager.getLogger(SolrServiceFileInfoPlugin.class); + /** + * Class used to map a target metadata into a solr index using {@code SolrInputDocument} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + * + * @param + */ + private static class SolrFieldMetadataMapper { + private final MetadataFieldName metadata; + private final BiFunction> fieldAdder; + + public SolrFieldMetadataMapper(MetadataFieldName metadata, + BiFunction> fieldAdder) { + super(); + this.metadata = metadata; + this.fieldAdder = fieldAdder; + } + + public void map(SolrInputDocument document, String field, T value) { + this.fieldAdder.apply(document, field).accept(value); + } + + public MetadataFieldName getMetadata() { + return metadata; + } + + } + + private static final DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyy-MM-dd"); private static final String BUNDLE_NAME = "ORIGINAL"; private static final String SOLR_FIELD_NAME_FOR_FILENAMES = "original_bundle_filenames"; private static final String SOLR_FIELD_NAME_FOR_DESCRIPTIONS = "original_bundle_descriptions"; + private static final String SOLR_FIELD_NAME_FOR_OAIRE_LICENSE_CONDITION = "original_bundle_oaire_licenseCondition"; + private static final String SOLR_FIELD_NAME_FOR_DATACITE_RIGHTS = "original_bundle_datacite_rights"; + private static final String SOLR_FIELD_NAME_FOR_DATACITE_AVAILABLE = "original_bundle_datacite_available"; + private static final String SOLR_FIELD_NAME_FOR_FILETYPE = "dspace_file_type"; + private static final String SOLR_POSTFIX_FILTER = "_filter"; + private static final String SOLR_POSTFIX_KEYWORD = "_keyword"; + // used for facets and filters of type Date to correctly search them and visualize in facets. + private static final String SOLR_POSTFIX_YEAR = ".year"; + private static final MetadataFieldName METADATA_DATACITE_RIGHTS = new MetadataFieldName("datacite", "rights"); + private static final MetadataFieldName METADATA_DATACITE_AVAILABLE = new MetadataFieldName("datacite", "available"); + private static final MetadataFieldName METADATA_LICENSE_CONDITION = + new MetadataFieldName("oaire", "licenseCondition"); + private static final MetadataFieldName METADATA_FILE_TYPE = new MetadataFieldName("dc", "type"); + + private static final SolrFieldMetadataMapper OAIRE_LICENSE_MAPPER = + new SolrFieldMetadataMapper( + METADATA_LICENSE_CONDITION, + (document, fieldName) -> value -> { + addField(document, fieldName, value); + addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); + addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); + } + ); + + private static final SolrFieldMetadataMapper DATACITE_RIGHTS_MAPPER = + new SolrFieldMetadataMapper( + METADATA_DATACITE_RIGHTS, + (document, fieldName) -> value -> { + addField(document, fieldName, value); + addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); + addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); + } + ); + + private static final SolrFieldMetadataMapper DATACITE_AVAILABLE_MAPPER = + new SolrFieldMetadataMapper( + METADATA_DATACITE_AVAILABLE, + (document, fieldName) -> value -> { + addField(document, fieldName, value); + addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); + addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); + addField(document, fieldName.concat(SOLR_POSTFIX_YEAR), dtf.parseLocalDate(value).getYear()); + } + ); + + private static final SolrFieldMetadataMapper FILE_TYPE_MAPPER = + new SolrFieldMetadataMapper( + METADATA_FILE_TYPE, + (document, fieldName) -> value -> { + addField(document, fieldName, value); + addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); + addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); + } + ); + + private static final Map> mappableMetadatas = Stream.of( + Map.entry(SOLR_FIELD_NAME_FOR_OAIRE_LICENSE_CONDITION, OAIRE_LICENSE_MAPPER), + Map.entry(SOLR_FIELD_NAME_FOR_DATACITE_RIGHTS, DATACITE_RIGHTS_MAPPER), + Map.entry(SOLR_FIELD_NAME_FOR_DATACITE_AVAILABLE, DATACITE_AVAILABLE_MAPPER), + Map.entry(SOLR_FIELD_NAME_FOR_FILETYPE, FILE_TYPE_MAPPER) + ) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + + private static void addField(SolrInputDocument document, String name, Object value) { + document.addField(name, value); + } @Override public void additionalIndex(Context context, IndexableObject indexableObject, SolrInputDocument document) { if (indexableObject instanceof IndexableItem) { - Item item = ((IndexableItem) indexableObject).getIndexedObject(); - List bundles = item.getBundles(); - if (bundles != null) { - for (Bundle bundle : bundles) { - String bundleName = bundle.getName(); - if ((bundleName != null) && bundleName.equals(BUNDLE_NAME)) { - List bitstreams = bundle.getBitstreams(); - if (bitstreams != null) { - for (Bitstream bitstream : bitstreams) { - try { - document.addField(SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName()); - - String description = bitstream.getDescription(); - if ((description != null) && !description.isEmpty()) { - document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description); - } - } catch (Exception e) { - log.warn("Error occurred during update index for item {}", item.getID()); - } - } - } - } + generateBundleIndex(document, ((IndexableItem) indexableObject).getIndexedObject().getBundles()); + } + } + + private void generateBundleIndex(SolrInputDocument document, List bundles) { + if (bundles != null) { + for (Bundle bundle : bundles) { + String bundleName = bundle.getName(); + if (bundleName != null && bundleName.equals(BUNDLE_NAME)) { + generateBitstreamIndex(document, bundle.getBitstreams()); } } } } -} \ No newline at end of file + + /** + * Method that adds index to {@link SolrInputDocument}, iterates between {@code bitstreams} and {@code mappableMetadatas} + * then applies the corresponding mapping function to the bitstream + * + * @param document solr document + * @param bitstreams list of bitstreams to analyze + */ + private void generateBitstreamIndex(SolrInputDocument document, List bitstreams) { + if (document != null && bitstreams != null) { + for (Bitstream bitstream : bitstreams) { + addField(document, SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName()); + + Optional.ofNullable(bitstream.getDescription()) + .filter(StringUtils::isNotEmpty) + .ifPresent( + (description) -> + addField(document, SOLR_FIELD_NAME_FOR_DESCRIPTIONS,description) + ); + + mappableMetadatas + .entrySet() + .stream() + .forEach( + entry -> + this.addNonNullMetadataValueField(bitstream, entry.getValue(), document, entry.getKey()) + ); + } + } + } + + /** + * Method that iterates bitstream's metadatas, verifies if is mappable and then maps the ones configured + * using the {@link SolrFieldMetadataMapper} function. + * + * @param bitstream that contains metadatas to verify + * @param metadataMapper the mapper that will be applied to the metadatas + * @param document solrdocument + * @param fieldName solr index name + */ + private void addNonNullMetadataValueField(Bitstream bitstream, SolrFieldMetadataMapper metadataMapper, + SolrInputDocument document, String fieldName) { + bitstream.getMetadata() + .stream() + .filter(metadata -> + StringUtils.equals(metadataMapper.getMetadata().schema, metadata.getSchema()) && + StringUtils.equals(metadataMapper.getMetadata().element, metadata.getElement()) && + StringUtils.equals(metadataMapper.getMetadata().qualifier, metadata.getQualifier()) + ) + .map(MetadataValue::getValue) + .filter(Objects::nonNull) + .findFirst() + .ifPresent(value -> metadataMapper.map(document, fieldName, (T) value)); + } +} diff --git a/dspace-api/src/test/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumerIT.java b/dspace-api/src/test/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumerIT.java new file mode 100644 index 000000000000..bfa29ab330d4 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumerIT.java @@ -0,0 +1,432 @@ +/** + * 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.app.filetype.consumer; + +import static org.dspace.app.matcher.MetadataValueMatcher.with; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.sql.SQLException; +import java.text.ParseException; +import java.util.function.Predicate; + +import org.apache.commons.codec.binary.StringUtils; +import org.apache.tools.ant.filters.StringInputStream; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.ResourcePolicyBuilder; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.MetadataFieldName; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class FileTypeMetadataEnhancerConsumerIT extends AbstractIntegrationTestWithDatabase { + + private Collection collection; + + private final BitstreamService bitstreamService = ContentServiceFactory.getInstance() + .getBitstreamService(); + private final ItemService itemService = ContentServiceFactory.getInstance() + .getItemService(); + + @Before + public void setup() { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + + collection = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + + context.restoreAuthSystemState(); + } + + @Test + public void testWithoutBitstreams() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection).build(); + context.restoreAuthSystemState(); + context.commit(); + + item = context.reloadEntity(item); + + assertThat(item.getMetadata(), not(hasItem(with("dc.type", null)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file_type", null)))); + + context.turnOffAuthorisationSystem(); + this.itemService.update(context, item); + context.restoreAuthSystemState(); + + item = context.reloadEntity(item); + + assertThat(item.getMetadata(), not(hasItem(with("dc.type", null)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", null)))); + } + + @Test + public void testWithoutEntityType() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection).build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), not(hasItem(with("dc.type", null)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", null)))); + } + + @Test + public void testWithEntityTypeDelete() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection).build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .build(); + + ResourcePolicyBuilder + .createResourcePolicy(context) + .withDspaceObject(bitstream) + .withAction(Constants.READ) + .withUser(admin) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + context.turnOffAuthorisationSystem(); + + this.bitstreamService.delete(context, bitstream); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), not(hasItem(with("dc.type", null)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", null)))); + } + + @Test + public void testWithEntityType() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + final String type = "Publication"; + context.turnOffAuthorisationSystem(); + final Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type))); + } + + @Test + public void testWithTypeEdited() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + String type = "Publication"; + context.turnOffAuthorisationSystem(); + Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + + context.turnOffAuthorisationSystem(); + + type = "Thesis"; + this.bitstreamService.setMetadataSingleValue(context, bitstream, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata, null, type); + this.bitstreamService.update(context, bitstream); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + } + + @Test + public void testWithTypeDeleted() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + final String type = "Publication"; + context.turnOffAuthorisationSystem(); + Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + final MetadataValue entityType = bitstream.getMetadata() + .stream() + .filter(metadataFilter(FileTypeMetadataEnhancerConsumer.entityTypeMetadata)) + .findFirst() + .orElseThrow(); + bitstream.getMetadata().remove(entityType); + context.turnOffAuthorisationSystem(); + + this.bitstreamService.update(context, bitstream); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), not(hasItem(with("dc.type", Mockito.any())))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", Mockito.any())))); + } + + @Test + public void testWithMultipleEntityType() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + final String type = "Publication"; + final String type1 = "Thesis"; + context.turnOffAuthorisationSystem(); + final Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + final Bitstream bitstream1 = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type1) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type, null, 0, -1))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 1, -1))); + } + + @Test + public void testWithMultipleEntityTypeEdited() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + String type = "Publication"; + String type1 = "Thesis"; + context.turnOffAuthorisationSystem(); + Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + Bitstream bitstream1 = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type1) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + bitstream1 = context.reloadEntity(bitstream1); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type, null, 0, -1))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 1, -1))); + + context.turnOffAuthorisationSystem(); + + type = "Journal"; + this.bitstreamService.setMetadataSingleValue( + context, + bitstream, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata, + null, + type + ); + this.bitstreamService.update(context, bitstream); + + type1 = "Journal Article"; + this.bitstreamService.setMetadataSingleValue( + context, + bitstream1, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata, + null, + type1 + ); + this.bitstreamService.update(context, bitstream1); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + bitstream1 = context.reloadEntity(bitstream1); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type, null, 0, -1))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 1, -1))); + } + + @Test + public void testWithMultipleEntityTypeDelete() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + final String type = "Publication"; + final String type1 = "Thesis"; + context.turnOffAuthorisationSystem(); + Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + Bitstream bitstream1 = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type1) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + bitstream1 = context.reloadEntity(bitstream1); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type, null, 0, -1))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 1, -1))); + + context.turnOffAuthorisationSystem(); + + this.bitstreamService.clearMetadata( + context, + bitstream, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata.schema, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata.element, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata.qualifier, + null + ); + this.bitstreamService.update(context, bitstream); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + bitstream1 = context.reloadEntity(bitstream1); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", type, null, 0, -1)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", type1, null, 1, -1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 0, -1))); + } + + private Predicate metadataFilter(MetadataFieldName metadataField) { + return metadata -> + StringUtils.equals(metadataField.schema, metadata.getSchema()) && + StringUtils.equals(metadataField.element, metadata.getElement()) && + StringUtils.equals(metadataField.qualifier, metadata.getQualifier()); + } +} diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 65405e795af8..8ee789089398 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -793,7 +793,7 @@ event.dispatcher.default.class = org.dspace.event.BasicDispatcher # Add doi here if you are using org.dspace.identifier.DOIIdentifierProvider to generate DOIs. # Adding doi here makes DSpace send metadata updates to your doi registration agency. # Add rdf here, if you are using dspace-rdf to export your repository content as RDF. -event.dispatcher.default.consumers = versioning, discovery, eperson, dedup, crisconsumer, orcidqueue, audit, nbeventsdelete, referenceresolver, orcidwebhook, itemenhancer, customurl, reciprocal +event.dispatcher.default.consumers = versioning, discovery, eperson, dedup, crisconsumer, orcidqueue, audit, nbeventsdelete, referenceresolver, orcidwebhook, itemenhancer, customurl, reciprocal, filetypemetadataenhancer # The noindex dispatcher will not create search or browse indexes (useful for batch item imports) @@ -873,6 +873,10 @@ event.consumer.orcidqueue.filters = Item+Install|Modify|Modify_Metadata|Delete|R event.consumer.reciprocal.class = org.dspace.content.authority.ReciprocalItemAuthorityConsumer event.consumer.reciprocal.filters = Item+INSTALL|MODIFY_METADATA|MODIFY +# FileType consumer +event.consumer.filetypemetadataenhancer.class = org.dspace.app.filetype.consumer.FileTypeMetadataEnhancerConsumer +event.consumer.filetypemetadataenhancer.filters = Item+Create|Modify_Metadata:Bitstream+Create|Modify_Metadata|Delete + # ...set to true to enable testConsumer messages to standard output #testConsumer.verbose = true diff --git a/dspace/config/registries/dspace-types.xml b/dspace/config/registries/dspace-types.xml index eb71b9edd9d6..094f2543a42e 100644 --- a/dspace/config/registries/dspace-types.xml +++ b/dspace/config/registries/dspace-types.xml @@ -51,6 +51,13 @@ enabled Stores a boolean text value (true or false) to indicate if the iiif feature is enabled or not for the dspace object. If absent the value is derived from the parent dspace object + + + dspace + file + type + Stores the bitstream's children file type inside the item it self + dspace diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 1ddf0da887e9..d9dfa136a831 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -3628,4 +3628,55 @@ + + + + + + dc.identifier.doi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c8d232e2dc01b3dd4a4760cafa48f0e5d716db72 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 19 Jun 2023 16:37:24 +0200 Subject: [PATCH 05/81] [DSC-782][CST-6963] Indexing bundle-bitstreams metadatas feat: - indexing description, checksum, mime-type size-bytes from bundle --- .../discovery/SolrServiceFileInfoPlugin.java | 262 +++++++++++------- dspace/config/spring/api/discovery.xml | 2 +- 2 files changed, 166 insertions(+), 98 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java index b4305dc3dd0b..4b2aaed46863 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -7,6 +7,8 @@ */ package org.dspace.discovery; +import java.sql.SQLException; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Objects; @@ -19,13 +21,17 @@ import org.apache.commons.lang3.StringUtils; import org.apache.solr.common.SolrInputDocument; import org.dspace.content.Bitstream; +import org.dspace.content.BitstreamFormat; import org.dspace.content.Bundle; +import org.dspace.content.MetadataField; import org.dspace.content.MetadataFieldName; import org.dspace.content.MetadataValue; import org.dspace.core.Context; import org.dspace.discovery.indexobject.IndexableItem; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** *

@@ -58,26 +64,26 @@ public class SolrServiceFileInfoPlugin implements SolrServiceIndexPlugin { * @param */ private static class SolrFieldMetadataMapper { - private final MetadataFieldName metadata; + private final String solrField; private final BiFunction> fieldAdder; - public SolrFieldMetadataMapper(MetadataFieldName metadata, - BiFunction> fieldAdder) { + public SolrFieldMetadataMapper( + String metadata, + BiFunction> fieldAdder + ) { super(); - this.metadata = metadata; + this.solrField = metadata; this.fieldAdder = fieldAdder; } - public void map(SolrInputDocument document, String field, T value) { - this.fieldAdder.apply(document, field).accept(value); - } - - public MetadataFieldName getMetadata() { - return metadata; + public void map(SolrInputDocument document, T value) { + this.fieldAdder.apply(document, this.solrField).accept(value); } } + private static final Logger logger = LoggerFactory.getLogger(SolrServiceFileInfoPlugin.class); + private static final DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyy-MM-dd"); private static final String BUNDLE_NAME = "ORIGINAL"; private static final String SOLR_FIELD_NAME_FOR_FILENAMES = "original_bundle_filenames"; @@ -85,65 +91,92 @@ public MetadataFieldName getMetadata() { private static final String SOLR_FIELD_NAME_FOR_OAIRE_LICENSE_CONDITION = "original_bundle_oaire_licenseCondition"; private static final String SOLR_FIELD_NAME_FOR_DATACITE_RIGHTS = "original_bundle_datacite_rights"; private static final String SOLR_FIELD_NAME_FOR_DATACITE_AVAILABLE = "original_bundle_datacite_available"; - private static final String SOLR_FIELD_NAME_FOR_FILETYPE = "dspace_file_type"; + private static final String SOLR_FIELD_NAME_FOR_MIMETYPE = "original_bundle_mime_type"; + private static final String SOLR_FIELD_NAME_FOR_CHECKSUM = "original_bundle_checksum"; + private static final String SOLR_FIELD_NAME_FOR_SIZEBYTES = "original_bundle_sizebytes"; + private static final String SOLR_FIELD_NAME_FOR_SHORT_DESCRIPTION = "original_bundle_short_description"; private static final String SOLR_POSTFIX_FILTER = "_filter"; private static final String SOLR_POSTFIX_KEYWORD = "_keyword"; + private static final String BITSTREAM_METADATA_SOLR_PREFIX_KEYWORD = "bitstreams."; // used for facets and filters of type Date to correctly search them and visualize in facets. private static final String SOLR_POSTFIX_YEAR = ".year"; private static final MetadataFieldName METADATA_DATACITE_RIGHTS = new MetadataFieldName("datacite", "rights"); private static final MetadataFieldName METADATA_DATACITE_AVAILABLE = new MetadataFieldName("datacite", "available"); private static final MetadataFieldName METADATA_LICENSE_CONDITION = - new MetadataFieldName("oaire", "licenseCondition"); - private static final MetadataFieldName METADATA_FILE_TYPE = new MetadataFieldName("dc", "type"); + new MetadataFieldName("oaire", "licenseCondition"); - private static final SolrFieldMetadataMapper OAIRE_LICENSE_MAPPER = - new SolrFieldMetadataMapper( - METADATA_LICENSE_CONDITION, - (document, fieldName) -> value -> { - addField(document, fieldName, value); - addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); - addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); - } - ); + private static final BiFunction> defaultSolrIndexAdder = + (document, fieldName) -> value -> { + Collection fieldValues = document.getFieldValues(fieldName); + if (fieldValues == null || !fieldValues.contains(value)) { + addField(document, fieldName, value); + addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); + addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); + } + }; - private static final SolrFieldMetadataMapper DATACITE_RIGHTS_MAPPER = - new SolrFieldMetadataMapper( - METADATA_DATACITE_RIGHTS, - (document, fieldName) -> value -> { + private static final BiFunction> simpleSolrIndexAdder = + (document, fieldName) -> value -> { + Collection fieldValues = document.getFieldValues(fieldName); + if (fieldValues == null || !fieldValues.contains(value)) { addField(document, fieldName, value); - addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); - addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); } - ); + }; - private static final SolrFieldMetadataMapper DATACITE_AVAILABLE_MAPPER = - new SolrFieldMetadataMapper( - METADATA_DATACITE_AVAILABLE, - (document, fieldName) -> value -> { - addField(document, fieldName, value); - addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); - addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); - addField(document, fieldName.concat(SOLR_POSTFIX_YEAR), dtf.parseLocalDate(value).getYear()); - } - ); + private static final BiFunction> bitstreamMetadataSolrIndexAdder = + (document, fieldName) -> value -> { + String baseIndex = BITSTREAM_METADATA_SOLR_PREFIX_KEYWORD.concat(fieldName); + Collection fieldValues = document.getFieldValues(baseIndex); + if (fieldValues == null || !fieldValues.contains(value)) { + addField(document, baseIndex, value); + addField(document, baseIndex.concat(SOLR_POSTFIX_KEYWORD), value); + addField(document, baseIndex.concat(SOLR_POSTFIX_FILTER), value); + } + }; - private static final SolrFieldMetadataMapper FILE_TYPE_MAPPER = - new SolrFieldMetadataMapper( - METADATA_FILE_TYPE, - (document, fieldName) -> value -> { - addField(document, fieldName, value); - addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); - addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); - } - ); + private static final BiFunction> yearSolrIndexAdder = + (document, fieldName) -> value -> { + Collection fieldValues = document.getFieldValues(fieldName); + if (fieldValues == null || !fieldValues.contains(value)) { + addField(document, fieldName, value); + addField(document, fieldName.concat(SOLR_POSTFIX_KEYWORD), value); + addField(document, fieldName.concat(SOLR_POSTFIX_FILTER), value); + addField(document, fieldName.concat(SOLR_POSTFIX_YEAR), dtf.parseLocalDate(value).getYear()); + } + }; + + private static final SolrFieldMetadataMapper getFieldMapper( + String solrField, + BiFunction> adder + ) { + return new SolrFieldMetadataMapper(solrField, adder); + } + + private static final SolrFieldMetadataMapper OAIRE_LICENSE_MAPPER = + new SolrFieldMetadataMapper( + SOLR_FIELD_NAME_FOR_OAIRE_LICENSE_CONDITION, + defaultSolrIndexAdder + ); + + private static final SolrFieldMetadataMapper DATACITE_RIGHTS_MAPPER = + new SolrFieldMetadataMapper( + SOLR_FIELD_NAME_FOR_DATACITE_RIGHTS, + defaultSolrIndexAdder + ); + + private static final SolrFieldMetadataMapper DATACITE_AVAILABLE_MAPPER = + new SolrFieldMetadataMapper( + SOLR_FIELD_NAME_FOR_DATACITE_AVAILABLE, + yearSolrIndexAdder + ); - private static final Map> mappableMetadatas = Stream.of( - Map.entry(SOLR_FIELD_NAME_FOR_OAIRE_LICENSE_CONDITION, OAIRE_LICENSE_MAPPER), - Map.entry(SOLR_FIELD_NAME_FOR_DATACITE_RIGHTS, DATACITE_RIGHTS_MAPPER), - Map.entry(SOLR_FIELD_NAME_FOR_DATACITE_AVAILABLE, DATACITE_AVAILABLE_MAPPER), - Map.entry(SOLR_FIELD_NAME_FOR_FILETYPE, FILE_TYPE_MAPPER) - ) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + private static final Map> mappableMetadatas = + Stream.of( + Map.entry(METADATA_LICENSE_CONDITION.toString(), OAIRE_LICENSE_MAPPER), + Map.entry(METADATA_DATACITE_RIGHTS.toString(), DATACITE_RIGHTS_MAPPER), + Map.entry(METADATA_DATACITE_AVAILABLE.toString(), DATACITE_AVAILABLE_MAPPER) + ) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); private static void addField(SolrInputDocument document, String name, Object value) { @@ -153,16 +186,16 @@ private static void addField(SolrInputDocument document, String name, Object val @Override public void additionalIndex(Context context, IndexableObject indexableObject, SolrInputDocument document) { if (indexableObject instanceof IndexableItem) { - generateBundleIndex(document, ((IndexableItem) indexableObject).getIndexedObject().getBundles()); + generateBundleIndex(context, document, ((IndexableItem) indexableObject).getIndexedObject().getBundles()); } } - private void generateBundleIndex(SolrInputDocument document, List bundles) { + private void generateBundleIndex(Context context, SolrInputDocument document, List bundles) { if (bundles != null) { for (Bundle bundle : bundles) { String bundleName = bundle.getName(); if (bundleName != null && bundleName.equals(BUNDLE_NAME)) { - generateBitstreamIndex(document, bundle.getBitstreams()); + generateBitstreamIndex(context, document, bundle.getBitstreams()); } } } @@ -175,50 +208,85 @@ private void generateBundleIndex(SolrInputDocument document, List bundle * @param document solr document * @param bitstreams list of bitstreams to analyze */ - private void generateBitstreamIndex(SolrInputDocument document, List bitstreams) { + private void generateBitstreamIndex(Context context, SolrInputDocument document, List bitstreams) { if (document != null && bitstreams != null) { for (Bitstream bitstream : bitstreams) { - addField(document, SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName()); - - Optional.ofNullable(bitstream.getDescription()) - .filter(StringUtils::isNotEmpty) - .ifPresent( - (description) -> - addField(document, SOLR_FIELD_NAME_FOR_DESCRIPTIONS,description) - ); - - mappableMetadatas - .entrySet() - .stream() - .forEach( - entry -> - this.addNonNullMetadataValueField(bitstream, entry.getValue(), document, entry.getKey()) - ); + + indexBitstreamFields(context, document, bitstream); + + indexBitstreamsMetadatadas(document, bitstream); } } } - /** - * Method that iterates bitstream's metadatas, verifies if is mappable and then maps the ones configured - * using the {@link SolrFieldMetadataMapper} function. - * - * @param bitstream that contains metadatas to verify - * @param metadataMapper the mapper that will be applied to the metadatas - * @param document solrdocument - * @param fieldName solr index name - */ - private void addNonNullMetadataValueField(Bitstream bitstream, SolrFieldMetadataMapper metadataMapper, - SolrInputDocument document, String fieldName) { - bitstream.getMetadata() - .stream() - .filter(metadata -> - StringUtils.equals(metadataMapper.getMetadata().schema, metadata.getSchema()) && - StringUtils.equals(metadataMapper.getMetadata().element, metadata.getElement()) && - StringUtils.equals(metadataMapper.getMetadata().qualifier, metadata.getQualifier()) - ) - .map(MetadataValue::getValue) - .filter(Objects::nonNull) - .findFirst() - .ifPresent(value -> metadataMapper.map(document, fieldName, (T) value)); + private void indexBitstreamFields(Context context, SolrInputDocument document, Bitstream bitstream) { + simpleSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_FILENAMES).accept(bitstream.getName()); + + Optional.ofNullable(bitstream.getDescription()) + .filter(StringUtils::isNotEmpty) + .ifPresent( + (description) -> + simpleSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_DESCRIPTIONS).accept(description) + ); + + try { + Optional formatOptional = + Optional.ofNullable(bitstream.getFormat(context)) + .filter(Objects::nonNull); + + formatOptional + .map(BitstreamFormat::getMIMEType) + .filter(StringUtils::isNotBlank) + .ifPresent(format -> + defaultSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_MIMETYPE).accept(format) + ); + + formatOptional + .map(BitstreamFormat::getShortDescription) + .ifPresent(format -> + simpleSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_SHORT_DESCRIPTION).accept(format) + ); + } catch (SQLException e) { + logger.error("Error while retrievig bitstream format", e); + throw new RuntimeException("Error while retrievig bitstream format", e); + } + + Optional.of(bitstream.getChecksum()) + .filter(StringUtils::isNotBlank) + .map(checksum -> bitstream.getChecksumAlgorithm() + ":" + bitstream.getChecksum()) + .ifPresent(checksum -> + defaultSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_CHECKSUM).accept(checksum) + ); + + Optional.of(bitstream.getSizeBytes()) + .filter(l -> l > 0) + .map(String::valueOf) + .ifPresent(size -> simpleSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_SIZEBYTES).accept(size)); + } + + private void indexBitstreamsMetadatadas(SolrInputDocument document, Bitstream bitstream) { + bitstream + .getMetadata() + .stream() + .filter(metadata -> metadata != null && StringUtils.isNotBlank(metadata.getValue())) + .forEach(metadata -> { + MetadataField metadataField = metadata.getMetadataField(); + String bitstreamMetadata = metadataField.toString('.'); + Optional.ofNullable(mappableMetadatas.get(bitstreamMetadata)) + .filter(Objects::nonNull) + .orElse( + getFieldMapper( + metadataField.toString(), + bitstreamMetadataSolrIndexAdder + ) + ) + .map(document, metadata.getValue()); + }); + } + + private boolean areEquals(MetadataFieldName metadataFieldName, MetadataValue metadata) { + return StringUtils.equals(metadataFieldName.schema, metadata.getSchema()) && + StringUtils.equals(metadataFieldName.element, metadata.getElement()) && + StringUtils.equals(metadataFieldName.qualifier, metadata.getQualifier()); } } diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index d9dfa136a831..773bf797285e 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -3671,7 +3671,7 @@ - + From 1c8baf669b7794f29a118834f91055f7aff7557b Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 19 Jun 2023 16:41:01 +0200 Subject: [PATCH 06/81] [DSC-782][CST-6963] Updated local.cfg for test folder --- dspace-api/src/test/data/dspaceFolder/config/local.cfg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 08eb98710584..fb8b2863506e 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -157,11 +157,11 @@ wos.apiKey = submission.lookup.epo.consumerKey= submission.lookup.epo.consumerSecretKey= -event.dispatcher.default.consumers = versioning, discovery, eperson, dedup, crisconsumer, audit, nbeventsdelete, referenceresolver, orcidwebhook, iiif, itemenhancer, customurl, reciprocal +event.dispatcher.default.consumers = versioning, discovery, eperson, dedup, crisconsumer, audit, nbeventsdelete, referenceresolver, orcidwebhook, iiif, itemenhancer, customurl, reciprocal, filetypemetadataenhancer # setup a dispatcher also with the cris consumer event.dispatcher.cris-default.class = org.dspace.event.BasicDispatcher -event.dispatcher.cris-default.consumers = versioning, discovery, eperson, dedup, crisconsumer, orcidqueue, audit, referenceresolver, orcidwebhook, itemenhancer, customurl +event.dispatcher.cris-default.consumers = versioning, discovery, eperson, dedup, crisconsumer, orcidqueue, audit, referenceresolver, orcidwebhook, itemenhancer, customurl, filetypemetadataenhancer # Enable a test authority control on dc.language.iso field choices.plugin.dc.language.iso = common_iso_languages @@ -213,4 +213,4 @@ logging.server.include-stacktrace-for-httpcode = 400, 401, 404, 403, 422 # Configuration required for thorough testing of browse links webui.browse.link.1 = author:dc.contributor.* -webui.browse.link.2 = subject:dc.subject.* \ No newline at end of file +webui.browse.link.2 = subject:dc.subject.* From 524dd01dc5f321b0ec9afa074e5ac5d3bc9d8787 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 19 Jun 2023 17:36:06 +0200 Subject: [PATCH 07/81] [DSC-782] Fixes compilation errors --- .../java/org/dspace/util/FunctionalUtils.java | 28 +++++++++++++++++++ .../org/dspace/util/ThrowingConsumer.java | 12 ++++++++ .../java/org/dspace/util/ThrowingMapper.java | 12 ++++++++ 3 files changed, 52 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/util/ThrowingConsumer.java create mode 100644 dspace-api/src/main/java/org/dspace/util/ThrowingMapper.java diff --git a/dspace-api/src/main/java/org/dspace/util/FunctionalUtils.java b/dspace-api/src/main/java/org/dspace/util/FunctionalUtils.java index 422c2405a875..66921d041799 100644 --- a/dspace-api/src/main/java/org/dspace/util/FunctionalUtils.java +++ b/dspace-api/src/main/java/org/dspace/util/FunctionalUtils.java @@ -8,6 +8,8 @@ package org.dspace.util; import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -58,4 +60,30 @@ public static T getCheckDefaultOrBuild(Predicate defaultValueChecker, T d return builder.get(); } + public static Consumer throwingConsumerWrapper( + ThrowingConsumer throwingConsumer) { + return i -> { + try { + throwingConsumer.accept(i); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + + public static Function throwingMapperWrapper( + ThrowingMapper throwingConsumer, + R defaultValue + ) { + return i -> { + R value = defaultValue; + try { + value = throwingConsumer.accept(i); + } catch (Exception e) { + throw new RuntimeException(e); + } + return value; + }; + } + } diff --git a/dspace-api/src/main/java/org/dspace/util/ThrowingConsumer.java b/dspace-api/src/main/java/org/dspace/util/ThrowingConsumer.java new file mode 100644 index 000000000000..a04fea3ef41f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/ThrowingConsumer.java @@ -0,0 +1,12 @@ +/** + * 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.util; + +public interface ThrowingConsumer { + void accept(T t) throws E; +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/util/ThrowingMapper.java b/dspace-api/src/main/java/org/dspace/util/ThrowingMapper.java new file mode 100644 index 000000000000..ac4767a85706 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/ThrowingMapper.java @@ -0,0 +1,12 @@ +/** + * 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.util; + +public interface ThrowingMapper { + R accept(T t) throws E; +} \ No newline at end of file From e4703a77fb60cb6e4b80e5d05e692cd3335edb00 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Tue, 20 Jun 2023 12:01:39 +0200 Subject: [PATCH 08/81] [DSC-782] Fixes NPE --- .../discovery/SolrServiceFileInfoPlugin.java | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java index 4b2aaed46863..36fc8f381927 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -220,13 +220,17 @@ private void generateBitstreamIndex(Context context, SolrInputDocument document, } private void indexBitstreamFields(Context context, SolrInputDocument document, Bitstream bitstream) { - simpleSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_FILENAMES).accept(bitstream.getName()); + addAndHandleException( + simpleSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName() + ); Optional.ofNullable(bitstream.getDescription()) .filter(StringUtils::isNotEmpty) .ifPresent( (description) -> - simpleSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_DESCRIPTIONS).accept(description) + addAndHandleException( + simpleSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description + ) ); try { @@ -238,30 +242,56 @@ private void indexBitstreamFields(Context context, SolrInputDocument document, B .map(BitstreamFormat::getMIMEType) .filter(StringUtils::isNotBlank) .ifPresent(format -> - defaultSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_MIMETYPE).accept(format) + addAndHandleException( + defaultSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_MIMETYPE, format + ) ); formatOptional .map(BitstreamFormat::getShortDescription) .ifPresent(format -> - simpleSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_SHORT_DESCRIPTION).accept(format) + addAndHandleException( + simpleSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_SHORT_DESCRIPTION, format + ) ); } catch (SQLException e) { logger.error("Error while retrievig bitstream format", e); throw new RuntimeException("Error while retrievig bitstream format", e); } - Optional.of(bitstream.getChecksum()) + Optional.ofNullable(bitstream.getChecksum()) .filter(StringUtils::isNotBlank) .map(checksum -> bitstream.getChecksumAlgorithm() + ":" + bitstream.getChecksum()) .ifPresent(checksum -> - defaultSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_CHECKSUM).accept(checksum) + addAndHandleException( + defaultSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_CHECKSUM, checksum + ) ); - Optional.of(bitstream.getSizeBytes()) + Optional.ofNullable(bitstream.getSizeBytes()) .filter(l -> l > 0) .map(String::valueOf) - .ifPresent(size -> simpleSolrIndexAdder.apply(document, SOLR_FIELD_NAME_FOR_SIZEBYTES).accept(size)); + .ifPresent(size -> + addAndHandleException( + simpleSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_SIZEBYTES, size + ) + ); + } + + protected void addAndHandleException( + BiFunction> solrIndexAdder, + SolrInputDocument document, Bitstream bitstream, + String field, String value + ) { + try { + solrIndexAdder.apply(document, field).accept(value); + } catch (Exception e) { + logger.warn( + "Error occurred during the update of index field {} for bitstream {}", + field, + bitstream.getID() + ); + } } private void indexBitstreamsMetadatadas(SolrInputDocument document, Bitstream bitstream) { From 61cbe84f1f467bee9cacf49e03f05d47dfb2d1ef Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Tue, 20 Jun 2023 10:31:45 +0000 Subject: [PATCH 09/81] Checkstyle fix for SolrServiceFileInfoPlugin.java --- .../dspace/discovery/SolrServiceFileInfoPlugin.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java index 36fc8f381927..a2c3056ae38d 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -116,12 +116,12 @@ public void map(SolrInputDocument document, T value) { }; private static final BiFunction> simpleSolrIndexAdder = - (document, fieldName) -> value -> { - Collection fieldValues = document.getFieldValues(fieldName); - if (fieldValues == null || !fieldValues.contains(value)) { - addField(document, fieldName, value); - } - }; + (document, fieldName) -> value -> { + Collection fieldValues = document.getFieldValues(fieldName); + if (fieldValues == null || !fieldValues.contains(value)) { + addField(document, fieldName, value); + } + }; private static final BiFunction> bitstreamMetadataSolrIndexAdder = (document, fieldName) -> value -> { From 83393d9a72cd53ee7e9aa0782de94818f0d353ff Mon Sep 17 00:00:00 2001 From: corrado lombardi Date: Thu, 27 Jul 2023 13:17:25 +0200 Subject: [PATCH 10/81] [CST-1072] Added support for additional custom filter to be injected in AccessItemMode --- .../dspace/content/edit/CorrectItemMode.java | 11 +++ .../org/dspace/content/edit/EditItemMode.java | 11 +++ .../content/security/AccessItemMode.java | 4 + .../dspace/content/security/CrisSecurity.java | 3 +- .../security/CrisSecurityServiceImpl.java | 50 +++++++---- .../security/CrisSecurityServiceIT.java | 89 +++++++++++++++++++ 6 files changed, 148 insertions(+), 20 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/edit/CorrectItemMode.java b/dspace-api/src/main/java/org/dspace/content/edit/CorrectItemMode.java index b374861db9a3..2945065db4ea 100644 --- a/dspace-api/src/main/java/org/dspace/content/edit/CorrectItemMode.java +++ b/dspace-api/src/main/java/org/dspace/content/edit/CorrectItemMode.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; +import org.dspace.content.logic.Filter; import org.dspace.content.security.AccessItemMode; import org.dspace.content.security.CrisSecurity; @@ -42,6 +43,7 @@ public class CorrectItemMode implements AccessItemMode { * Contains the list of users metadata for CUSTOM security */ private List items = new ArrayList(); + private Filter additionalFilter; @Override public List getSecurities() { @@ -87,4 +89,13 @@ public void setItems(List items) { public List getGroups() { return groups; } + + public void setAdditionalFilter(Filter additionalFilter) { + this.additionalFilter = additionalFilter; + } + + @Override + public Filter getAdditionalFilter() { + return additionalFilter; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/edit/EditItemMode.java b/dspace-api/src/main/java/org/dspace/content/edit/EditItemMode.java index 4d56ddafe731..6f6b33ecaa28 100644 --- a/dspace-api/src/main/java/org/dspace/content/edit/EditItemMode.java +++ b/dspace-api/src/main/java/org/dspace/content/edit/EditItemMode.java @@ -9,6 +9,7 @@ import java.util.List; +import org.dspace.content.logic.Filter; import org.dspace.content.security.AccessItemMode; import org.dspace.content.security.CrisSecurity; @@ -49,6 +50,7 @@ public class EditItemMode implements AccessItemMode { * Contains the list of items metadata for CUSTOM security */ private List items; + private Filter additionalFilter; @Override public List getSecurities() { @@ -100,6 +102,15 @@ public void setItems(List items) { this.items = items; } + public void setAdditionalFilter(Filter additionalFilter) { + this.additionalFilter = additionalFilter; + } + + @Override + public Filter getAdditionalFilter() { + return additionalFilter; + } + @Override public List getGroups() { return groups; diff --git a/dspace-api/src/main/java/org/dspace/content/security/AccessItemMode.java b/dspace-api/src/main/java/org/dspace/content/security/AccessItemMode.java index 2aee66fed1ff..e2954bf8f83c 100644 --- a/dspace-api/src/main/java/org/dspace/content/security/AccessItemMode.java +++ b/dspace-api/src/main/java/org/dspace/content/security/AccessItemMode.java @@ -9,6 +9,8 @@ import java.util.List; +import org.dspace.content.logic.Filter; + /** * Interface to be extended for the configuration related to access item modes. * @@ -50,4 +52,6 @@ public interface AccessItemMode { * @return the group list */ public List getGroups(); + + public Filter getAdditionalFilter(); } diff --git a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurity.java b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurity.java index 3fcd83864175..9a472b8a40c3 100644 --- a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurity.java +++ b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurity.java @@ -23,6 +23,7 @@ public enum CrisSecurity { ITEM_ADMIN, SUBMITTER, SUBMITTER_GROUP, - GROUP; + GROUP, + ALL; } diff --git a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java index 4a8b2c313846..3b92ca5985f4 100644 --- a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java @@ -18,6 +18,7 @@ import org.dspace.content.Collection; import org.dspace.content.Item; import org.dspace.content.MetadataValue; +import org.dspace.content.logic.Filter; import org.dspace.content.security.service.CrisSecurityService; import org.dspace.content.service.ItemService; import org.dspace.core.Context; @@ -60,25 +61,11 @@ private boolean hasAccess(Context context, Item item, EPerson user, AccessItemMo try { - switch (crisSecurity) { - case ADMIN: - return authorizeService.isAdmin(context, user); - case CUSTOM: - return hasAccessByCustomPolicy(context, item, user, accessMode); - case GROUP: - return hasAccessByGroup(context, user, accessMode.getGroups()); - case ITEM_ADMIN: - return authorizeService.isAdmin(context, user, item); - case OWNER: - return isOwner(user, item); - case SUBMITTER: - return user != null && user.equals(item.getSubmitter()); - case SUBMITTER_GROUP: - return isUserInSubmitterGroup(context, item, user); - case NONE: - default: - return false; - } + boolean checkSecurity = checkSecurity(context, item, user, accessMode, crisSecurity); + Filter additionalFilter = accessMode.getAdditionalFilter(); + + return additionalFilter == null ? checkSecurity + : checkSecurity && additionalFilter.getResult(context, item); } catch (SQLException e) { throw new RuntimeException(e); @@ -86,6 +73,31 @@ private boolean hasAccess(Context context, Item item, EPerson user, AccessItemMo } + private boolean checkSecurity(Context context, Item item, EPerson user, AccessItemMode accessMode, + CrisSecurity crisSecurity) throws SQLException { + switch (crisSecurity) { + case ADMIN: + return authorizeService.isAdmin(context, user); + case CUSTOM: + return hasAccessByCustomPolicy(context, item, user, accessMode); + case GROUP: + return hasAccessByGroup(context, user, accessMode.getGroups()); + case ITEM_ADMIN: + return authorizeService.isAdmin(context, user, item); + case OWNER: + return isOwner(user, item); + case SUBMITTER: + return user != null && user.equals(item.getSubmitter()); + case SUBMITTER_GROUP: + return isUserInSubmitterGroup(context, item, user); + case ALL: + return true; + case NONE: + default: + return false; + } + } + private boolean isOwner(EPerson eperson, Item item) { return ePersonService.isOwnerOfItem(eperson, item); } diff --git a/dspace-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java b/dspace-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java index 854ab0fa300f..2fc14dbf0346 100644 --- a/dspace-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java @@ -24,7 +24,10 @@ import org.dspace.builder.ItemBuilder; import org.dspace.content.Collection; import org.dspace.content.Item; +import org.dspace.content.logic.Filter; +import org.dspace.content.logic.LogicalStatementException; import org.dspace.content.security.service.CrisSecurityService; +import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.utils.DSpace; @@ -368,6 +371,92 @@ public void testHasAccessWithGroupConfig() throws SQLException, AuthorizeExcepti assertThat(crisSecurityService.hasAccess(context, item, fourthUser, accessMode), is(true)); } + @Test + public void testHasAccessWithGroupConfigAndAdditionalFilter() throws SQLException, AuthorizeException { + + context.turnOffAuthorisationSystem(); + + Group firstGroup = GroupBuilder.createGroup(context) + .withName("Group 1") + .build(); + + Group secondGroup = GroupBuilder.createGroup(context) + .withName("Group 2") + .build(); + + Group thirdGroup = GroupBuilder.createGroup(context) + .withName("Group 3") + .build(); + + EPerson firstUser = EPersonBuilder.createEPerson(context) + .withEmail("user@mail.it") + .withGroupMembership(firstGroup) + .build(); + + EPerson secondUser = EPersonBuilder.createEPerson(context) + .withEmail("user2@mail.it") + .withGroupMembership(secondGroup) + .build(); + + EPerson thirdUser = EPersonBuilder.createEPerson(context) + .withEmail("user3@mail.it") + .withGroupMembership(thirdGroup) + .build(); + + EPerson fourthUser = EPersonBuilder.createEPerson(context) + .withEmail("user4@mail.it") + .withGroupMembership(thirdGroup) + .build(); + + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Test item") + .withDspaceObjectOwner("Owner", owner.getID().toString()) + .build(); + + Item itemNotAccessible = ItemBuilder.createItem(context, collection) + .withTitle("Test item not accessible") + .withDspaceObjectOwner("Owner", owner.getID().toString()) + .build(); + + context.restoreAuthSystemState(); + + AccessItemMode accessMode = buildAccessItemMode(CrisSecurity.GROUP); + when(accessMode.getGroups()).thenReturn(List.of("Group 1", thirdGroup.getID().toString())); + // filter valid only on first item + when(accessMode.getAdditionalFilter()).thenReturn(new Filter() { + @Override + public Boolean getResult(Context context, Item item) throws LogicalStatementException { + return item.getName().equals("Test item"); + } + + @Override + public String getName() { + return null; + } + + @Override + public void setBeanName(String s) {} + }); + + assertThat(crisSecurityService.hasAccess(context, item, eperson, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, admin, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, owner, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, collectionAdmin, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, communityAdmin, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, submitter, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, anotherSubmitter, accessMode), is(false)); + + assertThat(crisSecurityService.hasAccess(context, item, firstUser, accessMode), is(true)); + assertThat(crisSecurityService.hasAccess(context, item, secondUser, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, thirdUser, accessMode), is(true)); + assertThat(crisSecurityService.hasAccess(context, item, fourthUser, accessMode), is(true)); + + assertThat(crisSecurityService.hasAccess(context, itemNotAccessible, firstUser, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, itemNotAccessible, secondUser, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, itemNotAccessible, thirdUser, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, itemNotAccessible, fourthUser, accessMode), is(false)); + } + private AccessItemMode buildAccessItemMode(CrisSecurity... securities) { AccessItemMode mode = mock(AccessItemMode.class); when(mode.getSecurities()).thenReturn(List.of(securities)); From 144657812feb65c1d21c31b53f30924198854b9b Mon Sep 17 00:00:00 2001 From: Alexander K Date: Thu, 27 Jul 2023 16:00:09 +0200 Subject: [PATCH 11/81] [DSC-1161] Fix metadata validator for bind related metadata --- .../dspace/validation/MetadataValidator.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java index 3d50ddf66cd8..b6852596498b 100644 --- a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java +++ b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java @@ -64,15 +64,16 @@ public class MetadataValidator implements SubmissionStepValidator { private String name; @Override - public List validate(Context context, InProgressSubmission obj, SubmissionStepConfig config) { + public List validate(Context context, InProgressSubmission obj, SubmissionStepConfig stepConfig) { List errors = new ArrayList<>(); - DCInputSet inputConfig = getDCInputSet(config); + DCInputSet inputConfig = getDCInputSet(stepConfig); String documentTypeValue = getDocumentTypeValue(obj); + String documentTypeAuthority = getDocumentTypeAuthority(obj); // Get list of all field names (including qualdrop names) allowed for this dc.type - List allowedFieldNames = inputConfig.populateAllowedFieldNames(documentTypeValue); + List allowedFieldNames = inputConfig.populateAllowedFieldNames(documentTypeAuthority); for (DCInput[] row : inputConfig.getFields()) { for (DCInput input : row) { @@ -93,11 +94,11 @@ public List validate(Context context, InProgressSubmission o // Check the lookup list. If no other inputs of the same field name allow this type, // then remove. This includes field name without qualifier. - if (!input.isAllowedFor(documentTypeValue) && (!allowedFieldNames.contains(fullFieldname) + if (!input.isAllowedFor(documentTypeAuthority) && (!allowedFieldNames.contains(fullFieldname) && !allowedFieldNames.contains(input.getFieldName()))) { removeMetadataValues(context, obj.getItem(), mdv); } else { - validateMetadataValues(obj.getCollection(), mdv, input, config, isAuthorityControlled, + validateMetadataValues(obj.getCollection(), mdv, input, stepConfig, isAuthorityControlled, fieldKey, errors); if (mdv.size() > 0 && input.isVisible(DCInput.SUBMISSION_SCOPE)) { foundResult = true; @@ -107,7 +108,7 @@ public List validate(Context context, InProgressSubmission o if (input.isRequired() && ! foundResult) { // for this required qualdrop no value was found, add to the list of error fields addError(errors, ERROR_VALIDATION_REQUIRED, - "/" + OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + + "/" + OPERATION_PATH_SECTIONS + "/" + stepConfig.getId() + "/" + input.getFieldName()); } @@ -118,7 +119,7 @@ public List validate(Context context, InProgressSubmission o for (String fieldName : fieldsName) { boolean valuesRemoved = false; List mdv = itemService.getMetadataByMetadataString(obj.getItem(), fieldName); - if (!input.isAllowedFor(documentTypeValue)) { + if (!input.isAllowedFor(documentTypeAuthority)) { // Check the lookup list. If no other inputs of the same field name allow this type, // then remove. Otherwise, do not if (!(allowedFieldNames.contains(fieldName))) { @@ -133,17 +134,17 @@ public List validate(Context context, InProgressSubmission o "name"); } } - validateMetadataValues(obj.getCollection(), mdv, input, config, + validateMetadataValues(obj.getCollection(), mdv, input, stepConfig, isAuthorityControlled, fieldKey, errors); if ((input.isRequired() && mdv.size() == 0) && input.isVisible(DCInput.SUBMISSION_SCOPE) && !valuesRemoved) { // Is the input required for *this* type? In other words, are we looking at a required // input that is also allowed for this document type - if (input.isAllowedFor(documentTypeValue)) { + if (input.isAllowedFor(documentTypeAuthority)) { // since this field is missing add to list of error // fields addError(errors, ERROR_VALIDATION_REQUIRED, - "/" + OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + + "/" + OPERATION_PATH_SECTIONS + "/" + stepConfig.getId() + "/" + input.getFieldName()); } } @@ -159,6 +160,12 @@ private String getDocumentTypeValue(InProgressSubmission obj) { return documentType.size() > 0 ? documentType.get(0).getValue() : ""; } + private String getDocumentTypeAuthority(InProgressSubmission obj) { + String documentTypeField = configurationService.getProperty("submit.type-bind.field", "dc.type"); + List documentType = itemService.getMetadataByMetadataString(obj.getItem(), documentTypeField); + return documentType.size() > 0 ? documentType.get(0).getAuthority() : ""; + } + private DCInputSet getDCInputSet(SubmissionStepConfig config) { try { return getInputReader().getInputsByFormName(config.getId()); From 64e2610a490428897be7b34f7c39ecfefd903749 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Mon, 31 Jul 2023 13:57:50 +0200 Subject: [PATCH 12/81] [DSC-1161] avoid longer variable name --- .../org/dspace/validation/MetadataValidator.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java index b6852596498b..9030811352fe 100644 --- a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java +++ b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java @@ -64,11 +64,11 @@ public class MetadataValidator implements SubmissionStepValidator { private String name; @Override - public List validate(Context context, InProgressSubmission obj, SubmissionStepConfig stepConfig) { + public List validate(Context context, InProgressSubmission obj, SubmissionStepConfig config) { List errors = new ArrayList<>(); - DCInputSet inputConfig = getDCInputSet(stepConfig); + DCInputSet inputConfig = getDCInputSet(config); String documentTypeValue = getDocumentTypeValue(obj); String documentTypeAuthority = getDocumentTypeAuthority(obj); @@ -98,7 +98,7 @@ public List validate(Context context, InProgressSubmission o && !allowedFieldNames.contains(input.getFieldName()))) { removeMetadataValues(context, obj.getItem(), mdv); } else { - validateMetadataValues(obj.getCollection(), mdv, input, stepConfig, isAuthorityControlled, + validateMetadataValues(obj.getCollection(), mdv, input, config, isAuthorityControlled, fieldKey, errors); if (mdv.size() > 0 && input.isVisible(DCInput.SUBMISSION_SCOPE)) { foundResult = true; @@ -108,7 +108,7 @@ public List validate(Context context, InProgressSubmission o if (input.isRequired() && ! foundResult) { // for this required qualdrop no value was found, add to the list of error fields addError(errors, ERROR_VALIDATION_REQUIRED, - "/" + OPERATION_PATH_SECTIONS + "/" + stepConfig.getId() + "/" + + "/" + OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + input.getFieldName()); } @@ -134,7 +134,7 @@ public List validate(Context context, InProgressSubmission o "name"); } } - validateMetadataValues(obj.getCollection(), mdv, input, stepConfig, + validateMetadataValues(obj.getCollection(), mdv, input, config, isAuthorityControlled, fieldKey, errors); if ((input.isRequired() && mdv.size() == 0) && input.isVisible(DCInput.SUBMISSION_SCOPE) && !valuesRemoved) { @@ -144,7 +144,7 @@ public List validate(Context context, InProgressSubmission o // since this field is missing add to list of error // fields addError(errors, ERROR_VALIDATION_REQUIRED, - "/" + OPERATION_PATH_SECTIONS + "/" + stepConfig.getId() + "/" + + "/" + OPERATION_PATH_SECTIONS + "/" + config.getId() + "/" + input.getFieldName()); } } From b14d87f62e4ef2a0e59d0b1af0cd522bbb86bc7e Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 23 Aug 2023 15:08:26 +0200 Subject: [PATCH 13/81] [DSC-1125] Upgrade namespace --- .../main/java/org/dspace/identifier/doi/DataCiteConnector.java | 2 +- dspace/config/dspace.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java index bd5791481e90..43882918cd4a 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java @@ -802,7 +802,7 @@ protected Element addDOI(String doi, Element root) { } Element identifier = new Element("identifier", configurationService.getProperty(CFG_NAMESPACE, - "http://datacite.org/schema/kernel-3")); + "http://datacite.org/schema/kernel-4")); identifier.setAttribute("identifierType", "DOI"); identifier.addContent(doi.substring(DOI.SCHEME.length())); return root.addContent(0, identifier); diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 2fa3578cd90e..7db34a16b978 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -621,7 +621,7 @@ crosswalk.dissemination.DataCite.preferList = false crosswalk.dissemination.DataCite.publisher = My University #crosswalk.dissemination.DataCite.dataManager = # defaults to publisher #crosswalk.dissemination.DataCite.hostingInstitution = # defaults to publisher -crosswalk.dissemination.DataCite.namespace = http://datacite.org/schema/kernel-3 +crosswalk.dissemination.DataCite.namespace = http://datacite.org/schema/kernel-4 # Crosswalk Plugin Configuration: # The purpose of Crosswalks is to translate an external metadata format to/from From 5d99682ad1ce19f9960baa4a14eae709adc1b308 Mon Sep 17 00:00:00 2001 From: Alexander K Date: Fri, 25 Aug 2023 10:12:23 +0200 Subject: [PATCH 14/81] [DSC-1161] fix MetadataValidator --- .../dspace/validation/MetadataValidator.java | 22 +++++++------- .../app/rest/submit/step/DescribeStep.java | 30 ++++++++++++++----- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java index b6852596498b..de46f5c2c789 100644 --- a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java +++ b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java @@ -22,10 +22,7 @@ import org.dspace.app.util.DCInputsReader; import org.dspace.app.util.DCInputsReaderException; import org.dspace.app.util.SubmissionStepConfig; -import org.dspace.content.Collection; -import org.dspace.content.InProgressSubmission; -import org.dspace.content.Item; -import org.dspace.content.MetadataValue; +import org.dspace.content.*; import org.dspace.content.authority.service.MetadataAuthorityService; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; @@ -69,11 +66,12 @@ public List validate(Context context, InProgressSubmission o List errors = new ArrayList<>(); DCInputSet inputConfig = getDCInputSet(stepConfig); - String documentTypeValue = getDocumentTypeValue(obj); - String documentTypeAuthority = getDocumentTypeAuthority(obj); + String documentTypeField = configurationService.getProperty("submit.type-bind.field", "dc.type"); + boolean isAuthority = metadataAuthorityService.isAuthorityAllowed(documentTypeField.replace(".","_"),Constants.ITEM,obj.getCollection()); + String documentType = isAuthority ? getDocumentTypeAuthority(obj) : getDocumentTypeValue(obj); // Get list of all field names (including qualdrop names) allowed for this dc.type - List allowedFieldNames = inputConfig.populateAllowedFieldNames(documentTypeAuthority); + List allowedFieldNames = inputConfig.populateAllowedFieldNames(documentType); for (DCInput[] row : inputConfig.getFields()) { for (DCInput input : row) { @@ -94,7 +92,7 @@ public List validate(Context context, InProgressSubmission o // Check the lookup list. If no other inputs of the same field name allow this type, // then remove. This includes field name without qualifier. - if (!input.isAllowedFor(documentTypeAuthority) && (!allowedFieldNames.contains(fullFieldname) + if (!input.isAllowedFor(documentType) && (!allowedFieldNames.contains(fullFieldname) && !allowedFieldNames.contains(input.getFieldName()))) { removeMetadataValues(context, obj.getItem(), mdv); } else { @@ -119,18 +117,18 @@ public List validate(Context context, InProgressSubmission o for (String fieldName : fieldsName) { boolean valuesRemoved = false; List mdv = itemService.getMetadataByMetadataString(obj.getItem(), fieldName); - if (!input.isAllowedFor(documentTypeAuthority)) { + if (!input.isAllowedFor(documentType)) { // Check the lookup list. If no other inputs of the same field name allow this type, // then remove. Otherwise, do not if (!(allowedFieldNames.contains(fieldName))) { removeMetadataValues(context, obj.getItem(), mdv); valuesRemoved = true; log.debug("Stripping metadata values for " + input.getFieldName() + " on type " - + documentTypeValue + " as it is allowed by another input of the same field " + + + documentType + " as it is allowed by another input of the same field " + "name"); } else { log.debug("Not removing unallowed metadata values for " + input.getFieldName() + " on type " - + documentTypeValue + " as it is allowed by another input of the same field " + + + documentType + " as it is allowed by another input of the same field " + "name"); } } @@ -140,7 +138,7 @@ public List validate(Context context, InProgressSubmission o && !valuesRemoved) { // Is the input required for *this* type? In other words, are we looking at a required // input that is also allowed for this document type - if (input.isAllowedFor(documentTypeAuthority)) { + if (input.isAllowedFor(documentType)) { // since this field is missing add to list of error // fields addError(errors, ERROR_VALIDATION_REQUIRED, diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java index fa2dc320b87b..01a895961963 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java @@ -31,6 +31,8 @@ import org.dspace.app.util.SubmissionStepConfig; import org.dspace.content.InProgressSubmission; import org.dspace.content.MetadataValue; +import org.dspace.content.authority.service.MetadataAuthorityService; +import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.Utils; import org.dspace.services.ConfigurationService; @@ -53,7 +55,10 @@ public class DescribeStep extends AbstractProcessingStep { private final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - public DescribeStep() throws DCInputsReaderException { + private MetadataAuthorityService metadataAuthorityService; + + public DescribeStep(MetadataAuthorityService metadataAuthorityService) throws DCInputsReaderException { + this.metadataAuthorityService = metadataAuthorityService; inputReader = DCInputsReaderFactory.getDCInputsReader(); } @@ -72,15 +77,13 @@ public DataDescribe getData(SubmissionService submissionService, InProgressSubmi private void readField(InProgressSubmission obj, SubmissionStepConfig config, DataDescribe data, DCInputSet inputConfig) throws DCInputsReaderException { - String documentTypeValue = ""; - List documentType = itemService.getMetadataByMetadataString(obj.getItem(), - configurationService.getProperty("submit.type-bind.field", "dc.type")); - if (documentType.size() > 0) { - documentTypeValue = documentType.get(0).getValue(); - } + + String documentTypeField = configurationService.getProperty("submit.type-bind.field", "dc.type"); + boolean isAuthority = metadataAuthorityService.isAuthorityAllowed(documentTypeField.replace(".","_"), Constants.ITEM,obj.getCollection()); + String documentType = isAuthority ? getDocumentTypeAuthority(obj) : getDocumentTypeValue(obj); // Get list of all field names (including qualdrop names) allowed for this dc.type - List allowedFieldNames = inputConfig.populateAllowedFieldNames(documentTypeValue); + List allowedFieldNames = inputConfig.populateAllowedFieldNames(documentType); // Loop input rows and process submitted metadata for (DCInput[] row : inputConfig.getFields()) { @@ -206,5 +209,16 @@ private List getInputFieldsName(DCInputSet inputConfig, String configId) } return fieldsName; } + private String getDocumentTypeValue(InProgressSubmission obj) { + String documentTypeField = configurationService.getProperty("submit.type-bind.field", "dc.type"); + List documentType = itemService.getMetadataByMetadataString(obj.getItem(), documentTypeField); + return documentType.size() > 0 ? documentType.get(0).getValue() : ""; + } + + private String getDocumentTypeAuthority(InProgressSubmission obj) { + String documentTypeField = configurationService.getProperty("submit.type-bind.field", "dc.type"); + List documentType = itemService.getMetadataByMetadataString(obj.getItem(), documentTypeField); + return documentType.size() > 0 ? documentType.get(0).getAuthority() : ""; + } } From 9850542e62aefc475eed39657c019c624e062297 Mon Sep 17 00:00:00 2001 From: Alexander K Date: Mon, 28 Aug 2023 18:20:58 +0200 Subject: [PATCH 15/81] [DSC-1161] hot fix MetadataAuthorityService inject --- .../org/dspace/app/rest/submit/step/DescribeStep.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java index 01a895961963..559de5e83258 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/DescribeStep.java @@ -31,6 +31,7 @@ import org.dspace.app.util.SubmissionStepConfig; import org.dspace.content.InProgressSubmission; import org.dspace.content.MetadataValue; +import org.dspace.content.authority.factory.ContentAuthorityServiceFactory; import org.dspace.content.authority.service.MetadataAuthorityService; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -55,10 +56,11 @@ public class DescribeStep extends AbstractProcessingStep { private final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - private MetadataAuthorityService metadataAuthorityService; + private final MetadataAuthorityService metadataAuthorityService = ContentAuthorityServiceFactory + .getInstance() + .getMetadataAuthorityService(); - public DescribeStep(MetadataAuthorityService metadataAuthorityService) throws DCInputsReaderException { - this.metadataAuthorityService = metadataAuthorityService; + public DescribeStep() throws DCInputsReaderException { inputReader = DCInputsReaderFactory.getDCInputsReader(); } From b2262050a38d4dd943f541f6e93a3c2c96b8c496 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 30 Aug 2023 18:24:23 +0200 Subject: [PATCH 16/81] [DSC-1224] Export script that generates registry-file for schema --- .../DspaceExportMetadataSchemaException.java | 23 ++ .../export/MetadataSchemaExportCliScript.java | 51 +++++ ...ataSchemaExportCliScriptConfiguration.java | 34 +++ .../export/MetadataSchemaExportScript.java | 119 ++++++++++ ...tadataSchemaExportScriptConfiguration.java | 73 ++++++ .../export/model/AbstractJaxbBuilder.java | 54 +++++ .../app/metadata/export/model/DcSchema.java | 80 +++++++ .../export/model/DcSchemaBuilder.java | 39 ++++ .../app/metadata/export/model/DcType.java | 86 +++++++ .../metadata/export/model/DcTypeBuilder.java | 49 ++++ .../metadata/export/model/DspaceDcTypes.java | 82 +++++++ .../export/model/DspaceDcTypesBuilder.java | 59 +++++ .../metadata/export/model/DspaceHeader.java | 92 ++++++++ .../export/model/DspaceHeaderBuilder.java | 59 +++++ .../metadata/export/model/ObjectFactory.java | 212 ++++++++++++++++++ .../service/MetadataExportServiceFactory.java | 28 +++ .../MetadataExportServiceFactoryImpl.java | 31 +++ .../service/MetadataFieldExportService.java | 35 +++ .../MetadataFieldExportServiceImpl.java | 49 ++++ .../service/MetadataSchemaExportService.java | 68 ++++++ .../MetadataSchemaExportServiceImpl.java | 107 +++++++++ .../config/spring/api/scripts.xml | 5 + .../export/MetadataSchemaExportScriptIT.java | 145 ++++++++++++ dspace/config/registries/dspace-dc-types.xsd | 59 +++++ .../spring/api/metadata-schema-export.xml | 17 ++ dspace/config/spring/api/scripts.xml | 5 + dspace/config/spring/rest/scripts.xml | 5 + 27 files changed, 1666 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/DspaceExportMetadataSchemaException.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScript.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScriptConfiguration.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptConfiguration.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchema.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchemaBuilder.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcTypeBuilder.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypes.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypesBuilder.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceHeader.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceHeaderBuilder.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/model/ObjectFactory.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactory.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactoryImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportService.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportService.java create mode 100644 dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportServiceImpl.java create mode 100644 dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java create mode 100644 dspace/config/registries/dspace-dc-types.xsd create mode 100644 dspace/config/spring/api/metadata-schema-export.xml diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/DspaceExportMetadataSchemaException.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/DspaceExportMetadataSchemaException.java new file mode 100644 index 000000000000..1f2cbd824a80 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/DspaceExportMetadataSchemaException.java @@ -0,0 +1,23 @@ +/** + * 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.app.metadata.export; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class DspaceExportMetadataSchemaException extends Exception { + + public DspaceExportMetadataSchemaException(Exception e) { + super(e); + } + + public DspaceExportMetadataSchemaException(String message, Exception e) { + super(message, e); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScript.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScript.java new file mode 100644 index 000000000000..83b8e94330ba --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScript.java @@ -0,0 +1,51 @@ +/** + * 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.app.metadata.export; + +import java.io.File; + +import org.apache.commons.cli.ParseException; +import org.dspace.core.Context; + +/** + * This script can be use to export a given {@code MetadataSchema} into its + * registry file, that respects the standard DTD / XSD DSpace xml registry. + *

+ * This script is supposed to work with the CLI (command-line-interface), + * it accepts only two parameters {@code -i -f } + * respectively representing: + *

    + *
  • {@code schema-id}: id of the schema to export
  • + *
  • {@code file-path}:full file path of the file that will contain the export
  • + *
      + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + * + */ +public class MetadataSchemaExportCliScript extends MetadataSchemaExportScript { + + protected String filename; + + @Override + public void setup() throws ParseException { + super.setup(); + filename = commandLine.getOptionValue('f'); + } + + @Override + protected File getExportedFile(Context context) throws DspaceExportMetadataSchemaException { + try { + File file = new File(filename); + return metadataSchemaExportService.exportMetadataSchemaToFile(context, metadataSchema, file); + } catch (DspaceExportMetadataSchemaException e) { + handler.logError("Problem occured while exporting the schema to file: " + filename, e); + throw e; + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScriptConfiguration.java new file mode 100644 index 000000000000..5adfa2a725fc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScriptConfiguration.java @@ -0,0 +1,34 @@ +/** + * 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.app.metadata.export; + +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + * + */ +public class MetadataSchemaExportCliScriptConfiguration + extends MetadataSchemaExportScriptConfiguration { + + @Override + public Options getOptions() { + Options options = super.getOptions(); + + options.addOption( + Option.builder("f").longOpt("file") + .desc("The temporary file-name to use") + .hasArg() + .build() + ); + + return options; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java new file mode 100644 index 000000000000..59b3ddfda1a1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java @@ -0,0 +1,119 @@ +/** + * 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.app.metadata.export; + +import java.io.File; +import java.io.FileInputStream; +import java.sql.SQLException; + +import org.apache.commons.cli.ParseException; +import org.dspace.app.metadata.export.service.MetadataExportServiceFactory; +import org.dspace.app.metadata.export.service.MetadataSchemaExportService; +import org.dspace.content.MetadataSchema; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.MetadataSchemaService; +import org.dspace.core.Context; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * This script can be use to export a given {@code MetadataSchema} into its + * registry file, that respects the standard DTD / XSD DSpace xml registry. + *

      + * This script is supposed to work with the webapp, it accepts only one + * parameter {@code -i } representing the id of the schema that + * will be exported. + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class MetadataSchemaExportScript + extends DSpaceRunnable> { + + protected MetadataSchemaService metadataSchemaService = + ContentServiceFactory.getInstance().getMetadataSchemaService(); + + protected MetadataSchemaExportService metadataSchemaExportService = + MetadataExportServiceFactory.getInstance().getMetadataSchemaExportService(); + + protected boolean help; + protected int id; + + protected MetadataSchema metadataSchema; + + @Override + public MetadataSchemaExportScriptConfiguration getScriptConfiguration() { + return DSpaceServicesFactory + .getInstance().getServiceManager() + .getServiceByName("export-schema", MetadataSchemaExportScriptConfiguration.class); + } + + @Override + public void setup() throws ParseException { + help = commandLine.hasOption('h'); + try { + id = Integer.parseInt(commandLine.getOptionValue('i')); + } catch (Exception e) { + handler.logError("Cannot parse the id argument ( " + id + " )! You should provide an integer!"); + throw new ParseException("Cannot parse the id argument ( " + id + " )! You should provide an integer!"); + } + } + + @Override + public void internalRun() throws Exception { + if (help) { + printHelp(); + return; + } + + Context context = new Context(); + try { + validate(context); + exportMetadataSchema(context); + } catch (Exception e) { + context.abort(); + throw e; + } + } + + private void validate(Context context) throws SQLException, ParseException { + metadataSchema = this.metadataSchemaService.find(context, id); + if (metadataSchema == null) { + handler.logError("Cannot find the metadata-schema with id: " + id); + throw new ParseException("Cannot find the metadata-schema with id: " + id); + } + } + + private void exportMetadataSchema(Context context) throws Exception { + handler.logInfo( + "Exporting the metadata-schema file for the schema " + metadataSchema.getName() + ); + try { + File tempFile = getExportedFile(context); + + handler.logInfo( + "Exported to file: " + tempFile.getAbsolutePath() + ); + + try (FileInputStream fis = new FileInputStream(tempFile)) { + handler.logInfo("Summarizing export ..."); + context.turnOffAuthorisationSystem(); + handler.writeFilestream( + context, metadataSchema.getName(), fis, "application/xml", false + ); + context.restoreAuthSystemState(); + } + } catch (Exception e) { + handler.logError("Problem occured while exporting the schema!", e); + throw e; + } + } + + protected File getExportedFile(Context context) throws DspaceExportMetadataSchemaException { + return this.metadataSchemaExportService.exportMetadataSchemaToFile(context, metadataSchema); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptConfiguration.java new file mode 100644 index 000000000000..665dbe15567c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptConfiguration.java @@ -0,0 +1,73 @@ +/** + * 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.app.metadata.export; + +import java.sql.SQLException; + +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.dspace.scripts.configuration.ScriptConfiguration; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Configuration of the Script {@code MetadataSchemaExportScript} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class MetadataSchemaExportScriptConfiguration + extends ScriptConfiguration { + + @Autowired + private AuthorizeService authorizeService; + + private Class dspaceRunnableClass; + + @Override + public Class getDspaceRunnableClass() { + return this.dspaceRunnableClass; + } + + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + + @Override + public boolean isAllowedToExecute(Context context) { + try { + return authorizeService.isAdmin(context); + } catch (SQLException e) { + throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); + } + } + + @Override + public Options getOptions() { + Options options = new Options(); + + options.addOption( + Option.builder("i").longOpt("id") + .desc("Metadata schema id") + .hasArg() + .required() + .build() + ); + + options.addOption( + Option.builder("h").longOpt("help") + .desc("help") + .hasArg(false) + .required(false) + .build() + ); + + return options; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java new file mode 100644 index 000000000000..038d8552e1e2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java @@ -0,0 +1,54 @@ +/** + * 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.app.metadata.export.model; + +import java.lang.reflect.InvocationTargetException; +import java.util.function.Function; +import javax.xml.bind.JAXBElement; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public abstract class AbstractJaxbBuilder { + + T object; + Class clazz; + + protected final ObjectFactory objectFactory = new ObjectFactory(); + + protected AbstractJaxbBuilder(Class clazz) { + this.clazz = clazz; + } + + protected T getObejct() { + if (object == null) { + try { + object = clazz.getDeclaredConstructor().newInstance(); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + return object; + } + + public T build() { + return object; + } + + protected void addChildElement(C value, Function> mapper) { + addChildElement(mapper.apply(value)); + } + + protected abstract void addChildElement(JAXBElement v); +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchema.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchema.java new file mode 100644 index 000000000000..e0ad541bdb84 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchema.java @@ -0,0 +1,80 @@ +/** + * 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.app.metadata.export.model; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlElementRefs; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + + +/** + *

      Classe Java per anonymous complex type. + * + *

      Il seguente frammento di schema specifica il contenuto previsto contenuto in questa classe. + * + *

      + * <complexType>
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <choice maxOccurs="unbounded" minOccurs="0">
      + *         <element ref="{}name"/>
      + *         <element ref="{}namespace"/>
      + *       </choice>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "nameOrNamespace" +}) +@XmlRootElement(name = "dc-schema") +public class DcSchema { + + @XmlElementRefs({ + @XmlElementRef(name = "name", type = JAXBElement.class, required = false), + @XmlElementRef(name = "namespace", type = JAXBElement.class, required = false) + }) + protected List> nameOrNamespace; + + /** + * Gets the value of the nameOrNamespace property. + * + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the nameOrNamespace property. + * + *

      + * For example, to add a new item, do as follows: + *

      +     *    getNameOrNamespace().add(newItem);
      +     * 
      + * + * + *

      + * Objects of the following type(s) are allowed in the list + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + */ + public List> getNameOrNamespace() { + if (nameOrNamespace == null) { + nameOrNamespace = new ArrayList>(); + } + return this.nameOrNamespace; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchemaBuilder.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchemaBuilder.java new file mode 100644 index 000000000000..fe7144bda854 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchemaBuilder.java @@ -0,0 +1,39 @@ +/** + * 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.app.metadata.export.model; + +import javax.xml.bind.JAXBElement; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class DcSchemaBuilder extends AbstractJaxbBuilder { + + protected DcSchemaBuilder() { + super(DcSchema.class); + } + + public static DcSchemaBuilder createBuilder() { + return new DcSchemaBuilder(); + } + + public DcSchemaBuilder withName(String name) { + this.addChildElement(name, objectFactory::createName); + return this; + } + + public DcSchemaBuilder withNamespace(String namespace) { + this.addChildElement(namespace, objectFactory::createNamespace); + return this; + } + + @Override + protected void addChildElement(JAXBElement v) { + getObejct().getNameOrNamespace().add(v); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java new file mode 100644 index 000000000000..dcf4d9821cd0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java @@ -0,0 +1,86 @@ +/** + * 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.app.metadata.export.model; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlElementRefs; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + + +/** + *

      Classe Java per anonymous complex type. + * + *

      Il seguente frammento di schema specifica il contenuto previsto contenuto in questa classe. + * + *

      + * <complexType>
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <choice maxOccurs="unbounded" minOccurs="0">
      + *         <element ref="{}schema"/>
      + *         <element ref="{}element"/>
      + *         <element ref="{}qualifier"/>
      + *         <element ref="{}scope_note"/>
      + *       </choice>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "schemaOrElementOrQualifier" +}) +@XmlRootElement(name = "dc-type") +public class DcType { + + @XmlElementRefs({ + @XmlElementRef(name = "schema", type = JAXBElement.class), + @XmlElementRef(name = "element", type = JAXBElement.class), + @XmlElementRef(name = "qualifier", type = JAXBElement.class), + @XmlElementRef(name = "scope_note", type = JAXBElement.class) + }) + protected List> schemaOrElementOrQualifier; + + /** + * Gets the value of the schemaOrElementOrQualifier property. + * + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the schemaOrElementOrQualifier property. + * + *

      + * For example, to add a new item, do as follows: + *

      +     *    getSchemaOrElementOrQualifier().add(newItem);
      +     * 
      + * + * + *

      + * Objects of the following type(s) are allowed in the list + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + */ + public List> getSchemaOrElementOrQualifier() { + if (schemaOrElementOrQualifier == null) { + schemaOrElementOrQualifier = new ArrayList>(); + } + return this.schemaOrElementOrQualifier; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcTypeBuilder.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcTypeBuilder.java new file mode 100644 index 000000000000..47fd64763ead --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcTypeBuilder.java @@ -0,0 +1,49 @@ +/** + * 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.app.metadata.export.model; + +import javax.xml.bind.JAXBElement; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class DcTypeBuilder extends AbstractJaxbBuilder { + + protected DcTypeBuilder() { + super(DcType.class); + } + + public static DcTypeBuilder createBuilder() { + return new DcTypeBuilder(); + } + + public DcTypeBuilder withSchema(String schema) { + addChildElement(schema, objectFactory::createSchema); + return this; + } + + public DcTypeBuilder withElement(String element) { + addChildElement(element, objectFactory::createElement); + return this; + } + + public DcTypeBuilder withQualifier(String qualifier) { + addChildElement(qualifier, objectFactory::createQualifier); + return this; + } + + public DcTypeBuilder withScopeNote(String scopeNote) { + addChildElement(scopeNote, objectFactory::createScopeNote); + return this; + } + + @Override + protected void addChildElement(JAXBElement v) { + getObejct().getSchemaOrElementOrQualifier().add(v); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypes.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypes.java new file mode 100644 index 000000000000..4cba081a8a30 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypes.java @@ -0,0 +1,82 @@ +/** + * 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.app.metadata.export.model; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElements; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + + +/** + *

      Classe Java per anonymous complex type. + * + *

      Il seguente frammento di schema specifica il contenuto previsto contenuto in questa classe. + * + *

      + * <complexType>
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <choice maxOccurs="unbounded" minOccurs="0">
      + *         <element ref="{}dspace-header"/>
      + *         <element ref="{}dc-schema"/>
      + *         <element ref="{}dc-type"/>
      + *       </choice>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "dspaceHeaderOrDcSchemaOrDcType" +}) +@XmlRootElement(name = "dspace-dc-types") +public class DspaceDcTypes { + + @XmlElements({ + @XmlElement(name = "dspace-header", type = DspaceHeader.class), + @XmlElement(name = "dc-schema", type = DcSchema.class), + @XmlElement(name = "dc-type", type = DcType.class) + }) + protected List dspaceHeaderOrDcSchemaOrDcType; + + /** + * Gets the value of the dspaceHeaderOrDcSchemaOrDcType property. + * + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the dspaceHeaderOrDcSchemaOrDcType property. + * + *

      + * For example, to add a new item, do as follows: + *

      +     *    getDspaceHeaderOrDcSchemaOrDcType().add(newItem);
      +     * 
      + * + * + *

      + * Objects of the following type(s) are allowed in the list + * {@link DspaceHeader } + * {@link DcSchema } + * {@link DcType } + */ + public List getDspaceHeaderOrDcSchemaOrDcType() { + if (dspaceHeaderOrDcSchemaOrDcType == null) { + dspaceHeaderOrDcSchemaOrDcType = new ArrayList(); + } + return this.dspaceHeaderOrDcSchemaOrDcType; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypesBuilder.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypesBuilder.java new file mode 100644 index 000000000000..1e4cdb83393c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypesBuilder.java @@ -0,0 +1,59 @@ +/** + * 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.app.metadata.export.model; + +import java.util.Collection; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class DspaceDcTypesBuilder { + + private DspaceDcTypes dcTypes; + + private final ObjectFactory objectFactory = new ObjectFactory(); + + private DspaceDcTypes getDcTypes() { + if (dcTypes == null) { + dcTypes = new DspaceDcTypes(); + } + return dcTypes; + } + + private DspaceDcTypesBuilder() { + } + + public static DspaceDcTypesBuilder createBuilder() { + return new DspaceDcTypesBuilder(); + } + + public DspaceDcTypesBuilder witheader(DspaceHeader header) { + this.getDcTypes().getDspaceHeaderOrDcSchemaOrDcType().add(header); + return this; + } + + public DspaceDcTypesBuilder withSchema(DcSchema schema) { + this.getDcTypes().getDspaceHeaderOrDcSchemaOrDcType().add(schema); + return this; + } + + public DspaceDcTypesBuilder withDcType(DcType dcType) { + this.getDcTypes().getDspaceHeaderOrDcSchemaOrDcType().add(dcType); + return this; + } + + public DspaceDcTypesBuilder withDcTypes(Collection dcTypes) { + this.getDcTypes().getDspaceHeaderOrDcSchemaOrDcType().addAll(dcTypes); + return this; + } + + public DspaceDcTypes build() { + return dcTypes; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceHeader.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceHeader.java new file mode 100644 index 000000000000..151c8b28292d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceHeader.java @@ -0,0 +1,92 @@ +/** + * 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.app.metadata.export.model; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlElementRefs; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + + +/** + *

      Classe Java per anonymous complex type. + * + *

      Il seguente frammento di schema specifica il contenuto previsto contenuto in questa classe. + * + *

      + * <complexType>
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <choice maxOccurs="unbounded" minOccurs="0">
      + *         <element ref="{}title"/>
      + *         <element ref="{}contributor.author"/>
      + *         <element ref="{}contributor.editor"/>
      + *         <element ref="{}date.created"/>
      + *         <element ref="{}description"/>
      + *         <element ref="{}description.version"/>
      + *       </choice>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "titleOrContributorAuthorOrContributorEditor" +}) +@XmlRootElement(name = "dspace-header") +public class DspaceHeader { + + @XmlElementRefs({ + @XmlElementRef(name = "title", type = JAXBElement.class, required = false), + @XmlElementRef(name = "contributor.author", type = JAXBElement.class, required = false), + @XmlElementRef(name = "contributor.editor", type = JAXBElement.class, required = false), + @XmlElementRef(name = "date.created", type = JAXBElement.class, required = false), + @XmlElementRef(name = "description", type = JAXBElement.class, required = false), + @XmlElementRef(name = "description.version", type = JAXBElement.class, required = false) + }) + protected List> titleOrContributorAuthorOrContributorEditor; + + /** + * Gets the value of the titleOrContributorAuthorOrContributorEditor property. + * + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the titleOrContributorAuthorOrContributorEditor property. + * + *

      + * For example, to add a new item, do as follows: + *

      +     *    getTitleOrContributorAuthorOrContributorEditor().add(newItem);
      +     * 
      + * + * + *

      + * Objects of the following type(s) are allowed in the list + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + */ + public List> getTitleOrContributorAuthorOrContributorEditor() { + if (titleOrContributorAuthorOrContributorEditor == null) { + titleOrContributorAuthorOrContributorEditor = new ArrayList>(); + } + return this.titleOrContributorAuthorOrContributorEditor; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceHeaderBuilder.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceHeaderBuilder.java new file mode 100644 index 000000000000..fb4028a2057b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceHeaderBuilder.java @@ -0,0 +1,59 @@ +/** + * 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.app.metadata.export.model; + +import javax.xml.bind.JAXBElement; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class DspaceHeaderBuilder extends AbstractJaxbBuilder { + + protected DspaceHeaderBuilder() { + super(DspaceHeader.class); + } + + public static DspaceHeaderBuilder createBuilder() { + return new DspaceHeaderBuilder(); + } + + public DspaceHeaderBuilder withTitle(String title) { + addChildElement(title, objectFactory::createTitle); + return this; + } + + public DspaceHeaderBuilder withContributorAuthor(String contributorAuthor) { + addChildElement(contributorAuthor, objectFactory::createContributorAuthor); + return this; + } + + public DspaceHeaderBuilder withContributorEditor(String contributorEditor) { + addChildElement(contributorEditor, objectFactory::createContributorEditor); + return this; + } + + public DspaceHeaderBuilder withDateCreated(String dateCreated) { + addChildElement(dateCreated, objectFactory::createDateCreated); + return this; + } + + public DspaceHeaderBuilder withDescription(String description) { + addChildElement(description, objectFactory::createDescription); + return this; + } + + public DspaceHeaderBuilder withDescriptionVersion(String descriptionVersion) { + addChildElement(descriptionVersion, objectFactory::createDescriptionVersion); + return this; + } + + @Override + protected void addChildElement(JAXBElement v) { + getObejct().getTitleOrContributorAuthorOrContributorEditor().add(v); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/ObjectFactory.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/ObjectFactory.java new file mode 100644 index 000000000000..085e8af5f81b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/ObjectFactory.java @@ -0,0 +1,212 @@ +/** + * 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.app.metadata.export.model; + +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlElementDecl; +import javax.xml.bind.annotation.XmlRegistry; +import javax.xml.namespace.QName; + + +/** + * This object contains factory methods for each + * Java content interface and Java element interface + * generated in the org.dspace.app.metadata.export.model package. + *

      An ObjectFactory allows you to programatically + * construct new instances of the Java representation + * for XML content. The Java representation of XML + * content can consist of schema derived interfaces + * and classes representing the binding of schema + * type definitions, element declarations and model + * groups. Factory methods for each of these are + * provided in this class. + */ +@XmlRegistry +public class ObjectFactory { + + private final static QName _Title_QNAME = new QName("", "title"); + private final static QName _ContributorAuthor_QNAME = new QName("", "contributor.author"); + private final static QName _ContributorEditor_QNAME = new QName("", "contributor.editor"); + private final static QName _DateCreated_QNAME = new QName("", "date.created"); + private final static QName _Description_QNAME = new QName("", "description"); + private final static QName _DescriptionVersion_QNAME = new QName("", "description.version"); + private final static QName _Name_QNAME = new QName("", "name"); + private final static QName _Namespace_QNAME = new QName("", "namespace"); + private final static QName _Schema_QNAME = new QName("", "schema"); + private final static QName _Element_QNAME = new QName("", "element"); + private final static QName _Qualifier_QNAME = new QName("", "qualifier"); + private final static QName _ScopeNote_QNAME = new QName("", "scope_note"); + + /** + * Create a new ObjectFactory that can be used to create new instances of schema derived classes for package: org + * .dspace.app.metadata.export.model + */ + public ObjectFactory() { + } + + /** + * Create an instance of {@link DspaceDcTypes } + */ + public DspaceDcTypes createDspaceDcTypes() { + return new DspaceDcTypes(); + } + + /** + * Create an instance of {@link DspaceHeader } + */ + public DspaceHeader createDspaceHeader() { + return new DspaceHeader(); + } + + /** + * Create an instance of {@link DcSchema } + */ + public DcSchema createDcSchema() { + return new DcSchema(); + } + + /** + * Create an instance of {@link DcType } + */ + public DcType createDcType() { + return new DcType(); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "title") + public JAXBElement createTitle(String value) { + return new JAXBElement(_Title_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "contributor.author") + public JAXBElement createContributorAuthor(String value) { + return new JAXBElement(_ContributorAuthor_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "contributor.editor") + public JAXBElement createContributorEditor(String value) { + return new JAXBElement(_ContributorEditor_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "date.created") + public JAXBElement createDateCreated(String value) { + return new JAXBElement(_DateCreated_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "description") + public JAXBElement createDescription(String value) { + return new JAXBElement(_Description_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "description.version") + public JAXBElement createDescriptionVersion(String value) { + return new JAXBElement(_DescriptionVersion_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "name") + public JAXBElement createName(String value) { + return new JAXBElement(_Name_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "namespace") + public JAXBElement createNamespace(String value) { + return new JAXBElement(_Namespace_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "schema") + public JAXBElement createSchema(String value) { + return new JAXBElement(_Schema_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "element") + public JAXBElement createElement(String value) { + return new JAXBElement(_Element_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "qualifier") + public JAXBElement createQualifier(String value) { + return new JAXBElement(_Qualifier_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "scope_note") + public JAXBElement createScopeNote(String value) { + return new JAXBElement(_ScopeNote_QNAME, String.class, null, value); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactory.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactory.java new file mode 100644 index 000000000000..3553cbcba2fd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactory.java @@ -0,0 +1,28 @@ +/** + * 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.app.metadata.export.service; + +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Factory for the export services related to metadata-schema and metadata-fields. + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public abstract class MetadataExportServiceFactory { + + public static MetadataExportServiceFactory getInstance() { + return DSpaceServicesFactory + .getInstance().getServiceManager() + .getServiceByName("metadataExportServiceFactory", MetadataExportServiceFactory.class); + } + + public abstract MetadataSchemaExportService getMetadataSchemaExportService(); + public abstract MetadataFieldExportService getMetadataFieldExportService(); + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactoryImpl.java new file mode 100644 index 000000000000..a69d5dfd0fde --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactoryImpl.java @@ -0,0 +1,31 @@ +/** + * 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.app.metadata.export.service; + +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class MetadataExportServiceFactoryImpl extends MetadataExportServiceFactory { + + @Autowired + private MetadataSchemaExportService metadataSchemaExportService; + @Autowired + private MetadataFieldExportService metadataFieldExportService; + + @Override + public MetadataSchemaExportService getMetadataSchemaExportService() { + return metadataSchemaExportService; + } + + @Override + public MetadataFieldExportService getMetadataFieldExportService() { + return metadataFieldExportService; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportService.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportService.java new file mode 100644 index 000000000000..ace312885230 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportService.java @@ -0,0 +1,35 @@ +/** + * 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.app.metadata.export.service; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.metadata.export.model.DcType; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchema; +import org.dspace.core.Context; + +/** + * Exports {@code MetadataField} into {@code DcType} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public interface MetadataFieldExportService { + + /** + * Creates a one {@link DCType} for each {@link MetadataField} + * in the given {@link MetadataSchema}, and returns them in a list + * + * @param context + * @param metadataSchema + * @return + * @throws SQLException + */ + List exportMetadataFieldsBy(Context context, MetadataSchema metadataSchema) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportServiceImpl.java new file mode 100644 index 000000000000..1ace35f4e45d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportServiceImpl.java @@ -0,0 +1,49 @@ +/** + * 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.app.metadata.export.service; + +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +import org.dspace.app.metadata.export.model.DcType; +import org.dspace.app.metadata.export.model.DcTypeBuilder; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchema; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.core.Context; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class MetadataFieldExportServiceImpl implements MetadataFieldExportService { + + private MetadataFieldService metadataFieldService = + ContentServiceFactory.getInstance().getMetadataFieldService(); + + public List exportMetadataFieldsBy(Context context, MetadataSchema metadataSchema) throws SQLException { + return metadataFieldService + .findAllInSchema(context, metadataSchema) + .stream() + .map(this::toDcType) + .collect(Collectors.toList()); + } + + private DcType toDcType(MetadataField metadataField) { + return DcTypeBuilder + .createBuilder() + .withSchema(metadataField.getMetadataSchema().getName()) + .withElement(metadataField.getElement()) + .withQualifier(metadataField.getQualifier()) + .withScopeNote(metadataField.getScopeNote()) + .build(); + } + +} + diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportService.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportService.java new file mode 100644 index 000000000000..cd1f35e2ef9b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportService.java @@ -0,0 +1,68 @@ +/** + * 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.app.metadata.export.service; + +import java.io.File; +import java.sql.SQLException; + +import org.dspace.app.metadata.export.DspaceExportMetadataSchemaException; +import org.dspace.app.metadata.export.model.DspaceDcTypes; +import org.dspace.content.MetadataSchema; +import org.dspace.core.Context; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public interface MetadataSchemaExportService { + + /** + * Exports the given {@code schemaId} into a {@link DspaceDcTypes} entity + * + * @param context + * @param schemaId + * @return + * @throws SQLException + */ + DspaceDcTypes exportMetadataSchema(Context context, int schemaId) throws SQLException; + + /** + * Exports the given {@code metadataSchema} into a {@link DspaceDcTypes} entity + * + * @param context + * @param metadataSchema + * @return + * @throws SQLException + */ + DspaceDcTypes exportMetadataSchema(Context context, MetadataSchema metadataSchema) throws SQLException; + + /** + * Exports the given {@code metadataSchema} to a temporary {@code File}, + * that will respect the {@code registry} xml format of dspace + * + * @param context + * @param metadataSchema + * @return + * @throws DspaceExportMetadataSchemaException + */ + File exportMetadataSchemaToFile(Context context, MetadataSchema metadataSchema) + throws DspaceExportMetadataSchemaException; + + /** + * Exports the given {@code metadataSchema} to a target {@code File}, + * that will respect the {@code registry} xml format of dspace + * + * @param context + * @param metadataSchema + * @param file + * @return + * @throws DspaceExportMetadataSchemaException + */ + File exportMetadataSchemaToFile(Context context, MetadataSchema metadataSchema, File file) + throws DspaceExportMetadataSchemaException; + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportServiceImpl.java new file mode 100644 index 000000000000..eea9a09f7970 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportServiceImpl.java @@ -0,0 +1,107 @@ +/** + * 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.app.metadata.export.service; + +import java.io.File; +import java.io.IOException; +import java.sql.SQLException; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; + +import org.dspace.app.metadata.export.DspaceExportMetadataSchemaException; +import org.dspace.app.metadata.export.model.DcSchema; +import org.dspace.app.metadata.export.model.DcSchemaBuilder; +import org.dspace.app.metadata.export.model.DspaceDcTypes; +import org.dspace.app.metadata.export.model.DspaceDcTypesBuilder; +import org.dspace.content.MetadataSchema; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.MetadataSchemaService; +import org.dspace.core.Context; + +/** + * This service can be used to export a target schema into a registry-file + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class MetadataSchemaExportServiceImpl implements MetadataSchemaExportService { + + private MetadataSchemaService metadataSchemaService = + ContentServiceFactory.getInstance().getMetadataSchemaService(); + + @Override + public DspaceDcTypes exportMetadataSchema(Context context, int schemaId) throws SQLException { + return this.exportMetadataSchema(context, metadataSchemaService.find(context, schemaId)); + } + + @Override + public DspaceDcTypes exportMetadataSchema(Context context, MetadataSchema metadataSchema) throws SQLException { + return DspaceDcTypesBuilder + .createBuilder() + .withSchema(this.mapToDcSchema(metadataSchema)) + .withDcTypes( + MetadataExportServiceFactory.getInstance() + .getMetadataFieldExportService() + .exportMetadataFieldsBy(context, metadataSchema) + ) + .build(); + } + + @Override + public File exportMetadataSchemaToFile(Context context, MetadataSchema metadataSchema) + throws DspaceExportMetadataSchemaException { + File tempFile; + try { + tempFile = + File.createTempFile( + metadataSchema.getName() + "-" + metadataSchema.getID(), + ".xml" + ); + tempFile.deleteOnExit(); + return this.exportMetadataSchemaToFile(context, metadataSchema, tempFile); + } catch (IOException e) { + throw new DspaceExportMetadataSchemaException( + "Probelm occured during while exporting to temporary file!", + e + ); + } + } + + @Override + public File exportMetadataSchemaToFile(Context context, MetadataSchema metadataSchema, File file) + throws DspaceExportMetadataSchemaException { + try { + DspaceDcTypes dspaceDcTypes = this.exportMetadataSchema(context, metadataSchema); + + JAXBContext jaxb = JAXBContext.newInstance(DspaceDcTypes.class); + Marshaller jaxbMarshaller = jaxb.createMarshaller(); + jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + jaxbMarshaller.marshal(dspaceDcTypes, file); + } catch (SQLException e) { + throw new DspaceExportMetadataSchemaException( + "Problem occured while retrieving data from DB!", + e + ); + } catch (JAXBException e) { + throw new DspaceExportMetadataSchemaException( + "Problem occured during the export to XML file!", + e + ); + } + return file; + } + + private DcSchema mapToDcSchema(MetadataSchema metadataSchema) { + return DcSchemaBuilder + .createBuilder() + .withName(metadataSchema.getName()) + .withNamespace(metadataSchema.getNamespace()) + .build(); + } + +} diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml index 0697423578bc..fdd7886c477b 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml @@ -155,5 +155,10 @@ + + + + + diff --git a/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java b/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java new file mode 100644 index 000000000000..bd53c48602b5 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java @@ -0,0 +1,145 @@ +/** + * 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.app.metadata.export; + +import static org.dspace.app.launcher.ScriptLauncher.handleScript; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.is; + +import java.io.File; +import java.io.FileInputStream; +import java.nio.charset.Charset; +import java.sql.SQLException; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.app.launcher.ScriptLauncher; +import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.MetadataFieldBuilder; +import org.dspace.builder.MetadataSchemaBuilder; +import org.dspace.content.MetadataSchema; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.Before; +import org.junit.Test; + + +/** + * Integration tests for {@link MetadataSchemaExportScript} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + * + */ +public class MetadataSchemaExportScriptIT extends AbstractIntegrationTestWithDatabase { + + private final ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); + + private MetadataSchema schema; + private List fields; + private String fileLocation; + + @Before + @SuppressWarnings("deprecation") + public void beforeTests() throws SQLException, AuthorizeException { + context.turnOffAuthorisationSystem(); + schema = createMetadataSchema(); + fields = createFields(); + fileLocation = configurationService.getProperty("dspace.dir"); + context.restoreAuthSystemState(); + } + + private List createFields() throws SQLException, AuthorizeException { + return List.of( + MetadataFieldBuilder.createMetadataField(context, schema, "first", "metadata", "notes first"), + MetadataFieldBuilder.createMetadataField(context, schema, "second", "metadata", "notes second"), + MetadataFieldBuilder.createMetadataField(context, schema, "third", "metadata", "notes third"), + MetadataFieldBuilder.createMetadataField(context, schema, "element", null, null) + ); + } + + private MetadataSchema createMetadataSchema() throws SQLException, AuthorizeException { + return MetadataSchemaBuilder.createMetadataSchema(context, "test", "http://dspace.org/test").build(); + } + + @Test + public void testMetadataSchemaExport() throws Exception { + + File xml = new File(fileLocation + "/test-types.xml"); + xml.deleteOnExit(); + + String[] args = + new String[] { + "export-schema", + "-i", schema.getID().toString(), + "-f", xml.getAbsolutePath() + }; + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); + + assertThat(handler.getErrorMessages(), empty()); + assertThat( + handler.getInfoMessages(), + hasItem("Exporting the metadata-schema file for the schema " + schema.getName()) + ); + assertThat("The xml file should be created", xml.exists(), is(true)); + + + try (FileInputStream fis = new FileInputStream(xml)) { + String content = IOUtils.toString(fis, Charset.defaultCharset()); + assertThat(content, containsString("")); + assertThat(content, containsString("test")); + assertThat(content, containsString("http://dspace.org/test")); + assertThat(content, containsString("")); + assertThat(content, containsString("test")); + assertThat(content, containsString("first")); + assertThat(content, containsString("metadata")); + assertThat(content, containsString("notes first")); + assertThat(content, containsString("")); + assertThat(content, containsString("")); + assertThat(content, containsString("test")); + assertThat(content, containsString("third")); + assertThat(content, containsString("metadata")); + assertThat(content, containsString("notes third")); + assertThat(content, containsString("")); + assertThat(content, containsString("")); + assertThat(content, containsString("test")); + assertThat(content, containsString("element")); + assertThat(content, containsString("")); + assertThat(content, containsString("")); + assertThat(content, containsString("")); + } + } + + @Test + public void testMetadataNotExistingSchemaExport() throws Exception { + + File xml = new File(fileLocation + "/test-types.xml"); + xml.deleteOnExit(); + + String[] args = + new String[] { + "export-schema", + "-i", "-1", + "-f", xml.getAbsolutePath() + }; + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); + + assertThat(handler.getErrorMessages(), hasItem("Cannot find the metadata-schema with id: -1")); + assertThat("The xml file should not be created", xml.exists(), is(false)); + } + +} diff --git a/dspace/config/registries/dspace-dc-types.xsd b/dspace/config/registries/dspace-dc-types.xsd new file mode 100644 index 000000000000..1722d3c505c9 --- /dev/null +++ b/dspace/config/registries/dspace-dc-types.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/spring/api/metadata-schema-export.xml b/dspace/config/spring/api/metadata-schema-export.xml new file mode 100644 index 000000000000..26b9daf76045 --- /dev/null +++ b/dspace/config/spring/api/metadata-schema-export.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index a99d580dd901..f4f7c97c0011 100644 --- a/dspace/config/spring/api/scripts.xml +++ b/dspace/config/spring/api/scripts.xml @@ -160,4 +160,9 @@ + + + + + diff --git a/dspace/config/spring/rest/scripts.xml b/dspace/config/spring/rest/scripts.xml index 6853d7499276..20ca17f2dabc 100644 --- a/dspace/config/spring/rest/scripts.xml +++ b/dspace/config/spring/rest/scripts.xml @@ -108,4 +108,9 @@ + + + + + From 404c825e559a69fabb8f596e1a4a65ec1d132f8c Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 31 Aug 2023 09:27:18 +0200 Subject: [PATCH 17/81] [DSC-1117] Fixes wrong property name in ApplicationConfig --- .../main/java/org/dspace/app/rest/utils/ApplicationConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java index 67983ba8f007..91c832c0533e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java @@ -38,7 +38,7 @@ public class ApplicationConfig { // Allowed IIIF CORS origins ("Access-Control-Allow-Origin" header) // Can be overridden in DSpace configuration - @Value("${rest.cors.bitstream-allow-origins}") + @Value("${rest.cors.bitstream-allowed-origins}") private String[] bitstreamCorsAllowedOrigins; // Whether to allow credentials (cookies) in CORS requests ("Access-Control-Allow-Credentials" header) From 8264b30b9a0959c2d1343936f9fabf44ceba0350 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 31 Aug 2023 11:00:59 +0200 Subject: [PATCH 18/81] [DSC-1214] Fixed virtual field place after item update --- .../impl/RelatedEntityItemEnhancer.java | 33 +++++++-- .../consumer/ItemEnhancerConsumerIT.java | 70 +++++++++++++++++++ 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/enhancer/impl/RelatedEntityItemEnhancer.java b/dspace-api/src/main/java/org/dspace/content/enhancer/impl/RelatedEntityItemEnhancer.java index a5d95582e41d..a6c97cc84e65 100644 --- a/dspace-api/src/main/java/org/dspace/content/enhancer/impl/RelatedEntityItemEnhancer.java +++ b/dspace-api/src/main/java/org/dspace/content/enhancer/impl/RelatedEntityItemEnhancer.java @@ -78,7 +78,7 @@ private void cleanObsoleteVirtualFields(Context context, Item item) throws SQLEx } private void updateVirtualFieldsPlaces(Context context, Item item) { - List virtualSourceFields = getMetadataValues(item, getVirtualSourceMetadataField()); + List virtualSourceFields = getVirtualSourceFields(item); for (MetadataValue virtualSourceField : virtualSourceFields) { metadataWithPlaceToUpdate(item, virtualSourceField) .ifPresent(updatePlaces(item, virtualSourceField)); @@ -113,9 +113,9 @@ private List getObsoleteVirtualFields(Item item) { List obsoleteVirtualFields = new ArrayList<>(); - List virtualSourceFields = getMetadataValues(item, getVirtualSourceMetadataField()); + List virtualSourceFields = getVirtualSourceFields(item); for (MetadataValue virtualSourceField : virtualSourceFields) { - if (isRelatedSourceNoMorePresent(item, virtualSourceField)) { + if (!isPlaceholder(virtualSourceField) && isRelatedSourceNoMorePresent(item, virtualSourceField)) { obsoleteVirtualFields.add(virtualSourceField); getRelatedVirtualField(item, virtualSourceField).ifPresent(obsoleteVirtualFields::add); } @@ -131,7 +131,7 @@ private boolean isRelatedSourceNoMorePresent(Item item, MetadataValue virtualSou } private Optional getRelatedVirtualField(Item item, MetadataValue virtualSourceField) { - return getMetadataValues(item, getVirtualMetadataField()).stream() + return getVirtualFields(item).stream() .filter(metadataValue -> metadataValue.getPlace() == virtualSourceField.getPlace()) .findFirst(); } @@ -141,6 +141,7 @@ private void performEnhancement(Context context, Item item) throws SQLException if (noEnhanceableMetadata(context, item)) { return; } + for (MetadataValue metadataValue : getEnhanceableMetadataValue(item)) { if (wasValueAlreadyUsedForEnhancement(item, metadataValue)) { @@ -191,9 +192,19 @@ private List getEnhanceableMetadataValue(Item item) { } private boolean wasValueAlreadyUsedForEnhancement(Item item, MetadataValue metadataValue) { - return getMetadataValues(item, getVirtualSourceMetadataField()).stream() + + if (isPlaceholderAtPlace(getVirtualFields(item), metadataValue.getPlace())) { + return true; + } + + return getVirtualSourceFields(item).stream() .anyMatch(virtualSourceField -> virtualSourceField.getPlace() == metadataValue.getPlace() && hasAuthorityEqualsTo(metadataValue, virtualSourceField.getValue())); + + } + + private boolean isPlaceholderAtPlace(List metadataValues, int place) { + return place < metadataValues.size() ? isPlaceholder(metadataValues.get(place)) : false; } private boolean hasAuthorityEqualsTo(MetadataValue metadataValue, String authority) { @@ -209,10 +220,22 @@ private Item findRelatedEntityItem(Context context, MetadataValue metadataValue) } } + private boolean isPlaceholder(MetadataValue metadataValue) { + return PLACEHOLDER_PARENT_METADATA_VALUE.equals(metadataValue.getValue()); + } + private List getMetadataValues(Item item, String metadataField) { return itemService.getMetadataByMetadataString(item, metadataField); } + private List getVirtualSourceFields(Item item) { + return getMetadataValues(item, getVirtualSourceMetadataField()); + } + + private List getVirtualFields(Item item) { + return getMetadataValues(item, getVirtualMetadataField()); + } + private void addVirtualField(Context context, Item item, String value) throws SQLException { itemService.addMetadata(context, item, VIRTUAL_METADATA_SCHEMA, VIRTUAL_METADATA_ELEMENT, getVirtualQualifier(), null, value); diff --git a/dspace-api/src/test/java/org/dspace/content/enhancer/consumer/ItemEnhancerConsumerIT.java b/dspace-api/src/test/java/org/dspace/content/enhancer/consumer/ItemEnhancerConsumerIT.java index 5e95c28f65b7..eee35a81d045 100644 --- a/dspace-api/src/test/java/org/dspace/content/enhancer/consumer/ItemEnhancerConsumerIT.java +++ b/dspace-api/src/test/java/org/dspace/content/enhancer/consumer/ItemEnhancerConsumerIT.java @@ -10,6 +10,7 @@ import static org.dspace.app.matcher.MetadataValueMatcher.with; import static org.dspace.core.CrisConstants.PLACEHOLDER_PARENT_METADATA_VALUE; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; @@ -290,6 +291,75 @@ public void testWithWorkspaceItem() throws Exception { } + @Test + @SuppressWarnings("unchecked") + public void testEnhancementAfterItemUpdate() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item person = ItemBuilder.createItem(context, collection) + .withTitle("Walter White") + .withOrcidIdentifier("0000-0000-1111-2222") + .build(); + + String personId = person.getID().toString(); + + Item publication = ItemBuilder.createItem(context, collection) + .withTitle("Test publication") + .withEntityType("Publication") + .withAuthor("Jesse Pinkman") + .withAuthor("Saul Goodman") + .withAuthor("Walter White", person.getID().toString()) + .withAuthor("Gus Fring") + .build(); + + context.restoreAuthSystemState(); + publication = commitAndReload(publication); + + assertThat(getMetadataValues(publication, "dc.contributor.author"), contains( + with("dc.contributor.author", "Jesse Pinkman"), + with("dc.contributor.author", "Saul Goodman", 1), + with("dc.contributor.author", "Walter White", personId, 2, 600), + with("dc.contributor.author", "Gus Fring", 3))); + + assertThat(getMetadataValues(publication, "cris.virtual.author-orcid"), contains( + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE), + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 1), + with("cris.virtual.author-orcid", "0000-0000-1111-2222", 2), + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 3))); + + assertThat(getMetadataValues(publication, "cris.virtualsource.author-orcid"), contains( + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE), + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 1), + with("cris.virtualsource.author-orcid", personId, 2), + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 3))); + + context.turnOffAuthorisationSystem(); + itemService.addMetadata(context, publication, "dc", "title", "alternative", null, "Other name"); + itemService.update(context, publication); + context.restoreAuthSystemState(); + publication = commitAndReload(publication); + + assertThat(getMetadataValues(publication, "dc.contributor.author"), contains( + with("dc.contributor.author", "Jesse Pinkman"), + with("dc.contributor.author", "Saul Goodman", 1), + with("dc.contributor.author", "Walter White", personId, 2, 600), + with("dc.contributor.author", "Gus Fring", 3))); + + assertThat(getMetadataValues(publication, "cris.virtual.author-orcid"), contains( + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE), + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 1), + with("cris.virtual.author-orcid", "0000-0000-1111-2222", 2), + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 3))); + + assertThat(getMetadataValues(publication, "cris.virtualsource.author-orcid"), contains( + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE), + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 1), + with("cris.virtualsource.author-orcid", personId, 2), + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 3))); + + } + private MetadataValue getFirstMetadataValue(Item item, String metadataField) { return getMetadataValues(item, metadataField).get(0); } From cfaafed8138ca6ec0e2d43de776a5600313d46b6 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 31 Aug 2023 11:54:42 +0200 Subject: [PATCH 19/81] [DSC-1117] Fixes property key in rest.cfg --- dspace/config/modules/rest.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 9d2eb77be2cc..cfe954d410c7 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -134,7 +134,7 @@ rest.regex-clause = text_value ~ ? ##### Customize the REST origins allowed to retrieve the bitstreams ##### ##### default is set to pattern * - use this configuration to restrict/modify this behavior ##### This configuration doens't support the wildcard -bitstream.cors.allowed-origins = +rest.cors.bitstream-allowed-origins = ##### Configure REST Report Filters ##### @@ -233,4 +233,4 @@ rest.search.max.results = 100 # Patterns associated with uri for which do not set the context in read-only mode for GET calls rest.get-in-read-only-mode.exception-patterns = /api/authn/** -rest.get-in-read-only-mode.exception-patterns = /api/cris/orcid/** +rest.get-in-read-only-mode.exception-patterns = /api/cris/orcid/** \ No newline at end of file From f0356d586abfca7bc793d71986e8f79a727e1e83 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 31 Aug 2023 12:02:31 +0200 Subject: [PATCH 20/81] [DSC-1224] Refactoring of filename generation in script --- .../app/metadata/export/MetadataSchemaExportScript.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java index 59b3ddfda1a1..3b07722a4b13 100644 --- a/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java @@ -10,6 +10,7 @@ import java.io.File; import java.io.FileInputStream; import java.sql.SQLException; +import java.text.MessageFormat; import org.apache.commons.cli.ParseException; import org.dspace.app.metadata.export.service.MetadataExportServiceFactory; @@ -34,6 +35,8 @@ public class MetadataSchemaExportScript extends DSpaceRunnable> { + protected static String REGISTRY_FILENAME_TEMPLATE = "{0}-types.xml"; + protected MetadataSchemaService metadataSchemaService = ContentServiceFactory.getInstance().getMetadataSchemaService(); @@ -103,7 +106,7 @@ private void exportMetadataSchema(Context context) throws Exception { handler.logInfo("Summarizing export ..."); context.turnOffAuthorisationSystem(); handler.writeFilestream( - context, metadataSchema.getName(), fis, "application/xml", false + context, getFilename(metadataSchema), fis, "application/xml", false ); context.restoreAuthSystemState(); } @@ -113,6 +116,10 @@ private void exportMetadataSchema(Context context) throws Exception { } } + protected String getFilename(MetadataSchema ms) { + return MessageFormat.format(REGISTRY_FILENAME_TEMPLATE, ms.getName()); + } + protected File getExportedFile(Context context) throws DspaceExportMetadataSchemaException { return this.metadataSchemaExportService.exportMetadataSchemaToFile(context, metadataSchema); } From 1df9cb8d53568c1ce8f1008625969051e3600f0e Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Tue, 5 Sep 2023 12:59:08 +0200 Subject: [PATCH 21/81] [DSC-1233] fixed issue with type of Pubmed during import --- .../contributor/SimpleXpathMetadatumContributor.java | 3 +-- dspace/config/spring/api/pubmed-integration.xml | 10 ++++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java index 598b827011b8..76c881817967 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java @@ -9,7 +9,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.annotation.Resource; @@ -149,7 +148,7 @@ public void setQuery(String query) { */ @Override public Collection contributeMetadata(Element t) { - List values = new LinkedList<>(); + List values = new ArrayList<>(); List namespaces = new ArrayList<>(); for (String ns : prefixToNamespaceMapping.keySet()) { diff --git a/dspace/config/spring/api/pubmed-integration.xml b/dspace/config/spring/api/pubmed-integration.xml index 084d63579745..f316a1c0d2dd 100644 --- a/dspace/config/spring/api/pubmed-integration.xml +++ b/dspace/config/spring/api/pubmed-integration.xml @@ -29,7 +29,7 @@ - + @@ -151,16 +151,15 @@ - - + - - + + @@ -231,6 +230,5 @@ these must be present. If multiple are present the result is undefined. - From f813ff26c6868c13393b34ab3f568e006187e0d4 Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Tue, 5 Sep 2023 13:28:00 +0200 Subject: [PATCH 22/81] [DSC-1233] minor fix --- .../contributor/SimpleXpathMetadatumContributor.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java index 76c881817967..598b827011b8 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.annotation.Resource; @@ -148,7 +149,7 @@ public void setQuery(String query) { */ @Override public Collection contributeMetadata(Element t) { - List values = new ArrayList<>(); + List values = new LinkedList<>(); List namespaces = new ArrayList<>(); for (String ns : prefixToNamespaceMapping.keySet()) { From 62ca6eb5aa496dbcd69d991faeb4a97a5574b2df Mon Sep 17 00:00:00 2001 From: Mykhaylo Boychuk Date: Tue, 5 Sep 2023 16:04:45 +0200 Subject: [PATCH 23/81] [DSC-1233] return null instead of launch the exception --- .../external/provider/impl/LiveImportDataProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/LiveImportDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/LiveImportDataProvider.java index 21c14813f93d..9897639f04a6 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/LiveImportDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/LiveImportDataProvider.java @@ -9,6 +9,7 @@ import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -135,9 +136,8 @@ public int getNumberOfResults(String query) { * @return */ private ExternalDataObject getExternalDataObject(ImportRecord record) { - //return 400 if no record were found - if (record == null) { - throw new IllegalArgumentException("No record found for query or id"); + if (Objects.isNull(record)) { + return null; } ExternalDataObject externalDataObject = new ExternalDataObject(sourceIdentifier); String id = getFirstValue(record, recordIdMetadata); From f9de9fe6354b49fcec34c2a8801db6639d18f41a Mon Sep 17 00:00:00 2001 From: eskander Date: Wed, 6 Sep 2023 11:13:36 +0300 Subject: [PATCH 24/81] [DSC-1217] Update the wrong Scopus To Coar types mapping --- ...nverter-scopusToCoarPublicationTypes.properties | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dspace/config/crosswalks/mapConverter-scopusToCoarPublicationTypes.properties b/dspace/config/crosswalks/mapConverter-scopusToCoarPublicationTypes.properties index 35e759cd6860..33aa97437dda 100644 --- a/dspace/config/crosswalks/mapConverter-scopusToCoarPublicationTypes.properties +++ b/dspace/config/crosswalks/mapConverter-scopusToCoarPublicationTypes.properties @@ -1,14 +1,14 @@ ar = Resource Types::text::journal::journal article er = Resource Types::text::journal::journal article::corrigendum re = Resource Types::text::journal::journal article::review article -cp = Resource Types::text::conference outputs::conference proceedings::conference paper +cp = Resource Types::text::conference output::conference proceedings::conference paper bk = Resource Types::text::book ch = Resource Types::text::book chapter ed = Resource Types::text::journal::editorial le = Resource Types::text::letter -cr = Conference Review -ab = Abstract Report -bz = Business Article -no = Note -pr = Press Release -sh = Short Survey \ No newline at end of file +cr = Resource Types::text::review +ab = Resource Types::text::report +bz = Resource Types::text::journal::journal article +no = Resource Types::text +pr = Resource Types::text +sh = Resource Types::text \ No newline at end of file From 4dfcf692fa944e9d7c7b3eefbadc97e8d2958a23 Mon Sep 17 00:00:00 2001 From: eskander Date: Thu, 7 Sep 2023 17:38:06 +0300 Subject: [PATCH 25/81] [DSC-897] changed the implementation of Categories in statistics section depending --- .../statistics/TopCategoriesGenerator.java | 50 ++++++++++- dspace/config/spring/rest/statistics.xml | 87 ++++++++++++------- 2 files changed, 103 insertions(+), 34 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java index 39d8a1730c06..e0ced24e0eb2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java @@ -10,6 +10,7 @@ import static org.dspace.core.Constants.ITEM; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -21,9 +22,11 @@ import org.dspace.app.rest.model.UsageReportRest; import org.dspace.app.rest.utils.UsageReportUtils; import org.dspace.content.DSpaceObject; +import org.dspace.content.MetadataValue; import org.dspace.core.Context; import org.dspace.discovery.configuration.DiscoveryConfiguration; import org.dspace.discovery.configuration.DiscoveryConfigurationService; +import org.dspace.services.ConfigurationService; import org.dspace.statistics.content.StatisticsDatasetDisplay; import org.dspace.statistics.service.SolrLoggerService; import org.springframework.beans.factory.annotation.Autowired; @@ -40,13 +43,19 @@ public class TopCategoriesGenerator extends AbstractUsageReportGenerator { @Autowired private SolrLoggerService solrLoggerService; + @Autowired + private ConfigurationService configurationService; + @Autowired private DiscoveryConfigurationService discoveryConfigurationService; private Map categoryQueries; + private Map> entityCategoryQueries; + public UsageReportRest createUsageReport(Context context, DSpaceObject dso, String startDate, String endDate) { + setCategoryQueries(getEntityType(dso)); Map categoriesCount = getCategoriesCount(dso, startDate, endDate); UsageReportRest usageReportRest = new UsageReportRest(); @@ -129,11 +138,44 @@ public String getReportType() { return UsageReportUtils.TOP_CATEGORIES_REPORT_ID; } - public Map getCategoryQueries() { - return categoryQueries; + private String getEntityType(DSpaceObject dso) { + return dso.getMetadata() + .stream() + .filter(metadataValue -> + "dspace.entity.type".equals(metadataValue.getMetadataField().toString('.'))) + .map(MetadataValue::getValue) + .findFirst() + .orElse(""); } - public void setCategoryQueries(Map categoryQueries) { - this.categoryQueries = categoryQueries; + private void setCategoryQueries(String entityType) { + if (entityCategoryQueries != null && + entityCategoryQueries.containsKey(entityType)) { + categoryQueries = entityCategoryQueries.get(entityType); + } else { + categoryQueries = getDefaultCategoryQueries(); + } } + + private Map getDefaultCategoryQueries() { + return Arrays.stream(getDefaultEntityTypes()) + .collect(Collectors.toMap( + type -> type.toLowerCase(), + type -> "entityType_keyword: '" + type + "'" + )); + } + + private String[] getDefaultEntityTypes() { + return configurationService.getArrayProperty("cris.entity-type"); + } + + public Map> getEntityCategoryQueries() { + return entityCategoryQueries; + } + + public void setEntityCategoryQueries( + Map> entityCategoryQueries) { + this.entityCategoryQueries = entityCategoryQueries; + } + } diff --git a/dspace/config/spring/rest/statistics.xml b/dspace/config/spring/rest/statistics.xml index 23b528eddcb5..76b4944e6e8c 100644 --- a/dspace/config/spring/rest/statistics.xml +++ b/dspace/config/spring/rest/statistics.xml @@ -2,10 +2,13 @@ + http://www.springframework.org/schema/context/spring-context-2.5.xsd + http://www.springframework.org/schema/util + http://www.springframework.org/schema/util/spring-util.xsd"> @@ -405,16 +408,6 @@ - - - - - - - - - - @@ -449,18 +442,62 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -501,16 +538,6 @@ - - - - - - - - - - From 15f603401975f9780198d81d1014612370e86f7c Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 8 Sep 2023 15:21:49 +0200 Subject: [PATCH 26/81] [DSC-1224] Fixes nil element generation --- .../java/org/dspace/app/metadata/export/model/DcType.java | 8 ++++---- .../app/metadata/export/MetadataSchemaExportScriptIT.java | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java index dcf4d9821cd0..bff2fc77978a 100644 --- a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java @@ -46,10 +46,10 @@ public class DcType { @XmlElementRefs({ - @XmlElementRef(name = "schema", type = JAXBElement.class), - @XmlElementRef(name = "element", type = JAXBElement.class), - @XmlElementRef(name = "qualifier", type = JAXBElement.class), - @XmlElementRef(name = "scope_note", type = JAXBElement.class) + @XmlElementRef(name = "schema", type = JAXBElement.class, required = false), + @XmlElementRef(name = "element", type = JAXBElement.class, required = false), + @XmlElementRef(name = "qualifier", type = JAXBElement.class, required = false), + @XmlElementRef(name = "scope_note", type = JAXBElement.class, required = false) }) protected List> schemaOrElementOrQualifier; diff --git a/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java b/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java index bd53c48602b5..6ed2279bb1fa 100644 --- a/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java +++ b/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java @@ -116,8 +116,6 @@ public void testMetadataSchemaExport() throws Exception { assertThat(content, containsString("")); assertThat(content, containsString("test")); assertThat(content, containsString("element")); - assertThat(content, containsString("")); - assertThat(content, containsString("")); assertThat(content, containsString("")); } } From 2d6a75cb3911ad54e99277e26c9ac63f4db9b878 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 8 Sep 2023 15:23:40 +0200 Subject: [PATCH 27/81] [DSC-1224] Fixes nil element generation --- .../dspace/app/metadata/export/model/AbstractJaxbBuilder.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java index 038d8552e1e2..925020a52631 100644 --- a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java @@ -47,6 +47,9 @@ public T build() { } protected void addChildElement(C value, Function> mapper) { + if (value == null) { + return; + } addChildElement(mapper.apply(value)); } From 1e1cccd69e772da26eaacb18fd8e51078a986f60 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 31 Aug 2023 09:27:18 +0200 Subject: [PATCH 28/81] [DSC-1117] Fixes wrong property name in ApplicationConfig --- .../main/java/org/dspace/app/rest/utils/ApplicationConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java index 67983ba8f007..91c832c0533e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java @@ -38,7 +38,7 @@ public class ApplicationConfig { // Allowed IIIF CORS origins ("Access-Control-Allow-Origin" header) // Can be overridden in DSpace configuration - @Value("${rest.cors.bitstream-allow-origins}") + @Value("${rest.cors.bitstream-allowed-origins}") private String[] bitstreamCorsAllowedOrigins; // Whether to allow credentials (cookies) in CORS requests ("Access-Control-Allow-Credentials" header) From a068fa4ec80ae499e5f95e347632f176035f4358 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 31 Aug 2023 11:54:42 +0200 Subject: [PATCH 29/81] [DSC-1117] Fixes property key in rest.cfg --- dspace/config/modules/rest.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 9d2eb77be2cc..cfe954d410c7 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -134,7 +134,7 @@ rest.regex-clause = text_value ~ ? ##### Customize the REST origins allowed to retrieve the bitstreams ##### ##### default is set to pattern * - use this configuration to restrict/modify this behavior ##### This configuration doens't support the wildcard -bitstream.cors.allowed-origins = +rest.cors.bitstream-allowed-origins = ##### Configure REST Report Filters ##### @@ -233,4 +233,4 @@ rest.search.max.results = 100 # Patterns associated with uri for which do not set the context in read-only mode for GET calls rest.get-in-read-only-mode.exception-patterns = /api/authn/** -rest.get-in-read-only-mode.exception-patterns = /api/cris/orcid/** +rest.get-in-read-only-mode.exception-patterns = /api/cris/orcid/** \ No newline at end of file From 191ac97dbc2fa4e1dc15e5f17887c3334d450757 Mon Sep 17 00:00:00 2001 From: corrado lombardi Date: Mon, 11 Sep 2023 12:57:11 +0200 Subject: [PATCH 30/81] [DSC-1238][CST-11734] added type option to subscription-send process --- .../SubscriptionEmailNotification.java | 13 ++++++++++--- .../SubscriptionEmailNotificationConfiguration.java | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java index b429ecbd46e7..cc5cac24eabb 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java @@ -48,16 +48,23 @@ public void setup() throws ParseException { public void internalRun() throws Exception { assignCurrentUserInContext(); assignSpecialGroupsInContext(); + String typeOption = commandLine.getOptionValue("t"); String frequencyOption = commandLine.getOptionValue("f"); - if (StringUtils.isBlank(frequencyOption)) { - throw new IllegalArgumentException("Option --frequency (-f) must be set"); + if (StringUtils.isBlank(frequencyOption) || StringUtils.isBlank(typeOption)) { + throw new IllegalArgumentException("Options --frequency (-f) and --type (-t) must be set"); } if (!FrequencyType.isSupportedFrequencyType(frequencyOption)) { throw new IllegalArgumentException( "Option f must be one of following values D(Day), W(Week) or M(Month)"); } - subscriptionEmailNotificationService.perform(getContext(), handler, "content", frequencyOption); + + if (!StringUtils.equalsAny(typeOption, "content", "statistics")) { + throw new IllegalArgumentException( + "Option t (type) must be one of \"content\" or \"statistics\""); + } + + subscriptionEmailNotificationService.perform(getContext(), handler, typeOption, frequencyOption); } private void assignCurrentUserInContext() throws SQLException { diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java index 52685b563d9b..d4f76a555936 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java @@ -42,6 +42,9 @@ public boolean isAllowedToExecute(Context context) { public Options getOptions() { if (Objects.isNull(options)) { Options options = new Options(); + options.addOption("t", "type", true, + "Subscription type, Valid values are \"content\" or \"statistics\""); + options.getOption("t").setRequired(true); options.addOption("f", "frequency", true, "Subscription frequency. Valid values include: D (Day), W (Week) and M (Month)"); options.getOption("f").setRequired(true); From c15db0d530aca60f632a2c167f96b3e8db5e865f Mon Sep 17 00:00:00 2001 From: eskander Date: Tue, 12 Sep 2023 17:59:45 +0300 Subject: [PATCH 31/81] [DSC-897] excluded top categories report from Person Collection statistics --- .../AbstractUsageReportGenerator.java | 27 +++++++++++++++++++ .../rest/statistics/UsageReportGenerator.java | 6 +++++ .../app/rest/utils/UsageReportUtils.java | 14 +++++++++- dspace/config/spring/rest/statistics.xml | 5 ++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java index 940773547da4..368733f2ad0a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java @@ -7,6 +7,11 @@ */ package org.dspace.app.rest.statistics; +import java.util.List; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + /** * This is an abstract class that adds common configurable options for the generator * @@ -16,6 +21,7 @@ public abstract class AbstractUsageReportGenerator implements UsageReportGenerat private String viewMode = "table"; private int maxResults = 100; private String relation; + private List excludedEntityTypes; public void setViewMode(String viewMode) { this.viewMode = viewMode; @@ -40,5 +46,26 @@ public String getRelation() { public void setRelation(String relation) { this.relation = relation; } + + public boolean isApplicable(String entityType) { + + if (StringUtils.isEmpty(entityType)) { + return true; + } + + if (CollectionUtils.isNotEmpty(excludedEntityTypes)) { + return !excludedEntityTypes.contains(entityType); + } + + return true; + } + + public List getExcludedEntityTypes() { + return excludedEntityTypes; + } + + public void setExcludedEntityTypes(List excludedEntityTypes) { + this.excludedEntityTypes = excludedEntityTypes; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/UsageReportGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/UsageReportGenerator.java index ee542785c8cb..70e0bd499cae 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/UsageReportGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/UsageReportGenerator.java @@ -45,4 +45,10 @@ public UsageReportRest createUsageReport(Context context, */ public String getViewMode(); + /** + * + * @return true if the report applicable for the provided entity type + */ + public boolean isApplicable(String entityType); + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java index 47dbcef1749d..199f57b33405 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java @@ -27,6 +27,7 @@ import org.dspace.content.Bitstream; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; +import org.dspace.content.MetadataValue; import org.dspace.content.Site; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -126,7 +127,8 @@ public List getUsageReportsOfDSO(Context context, for (UsageReportCategoryRest cat : categories) { if (category == null || StringUtils.equals(cat.getId(), category)) { for (Entry entry : cat.getReports().entrySet()) { - if (!reportIds.contains(entry.getKey())) { + if (!reportIds.contains(entry.getKey()) && + entry.getValue().isApplicable(getEntityType(dso))) { reportIds.add(entry.getKey()); reports.add(createUsageReport(context, dso, entry.getKey(), startDate, endDate)); } @@ -136,6 +138,16 @@ public List getUsageReportsOfDSO(Context context, return reports; } + private String getEntityType(DSpaceObject dso) { + return dso.getMetadata() + .stream() + .filter(metadataValue -> + "dspace.entity.type".equals(metadataValue.getMetadataField().toString('.'))) + .map(MetadataValue::getValue) + .findFirst() + .orElse(""); + } + private List getReports(Context context, DSpaceObject dso, String category) { List reports = new ArrayList(); if (dso instanceof Site) { diff --git a/dspace/config/spring/rest/statistics.xml b/dspace/config/spring/rest/statistics.xml index 76b4944e6e8c..f5028b6e3e6d 100644 --- a/dspace/config/spring/rest/statistics.xml +++ b/dspace/config/spring/rest/statistics.xml @@ -441,6 +441,11 @@ + + + Person + + From a1b684509727422630b056f0d30a07e5607d2d45 Mon Sep 17 00:00:00 2001 From: "aliaksei.bykau" Date: Tue, 12 Sep 2023 17:17:49 +0200 Subject: [PATCH 32/81] [DSC-1234] added cell limit. Now if content of the cell is bigger than limit it will be truncated and end with "..." --- .../org/dspace/app/bulkimport/model/BulkImportSheet.java | 8 +++++++- .../content/integration/crosswalks/XlsCrosswalk.java | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java b/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java index 14fbb60524fb..53c5f9b99166 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; +import org.apache.commons.lang.StringUtils; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -107,7 +108,12 @@ public void appendValueOnLastRow(String header, String value, String separator) throw new IllegalArgumentException("Unknown header '" + header + "'"); } String cellContent = WorkbookUtils.getCellValue(lastRow, column); - createCell(lastRow, column, isEmpty(cellContent) ? value : cellContent + separator + value); + createCell(lastRow, column, + getValueLimitedByLength(isEmpty(cellContent) ? value : cellContent + separator + value)); + } + + private String getValueLimitedByLength(String value) { + return StringUtils.length(value) > 32726 ? value.substring(0, 32725) + "…" : value; } } diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java index 026b6f375dfa..cbbfee4fb49b 100644 --- a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java @@ -11,6 +11,7 @@ import java.io.OutputStream; import java.util.List; +import org.apache.commons.lang.StringUtils; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; @@ -45,7 +46,7 @@ protected void writeRows(List> rows, OutputStream out) { int cellCount = 0; for (String field : row) { Cell cell = sheetRow.createCell(cellCount++); - cell.setCellValue(field); + cell.setCellValue(StringUtils.length(field) > 32726 ? field.substring(0, 32725) + "…" : field ); } } From 883262c908b7b7c35ced2bec04e23c95f27a957f Mon Sep 17 00:00:00 2001 From: eskander Date: Tue, 12 Sep 2023 19:23:02 +0300 Subject: [PATCH 33/81] [DSC-897] fixed broken ITs methods --- .../app/rest/StatisticsRestRepositoryIT.java | 136 +++++++++++------- 1 file changed, 86 insertions(+), 50 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java index 68dee1555f72..c54487620c9f 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java @@ -1296,21 +1296,20 @@ public void usageReportsSearch_Site_mainReports() throws Exception { context.turnOffAuthorisationSystem(); Site site = SiteBuilder.createSite(context).build(); Item item = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Publication") .withTitle("My item") - .withType("Controlled Vocabulary for Resource Type Genres::image") .build(); Item item2 = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Patent") .withTitle("My item 2") - .withType("Controlled Vocabulary for Resource Type Genres::thesis") .build(); Item item3 = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Funding") .withTitle("My item 3") - .withType("Controlled Vocabulary for Resource Type Genres::thesis::bachelor thesis") .build(); Item item4 = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Project") .withTitle("My item 4") - .withType("Controlled Vocabulary for Resource Type Genres::text::periodical::" - + "journal::contribution to journal::journal article") .build(); context.restoreAuthSystemState(); @@ -1395,32 +1394,49 @@ public void usageReportsSearch_Site_mainReports() throws Exception { pointCountry.addValue("views", 5); pointCountry.setIdAndLabel(Locale.US.getCountry(), Locale.US.getDisplayCountry(context.getCurrentLocale())); - UsageReportPointCategoryRest articleCategory = new UsageReportPointCategoryRest(); - articleCategory.addValue("views", 1); - articleCategory.setId("article"); + UsageReportPointCategoryRest publicationCategory = new UsageReportPointCategoryRest(); + publicationCategory.addValue("views", 1); + publicationCategory.setId("publication"); - UsageReportPointCategoryRest thesisCategory = new UsageReportPointCategoryRest(); - thesisCategory.addValue("views", 3); - thesisCategory.setId("thesis"); + UsageReportPointCategoryRest patentCategory = new UsageReportPointCategoryRest(); + patentCategory.addValue("views", 2); + patentCategory.setId("patent"); - UsageReportPointCategoryRest otherCategory = new UsageReportPointCategoryRest(); - otherCategory.addValue("views", 1); - otherCategory.setId("other"); + UsageReportPointCategoryRest fundingCategory = new UsageReportPointCategoryRest(); + fundingCategory.addValue("views", 1); + fundingCategory.setId("funding"); - UsageReportPointCategoryRest bookCategory = new UsageReportPointCategoryRest(); - bookCategory.addValue("views", 0); - bookCategory.setId("book"); + UsageReportPointCategoryRest projectCategory = new UsageReportPointCategoryRest(); + projectCategory.addValue("views", 1); + projectCategory.setId("project"); - UsageReportPointCategoryRest bookChapterCategory = new UsageReportPointCategoryRest(); - bookChapterCategory.addValue("views", 0); - bookChapterCategory.setId("bookChapter"); + UsageReportPointCategoryRest productCategory = new UsageReportPointCategoryRest(); + productCategory.addValue("views", 0); + productCategory.setId("product"); - UsageReportPointCategoryRest datasetCategory = new UsageReportPointCategoryRest(); - datasetCategory.addValue("views", 0); - datasetCategory.setId("dataset"); + UsageReportPointCategoryRest journalCategory = new UsageReportPointCategoryRest(); + journalCategory.addValue("views", 0); + journalCategory.setId("journal"); - List categories = List.of(articleCategory, thesisCategory, otherCategory, bookCategory, - bookChapterCategory, datasetCategory); + UsageReportPointCategoryRest personCategory = new UsageReportPointCategoryRest(); + personCategory.addValue("views", 0); + personCategory.setId("person"); + + UsageReportPointCategoryRest orgUnitCategory = new UsageReportPointCategoryRest(); + orgUnitCategory.addValue("views", 0); + orgUnitCategory.setId("orgunit"); + + UsageReportPointCategoryRest equipmentCategory = new UsageReportPointCategoryRest(); + equipmentCategory.addValue("views", 0); + equipmentCategory.setId("equipment"); + + UsageReportPointCategoryRest eventCategory = new UsageReportPointCategoryRest(); + eventCategory.addValue("views", 0); + eventCategory.setId("event"); + + List categories = List.of(publicationCategory, patentCategory, fundingCategory, + projectCategory, productCategory, journalCategory, personCategory, orgUnitCategory, + equipmentCategory, eventCategory); // And request the sites global usage report (show top most popular items) getClient(adminToken) @@ -2445,6 +2461,11 @@ public void usageReportsSearch_OrgUnitWithPublicationVisited() throws Exception public void usageReportsSearch_Collection_ItemReports() throws Exception { context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).build(); + collectionNotVisited = CollectionBuilder.createCollection(context, community) + .withEntityType("Publication") + .build(); + Item item = ItemBuilder.createItem(context, collectionNotVisited) .withTitle("My item") .withType("Controlled Vocabulary for Resource Type Genres::image") @@ -2701,21 +2722,20 @@ public void usageReportsSearch_Community_ItemReports() throws Exception { collectionNotVisited = CollectionBuilder.createCollection(context, community).build(); Item item = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Publication") .withTitle("My item") - .withType("Controlled Vocabulary for Resource Type Genres::image") .build(); Item item2 = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Patent") .withTitle("My item 2") - .withType("Controlled Vocabulary for Resource Type Genres::thesis") .build(); Item item3 = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Funding") .withTitle("My item 3") - .withType("Controlled Vocabulary for Resource Type Genres::thesis::bachelor thesis") .build(); Item item4 = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Project") .withTitle("My item 4") - .withType("Controlled Vocabulary for Resource Type Genres::text::periodical::" - + "journal::contribution to journal::journal article") .build(); context.restoreAuthSystemState(); @@ -2800,33 +2820,49 @@ public void usageReportsSearch_Community_ItemReports() throws Exception { pointCountry.addValue("views", 5); pointCountry.setIdAndLabel(Locale.US.getCountry(), Locale.US.getDisplayCountry(context.getCurrentLocale())); - UsageReportPointCategoryRest articleCategory = new UsageReportPointCategoryRest(); - articleCategory.addValue("views", 1); - articleCategory.setId("article"); + UsageReportPointCategoryRest publicationCategory = new UsageReportPointCategoryRest(); + publicationCategory.addValue("views", 1); + publicationCategory.setId("publication"); - UsageReportPointCategoryRest thesisCategory = new UsageReportPointCategoryRest(); - thesisCategory.addValue("views", 3); - thesisCategory.setId("thesis"); + UsageReportPointCategoryRest patentCategory = new UsageReportPointCategoryRest(); + patentCategory.addValue("views", 2); + patentCategory.setId("patent"); - UsageReportPointCategoryRest otherCategory = new UsageReportPointCategoryRest(); - otherCategory.addValue("views", 1); - otherCategory.setId("other"); + UsageReportPointCategoryRest fundingCategory = new UsageReportPointCategoryRest(); + fundingCategory.addValue("views", 1); + fundingCategory.setId("funding"); - UsageReportPointCategoryRest bookCategory = new UsageReportPointCategoryRest(); - bookCategory.addValue("views", 0); - bookCategory.setId("book"); + UsageReportPointCategoryRest projectCategory = new UsageReportPointCategoryRest(); + projectCategory.addValue("views", 1); + projectCategory.setId("project"); - UsageReportPointCategoryRest bookChapterCategory = new UsageReportPointCategoryRest(); - bookChapterCategory.addValue("views", 0); - bookChapterCategory.setId("bookChapter"); + UsageReportPointCategoryRest productCategory = new UsageReportPointCategoryRest(); + productCategory.addValue("views", 0); + productCategory.setId("product"); - UsageReportPointCategoryRest datasetCategory = new UsageReportPointCategoryRest(); - datasetCategory.addValue("views", 0); - datasetCategory.setId("dataset"); + UsageReportPointCategoryRest journalCategory = new UsageReportPointCategoryRest(); + journalCategory.addValue("views", 0); + journalCategory.setId("journal"); - List categories = List.of(articleCategory, thesisCategory, otherCategory, bookCategory, - bookChapterCategory, datasetCategory); + UsageReportPointCategoryRest personCategory = new UsageReportPointCategoryRest(); + personCategory.addValue("views", 0); + personCategory.setId("person"); + + UsageReportPointCategoryRest orgUnitCategory = new UsageReportPointCategoryRest(); + orgUnitCategory.addValue("views", 0); + orgUnitCategory.setId("orgunit"); + + UsageReportPointCategoryRest equipmentCategory = new UsageReportPointCategoryRest(); + equipmentCategory.addValue("views", 0); + equipmentCategory.setId("equipment"); + + UsageReportPointCategoryRest eventCategory = new UsageReportPointCategoryRest(); + eventCategory.addValue("views", 0); + eventCategory.setId("event"); + List categories = List.of(publicationCategory, patentCategory, fundingCategory, + projectCategory, productCategory, journalCategory, personCategory, orgUnitCategory, + equipmentCategory, eventCategory); // And request the collections global usage report (show top most popular items) getClient(adminToken) .perform(get("/api/statistics/usagereports/search/object") From 92fb4d3c1265a4370b6d408fe9fb8f966f1643ee Mon Sep 17 00:00:00 2001 From: eskander Date: Tue, 12 Sep 2023 20:14:28 +0300 Subject: [PATCH 34/81] [DSC-897] fixed broken IT method --- .../app/rest/StatisticsRestRepositoryIT.java | 58 ++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java index c54487620c9f..ac28124b4ad4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java @@ -1972,32 +1972,50 @@ public void usageReportsSearch_ItemNotVisited_AtTime() throws Exception { expectedPoint1.setType("item"); points.add(expectedPoint1); - UsageReportPointCategoryRest articleCategory = new UsageReportPointCategoryRest(); - articleCategory.addValue("views", 0); - articleCategory.setId("article"); - UsageReportPointCategoryRest thesisCategory = new UsageReportPointCategoryRest(); - thesisCategory.addValue("views", 0); - thesisCategory.setId("thesis"); + UsageReportPointCategoryRest publicationCategory = new UsageReportPointCategoryRest(); + publicationCategory.addValue("views", 0); + publicationCategory.setId("publication"); - UsageReportPointCategoryRest otherCategory = new UsageReportPointCategoryRest(); - otherCategory.addValue("views", 0); - otherCategory.setId("other"); + UsageReportPointCategoryRest patentCategory = new UsageReportPointCategoryRest(); + patentCategory.addValue("views", 0); + patentCategory.setId("patent"); - UsageReportPointCategoryRest bookCategory = new UsageReportPointCategoryRest(); - bookCategory.addValue("views", 0); - bookCategory.setId("book"); + UsageReportPointCategoryRest fundingCategory = new UsageReportPointCategoryRest(); + fundingCategory.addValue("views", 0); + fundingCategory.setId("funding"); - UsageReportPointCategoryRest bookChapterCategory = new UsageReportPointCategoryRest(); - bookChapterCategory.addValue("views", 0); - bookChapterCategory.setId("bookChapter"); + UsageReportPointCategoryRest projectCategory = new UsageReportPointCategoryRest(); + projectCategory.addValue("views", 0); + projectCategory.setId("project"); - UsageReportPointCategoryRest datasetCategory = new UsageReportPointCategoryRest(); - datasetCategory.addValue("views", 0); - datasetCategory.setId("dataset"); + UsageReportPointCategoryRest productCategory = new UsageReportPointCategoryRest(); + productCategory.addValue("views", 0); + productCategory.setId("product"); - List categories = List.of(articleCategory, thesisCategory, otherCategory, bookCategory, - bookChapterCategory, datasetCategory); + UsageReportPointCategoryRest journalCategory = new UsageReportPointCategoryRest(); + journalCategory.addValue("views", 0); + journalCategory.setId("journal"); + + UsageReportPointCategoryRest personCategory = new UsageReportPointCategoryRest(); + personCategory.addValue("views", 0); + personCategory.setId("person"); + + UsageReportPointCategoryRest orgUnitCategory = new UsageReportPointCategoryRest(); + orgUnitCategory.addValue("views", 0); + orgUnitCategory.setId("orgunit"); + + UsageReportPointCategoryRest equipmentCategory = new UsageReportPointCategoryRest(); + equipmentCategory.addValue("views", 0); + equipmentCategory.setId("equipment"); + + UsageReportPointCategoryRest eventCategory = new UsageReportPointCategoryRest(); + eventCategory.addValue("views", 0); + eventCategory.setId("event"); + + List categories = List.of(publicationCategory, patentCategory, fundingCategory, + projectCategory, productCategory, journalCategory, personCategory, orgUnitCategory, + equipmentCategory, eventCategory); UsageReportPointRest pointPerMonth = new UsageReportPointDateRest(); pointPerMonth.setId("June 2019"); From a11f60bbaa5eb18b839aa4fed0afb509206f43d5 Mon Sep 17 00:00:00 2001 From: eskander Date: Wed, 13 Sep 2023 14:44:15 +0300 Subject: [PATCH 35/81] [DSC-897] separated the configuration of statistics of all entity types of collection. --- .../AbstractUsageReportGenerator.java | 26 -- .../StatisticsReportsConfiguration.java | 18 ++ .../statistics/TopCategoriesGenerator.java | 41 +-- .../rest/statistics/UsageReportGenerator.java | 6 - .../app/rest/utils/UsageReportUtils.java | 14 +- .../app/rest/StatisticsRestRepositoryIT.java | 2 +- dspace/config/spring/rest/statistics.xml | 249 ++++++++++++++++-- 7 files changed, 263 insertions(+), 93 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java index 368733f2ad0a..23c8f99e5857 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java @@ -7,11 +7,6 @@ */ package org.dspace.app.rest.statistics; -import java.util.List; - -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; - /** * This is an abstract class that adds common configurable options for the generator * @@ -21,7 +16,6 @@ public abstract class AbstractUsageReportGenerator implements UsageReportGenerat private String viewMode = "table"; private int maxResults = 100; private String relation; - private List excludedEntityTypes; public void setViewMode(String viewMode) { this.viewMode = viewMode; @@ -47,25 +41,5 @@ public void setRelation(String relation) { this.relation = relation; } - public boolean isApplicable(String entityType) { - - if (StringUtils.isEmpty(entityType)) { - return true; - } - - if (CollectionUtils.isNotEmpty(excludedEntityTypes)) { - return !excludedEntityTypes.contains(entityType); - } - - return true; - } - - public List getExcludedEntityTypes() { - return excludedEntityTypes; - } - - public void setExcludedEntityTypes(List excludedEntityTypes) { - this.excludedEntityTypes = excludedEntityTypes; - } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/StatisticsReportsConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/StatisticsReportsConfiguration.java index 3e366f7cc9de..8fef2b35853a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/StatisticsReportsConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/StatisticsReportsConfiguration.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Optional; +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.UsageReportCategoryRest; import org.dspace.content.Bitstream; import org.dspace.content.Collection; @@ -40,6 +41,13 @@ public List getCategories(DSpaceObject dso) { } else if (dso instanceof Community) { return mapping.get("community"); } else if (dso instanceof Collection) { + String entityType = getEntityType(dso); + if (StringUtils.isNotEmpty(entityType)) { + List result = mapping.get("collection-" + entityType); + if (result != null) { + return result; + } + } return mapping.get("collection"); } else if (dso instanceof Item) { Item item = (Item) dso; @@ -59,6 +67,16 @@ public List getCategories(DSpaceObject dso) { return null; } + private String getEntityType(DSpaceObject dso) { + return dso.getMetadata() + .stream() + .filter(metadataValue -> + "dspace.entity.type".equals(metadataValue.getMetadataField().toString('.'))) + .map(MetadataValue::getValue) + .findFirst() + .orElse(""); + } + public UsageReportGenerator getReportGenerator(DSpaceObject dso, String reportId) { List categories = getCategories(dso); Optional cat = categories.stream().filter(x -> x.getReports().containsKey(reportId)) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java index e0ced24e0eb2..fbba8f902ee1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java @@ -22,7 +22,6 @@ import org.dspace.app.rest.model.UsageReportRest; import org.dspace.app.rest.utils.UsageReportUtils; import org.dspace.content.DSpaceObject; -import org.dspace.content.MetadataValue; import org.dspace.core.Context; import org.dspace.discovery.configuration.DiscoveryConfiguration; import org.dspace.discovery.configuration.DiscoveryConfigurationService; @@ -51,11 +50,8 @@ public class TopCategoriesGenerator extends AbstractUsageReportGenerator { private Map categoryQueries; - private Map> entityCategoryQueries; - public UsageReportRest createUsageReport(Context context, DSpaceObject dso, String startDate, String endDate) { - setCategoryQueries(getEntityType(dso)); Map categoriesCount = getCategoriesCount(dso, startDate, endDate); UsageReportRest usageReportRest = new UsageReportRest(); @@ -76,8 +72,8 @@ private Map getCategoriesCount(DSpaceObject dso, String startDa Map categoriesCount = new HashMap(); - for (String category : categoryQueries.keySet()) { - String categoryQuery = categoryQueries.get(category); + for (String category : getCategoryQueries().keySet()) { + String categoryQuery = getCategoryQueries().get(category); Integer categoryCount = getCategoryCount(dso, discoveryConfiguration, categoryQuery, startDate, endDate); categoriesCount.put(category, categoryCount); } @@ -114,7 +110,7 @@ private String composeCategoryQuery(DSpaceObject dso, DiscoveryConfiguration con } private String getAllCategoryQueriesReverted() { - return categoryQueries.values().stream() + return getCategoryQueries().values().stream() .filter(categoryQuery -> !OTHER_CATEGORY.equals(categoryQuery)) .map(categoryQuery -> "-" + formatCategoryQuery(categoryQuery)) .collect(Collectors.joining(" AND ")); @@ -138,23 +134,15 @@ public String getReportType() { return UsageReportUtils.TOP_CATEGORIES_REPORT_ID; } - private String getEntityType(DSpaceObject dso) { - return dso.getMetadata() - .stream() - .filter(metadataValue -> - "dspace.entity.type".equals(metadataValue.getMetadataField().toString('.'))) - .map(MetadataValue::getValue) - .findFirst() - .orElse(""); + public Map getCategoryQueries() { + if (categoryQueries == null) { + return getDefaultCategoryQueries(); + } + return categoryQueries; } - private void setCategoryQueries(String entityType) { - if (entityCategoryQueries != null && - entityCategoryQueries.containsKey(entityType)) { - categoryQueries = entityCategoryQueries.get(entityType); - } else { - categoryQueries = getDefaultCategoryQueries(); - } + public void setCategoryQueries(Map categoryQueries) { + this.categoryQueries = categoryQueries; } private Map getDefaultCategoryQueries() { @@ -169,13 +157,4 @@ private String[] getDefaultEntityTypes() { return configurationService.getArrayProperty("cris.entity-type"); } - public Map> getEntityCategoryQueries() { - return entityCategoryQueries; - } - - public void setEntityCategoryQueries( - Map> entityCategoryQueries) { - this.entityCategoryQueries = entityCategoryQueries; - } - } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/UsageReportGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/UsageReportGenerator.java index 70e0bd499cae..ee542785c8cb 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/UsageReportGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/UsageReportGenerator.java @@ -45,10 +45,4 @@ public UsageReportRest createUsageReport(Context context, */ public String getViewMode(); - /** - * - * @return true if the report applicable for the provided entity type - */ - public boolean isApplicable(String entityType); - } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java index 199f57b33405..47dbcef1749d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java @@ -27,7 +27,6 @@ import org.dspace.content.Bitstream; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; -import org.dspace.content.MetadataValue; import org.dspace.content.Site; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -127,8 +126,7 @@ public List getUsageReportsOfDSO(Context context, for (UsageReportCategoryRest cat : categories) { if (category == null || StringUtils.equals(cat.getId(), category)) { for (Entry entry : cat.getReports().entrySet()) { - if (!reportIds.contains(entry.getKey()) && - entry.getValue().isApplicable(getEntityType(dso))) { + if (!reportIds.contains(entry.getKey())) { reportIds.add(entry.getKey()); reports.add(createUsageReport(context, dso, entry.getKey(), startDate, endDate)); } @@ -138,16 +136,6 @@ public List getUsageReportsOfDSO(Context context, return reports; } - private String getEntityType(DSpaceObject dso) { - return dso.getMetadata() - .stream() - .filter(metadataValue -> - "dspace.entity.type".equals(metadataValue.getMetadataField().toString('.'))) - .map(MetadataValue::getValue) - .findFirst() - .orElse(""); - } - private List getReports(Context context, DSpaceObject dso, String category) { List reports = new ArrayList(); if (dso instanceof Site) { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java index ac28124b4ad4..b0d740142c9d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java @@ -2614,7 +2614,7 @@ public void usageReportsSearch_Collection_ItemReports() throws Exception { // And request the collections global usage report (show top most popular items) getClient(adminToken) .perform(get("/api/statistics/usagereports/search/object") - .param("category", "collection-itemReports") + .param("category", "publicationCollection-itemReports") .param("uri", "http://localhost:8080/server/api/core/collections/" + collectionNotVisited.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.usagereports", not(empty()))) diff --git a/dspace/config/spring/rest/statistics.xml b/dspace/config/spring/rest/statistics.xml index f5028b6e3e6d..2ec3f3be3eed 100644 --- a/dspace/config/spring/rest/statistics.xml +++ b/dspace/config/spring/rest/statistics.xml @@ -441,23 +441,63 @@ - - - Person - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -503,6 +543,10 @@ + + + + @@ -833,6 +877,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -922,6 +1083,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fc82fa80dbc44696d91f43ffe0b47ceb7941e593 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 13 Sep 2023 16:34:05 +0200 Subject: [PATCH 36/81] [DSC-1072] Refactored ternary operator with optional --- .../security/CrisSecurityServiceImpl.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java index 3b92ca5985f4..99add81e862b 100644 --- a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import org.apache.commons.collections.CollectionUtils; @@ -18,7 +19,6 @@ import org.dspace.content.Collection; import org.dspace.content.Item; import org.dspace.content.MetadataValue; -import org.dspace.content.logic.Filter; import org.dspace.content.security.service.CrisSecurityService; import org.dspace.content.service.ItemService; import org.dspace.core.Context; @@ -56,19 +56,17 @@ public boolean hasAccess(Context context, Item item, EPerson user, AccessItemMod .anyMatch(security -> hasAccess(context, item, user, accessMode, security)); } - private boolean hasAccess(Context context, Item item, EPerson user, AccessItemMode accessMode, - CrisSecurity crisSecurity) { - + private boolean hasAccess( + Context context, Item item, EPerson user, AccessItemMode accessMode, CrisSecurity crisSecurity + ) { try { + final boolean checkSecurity = checkSecurity(context, item, user, accessMode, crisSecurity); - boolean checkSecurity = checkSecurity(context, item, user, accessMode, crisSecurity); - Filter additionalFilter = accessMode.getAdditionalFilter(); - - return additionalFilter == null ? checkSecurity - : checkSecurity && additionalFilter.getResult(context, item); - + return Optional.ofNullable(accessMode.getAdditionalFilter()) + .map(filter -> checkSecurity && filter.getResult(context, item)) + .orElse(checkSecurity); } catch (SQLException e) { - throw new RuntimeException(e); + throw new SQLRuntimeException(e); } } From 55d20afd0efa7bad8e9628565fe73c073940b273 Mon Sep 17 00:00:00 2001 From: "aliaksei.bykau" Date: Tue, 19 Sep 2023 15:45:03 +0200 Subject: [PATCH 37/81] [DSC-1240] updated default doi metadata and added property for setting doi metadata --- .../identifier/DOIIdentifierProvider.java | 19 ++++- .../identifier/DOIIdentifierProviderTest.java | 72 +++++++++---------- dspace/config/dspace.cfg | 2 + 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java index b70eda960d35..e562398b2324 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import javax.annotation.PostConstruct; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; @@ -67,13 +68,14 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { static final String CFG_PREFIX = "identifier.doi.prefix"; static final String CFG_NAMESPACE_SEPARATOR = "identifier.doi.namespaceseparator"; + static final String DOI_METADATA = "identifier.doi.metadata"; static final char SLASH = '/'; // Metadata field name elements // TODO: move these to MetadataSchema or some such? - public static final String MD_SCHEMA = "dc"; - public static final String DOI_ELEMENT = "identifier"; - public static final String DOI_QUALIFIER = "uri"; + public String MD_SCHEMA = "dc"; + public String DOI_ELEMENT = "identifier"; + public String DOI_QUALIFIER = "doi"; // The DOI is queued for registered with the service provider public static final Integer TO_BE_REGISTERED = 1; // The DOI is queued for reservation with the service provider @@ -170,6 +172,17 @@ protected String getNamespaceSeparator() { return this.NAMESPACE_SEPARATOR; } + @PostConstruct + protected void setDoiMetadata() { + String doiMetadata = this.configurationService.getProperty(DOI_METADATA); + if (doiMetadata != null) { + String[] parts = doiMetadata.split("\\."); + this.MD_SCHEMA = parts[0]; + this.DOI_ELEMENT = parts[1]; + this.DOI_QUALIFIER = parts[2]; + } + } + /** * Set the DOI connector, which is the component that commuincates with the remote registration service * (eg. DataCite, EZID, Crossref) diff --git a/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java b/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java index db2be516ae49..4241ba26f223 100644 --- a/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java @@ -187,9 +187,9 @@ private Item newItem() provider.delete(context, item); List metadata = itemService.getMetadata(item, - DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); List remainder = new ArrayList<>(); @@ -200,13 +200,13 @@ private Item newItem() } itemService.clearMetadata(context, item, - DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); - itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + itemService.addMetadata(context, item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null, remainder); @@ -252,9 +252,9 @@ public String createDOI(Item item, Integer status, boolean metadata, String doi) doiService.update(context, doiRow); if (metadata) { - itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + itemService.addMetadata(context, item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null, doiService.DOIToExternalForm(doi)); itemService.update(context, item); @@ -315,9 +315,9 @@ public void testStore_DOI_as_item_metadata() provider.saveDOIToObject(context, item, doi); context.restoreAuthSystemState(); - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean result = false; for (MetadataValue id : metadata) { @@ -337,9 +337,9 @@ public void testGet_DOI_out_of_item_metadata() + Long.toHexString(new Date().getTime()); context.turnOffAuthorisationSystem(); - itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + itemService.addMetadata(context, item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null, doiService.DOIToExternalForm(doi)); itemService.update(context, item); @@ -358,9 +358,9 @@ public void testRemove_DOI_from_item_metadata() + Long.toHexString(new Date().getTime()); context.turnOffAuthorisationSystem(); - itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + itemService.addMetadata(context, item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null, doiService.DOIToExternalForm(doi)); itemService.update(context, item); @@ -368,9 +368,9 @@ public void testRemove_DOI_from_item_metadata() provider.removeDOIFromObject(context, item, doi); context.restoreAuthSystemState(); - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean foundDOI = false; for (MetadataValue id : metadata) { @@ -456,9 +456,9 @@ public void testRemove_two_DOIs_from_item_metadata() context.restoreAuthSystemState(); // assure that the right one was removed - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean foundDOI1 = false; boolean foundDOI2 = false; @@ -480,9 +480,9 @@ public void testRemove_two_DOIs_from_item_metadata() context.restoreAuthSystemState(); // check it - metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); foundDOI1 = false; foundDOI2 = false; @@ -691,9 +691,9 @@ public void testDelete_specified_DOI() context.restoreAuthSystemState(); // assure that the right one was removed - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean foundDOI1 = false; boolean foundDOI2 = false; @@ -733,9 +733,9 @@ public void testDelete_all_DOIs() context.restoreAuthSystemState(); // assure that the right one was removed - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean foundDOI1 = false; boolean foundDOI2 = false; diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 7db34a16b978..bbef643512de 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -281,6 +281,8 @@ identifier.doi.prefix = 10.5072 # it from other services also minting DOIs under your prefix? identifier.doi.namespaceseparator = dspace/ +identifier.doi.metadata = dc.identifier.doi + ##### Edit Item configurations ##### # This configuration allows to set a group that will able to # use edit metadata mode From 4efae73ab889227991261f83554f98b9c17fcaf2 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 20 Sep 2023 10:59:54 +0200 Subject: [PATCH 38/81] [DSC-1241] Bitstream restrictions not reflected on its thumbnail --- .../dspace/content/BitstreamServiceImpl.java | 62 +++++++++++++++++++ .../content/service/BitstreamService.java | 5 ++ ...rcePolicyEPersonReplaceRestController.java | 9 +++ ...ourcePolicyGroupReplaceRestController.java | 9 +++ .../ResourcePolicyRestRepository.java | 10 +++ 5 files changed, 95 insertions(+) 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 f2a8680ee58d..b07f23ee23ff 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -7,12 +7,15 @@ */ package org.dspace.content; +import static org.apache.commons.lang.StringUtils.startsWith; + import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Spliterators; import java.util.UUID; import java.util.regex.Pattern; @@ -606,4 +609,63 @@ private Stream streamOf(Iterator iterator) { return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); } + @Override + public boolean isOriginalBitstream(DSpaceObject dso) throws SQLException { + + if (dso.getType() != Constants.BITSTREAM) { + return false; + } + + Bitstream bitstream = (Bitstream) dso; + + return bitstream.getBundles().stream() + .anyMatch(bundle -> "ORIGINAL".equals(bundle.getName())); + + } + + @Override + public void updateThumbnailResourcePolicies(Context context, Bitstream bitstream) throws SQLException { + getThumbnail(bitstream) + .ifPresent(thumbnail -> replacePolicies(context, bitstream, thumbnail)); + } + + private void replacePolicies(Context context, Bitstream bitstream, Bitstream thumbnail) { + try { + authorizeService.replaceAllPolicies(context, bitstream, thumbnail); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e); + } + } + + private Optional getThumbnail(Bitstream bitstream) throws SQLException { + return getItem(bitstream) + .flatMap(item -> getThumbnail(item, bitstream.getName())); + } + + private Optional getItem(Bitstream bitstream) throws SQLException { + return bitstream.getBundles().stream() + .flatMap(bundle -> bundle.getItems().stream()) + .findFirst(); + } + + private Optional getThumbnail(Item item, String name) { + List bundles = getThumbnailBundles(item); + if (CollectionUtils.isEmpty(bundles)) { + return Optional.empty(); + } + + return bundles.stream() + .flatMap(bundle -> bundle.getBitstreams().stream()) + .filter(bitstream -> startsWith(bitstream.getName(), name)) + .findFirst(); + } + + private List getThumbnailBundles(Item item) { + try { + return itemService.getBundles(item, "THUMBNAIL"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } 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 3f5b17630a27..85a4fd140e9a 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 @@ -22,6 +22,7 @@ import org.dspace.content.Bundle; import org.dspace.content.Collection; import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.core.Context; @@ -243,4 +244,8 @@ List findShowableByItem(Context context, UUID itemId, String bundleNa List findByItemAndBundleAndMetadata(Context context, Item item, String bundleName, Map filterMetadata); + boolean isOriginalBitstream(DSpaceObject dso) throws SQLException; + + void updateThumbnailResourcePolicies(Context context, Bitstream bitstream) throws SQLException; + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java index e772aa0abe18..b02869962156 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java @@ -23,7 +23,9 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.content.Bitstream; import org.dspace.content.DSpaceObject; +import org.dspace.content.service.BitstreamService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +53,8 @@ public class ResourcePolicyEPersonReplaceRestController { private Utils utils; @Autowired private ResourcePolicyService resourcePolicyService; + @Autowired + private BitstreamService bitstreamService; @PreAuthorize("hasPermission(#id, 'resourcepolicy', 'ADMIN')") @RequestMapping(method = PUT, consumes = {"text/uri-list"}) @@ -75,6 +79,11 @@ public ResponseEntity> replaceEPersonOfResourcePolicy(@Pa } EPerson newEPerson = (EPerson) dsoList.get(0); resourcePolicy.setEPerson(newEPerson); + + if (bitstreamService.isOriginalBitstream(resourcePolicy.getdSpaceObject())) { + bitstreamService.updateThumbnailResourcePolicies(context, (Bitstream) resourcePolicy.getdSpaceObject()); + } + context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java index e9ba0dff4429..40a82068dbce 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java @@ -23,7 +23,9 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.content.Bitstream; import org.dspace.content.DSpaceObject; +import org.dspace.content.service.BitstreamService; import org.dspace.core.Context; import org.dspace.eperson.Group; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +53,8 @@ public class ResourcePolicyGroupReplaceRestController { private Utils utils; @Autowired private ResourcePolicyService resourcePolicyService; + @Autowired + private BitstreamService bitstreamService; @PreAuthorize("hasPermission(#id, 'resourcepolicy', 'ADMIN')") @RequestMapping(method = PUT, consumes = {"text/uri-list"}) @@ -75,6 +79,11 @@ public ResponseEntity> replaceGroupOfResourcePolicy(@Path Group newGroup = (Group) dsoList.get(0); resourcePolicy.setGroup(newGroup); + + if (bitstreamService.isOriginalBitstream(resourcePolicy.getdSpaceObject())) { + bitstreamService.updateThumbnailResourcePolicies(context, (Bitstream) resourcePolicy.getdSpaceObject()); + } + context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java index 0b77f96b9b5f..72ca3f254256 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java @@ -30,7 +30,9 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.content.Bitstream; import org.dspace.content.DSpaceObject; +import org.dspace.content.service.BitstreamService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -76,6 +78,9 @@ public class ResourcePolicyRestRepository extends DSpaceRestRepository Date: Mon, 25 Sep 2023 13:52:41 +0200 Subject: [PATCH 40/81] [DSC-1246] change authentication-oidc.cfg --- dspace/config/modules/authentication-oidc.cfg | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dspace/config/modules/authentication-oidc.cfg b/dspace/config/modules/authentication-oidc.cfg index c9f4a3fdbc86..ed31e69af169 100644 --- a/dspace/config/modules/authentication-oidc.cfg +++ b/dspace/config/modules/authentication-oidc.cfg @@ -32,7 +32,9 @@ authentication-oidc.redirect-url = ${dspace.server.url}/api/authn/oidc # The scopes to request -authentication-oidc.scopes = openid,email,profile +authentication-oidc.scopes = openid +authentication-oidc.scopes = email +authentication-oidc.scopes = profile # Specify if the user can self register using OIDC (true|false). If not specified, true is assumed # This should match the configuration of the OIDC server you are using. The default setting for From bab8602c7f2bcbd741b6c6032e6af22fde66e9b3 Mon Sep 17 00:00:00 2001 From: "aliaksei.bykau" Date: Tue, 26 Sep 2023 10:20:29 +0200 Subject: [PATCH 41/81] [DSC-1240] made DOI_METADATA field in DOIIdentifierProvider private --- .../main/java/org/dspace/identifier/DOIIdentifierProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java index e562398b2324..4550a84b1c0a 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java @@ -68,7 +68,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { static final String CFG_PREFIX = "identifier.doi.prefix"; static final String CFG_NAMESPACE_SEPARATOR = "identifier.doi.namespaceseparator"; - static final String DOI_METADATA = "identifier.doi.metadata"; + private static final String DOI_METADATA = "identifier.doi.metadata"; static final char SLASH = '/'; // Metadata field name elements From 5f63749b2e0e5aa3a2dc8dd30d792f170d3b4f1c Mon Sep 17 00:00:00 2001 From: "aliaksei.bykau" Date: Tue, 26 Sep 2023 11:10:44 +0200 Subject: [PATCH 42/81] [DSC-1240] added description for identifier.doi.metadata in dspace.cfg --- dspace/config/dspace.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index bbef643512de..b4e729d2db3d 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -281,6 +281,8 @@ identifier.doi.prefix = 10.5072 # it from other services also minting DOIs under your prefix? identifier.doi.namespaceseparator = dspace/ +# if you want, you can specify custom metadata field for doi identifier +# if nothing specified, then will be used dc.identifier.doi as default identifier.doi.metadata = dc.identifier.doi ##### Edit Item configurations ##### From d7ca5e1fa729ee84c5af72f356642641e88bfb02 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 27 Sep 2023 11:23:24 +0200 Subject: [PATCH 43/81] [CST-11876] Fixed bulk import values split --- .../org/dspace/app/bulkedit/BulkImport.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImport.java index 491039cff835..6b1e6d42a095 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImport.java @@ -13,7 +13,7 @@ import static org.apache.commons.lang3.StringUtils.isAllBlank; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; -import static org.apache.commons.lang3.StringUtils.split; +import static org.apache.commons.lang3.StringUtils.splitByWholeSeparator; import static org.apache.commons.lang3.StringUtils.startsWith; import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; import static org.apache.commons.lang3.math.NumberUtils.isCreatable; @@ -609,7 +609,8 @@ private boolean areMetadataValuesValid(Row row, boolean manyMetadataValuesAllowe for (int index = firstMetadataIndex; index < row.getLastCellNum(); index++) { String cellValue = WorkbookUtils.getCellValue(row, index); - String[] values = isNotBlank(cellValue) ? split(cellValue, METADATA_SEPARATOR) : new String[] { "" }; + String[] values = isNotBlank(cellValue) ? splitByWholeSeparator(cellValue, METADATA_SEPARATOR) + : new String[] { "" }; if (values.length > 1 && !manyMetadataValuesAllowed) { handleValidationErrorOnRow(row, "Multiple metadata value on the same cell not allowed " + "in the metadata group sheets: " + cellValue); @@ -743,7 +744,7 @@ private List validateAccessConditions(Row row) { Map accessConditionOptions = getUploadAccessConditions(); return Arrays.stream(getAccessConditionValues(row)) - .map(accessCondition -> split(accessCondition, ACCESS_CONDITION_ATTRIBUTES_SEPARATOR)[0]) + .map(accessCondition -> splitByWholeSeparator(accessCondition, ACCESS_CONDITION_ATTRIBUTES_SEPARATOR)[0]) .filter(accessConditionName -> !accessConditionOptions.containsKey(accessConditionName)) .collect(Collectors.toList()); } @@ -788,14 +789,14 @@ private List buildAccessConditions(Row row, String[] accessCond } return Arrays.stream(accessConditions) - .map(accessCondition -> split(accessCondition, ACCESS_CONDITION_ATTRIBUTES_SEPARATOR)) + .map(accessCondition -> splitByWholeSeparator(accessCondition, ACCESS_CONDITION_ATTRIBUTES_SEPARATOR)) .map(accessConditionAttributes -> buildAccessCondition(accessConditionAttributes)) .collect(Collectors.toList()); } private String[] getAccessConditionValues(Row row) { String accessConditionCellValue = getCellValue(row, ACCESS_CONDITION_HEADER); - return split(accessConditionCellValue, METADATA_SEPARATOR); + return splitByWholeSeparator(accessConditionCellValue, METADATA_SEPARATOR); } private AccessCondition buildAccessCondition(String[] accessCondition) { @@ -1306,12 +1307,13 @@ private void removeSingleMetadata(DSpaceObject dso, MetadataField field, String } private String getMetadataField(String field) { - return field.contains(LANGUAGE_SEPARATOR_PREFIX) ? split(field, LANGUAGE_SEPARATOR_PREFIX)[0] : field; + return field.contains(LANGUAGE_SEPARATOR_PREFIX) ? splitByWholeSeparator(field, LANGUAGE_SEPARATOR_PREFIX)[0] + : field; } private String getMetadataLanguage(String field) { if (field.contains(LANGUAGE_SEPARATOR_PREFIX)) { - return split(field, LANGUAGE_SEPARATOR_PREFIX)[1].replace(LANGUAGE_SEPARATOR_SUFFIX, ""); + return splitByWholeSeparator(field, LANGUAGE_SEPARATOR_PREFIX)[1].replace(LANGUAGE_SEPARATOR_SUFFIX, ""); } return null; } @@ -1364,7 +1366,8 @@ private MultiValuedMap getMetadataFromRow(Row row, Map< if (index >= firstMetadataIndex) { String cellValue = WorkbookUtils.getCellValue(row, index); - String[] values = isNotBlank(cellValue) ? split(cellValue, METADATA_SEPARATOR) : new String[] { "" }; + String[] values = isNotBlank(cellValue) ? splitByWholeSeparator(cellValue, METADATA_SEPARATOR) + : new String[] { "" }; List metadataValues = Arrays.stream(values) .map(value -> buildMetadataValueVO(row, value, isMetadataGroupsSheet)) From bc8b5e053c45585a1b96ddc557203320453eb6fa Mon Sep 17 00:00:00 2001 From: "aliaksei.bykau" Date: Thu, 28 Sep 2023 09:33:01 +0200 Subject: [PATCH 44/81] [DSC-1243] added virtualField generator for primary and alt dois --- .../virtualfields/ItemDOIService.java | 59 ++++++++++++++++ .../VirtualFieldAlternativeDOI.java | 31 +++++++++ .../virtualfields/VirtualFieldPrimaryDOI.java | 31 +++++++++ .../app/DataciteDOIItemCompilePlugin.java | 69 +++++++++++++++++++ .../oai/metadataFormats/oai_openaire.xsl | 22 ++++++ .../template/patent-datacite-xml.template | 2 +- .../template/product-datacite-xml.template | 2 +- .../publication-datacite-xml.template | 2 +- dspace/config/spring/api/core-services.xml | 1 + dspace/config/spring/api/crosswalks.xml | 4 ++ dspace/config/spring/oai/oai.xml | 2 + 11 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/ItemDOIService.java create mode 100644 dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java create mode 100644 dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldPrimaryDOI.java create mode 100644 dspace-oai/src/main/java/org/dspace/xoai/app/DataciteDOIItemCompilePlugin.java diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/ItemDOIService.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/ItemDOIService.java new file mode 100644 index 000000000000..03229f634a6b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/ItemDOIService.java @@ -0,0 +1,59 @@ +/** + * 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.virtualfields; + +import java.util.Comparator; +import java.util.List; + +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + + +public class ItemDOIService { + static final String CFG_PREFIX = "identifier.doi.prefix"; + + static final String DOI_METADATA = "dc.identifier.doi"; + + @Autowired + protected ItemService itemService; + @Autowired + private ConfigurationService configurationService; + + public String[] getAlternativeDOIFromItem(Item item) { + List metadataValueList = itemService.getMetadataByMetadataString(item, DOI_METADATA); + return getAlternativeDOI(metadataValueList, getPrimaryDOI(metadataValueList)); + } + private String[] getAlternativeDOI(List metadataValueList, String primaryValue) { + return metadataValueList.stream().map(MetadataValue::getValue) + .filter(value -> !value.equals(primaryValue)).toArray(String[]::new); + } + + public String getPrimaryDOIFromItem(Item item) { + return getPrimaryDOI(itemService.getMetadataByMetadataString(item, DOI_METADATA)); + } + + private String getPrimaryDOI(List metadataValueList) { + return metadataValueList.stream().filter(metadata -> metadata.getValue().contains(getPrefix())) + .min(Comparator.comparingInt(MetadataValue::getPlace)).map(MetadataValue::getValue) + .orElse(!metadataValueList.isEmpty() ? metadataValueList.get(0).getValue() : null); + } + + protected String getPrefix() { + String prefix; + prefix = this.configurationService.getProperty(CFG_PREFIX); + if (null == prefix) { + throw new RuntimeException("Unable to load DOI prefix from " + + "configuration. Cannot find property " + + CFG_PREFIX + "."); + } + return prefix; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java new file mode 100644 index 000000000000..93bc83131e3c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java @@ -0,0 +1,31 @@ +/** + * 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.virtualfields; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; + + +public class VirtualFieldAlternativeDOI implements VirtualField { + + @Autowired + private ItemDOIService itemDOIService; + + @Override + public String[] getMetadata(Context context, Item item, String fieldName) { + String[] qualifiers = StringUtils.split(fieldName, "."); + if (qualifiers.length != 3) { + throw new IllegalArgumentException("Invalid field name " + fieldName); + } + + String metadataField = StringUtils.replaceAll(qualifiers[2], "-", "."); + return itemDOIService.getAlternativeDOIFromItem(item); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldPrimaryDOI.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldPrimaryDOI.java new file mode 100644 index 000000000000..059f9fd2c448 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldPrimaryDOI.java @@ -0,0 +1,31 @@ +/** + * 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.virtualfields; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; + + +public class VirtualFieldPrimaryDOI implements VirtualField { + + @Autowired + private ItemDOIService itemDOIService; + + @Override + public String[] getMetadata(Context context, Item item, String fieldName) { + String[] qualifiers = StringUtils.split(fieldName, "."); + if (qualifiers.length != 3) { + throw new IllegalArgumentException("Invalid field name " + fieldName); + } + + String metadataField = StringUtils.replaceAll(qualifiers[2], "-", "."); + return new String[] {itemDOIService.getPrimaryDOIFromItem(item)}; + } +} diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/DataciteDOIItemCompilePlugin.java b/dspace-oai/src/main/java/org/dspace/xoai/app/DataciteDOIItemCompilePlugin.java new file mode 100644 index 000000000000..b56ab779b265 --- /dev/null +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/DataciteDOIItemCompilePlugin.java @@ -0,0 +1,69 @@ +/** + * 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.xoai.app; + +import java.util.Arrays; +import java.util.List; + +import com.lyncode.xoai.dataprovider.xml.xoai.Element; +import com.lyncode.xoai.dataprovider.xml.xoai.Metadata; +import org.apache.commons.lang.StringUtils; +import org.dspace.content.Item; +import org.dspace.content.integration.crosswalks.virtualfields.ItemDOIService; +import org.dspace.core.Context; +import org.dspace.xoai.util.ItemUtils; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * XOAIExtensionItemCompilePlugin aims to add structured information about the + * creative commons license applied to the item (if any). + * The xoai document will be enriched with a structure like that + * + * + * + * + * + * + * + * + * + */ +public class DataciteDOIItemCompilePlugin implements XOAIExtensionItemCompilePlugin { + + @Autowired + private ItemDOIService itemDOIService; + + @Override + public Metadata additionalMetadata(Context context, Metadata metadata, Item item) { + String primaryDoiValue = itemDOIService.getPrimaryDOIFromItem(item); + String[] alternativeDoiValue = itemDOIService.getAlternativeDOIFromItem(item); + Element datacite = ItemUtils.create("datacite"); + if (StringUtils.isNotBlank(primaryDoiValue)) { + Element primary = ItemUtils.create("primary"); + datacite.getElement().add(primary); + primary.getField().add(ItemUtils.createValue("doi", primaryDoiValue)); + if (alternativeDoiValue != null && alternativeDoiValue.length != 0) { + Element alternative = ItemUtils.create("alternative"); + datacite.getElement().add(alternative); + Arrays.stream(alternativeDoiValue) + .forEach(value -> alternative.getField().add(ItemUtils.createValue("doi", value))); + } + Element other; + List elements = metadata.getElement(); + if (ItemUtils.getElement(elements, "others") != null) { + other = ItemUtils.getElement(elements, "others"); + } else { + other = ItemUtils.create("others"); + } + other.getElement().add(datacite); + } + return metadata; + } + +} diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 7b66eaf04372..c617223cc893 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -1668,6 +1668,28 @@ + + + + doi + + + + + + + + + + + doi + + + + + + diff --git a/dspace/config/crosswalks/template/patent-datacite-xml.template b/dspace/config/crosswalks/template/patent-datacite-xml.template index 22b400e96926..2ae1d662e5f5 100644 --- a/dspace/config/crosswalks/template/patent-datacite-xml.template +++ b/dspace/config/crosswalks/template/patent-datacite-xml.template @@ -1,6 +1,6 @@ - @dc.identifier.doi@ + @virtual.primary-doi.dc-identifier-doi@ @group.dc-contributor-author.start@ diff --git a/dspace/config/crosswalks/template/product-datacite-xml.template b/dspace/config/crosswalks/template/product-datacite-xml.template index 50224aa35cca..2db9b247145d 100644 --- a/dspace/config/crosswalks/template/product-datacite-xml.template +++ b/dspace/config/crosswalks/template/product-datacite-xml.template @@ -1,6 +1,6 @@ - @dc.identifier.doi@ + @virtual.primary-doi.dc-identifier-doi@ @group.dc-contributor-author.start@ diff --git a/dspace/config/crosswalks/template/publication-datacite-xml.template b/dspace/config/crosswalks/template/publication-datacite-xml.template index 22b400e96926..2ae1d662e5f5 100644 --- a/dspace/config/crosswalks/template/publication-datacite-xml.template +++ b/dspace/config/crosswalks/template/publication-datacite-xml.template @@ -1,6 +1,6 @@ - @dc.identifier.doi@ + @virtual.primary-doi.dc-identifier-doi@ @group.dc-contributor-author.start@ diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index a999b7d7bda7..2b3d1541fe1d 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -40,6 +40,7 @@ + diff --git a/dspace/config/spring/api/crosswalks.xml b/dspace/config/spring/api/crosswalks.xml index 504645bd83b6..d9d842a231ff 100644 --- a/dspace/config/spring/api/crosswalks.xml +++ b/dspace/config/spring/api/crosswalks.xml @@ -522,6 +522,8 @@ + + @@ -538,6 +540,8 @@ + + diff --git a/dspace/config/spring/oai/oai.xml b/dspace/config/spring/oai/oai.xml index 8fbc01ede5ce..d5d735b2f328 100644 --- a/dspace/config/spring/oai/oai.xml +++ b/dspace/config/spring/oai/oai.xml @@ -31,6 +31,8 @@ the fulltext access condition --> + + + + + + @@ -1668,25 +1676,23 @@ - doi - + - - - - + + + doi - diff --git a/dspace/config/crosswalks/template/patent-datacite-xml.template b/dspace/config/crosswalks/template/patent-datacite-xml.template index 2ae1d662e5f5..9f31693eb4e2 100644 --- a/dspace/config/crosswalks/template/patent-datacite-xml.template +++ b/dspace/config/crosswalks/template/patent-datacite-xml.template @@ -28,6 +28,7 @@ @dc.identifier.uri@ + @virtual.alternative-doi.dc-identifier-doi@ @dc.description.version@ diff --git a/dspace/config/crosswalks/template/product-datacite-xml.template b/dspace/config/crosswalks/template/product-datacite-xml.template index 2db9b247145d..414527ee3378 100644 --- a/dspace/config/crosswalks/template/product-datacite-xml.template +++ b/dspace/config/crosswalks/template/product-datacite-xml.template @@ -27,6 +27,7 @@ @dc.identifier.uri@ + @virtual.alternative-doi.dc-identifier-doi@ @dc.description.version@ diff --git a/dspace/config/crosswalks/template/publication-datacite-xml.template b/dspace/config/crosswalks/template/publication-datacite-xml.template index 2ae1d662e5f5..9f31693eb4e2 100644 --- a/dspace/config/crosswalks/template/publication-datacite-xml.template +++ b/dspace/config/crosswalks/template/publication-datacite-xml.template @@ -28,6 +28,7 @@ @dc.identifier.uri@ + @virtual.alternative-doi.dc-identifier-doi@ @dc.description.version@ From 973136305b03c4d8359e9b0e080565ccb8a5c5ad Mon Sep 17 00:00:00 2001 From: corrado lombardi Date: Mon, 2 Oct 2023 16:46:10 +0200 Subject: [PATCH 49/81] [CST-12020] updated context retrieval process --- .../main/java/org/dspace/content/authority/ItemAuthority.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java index 6ec39db9764f..0779a294aef2 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java @@ -181,9 +181,8 @@ private List getChoiceListFromQueryResults(SolrDocumentList results, Str public String getLabel(String key, String locale) { String title = key; if (key != null) { - Context context = null; + Context context = getContext(); try { - context = new Context(); DSpaceObject dso = itemService.find(context, UUIDUtils.fromString(key)); if (dso != null) { title = dso.getName(); From 5670b24b2c43b2799ef44980a87efbbb69ffffa3 Mon Sep 17 00:00:00 2001 From: corrado lombardi Date: Mon, 2 Oct 2023 17:41:16 +0200 Subject: [PATCH 50/81] ported getContext method --- .../java/org/dspace/content/authority/ItemAuthority.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java index 0779a294aef2..7a744a2515c6 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java @@ -42,6 +42,7 @@ import org.dspace.util.ItemAuthorityUtils; import org.dspace.util.UUIDUtils; import org.dspace.utils.DSpace; +import org.dspace.web.ContextUtil; /** * Sample authority to link a dspace item with another (i.e a publication with @@ -291,4 +292,9 @@ private boolean hasValidExternalSource(String sourceIdentifier) { return false; } + private Context getContext() { + Context context = ContextUtil.obtainCurrentRequestContext(); + return context != null ? context : new Context(); + } + } From 48d1b75ac4f30e9ae5fa83ec50820bff20c85372 Mon Sep 17 00:00:00 2001 From: "aliaksei.bykau" Date: Mon, 2 Oct 2023 19:02:57 +0200 Subject: [PATCH 51/81] [DSC-1243] added fixes according to the feedback and added tests for the VirtualFieldAlternativeDOI and VirtualFieldPrimaryDOI --- .../VirtualFieldAlternativeDOI.java | 1 - .../virtualfields/VirtualFieldPrimaryDOI.java | 1 - .../template/virtual-field-doi-json.template | 4 + .../config/spring/api/test-beans.xml | 10 +++ .../crosswalks/ReferCrosswalkIT.java | 82 +++++++++++++++++++ .../app/DataciteDOIItemCompilePlugin.java | 14 +++- 6 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-doi-json.template diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java index 93bc83131e3c..3966566196cb 100644 --- a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java @@ -25,7 +25,6 @@ public String[] getMetadata(Context context, Item item, String fieldName) { throw new IllegalArgumentException("Invalid field name " + fieldName); } - String metadataField = StringUtils.replaceAll(qualifiers[2], "-", "."); return itemDOIService.getAlternativeDOIFromItem(item); } } diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldPrimaryDOI.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldPrimaryDOI.java index 059f9fd2c448..3039ded0df84 100644 --- a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldPrimaryDOI.java +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldPrimaryDOI.java @@ -25,7 +25,6 @@ public String[] getMetadata(Context context, Item item, String fieldName) { throw new IllegalArgumentException("Invalid field name " + fieldName); } - String metadataField = StringUtils.replaceAll(qualifiers[2], "-", "."); return new String[] {itemDOIService.getPrimaryDOIFromItem(item)}; } } diff --git a/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-doi-json.template b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-doi-json.template new file mode 100644 index 000000000000..841a6a03fbd3 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-doi-json.template @@ -0,0 +1,4 @@ +{ + "primary-doi": "@virtual.primary-doi.dc-identifier-doi@", + "alternative-doi": "@virtual.alternative-doi.dc-identifier-doi@", +} \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml index 87cc17de18a9..8daa7cd4ae02 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml @@ -22,6 +22,16 @@ + + + + + + + + + + diff --git a/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java index b4ecc73a0c46..bd01d3519bc5 100644 --- a/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java +++ b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java @@ -69,6 +69,7 @@ import org.dspace.eperson.EPerson; import org.dspace.layout.CrisLayoutBox; import org.dspace.layout.LayoutSecurity; +import org.dspace.services.ConfigurationService; import org.dspace.utils.DSpace; import org.json.JSONObject; import org.junit.After; @@ -82,6 +83,7 @@ * */ public class ReferCrosswalkIT extends AbstractIntegrationTestWithDatabase { + static final String CFG_PREFIX = "identifier.doi.prefix"; private static final String BASE_OUTPUT_DIR_PATH = "./target/testing/dspace/assetstore/crosswalk/"; @@ -99,6 +101,8 @@ public class ReferCrosswalkIT extends AbstractIntegrationTestWithDatabase { private VirtualField virtualFieldId; + private ConfigurationService configurationService; + @Before public void setup() throws SQLException, AuthorizeException { @@ -117,6 +121,8 @@ public void setup() throws SQLException, AuthorizeException { when(mockedVirtualFieldId.getMetadata(any(), any(), any())).thenReturn(new String[] { "mock-id" }); this.virtualFieldMapper.setVirtualField("id", mockedVirtualFieldId); + this.configurationService = new DSpace().getSingletonService(ConfigurationService.class); + context.turnOffAuthorisationSystem(); community = createCommunity(context).build(); collection = createCollection(context, community).withAdminGroup(eperson).build(); @@ -2530,6 +2536,82 @@ public void testVirtualBitstreamFieldWithProject() throws Exception { assertThat(resultLines[54].trim(), equalTo("")); } + @Test + public void testExportToDataciteFormatItemWithThreeDOI() throws Exception { + String prefix; + prefix = this.configurationService.getProperty(CFG_PREFIX); + if (null == prefix) { + throw new RuntimeException("Unable to load DOI prefix from " + + "configuration. Cannot find property " + + CFG_PREFIX + "."); + } + + context.turnOffAuthorisationSystem(); + + Item publication = createItem(context, collection) + .withEntityType("Publication") + .withTitle("publication title") + .withDoiIdentifier("test doi") + .withDoiIdentifier("test doi2") + .withDoiIdentifier("test" + prefix + "test") + .build(); + + context.restoreAuthSystemState(); + + ReferCrosswalk referCrosswalk = new DSpace().getServiceManager() + .getServiceByName("referCrosswalkVirtualFieldDOI", ReferCrosswalk.class); + assertThat(referCrosswalk, notNullValue()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publication, out); + + String[] resultLines = out.toString().split("\n"); + + assertThat(resultLines.length, is(5)); + assertThat(resultLines[0].trim(), is("{")); + assertThat(resultLines[1].trim(), is("\"primary-doi\": \"test" + prefix + "test\",")); + assertThat(resultLines[2].trim(), is("\"alternative-doi\": \"test doi\",")); + assertThat(resultLines[3].trim(), is("\"alternative-doi\": \"test doi2\"")); + assertThat(resultLines[4].trim(), is("}")); + } + + @Test + public void testExportToDataciteFormatItemWithSingleDOINotMatchingPrefix() throws Exception { + String prefix; + prefix = this.configurationService.getProperty(CFG_PREFIX); + if (null == prefix) { + throw new RuntimeException("Unable to load DOI prefix from " + + "configuration. Cannot find property " + + CFG_PREFIX + "."); + } + + context.turnOffAuthorisationSystem(); + + Item publication = createItem(context, collection) + .withEntityType("Publication") + .withTitle("publication title") + .withDoiIdentifier("test doi") + .build(); + + context.restoreAuthSystemState(); + + ReferCrosswalk referCrosswalk = new DSpace().getServiceManager() + .getServiceByName("referCrosswalkVirtualFieldDOI", ReferCrosswalk.class); + assertThat(referCrosswalk, notNullValue()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publication, out); + + String[] resultLines = out.toString().split("\n"); + + assertThat(resultLines.length, is(3)); + assertThat(resultLines[0].trim(), is("{")); + assertThat(resultLines[1].trim(), is("\"primary-doi\": \"test doi\"")); + assertThat(resultLines[2].trim(), is("}")); + } + + + private void createSelectedRelationship(Item author, Item publication, RelationshipType selectedRelationshipType) { createRelationshipBuilder(context, publication, author, selectedRelationshipType, -1, -1).build(); diff --git a/dspace-oai/src/main/java/org/dspace/xoai/app/DataciteDOIItemCompilePlugin.java b/dspace-oai/src/main/java/org/dspace/xoai/app/DataciteDOIItemCompilePlugin.java index b56ab779b265..5c40465f5908 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/DataciteDOIItemCompilePlugin.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/DataciteDOIItemCompilePlugin.java @@ -22,13 +22,19 @@ /** * XOAIExtensionItemCompilePlugin aims to add structured information about the - * creative commons license applied to the item (if any). + * DOIs of the item (if any). * The xoai document will be enriched with a structure like that * * - * - * - * + * + * + * + * + * + * + * ... + * + * * * * From 8c0aa2d7ca6fa321919e1fe808ceef35bc1c4354 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 4 Oct 2023 09:52:13 +0200 Subject: [PATCH 52/81] [CST-10704] New external login flow for ORCID feat: - new tables and entities related to a registration data - new metadata for registration data - new rest method (PATCH) to update mail of a registration data - new mail templates - new ITs for orcid and registration PATCH --- .../authenticate/OrcidAuthenticationBean.java | 75 +-- .../dspace/eperson/AccountServiceImpl.java | 340 ++++++++++- .../org/dspace/eperson/RegistrationData.java | 81 ++- ...gistrationDataExpirationConfiguration.java | 83 +++ .../eperson/RegistrationDataMetadata.java | 109 ++++ .../RegistrationDataMetadataServiceImpl.java | 88 +++ .../eperson/RegistrationDataScheduler.java | 53 ++ .../eperson/RegistrationDataServiceImpl.java | 163 ++++- .../dspace/eperson/RegistrationTypeEnum.java | 33 ++ .../eperson/dao/RegistrationDataDAO.java | 45 ++ .../dao/RegistrationDataMetadataDAO.java | 22 + .../dao/impl/RegistrationDataDAOImpl.java | 29 + .../impl/RegistrationDataMetadataDAOImpl.java | 19 + .../eperson/dto/RegistrationDataChanges.java | 49 ++ .../eperson/dto/RegistrationDataPatch.java | 32 + .../eperson/service/AccountService.java | 15 + .../RegistrationDataMetadataService.java | 34 ++ .../service/RegistrationDataService.java | 43 +- .../h2/V7.6_2023.09.22__registration_data.sql | 46 ++ .../V7.6_2023.09.22__registration_data.sql | 55 ++ .../app/rest/EPersonGroupRestController.java | 12 + .../EPersonRegistrationRestController.java | 76 +++ .../app/rest/converter/MetadataConverter.java | 47 +- .../converter/RegistrationDataConverter.java | 140 +++++ .../app/rest/model/DSpaceObjectRest.java | 6 +- .../dspace/app/rest/model/MetadataRest.java | 12 +- .../rest/model/RegistrationMetadataRest.java | 37 ++ .../app/rest/model/RegistrationRest.java | 48 +- .../repository/EPersonRestRepository.java | 26 +- .../RegistrationRestRepository.java | 94 ++- .../RegistrationEmailPatchOperation.java | 149 +++++ .../app/rest/security/OrcidLoginFilter.java | 49 +- .../dspace/app/rest/OrcidLoginFilterIT.java | 111 +++- .../rest/RegistrationRestRepositoryIT.java | 559 ++++++++++++++++++ dspace/config/dspace.cfg | 27 + dspace/config/emails/orcid | 22 + dspace/config/hibernate.cfg.xml | 1 + .../config/spring/api/core-dao-services.xml | 1 + dspace/config/spring/api/core-services.xml | 1 + 39 files changed, 2649 insertions(+), 183 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/eperson/RegistrationDataExpirationConfiguration.java create mode 100644 dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadata.java create mode 100644 dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadataServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/eperson/RegistrationDataScheduler.java create mode 100644 dspace-api/src/main/java/org/dspace/eperson/RegistrationTypeEnum.java create mode 100644 dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataMetadataDAO.java create mode 100644 dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataMetadataDAOImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataChanges.java create mode 100644 dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataPatch.java create mode 100644 dspace-api/src/main/java/org/dspace/eperson/service/RegistrationDataMetadataService.java create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2023.09.22__registration_data.sql create mode 100644 dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2023.09.22__registration_data.sql create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonRegistrationRestController.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/RegistrationEmailPatchOperation.java create mode 100644 dspace/config/emails/orcid diff --git a/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java b/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java index f77d7e57119a..9036b8586c0d 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java @@ -27,7 +27,10 @@ import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; import org.dspace.eperson.service.EPersonService; +import org.dspace.eperson.service.RegistrationDataService; import org.dspace.orcid.OrcidToken; import org.dspace.orcid.client.OrcidClient; import org.dspace.orcid.client.OrcidConfiguration; @@ -47,11 +50,12 @@ * ORCID authentication for DSpace. * * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * */ public class OrcidAuthenticationBean implements AuthenticationMethod { public static final String ORCID_AUTH_ATTRIBUTE = "orcid-authentication"; + public static final String ORCID_REGISTRATION_TOKEN = "orcid-registration-token"; + public static final String ORCID_DEFAULT_REGISTRATION_URL = "/external-registration?token={0}"; private final static Logger LOGGER = LoggerFactory.getLogger(OrcidAuthenticationBean.class); @@ -78,6 +82,9 @@ public class OrcidAuthenticationBean implements AuthenticationMethod { @Autowired private OrcidTokenService orcidTokenService; + @Autowired + private RegistrationDataService registrationDataService; + @Override public int authenticate(Context context, String username, String password, String realm, HttpServletRequest request) throws SQLException { @@ -184,7 +191,7 @@ private int authenticateWithOrcid(Context context, String code, HttpServletReque return ePerson.canLogIn() ? logInEPerson(context, token, ePerson) : BAD_ARGS; } - return canSelfRegister() ? registerNewEPerson(context, person, token) : NO_SUCH_USER; + return canSelfRegister() ? createRegistrationData(context, request, person, token) : NO_SUCH_USER; } @@ -212,51 +219,51 @@ private ResearcherProfile findProfile(Context context, EPerson ePerson) throws S } } - private int registerNewEPerson(Context context, Person person, OrcidTokenResponseDTO token) throws SQLException { + private int createRegistrationData( + Context context, HttpServletRequest request, Person person, OrcidTokenResponseDTO token + ) throws SQLException { try { context.turnOffAuthorisationSystem(); - String email = getEmail(person) - .orElseThrow(() -> new IllegalStateException("The email is configured private on orcid")); - - String orcid = token.getOrcid(); - - EPerson eperson = ePersonService.create(context); - - eperson.setNetid(orcid); - - eperson.setEmail(email); + RegistrationData registrationData = + this.registrationDataService.create(context, token.getOrcid(), RegistrationTypeEnum.ORCID); - Optional firstName = getFirstName(person); - if (firstName.isPresent()) { - eperson.setFirstName(context, firstName.get()); - } + registrationData.setEmail(getEmail(person).orElse(null)); + setOrcidMetadataOnRegistration(context, registrationData, person, token); - Optional lastName = getLastName(person); - if (lastName.isPresent()) { - eperson.setLastName(context, lastName.get()); - } - eperson.setCanLogIn(true); - eperson.setSelfRegistered(true); + registrationDataService.update(context, registrationData); - setOrcidMetadataOnEPerson(context, eperson, token); - - ePersonService.update(context, eperson); - context.setCurrentUser(eperson); + request.setAttribute(ORCID_REGISTRATION_TOKEN, registrationData.getToken()); context.dispatchEvents(); - return SUCCESS; - } catch (Exception ex) { LOGGER.error("An error occurs registering a new EPerson from ORCID", ex); context.rollback(); - return NO_SUCH_USER; } finally { context.restoreAuthSystemState(); + return NO_SUCH_USER; } } + private void setOrcidMetadataOnRegistration( + Context context, RegistrationData registration, Person person, OrcidTokenResponseDTO token + ) throws SQLException, AuthorizeException { + String orcid = token.getOrcid(); + // String[] scopes = token.getScopeAsArray(); + + registrationDataService.setRegistrationMetadataValue( + context, registration, "eperson", "firstname", null, getFirstName(person).orElse(null) + ); + registrationDataService.setRegistrationMetadataValue( + context, registration, "eperson", "lastname", null, getLastName(person).orElse(null) + ); + registrationDataService.setRegistrationMetadataValue(context, registration, "eperson", "orcid", null, orcid); + /*for (String scope : scopes) { + registrationDataService.addMetadata(context, registration, "eperson", "orcid", "scope", null, scope); + }*/ + } + private void setOrcidMetadataOnEPerson(Context context, EPerson person, OrcidTokenResponseDTO token) throws SQLException { @@ -298,14 +305,14 @@ private Optional getEmail(Person person) { private Optional getFirstName(Person person) { return Optional.ofNullable(person.getName()) - .map(name -> name.getGivenNames()) - .map(givenNames -> givenNames.getContent()); + .map(name -> name.getGivenNames()) + .map(givenNames -> givenNames.getContent()); } private Optional getLastName(Person person) { return Optional.ofNullable(person.getName()) - .map(name -> name.getFamilyName()) - .map(givenNames -> givenNames.getContent()); + .map(name -> name.getFamilyName()) + .map(givenNames -> givenNames.getContent()); } private boolean canSelfRegister() { diff --git a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java index 283f101f2ba5..174040f08133 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java @@ -11,8 +11,13 @@ import java.sql.SQLException; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Stream; import javax.mail.MessagingException; import org.apache.commons.collections4.CollectionUtils; @@ -20,16 +25,21 @@ import org.apache.logging.log4j.Logger; import org.dspace.authenticate.service.AuthenticationService; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.MetadataValueService; import org.dspace.core.Context; import org.dspace.core.Email; import org.dspace.core.I18nUtil; import org.dspace.core.Utils; +import org.dspace.eperson.dto.RegistrationDataPatch; import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.GroupService; import org.dspace.eperson.service.RegistrationDataService; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.log.LogMessage; /** * Methods for handling registration by email and forgotten passwords. When @@ -50,8 +60,16 @@ public class AccountServiceImpl implements AccountService { * log4j log */ private static final Logger log = LogManager.getLogger(AccountServiceImpl.class); + + private static final Map> allowedMergeArguments = + Map.of( + "email", + (RegistrationData registrationData, EPerson eperson) -> eperson.setEmail(registrationData.getEmail()) + ); + @Autowired(required = true) protected EPersonService ePersonService; + @Autowired(required = true) protected RegistrationDataService registrationDataService; @Autowired @@ -63,6 +81,9 @@ public class AccountServiceImpl implements AccountService { @Autowired private AuthenticationService authenticationService; + @Autowired + private MetadataValueService metadataValueService; + protected AccountServiceImpl() { } @@ -79,9 +100,9 @@ protected AccountServiceImpl() { * * @param context DSpace context * @param email Email address to send the registration email to - * @throws java.sql.SQLException passed through. - * @throws java.io.IOException passed through. - * @throws javax.mail.MessagingException passed through. + * @throws java.sql.SQLException passed through. + * @throws java.io.IOException passed through. + * @throws javax.mail.MessagingException passed through. * @throws org.dspace.authorize.AuthorizeException passed through. */ @Override @@ -94,7 +115,7 @@ public void sendRegistrationInfo(Context context, String email, List group if (!authenticationService.canSelfRegister(context, null, email)) { throw new IllegalStateException("self registration is not allowed with this email address"); } - sendInfo(context, email, groups, true, true); + sendInfo(context, email, groups, RegistrationTypeEnum.REGISTER, true); } /** @@ -108,19 +129,36 @@ public void sendRegistrationInfo(Context context, String email, List group *

    • Authorization error (throws AuthorizeException).
    • * * - * * @param context DSpace context * @param email Email address to send the forgot-password email to - * @throws java.sql.SQLException passed through. - * @throws java.io.IOException passed through. - * @throws javax.mail.MessagingException passed through. + * @throws java.sql.SQLException passed through. + * @throws java.io.IOException passed through. + * @throws javax.mail.MessagingException passed through. * @throws org.dspace.authorize.AuthorizeException passed through. */ @Override public void sendForgotPasswordInfo(Context context, String email, List groups) - throws SQLException, IOException, MessagingException, - AuthorizeException { - sendInfo(context, email, groups, false, true); + throws SQLException, IOException, MessagingException, AuthorizeException { + sendInfo(context, email, groups, RegistrationTypeEnum.FORGOT, true); + } + + /** + * Checks if exists an account related to the token provided + * + * @param context DSpace context + * @param token Account token + * @return true if exists, false otherwise + * @throws SQLException + * @throws AuthorizeException + */ + @Override + public boolean existsAccountFor(Context context, String token) throws SQLException, AuthorizeException { + return getEPerson(context, token) != null; + } + + @Override + public boolean existsAccountWithEmail(Context context, String email) throws SQLException { + return ePersonService.findByEmail(context, email) != null; } /** @@ -137,8 +175,8 @@ public void sendForgotPasswordInfo(Context context, String email, List gro * @param context DSpace context * @param token Account token * @return The EPerson corresponding to token, or null. - * @throws SQLException If the token or eperson cannot be retrieved from the - * database. + * @throws SQLException If the token or eperson cannot be retrieved from the + * database. * @throws AuthorizeException passed through. */ @Override @@ -192,6 +230,213 @@ public void deleteToken(Context context, String token) registrationDataService.deleteByToken(context, token); } + public EPerson mergeRegistration(Context context, UUID personId, String token, List overrides) + throws AuthorizeException, SQLException { + + RegistrationData registrationData = getRegistrationData(context, token); + EPerson eperson = null; + if (personId != null) { + eperson = ePersonService.findByIdOrLegacyId(context, personId.toString()); + } + + if (!isValidationToken(registrationData)) { + throw new AuthorizeException("The token provided (" + token + ") has an invalid registration type."); + } + + if (hasLoggedEPerson(context) && !isSameContextEPerson(context, eperson)) { + throw new AuthorizeException("Only the user with id: " + personId + " can make this action."); + } + + context.turnOffAuthorisationSystem(); + + eperson = Optional.ofNullable(eperson).orElseGet(() -> createEPerson(context, registrationData)); + updateValuesFromRegistration(context, eperson, registrationData, overrides); + addEPersonToGroups(context, eperson, registrationData.getGroups()); + deleteToken(context, token); + + context.restoreAuthSystemState(); + + return eperson; + } + + private EPerson createEPerson(Context context, RegistrationData registrationData) { + EPerson eperson; + try { + eperson = ePersonService.create(context); + + eperson.setNetid(registrationData.getNetId()); + eperson.setEmail(registrationData.getEmail()); + + RegistrationDataMetadata firstName = + registrationDataService.getMetadataByMetadataString( + registrationData, + "eperson.firstname" + ); + if (firstName != null) { + eperson.setFirstName(context, firstName.getValue()); + } + + RegistrationDataMetadata lastName = + registrationDataService.getMetadataByMetadataString( + registrationData, + "eperson.lastname" + ); + if (lastName != null) { + eperson.setLastName(context, lastName.getValue()); + } + eperson.setCanLogIn(true); + eperson.setSelfRegistered(true); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException( + "Cannote create the eperson linked to the token: " + registrationData.getToken(), + e + ); + } + return eperson; + } + + private boolean hasLoggedEPerson(Context context) { + return context.getCurrentUser() != null; + } + + private boolean isSameContextEPerson(Context context, EPerson eperson) { + return eperson.equals(context.getCurrentUser()); + } + + private static boolean isValidationToken(RegistrationData registrationData) { + return registrationData.getRegistrationType().equals(RegistrationTypeEnum.VALIDATION); + } + + @Override + public RegistrationData renewRegistrationForEmail( + Context context, RegistrationDataPatch registrationDataPatch + ) throws AuthorizeException { + try { + RegistrationData newRegistration = registrationDataService.clone(context, registrationDataPatch); + registrationDataService.delete(context, registrationDataPatch.getOldRegistration()); + fillAndSendEmail(context, newRegistration); + return newRegistration; + } catch (SQLException | MessagingException | IOException e) { + log.error(e); + throw new RuntimeException(e); + } + } + + private boolean isEmailConfirmed(RegistrationData oldRegistration, String email) { + return email.equals(oldRegistration.getEmail()); + } + + @Override + public void confirmRegistration(Context context, RegistrationData registrationData) { + } + + + protected void updateValuesFromRegistration( + Context context, EPerson eperson, RegistrationData registrationData, List overrides + ) { + Stream.concat( + getMergeActions(registrationData, overrides), + getUpdateActions(context, eperson, registrationData) + ).forEach(c -> c.accept(eperson)); + } + + private Stream> getMergeActions(RegistrationData registrationData, List overrides) { + return overrides.stream().map(f -> mergeField(f, registrationData)); + } + + protected Stream> getUpdateActions( + Context context, EPerson eperson, RegistrationData registrationData + ) { + Stream.Builder> actions = Stream.builder(); + if (eperson.getNetid() == null) { + actions.add(p -> p.setNetid(registrationData.getNetId())); + } + if (eperson.getEmail() == null) { + actions.add(p -> p.setEmail(registrationData.getEmail())); + } + for (RegistrationDataMetadata metadatum : registrationData.getMetadata()) { + Optional> epersonMetadata = + Optional.ofNullable( + ePersonService.getMetadataByMetadataString( + eperson, metadatum.getMetadataField().toString('.') + ) + ); + if (epersonMetadata.isEmpty()) { + actions.add(p -> addMetadataValue(context, metadatum, p)); + } + } + return actions.build(); + } + + private List addMetadataValue(Context context, RegistrationDataMetadata metadatum, EPerson p) { + try { + return ePersonService.addMetadata( + context, p, metadatum.getMetadataField(), Item.ANY, List.of(metadatum.getValue()) + ); + } catch (SQLException e) { + throw new RuntimeException( + "Could not add metadata" + metadatum.getMetadataField() + " to eperson with uuid: " + p.getID(), e); + } + } + + protected Consumer mergeField(String field, RegistrationData registrationData) { + return person -> + allowedMergeArguments.getOrDefault( + field, + mergeRegistrationMetadata(field) + ).accept(registrationData, person); + } + + protected BiConsumer mergeRegistrationMetadata(String field) { + return (registrationData, person) -> { + RegistrationDataMetadata registrationMetadata = getMetadataOrThrow(registrationData, field); + MetadataValue metadata = getMetadataOrThrow(person, field); + metadata.setValue(registrationMetadata.getValue()); + ePersonService.setMetadataModified(person); + }; + } + + private RegistrationDataMetadata getMetadataOrThrow(RegistrationData registrationData, String field) { + return registrationDataService.getMetadataByMetadataString(registrationData, field); + } + + private MetadataValue getMetadataOrThrow(EPerson eperson, String field) { + return ePersonService.getMetadataByMetadataString(eperson, field).stream().findFirst() + .orElseThrow( + () -> new IllegalArgumentException( + "Could not find the metadata field: " + field + " for eperson: " + eperson.getID()) + ); + } + + + protected void addEPersonToGroups(Context context, EPerson eperson, List groups) { + if (CollectionUtils.isEmpty(groups)) { + return; + } + for (Group group : groups) { + groupService.addMember(context, group, eperson); + } + } + + private RegistrationData getRegistrationData(Context context, String token) + throws SQLException, AuthorizeException { + return Optional.ofNullable(registrationDataService.findByToken(context, token)) + .filter(rd -> + isValid(rd) || + !RegistrationTypeEnum.VALIDATION.equals(rd.getRegistrationType()) + ) + .orElseThrow( + () -> new AuthorizeException( + "The registration token: " + token + " is not valid!" + ) + ); + } + + private boolean isValid(RegistrationData rd) { + return registrationDataService.isValid(rd); + } + + /** * THIS IS AN INTERNAL METHOD. THE SEND PARAMETER ALLOWS IT TO BE USED FOR * TESTING PURPOSES. @@ -204,8 +449,7 @@ public void deleteToken(Context context, String token) * * @param context DSpace context * @param email Email address to send the forgot-password email to - * @param isRegister If true, this is for registration; otherwise, it is - * for forgot-password + * @param type Type of registration {@link RegistrationTypeEnum} * @param send If true, send email; otherwise do not send any email * @return null if no EPerson with that email found * @throws SQLException Cannot create registration data in database @@ -213,16 +457,17 @@ public void deleteToken(Context context, String token) * @throws IOException Error reading email template * @throws AuthorizeException Authorization error */ - protected RegistrationData sendInfo(Context context, String email, List groups, - boolean isRegister, boolean send) throws SQLException, IOException, - MessagingException, AuthorizeException { + protected RegistrationData sendInfo( + Context context, String email, List groups, RegistrationTypeEnum type, boolean send + ) throws SQLException, IOException, MessagingException, AuthorizeException { // See if a registration token already exists for this user - RegistrationData rd = registrationDataService.findByEmail(context, email); - + RegistrationData rd = registrationDataService.findBy(context, email, type); + boolean isRegister = RegistrationTypeEnum.REGISTER.equals(type); // If it already exists, just re-issue it if (rd == null) { rd = registrationDataService.create(context); + rd.setRegistrationType(type); rd.setToken(Utils.generateHexKey()); // don't set expiration date any more @@ -250,7 +495,7 @@ protected RegistrationData sendInfo(Context context, String email, List gr } } if (send) { - sendEmail(context, email, isRegister, rd); + fillAndSendEmail(context, email, isRegister, rd); } return rd; @@ -271,22 +516,19 @@ protected RegistrationData sendInfo(Context context, String email, List gr * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations. * @throws SQLException An exception that provides information on a database access error or other errors. */ - protected void sendEmail(Context context, String email, boolean isRegister, RegistrationData rd) + protected void fillAndSendEmail(Context context, String email, boolean isRegister, RegistrationData rd) throws MessagingException, IOException, SQLException { String base = configurationService.getProperty("dspace.ui.url"); // Note change from "key=" to "token=" - String specialLink = new StringBuffer().append(base).append( - base.endsWith("/") ? "" : "/").append( - isRegister ? "register" : (rd.getGroups().size() == 0) ? "forgot" : "invitation").append("/") - .append(rd.getToken()) - .toString(); + String specialLink = getSpecialLink( + base, rd, isRegister ? "register" : ((rd.getGroups().size() == 0) ? "forgot" : "invitation") + ); + Locale locale = context.getCurrentLocale(); - Email bean = Email.getEmail(I18nUtil.getEmailFilename(locale, isRegister ? "register" - : "change_password")); - bean.addRecipient(email); - bean.addArgument(specialLink); - bean.send(); + String emailFilename = I18nUtil.getEmailFilename(locale, isRegister ? "register" : "change_password"); + + fillAndSendEmail(email, emailFilename, specialLink); // Breadcrumbs if (log.isInfoEnabled()) { @@ -294,4 +536,38 @@ protected void sendEmail(Context context, String email, boolean isRegister, Regi + " information to " + email); } } + + private static String getSpecialLink(String base, RegistrationData rd, String subPath) { + return new StringBuffer(base) + .append(base.endsWith("/") ? "" : "/") + .append(subPath) + .append("/") + .append(rd.getToken()) + .toString(); + } + + protected void fillAndSendEmail( + Context context, RegistrationData rd + ) throws MessagingException, IOException { + String base = configurationService.getProperty("dspace.ui.url"); + + // Note change from "key=" to "token=" + String specialLink = getSpecialLink(base, rd, rd.getRegistrationType().getLink()); + + String emailFilename = I18nUtil.getEmailFilename( + context.getCurrentLocale(), rd.getRegistrationType().toString().toLowerCase() + ); + + fillAndSendEmail(rd.getEmail(), emailFilename, specialLink); + + log.info(LogMessage.of(() -> "Sent " + rd.getRegistrationType().getLink() + " link to " + rd.getEmail())); + } + + protected void fillAndSendEmail(String email, String emailFilename, String specialLink) + throws IOException, MessagingException { + Email bean = Email.getEmail(emailFilename); + bean.addRecipient(email); + bean.addArgument(specialLink); + bean.send(); + } } diff --git a/dspace-api/src/main/java/org/dspace/eperson/RegistrationData.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationData.java index 953a3e8bd0a6..2c0e1abb8238 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/RegistrationData.java +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationData.java @@ -10,9 +10,13 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -20,6 +24,7 @@ import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; import javax.persistence.SequenceGenerator; import javax.persistence.Table; import javax.persistence.Temporal; @@ -27,6 +32,7 @@ import org.dspace.core.Context; import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.SortNatural; /** * Database entity representation of the registrationdata table @@ -43,30 +49,75 @@ public class RegistrationData implements ReloadableEntity { @SequenceGenerator(name = "registrationdata_seq", sequenceName = "registrationdata_seq", allocationSize = 1) private Integer id; - @Column(name = "email", unique = true, length = 64) + /** + * Contains the email used to register the user. + */ + @Column(name = "email", length = 64) private String email; + /** + * Contains the unique id generated fot the user. + */ @Column(name = "token", length = 48) private String token; + /** + * Expiration date of this registration data. + */ @Column(name = "expires") @Temporal(TemporalType.TIMESTAMP) private Date expires; @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST}) @JoinTable( - name = "registrationdata2group", - joinColumns = {@JoinColumn(name = "registrationdata_id")}, - inverseJoinColumns = {@JoinColumn(name = "group_id")} + name = "registrationdata2group", + joinColumns = {@JoinColumn(name = "registrationdata_id")}, + inverseJoinColumns = {@JoinColumn(name = "group_id")} ) private final List groups = new ArrayList(); + + /** + * Metadata linked to this registration data + */ + @SortNatural + @OneToMany( + fetch = FetchType.LAZY, + mappedBy = "registrationData", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private SortedSet metadata = new TreeSet<>(); + + /** + * External service used to register the user. + * Allowed values are inside {@link RegistrationTypeEnum} + */ + @Column(name = "registration_type") + @Enumerated(EnumType.STRING) + private RegistrationTypeEnum registrationType; + + /** + * Contains the external id provided by the external service + * accordingly to the registration type. + */ + @Column(name = "net_id", length = 64) + private final String netId; + /** * Protected constructor, create object using: * {@link org.dspace.eperson.service.RegistrationDataService#create(Context)} */ protected RegistrationData() { + this(null); + } + /** + * Protected constructor, create object using: + * {@link org.dspace.eperson.service.RegistrationDataService#create(Context, String)} + */ + protected RegistrationData(String netId) { + this.netId = netId; } public Integer getID() { @@ -77,7 +128,7 @@ public String getEmail() { return email; } - void setEmail(String email) { + public void setEmail(String email) { this.email = email; } @@ -104,4 +155,24 @@ public List getGroups() { public void addGroup(Group group) { this.groups.add(group); } + + public RegistrationTypeEnum getRegistrationType() { + return registrationType; + } + + public void setRegistrationType(RegistrationTypeEnum registrationType) { + this.registrationType = registrationType; + } + + public SortedSet getMetadata() { + return metadata; + } + + public void setMetadata(SortedSet metadata) { + this.metadata = metadata; + } + + public String getNetId() { + return netId; + } } diff --git a/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataExpirationConfiguration.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataExpirationConfiguration.java new file mode 100644 index 000000000000..607f42315c0c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataExpirationConfiguration.java @@ -0,0 +1,83 @@ +/** + * 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.eperson; + +import java.text.MessageFormat; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationDataExpirationConfiguration { + + private static final String EXPIRATION_PROP = "eperson.registration-data.token.{0}.expiration"; + private static final String DURATION_FORMAT = "PT{0}"; + + public static final RegistrationDataExpirationConfiguration INSTANCE = + new RegistrationDataExpirationConfiguration(); + + public static RegistrationDataExpirationConfiguration getInstance() { + return INSTANCE; + } + + private final Map expirationMap; + + private RegistrationDataExpirationConfiguration() { + this.expirationMap = + Stream.of(RegistrationTypeEnum.values()) + .map(type -> Optional.ofNullable(getDurationOf(type)) + .map(duration -> Map.entry(type, duration)) + .orElse(null) + ) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private Duration getDurationOf(RegistrationTypeEnum type) { + String format = MessageFormat.format(EXPIRATION_PROP, type.toString()); + ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService(); + String typeValue = config.getProperty(format); + + if (StringUtils.isBlank(typeValue)) { + return null; + } + + return Duration.parse(MessageFormat.format(DURATION_FORMAT, typeValue)); + } + + public Duration getExpiration(RegistrationTypeEnum type) { + return expirationMap.get(type); + } + + public Date computeExpirationDate(RegistrationTypeEnum type) { + + if (type == null) { + return null; + } + + Duration duration = this.expirationMap.get(type); + + if (duration == null) { + return null; + } + + return Date.from(Instant.now().plus(duration)); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadata.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadata.java new file mode 100644 index 000000000000..dde8428fe1fe --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadata.java @@ -0,0 +1,109 @@ +/** + * 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.eperson; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.dspace.content.MetadataField; +import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.Type; + +/** + * Metadata related to a registration data {@link RegistrationData} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +@Entity +@Table(name = "registrationdata_metadata") +public class RegistrationDataMetadata implements ReloadableEntity, Comparable { + + @Id + @Column(name = "registrationdata_metadata_id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "registrationdata_metadatavalue_seq") + @SequenceGenerator( + name = "registrationdata_metadatavalue_seq", + sequenceName = "registrationdata_metadatavalue_seq", + allocationSize = 1 + ) + private final Integer id; + + /** + * {@link RegistrationData} linked to this metadata value + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "registrationdata_id") + private RegistrationData registrationData = null; + + /** + * The linked {@link MetadataField} instance + */ + @ManyToOne + @JoinColumn(name = "metadata_field_id") + private MetadataField metadataField = null; + + /** + * Value represented by this {@link RegistrationDataMetadata} instance + * related to the metadataField {@link MetadataField} + */ + @Lob + @Type(type = "org.dspace.storage.rdbms.hibernate.DatabaseAwareLobType") + @Column(name = "text_value") + private String value = null; + + /** + * Protected constructor + */ + protected RegistrationDataMetadata() { + id = 0; + } + + + @Override + public Integer getID() { + return id; + } + + public MetadataField getMetadataField() { + return metadataField; + } + + void setMetadataField(MetadataField metadataField) { + this.metadataField = metadataField; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public int compareTo(RegistrationDataMetadata o) { + return Integer.compare(this.id, o.id); + } + + void setRegistrationData(RegistrationData registrationData) { + this.registrationData = registrationData; + } + + public RegistrationData getRegistrationData() { + return registrationData; + } +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadataServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadataServiceImpl.java new file mode 100644 index 000000000000..0bd736e4b097 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadataServiceImpl.java @@ -0,0 +1,88 @@ +/** + * 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.eperson; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.MetadataField; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.core.Context; +import org.dspace.eperson.dao.RegistrationDataMetadataDAO; +import org.dspace.eperson.service.RegistrationDataMetadataService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationDataMetadataServiceImpl implements RegistrationDataMetadataService { + + @Autowired + private RegistrationDataMetadataDAO registrationDataMetadataDAO; + + @Autowired + private MetadataFieldService metadataFieldService; + + + @Override + public RegistrationDataMetadata create(Context context, RegistrationData registrationData, String schema, + String element, String qualifier) throws SQLException { + return create(context, registrationData, + metadataFieldService.findByElement(context, schema, element, qualifier)); + } + + @Override + public RegistrationDataMetadata create(Context context, RegistrationData registrationData, + MetadataField metadataField) throws SQLException { + RegistrationDataMetadata metadata = new RegistrationDataMetadata(); + metadata.setRegistrationData(registrationData); + metadata.setMetadataField(metadataField); + return registrationDataMetadataDAO.create(context, metadata); + } + + @Override + public RegistrationDataMetadata create( + Context context, RegistrationData registrationData, MetadataField metadataField, String value + ) throws SQLException { + RegistrationDataMetadata metadata = new RegistrationDataMetadata(); + metadata.setRegistrationData(registrationData); + metadata.setMetadataField(metadataField); + metadata.setValue(value); + return registrationDataMetadataDAO.create(context, metadata); + } + + @Override + public RegistrationDataMetadata create(Context context) throws SQLException, AuthorizeException { + return registrationDataMetadataDAO.create(context, new RegistrationDataMetadata()); + } + + @Override + public RegistrationDataMetadata find(Context context, int id) throws SQLException { + return registrationDataMetadataDAO.findByID(context, RegistrationData.class, id); + } + + @Override + public void update(Context context, RegistrationDataMetadata registrationDataMetadata) + throws SQLException, AuthorizeException { + registrationDataMetadataDAO.save(context, registrationDataMetadata); + } + + @Override + public void update(Context context, List t) throws SQLException, AuthorizeException { + for (RegistrationDataMetadata registrationDataMetadata : t) { + update(context, registrationDataMetadata); + } + } + + @Override + public void delete(Context context, RegistrationDataMetadata registrationDataMetadata) + throws SQLException, AuthorizeException { + registrationDataMetadataDAO.delete(context, registrationDataMetadata); + } +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataScheduler.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataScheduler.java new file mode 100644 index 000000000000..3bd4691fec36 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataScheduler.java @@ -0,0 +1,53 @@ +/** + * 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.eperson; + +import org.dspace.core.Context; +import org.dspace.eperson.service.RegistrationDataService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +/** + * Contains all the schedulable task related to {@link RegistrationData} entities. + * Can be enabled via the configuration property {@code eperson.registration-data.scheduler.enabled} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +@Service +@ConditionalOnProperty(prefix = "eperson.registration-data.scheduler", name = "enabled", havingValue = "true") +public class RegistrationDataScheduler { + + private static final Logger log = LoggerFactory.getLogger(RegistrationDataScheduler.class); + + @Autowired + private RegistrationDataService registrationDataService; + + /** + * Deletes expired {@link RegistrationData}. + * This task is scheduled to be run by the cron expression defined in the configuration file. + * + */ + @Scheduled(cron = "${eperson.registration-data.scheduler.expired-registration-data.cron:-}") + protected void deleteExpiredRegistrationData() { + Context context = new Context(); + context.turnOffAuthorisationSystem(); + try { + registrationDataService.deleteExpiredRegistrations(context); + } catch (Exception e) { + log.error("Failed to delete expired registrations", e); + } finally { + context.restoreAuthSystemState(); + } + } + + +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataServiceImpl.java index b27275168556..e370f59e86fe 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataServiceImpl.java @@ -9,12 +9,25 @@ import java.sql.SQLException; import java.util.Collections; +import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataValue; import org.dspace.core.Context; +import org.dspace.core.Utils; +import org.dspace.core.exception.SQLRuntimeException; import org.dspace.eperson.dao.RegistrationDataDAO; +import org.dspace.eperson.dto.RegistrationDataChanges; +import org.dspace.eperson.dto.RegistrationDataPatch; +import org.dspace.eperson.service.RegistrationDataMetadataService; import org.dspace.eperson.service.RegistrationDataService; import org.springframework.beans.factory.annotation.Autowired; @@ -26,19 +39,62 @@ * @author kevinvandevelde at atmire.com */ public class RegistrationDataServiceImpl implements RegistrationDataService { - @Autowired(required = true) + @Autowired() protected RegistrationDataDAO registrationDataDAO; + @Autowired() + protected RegistrationDataMetadataService registrationDataMetadataService; + + protected RegistrationDataExpirationConfiguration expirationConfiguration = + RegistrationDataExpirationConfiguration.getInstance(); + protected RegistrationDataServiceImpl() { } @Override public RegistrationData create(Context context) throws SQLException, AuthorizeException { - return registrationDataDAO.create(context, new RegistrationData()); + return create(context, null, null); } + @Override + public RegistrationData create(Context context, String netId) throws SQLException, AuthorizeException { + return this.create(context, netId, null); + } + + @Override + public RegistrationData create(Context context, String netId, RegistrationTypeEnum type) + throws SQLException, AuthorizeException { + RegistrationData rd = new RegistrationData(netId); + rd.setToken(Utils.generateHexKey()); + rd.setRegistrationType(type); + rd.setExpires(expirationConfiguration.computeExpirationDate(type)); + return registrationDataDAO.create(context, rd); + } + + @Override + public RegistrationData clone( + Context context, RegistrationDataPatch registrationDataPatch + ) throws SQLException, AuthorizeException { + RegistrationData old = registrationDataPatch.getOldRegistration(); + RegistrationDataChanges changes = registrationDataPatch.getChanges(); + RegistrationData rd = new RegistrationData(old.getNetId()); + rd.setEmail(changes.getEmail()); + rd.setRegistrationType(changes.getRegistrationType()); + rd.setToken(Utils.generateHexKey()); + + for (RegistrationDataMetadata metadata : old.getMetadata()) { + addMetadata(context, rd, metadata.getMetadataField(), metadata.getValue()); + } + + return registrationDataDAO.create(context, rd); + } + + private boolean isEmailConfirmed(RegistrationData old, String newEmail) { + return newEmail.equals(old.getEmail()); + } + @Override public RegistrationData findByToken(Context context, String token) throws SQLException { return registrationDataDAO.findByToken(context, token); @@ -49,12 +105,98 @@ public RegistrationData findByEmail(Context context, String email) throws SQLExc return registrationDataDAO.findByEmail(context, email); } + @Override + public RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException { + return registrationDataDAO.findBy(context, email, type); + } + @Override public void deleteByToken(Context context, String token) throws SQLException { registrationDataDAO.deleteByToken(context, token); } + @Override + public Stream>> groupEpersonMetadataByRegistrationData( + EPerson ePerson, RegistrationData registrationData + ) + throws SQLException { + Map> epersonMeta = + ePerson.getMetadata() + .stream() + .collect( + Collectors.groupingBy( + MetadataValue::getMetadataField + ) + ); + return registrationData.getMetadata() + .stream() + .map(meta -> + Map.entry( + meta, + Optional.ofNullable(epersonMeta.get(meta.getMetadataField())) + .filter(list -> list.size() == 1) + .map(values -> values.get(0)) + ) + ); + } + + @Override + public void setRegistrationMetadataValue( + Context context, RegistrationData registration, String schema, String element, String qualifier, String value + ) throws SQLException, AuthorizeException { + RegistrationDataMetadata metadata = + registration.getMetadata() + .stream() + .filter(m -> areEquals(m, schema, element, qualifier)) + .findAny() + .orElseGet(() -> createMetadata(context, registration, schema, element, qualifier)); + metadata.setValue(value); + registrationDataMetadataService.update(context, metadata); + } + + @Override + public void addMetadata( + Context context, RegistrationData registration, MetadataField mf, String value + ) throws SQLException, AuthorizeException { + registration.getMetadata().add( + registrationDataMetadataService.create(context, registration, mf, value) + ); + this.update(context, registration); + } + + @Override + public RegistrationDataMetadata getMetadataByMetadataString(RegistrationData registrationData, String field) { + return registrationData.getMetadata().stream() + .filter(m -> field.equals(m.getMetadataField().toString('.'))) + .findFirst().orElse(null); + } + + private boolean areEquals(RegistrationDataMetadata m, String schema, String element, String qualifier) { + return m.getMetadataField().getMetadataSchema().equals(schema) + && m.getMetadataField().getElement().equals(element) + && StringUtils.equals(m.getMetadataField().getQualifier(), qualifier); + } + + private RegistrationDataMetadata createMetadata(Context context, RegistrationData registration, + String schema, String element, String qualifier) { + try { + return registrationDataMetadataService.create( + context, registration, schema, element, qualifier + ); + } catch (SQLException e) { + throw new SQLRuntimeException(e); + } + } + + private RegistrationDataMetadata createMetadata(Context context, RegistrationData registration, MetadataField mf) { + try { + return registrationDataMetadataService.create(context, registration, mf); + } catch (SQLException e) { + throw new SQLRuntimeException(e); + } + } + @Override public RegistrationData find(Context context, int id) throws SQLException { return registrationDataDAO.findByID(context, RegistrationData.class, id); @@ -75,8 +217,25 @@ public void update(Context context, List registrationDataRecor } } + @Override + public void markAsExpired(Context context, RegistrationData registrationData) throws SQLException { + registrationData.setExpires(new Date()); + registrationDataDAO.save(context, registrationData); + } + @Override public void delete(Context context, RegistrationData registrationData) throws SQLException, AuthorizeException { registrationDataDAO.delete(context, registrationData); } + + @Override + public void deleteExpiredRegistrations(Context context) throws SQLException { + registrationDataDAO.deleteExpiredBy(context, new Date()); + } + + @Override + public boolean isValid(RegistrationData rd) { + return rd.getExpires() == null || rd.getExpires().after(new Date()); + } + } diff --git a/dspace-api/src/main/java/org/dspace/eperson/RegistrationTypeEnum.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationTypeEnum.java new file mode 100644 index 000000000000..569756ef5613 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationTypeEnum.java @@ -0,0 +1,33 @@ +/** + * 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.eperson; + +/** + * External provider allowed to register e-persons stored with {@link RegistrationData} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public enum RegistrationTypeEnum { + + ORCID("external-login"), + VALIDATION("review-account"), + FORGOT("forgot"), + REGISTER("register"), + INVITATION("invitation"), + CHANGE_PASSWORD("change-password"); + + private final String link; + + RegistrationTypeEnum(String link) { + this.link = link; + } + + public String getLink() { + return link; + } +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataDAO.java b/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataDAO.java index 5650c5e5b2be..0bdd6cc17cf8 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataDAO.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataDAO.java @@ -8,10 +8,12 @@ package org.dspace.eperson.dao; import java.sql.SQLException; +import java.util.Date; import org.dspace.core.Context; import org.dspace.core.GenericDAO; import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; /** * Database Access Object interface class for the RegistrationData object. @@ -23,9 +25,52 @@ */ public interface RegistrationDataDAO extends GenericDAO { + /** + * Finds {@link RegistrationData} by email. + * + * @param context Context for the current request + * @param email The email + * @return + * @throws SQLException + */ public RegistrationData findByEmail(Context context, String email) throws SQLException; + /** + * Finds {@link RegistrationData} by email and type. + * + * @param context Context for the current request + * @param email The email + * @param type The type of the {@link RegistrationData} + * @return + * @throws SQLException + */ + public RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException; + + /** + * Finds {@link RegistrationData} by token. + * + * @param context the context + * @param token The token related to the {@link RegistrationData}. + * @return + * @throws SQLException + */ public RegistrationData findByToken(Context context, String token) throws SQLException; + /** + * Deletes {@link RegistrationData} by token. + * + * @param context Context for the current request + * @param token The token to delete registrations for + * @throws SQLException + */ public void deleteByToken(Context context, String token) throws SQLException; + + /** + * Deletes expired {@link RegistrationData}. + * + * @param context Context for the current request + * @param date The date to delete expired registrations for + * @throws SQLException + */ + void deleteExpiredBy(Context context, Date date) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataMetadataDAO.java b/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataMetadataDAO.java new file mode 100644 index 000000000000..84ef2989cc45 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataMetadataDAO.java @@ -0,0 +1,22 @@ +/** + * 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.eperson.dao; + +import org.dspace.core.GenericDAO; +import org.dspace.eperson.RegistrationDataMetadata; + +/** + * Database Access Object interface class for the {@link org.dspace.eperson.RegistrationDataMetadata} object. + * The implementation of this class is responsible for all database calls for the RegistrationData object and is + * autowired by spring + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public interface RegistrationDataMetadataDAO extends GenericDAO { + +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataDAOImpl.java b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataDAOImpl.java index 4a15dcc86796..2dd023580dc8 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataDAOImpl.java @@ -8,8 +8,10 @@ package org.dspace.eperson.dao.impl; import java.sql.SQLException; +import java.util.Date; import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaDelete; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; @@ -17,6 +19,7 @@ import org.dspace.core.Context; import org.dspace.eperson.RegistrationData; import org.dspace.eperson.RegistrationData_; +import org.dspace.eperson.RegistrationTypeEnum; import org.dspace.eperson.dao.RegistrationDataDAO; /** @@ -42,6 +45,21 @@ public RegistrationData findByEmail(Context context, String email) throws SQLExc return uniqueResult(context, criteriaQuery, false, RegistrationData.class); } + @Override + public RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, RegistrationData.class); + Root registrationDataRoot = criteriaQuery.from(RegistrationData.class); + criteriaQuery.select(registrationDataRoot); + criteriaQuery.where( + criteriaBuilder.and( + criteriaBuilder.equal(registrationDataRoot.get(RegistrationData_.email), email), + criteriaBuilder.equal(registrationDataRoot.get(RegistrationData_.registrationType), type) + ) + ); + return uniqueResult(context, criteriaQuery, false, RegistrationData.class); + } + @Override public RegistrationData findByToken(Context context, String token) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); @@ -59,4 +77,15 @@ public void deleteByToken(Context context, String token) throws SQLException { query.setParameter("token", token); query.executeUpdate(); } + + @Override + public void deleteExpiredBy(Context context, Date date) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaDelete deleteQuery = criteriaBuilder.createCriteriaDelete(RegistrationData.class); + Root deleteRoot = deleteQuery.from(RegistrationData.class); + deleteQuery.where( + criteriaBuilder.lessThanOrEqualTo(deleteRoot.get(RegistrationData_.expires), date) + ); + getHibernateSession(context).createQuery(deleteQuery).executeUpdate(); + } } diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataMetadataDAOImpl.java b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataMetadataDAOImpl.java new file mode 100644 index 000000000000..713032b05bbc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataMetadataDAOImpl.java @@ -0,0 +1,19 @@ +/** + * 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.eperson.dao.impl; + +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.eperson.RegistrationDataMetadata; +import org.dspace.eperson.dao.RegistrationDataMetadataDAO; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationDataMetadataDAOImpl extends AbstractHibernateDAO + implements RegistrationDataMetadataDAO { +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataChanges.java b/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataChanges.java new file mode 100644 index 000000000000..431fa8496861 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataChanges.java @@ -0,0 +1,49 @@ +/** + * 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.eperson.dto; + +import org.dspace.eperson.RegistrationTypeEnum; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationDataChanges { + + private static final String EMAIL_PATTERN = + "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)" + + "+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"; + + private final String email; + private final RegistrationTypeEnum registrationType; + + public RegistrationDataChanges(String email, RegistrationTypeEnum type) { + if (email == null || email.trim().isBlank()) { + throw new IllegalArgumentException("Cannot update with an empty email address"); + } + if (type == null) { + throw new IllegalArgumentException("Cannot update with a null registration type"); + } + this.email = email; + if (!isValidEmail()) { + throw new IllegalArgumentException("Invalid email address provided!"); + } + this.registrationType = type; + } + + public boolean isValidEmail() { + return email.matches(EMAIL_PATTERN); + } + + public String getEmail() { + return email; + } + + public RegistrationTypeEnum getRegistrationType() { + return registrationType; + } +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataPatch.java b/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataPatch.java new file mode 100644 index 000000000000..e681193d3dd2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataPatch.java @@ -0,0 +1,32 @@ +/** + * 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.eperson.dto; + +import org.dspace.eperson.RegistrationData; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationDataPatch { + + private final RegistrationData oldRegistration; + private final RegistrationDataChanges changes; + + public RegistrationDataPatch(RegistrationData oldRegistration, RegistrationDataChanges changes) { + this.oldRegistration = oldRegistration; + this.changes = changes; + } + + public RegistrationData getOldRegistration() { + return oldRegistration; + } + + public RegistrationDataChanges getChanges() { + return changes; + } +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java b/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java index 2cc0c8c355ef..b39ba1062f6b 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java +++ b/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java @@ -16,6 +16,8 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.dspace.eperson.EPerson; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.dto.RegistrationDataPatch; /** * Methods for handling registration by email and forgotten passwords. When @@ -39,6 +41,10 @@ public void sendRegistrationInfo(Context context, String email, List group public void sendForgotPasswordInfo(Context context, String email, List groups) throws SQLException, IOException, MessagingException, AuthorizeException; + boolean existsAccountFor(Context context, String token) throws SQLException, AuthorizeException; + + boolean existsAccountWithEmail(Context context, String email) throws SQLException; + public EPerson getEPerson(Context context, String token) throws SQLException, AuthorizeException; @@ -48,4 +54,13 @@ public String getEmail(Context context, String token) public void deleteToken(Context context, String token) throws SQLException; + + EPerson mergeRegistration(Context context, UUID userId, String token, List overrides) + throws AuthorizeException, SQLException; + + RegistrationData renewRegistrationForEmail( + Context context, RegistrationDataPatch registrationDataPatch + ) throws AuthorizeException; + + void confirmRegistration(Context context, RegistrationData registrationData); } diff --git a/dspace-api/src/main/java/org/dspace/eperson/service/RegistrationDataMetadataService.java b/dspace-api/src/main/java/org/dspace/eperson/service/RegistrationDataMetadataService.java new file mode 100644 index 000000000000..5e0beacb34cf --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/service/RegistrationDataMetadataService.java @@ -0,0 +1,34 @@ +/** + * 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.eperson.service; + +import java.sql.SQLException; + +import org.dspace.content.MetadataField; +import org.dspace.core.Context; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationDataMetadata; +import org.dspace.service.DSpaceCRUDService; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public interface RegistrationDataMetadataService extends DSpaceCRUDService { + + RegistrationDataMetadata create( + Context context, RegistrationData registrationData, String schema, String element, String qualifier + ) throws SQLException; + + RegistrationDataMetadata create( + Context context, RegistrationData registrationData, MetadataField metadataField + ) throws SQLException; + + RegistrationDataMetadata create( + Context context, RegistrationData registrationData, MetadataField metadataField, String value + ) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/service/RegistrationDataService.java b/dspace-api/src/main/java/org/dspace/eperson/service/RegistrationDataService.java index d1e78fa2bce2..f15ff291e72a 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/service/RegistrationDataService.java +++ b/dspace-api/src/main/java/org/dspace/eperson/service/RegistrationDataService.java @@ -8,13 +8,23 @@ package org.dspace.eperson.service; import java.sql.SQLException; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataValue; import org.dspace.core.Context; +import org.dspace.eperson.EPerson; import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationDataMetadata; +import org.dspace.eperson.RegistrationTypeEnum; +import org.dspace.eperson.dto.RegistrationDataPatch; import org.dspace.service.DSpaceCRUDService; /** - * Service interface class for the RegistrationData object. + * Service interface class for the {@link RegistrationData} object. * The implementation of this class is responsible for all business logic calls for the RegistrationData object and * is autowired by spring * @@ -22,10 +32,41 @@ */ public interface RegistrationDataService extends DSpaceCRUDService { + RegistrationData create(Context context) throws SQLException, AuthorizeException; + + RegistrationData create(Context context, String netId) throws SQLException, AuthorizeException; + + RegistrationData create(Context context, String netId, RegistrationTypeEnum type) + throws SQLException, AuthorizeException; + + RegistrationData clone( + Context context, RegistrationDataPatch registrationDataPatch + ) throws SQLException, AuthorizeException; + public RegistrationData findByToken(Context context, String token) throws SQLException; public RegistrationData findByEmail(Context context, String email) throws SQLException; + RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException; + public void deleteByToken(Context context, String token) throws SQLException; + Stream>> groupEpersonMetadataByRegistrationData( + EPerson ePerson, RegistrationData registrationData + ) throws SQLException; + + void setRegistrationMetadataValue( + Context context, RegistrationData registration, String schema, String element, String qualifier, String value + ) throws SQLException, AuthorizeException; + + RegistrationDataMetadata getMetadataByMetadataString(RegistrationData registrationData, String field); + + void addMetadata(Context context, RegistrationData rd, MetadataField metadataField, String value) + throws SQLException, AuthorizeException; + + void markAsExpired(Context context, RegistrationData registrationData) throws SQLException, AuthorizeException; + + void deleteExpiredRegistrations(Context context) throws SQLException; + + boolean isValid(RegistrationData rd); } diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2023.09.22__registration_data.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2023.09.22__registration_data.sql new file mode 100644 index 000000000000..1e28d8181ecc --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.6_2023.09.22__registration_data.sql @@ -0,0 +1,46 @@ +-- +-- 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/ +-- + +----------------------------------------------------------------------------------- +-- ALTER table registrationdata +----------------------------------------------------------------------------------- + +EXECUTE IMMEDIATE 'ALTER TABLE registrationdata DROP CONSTRAINT ' || + QUOTE_IDENT((SELECT CONSTRAINT_NAME + FROM information_schema.key_column_usage + WHERE TABLE_SCHEMA = 'PUBLIC' AND TABLE_NAME = 'REGISTRATIONDATA' AND COLUMN_NAME = 'EMAIL')); + +ALTER TABLE registrationdata +ADD COLUMN registration_type VARCHAR2(255); + +ALTER TABLE registrationdata +ADD COLUMN net_id VARCHAR2(64); + +CREATE SEQUENCE IF NOT EXISTS registrationdata_metadatavalue_seq START WITH 1 INCREMENT BY 1; + +----------------------------------------------------------------------------------- +-- Creates table registrationdata_metadata +----------------------------------------------------------------------------------- + +CREATE TABLE registrationdata_metadata ( + registrationdata_metadata_id INTEGER NOT NULL, + registrationdata_id INTEGER, + metadata_field_id INTEGER, + text_value VARCHAR2(2000), + CONSTRAINT pk_registrationdata_metadata PRIMARY KEY (registrationdata_metadata_id) +); + +ALTER TABLE registrationdata_metadata +ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_METADATA_FIELD + FOREIGN KEY (metadata_field_id) + REFERENCES metadatafieldregistry (metadata_field_id); + +ALTER TABLE registrationdata_metadata +ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_REGISTRATIONDATA + FOREIGN KEY (registrationdata_id) + REFERENCES registrationdata (registrationdata_id); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2023.09.22__registration_data.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2023.09.22__registration_data.sql new file mode 100644 index 000000000000..003d9ee936d4 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.6_2023.09.22__registration_data.sql @@ -0,0 +1,55 @@ +-- +-- 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/ +-- + +----------------------------------------------------------------------------------- +-- ALTER table registrationdata +----------------------------------------------------------------------------------- + +SET @constraint_name = QUOTE_IDENT( + SELECT DISTINCT constraint_name + FROM information_schema.constraints + WHERE table_schema = 'public' + AND table_name = 'registrationdata' + AND constraint_type = 'UNIQUE' + AND column_list = 'email'); + +SET @command = 'ALTER TABLE public.registrationdata DROP CONSTRAINT public.' || @constraint_name; + +SELECT @command; + +EXECUTE IMMEDIATE @command; + +ALTER TABLE registrationdata +ADD COLUMN registration_type VARCHAR(255); + +ALTER TABLE registrationdata +ADD COLUMN net_id VARCHAR(64); + +CREATE SEQUENCE IF NOT EXISTS registrationdata_metadatavalue_seq START WITH 1 INCREMENT BY 1; + +----------------------------------------------------------------------------------- +-- Creates table registrationdata_metadata +----------------------------------------------------------------------------------- + +CREATE TABLE registrationdata_metadata ( + registrationdata_metadata_id INTEGER NOT NULL, + registrationdata_id INTEGER, + metadata_field_id INTEGER, + text_value OID, + CONSTRAINT pk_registrationdata_metadata PRIMARY KEY (registrationdata_metadata_id) +); + +ALTER TABLE registrationdata_metadata +ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_METADATA_FIELD + FOREIGN KEY (metadata_field_id) + REFERENCES metadatafieldregistry (metadata_field_id); + +ALTER TABLE registrationdata_metadata +ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_REGISTRATIONDATA + FOREIGN KEY (registrationdata_id) + REFERENCES registrationdata (registrationdata_id); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonGroupRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonGroupRestController.java index 8e098d28d2e7..ec101bf6d7f8 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonGroupRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonGroupRestController.java @@ -42,6 +42,18 @@ public class EPersonGroupRestController implements InitializingBean { private ConverterService converter; @Autowired private CollectionRestRepository collectionRestRepository; + + /** + * This request can be used to join a user to a target group by using a registration data token will be replaced + * by the {@link EPersonRegistrationRestController} features. + * + * @param context + * @param uuid + * @param token + * @return + * @throws Exception + */ + @Deprecated @RequestMapping(method = RequestMethod.POST, value = EPersonRest.CATEGORY + "/" + EPersonRest.PLURAL_NAME + "/{uuid}/" + EPersonRest.GROUPS) public ResponseEntity> joinUserToGroups(Context context, diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonRegistrationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonRegistrationRestController.java new file mode 100644 index 000000000000..db2bee32de3c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonRegistrationRestController.java @@ -0,0 +1,76 @@ +/** + * 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.app.rest; + +import java.util.List; +import java.util.UUID; +import javax.validation.constraints.NotNull; + +import org.dspace.app.rest.converter.ConverterService; +import org.dspace.app.rest.model.EPersonRest; +import org.dspace.app.rest.model.hateoas.EPersonResource; +import org.dspace.app.rest.repository.EPersonRestRepository; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.rest.webmvc.ControllerUtils; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +@RestController +@RequestMapping("/api/" + EPersonRest.CATEGORY + "/" + EPersonRest.PLURAL_NAME) +public class EPersonRegistrationRestController { + + @Autowired + private EPersonRestRepository ePersonRestRepository; + + @Autowired + private ConverterService converter; + + /** + * This method will merge the data coming from a {@link org.dspace.eperson.RegistrationData} into the current + * logged-in user. + *
      + * The request must have an empty body, and a token parameter should be provided: + *
      +     *  
      +     *   curl -X POST http://${dspace.url}/api/eperson/epersons/${id-eperson}?token=${token}&override=${metadata-fields}
      +     *        -H "Content-Type: application/json"
      +     *        -H "Authorization: Bearer ${bearer-token}"
      +     *  
      +     * 
      + * @param context dspace context + * @param uuid uuid of the eperson + * @param token registration token + * @param override fields to override inside from the registration data to the eperson + * @return + * @throws Exception + */ + @RequestMapping(method = RequestMethod.POST, value = "/{uuid}") + public ResponseEntity> post( + Context context, + @PathVariable String uuid, + @RequestParam @NotNull String token, + @RequestParam(required = false) List override + ) throws Exception { + EPersonRest epersonRest = + ePersonRestRepository.mergeFromRegistrationData(context, UUID.fromString(uuid), token, override); + EPersonResource resource = converter.toResource(epersonRest); + return ControllerUtils.toResponseEntity(HttpStatus.CREATED, new HttpHeaders(), resource); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java index 76aca4be231d..da47f3d8b659 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java @@ -35,7 +35,7 @@ * Converter to translate between lists of domain {@link MetadataValue}s and {@link MetadataRest} representations. */ @Component -public class MetadataConverter implements DSpaceConverter { +public class MetadataConverter implements DSpaceConverter> { @Autowired private ContentServiceFactory contentServiceFactory; @@ -46,7 +46,7 @@ public class MetadataConverter implements DSpaceConverter convert(MetadataValueList metadataValues, Projection projection) { // Convert each value to a DTO while retaining place order in a map of key -> SortedSet Map> mapOfSortedSets = new HashMap<>(); @@ -60,7 +60,7 @@ public MetadataRest convert(MetadataValueList metadataValues, set.add(converter.toRest(metadataValue, projection)); } - MetadataRest metadataRest = new MetadataRest(); + MetadataRest metadataRest = new MetadataRest<>(); // Populate MetadataRest's map of key -> List while respecting SortedSet's order Map> mapOfLists = metadataRest.getMap(); @@ -80,14 +80,14 @@ public Class getModelClass() { * Sets a DSpace object's domain metadata values from a rest representation. * Any existing metadata value is deleted or overwritten. * - * @param context the context to use. - * @param dso the DSpace object. + * @param context the context to use. + * @param dso the DSpace object. * @param metadataRest the rest representation of the new metadata. - * @throws SQLException if a database error occurs. + * @throws SQLException if a database error occurs. * @throws AuthorizeException if an authorization error occurs. */ public void setMetadata(Context context, T dso, MetadataRest metadataRest) - throws SQLException, AuthorizeException { + throws SQLException, AuthorizeException { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); dsoService.clearMetadata(context, dso, Item.ANY, Item.ANY, Item.ANY, Item.ANY); persistMetadataRest(context, dso, metadataRest, dsoService); @@ -97,14 +97,14 @@ public void setMetadata(Context context, T dso, Metadat * Add to a DSpace object's domain metadata values from a rest representation. * Any existing metadata value is preserved. * - * @param context the context to use. - * @param dso the DSpace object. + * @param context the context to use. + * @param dso the DSpace object. * @param metadataRest the rest representation of the new metadata. - * @throws SQLException if a database error occurs. + * @throws SQLException if a database error occurs. * @throws AuthorizeException if an authorization error occurs. */ public void addMetadata(Context context, T dso, MetadataRest metadataRest) - throws SQLException, AuthorizeException { + throws SQLException, AuthorizeException { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); persistMetadataRest(context, dso, metadataRest, dsoService); } @@ -113,33 +113,34 @@ public void addMetadata(Context context, T dso, Metadat * Merge into a DSpace object's domain metadata values from a rest representation. * Any existing metadata value is preserved or overwritten with the new ones * - * @param context the context to use. - * @param dso the DSpace object. + * @param context the context to use. + * @param dso the DSpace object. * @param metadataRest the rest representation of the new metadata. - * @throws SQLException if a database error occurs. + * @throws SQLException if a database error occurs. * @throws AuthorizeException if an authorization error occurs. */ - public void mergeMetadata(Context context, T dso, MetadataRest metadataRest) - throws SQLException, AuthorizeException { + public void mergeMetadata( + Context context, T dso, MetadataRest metadataRest + ) throws SQLException, AuthorizeException { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); - for (Map.Entry> entry: metadataRest.getMap().entrySet()) { + for (Map.Entry> entry : metadataRest.getMap().entrySet()) { List metadataByMetadataString = dsoService.getMetadataByMetadataString(dso, entry.getKey()); dsoService.removeMetadataValues(context, dso, metadataByMetadataString); } persistMetadataRest(context, dso, metadataRest, dsoService); } - private void persistMetadataRest(Context context, T dso, MetadataRest metadataRest, - DSpaceObjectService dsoService) - throws SQLException, AuthorizeException { - for (Map.Entry> entry: metadataRest.getMap().entrySet()) { + private void persistMetadataRest( + Context context, T dso, MetadataRest metadataRest, DSpaceObjectService dsoService + ) throws SQLException, AuthorizeException { + for (Map.Entry> entry : metadataRest.getMap().entrySet()) { String[] seq = entry.getKey().split("\\."); String schema = seq[0]; String element = seq[1]; String qualifier = seq.length == 3 ? seq[2] : null; - for (MetadataValueRest mvr: entry.getValue()) { + for (MetadataValueRest mvr : entry.getValue()) { dsoService.addMetadata(context, dso, schema, element, qualifier, mvr.getLanguage(), - mvr.getValue(), mvr.getAuthority(), mvr.getConfidence()); + mvr.getValue(), mvr.getAuthority(), mvr.getConfidence()); } } dsoService.update(context, dso); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.java new file mode 100644 index 000000000000..5b742366b582 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.java @@ -0,0 +1,140 @@ +/** + * 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.app.rest.converter; + +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.MetadataRest; +import org.dspace.app.rest.model.RegistrationMetadataRest; +import org.dspace.app.rest.model.RegistrationRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.MetadataValue; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.AccountService; +import org.dspace.eperson.service.RegistrationDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +@Component +public class RegistrationDataConverter implements DSpaceConverter { + + @Autowired + private HttpServletRequest request; + + @Autowired + private RegistrationDataService registrationDataService; + + @Override + public RegistrationRest convert(RegistrationData registrationData, Projection projection) { + + if (registrationData == null) { + return null; + } + + Context context = ContextUtil.obtainContext(request); + + AccountService accountService = EPersonServiceFactory.getInstance().getAccountService(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setId(registrationData.getID()); + registrationRest.setEmail(registrationData.getEmail()); + registrationRest.setNetId(registrationData.getNetId()); + registrationRest.setRegistrationType( + Optional.ofNullable(registrationData.getRegistrationType()) + .map(RegistrationTypeEnum::toString) + .orElse(null) + ); + + EPerson ePerson = null; + try { + ePerson = accountService.getEPerson(context, registrationData.getToken()); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e); + } + + if (ePerson != null) { + registrationRest.setUser(ePerson.getID()); + try { + MetadataRest metadataRest = getMetadataRest(ePerson, registrationData); + if (registrationData.getEmail() != null) { + metadataRest.put( + "email", + new RegistrationMetadataRest(registrationData.getEmail(), ePerson.getEmail()) + ); + } + registrationRest.setRegistrationMetadata(metadataRest); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } else { + registrationRest.setRegistrationMetadata(getMetadataRest(registrationData)); + } + + registrationRest.setGroupNames(getGroupNames(registrationData)); + registrationRest.setGroups( + registrationData.getGroups().stream().map(Group::getID).collect(Collectors.toList()) + ); + return registrationRest; + } + + + private MetadataRest getMetadataRest(EPerson ePerson, RegistrationData registrationData) + throws SQLException { + return registrationDataService.groupEpersonMetadataByRegistrationData(ePerson, registrationData) + .reduce( + new MetadataRest<>(), + (map, entry) -> map.put( + entry.getKey().getMetadataField().toString('.'), + new RegistrationMetadataRest( + entry.getKey().getValue(), + entry.getValue().map(MetadataValue::getValue).orElse(null) + ) + ), + (m1, m2) -> { + m1.getMap().putAll(m2.getMap()); + return m1; + } + ); + } + + private MetadataRest getMetadataRest(RegistrationData registrationData) { + MetadataRest metadataRest = new MetadataRest<>(); + registrationData.getMetadata().forEach( + (m) -> metadataRest.put( + m.getMetadataField().toString('.'), + new RegistrationMetadataRest(m.getValue()) + ) + ); + return metadataRest; + } + + private List getGroupNames(RegistrationData registrationData) { + return registrationData.getGroups().stream() + .map(Group::getName) + .collect(Collectors.toList()); + } + + @Override + public Class getModelClass() { + return RegistrationData.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java index 1b71eb8957a2..e7b43ebe33c2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java @@ -20,7 +20,7 @@ public abstract class DSpaceObjectRest extends BaseObjectRest { private String name; private String handle; - MetadataRest metadata = new MetadataRest(); + MetadataRest metadata = new MetadataRest<>(); @Override public String getId() { @@ -56,11 +56,11 @@ public void setHandle(String handle) { * * @return the metadata. */ - public MetadataRest getMetadata() { + public MetadataRest getMetadata() { return metadata; } - public void setMetadata(MetadataRest metadata) { + public void setMetadata(MetadataRest metadata) { this.metadata = metadata; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java index d1367c8fea82..072acbcfd71e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java @@ -19,10 +19,10 @@ /** * Rest representation of a map of metadata keys to ordered lists of values. */ -public class MetadataRest { +public class MetadataRest { @JsonAnySetter - private SortedMap> map = new TreeMap(); + private SortedMap> map = new TreeMap(); /** * Gets the map. @@ -30,7 +30,7 @@ public class MetadataRest { * @return the map of keys to ordered values. */ @JsonAnyGetter - public SortedMap> getMap() { + public SortedMap> getMap() { return map; } @@ -44,16 +44,16 @@ public SortedMap> getMap() { * they are passed to this method. * @return this instance, to support chaining calls for easy initialization. */ - public MetadataRest put(String key, MetadataValueRest... values) { + public MetadataRest put(String key, T... values) { // determine highest explicitly ordered value int highest = -1; - for (MetadataValueRest value : values) { + for (T value : values) { if (value.getPlace() > highest) { highest = value.getPlace(); } } // add any non-explicitly ordered values after highest - for (MetadataValueRest value : values) { + for (T value : values) { if (value.getPlace() < 0) { highest++; value.setPlace(highest); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java new file mode 100644 index 000000000000..370bd9027f62 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java @@ -0,0 +1,37 @@ +/** + * 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.app.rest.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationMetadataRest extends MetadataValueRest { + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String overrides; + + public RegistrationMetadataRest(String value, String overrides) { + super(); + this.value = value; + this.overrides = overrides; + } + + public RegistrationMetadataRest(String value) { + this(value, null); + } + + public String getOverrides() { + return overrides; + } + + public void setOverrides(String overrides) { + this.overrides = overrides; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java index 191aec88a414..7285a01a4a24 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java @@ -27,14 +27,28 @@ public class RegistrationRest extends RestAddressableModel { public static final String NAME_PLURAL = "registrations"; public static final String CATEGORY = EPERSON; + private Integer id; private String email; private UUID user; + private String registrationType; + private String netId; + @JsonInclude(JsonInclude.Include.NON_NULL) + private MetadataRest registrationMetadata; @JsonInclude(JsonInclude.Include.NON_NULL) private List groupNames = Collections.emptyList(); - private List groups = Collections.emptyList(); + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + /** * Generic getter for the email + * * @return the email value of this RegisterRest */ public String getEmail() { @@ -43,7 +57,8 @@ public String getEmail() { /** * Generic setter for the email - * @param email The email to be set on this RegisterRest + * + * @param email The email to be set on this RegisterRest */ public void setEmail(String email) { this.email = email; @@ -51,6 +66,7 @@ public void setEmail(String email) { /** * Generic getter for the user + * * @return the user value of this RegisterRest */ public UUID getUser() { @@ -59,12 +75,38 @@ public UUID getUser() { /** * Generic setter for the user - * @param user The user to be set on this RegisterRest + * + * @param user The user to be set on this RegisterRest */ public void setUser(UUID user) { this.user = user; } + public String getRegistrationType() { + return registrationType; + } + + public void setRegistrationType(String registrationType) { + this.registrationType = registrationType; + } + + public String getNetId() { + return netId; + } + + public void setNetId(String netId) { + this.netId = netId; + } + + public MetadataRest getRegistrationMetadata() { + return registrationMetadata; + } + + public void setRegistrationMetadata( + MetadataRest registrationMetadata) { + this.registrationMetadata = registrationMetadata; + } + public List getGroups() { return groups; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index 566917854532..bd0f8f4e92f7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java @@ -213,7 +213,7 @@ private EPersonRest createAndReturn(Context context, EPersonRest epersonRest, St } private void checkRequiredProperties(EPersonRest epersonRest) { - MetadataRest metadataRest = epersonRest.getMetadata(); + MetadataRest metadataRest = epersonRest.getMetadata(); if (metadataRest != null) { List epersonFirstName = metadataRest.getMap().get("eperson.firstname"); List epersonLastName = metadataRest.getMap().get("eperson.lastname"); @@ -393,6 +393,30 @@ public EPersonRest joinUserToGroups(UUID uuid, String token) throws AuthorizeExc throw new RuntimeException(e.getMessage()); } } + + public EPersonRest mergeFromRegistrationData( + Context context, UUID uuid, String token, List override + ) throws AuthorizeException { + try { + + if (uuid == null) { + throw new DSpaceBadRequestException("The uuid of the person cannot be null"); + } + + if (token == null) { + throw new DSpaceBadRequestException("You must provide a token for the eperson"); + } + + return converter.toRest( + accountService.mergeRegistration(context, uuid, token, override), + utils.obtainProjection() + ); + } catch (SQLException e) { + log.error(e); + throw new RuntimeException(e); + } + } + @Override public void afterPropertiesSet() throws Exception { discoverableEndpointsService.register(this, Arrays.asList( diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java index 3fbd6c9d9163..a96cd6eedba9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java @@ -14,10 +14,10 @@ import java.util.List; import java.util.Objects; import java.util.UUID; -import java.util.stream.Collectors; import javax.mail.MessagingException; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.BadRequestException; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; @@ -29,6 +29,9 @@ import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.RegistrationRest; +import org.dspace.app.rest.model.patch.Patch; +import org.dspace.app.rest.repository.patch.ResourcePatch; +import org.dspace.app.rest.utils.Utils; import org.dspace.app.util.AuthorizeUtil; import org.dspace.authenticate.service.AuthenticationService; import org.dspace.authorize.AuthorizeException; @@ -61,6 +64,7 @@ public class RegistrationRestRepository extends DSpaceRestRepository resourcePatch; + @Override public RegistrationRest findOne(Context context, Integer integer) { throw new RepositoryMethodNotImplementedException("No implementation found; Method not allowed!", ""); @@ -132,7 +142,7 @@ public RegistrationRest createAndReturn(Context context) { try { if (Objects.isNull(context.getCurrentUser()) || (!authorizeService.isAdmin(context) - && !hasPermission(context, registrationRest.getGroups()))) { + && !hasPermission(context, registrationRest.getGroups()))) { throw new AccessDeniedException("Only admin users can invite new users to join groups"); } } catch (SQLException e) { @@ -143,7 +153,8 @@ public RegistrationRest createAndReturn(Context context) { if (StringUtils.isBlank(accountType) || (!accountType.equalsIgnoreCase(TYPE_FORGOT) && !accountType.equalsIgnoreCase(TYPE_REGISTER))) { throw new IllegalArgumentException(String.format("Needs query param '%s' with value %s or %s indicating " + - "what kind of registration request it is", TYPE_QUERY_PARAM, TYPE_FORGOT, TYPE_REGISTER)); + "what kind of registration request it is", + TYPE_QUERY_PARAM, TYPE_FORGOT, TYPE_REGISTER)); } EPerson eperson = null; try { @@ -155,32 +166,32 @@ public RegistrationRest createAndReturn(Context context) { try { if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail())) { throw new DSpaceBadRequestException("Password cannot be updated for the given EPerson with email: " - + eperson.getEmail()); + + eperson.getEmail()); } accountService.sendForgotPasswordInfo(context, registrationRest.getEmail(), - registrationRest.getGroups()); + registrationRest.getGroups()); } catch (SQLException | IOException | MessagingException | AuthorizeException e) { log.error("Something went wrong with sending forgot password info email: " - + registrationRest.getEmail(), e); + + registrationRest.getEmail(), e); } } else if (accountType.equalsIgnoreCase(TYPE_REGISTER)) { try { String email = registrationRest.getEmail(); if (!AuthorizeUtil.authorizeNewAccountRegistration(context, request)) { throw new AccessDeniedException( - "Registration is disabled, you are not authorized to create a new Authorization"); + "Registration is disabled, you are not authorized to create a new Authorization"); } if (!authenticationService.canSelfRegister(context, request, registrationRest.getEmail())) { throw new UnprocessableEntityException( String.format("Registration is not allowed with email address" + - " %s", email)); + " %s", email)); } accountService.sendRegistrationInfo(context, registrationRest.getEmail(), registrationRest.getGroups()); } catch (SQLException | IOException | MessagingException | AuthorizeException e) { log.error("Something went wrong with sending registration info email: " - + registrationRest.getEmail(), e); + + registrationRest.getEmail(), e); } } return null; @@ -201,16 +212,12 @@ private boolean hasPermission(Context context, List groups) throws SQLExce return true; } - @Override - public Class getDomainClass() { - return RegistrationRest.class; - } - /** * This method will find the RegistrationRest object that is associated with the token given + * * @param token The token to be found and for which a RegistrationRest object will be found - * @return A RegistrationRest object for the given token - * @throws SQLException If something goes wrong + * @return A RegistrationRest object for the given token + * @throws SQLException If something goes wrong * @throws AuthorizeException If something goes wrong */ @SearchRestMethod(name = "findByToken") @@ -221,22 +228,55 @@ public RegistrationRest findByToken(@Parameter(value = "token", required = true) if (registrationData == null) { throw new ResourceNotFoundException("The token: " + token + " couldn't be found"); } - RegistrationRest registrationRest = new RegistrationRest(); - registrationRest.setEmail(registrationData.getEmail()); - EPerson ePerson = accountService.getEPerson(context, token); - if (ePerson != null) { - registrationRest.setUser(ePerson.getID()); + return converter.toRest(registrationData, utils.obtainProjection()); + } + + @Override + public RegistrationRest patch( + HttpServletRequest request, String apiCategory, String model, Integer id, Patch patch + ) throws UnprocessableEntityException, DSpaceBadRequestException { + if (id == null || id <= 0) { + throw new BadRequestException("The id of the registration cannot be null or negative"); + } + if (patch == null || patch.getOperations() == null || patch.getOperations().isEmpty()) { + throw new BadRequestException("Patch request is incomplete: cannot find operations"); + } + String token = request.getParameter("token"); + if (token == null || token.trim().isBlank()) { + throw new AccessDeniedException("The token is required"); + } + Context context = obtainContext(); + + validateToken(context, token); + + try { + resourcePatch.patch(context, registrationDataService.find(context, id), patch.getOperations()); + context.commit(); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return null; + } + + private void validateToken(Context context, String token) { + try { + RegistrationData registrationData = + registrationDataService.findByToken(context, token); + if (registrationData == null || !registrationDataService.isValid(registrationData)) { + throw new AccessDeniedException("The token is invalid"); + } + } catch (SQLException e) { + throw new RuntimeException(e); } - List groupNames = registrationData.getGroups() - .stream().map(Group::getName).collect(Collectors.toList()); - registrationRest.setGroupNames(groupNames); - registrationRest.setGroups(registrationData - .getGroups().stream().map(Group::getID).collect(Collectors.toList())); - return registrationRest; } public void setCaptchaService(CaptchaService captchaService) { this.captchaService = captchaService; } + @Override + public Class getDomainClass() { + return RegistrationRest.class; + } + } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/RegistrationEmailPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/RegistrationEmailPatchOperation.java new file mode 100644 index 000000000000..e92eb4559ce4 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/RegistrationEmailPatchOperation.java @@ -0,0 +1,149 @@ +/** + * 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.app.rest.repository.patch.operation; + +import java.sql.SQLException; +import java.text.MessageFormat; + +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; +import org.dspace.eperson.dto.RegistrationDataChanges; +import org.dspace.eperson.dto.RegistrationDataPatch; +import org.dspace.eperson.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for RegistrationData email patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/eperson/registration/<:registration-id>?token=<:token> -H " + * Content-Type: application/json" -d '[{ "op": "replace", "path": "/email", "value": "new@email"]' + * + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +@Component +public class RegistrationEmailPatchOperation extends PatchOperation { + + /** + * Path in json body of patch that uses this operation + */ + private static final String OPERATION_PATH_EMAIL = "/email"; + + private static final String EMAIL_PATTERN = + "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)" + + "+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"; + + @Autowired + private AccountService accountService; + + @Override + public R perform(Context context, R object, Operation operation) { + checkOperationValue(operation.getValue()); + + RegistrationDataPatch registrationDataPatch; + try { + String email = (String) operation.getValue(); + registrationDataPatch = + new RegistrationDataPatch( + object, + new RegistrationDataChanges( + email, + registrationTypeFor(context, object, email) + ) + ); + } catch (IllegalArgumentException e) { + throw new UnprocessableEntityException( + "Cannot perform the patch operation", + e + ); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + if (!supports(object, operation)) { + throw new UnprocessableEntityException( + MessageFormat.format( + "RegistrationEmailReplaceOperation does not support {0} operation", + operation.getOp() + ) + ); + } + + if (!isOperationAllowed(operation, object)) { + throw new UnprocessableEntityException( + MessageFormat.format( + "Attempting to perform {0} operation over {1} value (e-mail).", + operation.getOp(), + object.getEmail() == null ? "null" : "not null" + ) + ); + } + + + try { + return (R) accountService.renewRegistrationForEmail(context, registrationDataPatch); + } catch (AuthorizeException e) { + throw new DSpaceBadRequestException( + MessageFormat.format( + "Cannot perform {0} operation over {1} value (e-mail).", + operation.getOp(), + object.getEmail() == null ? "null" : "not null" + ), + e + ); + } + } + + private RegistrationTypeEnum registrationTypeFor( + Context context, R object, String email + ) + throws SQLException { + if (accountService.existsAccountWithEmail(context, email)) { + return RegistrationTypeEnum.VALIDATION; + } + return object.getRegistrationType(); + } + + + /** + * Checks whether the email of RegistrationData has an existing value to replace or adds a new value. + * + * @param operation operation to check + * @param registrationData Object on which patch is being done + */ + private boolean isOperationAllowed(Operation operation, RegistrationData registrationData) { + return isReplaceOperationAllowed(operation, registrationData) || + isAddOperationAllowed(operation, registrationData); + } + + private boolean isAddOperationAllowed(Operation operation, RegistrationData registrationData) { + return operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD) && registrationData.getEmail() == null; + } + + private static boolean isReplaceOperationAllowed(Operation operation, RegistrationData registrationData) { + return operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && registrationData.getEmail() != null; + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof RegistrationData && + ( + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) || + operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD) + ) && + operation.getPath().trim().equalsIgnoreCase(OPERATION_PATH_EMAIL)); + } +} + diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java index 9fdef6b050f7..0a50fec20803 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java @@ -7,7 +7,12 @@ */ package org.dspace.app.rest.security; +import static org.dspace.authenticate.OrcidAuthenticationBean.ORCID_AUTH_ATTRIBUTE; +import static org.dspace.authenticate.OrcidAuthenticationBean.ORCID_DEFAULT_REGISTRATION_URL; +import static org.dspace.authenticate.OrcidAuthenticationBean.ORCID_REGISTRATION_TOKEN; + import java.io.IOException; +import java.text.MessageFormat; import java.util.ArrayList; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -43,10 +48,11 @@ public class OrcidLoginFilter extends StatelessLoginFilter { private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); private OrcidAuthenticationBean orcidAuthentication = new DSpace().getServiceManager() - .getServiceByName("orcidAuthentication", OrcidAuthenticationBean.class); + .getServiceByName("orcidAuthentication", + OrcidAuthenticationBean.class); public OrcidLoginFilter(String url, AuthenticationManager authenticationManager, - RestAuthenticationService restAuthenticationService) { + RestAuthenticationService restAuthenticationService) { super(url, authenticationManager, restAuthenticationService); } @@ -64,13 +70,13 @@ public Authentication attemptAuthentication(HttpServletRequest req, HttpServletR @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, - Authentication auth) throws IOException, ServletException { + Authentication auth) throws IOException, ServletException { DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth; log.debug("Orcid authentication successful for EPerson {}. Sending back temporary auth cookie", - dSpaceAuthentication.getName()); + dSpaceAuthentication.getName()); restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, true); @@ -79,26 +85,41 @@ protected void successfulAuthentication(HttpServletRequest req, HttpServletRespo @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, - AuthenticationException failed) throws IOException, ServletException { + AuthenticationException failed) throws IOException, ServletException { Context context = ContextUtil.obtainContext(request); - if (orcidAuthentication.isUsed(context, request)) { - String baseRediredirectUrl = configurationService.getProperty("dspace.ui.url"); - String redirectUrl = baseRediredirectUrl + "/error?status=401&code=orcid.generic-error"; - response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection] - } else { + if (!orcidAuthentication.isUsed(context, request)) { super.unsuccessfulAuthentication(request, response, failed); + return; + } + + String baseRediredirectUrl = configurationService.getProperty("dspace.ui.url"); + String redirectUrl = baseRediredirectUrl + "/error?status=401&code=orcid.generic-error"; + Object registrationToken = request.getAttribute(ORCID_REGISTRATION_TOKEN); + if (registrationToken != null) { + final String orcidRegistrationDataUrl = + configurationService.getProperty("orcid.registration-data.url", ORCID_DEFAULT_REGISTRATION_URL); + redirectUrl = baseRediredirectUrl + MessageFormat.format(orcidRegistrationDataUrl, registrationToken); + if (log.isDebugEnabled()) { + log.debug( + "Orcid authentication failed for user with ORCID {}.", + request.getAttribute(ORCID_AUTH_ATTRIBUTE) + ); + log.debug("Redirecting to {} for registration completion.", redirectUrl); + } } + response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection] } /** * After successful login, redirect to the DSpace URL specified by this Orcid * request (in the "redirectUrl" request parameter). If that 'redirectUrl' is * not valid or trusted for this DSpace site, then return a 400 error. - * @param request - * @param response + * + * @param request + * @param response * @throws IOException */ private void redirectAfterSuccess(HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -126,9 +147,9 @@ private void redirectAfterSuccess(HttpServletRequest request, HttpServletRespons response.sendRedirect(redirectUrl); } else { log.error("Invalid Orcid redirectURL=" + redirectUrl + - ". URL doesn't match hostname of server or UI!"); + ". URL doesn't match hostname of server or UI!"); response.sendError(HttpServletResponse.SC_BAD_REQUEST, - "Invalid redirectURL! Must match server or ui hostname."); + "Invalid redirectURL! Must match server or ui hostname."); } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidLoginFilterIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidLoginFilterIT.java index 4b441b1bc8fc..574efa2c127e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidLoginFilterIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidLoginFilterIT.java @@ -10,10 +10,16 @@ import static java.util.Arrays.asList; import static org.dspace.app.matcher.MetadataValueMatcher.with; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.matches; +import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -21,6 +27,7 @@ import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; @@ -29,11 +36,14 @@ import java.sql.SQLException; import java.text.ParseException; import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.http.Cookie; import com.jayway.jsonpath.JsonPath; import com.nimbusds.jose.JOSEException; import com.nimbusds.jwt.SignedJWT; +import org.dspace.app.rest.matcher.MetadataMatcher; import org.dspace.app.rest.model.AuthnRest; import org.dspace.app.rest.security.OrcidLoginFilter; import org.dspace.app.rest.security.jwt.EPersonClaimProvider; @@ -46,7 +56,10 @@ import org.dspace.content.Item; import org.dspace.content.service.ItemService; import org.dspace.eperson.EPerson; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; import org.dspace.eperson.service.EPersonService; +import org.dspace.eperson.service.RegistrationDataService; import org.dspace.orcid.OrcidToken; import org.dspace.orcid.client.OrcidClient; import org.dspace.orcid.exception.OrcidClientException; @@ -54,6 +67,7 @@ import org.dspace.orcid.service.OrcidTokenService; import org.dspace.services.ConfigurationService; import org.dspace.util.UUIDUtils; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -104,6 +118,9 @@ public class OrcidLoginFilterIT extends AbstractControllerIntegrationTest { @Autowired private OrcidTokenService orcidTokenService; + @Autowired + private RegistrationDataService registrationDataService; + @Before public void setup() { originalOrcidClient = orcidAuthentication.getOrcidClient(); @@ -137,45 +154,75 @@ public void testNoRedirectIfOrcidDisabled() throws Exception { @Test public void testEPersonCreationViaOrcidLogin() throws Exception { - when(orcidClientMock.getAccessToken(CODE)).thenReturn(buildOrcidTokenResponse(ORCID, ACCESS_TOKEN)); - when(orcidClientMock.getPerson(ACCESS_TOKEN, ORCID)).thenReturn(buildPerson("Test", "User", "test@email.it")); - - MvcResult mvcResult = getClient().perform(get("/api/" + AuthnRest.CATEGORY + "/orcid") - .param("code", CODE)) - .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl(configurationService.getProperty("dspace.ui.url"))) - .andExpect(cookie().exists("Authorization-cookie")) - .andReturn(); - - verify(orcidClientMock).getAccessToken(CODE); - verify(orcidClientMock).getPerson(ACCESS_TOKEN, ORCID); - verifyNoMoreInteractions(orcidClientMock); - - String ePersonId = getEPersonIdFromAuthorizationCookie(mvcResult); - - createdEperson = ePersonService.find(context, UUIDUtils.fromString(ePersonId)); - assertThat(createdEperson, notNullValue()); - assertThat(createdEperson.getEmail(), equalTo("test@email.it")); - assertThat(createdEperson.getFullName(), equalTo("Test User")); - assertThat(createdEperson.getNetid(), equalTo(ORCID)); - assertThat(createdEperson.canLogIn(), equalTo(true)); - assertThat(createdEperson.getMetadata(), hasItem(with("eperson.orcid", ORCID))); - assertThat(createdEperson.getMetadata(), hasItem(with("eperson.orcid.scope", ORCID_SCOPES[0], 0))); - assertThat(createdEperson.getMetadata(), hasItem(with("eperson.orcid.scope", ORCID_SCOPES[1], 1))); - - assertThat(getOrcidAccessToken(createdEperson), is(ACCESS_TOKEN)); + String defaultProp = configurationService.getProperty("orcid.registration-data.url"); + configurationService.setProperty("orcid.registration-data.url", "/test-redirect?random-token={0}"); + try { + when(orcidClientMock.getAccessToken(CODE)).thenReturn(buildOrcidTokenResponse(ORCID, ACCESS_TOKEN)); + when(orcidClientMock.getPerson(ACCESS_TOKEN, ORCID)).thenReturn(buildPerson("Test", "User", "test@email.it")); + + MvcResult mvcResult = + getClient().perform(get("/api/" + AuthnRest.CATEGORY + "/orcid").param("code", CODE)) + .andExpect(status().is3xxRedirection()) + .andReturn(); + + String redirectedUrl = mvcResult.getResponse().getRedirectedUrl(); + assertThat(redirectedUrl, not(emptyString())); + + verify(orcidClientMock).getAccessToken(CODE); + verify(orcidClientMock).getPerson(ACCESS_TOKEN, ORCID); + verifyNoMoreInteractions(orcidClientMock); + + final Pattern pattern = Pattern.compile("test-redirect\\?random-token=([a-zA-Z0-9]+)"); + final Matcher matcher = pattern.matcher(redirectedUrl); + matcher.find(); + + assertThat(matcher.groupCount(), is(1)); + assertThat(matcher.group(1), not(emptyString())); + + String rdToken = matcher.group(1); + + getClient().perform(get("/api/eperson/registration/search/findByToken") + .param("token", rdToken)) + .andExpect(status().is2xxSuccessful()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.netId", equalTo(ORCID))) + .andExpect(jsonPath("$.registrationType", equalTo(RegistrationTypeEnum.ORCID.toString()))) + .andExpect(jsonPath("$.email", equalTo("test@email.it"))) + .andExpect( + jsonPath("$.registrationMetadata", + Matchers.allOf( + MetadataMatcher.matchMetadata("eperson.orcid", ORCID), + MetadataMatcher.matchMetadata("eperson.firstname", "Test"), + MetadataMatcher.matchMetadata("eperson.lastname", "User") + ) + ) + ); + } finally { + configurationService.setProperty("orcid.registration-data.url", defaultProp); + } } @Test - public void testEPersonCreationViaOrcidLoginWithoutEmail() throws Exception { + public void testRedirectiViaOrcidLoginWithoutEmail() throws Exception { when(orcidClientMock.getAccessToken(CODE)).thenReturn(buildOrcidTokenResponse(ORCID, ACCESS_TOKEN)); when(orcidClientMock.getPerson(ACCESS_TOKEN, ORCID)).thenReturn(buildPerson("Test", "User")); - getClient().perform(get("/api/" + AuthnRest.CATEGORY + "/orcid") - .param("code", CODE)) - .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("http://localhost:4000/error?status=401&code=orcid.generic-error")); + MvcResult orcidLogin = + getClient().perform(get("/api/" + AuthnRest.CATEGORY + "/orcid").param("code", CODE)) + .andExpect(status().is3xxRedirection()) + .andReturn(); + + String redirectedUrl = orcidLogin.getResponse().getRedirectedUrl(); + + assertThat(redirectedUrl, notNullValue()); + + final Pattern pattern = Pattern.compile("external-registration\\?token=([a-zA-Z0-9]+)"); + final Matcher matcher = pattern.matcher(redirectedUrl); + matcher.find(); + + assertThat(matcher.groupCount(), is(1)); + assertThat(matcher.group(1), not(emptyString())); verify(orcidClientMock).getAccessToken(CODE); verify(orcidClientMock).getPerson(ACCESS_TOKEN, ORCID); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java index d597b68a550f..1fad7053512c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java @@ -7,21 +7,37 @@ */ package org.dspace.app.rest; +import static org.dspace.app.rest.repository.RegistrationRestRepository.TOKEN_QUERY_PARAM; import static org.dspace.app.rest.repository.RegistrationRestRepository.TYPE_FORGOT; import static org.dspace.app.rest.repository.RegistrationRestRepository.TYPE_QUERY_PARAM; import static org.dspace.app.rest.repository.RegistrationRestRepository.TYPE_REGISTER; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.sql.SQLException; import java.util.Iterator; import java.util.List; import javax.servlet.http.HttpServletResponse; @@ -30,17 +46,35 @@ import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.matcher.RegistrationMatcher; import org.dspace.app.rest.model.RegistrationRest; +import org.dspace.app.rest.model.patch.AddOperation; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.patch.ReplaceOperation; import org.dspace.app.rest.repository.RegistrationRestRepository; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.authorize.AuthorizeException; import org.dspace.builder.EPersonBuilder; +import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.core.Email; import org.dspace.eperson.CaptchaServiceImpl; +import org.dspace.eperson.EPerson; import org.dspace.eperson.InvalidReCaptchaException; import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; import org.dspace.eperson.dao.RegistrationDataDAO; +import org.dspace.eperson.dto.RegistrationDataPatch; import org.dspace.eperson.service.CaptchaService; +import org.dspace.eperson.service.RegistrationDataService; import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.internal.hamcrest.HamcrestArgumentMatcher; import org.springframework.beans.factory.annotation.Autowired; public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationTest { @@ -50,9 +84,31 @@ public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationT @Autowired private RegistrationDataDAO registrationDataDAO; @Autowired + private RegistrationDataService registrationDataService; + @Autowired private ConfigurationService configurationService; @Autowired private RegistrationRestRepository registrationRestRepository; + private static MockedStatic emailMockedStatic; + + @After + public void tearDown() throws Exception { + Iterator iterator = registrationDataDAO.findAll(context, RegistrationData.class).iterator(); + while (iterator.hasNext()) { + RegistrationData registrationData = iterator.next(); + registrationDataDAO.delete(context, registrationData); + } + } + + @BeforeClass + public static void init() throws Exception { + emailMockedStatic = Mockito.mockStatic(Email.class); + } + + @AfterClass + public static void tearDownClass() throws Exception { + emailMockedStatic.close(); + } @Test public void findByTokenTestExistingUserTest() throws Exception { @@ -442,4 +498,507 @@ public void accountEndpoint_WrongAccountTypeParam() throws Exception { .andExpect(status().isBadRequest()); } + @Test + public void givenRegistrationData_whenPatchInvalidValue_thenUnprocessableEntityResponse() + throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + registrationRest.setUser(eperson.getID()); + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + // given RegistrationData with email + getClient().perform(post("/api/eperson/registrations") + .param(TYPE_QUERY_PARAM, TYPE_REGISTER) + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + RegistrationData registrationData = + registrationDataService.findByEmail(context, registrationRest.getEmail()); + + assertThat(registrationData, notNullValue()); + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + String token = registrationData.getToken(); + String newMail = null; + String patchContent = getPatchContent( + List.of(new ReplaceOperation("/email", newMail)) + ); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + // then succesful response returned + .andExpect(status().isBadRequest()); + + newMail = "test@email.com"; + patchContent = getPatchContent( + List.of(new AddOperation("/email", newMail)) + ); + + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + // then succesful response returned + .andExpect(status().isUnprocessableEntity()); + + newMail = "invalidemail!!!!"; + patchContent = getPatchContent( + List.of(new ReplaceOperation("/email", newMail)) + ); + + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + // then succesful response returned + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void givenRegistrationData_whenPatchWithInvalidToken_thenUnprocessableEntityResponse() + throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + registrationRest.setUser(eperson.getID()); + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + // given RegistrationData with email + getClient().perform(post("/api/eperson/registrations") + .param(TYPE_QUERY_PARAM, TYPE_REGISTER) + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + RegistrationData registrationData = + registrationDataService.findByEmail(context, registrationRest.getEmail()); + + + assertThat(registrationData, notNullValue()); + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + String token = null; + String newMail = "validemail@email.com"; + String patchContent = getPatchContent( + List.of(new ReplaceOperation("/email", newMail)) + ); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + // then succesful response returned + .andExpect(status().isUnauthorized()); + + token = "notexistingtoken"; + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + // then succesful response returned + .andExpect(status().isUnauthorized()); + + context.turnOffAuthorisationSystem(); + registrationData = context.reloadEntity(registrationData); + registrationDataService.markAsExpired(context, registrationData); + context.commit(); + context.restoreAuthSystemState(); + + registrationData = context.reloadEntity(registrationData); + + assertThat(registrationData.getExpires(), notNullValue()); + + token = registrationData.getToken(); + newMail = "validemail@email.com"; + patchContent = getPatchContent( + List.of(new ReplaceOperation("/email", newMail)) + ); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + // then succesful response returned + .andExpect(status().isUnauthorized()); + } + + @Test + public void givenRegistrationDataWithEmail_whenPatchForReplaceEmail_thenSuccessfullResponse() + throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + registrationRest.setUser(eperson.getID()); + + // given RegistrationData with email + getClient().perform(post("/api/eperson/registrations") + .param(TYPE_QUERY_PARAM, TYPE_REGISTER) + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + RegistrationData registrationData = + registrationDataService.findByEmail(context, registrationRest.getEmail()); + + assertThat(registrationData, notNullValue()); + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + String token = registrationData.getToken(); + String newMail = "vincenzo.mecca@4science.com"; + String patchContent = getPatchContent( + List.of(new ReplaceOperation("/email", newMail)) + ); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + // then succesful response returned + .andExpect(status().is2xxSuccessful()); + } + + @Test + public void givenRegistrationDataWithoutEmail_whenPatchForAddEmail_thenSuccessfullResponse() + throws Exception { + + RegistrationData registrationData = + createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID); + + assertThat(registrationData, notNullValue()); + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + String token = registrationData.getToken(); + String newMail = "vincenzo.mecca@4science.com"; + String patchContent = getPatchContent( + List.of(new AddOperation("/email", newMail)) + ); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + // then succesful response returned + .andExpect(status().is2xxSuccessful()); + } + + @Test + public void givenRegistrationDataWithEmail_whenPatchForReplaceEmail_thenNewRegistrationDataCreated() + throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + registrationRest.setUser(eperson.getID()); + + // given RegistrationData with email + getClient().perform(post("/api/eperson/registrations") + .param(TYPE_QUERY_PARAM, TYPE_REGISTER) + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + RegistrationData registrationData = + registrationDataService.findByEmail(context, registrationRest.getEmail()); + + assertThat(registrationData, notNullValue()); + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + String token = registrationData.getToken(); + String newMail = "vincenzo.mecca@4science.com"; + String patchContent = getPatchContent( + List.of(new ReplaceOperation("/email", newMail)) + ); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + .andExpect(status().is2xxSuccessful()); + + // then email updated with new registration + RegistrationData newRegistration = registrationDataService.findByEmail(context, newMail); + assertThat(newRegistration, notNullValue()); + assertThat(newRegistration.getToken(), not(emptyOrNullString())); + assertThat(newRegistration.getEmail(), equalTo(newMail)); + + assertThat(newRegistration.getEmail(), not(equalTo(registrationData.getEmail()))); + assertThat(newRegistration.getToken(), not(equalTo(registrationData.getToken()))); + + registrationData = context.reloadEntity(registrationData); + assertThat(registrationData, nullValue()); + } + + @Test + public void givenRegistrationDataWithoutEmail_whenPatchForReplaceEmail_thenNewRegistrationDataCreated() + throws Exception { + RegistrationData registrationData = + createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID); + + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + String token = registrationData.getToken(); + String newMail = "vincenzo.mecca@4science.com"; + String patchContent = getPatchContent( + List.of(new AddOperation("/email", newMail)) + ); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + .andExpect(status().is2xxSuccessful()); + + // then email updated with new registration + RegistrationData newRegistration = registrationDataService.findByEmail(context, newMail); + assertThat(newRegistration, notNullValue()); + assertThat(newRegistration.getToken(), not(emptyOrNullString())); + assertThat(newRegistration.getEmail(), equalTo(newMail)); + + assertThat(newRegistration.getEmail(), not(equalTo(registrationData.getEmail()))); + assertThat(newRegistration.getToken(), not(equalTo(registrationData.getToken()))); + + registrationData = context.reloadEntity(registrationData); + assertThat(registrationData, nullValue()); + } + + @Test + public void givenRegistrationDataWithoutEmail_whenPatchForAddEmail_thenExternalLoginSent() throws Exception { + RegistrationData registrationData = + createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID); + + assertThat(registrationData, notNullValue()); + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + String token = registrationData.getToken(); + String newMail = "vincenzo.mecca@4science.com"; + String patchContent = getPatchContent( + List.of(new AddOperation("/email", newMail)) + ); + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + .andExpect(status().is2xxSuccessful()); + + // then verification email sent + verify(spy, times(1)).addRecipient(newMail); + verify(spy).addArgument( + ArgumentMatchers.contains( + RegistrationTypeEnum.ORCID.getLink() + ) + ); + verify(spy, times(1)).send(); + } + + @Test + public void givenRegistrationDataWithEmail_whenPatchForNewEmail_thenExternalLoginSent() throws Exception { + RegistrationData registrationData = + createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID); + + String token = registrationData.getToken(); + String newMail = "vincenzo.mecca@orcid.com"; + String patchContent = getPatchContent( + List.of(new AddOperation("/email", newMail)) + ); + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + .andExpect(status().is2xxSuccessful()); + + verify(spy, times(1)).addRecipient(newMail); + verify(spy).addArgument( + ArgumentMatchers.contains( + registrationData.getRegistrationType().getLink() + ) + ); + verify(spy, times(1)).send(); + + registrationData = registrationDataService.findByEmail(context, newMail); + + assertThat(registrationData, notNullValue()); + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + token = registrationData.getToken(); + newMail = "vincenzo.mecca@4science.com"; + patchContent = getPatchContent( + List.of(new ReplaceOperation("/email", newMail)) + ); + + spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + .andExpect(status().is2xxSuccessful()); + + // then verification email sent + verify(spy, times(1)).addRecipient(newMail); + verify(spy).addArgument( + ArgumentMatchers.contains( + registrationData.getRegistrationType().getLink() + ) + ); + verify(spy, times(1)).send(); + } + + @Test + public void givenRegistrationDataWithEmail_whenPatchForExistingEPersonEmail_thenReviewAccountLinkSent() + throws Exception { + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + registrationRest.setNetId("0000-0000-0000-0000"); + + // given RegistrationData with email + getClient().perform(post("/api/eperson/registrations") + .param(TYPE_QUERY_PARAM, TYPE_REGISTER) + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + RegistrationData registrationData = + registrationDataService.findByEmail(context, registrationRest.getEmail()); + + assertThat(registrationData, notNullValue()); + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + context.turnOffAuthorisationSystem(); + final EPerson vins = + EPersonBuilder.createEPerson(context) + .withEmail("vincenzo.mecca@4science.com") + .withNameInMetadata("Vincenzo", "Mecca") + .withOrcid("0101-0101-0101-0101") + .build(); + context.restoreAuthSystemState(); + + String token = registrationData.getToken(); + String vinsEmail = vins.getEmail(); + String patchContent = getPatchContent( + List.of(new ReplaceOperation("/email", vins.getEmail())) + ); + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + .andExpect(status().is2xxSuccessful()); + + // then verification email sent + verify(spy, times(1)).addRecipient(vinsEmail); + verify(spy).addArgument( + ArgumentMatchers.contains( + RegistrationTypeEnum.VALIDATION.getLink() + ) + ); + verify(spy, times(1)).send(); + } + + @Test + public void givenRegistrationDataWithoutEmail_whenPatchForExistingAccount_thenReviewAccountSent() throws Exception { + RegistrationData registrationData = + createNewRegistrationData("0000-1111-2222-3333", RegistrationTypeEnum.ORCID); + + assertThat(registrationData, notNullValue()); + assertThat(registrationData.getToken(), not(emptyOrNullString())); + + context.turnOffAuthorisationSystem(); + final EPerson vins = + EPersonBuilder.createEPerson(context) + .withEmail("vincenzo.mecca@4science.com") + .withNameInMetadata("Vincenzo", "Mecca") + .withOrcid("0101-0101-0101-0101") + .build(); + context.commit(); + context.restoreAuthSystemState(); + + String token = registrationData.getToken(); + String vinsEmail = vins.getEmail(); + String patchContent = getPatchContent( + List.of(new AddOperation("/email", vins.getEmail())) + ); + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + // when patch for replace email + getClient().perform(patch("/api/eperson/registrations/" + registrationData.getID()) + .param(TOKEN_QUERY_PARAM, token) + .content(patchContent) + .contentType(contentType)) + .andExpect(status().is2xxSuccessful()); + + // then verification email sent + verify(spy, times(1)).addRecipient(vinsEmail); + verify(spy).addArgument( + ArgumentMatchers.contains( + RegistrationTypeEnum.VALIDATION.getLink() + ) + ); + verify(spy, times(1)).send(); + } + + + private RegistrationData createNewRegistrationData( + String netId, RegistrationTypeEnum type + ) throws SQLException, AuthorizeException { + context.turnOffAuthorisationSystem(); + RegistrationData registrationData = + registrationDataService.create(context, netId, type); + context.commit(); + context.restoreAuthSystemState(); + return registrationData; + } + } diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 7db34a16b978..eed43c8ee5b9 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1858,6 +1858,33 @@ google.recaptcha.site-verify = https://www.google.com/recaptcha/api/siteverify # checkbox - The "I'm not a robot" Checkbox requires the user to click a checkbox indicating the user is not a robot. #google.recaptcha.mode = +#------------------------------------------------------------------# +#---------------REGISTRATION DATA CONFIGURATION--------------------# +#------------------------------------------------------------------# + +# Configuration for the duration of the token depending on the type +# the format used should be compatible with the standard DURATION format, +# but without the prefix `PT`: +# +# - PT1H -> 1H // hours +# - PT1M -> 1M // minutes +# - PT1S -> 1S // seconds +# +eperson.registration-data.token.orcid.expiration = 1H +eperson.registration-data.token.validation.expiration = 1H +eperson.registration-data.token.forgot.expiration = 24H +eperson.registration-data.token.register.expiration = 24H +eperson.registration-data.token.invitation.expiration = 24H +eperson.registration-data.token.change_password.expiration = 1H + +# Configuration that enables the schedulable tasks related to the registration +# The property `enabled` should be setted to true to enable it. +eperson.registration-data.scheduler.enabled = true +# Configuration for the task that deletes expired registrations. +# Its value should be compatible with the cron format. +# By default it's scheduled to be run every 15 minutes. +eperson.registration-data.scheduler.expired-registration-data.cron = */15 * * * * + #------------------------------------------------------------------# #-------------------MODULE CONFIGURATIONS--------------------------# #------------------------------------------------------------------# diff --git a/dspace/config/emails/orcid b/dspace/config/emails/orcid new file mode 100644 index 000000000000..f2cd1f50c02c --- /dev/null +++ b/dspace/config/emails/orcid @@ -0,0 +1,22 @@ +## E-mail sent to DSpace users when they try to register with an ORCID account +## +## Parameters: {0} is expanded to a special registration URL +## +## See org.dspace.core.Email for information on the format of this file. +## +#set($subject = "${config.get('dspace.name')} Account Registration") +#set($phone = ${config.get('mail.message.helpdesk.telephone')}) +To complete registration for a DSpace account, please click the link +below: + + ${params[0]} + +If you need assistance with your account, please email + + ${config.get("mail.helpdesk")} +#if( $phone ) + +or call us at ${phone}. +#end + +The DSpace-CRIS Team diff --git a/dspace/config/hibernate.cfg.xml b/dspace/config/hibernate.cfg.xml index 563fd86735bc..8597bedbc34a 100644 --- a/dspace/config/hibernate.cfg.xml +++ b/dspace/config/hibernate.cfg.xml @@ -69,6 +69,7 @@ +