diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index c70f5a2a637..0dbd31b4ed4 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. 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 491039cff83..6b1e6d42a09 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)) 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 14fbb60524f..53c5f9b9916 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/app/filetype/consumer/FileTypeMetadataEnhancerConsumer.java b/dspace-api/src/main/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumer.java new file mode 100644 index 00000000000..b5c51e93e76 --- /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/app/metadata/export/DspaceExportMetadataSchemaException.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/DspaceExportMetadataSchemaException.java new file mode 100644 index 00000000000..1f2cbd824a8 --- /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 00000000000..83b8e94330b --- /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 00000000000..5adfa2a725f --- /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 00000000000..3b07722a4b1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java @@ -0,0 +1,126 @@ +/** + * 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 java.text.MessageFormat; + +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 static String REGISTRY_FILENAME_TEMPLATE = "{0}-types.xml"; + + 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, getFilename(metadataSchema), fis, "application/xml", false + ); + context.restoreAuthSystemState(); + } + } catch (Exception e) { + handler.logError("Problem occured while exporting the schema!", e); + throw e; + } + } + + 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); + } +} 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 00000000000..665dbe15567 --- /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 00000000000..925020a5263 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java @@ -0,0 +1,57 @@ +/** + * 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) { + if (value == null) { + return; + } + 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 00000000000..e0ad541bdb8 --- /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 00000000000..fe7144bda85 --- /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 00000000000..bff2fc77978 --- /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, 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; + + /** + * 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 00000000000..47fd64763ea --- /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 00000000000..4cba081a8a3 --- /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 00000000000..1e4cdb83393 --- /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 00000000000..151c8b28292 --- /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 00000000000..fb4028a2057 --- /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 00000000000..085e8af5f81 --- /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 00000000000..3553cbcba2f --- /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 00000000000..a69d5dfd0fd --- /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 00000000000..ace31288523 --- /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 00000000000..1ace35f4e45 --- /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 00000000000..cd1f35e2ef9 --- /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 00000000000..eea9a09f797 --- /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/main/java/org/dspace/app/util/DCInputsReader.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java index ac803188023..86b45c36794 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(); diff --git a/dspace-api/src/main/java/org/dspace/app/util/TypeBindUtils.java b/dspace-api/src/main/java/org/dspace/app/util/TypeBindUtils.java new file mode 100644 index 00000000000..97104bbb63f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/util/TypeBindUtils.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.util; + +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +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.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Utility methods for the type bind functionality. + * + * @author Francesco Pio Scognamiglio (francescopio.scognamiglio at 4science.com) + * + */ +public class TypeBindUtils { + + private static final ConfigurationService configurationService = DSpaceServicesFactory + .getInstance().getConfigurationService(); + private static final ItemService itemService = ContentServiceFactory + .getInstance().getItemService(); + private static final MetadataAuthorityService metadataAuthorityService = ContentAuthorityServiceFactory + .getInstance().getMetadataAuthorityService(); + + private TypeBindUtils() {} + + /** + * This method gets the field used for type-bind. + * @return the field used for type-bind. + */ + public static String getTypeBindField() { + return configurationService.getProperty("submit.type-bind.field", "dc.type"); + } + + /** + * This method gets the value of the type-bind field from the current item. + * @return the value of the type-bind field from the current item. + */ + public static String getTypeBindValue(InProgressSubmission obj) { + List documentType = itemService.getMetadataByMetadataString( + obj.getItem(), getTypeBindField()); + + // check empty type-bind field + if (documentType == null || documentType.isEmpty() + || StringUtils.isBlank(documentType.get(0).getValue())) { + return null; + } + + MetadataValue typeBindValue = documentType.get(0); + + boolean isAuthorityAllowed = metadataAuthorityService.isAuthorityAllowed( + getTypeBindField().replace(".","_"), Constants.ITEM, obj.getCollection()); + if (isAuthorityAllowed && typeBindValue.getAuthority() != null) { + return typeBindValue.getAuthority(); + } + + return typeBindValue.getValue(); + } + +} 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 f77d7e57119..88797e9b1a7 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,15 @@ * ORCID authentication for DSpace. * * @author Luca Giamminonni (luca.giamminonni at 4science.it) - * */ public class OrcidAuthenticationBean implements AuthenticationMethod { + + public static final String ORCID_DEFAULT_FIRSTNAME = "Unnamed"; + public static final String ORCID_DEFAULT_LASTNAME = ORCID_DEFAULT_FIRSTNAME; 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-login/{0}"; private final static Logger LOGGER = LoggerFactory.getLogger(OrcidAuthenticationBean.class); @@ -78,6 +85,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 +194,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,48 +222,59 @@ 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); + RegistrationData registrationData = + this.registrationDataService.create(context, token.getOrcid(), RegistrationTypeEnum.ORCID); - eperson.setNetid(orcid); + registrationData.setEmail(getEmail(person).orElse(null)); + setOrcidMetadataOnRegistration(context, registrationData, person, token); - eperson.setEmail(email); + registrationDataService.update(context, registrationData); - Optional firstName = getFirstName(person); - if (firstName.isPresent()) { - eperson.setFirstName(context, firstName.get()); - } - - Optional lastName = getLastName(person); - if (lastName.isPresent()) { - eperson.setLastName(context, lastName.get()); - } - eperson.setCanLogIn(true); - eperson.setSelfRegistered(true); - - setOrcidMetadataOnEPerson(context, eperson, token); - - ePersonService.update(context, eperson); - context.setCurrentUser(eperson); + request.setAttribute(ORCID_REGISTRATION_TOKEN, registrationData.getToken()); + context.commit(); 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(); + + setRegistrationMetadata(context, registration, "eperson.firstname", getFirstName(person)); + setRegistrationMetadata(context, registration, "eperson.lastname", getLastName(person)); + registrationDataService.setRegistrationMetadataValue(context, registration, "eperson", "orcid", null, orcid); + + for (String scope : token.getScopeAsArray()) { + registrationDataService.addMetadata(context, registration, "eperson", "orcid", "scope", scope); + } + } + + private void setRegistrationMetadata( + Context context, RegistrationData registration, String metadataString, String value) { + String[] split = metadataString.split("\\."); + String qualifier = split.length > 2 ? split[2] : null; + try { + registrationDataService.setRegistrationMetadataValue( + context, registration, split[0], split[1], qualifier, value + ); + } catch (SQLException | AuthorizeException ex) { + LOGGER.error("An error occurs setting metadata", ex); + throw new RuntimeException(ex); } } @@ -296,16 +317,20 @@ private Optional getEmail(Person person) { return Optional.ofNullable(emails.get(0).getEmail()); } - private Optional getFirstName(Person person) { + private String getFirstName(Person person) { return Optional.ofNullable(person.getName()) - .map(name -> name.getGivenNames()) - .map(givenNames -> givenNames.getContent()); + .map(name -> name.getGivenNames()) + .map(givenNames -> givenNames.getContent()) + .filter(StringUtils::isNotBlank) + .orElse(ORCID_DEFAULT_FIRSTNAME); } - private Optional getLastName(Person person) { + private String getLastName(Person person) { return Optional.ofNullable(person.getName()) - .map(name -> name.getFamilyName()) - .map(givenNames -> givenNames.getContent()); + .map(name -> name.getFamilyName()) + .map(givenNames -> givenNames.getContent()) + .filter(StringUtils::isNotBlank) + .orElse(ORCID_DEFAULT_LASTNAME); } private boolean canSelfRegister() { diff --git a/dspace-api/src/main/java/org/dspace/authority/CrisConsumer.java b/dspace-api/src/main/java/org/dspace/authority/CrisConsumer.java index eec4412c0c9..f5d88e1045e 100644 --- a/dspace-api/src/main/java/org/dspace/authority/CrisConsumer.java +++ b/dspace-api/src/main/java/org/dspace/authority/CrisConsumer.java @@ -255,7 +255,7 @@ private String getFieldKey(MetadataValue metadata) { private Item buildRelatedItem(Context context, Item item, Collection collection, MetadataValue metadata, String entityType, String crisSourceId) throws Exception { - WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, false); + WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, useOfTemplate(metadata)); Item relatedItem = workspaceItem.getItem(); itemService.addMetadata(context, relatedItem, CRIS.getName(), "sourceId", null, null, crisSourceId); if (!hasEntityType(relatedItem, entityType)) { @@ -299,6 +299,17 @@ private boolean isSubmissionEnabled(MetadataValue value) { } } + private boolean useOfTemplate(MetadataValue value) { + + String useOfTemplateByMetadata = "cris.import.submission.enabled.entity." + + getFieldKey(value) + ".use-template"; + if (configurationService.hasProperty(useOfTemplateByMetadata)) { + return configurationService.getBooleanProperty(useOfTemplateByMetadata); + } + + return configurationService.getBooleanProperty("cris.import.submission.enabled.entity.use-template"); + } + private void fillRelatedItem(Context context, MetadataValue metadata, Item relatedItem, boolean alreadyPresent) throws SQLException { 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 f2a8680ee58..b07f23ee23f 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/authority/ChoiceAuthorityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java index 6e080045739..cbc92c3be5f 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); } } 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 9695f9c3255..ca9f42f13a3 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/authority/ItemAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java index 6ec39db9764..173ea83f62a 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 @@ -58,7 +59,7 @@ public class ItemAuthority implements ChoiceAuthority, LinkableEntityAuthority { /** the name assigned to the specific instance by the PluginService, @see {@link NameAwarePlugin} **/ private String authorityName; - private DSpace dspace = new DSpace(); + protected DSpace dspace = new DSpace(); protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); @@ -181,9 +182,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(); @@ -292,4 +292,9 @@ private boolean hasValidExternalSource(String sourceIdentifier) { return false; } + private Context getContext() { + Context context = ContextUtil.obtainCurrentRequestContext(); + return context != null ? context : new Context(); + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/RorOrgUnitAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/RorOrgUnitAuthority.java new file mode 100644 index 00000000000..09f7330b62f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/authority/RorOrgUnitAuthority.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.content.authority; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.dspace.content.authority.factory.ItemAuthorityServiceFactory; +import org.dspace.ror.ROROrgUnitDTO; +import org.dspace.ror.service.RORApiService; +import org.dspace.ror.service.RORApiServiceImpl; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; + +public class RorOrgUnitAuthority extends ItemAuthority { + + private final RORApiService rorApiService = dspace.getSingletonService(RORApiServiceImpl.class); + private final ItemAuthorityServiceFactory itemAuthorityServiceFactory = + dspace.getServiceManager().getServiceByName("itemAuthorityServiceFactory", ItemAuthorityServiceFactory.class); + private final ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); + + private String authorityName; + + @Override + public Choices getMatches(String text, int start, int limit, String locale) { + super.setPluginInstanceName(authorityName); + Choices solrChoices = super.getMatches(text, start, limit, locale); + + return solrChoices.values.length == 0 ? getRORApiMatches(text, start, limit) : solrChoices; + } + + private Choices getRORApiMatches(String text, int start, int limit) { + Choice[] rorApiChoices = getChoiceFromRORQueryResults( + rorApiService.getOrgUnits(text).stream() + .filter(ou -> "active".equals(ou.getStatus())) + .collect(Collectors.toList()) + ).toArray(new Choice[0]); + + int confidenceValue = itemAuthorityServiceFactory.getInstance(authorityName) + .getConfidenceForChoices(rorApiChoices); + + return new Choices(rorApiChoices, start, rorApiChoices.length, confidenceValue, + rorApiChoices.length > (start + limit), 0); + } + + private List getChoiceFromRORQueryResults(List orgUnits) { + return orgUnits + .stream() + .map(orgUnit -> new Choice(composeAuthorityValue(orgUnit.getIdentifier()), orgUnit.getName(), + orgUnit.getName(), buildExtras(orgUnit))) + .collect(Collectors.toList()); + } + + private Map buildExtras(ROROrgUnitDTO orgUnit) { + return new HashMap<>(); + } + + private String composeAuthorityValue(String rorId) { + String prefix = configurationService.getProperty("ror.authority.prefix", "will be referenced::ROR-ID::"); + return prefix + rorId; + } + + @Override + public String getLinkedEntityType() { + return configurationService.getProperty("cris.ItemAuthority." + authorityName + ".entityType"); + } + + @Override + public void setPluginInstanceName(String name) { + authorityName = name; + } + + @Override + public String getPluginInstanceName() { + return authorityName; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dto/MetadataValueDTO.java b/dspace-api/src/main/java/org/dspace/content/dto/MetadataValueDTO.java index 630efd5b028..7bfa8504f90 100644 --- a/dspace-api/src/main/java/org/dspace/content/dto/MetadataValueDTO.java +++ b/dspace-api/src/main/java/org/dspace/content/dto/MetadataValueDTO.java @@ -69,6 +69,14 @@ public MetadataValueDTO(String schema, String element, String qualifier, String this.confidence = confidence; } + public MetadataValueDTO(String metadataField, String value) { + MetadataFieldName fieldName = new MetadataFieldName(metadataField); + this.schema = fieldName.schema; + this.element = fieldName.element; + this.qualifier = fieldName.qualifier; + this.value = value; + } + /** * Constructor for the MetadataValueDTO class * @param schema The schema to be assigned to this MetadataValueDTO object 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 b374861db9a..2945065db4e 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 4d56ddafe73..6f6b33ecaa2 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/enhancer/impl/RelatedEntityItemEnhancer.java b/dspace-api/src/main/java/org/dspace/content/enhancer/impl/RelatedEntityItemEnhancer.java index a5d95582e41..a6c97cc84e6 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/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java index 026b6f375df..cbbfee4fb49 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 ); } } 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 d52287ad363..43b5d0b0971 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/ItemDOIService.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/ItemDOIService.java new file mode 100644 index 00000000000..03229f634a6 --- /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 00000000000..3966566196c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java @@ -0,0 +1,30 @@ +/** + * 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); + } + + 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 00000000000..3039ded0df8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldPrimaryDOI.java @@ -0,0 +1,30 @@ +/** + * 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); + } + + return new String[] {itemDOIService.getPrimaryDOIFromItem(item)}; + } +} 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 00000000000..89d9181c20b --- /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/main/java/org/dspace/content/security/AccessItemMode.java b/dspace-api/src/main/java/org/dspace/content/security/AccessItemMode.java index 2aee66fed1f..e2954bf8f83 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 3fcd8386417..9a472b8a40c 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 4a8b2c31384..99add81e862 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; @@ -55,37 +56,46 @@ 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); - 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; - } - + 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); } } + 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/main/java/org/dspace/content/service/BitstreamService.java b/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java index 3f5b17630a2..85a4fd140e9 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-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java index ab56e4692e3..a2c3056ae38 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java +++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java @@ -7,16 +7,31 @@ */ package org.dspace.discovery; +import java.sql.SQLException; +import java.util.Collection; 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.BitstreamFormat; import org.dspace.content.Bundle; -import org.dspace.content.Item; +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; /** *

      @@ -36,41 +51,272 @@ * * * @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 String solrField; + private final BiFunction> fieldAdder; + + public SolrFieldMetadataMapper( + String metadata, + BiFunction> fieldAdder + ) { + super(); + this.solrField = metadata; + this.fieldAdder = fieldAdder; + } + + 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"; 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_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 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 BiFunction> simpleSolrIndexAdder = + (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 -> { + 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 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(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) { + 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(context, document, ((IndexableItem) indexableObject).getIndexedObject().getBundles()); + } + } + + 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(context, 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(Context context, SolrInputDocument document, List bitstreams) { + if (document != null && bitstreams != null) { + for (Bitstream bitstream : bitstreams) { + + indexBitstreamFields(context, document, bitstream); + + indexBitstreamsMetadatadas(document, bitstream); + } + } + } + + private void indexBitstreamFields(Context context, SolrInputDocument document, Bitstream bitstream) { + addAndHandleException( + simpleSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName() + ); + + Optional.ofNullable(bitstream.getDescription()) + .filter(StringUtils::isNotEmpty) + .ifPresent( + (description) -> + addAndHandleException( + simpleSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description + ) + ); + + try { + Optional formatOptional = + Optional.ofNullable(bitstream.getFormat(context)) + .filter(Objects::nonNull); + + formatOptional + .map(BitstreamFormat::getMIMEType) + .filter(StringUtils::isNotBlank) + .ifPresent(format -> + addAndHandleException( + defaultSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_MIMETYPE, format + ) + ); + + formatOptional + .map(BitstreamFormat::getShortDescription) + .ifPresent(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.ofNullable(bitstream.getChecksum()) + .filter(StringUtils::isNotBlank) + .map(checksum -> bitstream.getChecksumAlgorithm() + ":" + bitstream.getChecksum()) + .ifPresent(checksum -> + addAndHandleException( + defaultSolrIndexAdder, document, bitstream, SOLR_FIELD_NAME_FOR_CHECKSUM, checksum + ) + ); + + Optional.ofNullable(bitstream.getSizeBytes()) + .filter(l -> l > 0) + .map(String::valueOf) + .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) { + 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-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java index 283f101f2ba..8be6aac7e39 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java @@ -11,25 +11,36 @@ 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; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; 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 +61,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 +82,9 @@ public class AccountServiceImpl implements AccountService { @Autowired private AuthenticationService authenticationService; + @Autowired + private MetadataValueService metadataValueService; + protected AccountServiceImpl() { } @@ -79,9 +101,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 +116,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 +130,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 +176,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 +231,239 @@ 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 (!canCreateUserBy(context, registrationData.getRegistrationType())) { + throw new AuthorizeException("Token type invalid for the current user."); + } + + 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); + ePersonService.update(context, eperson); + + context.commit(); + 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()); + } + + + @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 boolean isTokenValidForCreation(RegistrationData registrationData) { + return ( + isExternalRegistrationToken(registrationData.getRegistrationType()) || + isValidationToken(registrationData.getRegistrationType()) + ) && + StringUtils.isNotBlank(registrationData.getNetId()); + } + + private boolean canCreateUserBy(Context context, RegistrationTypeEnum registrationTypeEnum) { + return isValidationToken(registrationTypeEnum) || + canCreateUserFromExternalRegistrationToken(context, registrationTypeEnum); + } + + private static boolean canCreateUserFromExternalRegistrationToken( + Context context, RegistrationTypeEnum registrationTypeEnum + ) { + return context.getCurrentUser() != null && isExternalRegistrationToken(registrationTypeEnum); + } + + private static boolean isExternalRegistrationToken(RegistrationTypeEnum registrationTypeEnum) { + return RegistrationTypeEnum.ORCID.equals(registrationTypeEnum); + } + + private static boolean isValidationToken(RegistrationTypeEnum registrationTypeEnum) { + return RegistrationTypeEnum.VALIDATION_ORCID.equals(registrationTypeEnum); + } + + + 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) { + if (overrides == null || overrides.isEmpty()) { + return Stream.empty(); + } + 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('.') + ) + ).filter(l -> !l.isEmpty()); + 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) || + !isValidationToken(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 +476,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 +484,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 +522,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 +543,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 +563,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 953a3e8bd0a..2c0e1abb823 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 00000000000..3bd8def0c44 --- /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().toLowerCase()); + 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 00000000000..dde8428fe1f --- /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 00000000000..34f0e5590fa --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadataServiceImpl.java @@ -0,0 +1,90 @@ +/** + * 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, String value) throws SQLException { + return create( + context, registrationData, + metadataFieldService.findByElement(context, schema, element, qualifier), + value + ); + } + + @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/RegistrationDataServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataServiceImpl.java index b2727516855..4448cefb1bd 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,26 @@ 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.content.service.MetadataFieldService; 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,18 +40,66 @@ * @author kevinvandevelde at atmire.com */ public class RegistrationDataServiceImpl implements RegistrationDataService { - @Autowired(required = true) + @Autowired() protected RegistrationDataDAO registrationDataDAO; + @Autowired() + protected RegistrationDataMetadataService registrationDataMetadataService; + + @Autowired() + protected MetadataFieldService metadataFieldService; + + 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 { + return registrationDataDAO.create(context, newInstance(netId, type, null)); } + private RegistrationData newInstance(String netId, RegistrationTypeEnum type, String email) { + RegistrationData rd = new RegistrationData(netId); + rd.setToken(Utils.generateHexKey()); + rd.setRegistrationType(type); + rd.setExpires(expirationConfiguration.computeExpirationDate(type)); + rd.setEmail(email); + return rd; + } + + @Override + public RegistrationData clone( + Context context, RegistrationDataPatch registrationDataPatch + ) throws SQLException, AuthorizeException { + RegistrationData old = registrationDataPatch.getOldRegistration(); + RegistrationDataChanges changes = registrationDataPatch.getChanges(); + RegistrationData rd = newInstance(old.getNetId(), changes.getRegistrationType(), changes.getEmail()); + + 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 { @@ -49,12 +111,124 @@ 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 { + + List metadata = + registration.getMetadata() + .stream() + .filter(m -> areEquals(m, schema, element, qualifier)) + .collect(Collectors.toList()); + + if (metadata.size() > 1) { + throw new IllegalStateException("Find more than one registration metadata to update!"); + } + + RegistrationDataMetadata registrationDataMetadata; + if (metadata.isEmpty()) { + registrationDataMetadata = + createMetadata(context, registration, schema, element, qualifier, value); + } else { + registrationDataMetadata = metadata.get(0); + registrationDataMetadata.setValue(value); + } + registrationDataMetadataService.update(context, registrationDataMetadata); + } + + @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 void addMetadata( + Context context, RegistrationData registration, String schema, String element, String qualifier, String value + ) throws SQLException, AuthorizeException { + MetadataField mf = metadataFieldService.findByElement(context, schema, element, qualifier); + 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, + String value + ) { + try { + return registrationDataMetadataService.create( + context, registration, schema, element, qualifier, value + ); + } 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 +249,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 00000000000..28a594742f6 --- /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_ORCID("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 5650c5e5b2b..0bdd6cc17cf 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 00000000000..84ef2989cc4 --- /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 4a15dcc8679..2dd023580dc 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 00000000000..713032b05bb --- /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 00000000000..431fa849686 --- /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 00000000000..e681193d3dd --- /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 2cc0c8c355e..ebfa7fc89d9 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,14 @@ 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; + + + boolean isTokenValidForCreation(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 00000000000..b547c4fca80 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/service/RegistrationDataMetadataService.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.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, String value) 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 d1e78fa2bce..f10da961ca4 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,45 @@ */ 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; + + void addMetadata( + 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/java/org/dspace/external/provider/impl/LiveImportDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/LiveImportDataProvider.java index 21c14813f93..9897639f04a 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); diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/RorOrgUnitDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/RorOrgUnitDataProvider.java new file mode 100644 index 00000000000..76a38796d5f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/RorOrgUnitDataProvider.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.external.provider.impl; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.external.model.ExternalDataObject; +import org.dspace.external.provider.AbstractExternalDataProvider; +import org.dspace.ror.ROROrgUnitDTO; +import org.dspace.ror.service.RORApiService; +import org.springframework.beans.factory.annotation.Autowired; + +public class RorOrgUnitDataProvider extends AbstractExternalDataProvider { + + @Autowired + private RORApiService rorApiService; + + private String sourceIdentifier; + + @Override + public Optional getExternalDataObject(String id) { + return rorApiService.getOrgUnit(id) + .map(this::convertToExternalDataObject); + } + + @Override + public List searchExternalDataObjects(String query, int start, int limit) { + return rorApiService.getOrgUnits(query).stream() + .map(this::convertToExternalDataObject) + .collect(Collectors.toList()); + } + + private ExternalDataObject convertToExternalDataObject(ROROrgUnitDTO orgUnit) { + ExternalDataObject object = new ExternalDataObject(sourceIdentifier); + object.setId(orgUnit.getIdentifier()); + object.setValue(orgUnit.getName()); + object.setDisplayValue(orgUnit.getName()); + object.setMetadata(rorApiService.getMetadataValues(orgUnit)); + return object; + } + + @Override + public boolean supports(String source) { + return StringUtils.equals(sourceIdentifier, source); + } + + @Override + public int getNumberOfResults(String query) { + return searchExternalDataObjects(query, 0, -1).size(); + } + + public void setSourceIdentifier(String sourceIdentifier) { + this.sourceIdentifier = sourceIdentifier; + } + + @Override + public String getSourceIdentifier() { + return sourceIdentifier; + } + +} 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 b70eda960d3..4550a84b1c0 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"; + private 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/main/java/org/dspace/identifier/doi/DataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java index bd5791481e9..43882918cd4 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-api/src/main/java/org/dspace/layout/CrisLayoutBoxTypes.java b/dspace-api/src/main/java/org/dspace/layout/CrisLayoutBoxTypes.java index ad4be3ddc8b..198d0063643 100644 --- a/dspace-api/src/main/java/org/dspace/layout/CrisLayoutBoxTypes.java +++ b/dspace-api/src/main/java/org/dspace/layout/CrisLayoutBoxTypes.java @@ -11,5 +11,6 @@ public enum CrisLayoutBoxTypes { IIIFVIEWER, METADATA, RELATION, - METRICS + METRICS, + COLLECTIONS } diff --git a/dspace-api/src/main/java/org/dspace/layout/service/impl/CrisLayoutBoxServiceImpl.java b/dspace-api/src/main/java/org/dspace/layout/service/impl/CrisLayoutBoxServiceImpl.java index a1bc95b818c..269dd660185 100644 --- a/dspace-api/src/main/java/org/dspace/layout/service/impl/CrisLayoutBoxServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/layout/service/impl/CrisLayoutBoxServiceImpl.java @@ -159,6 +159,8 @@ public boolean hasContent(Context context, CrisLayoutBox box, Item item) { return hasRelationBoxContent(context, box, item); case "METRICS": return hasMetricsBoxContent(context, box, item); + case "COLLECTIONS": + return isOwningCollectionPresent(item); case "IIIFVIEWER": return isIiifEnabled(item); case "METADATA": @@ -246,6 +248,10 @@ private boolean isIiifEnabled(Item item) { new MetadataFieldName("dspace.iiif.enabled"), Item.ANY)); } + private boolean isOwningCollectionPresent(Item item) { + return Objects.nonNull(item.getOwningCollection()); + } + private boolean currentUserIsNotAllowedToReadItem(Context context, Item item) { try { return !authorizeService.authorizeActionBoolean(context, item, Constants.READ); diff --git a/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitDTO.java b/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitDTO.java new file mode 100644 index 00000000000..9eac4494fc5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitDTO.java @@ -0,0 +1,87 @@ +/** + * 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.ror; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.StringUtils; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ROROrgUnitDTO { + + @JsonProperty("id") + private String url; + + private String name; + + private String[] acronyms; + + private String[] aliases; + + private String status; + + private String[] types; + + public String getIdentifier() { + if (StringUtils.isBlank(url)) { + return null; + } + + String[] splittedUrl = url.split("/"); + return splittedUrl[splittedUrl.length - 1]; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String[] getAcronyms() { + return acronyms; + } + + public void setAcronyms(String[] acronyms) { + this.acronyms = acronyms; + } + + public String[] getAliases() { + return aliases; + } + + public void setAliases(String[] aliases) { + this.aliases = aliases; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String[] getTypes() { + return types; + } + + public void setTypes(String[] types) { + this.types = types; + } +} diff --git a/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitListDTO.java b/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitListDTO.java new file mode 100644 index 00000000000..1b3b5c7c959 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitListDTO.java @@ -0,0 +1,36 @@ +/** + * 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.ror; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ROROrgUnitListDTO { + private ROROrgUnitDTO[] items; + + @JsonProperty(value = "number_of_results") + private int total; + + public ROROrgUnitDTO[] getItems() { + return items; + } + + public void setItems(ROROrgUnitDTO[] items) { + this.items = items; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } +} diff --git a/dspace-api/src/main/java/org/dspace/ror/client/RORApiClient.java b/dspace-api/src/main/java/org/dspace/ror/client/RORApiClient.java new file mode 100644 index 00000000000..5e2682c3a00 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/client/RORApiClient.java @@ -0,0 +1,21 @@ +/** + * 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.ror.client; + +import java.util.List; +import java.util.Optional; + +import org.dspace.ror.ROROrgUnitDTO; + +public interface RORApiClient { + + List searchOrganizations(String text); + + Optional findOrganizationByRORId(String rorId); +} diff --git a/dspace-api/src/main/java/org/dspace/ror/client/RORApiClientImpl.java b/dspace-api/src/main/java/org/dspace/ror/client/RORApiClientImpl.java new file mode 100644 index 00000000000..c6934de86d0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/client/RORApiClientImpl.java @@ -0,0 +1,163 @@ +/** + * 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.ror.client; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.http.client.methods.RequestBuilder.get; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.dspace.ror.ROROrgUnitDTO; +import org.dspace.ror.ROROrgUnitListDTO; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +public class RORApiClientImpl implements RORApiClient { + + public static final int TIMEOUT_MS = 15 * 1000; + + @Autowired + private ConfigurationService configurationService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(TIMEOUT_MS) + .setConnectionRequestTimeout(TIMEOUT_MS) + .setSocketTimeout(TIMEOUT_MS) + .build(); + + @Override + public List searchOrganizations(String text) { + RorResponse response = performGetRequest(buildGetWithQueryExact(getRORApiUrl(), text.trim())); + + if (isNotFound(response)) { + return Collections.emptyList(); + } + + if (isNotSuccessful(response)) { + String message = "ROR API request was not successful. " + + "Status: " + response.getStatusCode() + " - Content: " + response.getContent(); + throw new RuntimeException(message); + } + + ROROrgUnitListDTO orgUnits = parseResponse(response, ROROrgUnitListDTO.class); + + return List.of(orgUnits.getItems()); + } + + @Override + public Optional findOrganizationByRORId(String rorId) { + RorResponse response = performGetRequest(buildGetWithRORId(getRORApiUrl(), rorId)); + + if (isNotFound(response)) { + return Optional.empty(); + } + + if (isNotSuccessful(response)) { + String message = "ROR API request was not successful. " + + "Status: " + response.getStatusCode() + " - Content: " + response.getContent(); + throw new RuntimeException(message); + } + + ROROrgUnitDTO orgUnit = parseResponse(response, ROROrgUnitDTO.class); + + return Optional.ofNullable(orgUnit); + } + + private RorResponse performGetRequest(HttpUriRequest request) { + try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { + CloseableHttpResponse httpResponse = httpClient.execute(request); + int statusCode = getStatusCode(httpResponse); + HttpEntity entity = httpResponse.getEntity(); + return new RorResponse(statusCode, getContent(httpResponse)); +// return httpResponse; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private HttpUriRequest buildGetWithRORId(String url, String rorId) { + return get(url + "/" + rorId).setConfig(requestConfig).build(); + } + + private HttpUriRequest buildGetWithQuery(String url, String value) { + return get(url).addParameter("query", value).setConfig(requestConfig).build(); + } + + private HttpUriRequest buildGetWithQueryExact(String url, String value) { + return get(url).addParameter("query", "\"" + value + "\"").setConfig(requestConfig).build(); + } + + private T parseResponse(RorResponse response, Class clazz) { + try { + return objectMapper.readValue(response.getContent(), clazz); + } catch (UnsupportedOperationException | IOException e) { + throw new RuntimeException(e); + } + } + + private String getContent(HttpResponse response) { + try { + HttpEntity entity = response.getEntity(); + return entity != null ? IOUtils.toString(entity.getContent(), UTF_8) : null; + } catch (UnsupportedOperationException | IOException e) { + throw new RuntimeException(e); + } + } + + private boolean isNotSuccessful(RorResponse response) { + int statusCode = response.getStatusCode(); + return statusCode < 200 || statusCode > 299; + } + + private boolean isNotFound(RorResponse response) { + return response.getStatusCode() == HttpStatus.SC_NOT_FOUND; + } + + private int getStatusCode(HttpResponse response) { + return response.getStatusLine().getStatusCode(); + } + + private String getRORApiUrl() { + return configurationService.getProperty("ror.orgunit-import.api-url"); + } + + private static class RorResponse { + private final int statusCode; + private final String content; + + public RorResponse(int statusCode, String content) { + + this.statusCode = statusCode; + this.content = content; + } + + public int getStatusCode() { + return statusCode; + } + + public String getContent() { + return content; + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/ror/service/RORApiService.java b/dspace-api/src/main/java/org/dspace/ror/service/RORApiService.java new file mode 100644 index 00000000000..fffaa14ee77 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/service/RORApiService.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.ror.service; +import java.util.List; +import java.util.Optional; + +import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.ror.ROROrgUnitDTO; + +public interface RORApiService { + + public List getOrgUnits(String query); + + public Optional getOrgUnit(String rorId); + + public List getMetadataValues(ROROrgUnitDTO orgUnit); + + public List getMetadataValues(String rorId); + + public List getMetadataFields(); + +} diff --git a/dspace-api/src/main/java/org/dspace/ror/service/RORApiServiceImpl.java b/dspace-api/src/main/java/org/dspace/ror/service/RORApiServiceImpl.java new file mode 100644 index 00000000000..cff214fe3f1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/service/RORApiServiceImpl.java @@ -0,0 +1,123 @@ +/** + * 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.ror.service; + +import static java.util.Optional.ofNullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.ror.ROROrgUnitDTO; +import org.dspace.ror.client.RORApiClient; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +public class RORApiServiceImpl implements RORApiService { + + private static final String ORGUNIT_MAPPING_PREFIX = "ror.orgunit-import.api.metadata-field."; + + @Autowired + private RORApiClient apiClient; + + @Autowired + private ConfigurationService configurationService; + + @Override + public List getOrgUnits(String query) { + return apiClient.searchOrganizations(query); + } + + @Override + public Optional getOrgUnit(String rorId) { + return apiClient.findOrganizationByRORId(rorId); + } + + @Override + public List getMetadataValues(String rorId) { + return getOrgUnit(rorId) + .map(this::getMetadataValues) + .orElse(getInactiveMetadataField()); + } + + @Override + public List getMetadataFields() { + return configurationService.getPropertyKeys(ORGUNIT_MAPPING_PREFIX).stream() + .map(key -> configurationService.getProperty(key)) + .filter(this::isMetadataField) + .collect(Collectors.toList()); + } + + @Override + public List getMetadataValues(ROROrgUnitDTO orgUnit) { + + List metadataValues = new ArrayList<>(); + + getPersonMetadataField("name") + .flatMap(field -> getMetadataValue(orgUnit.getName(), field)) + .ifPresent(metadataValues::add); + + getPersonMetadataField("acronym") + .flatMap(field -> getMetadataArrayValue(orgUnit.getAcronyms(), field)) + .ifPresent(metadataValues::add); + + getPersonMetadataField("url") + .flatMap(field -> getMetadataValue(orgUnit.getUrl(), field)) + .ifPresent(metadataValues::add); + + getPersonMetadataField("identifier") + .flatMap(field -> getMetadataValue(orgUnit.getIdentifier(), field)) + .ifPresent(metadataValues::add); + + return metadataValues; + + } + + private List getInactiveMetadataField() { + return getPersonMetadataField("active") + .flatMap(field -> getMetadataValue("false", field)) + .map(List::of) + .orElse(List.of()); + } + + private Optional getMetadataValue(String value, String field) { + return Optional.ofNullable(value) + .filter(StringUtils::isNotBlank) + .map(metadataValue -> new MetadataValueDTO(field, metadataValue)); + } + + private Optional getMetadataArrayValue(String[] values, String field) { + String joinedAcronym = Arrays.stream(values) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining("/")); + return StringUtils.isNotEmpty(joinedAcronym) + ? Optional.of(new MetadataValueDTO(field, joinedAcronym)) + : Optional.empty(); + } + + private boolean isMetadataField(String property) { + return property != null && property.contains("."); + } + + private Optional getPersonMetadataField(String fieldName) { + return ofNullable(configurationService.getProperty(ORGUNIT_MAPPING_PREFIX + fieldName)); + } + + public RORApiClient getApiClient() { + return apiClient; + } + + public void setApiClient(RORApiClient apiClient) { + this.apiClient = apiClient; + } +} 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 b429ecbd46e..cc5cac24eab 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 52685b563d9..d4f76a55593 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); 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 422c2405a87..66921d04179 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 00000000000..a04fea3ef41 --- /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 00000000000..ac4767a8570 --- /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 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 3d50ddf66cd..cd9c2dd3c2f 100644 --- a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java +++ b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java @@ -22,6 +22,7 @@ import org.dspace.app.util.DCInputsReader; import org.dspace.app.util.DCInputsReaderException; import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.app.util.TypeBindUtils; import org.dspace.content.Collection; import org.dspace.content.InProgressSubmission; import org.dspace.content.Item; @@ -69,10 +70,10 @@ public List validate(Context context, InProgressSubmission o List errors = new ArrayList<>(); DCInputSet inputConfig = getDCInputSet(config); - String documentTypeValue = getDocumentTypeValue(obj); + String documentType = TypeBindUtils.getTypeBindValue(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); for (DCInput[] row : inputConfig.getFields()) { for (DCInput input : row) { @@ -93,7 +94,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(documentTypeValue) && (!allowedFieldNames.contains(fullFieldname) + if (!input.isAllowedFor(documentType) && (!allowedFieldNames.contains(fullFieldname) && !allowedFieldNames.contains(input.getFieldName()))) { removeMetadataValues(context, obj.getItem(), mdv); } else { @@ -118,18 +119,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(documentTypeValue)) { + 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"); } } @@ -139,7 +140,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(documentTypeValue)) { + if (input.isAllowedFor(documentType)) { // since this field is missing add to list of error // fields addError(errors, ERROR_VALIDATION_REQUIRED, @@ -153,12 +154,6 @@ public List validate(Context context, InProgressSubmission o return errors; } - 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 DCInputSet getDCInputSet(SubmissionStepConfig config) { try { return getInputReader().getInputsByFormName(config.getId()); 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 00000000000..6b4994b6644 --- /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) ON DELETE CASCADE; + +ALTER TABLE registrationdata_metadata +ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_REGISTRATIONDATA + FOREIGN KEY (registrationdata_id) + REFERENCES registrationdata (registrationdata_id) ON DELETE CASCADE; 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 00000000000..69e2d34b4b4 --- /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,52 @@ +-- +-- 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 +----------------------------------------------------------------------------------- + +DO $$ + BEGIN + EXECUTE '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' + )); + end +$$; + +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 TEXT, + 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) ON DELETE CASCADE; + +ALTER TABLE registrationdata_metadata +ADD CONSTRAINT FK_REGISTRATIONDATA_METADATA_ON_REGISTRATIONDATA + FOREIGN KEY (registrationdata_id) + REFERENCES registrationdata (registrationdata_id) ON DELETE CASCADE; 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 00000000000..841a6a03fbd --- /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/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 00000000000..a8ca0b6f5b8 --- /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 00000000000..099f285a32e --- /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/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 08eb9871058..fb8b2863506 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.* 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 0697423578b..fdd7886c477 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/data/dspaceFolder/config/spring/api/test-beans.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml index 87cc17de18a..3b7f8829bfc 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 @@ + + + + + + + + + + @@ -75,6 +85,24 @@ + + + + + + + + + + + + + + + + + + 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 00000000000..bfa29ab330d --- /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-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 00000000000..6ed2279bb1f --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java @@ -0,0 +1,143 @@ +/** + * 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("")); + } + } + + @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-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 5e95c28f65b..eee35a81d04 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); } 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 b4ecc73a0c4..cfe0aa60663 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; @@ -82,6 +90,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 +108,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 +126,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 +2549,303 @@ 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("}")); + } + + + + @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-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java b/dspace-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java index 854ab0fa300..2fc14dbf034 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)); 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 db2be516ae4..4241ba26f22 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-iiif/pom.xml b/dspace-iiif/pom.xml index 97704a14dfe..9a2da1377f9 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index 5c6050d48a4..8758aa67fe2 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -8,7 +8,7 @@ dspace-parent org.dspace - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. 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 00000000000..5c40465f590 --- /dev/null +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/DataciteDOIItemCompilePlugin.java @@ -0,0 +1,75 @@ +/** + * 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 + * DOIs of 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-rdf/pom.xml b/dspace-rdf/pom.xml index c6d887b773c..d36c9d236ec 100644 --- a/dspace-rdf/pom.xml +++ b/dspace-rdf/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. diff --git a/dspace-rest/pom.xml b/dspace-rest/pom.xml index 6a594556068..b17b2205794 100644 --- a/dspace-rest/pom.xml +++ b/dspace-rest/pom.xml @@ -3,7 +3,7 @@ org.dspace dspace-rest war - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT DSpace (Deprecated) REST Webapp DSpace RESTful Web Services API. NOTE: this REST API is DEPRECATED. Please consider using the REST API in the dspace-server-webapp instead! @@ -12,7 +12,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 5ad15c1097b..6c0e8b5cf61 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. 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 8e098d28d2e..ec101bf6d7f 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 00000000000..87b402df9d2 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonRegistrationRestController.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.rest; + +import java.util.List; +import java.util.UUID; +import javax.servlet.http.HttpServletRequest; +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.app.rest.utils.ContextUtil; +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 request httpServletRequest incoming + * @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( + HttpServletRequest request, + @PathVariable String uuid, + @RequestParam @NotNull String token, + @RequestParam(required = false) List override + ) throws Exception { + Context context = ContextUtil.obtainContext(request); + try { + context.turnOffAuthorisationSystem(); + EPersonRest epersonRest = + ePersonRestRepository.mergeFromRegistrationData(context, UUID.fromString(uuid), token, override); + EPersonResource resource = converter.toResource(epersonRest); + return ControllerUtils.toResponseEntity(HttpStatus.CREATED, new HttpHeaders(), resource); + } catch (Exception e) { + throw e; + } finally { + context.restoreAuthSystemState(); + } + } + +} 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 e772aa0abe1..b0286996215 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 e9ba0dff442..40a82068dbc 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/converter/MetadataConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java index 76aca4be231..da47f3d8b65 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 00000000000..5b742366b58 --- /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 1b71eb8957a..e7b43ebe33c 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 d1367c8fea8..072acbcfd71 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 00000000000..370bd9027f6 --- /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 191aec88a41..7285a01a4a2 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 56691785453..ae689807e2d 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 @@ -197,7 +197,7 @@ private EPersonRest createAndReturn(Context context, EPersonRest epersonRest, St throw new DSpaceBadRequestException("The self registered property cannot be set to false using this method" + " with a token"); } - checkRequiredProperties(epersonRest); + checkRequiredProperties(registrationData, epersonRest); // We'll turn off authorisation system because this call isn't admin based as it's token based context.turnOffAuthorisationSystem(); EPerson ePerson = createEPersonFromRestObject(context, epersonRest); @@ -212,8 +212,8 @@ private EPersonRest createAndReturn(Context context, EPersonRest epersonRest, St return converter.toRest(ePerson, utils.obtainProjection()); } - private void checkRequiredProperties(EPersonRest epersonRest) { - MetadataRest metadataRest = epersonRest.getMetadata(); + private void checkRequiredProperties(RegistrationData registration, EPersonRest epersonRest) { + MetadataRest metadataRest = epersonRest.getMetadata(); if (metadataRest != null) { List epersonFirstName = metadataRest.getMap().get("eperson.firstname"); List epersonLastName = metadataRest.getMap().get("eperson.lastname"); @@ -222,12 +222,27 @@ private void checkRequiredProperties(EPersonRest epersonRest) { throw new EPersonNameNotProvidedException(); } } + String password = epersonRest.getPassword(); - if (StringUtils.isBlank(password)) { - throw new DSpaceBadRequestException("A password is required"); + String netId = epersonRest.getNetid(); + if (StringUtils.isBlank(password) && StringUtils.isBlank(netId)) { + throw new DSpaceBadRequestException( + "You must provide a password or register using an external account" + ); + } + + if (StringUtils.isBlank(password) && !canRegisterExternalAccount(registration, epersonRest)) { + throw new DSpaceBadRequestException( + "Cannot register external account with netId: " + netId + ); } } + private boolean canRegisterExternalAccount(RegistrationData registration, EPersonRest epersonRest) { + return accountService.isTokenValidForCreation(registration) && + StringUtils.equals(registration.getNetId(), epersonRest.getNetid()); + } + @Override @PreAuthorize("hasPermission(#id, 'EPERSON', 'READ')") public EPersonRest findOne(Context context, UUID id) { @@ -393,6 +408,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 3fbd6c9d916..3cc2ecb468c 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; @@ -39,6 +42,7 @@ import org.dspace.eperson.Group; import org.dspace.eperson.InvalidReCaptchaException; import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.CaptchaService; import org.dspace.eperson.service.EPersonService; @@ -61,9 +65,10 @@ 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 +143,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 +154,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 +167,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 +213,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 +229,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/ResourcePolicyRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java index 0b77f96b9b5..72ca3f25425 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 + * + * @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"; + + @Autowired + private AccountService accountService; + + @Override + public R perform(Context context, R object, Operation operation) { + checkOperationValue(operation.getValue()); + + RegistrationDataPatch registrationDataPatch; + try { + String email = getTextValue(operation); + 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 static String getTextValue(Operation operation) { + Object value = operation.getValue(); + + if (value instanceof String) { + return ((String) value); + } + + if (value instanceof JsonValueEvaluator) { + return Optional.of((JsonValueEvaluator) value) + .map(JsonValueEvaluator::getValueNode) + .filter(nodes -> !nodes.isEmpty()) + .map(nodes -> nodes.get(0)) + .map(JsonNode::asText) + .orElseThrow(() -> new DSpaceBadRequestException("No value provided for operation")); + } + throw new DSpaceBadRequestException("Invalid patch value for operation!"); + } + + private RegistrationTypeEnum registrationTypeFor( + Context context, R object, String email + ) + throws SQLException { + if (accountService.existsAccountWithEmail(context, email)) { + return RegistrationTypeEnum.VALIDATION_ORCID; + } + 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 9fdef6b050f..0a50fec2080 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/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java index 940773547da..23c8f99e585 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 @@ -40,5 +40,6 @@ public String getRelation() { public void setRelation(String relation) { this.relation = relation; } + } 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 3e366f7cc9d..8fef2b35853 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 39d8a1730c0..fbba8f902ee 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; @@ -24,6 +25,7 @@ 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,6 +42,9 @@ public class TopCategoriesGenerator extends AbstractUsageReportGenerator { @Autowired private SolrLoggerService solrLoggerService; + @Autowired + private ConfigurationService configurationService; + @Autowired private DiscoveryConfigurationService discoveryConfigurationService; @@ -67,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); } @@ -105,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 ")); @@ -130,10 +135,26 @@ public String getReportType() { } public Map getCategoryQueries() { + if (categoryQueries == null) { + return getDefaultCategoryQueries(); + } return categoryQueries; } public void setCategoryQueries(Map categoryQueries) { this.categoryQueries = categoryQueries; } + + 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"); + } + } 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 fa2dc320b87..e43a060bdf8 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 @@ -29,8 +29,11 @@ import org.dspace.app.util.DCInputsReader; import org.dspace.app.util.DCInputsReaderException; import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.app.util.TypeBindUtils; 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.Context; import org.dspace.core.Utils; import org.dspace.services.ConfigurationService; @@ -53,6 +56,10 @@ public class DescribeStep extends AbstractProcessingStep { private final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + private final MetadataAuthorityService metadataAuthorityService = ContentAuthorityServiceFactory + .getInstance() + .getMetadataAuthorityService(); + public DescribeStep() throws DCInputsReaderException { inputReader = DCInputsReaderFactory.getDCInputsReader(); } @@ -72,15 +79,11 @@ 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 documentType = TypeBindUtils.getTypeBindValue(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()) { 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 67983ba8f00..f5ec4428aa1 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 @@ -24,7 +24,7 @@ @Configuration @ComponentScan( {"org.dspace.app.rest.converter", "org.dspace.app.rest.repository", "org.dspace.app.rest.utils", "org.dspace.app.configuration", "org.dspace.iiif", "org.dspace.app.iiif", "org.dspace.app.rest.link", - "org.dspace.app.rest.converter.factory" }) + "org.dspace.app.rest.converter.factory", "org.dspace.app.scheduler" }) public class ApplicationConfig { // Allowed CORS origins ("Access-Control-Allow-Origin" header) // Can be overridden in DSpace configuration @@ -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) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/scheduler/eperson/RegistrationDataScheduler.java b/dspace-server-webapp/src/main/java/org/dspace/app/scheduler/eperson/RegistrationDataScheduler.java new file mode 100644 index 00000000000..49ceeba0dc9 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/scheduler/eperson/RegistrationDataScheduler.java @@ -0,0 +1,60 @@ +/** + * 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.scheduler.eperson; + +import java.sql.SQLException; + +import org.dspace.core.Context; +import org.dspace.eperson.RegistrationData; +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() throws SQLException { + Context context = new Context(); + context.turnOffAuthorisationSystem(); + try { + + registrationDataService.deleteExpiredRegistrations(context); + + context.restoreAuthSystemState(); + context.complete(); + } catch (Exception e) { + context.abort(); + log.error("Failed to delete expired registrations", e); + throw e; + } + } + + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRegistrationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRegistrationRestControllerIT.java new file mode 100644 index 00000000000..cfff06d501f --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRegistrationRestControllerIT.java @@ -0,0 +1,343 @@ +/** + * 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 static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +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 org.dspace.app.rest.matcher.MetadataMatcher; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.EPersonBuilder; +import org.dspace.content.MetadataField; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.core.Email; +import org.dspace.eperson.EPerson; +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.dspace.eperson.service.RegistrationDataService; +import org.hamcrest.Matchers; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class EPersonRegistrationRestControllerIT extends AbstractControllerIntegrationTest { + + private static MockedStatic emailMockedStatic; + + @Autowired + private AccountService accountService; + @Autowired + private RegistrationDataService registrationDataService; + @Autowired + private MetadataFieldService metadataFieldService; + + private RegistrationData orcidRegistration; + private MetadataField orcidMf; + private MetadataField firstNameMf; + private MetadataField lastNameMf; + private EPerson customEPerson; + private String customPassword; + + + @BeforeClass + public static void init() throws Exception { + emailMockedStatic = Mockito.mockStatic(Email.class); + } + + @AfterClass + public static void tearDownClass() throws Exception { + emailMockedStatic.close(); + } + + @Before + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + + orcidRegistration = + registrationDataService.create(context, "0000-0000-0000-0000", RegistrationTypeEnum.ORCID); + + orcidMf = + metadataFieldService.findByElement(context, "eperson", "orcid", null); + firstNameMf = + metadataFieldService.findByElement(context, "eperson", "firstname", null); + lastNameMf = + metadataFieldService.findByElement(context, "eperson", "lastname", null); + + registrationDataService.addMetadata( + context, orcidRegistration, orcidMf, "0000-0000-0000-0000" + ); + registrationDataService.addMetadata( + context, orcidRegistration, firstNameMf, "Vincenzo" + ); + registrationDataService.addMetadata( + context, orcidRegistration, lastNameMf, "Mecca" + ); + + registrationDataService.update(context, orcidRegistration); + + customPassword = "vins-01"; + customEPerson = + EPersonBuilder.createEPerson(context) + .withEmail("vincenzo.mecca@4science.com") + .withNameInMetadata("Vins", "4Science") + .withPassword(customPassword) + .withCanLogin(true) + .build(); + + context.restoreAuthSystemState(); + } + + + @Test + public void givenOrcidToken_whenPostForMerge_thenUnauthorized() throws Exception { + + getClient().perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", orcidRegistration.getToken()) + .param("override", "eperson.firtname,eperson.lastname,eperson.orcid") + ).andExpect(status().isUnauthorized()); + + } + + @Test + public void givenExpiredToken_whenPostForMerge_thenUnauthorized() throws Exception { + + context.turnOffAuthorisationSystem(); + registrationDataService.markAsExpired(context, orcidRegistration); + context.restoreAuthSystemState(); + + getClient().perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", orcidRegistration.getToken()) + .param("override", "eperson.firtname,eperson.lastname,eperson.orcid") + ).andExpect(status().isUnauthorized()); + + } + + @Test + public void givenExpiredToken_whenPostAuthForMerge_thenForbidden() throws Exception { + + context.turnOffAuthorisationSystem(); + registrationDataService.markAsExpired(context, orcidRegistration); + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + + getClient(tokenAdmin).perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", orcidRegistration.getToken()) + .param("override", "eperson.firtname,eperson.lastname,eperson.orcid") + ).andExpect(status().isForbidden()); + + } + + @Test + public void givenValidationRegistration_whenPostAuthDiffersFromIdPathParam_thenForbidden() throws Exception { + + context.turnOffAuthorisationSystem(); + RegistrationData validationRegistration = + registrationDataService.create(context, "0000-0000-0000-0000", RegistrationTypeEnum.VALIDATION_ORCID); + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + + getClient(tokenAdmin).perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", validationRegistration.getToken()) + ).andExpect(status().isForbidden()); + + } + + @Test + public void givenValidationRegistration_whenPostWithoutOverride_thenCreated() throws Exception { + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + context.turnOffAuthorisationSystem(); + RegistrationDataChanges changes = + new RegistrationDataChanges("vincenzo.mecca@4science.com", RegistrationTypeEnum.VALIDATION_ORCID); + RegistrationData validationRegistration = + this.accountService.renewRegistrationForEmail( + context, new RegistrationDataPatch(orcidRegistration, changes) + ); + context.restoreAuthSystemState(); + + String customToken = getAuthToken(customEPerson.getEmail(), customPassword); + + getClient(customToken).perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", validationRegistration.getToken()) + ).andExpect(status().isCreated()); + + } + + @Test + public void givenValidationRegistration_whenPostWithOverride_thenCreated() throws Exception { + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + context.turnOffAuthorisationSystem(); + RegistrationDataChanges changes = + new RegistrationDataChanges("vincenzo.mecca@4science.com", RegistrationTypeEnum.VALIDATION_ORCID); + RegistrationData validationRegistration = + this.accountService.renewRegistrationForEmail( + context, new RegistrationDataPatch(orcidRegistration, changes) + ); + context.restoreAuthSystemState(); + + String customToken = getAuthToken(customEPerson.getEmail(), customPassword); + + getClient(customToken).perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", validationRegistration.getToken()) + .param("override", "eperson.firstname,eperson.lastname") + ).andExpect(status().isCreated()); + + } + + @Test + public void givenValidationRegistration_whenPostWithoutOverride_thenOnlyNewMetadataAdded() throws Exception { + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + context.turnOffAuthorisationSystem(); + RegistrationDataChanges changes = + new RegistrationDataChanges("vincenzo.mecca@4science.com", RegistrationTypeEnum.VALIDATION_ORCID); + RegistrationData validationRegistration = + this.accountService.renewRegistrationForEmail( + context, new RegistrationDataPatch(orcidRegistration, changes) + ); + context.restoreAuthSystemState(); + + String customToken = getAuthToken(customEPerson.getEmail(), customPassword); + + getClient(customToken).perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", validationRegistration.getToken()) + ).andExpect(status().isCreated()) + .andExpect( + jsonPath("$.netid", equalTo("0000-0000-0000-0000")) + ) + .andExpect( + jsonPath("$.metadata", + Matchers.allOf( + MetadataMatcher.matchMetadata("eperson.firstname", "Vins"), + MetadataMatcher.matchMetadata("eperson.lastname", "4Science"), + MetadataMatcher.matchMetadata("eperson.orcid", "0000-0000-0000-0000") + ) + ) + ); + + } + + @Test + public void givenValidationRegistration_whenPostWithOverride_thenMetadataReplaced() throws Exception { + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + context.turnOffAuthorisationSystem(); + RegistrationDataChanges changes = + new RegistrationDataChanges("vincenzo.mecca@4science.com", RegistrationTypeEnum.VALIDATION_ORCID); + RegistrationData validationRegistration = + this.accountService.renewRegistrationForEmail( + context, new RegistrationDataPatch(orcidRegistration, changes) + ); + context.restoreAuthSystemState(); + + String customToken = getAuthToken(customEPerson.getEmail(), customPassword); + + getClient(customToken).perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", validationRegistration.getToken()) + .param("override", "eperson.firstname,eperson.lastname") + ).andExpect(status().isCreated()) + .andExpect( + jsonPath("$.netid", equalTo("0000-0000-0000-0000")) + ) + .andExpect( + jsonPath("$.metadata", + Matchers.allOf( + MetadataMatcher.matchMetadata("eperson.firstname", "Vincenzo"), + MetadataMatcher.matchMetadata("eperson.lastname", "Mecca"), + MetadataMatcher.matchMetadata("eperson.orcid", "0000-0000-0000-0000") + ) + ) + ); + + } + + @Test + public void givenValidationRegistration_whenPostWithOverrideAndMetadataNotFound_thenBadRequest() throws Exception { + + Email spy = Mockito.spy(Email.class); + doNothing().when(spy).send(); + + emailMockedStatic.when(() -> Email.getEmail(any())).thenReturn(spy); + + context.turnOffAuthorisationSystem(); + RegistrationDataChanges changes = + new RegistrationDataChanges("vincenzo.mecca@4science.com", RegistrationTypeEnum.VALIDATION_ORCID); + RegistrationData validationRegistration = + this.accountService.renewRegistrationForEmail( + context, new RegistrationDataPatch(orcidRegistration, changes) + ); + context.restoreAuthSystemState(); + + String customToken = getAuthToken(customEPerson.getEmail(), customPassword); + + getClient(customToken).perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", validationRegistration.getToken()) + .param("override", "eperson.phone") + ).andExpect(status().isBadRequest()); + + context.turnOffAuthorisationSystem(); + MetadataField phoneMf = + metadataFieldService.findByElement(context, "eperson", "phone", null); + + registrationDataService.addMetadata( + context, validationRegistration, phoneMf, "1234567890" + ); + context.restoreAuthSystemState(); + + getClient(customToken).perform( + post("/api/eperson/epersons/" + customEPerson.getID()) + .param("token", validationRegistration.getToken()) + .param("override", "eperson.phone") + ).andExpect(status().isBadRequest()); + + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index fab9dffa461..9fa507226ef 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -36,6 +36,7 @@ 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.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; @@ -66,6 +67,7 @@ import org.dspace.app.rest.model.patch.ReplaceOperation; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.authorize.AuthorizeException; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.EPersonBuilder; @@ -74,10 +76,14 @@ import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.service.MetadataFieldService; import org.dspace.core.I18nUtil; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.PasswordHash; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; import org.dspace.eperson.dao.RegistrationDataDAO; import org.dspace.eperson.service.AccountService; import org.dspace.eperson.service.EPersonService; @@ -112,6 +118,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { @Autowired private ConfigurationService configurationService; + @Autowired + private MetadataFieldService metadataFieldService; + @Test public void createTest() throws Exception { // we should check how to get it from Spring @@ -2988,6 +2997,138 @@ public void postEPersonWithTokenWithEmailPropertyAnonUser() throws Exception { } } + + @Test + public void postEpersonFromOrcidRegistrationToken() throws Exception { + + context.turnOffAuthorisationSystem(); + + String registrationEmail = "vincenzo.mecca@4science.com"; + RegistrationData orcidRegistration = + createRegistrationData(RegistrationTypeEnum.ORCID, registrationEmail); + + context.restoreAuthSystemState(); + + ObjectMapper mapper = new ObjectMapper(); + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(registrationEmail); + ePersonRest.setCanLogIn(true); + ePersonRest.setNetid(orcidRegistration.getNetId()); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + + AtomicReference idRef = new AtomicReference(); + + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", orcidRegistration.getToken()) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andDo(result -> idRef + .set(UUID.fromString(read(result.getResponse().getContentAsString(), "$.id")))); + } finally { + EPersonBuilder.deleteEPerson(idRef.get()); + } + } + + + @Test + public void postEPersonFromOrcidValidationRegistrationToken() throws Exception { + + context.turnOffAuthorisationSystem(); + + String registrationEmail = "vincenzo.mecca@4science.com"; + RegistrationData orcidRegistration = + createRegistrationData(RegistrationTypeEnum.VALIDATION_ORCID, registrationEmail); + + context.restoreAuthSystemState(); + + ObjectMapper mapper = new ObjectMapper(); + EPersonRest ePersonRest = createEPersonRest(registrationEmail, orcidRegistration.getNetId()); + + AtomicReference idRef = new AtomicReference<>(); + + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", orcidRegistration.getToken()) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.uuid", not(empty())), + // is it what you expect? EPerson.getName() returns the email... + //hasJsonPath("$.name", is("Doe John")), + hasJsonPath("$.email", is(registrationEmail)), + hasJsonPath("$.type", is("eperson")), + hasJsonPath("$.netid", is("0000-0000-0000-0000")), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "Vincenzo"), + matchMetadata("eperson.lastname", "Mecca"), + matchMetadata("eperson.orcid", "0000-0000-0000-0000") + ))))) + .andDo(result -> idRef + .set(UUID.fromString(read(result.getResponse().getContentAsString(), "$.id")))); + } finally { + EPersonBuilder.deleteEPerson(idRef.get()); + } + } + + @Test + public void postEpersonNetIdWithoutPasswordNotExternalRegistrationToken() throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + + String newRegisterEmail = "new-register@fake-email.com"; + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(newRegisterEmail); + registrationRest.setNetId("0000-0000-0000-0000"); + getClient().perform(post("/api/eperson/registrations") + .param(TYPE_QUERY_PARAM, TYPE_REGISTER) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(registrationRest))) + .andExpect(status().isCreated()); + + RegistrationData byEmail = registrationDataService.findByEmail(context, newRegisterEmail); + + String newRegisterToken = byEmail.getToken(); + + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(newRegisterEmail); + ePersonRest.setCanLogIn(true); + ePersonRest.setNetid("0000-0000-0000-0000"); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Doe"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("John"); + metadataRest.put("eperson.firstname", firstname); + ePersonRest.setMetadata(metadataRest); + + String token = getAuthToken(admin.getEmail(), password); + + try { + getClient().perform(post("/api/eperson/epersons") + .param("token", newRegisterToken) + .content(mapper.writeValueAsBytes(ePersonRest)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } finally { + context.turnOffAuthorisationSystem(); + registrationDataService.delete(context, byEmail); + context.restoreAuthSystemState(); + } + } + + @Test public void findByMetadataByCommAdminAndByColAdminTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -3534,6 +3675,7 @@ public void patchChangePasswordWithNoCurrentPassword() throws Exception { .andExpect(status().isForbidden()); } + private String buildPasswordAddOperationPatchBody(String password, String currentPassword) { Map value = new HashMap<>(); @@ -3548,4 +3690,51 @@ private String buildPasswordAddOperationPatchBody(String password, String curren } + private static EPersonRest createEPersonRest(String registrationEmail, String netId) { + EPersonRest ePersonRest = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); + ePersonRest.setEmail(registrationEmail); + ePersonRest.setCanLogIn(true); + ePersonRest.setNetid(netId); + MetadataValueRest surname = new MetadataValueRest(); + surname.setValue("Mecca"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); + firstname.setValue("Vincenzo"); + metadataRest.put("eperson.firstname", firstname); + MetadataValueRest orcid = new MetadataValueRest(); + orcid.setValue("0000-0000-0000-0000"); + metadataRest.put("eperson.orcid", orcid); + ePersonRest.setMetadata(metadataRest); + return ePersonRest; + } + + private RegistrationData createRegistrationData(RegistrationTypeEnum validationOrcid, String registrationEmail) + throws SQLException, AuthorizeException { + RegistrationData orcidRegistration = + registrationDataService.create(context, "0000-0000-0000-0000", validationOrcid); + orcidRegistration.setEmail(registrationEmail); + + MetadataField orcidMf = + metadataFieldService.findByElement(context, "eperson", "orcid", null); + MetadataField firstNameMf = + metadataFieldService.findByElement(context, "eperson", "firstname", null); + MetadataField lastNameMf = + metadataFieldService.findByElement(context, "eperson", "lastname", null); + + registrationDataService.addMetadata( + context, orcidRegistration, orcidMf, "0000-0000-0000-0000" + ); + registrationDataService.addMetadata( + context, orcidRegistration, firstNameMf, "Vincenzo" + ); + registrationDataService.addMetadata( + context, orcidRegistration, lastNameMf, "Mecca" + ); + + registrationDataService.update(context, orcidRegistration); + return orcidRegistration; + } + + } 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 4b441b1bc8f..5b167050780 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,9 +10,11 @@ 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.emptyString; import static org.hamcrest.Matchers.equalTo; 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.Mockito.mock; import static org.mockito.Mockito.verify; @@ -21,6 +23,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 +32,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,14 +52,16 @@ import org.dspace.content.Item; import org.dspace.content.service.ItemService; import org.dspace.eperson.EPerson; +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; import org.dspace.orcid.model.OrcidTokenResponseDTO; 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 +112,9 @@ public class OrcidLoginFilterIT extends AbstractControllerIntegrationTest { @Autowired private OrcidTokenService orcidTokenService; + @Autowired + private RegistrationDataService registrationDataService; + @Before public void setup() { originalOrcidClient = orcidAuthentication.getOrcidClient(); @@ -137,45 +148,76 @@ 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-login/([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 d597b68a550..93d963db2c8 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,32 @@ */ 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.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.eq; 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 +41,30 @@ 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.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.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.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.BeforeClass; import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; public class RegistrationRestRepositoryIT extends AbstractControllerIntegrationTest { @@ -50,9 +74,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 +488,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_ORCID.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_ORCID.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-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 68dee1555f7..b0d740142c9 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) @@ -1956,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"); @@ -2445,6 +2479,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") @@ -2575,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()))) @@ -2701,21 +2740,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 +2838,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") diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 8cb88f45781..eab37034a51 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -9,7 +9,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. diff --git a/dspace-sword/pom.xml b/dspace-sword/pom.xml index 16f8a396fbe..eb2ae6288a2 100644 --- a/dspace-sword/pom.xml +++ b/dspace-sword/pom.xml @@ -15,7 +15,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. diff --git a/dspace-swordv2/pom.xml b/dspace-swordv2/pom.xml index b35fb7388a1..90825211945 100644 --- a/dspace-swordv2/pom.xml +++ b/dspace-swordv2/pom.xml @@ -13,7 +13,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. diff --git a/dspace/config/crosswalks/mapConverter-scopusToCoarPublicationTypes.properties b/dspace/config/crosswalks/mapConverter-scopusToCoarPublicationTypes.properties index 35e759cd686..33aa97437dd 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 diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 7b66eaf0437..3bc1867277a 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -93,6 +93,14 @@ + + + + @@ -1668,6 +1676,26 @@ + + + + doi + + + + + + + + + + doi + + + + + diff --git a/dspace/config/crosswalks/template/patent-datacite-xml.template b/dspace/config/crosswalks/template/patent-datacite-xml.template index 22b400e9692..9f31693eb4e 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@ @@ -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 50224aa35cc..414527ee337 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@ @@ -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 22b400e9692..9f31693eb4e 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@ @@ -28,6 +28,7 @@ @dc.identifier.uri@ + @virtual.alternative-doi.dc-identifier-doi@ @dc.description.version@ diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 2fa3578cd90..d78479c2f23 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -281,6 +281,10 @@ 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 ##### # This configuration allows to set a group that will able to # use edit metadata mode @@ -621,7 +625,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 @@ -794,7 +798,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) @@ -874,6 +878,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 @@ -1858,6 +1866,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_orcid.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 = 0 0/15 * * * ? + #------------------------------------------------------------------# #-------------------MODULE CONFIGURATIONS--------------------------# #------------------------------------------------------------------# @@ -1961,3 +1996,4 @@ include = ${module_dir}/external-providers.cfg include = ${module_dir}/pushocr.cfg include = ${module_dir}/pushocr.force.cfg include = ${module_dir}/cleanup-authority-metadata-relation.cfg +include = ${module_dir}/ror.cfg diff --git a/dspace/config/emails/orcid b/dspace/config/emails/orcid new file mode 100644 index 00000000000..f2cd1f50c02 --- /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/emails/validation_orcid b/dspace/config/emails/validation_orcid new file mode 100644 index 00000000000..ec11b708ec5 --- /dev/null +++ b/dspace/config/emails/validation_orcid @@ -0,0 +1,22 @@ +## E-mail sent to DSpace users when they confirm the orcid email address for the 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 confirm your email and create the needed 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 563fd86735b..8597bedbc34 100644 --- a/dspace/config/hibernate.cfg.xml +++ b/dspace/config/hibernate.cfg.xml @@ -69,6 +69,7 @@ +
      @@ -117,6 +118,11 @@ + + + + + @@ -133,6 +139,7 @@ + @@ -184,6 +191,11 @@ + + diff --git a/dspace/config/spring/api/crosswalks.xml b/dspace/config/spring/api/crosswalks.xml index 504645bd83b..34941fe7b0d 100644 --- a/dspace/config/spring/api/crosswalks.xml +++ b/dspace/config/spring/api/crosswalks.xml @@ -522,6 +522,8 @@ + + @@ -530,6 +532,7 @@ + @@ -538,6 +541,8 @@ + + @@ -579,6 +584,7 @@ + diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index 6e759f05295..48b75831f99 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -3654,4 +3654,55 @@ + + + + + + dc.identifier.doi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 43ceb79c4be..1b521ae700c 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -224,6 +224,15 @@
      + + + + + + OrgUnit + + + 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 00000000000..26b9daf7604 --- /dev/null +++ b/dspace/config/spring/api/metadata-schema-export.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/dspace/config/spring/api/pubmed-integration.xml b/dspace/config/spring/api/pubmed-integration.xml index 084d6357974..f316a1c0d2d 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. - diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index a99d580dd90..f4f7c97c001 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/oai/oai.xml b/dspace/config/spring/oai/oai.xml index 8fbc01ede5c..d5d735b2f32 100644 --- a/dspace/config/spring/oai/oai.xml +++ b/dspace/config/spring/oai/oai.xml @@ -31,6 +31,8 @@ the fulltext access condition --> + + @@ -405,16 +408,6 @@ - - - - - - - - - - @@ -449,18 +442,111 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -501,16 +587,6 @@ - - - - - - - - - - @@ -801,6 +877,123 @@
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -890,6 +1083,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/etc/migration/migration_configuration.xls b/dspace/etc/migration/migration_configuration.xls index fe798d79a9b..ae104fa6ce0 100644 Binary files a/dspace/etc/migration/migration_configuration.xls and b/dspace/etc/migration/migration_configuration.xls differ diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 63d68c380bf..39b9f1e7151 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -17,7 +17,7 @@ org.dspace modules - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. diff --git a/dspace/modules/pom.xml b/dspace/modules/pom.xml index 6c2fd62d85e..f9734ad224f 100644 --- a/dspace/modules/pom.xml +++ b/dspace/modules/pom.xml @@ -11,7 +11,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT ../../pom.xml diff --git a/dspace/modules/rest/pom.xml b/dspace/modules/rest/pom.xml index eb5975f6323..05c3a4f5098 100644 --- a/dspace/modules/rest/pom.xml +++ b/dspace/modules/rest/pom.xml @@ -13,7 +13,7 @@ org.dspace modules - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 08ad7961e48..470681a555d 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -13,7 +13,7 @@ just adding new jar in the classloader modules org.dspace - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT .. diff --git a/dspace/pom.xml b/dspace/pom.xml index e943a66aee0..df9f33f5802 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -16,7 +16,7 @@ org.dspace dspace-parent - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index b25632a959f..9f07e8d733d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.dspace dspace-parent pom - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT DSpace Parent Project DSpace-CRIS is an open source extension of DSpace (http://www.dspace.org) providing out of box support for the CRIS / RIMS and moder Institution Repository use cases with advanced features and optimized configurations @@ -958,14 +958,14 @@ org.dspace dspace-rest - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT jar classes org.dspace dspace-rest - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT war @@ -1116,69 +1116,69 @@ org.dspace dspace-api - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT org.dspace dspace-api test-jar - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT test org.dspace.modules additions - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT org.dspace dspace-sword - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT org.dspace dspace-swordv2 - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT org.dspace dspace-oai - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT org.dspace dspace-services - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT org.dspace dspace-server-webapp test-jar - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT test org.dspace dspace-rdf - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT org.dspace dspace-iiif - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT org.dspace dspace-server-webapp - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT jar classes org.dspace dspace-server-webapp - cris-2023.01.01-SNAPSHOT + cris-2023.02.00-SNAPSHOT war