diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index c70f5a2a6370..0dbd31b4ed47 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 491039cff835..ba348be0fe4b 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; @@ -258,7 +258,7 @@ public void setup() throws ParseException { collectionId = commandLine.getOptionValue('c'); filename = commandLine.getOptionValue('f'); - if (commandLine.hasOption('e')) { + if (commandLine.hasOption("er")) { abortOnError = true; } } @@ -266,11 +266,9 @@ public void setup() throws ParseException { @Override public void internalRun() throws Exception { context = new Context(Context.Mode.BATCH_EDIT); - assignCurrentUserInContext(); + assignCurrentUserInContext(context); assignSpecialGroupsInContext(); - context.turnOffAuthorisationSystem(); - InputStream inputStream = handler.getFileStream(context, filename) .orElseThrow(() -> new IllegalArgumentException("Error reading file, the file couldn't be " + "found for filename: " + filename)); @@ -285,6 +283,7 @@ public void internalRun() throws Exception { } try { + context.turnOffAuthorisationSystem(); performImport(inputStream); context.complete(); context.restoreAuthSystemState(); @@ -609,7 +608,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 +743,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 +788,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 +1306,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 +1365,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)) @@ -1601,7 +1603,7 @@ private void rollback() { } } - private void assignCurrentUserInContext() throws SQLException { + protected void assignCurrentUserInContext(Context context) throws SQLException, ParseException { UUID uuid = getEpersonIdentifier(); if (uuid != null) { EPerson ePerson = EPersonServiceFactory.getInstance().getEPersonService().find(context, uuid); diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportCli.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportCli.java index 36da59c7a252..c1399c61413b 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportCli.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportCli.java @@ -7,6 +7,13 @@ */ package org.dspace.app.bulkedit; +import java.sql.SQLException; + +import org.apache.commons.cli.ParseException; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; + /** * Extension of {@link BulkImport} for CLI. * @@ -15,4 +22,24 @@ */ public class BulkImportCli extends BulkImport { + @Override + protected void assignCurrentUserInContext(Context context) throws ParseException { + if (commandLine.hasOption('e')) { + String ePersonEmail = commandLine.getOptionValue('e'); + try { + EPerson ePerson = + EPersonServiceFactory.getInstance().getEPersonService().findByEmail(context, ePersonEmail); + if (ePerson == null) { + super.handler.logError("EPerson not found: " + ePersonEmail); + throw new IllegalArgumentException("Unable to find a user with email: " + ePersonEmail); + } + context.setCurrentUser(ePerson); + } catch (SQLException e) { + throw new IllegalArgumentException("SQLException trying to find user with email: " + ePersonEmail); + } + } else { + throw new ParseException("Required parameter -e missing!"); + } + } + } diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportCliScriptConfiguration.java index f79c03e041e2..5e34f6a58464 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportCliScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportCliScriptConfiguration.java @@ -7,6 +7,8 @@ */ package org.dspace.app.bulkedit; +import org.apache.commons.cli.Options; + /** * Extension of {@link BulkImportScriptConfiguration} for CLI. * @@ -15,5 +17,13 @@ */ public class BulkImportCliScriptConfiguration extends BulkImportScriptConfiguration { + @Override + public Options getOptions() { + Options options = super.getOptions(); + options.addOption("e", "email", true, "email address of user"); + options.getOption("e").setRequired(true); + super.options = options; + return options; + } } diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportScriptConfiguration.java index e2fd7bacd0e1..3530687bf36f 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImportScriptConfiguration.java @@ -52,9 +52,9 @@ public Options getOptions() { options.getOption("f").setType(InputStream.class); options.getOption("f").setRequired(true); - options.addOption("e", "concludeOnError", false, "conclude the import at the first error"); - options.getOption("e").setType(boolean.class); - options.getOption("e").setRequired(false); + options.addOption("er", "concludeOnError", false, "conclude the import at the first error"); + options.getOption("er").setType(boolean.class); + options.getOption("er").setRequired(false); super.options = options; } diff --git a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java index 5a2e62332492..c2ba747ebc19 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/MetadataImport.java @@ -1369,8 +1369,8 @@ private int displayChanges(List changes, boolean changed) { * is the field is defined as authority controlled */ private boolean isAuthorityControlledField(String md) { - String mdf = StringUtils.substringAfter(md, ":"); - mdf = StringUtils.substringBefore(mdf, "["); + String mdf = md.contains(":") ? StringUtils.substringAfter(md, ":") : md; + mdf = mdf.contains("[") ? StringUtils.substringBefore(mdf, "[") : mdf; return metadataAuthorityService.isAuthorityAllowed(mdf.replaceAll("\\.", "_"), Constants.ITEM, null); } diff --git a/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java b/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java index 14fbb60524fb..53c5f9b99166 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; +import org.apache.commons.lang.StringUtils; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; @@ -107,7 +108,12 @@ public void appendValueOnLastRow(String header, String value, String separator) throw new IllegalArgumentException("Unknown header '" + header + "'"); } String cellContent = WorkbookUtils.getCellValue(lastRow, column); - createCell(lastRow, column, isEmpty(cellContent) ? value : cellContent + separator + value); + createCell(lastRow, column, + getValueLimitedByLength(isEmpty(cellContent) ? value : cellContent + separator + value)); + } + + private String getValueLimitedByLength(String value) { + return StringUtils.length(value) > 32726 ? value.substring(0, 32725) + "…" : value; } } diff --git a/dspace-api/src/main/java/org/dspace/app/deduplication/service/impl/SolrDedupServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/deduplication/service/impl/SolrDedupServiceImpl.java index 6f719ff85f2c..cc5c0f2bc861 100644 --- a/dspace-api/src/main/java/org/dspace/app/deduplication/service/impl/SolrDedupServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/deduplication/service/impl/SolrDedupServiceImpl.java @@ -314,6 +314,22 @@ private void fillSignature(Context ctx, DSpaceObject iu, Map plainSignatures = algo.getPlainSignature(iu, ctx); + for (String signature : plainSignatures) { + if (StringUtils.isNotEmpty(signature)) { + String key = "plain_" + algo.getSignatureType() + "_signature"; + if (tmpMapFilter.containsKey(key)) { + List obj = tmpMapFilter.get(key); + obj.add(signature); + tmpMapFilter.put(key, obj); + } else { + List obj = new ArrayList(); + obj.add(signature); + tmpMapFilter.put(key, obj); + } + } + } } } diff --git a/dspace-api/src/main/java/org/dspace/app/deduplication/utils/MD5ValueSignature.java b/dspace-api/src/main/java/org/dspace/app/deduplication/utils/MD5ValueSignature.java index 8b047584bc1b..aacc7aa9ae0e 100644 --- a/dspace-api/src/main/java/org/dspace/app/deduplication/utils/MD5ValueSignature.java +++ b/dspace-api/src/main/java/org/dspace/app/deduplication/utils/MD5ValueSignature.java @@ -6,6 +6,7 @@ * http://www.dspace.org/license/ */ package org.dspace.app.deduplication.utils; + import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -13,6 +14,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.stream.Collectors; import com.ibm.icu.text.CharsetDetector; import com.ibm.icu.text.CharsetMatch; @@ -22,12 +24,15 @@ import org.apache.logging.log4j.Logger; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; +import org.dspace.content.MetadataFieldName; import org.dspace.content.MetadataValue; import org.dspace.content.WorkspaceItem; +import org.dspace.content.dto.MetadataValueDTO; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.ItemService; import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Context; +import org.dspace.external.model.ExternalDataObject; import org.dspace.workflow.WorkflowItem; import org.dspace.workflow.WorkflowItemService; import org.dspace.workflow.factory.WorkflowServiceFactory; @@ -95,6 +100,37 @@ public List getSignature(DSpaceObject item, Context context) { } } + public List getPlainSignature(DSpaceObject item, Context context) { + List result = new ArrayList(); + try { + MessageDigest digester = MessageDigest.getInstance("MD5"); + List values = getMultiValue(item, metadata); + if (values != null) { + for (String value : values) { + if (StringUtils.isNotEmpty(value)) { + String valueNorm = normalize(item, value); + digester.update(valueNorm.getBytes("UTF-8")); + byte[] signature = digester.digest(); + char[] arr = new char[signature.length << 1]; + for (int i = 0; i < signature.length; i++) { + int b = signature[i]; + int idx = i << 1; + arr[idx] = HEX_DIGITS[(b >> 4) & 0xf]; + arr[idx + 1] = HEX_DIGITS[b & 0xf]; + } + String sigString = new String(arr); + result.add(sigString); + } + } + } + return result; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + protected String normalize(DSpaceObject item, Context context, String value) { if (value != null) { String temp = StringUtils.EMPTY; @@ -210,6 +246,70 @@ protected List getMultiValue(DSpaceObject item, String metadata) { return retValue; } + public List getSignature(ExternalDataObject object) { + List result = new ArrayList(); + try { + MessageDigest digester = MessageDigest.getInstance("MD5"); + List values = getMultiValue(object, metadata); + if (values != null) { + for (String value : values) { + if (StringUtils.isNotEmpty(value)) { + String valueNorm = normalize(object, value); + digester.update(valueNorm.getBytes("UTF-8")); + byte[] signature = digester.digest(); + char[] arr = new char[signature.length << 1]; + for (int i = 0; i < signature.length; i++) { + int b = signature[i]; + int idx = i << 1; + arr[idx] = HEX_DIGITS[(b >> 4) & 0xf]; + arr[idx + 1] = HEX_DIGITS[b & 0xf]; + } + String sigString = new String(arr); + result.add(sigString); + } + } + } + return result; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e.getMessage(), e); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + protected List getMultiValue(ExternalDataObject object, String metadata) { + return object.getMetadata() + .stream() + .filter(metadataValueDTO -> + new MetadataFieldName(metadataValueDTO.getSchema(), metadataValueDTO.getElement(), + metadataValueDTO.getQualifier()).toString().equals(metadata)) + .map(MetadataValueDTO::getValue) + .collect(Collectors.toList()); + } + + protected String normalize(ExternalDataObject object, String value) { + String result = value; + if (StringUtils.isEmpty(value)) { + if (StringUtils.isNotEmpty(prefix)) { + result = prefix + object.getId(); + } else { + result = "entity:" + object.getId(); + } + } else { + for (String prefix : ignorePrefix) { + if (value.startsWith(prefix)) { + result = value.substring(prefix.length()); + break; + } + } + if (StringUtils.isNotEmpty(prefix)) { + result = prefix + result; + } + } + + return result; + } + public String getMetadata() { return metadata; } diff --git a/dspace-api/src/main/java/org/dspace/app/deduplication/utils/Signature.java b/dspace-api/src/main/java/org/dspace/app/deduplication/utils/Signature.java index 2bf662b39d75..81a0fb228911 100644 --- a/dspace-api/src/main/java/org/dspace/app/deduplication/utils/Signature.java +++ b/dspace-api/src/main/java/org/dspace/app/deduplication/utils/Signature.java @@ -11,10 +11,15 @@ import org.dspace.content.DSpaceObject; import org.dspace.core.Context; +import org.dspace.external.model.ExternalDataObject; public interface Signature { public List getSignature(/* BrowsableDSpaceObject */DSpaceObject item, Context context); + public List getPlainSignature(DSpaceObject item, Context context); + + public List getSignature(ExternalDataObject object); + public int getResourceTypeID(); public String getSignatureType(); diff --git a/dspace-api/src/main/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumer.java b/dspace-api/src/main/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumer.java new file mode 100644 index 000000000000..b5c51e93e766 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumer.java @@ -0,0 +1,274 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.filetype.consumer; + +import static org.dspace.util.FunctionalUtils.throwingConsumerWrapper; +import static org.dspace.util.FunctionalUtils.throwingMapperWrapper; + +import java.sql.SQLException; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.codec.binary.StringUtils; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataFieldName; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.core.exception.SQLRuntimeException; +import org.dspace.event.Consumer; +import org.dspace.event.Event; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FileTypeMetadataEnhancerConsumer implements Consumer { + + private static final Logger logger = LoggerFactory.getLogger(FileTypeMetadataEnhancerConsumer.class); + + protected static final MetadataFieldName entityTypeMetadata = new MetadataFieldName("dc", "type"); + protected static final MetadataFieldName fileTypeMetadata = new MetadataFieldName("dspace", "file", "type"); + private static final List itemMetadatas = List.of(fileTypeMetadata); + private static final List bitstreamMetadatas = List.of(entityTypeMetadata); + private static final Map bitstreamToItemMetadatasMap = Map.of( + entityTypeMetadata.toString(), fileTypeMetadata + ); + + private BitstreamService bitstreamService; + private ItemService itemService; + + private Set bitstreamAlreadyProcessed = new HashSet<>(); + private Set itemsToProcess = new HashSet<>(); + + @Override + public void initialize() throws Exception { + this.bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); + this.itemService = ContentServiceFactory.getInstance().getItemService(); + } + + @Override + public void consume(Context ctx, Event event) throws Exception { + if (Constants.BITSTREAM == event.getSubjectType()) { + this.handleBitStreamConsumer( + ctx, + Optional.ofNullable((Bitstream) event.getObject(ctx)) + .orElse(this.loadBitstream(ctx, event)), + event + ); + } else if (Constants.ITEM == event.getSubjectType() && Event.CREATE == event.getEventType()) { + this.handleItemConsumer( + ctx, + Optional.ofNullable((Item) event.getObject(ctx)) + .orElse(this.loadItem(ctx, event)) + ); + } else { + logger.warn( + "Can't consume the DSPaceObject with id {}, only BITSTREAM and ITEMS'CREATION events are consumable!", + event.getSubjectID() + ); + } + } + + @Override + public void end(Context ctx) throws Exception { + bitstreamAlreadyProcessed.clear(); + this.itemsToProcess + .stream() + .forEach(item -> this.handleItemConsumer(ctx, item)); + itemsToProcess.clear(); + } + + @Override + public void finish(Context ctx) throws Exception {} + + private Bitstream loadBitstream(Context ctx, Event event) { + Bitstream found = null; + try { + found = this.bitstreamService.find(ctx, event.getSubjectID()); + } catch (SQLException e) { + logger.error("Error while retrieving the bitstream with ID: " + event.getSubjectID(), e); + throw new SQLRuntimeException("Error while retrieving the bitstream with ID: " + event.getSubjectID(), e); + } + return found; + } + + private Item loadItem(Context ctx, Event event) { + Item found = null; + try { + found = this.itemService.find(ctx, event.getSubjectID()); + } catch (SQLException e) { + logger.error("Error while retrieving the bitstream with ID: " + event.getSubjectID(), e); + throw new SQLRuntimeException("Error while retrieving the bitstream with ID: " + event.getSubjectID(), e); + } + return found; + } + + private void handleBitStreamConsumer(Context ctx, Bitstream bitstream, Event event) { + + if (bitstream == null || this.alreadyProcessed(bitstream)) { + return; + } + List bitstreamItems = List.of(); + try { + bitstreamItems = bitstream.getBundles() + .stream() + .filter(bundle -> "ORIGINAL".equals(bundle.getName())) + .map(Bundle::getItems) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + bitstreamAlreadyProcessed.add(bitstream); + bitstreamItems + .stream() + .forEach(item -> this.itemsToProcess.add(item)); + } + } + + private void handleItemConsumer(Context ctx, Item item) { + + if (item == null) { + return; + } + + try { + Item loadedItem = this.itemService.find(ctx, item.getID()); + Map> grouped = + Optional.ofNullable(loadedItem) + .map(i -> i.getBundles("ORIGINAL")) + .filter(bundles -> !bundles.isEmpty()) + .map(bundles -> bundles.get(0)) + .map(Bundle::getBitstreams) + .filter(bitstreams -> !bitstreams.isEmpty()) + .map(bitstreams -> getMetadatasForItem(ctx, bitstreams).collect(Collectors.toList())) + .map(metadatas -> groupByMetadataField(metadatas)) + .filter(metadatas -> !metadatas.isEmpty()) + .orElse(Map.of()); + + this.itemService.removeMetadataValues(ctx, loadedItem, getRemovableMetadatas(loadedItem)); + + grouped + .entrySet() + .stream() + .map(entry -> + Map.entry(bitstreamToItemMetadatasMap.get(entry.getKey().toString('.')), entry.getValue()) + ) + .filter(entry -> entry.getKey() != null) + .forEach( + throwingConsumerWrapper(entry -> + this.addMetadata( + ctx, + loadedItem, + entry.getKey(), + entry.getValue() + ) + ) + ); + + } catch (SQLException e) { + logger.error(MessageFormat.format("Error while processing item {}!", item.getID().toString()), e); + throw new SQLRuntimeException(e); + } + + } + + private void addMetadata(Context ctx, Item loadedItem, MetadataFieldName metadata, List value) + throws SQLException { + this.itemService.addMetadata( + ctx, + loadedItem, + metadata.schema, + metadata.element, + metadata.qualifier, + null, + value + ); + } + + private Stream getMetadatasForItem(Context ctx, List bitstreams) { + return bitstreams + .stream() + .map( + throwingMapperWrapper(bitstream -> + this.bitstreamService.find(ctx, bitstream.getID()), + null + ) + ) + .filter(Objects::nonNull) + .flatMap(bitstream -> filterBitstreamMetadatasForItem(bitstream)); + } + + private Stream filterBitstreamMetadatasForItem(Bitstream bitstream) { + return bitstream.getMetadata() + .stream() + .filter( + metadataFilter( + bitstreamMetadatas + ) + ); + } + + private Map> groupByMetadataField(List metadatas) { + return this.collectByGroupingMetadataFieldMappingValue(metadatas.stream()); + } + + private Map> collectByGroupingMetadataFieldMappingValue(Stream stream) { + return stream + .collect( + Collectors.groupingBy( + MetadataValue::getMetadataField, + Collectors.mapping(MetadataValue::getValue, Collectors.toList()) + ) + ); + } + + private boolean alreadyProcessed(Bitstream bitstream) { + return bitstreamAlreadyProcessed.contains(bitstream); + } + + private List getRemovableMetadatas(DSpaceObject dspaceObject) { + return dspaceObject + .getMetadata() + .stream() + .filter( + metadataFilter( + itemMetadatas + ) + ) + .collect(Collectors.toList()); + } + + private Predicate metadataFilter(List metadataFields) { + return metadata -> + metadataFields + .stream() + .filter(field -> + StringUtils.equals(field.schema, metadata.getSchema()) && + StringUtils.equals(field.element, metadata.getElement()) && + StringUtils.equals(field.qualifier, metadata.getQualifier()) + ) + .findFirst() + .isPresent(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/DspaceExportMetadataSchemaException.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/DspaceExportMetadataSchemaException.java new file mode 100644 index 000000000000..1f2cbd824a80 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/DspaceExportMetadataSchemaException.java @@ -0,0 +1,23 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class DspaceExportMetadataSchemaException extends Exception { + + public DspaceExportMetadataSchemaException(Exception e) { + super(e); + } + + public DspaceExportMetadataSchemaException(String message, Exception e) { + super(message, e); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScript.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScript.java new file mode 100644 index 000000000000..83b8e94330ba --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScript.java @@ -0,0 +1,51 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export; + +import java.io.File; + +import org.apache.commons.cli.ParseException; +import org.dspace.core.Context; + +/** + * This script can be use to export a given {@code MetadataSchema} into its + * registry file, that respects the standard DTD / XSD DSpace xml registry. + *

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

    + *
  • {@code schema-id}: id of the schema to export
  • + *
  • {@code file-path}:full file path of the file that will contain the export
  • + *
      + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + * + */ +public class MetadataSchemaExportCliScript extends MetadataSchemaExportScript { + + protected String filename; + + @Override + public void setup() throws ParseException { + super.setup(); + filename = commandLine.getOptionValue('f'); + } + + @Override + protected File getExportedFile(Context context) throws DspaceExportMetadataSchemaException { + try { + File file = new File(filename); + return metadataSchemaExportService.exportMetadataSchemaToFile(context, metadataSchema, file); + } catch (DspaceExportMetadataSchemaException e) { + handler.logError("Problem occured while exporting the schema to file: " + filename, e); + throw e; + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScriptConfiguration.java new file mode 100644 index 000000000000..5adfa2a725fc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportCliScriptConfiguration.java @@ -0,0 +1,34 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export; + +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + * + */ +public class MetadataSchemaExportCliScriptConfiguration + extends MetadataSchemaExportScriptConfiguration { + + @Override + public Options getOptions() { + Options options = super.getOptions(); + + options.addOption( + Option.builder("f").longOpt("file") + .desc("The temporary file-name to use") + .hasArg() + .build() + ); + + return options; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScript.java new file mode 100644 index 000000000000..3b07722a4b13 --- /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 000000000000..665dbe15567c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptConfiguration.java @@ -0,0 +1,73 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export; + +import java.sql.SQLException; + +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.dspace.scripts.configuration.ScriptConfiguration; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Configuration of the Script {@code MetadataSchemaExportScript} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class MetadataSchemaExportScriptConfiguration + extends ScriptConfiguration { + + @Autowired + private AuthorizeService authorizeService; + + private Class dspaceRunnableClass; + + @Override + public Class getDspaceRunnableClass() { + return this.dspaceRunnableClass; + } + + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + + @Override + public boolean isAllowedToExecute(Context context) { + try { + return authorizeService.isAdmin(context); + } catch (SQLException e) { + throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); + } + } + + @Override + public Options getOptions() { + Options options = new Options(); + + options.addOption( + Option.builder("i").longOpt("id") + .desc("Metadata schema id") + .hasArg() + .required() + .build() + ); + + options.addOption( + Option.builder("h").longOpt("help") + .desc("help") + .hasArg(false) + .required(false) + .build() + ); + + return options; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/AbstractJaxbBuilder.java new file mode 100644 index 000000000000..925020a52631 --- /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 000000000000..e0ad541bdb84 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchema.java @@ -0,0 +1,80 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.model; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlElementRefs; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + + +/** + *

      Classe Java per anonymous complex type. + * + *

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

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

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

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

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

      + * Objects of the following type(s) are allowed in the list + * {@link JAXBElement }{@code <}{@link String }{@code >} + * {@link JAXBElement }{@code <}{@link String }{@code >} + */ + public List> getNameOrNamespace() { + if (nameOrNamespace == null) { + nameOrNamespace = new ArrayList>(); + } + return this.nameOrNamespace; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchemaBuilder.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchemaBuilder.java new file mode 100644 index 000000000000..fe7144bda854 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcSchemaBuilder.java @@ -0,0 +1,39 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.model; + +import javax.xml.bind.JAXBElement; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class DcSchemaBuilder extends AbstractJaxbBuilder { + + protected DcSchemaBuilder() { + super(DcSchema.class); + } + + public static DcSchemaBuilder createBuilder() { + return new DcSchemaBuilder(); + } + + public DcSchemaBuilder withName(String name) { + this.addChildElement(name, objectFactory::createName); + return this; + } + + public DcSchemaBuilder withNamespace(String namespace) { + this.addChildElement(namespace, objectFactory::createNamespace); + return this; + } + + @Override + protected void addChildElement(JAXBElement v) { + getObejct().getNameOrNamespace().add(v); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcType.java new file mode 100644 index 000000000000..bff2fc77978a --- /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 000000000000..47fd64763ead --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DcTypeBuilder.java @@ -0,0 +1,49 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.model; + +import javax.xml.bind.JAXBElement; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class DcTypeBuilder extends AbstractJaxbBuilder { + + protected DcTypeBuilder() { + super(DcType.class); + } + + public static DcTypeBuilder createBuilder() { + return new DcTypeBuilder(); + } + + public DcTypeBuilder withSchema(String schema) { + addChildElement(schema, objectFactory::createSchema); + return this; + } + + public DcTypeBuilder withElement(String element) { + addChildElement(element, objectFactory::createElement); + return this; + } + + public DcTypeBuilder withQualifier(String qualifier) { + addChildElement(qualifier, objectFactory::createQualifier); + return this; + } + + public DcTypeBuilder withScopeNote(String scopeNote) { + addChildElement(scopeNote, objectFactory::createScopeNote); + return this; + } + + @Override + protected void addChildElement(JAXBElement v) { + getObejct().getSchemaOrElementOrQualifier().add(v); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypes.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypes.java new file mode 100644 index 000000000000..4cba081a8a30 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/model/DspaceDcTypes.java @@ -0,0 +1,82 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.model; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElements; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + + +/** + *

      Classe Java per anonymous complex type. + * + *

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

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

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

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

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

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

      Classe Java per anonymous complex type. + * + *

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

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

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

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

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

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

      An ObjectFactory allows you to programatically + * construct new instances of the Java representation + * for XML content. The Java representation of XML + * content can consist of schema derived interfaces + * and classes representing the binding of schema + * type definitions, element declarations and model + * groups. Factory methods for each of these are + * provided in this class. + */ +@XmlRegistry +public class ObjectFactory { + + private final static QName _Title_QNAME = new QName("", "title"); + private final static QName _ContributorAuthor_QNAME = new QName("", "contributor.author"); + private final static QName _ContributorEditor_QNAME = new QName("", "contributor.editor"); + private final static QName _DateCreated_QNAME = new QName("", "date.created"); + private final static QName _Description_QNAME = new QName("", "description"); + private final static QName _DescriptionVersion_QNAME = new QName("", "description.version"); + private final static QName _Name_QNAME = new QName("", "name"); + private final static QName _Namespace_QNAME = new QName("", "namespace"); + private final static QName _Schema_QNAME = new QName("", "schema"); + private final static QName _Element_QNAME = new QName("", "element"); + private final static QName _Qualifier_QNAME = new QName("", "qualifier"); + private final static QName _ScopeNote_QNAME = new QName("", "scope_note"); + + /** + * Create a new ObjectFactory that can be used to create new instances of schema derived classes for package: org + * .dspace.app.metadata.export.model + */ + public ObjectFactory() { + } + + /** + * Create an instance of {@link DspaceDcTypes } + */ + public DspaceDcTypes createDspaceDcTypes() { + return new DspaceDcTypes(); + } + + /** + * Create an instance of {@link DspaceHeader } + */ + public DspaceHeader createDspaceHeader() { + return new DspaceHeader(); + } + + /** + * Create an instance of {@link DcSchema } + */ + public DcSchema createDcSchema() { + return new DcSchema(); + } + + /** + * Create an instance of {@link DcType } + */ + public DcType createDcType() { + return new DcType(); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "title") + public JAXBElement createTitle(String value) { + return new JAXBElement(_Title_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "contributor.author") + public JAXBElement createContributorAuthor(String value) { + return new JAXBElement(_ContributorAuthor_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "contributor.editor") + public JAXBElement createContributorEditor(String value) { + return new JAXBElement(_ContributorEditor_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "date.created") + public JAXBElement createDateCreated(String value) { + return new JAXBElement(_DateCreated_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "description") + public JAXBElement createDescription(String value) { + return new JAXBElement(_Description_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "description.version") + public JAXBElement createDescriptionVersion(String value) { + return new JAXBElement(_DescriptionVersion_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "name") + public JAXBElement createName(String value) { + return new JAXBElement(_Name_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "namespace") + public JAXBElement createNamespace(String value) { + return new JAXBElement(_Namespace_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "schema") + public JAXBElement createSchema(String value) { + return new JAXBElement(_Schema_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "element") + public JAXBElement createElement(String value) { + return new JAXBElement(_Element_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "qualifier") + public JAXBElement createQualifier(String value) { + return new JAXBElement(_Qualifier_QNAME, String.class, null, value); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >} + * + * @param value Java instance representing xml element's value. + * @return the new instance of {@link JAXBElement }{@code <}{@link String }{@code >} + */ + @XmlElementDecl(namespace = "", name = "scope_note") + public JAXBElement createScopeNote(String value) { + return new JAXBElement(_ScopeNote_QNAME, String.class, null, value); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactory.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactory.java new file mode 100644 index 000000000000..3553cbcba2fd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactory.java @@ -0,0 +1,28 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.service; + +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Factory for the export services related to metadata-schema and metadata-fields. + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public abstract class MetadataExportServiceFactory { + + public static MetadataExportServiceFactory getInstance() { + return DSpaceServicesFactory + .getInstance().getServiceManager() + .getServiceByName("metadataExportServiceFactory", MetadataExportServiceFactory.class); + } + + public abstract MetadataSchemaExportService getMetadataSchemaExportService(); + public abstract MetadataFieldExportService getMetadataFieldExportService(); + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactoryImpl.java new file mode 100644 index 000000000000..a69d5dfd0fde --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataExportServiceFactoryImpl.java @@ -0,0 +1,31 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.service; + +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class MetadataExportServiceFactoryImpl extends MetadataExportServiceFactory { + + @Autowired + private MetadataSchemaExportService metadataSchemaExportService; + @Autowired + private MetadataFieldExportService metadataFieldExportService; + + @Override + public MetadataSchemaExportService getMetadataSchemaExportService() { + return metadataSchemaExportService; + } + + @Override + public MetadataFieldExportService getMetadataFieldExportService() { + return metadataFieldExportService; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportService.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportService.java new file mode 100644 index 000000000000..ace312885230 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportService.java @@ -0,0 +1,35 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.service; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.metadata.export.model.DcType; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchema; +import org.dspace.core.Context; + +/** + * Exports {@code MetadataField} into {@code DcType} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public interface MetadataFieldExportService { + + /** + * Creates a one {@link DCType} for each {@link MetadataField} + * in the given {@link MetadataSchema}, and returns them in a list + * + * @param context + * @param metadataSchema + * @return + * @throws SQLException + */ + List exportMetadataFieldsBy(Context context, MetadataSchema metadataSchema) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportServiceImpl.java new file mode 100644 index 000000000000..1ace35f4e45d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataFieldExportServiceImpl.java @@ -0,0 +1,49 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.service; + +import java.sql.SQLException; +import java.util.List; +import java.util.stream.Collectors; + +import org.dspace.app.metadata.export.model.DcType; +import org.dspace.app.metadata.export.model.DcTypeBuilder; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchema; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.core.Context; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class MetadataFieldExportServiceImpl implements MetadataFieldExportService { + + private MetadataFieldService metadataFieldService = + ContentServiceFactory.getInstance().getMetadataFieldService(); + + public List exportMetadataFieldsBy(Context context, MetadataSchema metadataSchema) throws SQLException { + return metadataFieldService + .findAllInSchema(context, metadataSchema) + .stream() + .map(this::toDcType) + .collect(Collectors.toList()); + } + + private DcType toDcType(MetadataField metadataField) { + return DcTypeBuilder + .createBuilder() + .withSchema(metadataField.getMetadataSchema().getName()) + .withElement(metadataField.getElement()) + .withQualifier(metadataField.getQualifier()) + .withScopeNote(metadataField.getScopeNote()) + .build(); + } + +} + diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportService.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportService.java new file mode 100644 index 000000000000..cd1f35e2ef9b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportService.java @@ -0,0 +1,68 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.service; + +import java.io.File; +import java.sql.SQLException; + +import org.dspace.app.metadata.export.DspaceExportMetadataSchemaException; +import org.dspace.app.metadata.export.model.DspaceDcTypes; +import org.dspace.content.MetadataSchema; +import org.dspace.core.Context; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public interface MetadataSchemaExportService { + + /** + * Exports the given {@code schemaId} into a {@link DspaceDcTypes} entity + * + * @param context + * @param schemaId + * @return + * @throws SQLException + */ + DspaceDcTypes exportMetadataSchema(Context context, int schemaId) throws SQLException; + + /** + * Exports the given {@code metadataSchema} into a {@link DspaceDcTypes} entity + * + * @param context + * @param metadataSchema + * @return + * @throws SQLException + */ + DspaceDcTypes exportMetadataSchema(Context context, MetadataSchema metadataSchema) throws SQLException; + + /** + * Exports the given {@code metadataSchema} to a temporary {@code File}, + * that will respect the {@code registry} xml format of dspace + * + * @param context + * @param metadataSchema + * @return + * @throws DspaceExportMetadataSchemaException + */ + File exportMetadataSchemaToFile(Context context, MetadataSchema metadataSchema) + throws DspaceExportMetadataSchemaException; + + /** + * Exports the given {@code metadataSchema} to a target {@code File}, + * that will respect the {@code registry} xml format of dspace + * + * @param context + * @param metadataSchema + * @param file + * @return + * @throws DspaceExportMetadataSchemaException + */ + File exportMetadataSchemaToFile(Context context, MetadataSchema metadataSchema, File file) + throws DspaceExportMetadataSchemaException; + +} diff --git a/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportServiceImpl.java new file mode 100644 index 000000000000..eea9a09f7970 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/metadata/export/service/MetadataSchemaExportServiceImpl.java @@ -0,0 +1,107 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.metadata.export.service; + +import java.io.File; +import java.io.IOException; +import java.sql.SQLException; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; + +import org.dspace.app.metadata.export.DspaceExportMetadataSchemaException; +import org.dspace.app.metadata.export.model.DcSchema; +import org.dspace.app.metadata.export.model.DcSchemaBuilder; +import org.dspace.app.metadata.export.model.DspaceDcTypes; +import org.dspace.app.metadata.export.model.DspaceDcTypesBuilder; +import org.dspace.content.MetadataSchema; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.MetadataSchemaService; +import org.dspace.core.Context; + +/** + * This service can be used to export a target schema into a registry-file + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class MetadataSchemaExportServiceImpl implements MetadataSchemaExportService { + + private MetadataSchemaService metadataSchemaService = + ContentServiceFactory.getInstance().getMetadataSchemaService(); + + @Override + public DspaceDcTypes exportMetadataSchema(Context context, int schemaId) throws SQLException { + return this.exportMetadataSchema(context, metadataSchemaService.find(context, schemaId)); + } + + @Override + public DspaceDcTypes exportMetadataSchema(Context context, MetadataSchema metadataSchema) throws SQLException { + return DspaceDcTypesBuilder + .createBuilder() + .withSchema(this.mapToDcSchema(metadataSchema)) + .withDcTypes( + MetadataExportServiceFactory.getInstance() + .getMetadataFieldExportService() + .exportMetadataFieldsBy(context, metadataSchema) + ) + .build(); + } + + @Override + public File exportMetadataSchemaToFile(Context context, MetadataSchema metadataSchema) + throws DspaceExportMetadataSchemaException { + File tempFile; + try { + tempFile = + File.createTempFile( + metadataSchema.getName() + "-" + metadataSchema.getID(), + ".xml" + ); + tempFile.deleteOnExit(); + return this.exportMetadataSchemaToFile(context, metadataSchema, tempFile); + } catch (IOException e) { + throw new DspaceExportMetadataSchemaException( + "Probelm occured during while exporting to temporary file!", + e + ); + } + } + + @Override + public File exportMetadataSchemaToFile(Context context, MetadataSchema metadataSchema, File file) + throws DspaceExportMetadataSchemaException { + try { + DspaceDcTypes dspaceDcTypes = this.exportMetadataSchema(context, metadataSchema); + + JAXBContext jaxb = JAXBContext.newInstance(DspaceDcTypes.class); + Marshaller jaxbMarshaller = jaxb.createMarshaller(); + jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + jaxbMarshaller.marshal(dspaceDcTypes, file); + } catch (SQLException e) { + throw new DspaceExportMetadataSchemaException( + "Problem occured while retrieving data from DB!", + e + ); + } catch (JAXBException e) { + throw new DspaceExportMetadataSchemaException( + "Problem occured during the export to XML file!", + e + ); + } + return file; + } + + private DcSchema mapToDcSchema(MetadataSchema metadataSchema) { + return DcSchemaBuilder + .createBuilder() + .withName(metadataSchema.getName()) + .withNamespace(metadataSchema.getNamespace()) + .build(); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java b/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java index 6188272aca47..f3e651d7d26e 100644 --- a/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java +++ b/dspace-api/src/main/java/org/dspace/app/sitemap/GenerateSitemaps.java @@ -17,10 +17,7 @@ import java.net.URL; import java.net.URLEncoder; import java.sql.SQLException; -import java.util.Date; import java.util.Iterator; -import java.util.List; -import java.util.Optional; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; @@ -28,29 +25,26 @@ import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; -import org.dspace.app.customurl.CustomUrlService; -import org.dspace.content.Collection; -import org.dspace.content.Community; -import org.dspace.content.Item; -import org.dspace.content.factory.ContentServiceFactory; -import org.dspace.content.service.CollectionService; -import org.dspace.content.service.CommunityService; -import org.dspace.content.service.ItemService; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.SolrDocument; +import org.apache.solr.common.SolrDocumentList; import org.dspace.core.Context; import org.dspace.core.LogHelper; -import org.dspace.discovery.DiscoverQuery; -import org.dspace.discovery.DiscoverResult; import org.dspace.discovery.SearchService; -import org.dspace.discovery.SearchServiceException; import org.dspace.discovery.SearchUtils; +import org.dspace.discovery.SolrSearchCore; +import org.dspace.eperson.Group; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.GroupService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.dspace.utils.DSpace; /** * Command-line utility for generating HTML and Sitemaps.org protocol Sitemaps. @@ -64,15 +58,11 @@ public class GenerateSitemaps { */ private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(GenerateSitemaps.class); - private static final CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService(); - private static final CollectionService collectionService = - ContentServiceFactory.getInstance().getCollectionService(); - private static final ItemService itemService = ContentServiceFactory.getInstance().getItemService(); private static final ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); private static final SearchService searchService = SearchUtils.getSearchService(); - - private static final CustomUrlService customUrlService = new DSpace().getSingletonService(CustomUrlService.class); + private static final GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); + private static final int PAGE_SIZE = 1000; /** * Default constructor @@ -213,112 +203,127 @@ public static void generateSitemaps(boolean makeHTMLMap, boolean makeSitemapOrg) } Context c = new Context(Context.Mode.READ_ONLY); - - List comms = communityService.findAll(c); - - for (Community comm : comms) { - String url = uiURLStem + "/communities/" + comm.getID(); - - if (makeHTMLMap) { - html.addURL(url, null); - } - if (makeSitemapOrg) { - sitemapsOrg.addURL(url, null); - } - - c.uncacheEntity(comm); - } - - List colls = collectionService.findAll(c); - - for (Collection coll : colls) { - String url = uiURLStem + "/collections/" + coll.getID(); - - if (makeHTMLMap) { - html.addURL(url, null); - } - if (makeSitemapOrg) { - sitemapsOrg.addURL(url, null); - } - - c.uncacheEntity(coll); + SolrSearchCore solrSearchCore = searchService.getSolrSearchCore(); + SolrClient solr = solrSearchCore.getSolr(); + Group anonymousGroup = groupService.findByName(c, Group.ANONYMOUS); + String anonGroupId = ""; + if (anonymousGroup != null) { + anonGroupId = anonymousGroup.getID().toString(); } - Iterator allItems = itemService.findAll(c); - int itemCount = 0; - - while (allItems.hasNext()) { - Item i = allItems.next(); - - Optional customUrl = customUrlService.getCustomUrl(i); - if (customUrl.isPresent()) { - - String url = uiURLStem + "/entities/" + StringUtils.lowerCase(itemService.getEntityTypeLabel(i)) - + "/" + customUrl.get(); - - if (makeHTMLMap) { - html.addURL(url, null); + try { + SolrQuery solrQuery = new SolrQuery(SearchUtils.RESOURCE_TYPE_FIELD + ":Community"); + solrQuery.addFilterQuery("read:g" + anonGroupId); + solrQuery.setFields(SearchUtils.RESOURCE_ID_FIELD); + solrQuery.setRows(PAGE_SIZE); + int offset = 0; + long commsCount = 0; + QueryResponse rsp; + do { + solrQuery.setStart(offset); + rsp = solr.query(solrQuery, solrSearchCore.REQUEST_METHOD); + SolrDocumentList docs = rsp.getResults(); + commsCount = docs.getNumFound(); + Iterator iter = docs.iterator(); + + while (iter.hasNext()) { + SolrDocument doc = (SolrDocument) iter.next(); + String url = uiURLStem + "/communities/" + doc.getFieldValue(SearchUtils.RESOURCE_ID_FIELD); + + if (makeHTMLMap) { + html.addURL(url, null); + } + if (makeSitemapOrg) { + sitemapsOrg.addURL(url, null); + } } - if (makeSitemapOrg) { - sitemapsOrg.addURL(url, null); + offset += PAGE_SIZE; + } while (offset < commsCount); + + solrQuery = new SolrQuery(SearchUtils.RESOURCE_TYPE_FIELD + ":Collection"); + solrQuery.addFilterQuery("read:g" + anonGroupId); + solrQuery.setFields(SearchUtils.RESOURCE_ID_FIELD); + solrQuery.setRows(PAGE_SIZE); + offset = 0; + long collsCount = 0; + do { + solrQuery.setStart(offset); + rsp = solr.query(solrQuery, solrSearchCore.REQUEST_METHOD); + SolrDocumentList docs = rsp.getResults(); + collsCount = docs.getNumFound(); + Iterator iter = docs.iterator(); + + while (iter.hasNext()) { + SolrDocument doc = (SolrDocument) iter.next(); + String url = uiURLStem + "/collections/" + doc.getFieldValue(SearchUtils.RESOURCE_ID_FIELD); + + if (makeHTMLMap) { + html.addURL(url, null); + } + if (makeSitemapOrg) { + sitemapsOrg.addURL(url, null); + } } + offset += PAGE_SIZE; + } while (offset < collsCount); + + solrQuery = new SolrQuery(SearchUtils.RESOURCE_TYPE_FIELD + ":Item"); + solrQuery.setFields(SearchUtils.RESOURCE_ID_FIELD, "customurl", "search.entitytype"); + solrQuery.addFilterQuery("read:g" + anonGroupId); + solrQuery.addFilterQuery("-discoverable:false"); + solrQuery.setRows(PAGE_SIZE); + offset = 0; + long itemsCount = 0; + do { + solrQuery.setStart(offset); + rsp = solr.query(solrQuery, solrSearchCore.REQUEST_METHOD); + SolrDocumentList docs = rsp.getResults(); + itemsCount = docs.getNumFound(); + Iterator iter = docs.iterator(); + + while (iter.hasNext()) { + SolrDocument doc = (SolrDocument) iter.next(); + String uuid = (String) doc.getFirstValue(SearchUtils.RESOURCE_ID_FIELD); + String entityType = (String) doc.getFirstValue("search.entitytype"); + String customUrl = (String) doc.getFirstValue("customUrl"); + String url = uiURLStem + "/items/" + uuid; + + if (StringUtils.isNotBlank(customUrl)) { + url = uiURLStem + "/entities/" + StringUtils.lowerCase(entityType) + "/" + customUrl; + } else if (StringUtils.isNoneBlank(entityType)) { + url = uiURLStem + "/entities/" + StringUtils.lowerCase(entityType) + "/" + uuid; + } + if (makeHTMLMap) { + html.addURL(url, null); + } + if (makeSitemapOrg) { + sitemapsOrg.addURL(url, null); + } - } - - DiscoverQuery entityQuery = new DiscoverQuery(); - entityQuery.setQuery("search.uniqueid:\"Item-" + i.getID() + "\" and entityType:*"); - entityQuery.addSearchField("entityType"); - - try { - DiscoverResult discoverResult = searchService.search(c, entityQuery); - - String url; - if (CollectionUtils.isNotEmpty(discoverResult.getIndexableObjects()) - && CollectionUtils.isNotEmpty(discoverResult.getSearchDocument( - discoverResult.getIndexableObjects().get(0)).get(0).getSearchFieldValues("entityType")) - && StringUtils.isNotBlank(discoverResult.getSearchDocument( - discoverResult.getIndexableObjects().get(0)).get(0).getSearchFieldValues("entityType").get(0)) - ) { - url = uiURLStem + "/entities/" + StringUtils.lowerCase(discoverResult.getSearchDocument( - discoverResult.getIndexableObjects().get(0)) - .get(0).getSearchFieldValues("entityType").get(0)) + "/" + i.getID(); - } else { - url = uiURLStem + "/items/" + i.getID(); } - Date lastMod = i.getLastModified(); + offset += PAGE_SIZE; + } while (offset < itemsCount); - if (makeHTMLMap) { - html.addURL(url, lastMod); - } - if (makeSitemapOrg) { - sitemapsOrg.addURL(url, lastMod); - } - } catch (SearchServiceException e) { - log.error("Failed getting entitytype through solr for item " + i.getID() + ": " + e.getMessage()); + if (makeHTMLMap) { + int files = html.finish(); + log.info(LogHelper.getHeader(c, "write_sitemap", + "type=html,num_files=" + files + ",communities=" + + commsCount + ",collections=" + collsCount + + ",items=" + itemsCount)); } - c.uncacheEntity(i); - - itemCount++; - } - - if (makeHTMLMap) { - int files = html.finish(); - log.info(LogHelper.getHeader(c, "write_sitemap", - "type=html,num_files=" + files + ",communities=" - + comms.size() + ",collections=" + colls.size() - + ",items=" + itemCount)); - } - - if (makeSitemapOrg) { - int files = sitemapsOrg.finish(); - log.info(LogHelper.getHeader(c, "write_sitemap", - "type=html,num_files=" + files + ",communities=" - + comms.size() + ",collections=" + colls.size() - + ",items=" + itemCount)); + if (makeSitemapOrg) { + int files = sitemapsOrg.finish(); + log.info(LogHelper.getHeader(c, "write_sitemap", + "type=html,num_files=" + files + ",communities=" + + commsCount + ",collections=" + collsCount + + ",items=" + itemsCount)); + } + } catch (SolrServerException e) { + throw new RuntimeException(e); + } finally { + c.abort(); } - - c.abort(); } /** diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java index ac8031880233..86b45c367941 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java @@ -316,6 +316,9 @@ public List getInputsByGroup(String formName) // cache miss - construct new DCInputSet List>> pages = formDefns.get(formName); + if (pages == null) { + return results; + } Iterator>> iterator = pages.iterator(); 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 000000000000..97104bbb63fe --- /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 f77d7e57119a..88797e9b1a79 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 eec4412c0c98..a83cc8692e31 100644 --- a/dspace-api/src/main/java/org/dspace/authority/CrisConsumer.java +++ b/dspace-api/src/main/java/org/dspace/authority/CrisConsumer.java @@ -193,7 +193,7 @@ private boolean isMetadataSkippable(MetadataValue metadata) { return true; } - if (isBlank(authority) && isMetadataWithEmptyAuthoritySkippable(metadata)) { + if (isBlank(authority) && (isBlank(metadata.getValue()) || isMetadataWithEmptyAuthoritySkippable(metadata))) { return true; } @@ -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/authority/filler/ExternalDataProviderImportFiller.java b/dspace-api/src/main/java/org/dspace/authority/filler/ExternalDataProviderImportFiller.java index ef218c76fb34..7a7d10e63499 100644 --- a/dspace-api/src/main/java/org/dspace/authority/filler/ExternalDataProviderImportFiller.java +++ b/dspace-api/src/main/java/org/dspace/authority/filler/ExternalDataProviderImportFiller.java @@ -7,7 +7,6 @@ */ package org.dspace.authority.filler; -import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.removeStart; import static org.apache.commons.lang3.StringUtils.startsWith; @@ -110,7 +109,11 @@ private void enrichItemWithExternalData(Context context, Item item, ExternalData } private boolean notAlreadyPresent(Item item, MetadataValueDTO value) { - return isEmpty(itemService.getMetadata(item, value.getSchema(), value.getElement(), value.getQualifier(), ANY)); + List metadataValues = itemService.getMetadata(item, value.getSchema(), + value.getElement(), value.getQualifier(), ANY); + + return metadataValues.stream().noneMatch(metadataValue -> + metadataValue.getValue().equals(value.getValue())); } private boolean isTitleNotSet(Item item) { diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index f2a8680ee58d..b07f23ee23ff 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -7,12 +7,15 @@ */ package org.dspace.content; +import static org.apache.commons.lang.StringUtils.startsWith; + import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Spliterators; import java.util.UUID; import java.util.regex.Pattern; @@ -606,4 +609,63 @@ private Stream streamOf(Iterator iterator) { return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); } + @Override + public boolean isOriginalBitstream(DSpaceObject dso) throws SQLException { + + if (dso.getType() != Constants.BITSTREAM) { + return false; + } + + Bitstream bitstream = (Bitstream) dso; + + return bitstream.getBundles().stream() + .anyMatch(bundle -> "ORIGINAL".equals(bundle.getName())); + + } + + @Override + public void updateThumbnailResourcePolicies(Context context, Bitstream bitstream) throws SQLException { + getThumbnail(bitstream) + .ifPresent(thumbnail -> replacePolicies(context, bitstream, thumbnail)); + } + + private void replacePolicies(Context context, Bitstream bitstream, Bitstream thumbnail) { + try { + authorizeService.replaceAllPolicies(context, bitstream, thumbnail); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e); + } + } + + private Optional getThumbnail(Bitstream bitstream) throws SQLException { + return getItem(bitstream) + .flatMap(item -> getThumbnail(item, bitstream.getName())); + } + + private Optional getItem(Bitstream bitstream) throws SQLException { + return bitstream.getBundles().stream() + .flatMap(bundle -> bundle.getItems().stream()) + .findFirst(); + } + + private Optional getThumbnail(Item item, String name) { + List bundles = getThumbnailBundles(item); + if (CollectionUtils.isEmpty(bundles)) { + return Optional.empty(); + } + + return bundles.stream() + .flatMap(bundle -> bundle.getBitstreams().stream()) + .filter(bitstream -> startsWith(bitstream.getName(), name)) + .findFirst(); + } + + private List getThumbnailBundles(Item item) { + try { + return itemService.getBundles(item, "THUMBNAIL"); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 3ad03377cb27..cfad7f87dba5 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -298,9 +298,7 @@ private List getThumbnailFields(List crisLayoutT * @param context * @param item * @param bundle - * @param metadata * @param value - * @param requireOriginal * @throws SQLException * @return Bitstream */ @@ -2138,4 +2136,12 @@ public boolean isLatestVersion(Context context, Item item) throws SQLException { } + @Override + public void addResourcePolicy(Context context, Item item, int actionID, EPerson eperson) + throws SQLException, AuthorizeException { + ResourcePolicy resourcePolicy = + this.authorizeService.createResourcePolicy(context, item, null, eperson, actionID, null); + item.getResourcePolicies().add(resourcePolicy); + } + } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java index 6e0800457397..cbc92c3be5f6 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java @@ -7,6 +7,7 @@ */ package org.dspace.content.authority; +import static java.lang.Integer.MAX_VALUE; import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.util.ArrayList; @@ -21,6 +22,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.app.util.DCInput; import org.dspace.app.util.DCInputSet; @@ -64,7 +66,7 @@ * @see ChoiceAuthority */ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService { - private Logger log = org.apache.logging.log4j.LogManager.getLogger(ChoiceAuthorityServiceImpl.class); + private Logger log = LogManager.getLogger(ChoiceAuthorityServiceImpl.class); // map of field key to authority plugin protected Map controller = new HashMap(); @@ -343,16 +345,26 @@ private void loadChoiceAuthorityConfigurations() { */ private void autoRegisterChoiceAuthorityFromInputReader() { try { - List submissionConfigs = itemSubmissionConfigReader - .getAllSubmissionConfigs(Integer.MAX_VALUE, 0); + List submissionConfigs = itemSubmissionConfigReader.getAllSubmissionConfigs(MAX_VALUE, 0); DCInputsReader dcInputsReader = new DCInputsReader(); // loop over all the defined item submission configuration for (SubmissionConfig subCfg : submissionConfigs) { String submissionName = subCfg.getSubmissionName(); List inputsBySubmissionName = dcInputsReader.getInputsBySubmissionName(submissionName); - autoRegisterChoiceAuthorityFromSubmissionForms(Constants.ITEM, submissionName, - inputsBySubmissionName); + List inputsByGroupOfAllSteps = new ArrayList(); + try { + List inputsByGroup = dcInputsReader.getInputsByGroup(submissionName); + inputsByGroupOfAllSteps.addAll(inputsByGroup); + for (DCInputSet step : inputsBySubmissionName) { + List inputsByGroupOfStep = dcInputsReader.getInputsByGroup(step.getFormName()); + inputsByGroupOfAllSteps.addAll(inputsByGroupOfStep); + } + } catch (DCInputsReaderException e) { + log.warn("Cannot load the groups of the submission: " + submissionName, e); + } + inputsBySubmissionName.addAll(inputsByGroupOfAllSteps); + autoRegisterChoiceAuthorityFromSubmissionForms(Constants.ITEM, submissionName, inputsBySubmissionName); } // loop over all the defined bitstream metadata submission configuration for (UploadConfiguration uploadCfg : uploadConfigurationService.getMap().values()) { @@ -363,8 +375,7 @@ private void autoRegisterChoiceAuthorityFromInputReader() { } } catch (DCInputsReaderException e) { // the system is in an illegal state as the submission definition is not valid - throw new IllegalStateException("Error reading the item submission configuration: " + e.getMessage(), - e); + throw new IllegalStateException("Error reading the item submission configuration: " + e.getMessage(), e); } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java index 9695f9c32552..ca9f42f13a3d 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/DCInputAuthority.java @@ -48,6 +48,8 @@ * fields. */ public class DCInputAuthority extends SelfNamedPlugin implements ChoiceAuthority { + public static final String UNKNOWN_KEY = "UNKNOWN KEY "; + private static Logger log = org.apache.logging.log4j.LogManager.getLogger(DCInputAuthority.class); /** @@ -92,7 +94,7 @@ public static String[] getPluginNames() { initPluginNames(); } - return (String[]) ArrayUtils.clone(pluginNames); + return ArrayUtils.clone(pluginNames); } private static synchronized void initPluginNames() { @@ -205,17 +207,17 @@ public String getLabel(String key, String locale) { String[] labelsLocale = labels.get(locale); int pos = -1; // search in the values to return the label - for (int i = 0; i < valuesLocale.length; i++) { + for (int i = 0; valuesLocale != null && i < valuesLocale.length; i++) { if (valuesLocale[i].equals(key)) { pos = i; break; } } - if (pos != -1) { + if (pos != -1 && labelsLocale != null) { // return the label in the same position where we found the value return labelsLocale[pos]; } else { - return "UNKNOWN KEY " + key; + return UNKNOWN_KEY + key; } } diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/ItemAuthority.java index 6ec39db9764f..173ea83f62ad 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/OrcidAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/OrcidAuthority.java index 4dfe09cdec64..8978c1f90fcc 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/OrcidAuthority.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/OrcidAuthority.java @@ -43,9 +43,9 @@ public class OrcidAuthority extends ItemAuthority { private static final Logger LOGGER = LoggerFactory.getLogger(OrcidAuthority.class); - public static final String ORCID_EXTRA = "data-person_identifier_orcid"; + public static final String DEFAULT_ORCID_KEY = "person_identifier_orcid"; - public static final String INSTITUTION_EXTRA = "institution-affiliation-name"; + public static final String DEFAULT_INSTITUTION_KEY = "institution-affiliation-name"; private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); @@ -131,11 +131,24 @@ private String composeAuthorityValue(String orcid) { private Map composeExtras(ExpandedResult result) { Map extras = new HashMap<>(); - extras.put(ORCID_EXTRA, result.getOrcidId()); - + String orcidIdKey = getOrcidIdKey(); + String orcidId = result.getOrcidId(); + if (StringUtils.isNotBlank(orcidId)) { + if (useOrcidIDAsData()) { + extras.put("data-" + orcidIdKey, orcidId); + } + if (useOrcidIDForDisplaying()) { + extras.put(orcidIdKey, orcidId); + } + } + String institutionKey = getInstitutionKey(); String[] institutionNames = result.getInstitutionNames(); - if (ArrayUtils.isNotEmpty(institutionNames)) { - extras.put(INSTITUTION_EXTRA, String.join(", ", institutionNames)); + + if (ArrayUtils.isNotEmpty(institutionNames) && useInstitutionAsData()) { + extras.put("data-" + institutionKey, String.join(", ", institutionNames)); + } + if (ArrayUtils.isNotEmpty(institutionNames) && useInstitutionForDisplaying()) { + extras.put(institutionKey, String.join(", ", institutionNames)); } return extras; @@ -165,4 +178,41 @@ public static void setAccessToken(String accessToken) { OrcidAuthority.accessToken = accessToken; } + public String getOrcidIdKey() { + return configurationService.getProperty("cris.OrcidAuthority." + + getPluginInstanceName() + ".orcid-id.key", + DEFAULT_ORCID_KEY); + } + + public String getInstitutionKey() { + return configurationService.getProperty("cris.OrcidAuthority." + + getPluginInstanceName() + ".institution.key", + DEFAULT_INSTITUTION_KEY); + } + + public boolean useInstitutionAsData() { + return configurationService + .getBooleanProperty("cris.OrcidAuthority." + + getPluginInstanceName() + ".institution.as-data", true); + } + + public boolean useInstitutionForDisplaying() { + return configurationService + .getBooleanProperty("cris.OrcidAuthority." + + getPluginInstanceName() + ".institution.display", true); + } + + public boolean useOrcidIDAsData() { + return configurationService + .getBooleanProperty("cris.OrcidAuthority." + + getPluginInstanceName() + ".orcid-id.as-data", true); + } + + public boolean useOrcidIDForDisplaying() { + return configurationService + .getBooleanProperty("cris.OrcidAuthority." + + getPluginInstanceName() + ".orcid-id.display", true); + + } + } 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 000000000000..de2271901819 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/authority/RorOrgUnitAuthority.java @@ -0,0 +1,152 @@ +/** + * 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.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.authority.factory.ItemAuthorityServiceFactory; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.ror.service.RorImportMetadataSourceServiceImpl; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.utils.DSpace; + +public class RorOrgUnitAuthority extends ItemAuthority { + + private final RorImportMetadataSourceServiceImpl rorImportMetadataSource = new DSpace().getServiceManager() + .getServicesByType(RorImportMetadataSourceServiceImpl.class).get(0); + + 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); + + try { + return solrChoices.values.length == 0 ? getRORApiMatches(text, start, limit) : solrChoices; + } catch (MetadataSourceException e) { + throw new RuntimeException(e); + } + } + + private Choices getRORApiMatches(String text, int start, int limit) throws MetadataSourceException { + Choice[] rorApiChoices = getChoiceFromRORQueryResults(rorImportMetadataSource.getRecords(text, 0, 0)) + .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(Collection orgUnits) { + return orgUnits + .stream() + .map(orgUnit -> new Choice(composeAuthorityValue(getIdentifier(orgUnit)), getName(orgUnit), + getName(orgUnit), buildExtras(orgUnit))) + .collect(Collectors.toList()); + } + + private String getIdentifier(ImportRecord orgUnit) { + return orgUnit.getValue("organization", "identifier", "ror").stream() + .findFirst() + .map(metadata -> metadata.getValue()) + .orElse(null); + } + + private String getName(ImportRecord orgUnit) { + return orgUnit.getValue("dc", "title", null).stream() + .findFirst() + .map(metadata -> metadata.getValue()) + .orElse(null); + } + + private Map buildExtras(ImportRecord orgUnit) { + + Map extras = new LinkedHashMap(); + + addExtra(extras, getIdentifier(orgUnit), "id"); + + orgUnit.getSingleValue("dc", "type", null) + .ifPresent(type -> addExtra(extras, type, "type")); + + String acronym = orgUnit.getValue("oairecerif", "acronym", null).stream() + .map(MetadatumDTO::getValue) + .collect(Collectors.joining(", ")); + + if (StringUtils.isNotBlank(acronym)) { + addExtra(extras, acronym, "acronym"); + } + + return extras; + } + + private void addExtra(Map extras, String value, String extraType) { + + String key = getKey(extraType); + + if (useAsData(extraType)) { + extras.put("data-" + key, value); + } + if (useForDisplaying(extraType)) { + extras.put(key, value); + } + + } + + private boolean useForDisplaying(String extraType) { + return configurationService.getBooleanProperty("cris.OrcidAuthority." + + getPluginInstanceName() + "." + extraType + ".display", true); + } + + private boolean useAsData(String extraType) { + return configurationService.getBooleanProperty("cris.OrcidAuthority." + + getPluginInstanceName() + "." + extraType + ".as-data", true); + } + + private String getKey(String extraType) { + return configurationService.getProperty("cris.OrcidAuthority." + + getPluginInstanceName() + "." + extraType + ".key", "ror_orgunit_" + extraType); + } + + 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 630efd5b0284..7bfa8504f902 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 b374861db9a3..2945065db4ea 100644 --- a/dspace-api/src/main/java/org/dspace/content/edit/CorrectItemMode.java +++ b/dspace-api/src/main/java/org/dspace/content/edit/CorrectItemMode.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; +import org.dspace.content.logic.Filter; import org.dspace.content.security.AccessItemMode; import org.dspace.content.security.CrisSecurity; @@ -42,6 +43,7 @@ public class CorrectItemMode implements AccessItemMode { * Contains the list of users metadata for CUSTOM security */ private List items = new ArrayList(); + private Filter additionalFilter; @Override public List getSecurities() { @@ -87,4 +89,13 @@ public void setItems(List items) { public List getGroups() { return groups; } + + public void setAdditionalFilter(Filter additionalFilter) { + this.additionalFilter = additionalFilter; + } + + @Override + public Filter getAdditionalFilter() { + return additionalFilter; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/edit/EditItemMode.java b/dspace-api/src/main/java/org/dspace/content/edit/EditItemMode.java index 4d56ddafe731..6f6b33ecaa28 100644 --- a/dspace-api/src/main/java/org/dspace/content/edit/EditItemMode.java +++ b/dspace-api/src/main/java/org/dspace/content/edit/EditItemMode.java @@ -9,6 +9,7 @@ import java.util.List; +import org.dspace.content.logic.Filter; import org.dspace.content.security.AccessItemMode; import org.dspace.content.security.CrisSecurity; @@ -49,6 +50,7 @@ public class EditItemMode implements AccessItemMode { * Contains the list of items metadata for CUSTOM security */ private List items; + private Filter additionalFilter; @Override public List getSecurities() { @@ -100,6 +102,15 @@ public void setItems(List items) { this.items = items; } + public void setAdditionalFilter(Filter additionalFilter) { + this.additionalFilter = additionalFilter; + } + + @Override + public Filter getAdditionalFilter() { + return additionalFilter; + } + @Override public List getGroups() { return groups; diff --git a/dspace-api/src/main/java/org/dspace/content/enhancer/impl/RelatedEntityItemEnhancer.java b/dspace-api/src/main/java/org/dspace/content/enhancer/impl/RelatedEntityItemEnhancer.java index a5d95582e41d..a6c97cc84e65 100644 --- a/dspace-api/src/main/java/org/dspace/content/enhancer/impl/RelatedEntityItemEnhancer.java +++ b/dspace-api/src/main/java/org/dspace/content/enhancer/impl/RelatedEntityItemEnhancer.java @@ -78,7 +78,7 @@ private void cleanObsoleteVirtualFields(Context context, Item item) throws SQLEx } private void updateVirtualFieldsPlaces(Context context, Item item) { - List virtualSourceFields = getMetadataValues(item, getVirtualSourceMetadataField()); + List virtualSourceFields = getVirtualSourceFields(item); for (MetadataValue virtualSourceField : virtualSourceFields) { metadataWithPlaceToUpdate(item, virtualSourceField) .ifPresent(updatePlaces(item, virtualSourceField)); @@ -113,9 +113,9 @@ private List getObsoleteVirtualFields(Item item) { List obsoleteVirtualFields = new ArrayList<>(); - List virtualSourceFields = getMetadataValues(item, getVirtualSourceMetadataField()); + List virtualSourceFields = getVirtualSourceFields(item); for (MetadataValue virtualSourceField : virtualSourceFields) { - if (isRelatedSourceNoMorePresent(item, virtualSourceField)) { + if (!isPlaceholder(virtualSourceField) && isRelatedSourceNoMorePresent(item, virtualSourceField)) { obsoleteVirtualFields.add(virtualSourceField); getRelatedVirtualField(item, virtualSourceField).ifPresent(obsoleteVirtualFields::add); } @@ -131,7 +131,7 @@ private boolean isRelatedSourceNoMorePresent(Item item, MetadataValue virtualSou } private Optional getRelatedVirtualField(Item item, MetadataValue virtualSourceField) { - return getMetadataValues(item, getVirtualMetadataField()).stream() + return getVirtualFields(item).stream() .filter(metadataValue -> metadataValue.getPlace() == virtualSourceField.getPlace()) .findFirst(); } @@ -141,6 +141,7 @@ private void performEnhancement(Context context, Item item) throws SQLException if (noEnhanceableMetadata(context, item)) { return; } + for (MetadataValue metadataValue : getEnhanceableMetadataValue(item)) { if (wasValueAlreadyUsedForEnhancement(item, metadataValue)) { @@ -191,9 +192,19 @@ private List getEnhanceableMetadataValue(Item item) { } private boolean wasValueAlreadyUsedForEnhancement(Item item, MetadataValue metadataValue) { - return getMetadataValues(item, getVirtualSourceMetadataField()).stream() + + if (isPlaceholderAtPlace(getVirtualFields(item), metadataValue.getPlace())) { + return true; + } + + return getVirtualSourceFields(item).stream() .anyMatch(virtualSourceField -> virtualSourceField.getPlace() == metadataValue.getPlace() && hasAuthorityEqualsTo(metadataValue, virtualSourceField.getValue())); + + } + + private boolean isPlaceholderAtPlace(List metadataValues, int place) { + return place < metadataValues.size() ? isPlaceholder(metadataValues.get(place)) : false; } private boolean hasAuthorityEqualsTo(MetadataValue metadataValue, String authority) { @@ -209,10 +220,22 @@ private Item findRelatedEntityItem(Context context, MetadataValue metadataValue) } } + private boolean isPlaceholder(MetadataValue metadataValue) { + return PLACEHOLDER_PARENT_METADATA_VALUE.equals(metadataValue.getValue()); + } + private List getMetadataValues(Item item, String metadataField) { return itemService.getMetadataByMetadataString(item, metadataField); } + private List getVirtualSourceFields(Item item) { + return getMetadataValues(item, getVirtualSourceMetadataField()); + } + + private List getVirtualFields(Item item) { + return getMetadataValues(item, getVirtualMetadataField()); + } + private void addVirtualField(Context context, Item item, String value) throws SQLException { itemService.addMetadata(context, item, VIRTUAL_METADATA_SCHEMA, VIRTUAL_METADATA_ELEMENT, getVirtualQualifier(), null, value); diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java index 026b6f375dfa..cbbfee4fb49b 100644 --- a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/XlsCrosswalk.java @@ -11,6 +11,7 @@ import java.io.OutputStream; import java.util.List; +import org.apache.commons.lang.StringUtils; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; @@ -45,7 +46,7 @@ protected void writeRows(List> rows, OutputStream out) { int cellCount = 0; for (String field : row) { Cell cell = sheetRow.createCell(cellCount++); - cell.setCellValue(field); + cell.setCellValue(StringUtils.length(field) > 32726 ? field.substring(0, 32725) + "…" : field ); } } diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/script/ItemExport.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/script/ItemExport.java index d52287ad3631..43b5d0b0971e 100644 --- a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/script/ItemExport.java +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/script/ItemExport.java @@ -10,6 +10,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.sql.SQLException; +import java.util.Objects; import java.util.UUID; import org.apache.commons.cli.ParseException; @@ -67,6 +68,7 @@ public void internalRun() throws Exception { context = new Context(Context.Mode.READ_ONLY); assignCurrentUserInContext(); + assignHandlerLocaleInContext(); assignSpecialGroupsInContext(); if (exportFormat == null) { @@ -140,6 +142,16 @@ private void assignSpecialGroupsInContext() throws SQLException { } } + private void assignHandlerLocaleInContext() { + if (Objects.nonNull(this.handler) && + Objects.nonNull(this.context) && + Objects.nonNull(this.handler.getLocale()) && + !this.handler.getLocale().equals(this.context.getCurrentLocale()) + ) { + this.context.setCurrentLocale(this.handler.getLocale()); + } + } + private StreamDisseminationCrosswalk getCrosswalkByType(String type) { return new DSpace().getSingletonService(StreamDisseminationCrosswalkMapper.class).getByType(type); } diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/ItemDOIService.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/ItemDOIService.java new file mode 100644 index 000000000000..03229f634a6b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/ItemDOIService.java @@ -0,0 +1,59 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.integration.crosswalks.virtualfields; + +import java.util.Comparator; +import java.util.List; + +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + + +public class ItemDOIService { + static final String CFG_PREFIX = "identifier.doi.prefix"; + + static final String DOI_METADATA = "dc.identifier.doi"; + + @Autowired + protected ItemService itemService; + @Autowired + private ConfigurationService configurationService; + + public String[] getAlternativeDOIFromItem(Item item) { + List metadataValueList = itemService.getMetadataByMetadataString(item, DOI_METADATA); + return getAlternativeDOI(metadataValueList, getPrimaryDOI(metadataValueList)); + } + private String[] getAlternativeDOI(List metadataValueList, String primaryValue) { + return metadataValueList.stream().map(MetadataValue::getValue) + .filter(value -> !value.equals(primaryValue)).toArray(String[]::new); + } + + public String getPrimaryDOIFromItem(Item item) { + return getPrimaryDOI(itemService.getMetadataByMetadataString(item, DOI_METADATA)); + } + + private String getPrimaryDOI(List metadataValueList) { + return metadataValueList.stream().filter(metadata -> metadata.getValue().contains(getPrefix())) + .min(Comparator.comparingInt(MetadataValue::getPlace)).map(MetadataValue::getValue) + .orElse(!metadataValueList.isEmpty() ? metadataValueList.get(0).getValue() : null); + } + + protected String getPrefix() { + String prefix; + prefix = this.configurationService.getProperty(CFG_PREFIX); + if (null == prefix) { + throw new RuntimeException("Unable to load DOI prefix from " + + "configuration. Cannot find property " + + CFG_PREFIX + "."); + } + return prefix; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldAlternativeDOI.java new file mode 100644 index 000000000000..3966566196cb --- /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 000000000000..3039ded0df84 --- /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 000000000000..89d9181c20bc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/integration/crosswalks/virtualfields/VirtualFieldVocabularyI18nValuePair.java @@ -0,0 +1,193 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.integration.crosswalks.virtualfields; + +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.authority.ChoiceAuthority; +import org.dspace.content.authority.DCInputAuthority; +import org.dspace.content.authority.service.ChoiceAuthorityService; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.core.I18nUtil; +import org.dspace.core.factory.CoreServiceFactory; +import org.dspace.core.service.PluginService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link VirtualField} that translates {@code value-pair} + * and {@code vocabulary-fields} into displayable labels. + * Internally uses the {@link ChoiceAuthorityService} to translate them. + *
      + *
      + * (Example: {@code @virtual.vocabulary_18n.metadataField@}) + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class VirtualFieldVocabularyI18nValuePair implements VirtualField { + + private final static Logger LOGGER = LoggerFactory.getLogger(VirtualFieldVocabularyI18nValuePair.class); + + @Autowired + private ItemService itemService; + @Autowired + private ChoiceAuthorityService choiceAuthorityService; + + private PluginService pluginService = CoreServiceFactory.getInstance().getPluginService(); + + @Override + public String[] getMetadata(Context context, Item item, String fieldName) { + String[] virtualFieldName = fieldName.split("\\.", 4); + + if (virtualFieldName.length < 3 || virtualFieldName.length > 4) { + LOGGER.warn("Invalid value-pairs virtual field: " + fieldName); + return new String[] {}; + } + String vocabularyName = getVocabularyName(virtualFieldName); + String metadataField = virtualFieldName[2].replaceAll("-", "."); + Locale locale = getLocale(context); + + return itemService.getMetadataByMetadataString(item, metadataField) + .stream() + .map(metadataValue -> + getLabelForVocabulary(vocabularyName, metadataValue, locale) + .orElse(getDisplayableLabel(item, metadataValue, locale.getLanguage())) + ) + .toArray(String[]::new); + } + + protected Optional getLabelForVocabulary( + String vocabularyName, MetadataValue metadataValue, Locale locale + ) { + return Optional.ofNullable(vocabularyName) + .map(vocabulary -> (ChoiceAuthority) pluginService.getNamedPlugin(ChoiceAuthority.class, vocabulary)) + .filter(Objects::nonNull) + .flatMap(choiceAuthority -> Optional.ofNullable(metadataValue.getAuthority()) + .flatMap( + authority -> getLabelWithFallback(choiceAuthority, authority, locale, I18nUtil.getDefaultLocale()) + ) + .or( + () -> getLabelWithFallback( + choiceAuthority, metadataValue.getValue(), + locale, I18nUtil.getDefaultLocale() + ) + ) + ); + } + + private Optional getLabelWithFallback( + ChoiceAuthority choiceAuthority, String authKey, Locale locale, Locale fallbackLocale + ) { + return getValidLabel( + Optional.ofNullable(choiceAuthority.getLabel(authKey, locale.getLanguage())) + ) + .or( + () -> getValidLabel( + Optional.ofNullable( + choiceAuthority.getLabel( + authKey, + fallbackLocale.getLanguage() + ) + ) + ) + ); + } + + protected String getDisplayableLabel(Item item, MetadataValue metadataValue, String language) { + return getLabelForCurrentLanguage(item, metadataValue, language) + .or(() -> getLabelForDefaultLanguage(item, metadataValue)) + .orElse(metadataValue.getValue()); + } + + protected Optional getLabelForDefaultLanguage(Item item, MetadataValue metadataValue) { + return getLabelForVocabulary(item, metadataValue, I18nUtil.getDefaultLocale().getLanguage()) + .or(() -> getLabelForValuePair(item, metadataValue, I18nUtil.getDefaultLocale().getLanguage())); + } + + protected Optional getLabelForCurrentLanguage(Item item, MetadataValue metadataValue, String language) { + return getLabelForVocabulary(item, metadataValue, language) + .or(() -> getLabelForValuePair(item, metadataValue, language)); + } + + private Optional getLabelForVocabulary(Item item, MetadataValue metadataValue, String language) { + return getValidLabel( + Optional.ofNullable(metadataValue) + .filter(mv -> StringUtils.isNotBlank(mv.getAuthority())) + .map(mv -> getVocabulary(item, mv, language)) + ); + } + + private Optional getLabelForValuePair(Item item, MetadataValue metadataValue, String language) { + return getValidLabel( + Optional.ofNullable(metadataValue) + .filter(mv -> StringUtils.isNotBlank(mv.getValue())) + .map(mv -> getValuePair(item, mv, language)) + ); + } + + private String getVocabulary(Item item, MetadataValue metadataValue, String language) { + try { + return this.choiceAuthorityService + .getLabel( + metadataValue, item.getType(), + item.getOwningCollection(), language + ); + } catch (Exception e) { + LOGGER.warn("Error while retrieving the vocabulary for: " + + metadataValue.getMetadataField().toString(), e + ); + } + return null; + } + + + private String getValuePair(Item item, MetadataValue metadataValue, String language) { + try { + return this.choiceAuthorityService + .getLabel( + metadataValue.getMetadataField().toString(), item.getType(), + item.getOwningCollection(), metadataValue.getValue(), language + ); + } catch (Exception e) { + LOGGER.warn( + "Error while retrievingthe value-pair for: " + + metadataValue.getMetadataField().toString(), + e + ); + } + return null; + } + + private String getVocabularyName(String[] virtualFieldName) { + return Optional.of(virtualFieldName.length) + .filter(l -> l == 4) + .map(l -> virtualFieldName[l - 1]) + .orElse(null); + } + + private Optional getValidLabel(Optional label) { + return label.filter(this::isValidLabel); + } + + private boolean isValidLabel(String s) { + return s != null && !s.contains(DCInputAuthority.UNKNOWN_KEY); + } + + private Locale getLocale(Context context) { + return Optional.ofNullable(context.getCurrentLocale()) + .orElse(I18nUtil.getDefaultLocale()); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/security/AccessItemMode.java b/dspace-api/src/main/java/org/dspace/content/security/AccessItemMode.java index 2aee66fed1ff..e2954bf8f83c 100644 --- a/dspace-api/src/main/java/org/dspace/content/security/AccessItemMode.java +++ b/dspace-api/src/main/java/org/dspace/content/security/AccessItemMode.java @@ -9,6 +9,8 @@ import java.util.List; +import org.dspace.content.logic.Filter; + /** * Interface to be extended for the configuration related to access item modes. * @@ -50,4 +52,6 @@ public interface AccessItemMode { * @return the group list */ public List getGroups(); + + public Filter getAdditionalFilter(); } diff --git a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurity.java b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurity.java index 3fcd83864175..9a472b8a40c3 100644 --- a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurity.java +++ b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurity.java @@ -23,6 +23,7 @@ public enum CrisSecurity { ITEM_ADMIN, SUBMITTER, SUBMITTER_GROUP, - GROUP; + GROUP, + ALL; } diff --git a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java index 4a8b2c313846..99add81e862b 100644 --- a/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/security/CrisSecurityServiceImpl.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import org.apache.commons.collections.CollectionUtils; @@ -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 3f5b17630a27..85a4fd140e9a 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java @@ -22,6 +22,7 @@ import org.dspace.content.Bundle; import org.dspace.content.Collection; import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.core.Context; @@ -243,4 +244,8 @@ List findShowableByItem(Context context, UUID itemId, String bundleNa List findByItemAndBundleAndMetadata(Context context, Item item, String bundleName, Map filterMetadata); + boolean isOriginalBitstream(DSpaceObject dso) throws SQLException; + + void updateThumbnailResourcePolicies(Context context, Bitstream bitstream) throws SQLException; + } diff --git a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java index e6823690743d..44ba4a6bcd99 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java @@ -922,4 +922,17 @@ public Iterator findRelatedItemsByAuthorityControlledFields(Context contex */ public boolean isLatestVersion(Context context, Item item) throws SQLException; + /** + * Adds a resource policy to the specified item for the given action and EPerson. + * + * @param context the DSpace context + * @param item the item to add the policy to + * @param actionID the ID of the action to add the policy for + * @param eperson the EPerson to add the policy for + * @throws SQLException if a database error occurs + * @throws AuthorizeException if the current user is not authorized to perform this action + */ + void addResourcePolicy(Context context, Item item, int actionID, EPerson eperson) + throws SQLException, AuthorizeException; + } diff --git a/dspace-api/src/main/java/org/dspace/core/CrisConstants.java b/dspace-api/src/main/java/org/dspace/core/CrisConstants.java index 18def2d10316..5bce199cfd76 100644 --- a/dspace-api/src/main/java/org/dspace/core/CrisConstants.java +++ b/dspace-api/src/main/java/org/dspace/core/CrisConstants.java @@ -21,7 +21,7 @@ public class CrisConstants { * same number than the parent leading metadata */ public static final String PLACEHOLDER_PARENT_METADATA_VALUE = "#PLACEHOLDER_PARENT_METADATA_VALUE#"; - + public static final String DSPACE_BASE_VERSION = "DSpace 7.5"; public static final MetadataFieldName MD_ENTITY_TYPE = new MetadataFieldName("dspace", "entity", "type"); public static final MetadataFieldName MD_SUBMISSION_TYPE = new MetadataFieldName("cris", "submission", "definition"); diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java index ab56e4692e39..a2c3056ae38d 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/discovery/configuration/DiscoveryConfigurationUtilsService.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationUtilsService.java index e2e83920eb70..3db9e04c2694 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationUtilsService.java +++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationUtilsService.java @@ -52,6 +52,7 @@ public Iterator findByRelation(Context context, Item item, String relation DiscoverQuery discoverQuery = new DiscoverQuery(); discoverQuery.setDSpaceObjectFilter(IndexableItem.TYPE); discoverQuery.setDiscoveryConfigurationName(discoveryConfiguration.getId()); + discoverQuery.setScopeObject(new IndexableItem(item)); List defaultFilterQueries = discoveryConfiguration.getDefaultFilterQueries(); for (String defaultFilterQuery : defaultFilterQueries) { discoverQuery.addFilterQueries(MessageFormat.format(defaultFilterQuery, item.getID())); diff --git a/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/AccountServiceImpl.java index 283f101f2ba5..8be6aac7e392 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 953a3e8bd0a6..2c0e1abb8238 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/RegistrationData.java +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationData.java @@ -10,9 +10,13 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -20,6 +24,7 @@ import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; +import javax.persistence.OneToMany; import javax.persistence.SequenceGenerator; import javax.persistence.Table; import javax.persistence.Temporal; @@ -27,6 +32,7 @@ import org.dspace.core.Context; import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.SortNatural; /** * Database entity representation of the registrationdata table @@ -43,30 +49,75 @@ public class RegistrationData implements ReloadableEntity { @SequenceGenerator(name = "registrationdata_seq", sequenceName = "registrationdata_seq", allocationSize = 1) private Integer id; - @Column(name = "email", unique = true, length = 64) + /** + * Contains the email used to register the user. + */ + @Column(name = "email", length = 64) private String email; + /** + * Contains the unique id generated fot the user. + */ @Column(name = "token", length = 48) private String token; + /** + * Expiration date of this registration data. + */ @Column(name = "expires") @Temporal(TemporalType.TIMESTAMP) private Date expires; @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST}) @JoinTable( - name = "registrationdata2group", - joinColumns = {@JoinColumn(name = "registrationdata_id")}, - inverseJoinColumns = {@JoinColumn(name = "group_id")} + name = "registrationdata2group", + joinColumns = {@JoinColumn(name = "registrationdata_id")}, + inverseJoinColumns = {@JoinColumn(name = "group_id")} ) private final List groups = new ArrayList(); + + /** + * Metadata linked to this registration data + */ + @SortNatural + @OneToMany( + fetch = FetchType.LAZY, + mappedBy = "registrationData", + cascade = CascadeType.ALL, + orphanRemoval = true + ) + private SortedSet metadata = new TreeSet<>(); + + /** + * External service used to register the user. + * Allowed values are inside {@link RegistrationTypeEnum} + */ + @Column(name = "registration_type") + @Enumerated(EnumType.STRING) + private RegistrationTypeEnum registrationType; + + /** + * Contains the external id provided by the external service + * accordingly to the registration type. + */ + @Column(name = "net_id", length = 64) + private final String netId; + /** * Protected constructor, create object using: * {@link org.dspace.eperson.service.RegistrationDataService#create(Context)} */ protected RegistrationData() { + this(null); + } + /** + * Protected constructor, create object using: + * {@link org.dspace.eperson.service.RegistrationDataService#create(Context, String)} + */ + protected RegistrationData(String netId) { + this.netId = netId; } public Integer getID() { @@ -77,7 +128,7 @@ public String getEmail() { return email; } - void setEmail(String email) { + public void setEmail(String email) { this.email = email; } @@ -104,4 +155,24 @@ public List getGroups() { public void addGroup(Group group) { this.groups.add(group); } + + public RegistrationTypeEnum getRegistrationType() { + return registrationType; + } + + public void setRegistrationType(RegistrationTypeEnum registrationType) { + this.registrationType = registrationType; + } + + public SortedSet getMetadata() { + return metadata; + } + + public void setMetadata(SortedSet metadata) { + this.metadata = metadata; + } + + public String getNetId() { + return netId; + } } diff --git a/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataExpirationConfiguration.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataExpirationConfiguration.java new file mode 100644 index 000000000000..3bd8def0c448 --- /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 000000000000..dde8428fe1fe --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadata.java @@ -0,0 +1,109 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.eperson; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.dspace.content.MetadataField; +import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.Type; + +/** + * Metadata related to a registration data {@link RegistrationData} + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +@Entity +@Table(name = "registrationdata_metadata") +public class RegistrationDataMetadata implements ReloadableEntity, Comparable { + + @Id + @Column(name = "registrationdata_metadata_id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "registrationdata_metadatavalue_seq") + @SequenceGenerator( + name = "registrationdata_metadatavalue_seq", + sequenceName = "registrationdata_metadatavalue_seq", + allocationSize = 1 + ) + private final Integer id; + + /** + * {@link RegistrationData} linked to this metadata value + */ + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "registrationdata_id") + private RegistrationData registrationData = null; + + /** + * The linked {@link MetadataField} instance + */ + @ManyToOne + @JoinColumn(name = "metadata_field_id") + private MetadataField metadataField = null; + + /** + * Value represented by this {@link RegistrationDataMetadata} instance + * related to the metadataField {@link MetadataField} + */ + @Lob + @Type(type = "org.dspace.storage.rdbms.hibernate.DatabaseAwareLobType") + @Column(name = "text_value") + private String value = null; + + /** + * Protected constructor + */ + protected RegistrationDataMetadata() { + id = 0; + } + + + @Override + public Integer getID() { + return id; + } + + public MetadataField getMetadataField() { + return metadataField; + } + + void setMetadataField(MetadataField metadataField) { + this.metadataField = metadataField; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public int compareTo(RegistrationDataMetadata o) { + return Integer.compare(this.id, o.id); + } + + void setRegistrationData(RegistrationData registrationData) { + this.registrationData = registrationData; + } + + public RegistrationData getRegistrationData() { + return registrationData; + } +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadataServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/RegistrationDataMetadataServiceImpl.java new file mode 100644 index 000000000000..34f0e5590fad --- /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 b27275168556..4448cefb1bd8 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 000000000000..28a594742f65 --- /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 5650c5e5b2be..0bdd6cc17cf8 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataDAO.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataDAO.java @@ -8,10 +8,12 @@ package org.dspace.eperson.dao; import java.sql.SQLException; +import java.util.Date; import org.dspace.core.Context; import org.dspace.core.GenericDAO; import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; /** * Database Access Object interface class for the RegistrationData object. @@ -23,9 +25,52 @@ */ public interface RegistrationDataDAO extends GenericDAO { + /** + * Finds {@link RegistrationData} by email. + * + * @param context Context for the current request + * @param email The email + * @return + * @throws SQLException + */ public RegistrationData findByEmail(Context context, String email) throws SQLException; + /** + * Finds {@link RegistrationData} by email and type. + * + * @param context Context for the current request + * @param email The email + * @param type The type of the {@link RegistrationData} + * @return + * @throws SQLException + */ + public RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException; + + /** + * Finds {@link RegistrationData} by token. + * + * @param context the context + * @param token The token related to the {@link RegistrationData}. + * @return + * @throws SQLException + */ public RegistrationData findByToken(Context context, String token) throws SQLException; + /** + * Deletes {@link RegistrationData} by token. + * + * @param context Context for the current request + * @param token The token to delete registrations for + * @throws SQLException + */ public void deleteByToken(Context context, String token) throws SQLException; + + /** + * Deletes expired {@link RegistrationData}. + * + * @param context Context for the current request + * @param date The date to delete expired registrations for + * @throws SQLException + */ + void deleteExpiredBy(Context context, Date date) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataMetadataDAO.java b/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataMetadataDAO.java new file mode 100644 index 000000000000..84ef2989cc45 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/RegistrationDataMetadataDAO.java @@ -0,0 +1,22 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.eperson.dao; + +import org.dspace.core.GenericDAO; +import org.dspace.eperson.RegistrationDataMetadata; + +/** + * Database Access Object interface class for the {@link org.dspace.eperson.RegistrationDataMetadata} object. + * The implementation of this class is responsible for all database calls for the RegistrationData object and is + * autowired by spring + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public interface RegistrationDataMetadataDAO extends GenericDAO { + +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataDAOImpl.java b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataDAOImpl.java index 4a15dcc86796..2dd023580dc8 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataDAOImpl.java @@ -8,8 +8,10 @@ package org.dspace.eperson.dao.impl; import java.sql.SQLException; +import java.util.Date; import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaDelete; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; @@ -17,6 +19,7 @@ import org.dspace.core.Context; import org.dspace.eperson.RegistrationData; import org.dspace.eperson.RegistrationData_; +import org.dspace.eperson.RegistrationTypeEnum; import org.dspace.eperson.dao.RegistrationDataDAO; /** @@ -42,6 +45,21 @@ public RegistrationData findByEmail(Context context, String email) throws SQLExc return uniqueResult(context, criteriaQuery, false, RegistrationData.class); } + @Override + public RegistrationData findBy(Context context, String email, RegistrationTypeEnum type) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, RegistrationData.class); + Root registrationDataRoot = criteriaQuery.from(RegistrationData.class); + criteriaQuery.select(registrationDataRoot); + criteriaQuery.where( + criteriaBuilder.and( + criteriaBuilder.equal(registrationDataRoot.get(RegistrationData_.email), email), + criteriaBuilder.equal(registrationDataRoot.get(RegistrationData_.registrationType), type) + ) + ); + return uniqueResult(context, criteriaQuery, false, RegistrationData.class); + } + @Override public RegistrationData findByToken(Context context, String token) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); @@ -59,4 +77,15 @@ public void deleteByToken(Context context, String token) throws SQLException { query.setParameter("token", token); query.executeUpdate(); } + + @Override + public void deleteExpiredBy(Context context, Date date) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaDelete deleteQuery = criteriaBuilder.createCriteriaDelete(RegistrationData.class); + Root deleteRoot = deleteQuery.from(RegistrationData.class); + deleteQuery.where( + criteriaBuilder.lessThanOrEqualTo(deleteRoot.get(RegistrationData_.expires), date) + ); + getHibernateSession(context).createQuery(deleteQuery).executeUpdate(); + } } diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataMetadataDAOImpl.java b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataMetadataDAOImpl.java new file mode 100644 index 000000000000..713032b05bbc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/RegistrationDataMetadataDAOImpl.java @@ -0,0 +1,19 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.eperson.dao.impl; + +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.eperson.RegistrationDataMetadata; +import org.dspace.eperson.dao.RegistrationDataMetadataDAO; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationDataMetadataDAOImpl extends AbstractHibernateDAO + implements RegistrationDataMetadataDAO { +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataChanges.java b/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataChanges.java new file mode 100644 index 000000000000..431fa8496861 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataChanges.java @@ -0,0 +1,49 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.eperson.dto; + +import org.dspace.eperson.RegistrationTypeEnum; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationDataChanges { + + private static final String EMAIL_PATTERN = + "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)" + + "+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"; + + private final String email; + private final RegistrationTypeEnum registrationType; + + public RegistrationDataChanges(String email, RegistrationTypeEnum type) { + if (email == null || email.trim().isBlank()) { + throw new IllegalArgumentException("Cannot update with an empty email address"); + } + if (type == null) { + throw new IllegalArgumentException("Cannot update with a null registration type"); + } + this.email = email; + if (!isValidEmail()) { + throw new IllegalArgumentException("Invalid email address provided!"); + } + this.registrationType = type; + } + + public boolean isValidEmail() { + return email.matches(EMAIL_PATTERN); + } + + public String getEmail() { + return email; + } + + public RegistrationTypeEnum getRegistrationType() { + return registrationType; + } +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataPatch.java b/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataPatch.java new file mode 100644 index 000000000000..e681193d3dd2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dto/RegistrationDataPatch.java @@ -0,0 +1,32 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.eperson.dto; + +import org.dspace.eperson.RegistrationData; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationDataPatch { + + private final RegistrationData oldRegistration; + private final RegistrationDataChanges changes; + + public RegistrationDataPatch(RegistrationData oldRegistration, RegistrationDataChanges changes) { + this.oldRegistration = oldRegistration; + this.changes = changes; + } + + public RegistrationData getOldRegistration() { + return oldRegistration; + } + + public RegistrationDataChanges getChanges() { + return changes; + } +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java b/dspace-api/src/main/java/org/dspace/eperson/service/AccountService.java index 2cc0c8c355ef..ebfa7fc89d91 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 000000000000..b547c4fca80b --- /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 d1e78fa2bce2..f10da961ca48 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/model/ExternalDataObject.java b/dspace-api/src/main/java/org/dspace/external/model/ExternalDataObject.java index eac9921df6cc..44ad6a70953e 100644 --- a/dspace-api/src/main/java/org/dspace/external/model/ExternalDataObject.java +++ b/dspace-api/src/main/java/org/dspace/external/model/ExternalDataObject.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.UUID; import org.dspace.content.dto.MetadataValueDTO; @@ -38,6 +39,8 @@ public class ExternalDataObject { */ private String displayValue; + private List matchUUIDs; + /** * Default constructor */ @@ -143,4 +146,16 @@ public String getValue() { public void setValue(String value) { this.value = value; } + + public List getMatchUUIDs() { + return matchUUIDs; + } + + public void setMatchUUIDs(List matchUUIDs) { + this.matchUUIDs = matchUUIDs; + } + + public boolean isDuplicated() { + return !matchUUIDs.isEmpty(); + } } diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/LiveImportDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/LiveImportDataProvider.java index 21c14813f93d..9897639f04a6 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/LiveImportDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/LiveImportDataProvider.java @@ -9,6 +9,7 @@ import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -135,9 +136,8 @@ public int getNumberOfResults(String query) { * @return */ private ExternalDataObject getExternalDataObject(ImportRecord record) { - //return 400 if no record were found - if (record == null) { - throw new IllegalArgumentException("No record found for query or id"); + if (Objects.isNull(record)) { + return null; } ExternalDataObject externalDataObject = new ExternalDataObject(sourceIdentifier); String id = getFirstValue(record, recordIdMetadata); diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java index 7a836113936c..a07cf89c503e 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidV3AuthorDataProvider.java @@ -16,6 +16,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -27,6 +28,7 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.content.MetadataFieldName; import org.dspace.content.dto.MetadataValueDTO; import org.dspace.external.OrcidRestConnector; import org.dspace.external.model.ExternalDataObject; @@ -35,6 +37,7 @@ import org.json.JSONObject; import org.orcid.jaxb.model.v3.release.common.OrcidIdentifier; import org.orcid.jaxb.model.v3.release.record.Person; +import org.orcid.jaxb.model.v3.release.record.Record; import org.orcid.jaxb.model.v3.release.search.Result; import org.springframework.beans.factory.annotation.Autowired; @@ -60,6 +63,8 @@ public class OrcidV3AuthorDataProvider extends AbstractExternalDataProvider { private XMLtoBio converter; + private Map externalIdentifiers; + public static final String ORCID_ID_SYNTAX = "\\d{4}-\\d{4}-\\d{4}-(\\d{3}X|\\d{4})"; private static final int MAX_INDEX = 10000; @@ -113,12 +118,13 @@ public void init() throws IOException { @Override public Optional getExternalDataObject(String id) { - Person person = getBio(id); - ExternalDataObject externalDataObject = convertToExternalDataObject(person); + Record record = getBio(id); + ExternalDataObject externalDataObject = convertToExternalDataObject(record); return Optional.of(externalDataObject); } - protected ExternalDataObject convertToExternalDataObject(Person person) { + protected ExternalDataObject convertToExternalDataObject(Record record) { + Person person = record.getPerson(); ExternalDataObject externalDataObject = new ExternalDataObject(sourceIdentifier); if (person.getName() != null) { String lastName = ""; @@ -141,6 +147,12 @@ protected ExternalDataObject convertToExternalDataObject(Person person) { externalDataObject .addMetadata(new MetadataValueDTO("dc", "identifier", "uri", null, orcidUrl + '/' + person.getName().getPath())); + + appendOtherNames(externalDataObject, person); + appendResearcherUrls(externalDataObject, person); + appendExternalIdentifiers(externalDataObject, person); + appendAffiliations(externalDataObject, record); + if (!StringUtils.isBlank(lastName) && !StringUtils.isBlank(firstName)) { externalDataObject.setDisplayValue(lastName + ", " + firstName); externalDataObject.setValue(lastName + ", " + firstName); @@ -157,24 +169,64 @@ protected ExternalDataObject convertToExternalDataObject(Person person) { return externalDataObject; } + private void appendOtherNames(ExternalDataObject externalDataObject, Person person) { + person.getOtherNames().getOtherNames().forEach(otherName -> + externalDataObject.addMetadata(new MetadataValueDTO("crisrp", "name", "variant", null, + otherName.getContent()))); + } + + private void appendResearcherUrls(ExternalDataObject externalDataObject, Person person) { + person.getResearcherUrls().getResearcherUrls().forEach(researcherUrl -> + externalDataObject.addMetadata(new MetadataValueDTO("oairecerif", "identifier", "url", null, + researcherUrl.getUrl().getValue()))); + } + + private void appendExternalIdentifiers(ExternalDataObject externalDataObject, Person person) { + if (getExternalIdentifiers() != null) { + person.getExternalIdentifiers() + .getExternalIdentifiers() + .forEach(externalIdentifier -> { + String metadataField = externalIdentifiers.get(externalIdentifier.getType()); + if (StringUtils.isNotEmpty(metadataField)) { + MetadataFieldName field = new MetadataFieldName(metadataField); + externalDataObject.addMetadata( + new MetadataValueDTO(field.schema, field.element, field.qualifier, null, + externalIdentifier.getValue())); + } + }); + } + } + + private void appendAffiliations(ExternalDataObject externalDataObject, Record record) { + record.getActivitiesSummary() + .getEmployments() + .getEmploymentGroups() + .stream() + .flatMap(affiliationGroup -> + affiliationGroup.getActivities().stream()) + .forEach(employmentSummary -> + externalDataObject.addMetadata(new MetadataValueDTO("person", "affiliation", "name", + null, employmentSummary.getOrganization().getName()))); + } + /** - * Retrieve a Person object based on a given orcid identifier. + * Retrieve a Record object based on a given orcid identifier. * @param id orcid identifier - * @return Person + * @return Record */ - public Person getBio(String id) { + public Record getBio(String id) { log.debug("getBio called with ID=" + id); if (!isValid(id)) { return null; } - InputStream bioDocument = orcidRestConnector.get(id + ((id.endsWith("/person")) ? "" : "/person"), accessToken); - Person person = converter.convertSinglePerson(bioDocument); + InputStream bioDocument = orcidRestConnector.get(id, accessToken); + Record record = converter.convertToRecord(bioDocument); try { bioDocument.close(); } catch (IOException e) { log.error(e.getMessage(), e); } - return person; + return record; } /** @@ -201,13 +253,13 @@ public List searchExternalDataObjects(String query, int star log.debug("queryBio searchPath=" + searchPath + " accessToken=" + accessToken); InputStream bioDocument = orcidRestConnector.get(searchPath, accessToken); List results = converter.convert(bioDocument); - List bios = new LinkedList<>(); + List bios = new LinkedList<>(); for (Result result : results) { OrcidIdentifier orcidIdentifier = result.getOrcidIdentifier(); if (orcidIdentifier != null) { log.debug("Found OrcidId=" + orcidIdentifier.toString()); String orcid = orcidIdentifier.getPath(); - Person bio = getBio(orcid); + Record bio = getBio(orcid); if (bio != null) { bios.add(bio); } @@ -298,4 +350,11 @@ public void setOrcidRestConnector(OrcidRestConnector orcidRestConnector) { this.orcidRestConnector = orcidRestConnector; } + public Map getExternalIdentifiers() { + return externalIdentifiers; + } + + public void setExternalIdentifiers(Map externalIdentifiers) { + this.externalIdentifiers = externalIdentifiers; + } } diff --git a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/XMLtoBio.java b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/XMLtoBio.java index 25b3cf787feb..ff7cedbb47ab 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/XMLtoBio.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/orcid/xml/XMLtoBio.java @@ -14,6 +14,7 @@ import org.apache.logging.log4j.Logger; import org.orcid.jaxb.model.v3.release.record.Person; +import org.orcid.jaxb.model.v3.release.record.Record; import org.orcid.jaxb.model.v3.release.search.Result; import org.orcid.jaxb.model.v3.release.search.Search; import org.xml.sax.SAXException; @@ -64,4 +65,15 @@ public Person convertSinglePerson(InputStream xml) { } return null; } + + public Record convertToRecord(InputStream xml) { + Record record = null; + try { + record = (Record) unmarshall(xml, Record.class); + return record; + } catch (SAXException | URISyntaxException e) { + log.error(e); + } + return record; + } } diff --git a/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java b/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java index 7804dfa5689f..76e4fff4f527 100644 --- a/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java @@ -7,12 +7,24 @@ */ package org.dspace.external.service.impl; +import static org.dspace.app.deduplication.service.impl.SolrDedupServiceImpl.RESOURCE_FLAG_FIELD; +import static org.dspace.app.deduplication.service.impl.SolrDedupServiceImpl.RESOURCE_IDS_FIELD; +import static org.dspace.app.deduplication.service.impl.SolrDedupServiceImpl.RESOURCE_SIGNATURE_FIELD; + import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.SolrDocument; +import org.dspace.app.deduplication.service.DedupService; +import org.dspace.app.deduplication.service.impl.SolrDedupServiceImpl; +import org.dspace.app.deduplication.utils.Signature; import org.dspace.app.suggestion.SuggestionProvider; import org.dspace.app.suggestion.SuggestionService; import org.dspace.authorize.AuthorizeException; @@ -22,11 +34,14 @@ import org.dspace.content.dto.MetadataValueDTO; import org.dspace.content.service.ItemService; import org.dspace.content.service.WorkspaceItemService; +import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogHelper; +import org.dspace.discovery.SearchServiceException; import org.dspace.external.model.ExternalDataObject; import org.dspace.external.provider.ExternalDataProvider; import org.dspace.external.service.ExternalDataService; +import org.dspace.utils.DSpace; import org.springframework.beans.factory.annotation.Autowired; /** @@ -49,6 +64,9 @@ public class ExternalDataServiceImpl implements ExternalDataService { @Autowired private SuggestionService suggestionService; + @Autowired + private DedupService dedupService; + @Override public Optional getExternalDataObject(String source, String id) { ExternalDataProvider provider = getExternalDataProvider(source); @@ -64,9 +82,53 @@ public List searchExternalDataObjects(String source, String if (provider == null) { throw new IllegalArgumentException("Provider for: " + source + " couldn't be found"); } - return provider.searchExternalDataObjects(query, start, limit); + + List externalDataObjects = provider.searchExternalDataObjects(query, start, limit); + appendMatchedUUIDs(externalDataObjects); + + return externalDataObjects; + } + + private void appendMatchedUUIDs(List externalDataObjects) { + for (ExternalDataObject externalDataObject : externalDataObjects) { + List uuids = new ArrayList<>(); + try { + QueryResponse response = dedupService.find("*:*", buildFilters(externalDataObject)); + for (SolrDocument resultDoc : response.getResults()) { + uuids.addAll(resultDoc.getFieldValues(RESOURCE_IDS_FIELD) + .stream() + .map(id -> + UUID.fromString(String.valueOf(id))) + .collect(Collectors.toList())); + } + externalDataObject.setMatchUUIDs(uuids); + } catch (SearchServiceException e) { + throw new RuntimeException(e); + } + } + } + + private String[] buildFilters(ExternalDataObject object) { + List filters = new ArrayList<>(); + List allSignatures = getAllSignatures(object); + + if (!allSignatures.isEmpty()) { + filters.add(RESOURCE_FLAG_FIELD + ":" + SolrDedupServiceImpl.DeduplicationFlag.FAKE.getDescription()); + filters.add(RESOURCE_SIGNATURE_FIELD + ":(" + + StringUtils.joinWith(" OR ", allSignatures.stream().toArray(String[]::new)) + ")"); + } + + return filters.toArray(new String[filters.size()]); } + private List getAllSignatures(ExternalDataObject iu) { + List signAlgo = new DSpace().getServiceManager().getServicesByType(Signature.class); + return signAlgo.stream() + .filter(algo -> Constants.ITEM == algo.getResourceTypeID()) + .flatMap(algo -> algo.getSignature(iu).stream()) + .filter(signature -> StringUtils.isNotEmpty(signature)) + .collect(Collectors.toList()); + } @Override public List getExternalDataProviders() { diff --git a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java index b70eda960d35..4550a84b1c0a 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/importer/external/bibtex/service/BibtexImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/bibtex/service/BibtexImportMetadataSourceServiceImpl.java index 0014088c8650..4b6a5aa92e72 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/bibtex/service/BibtexImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/bibtex/service/BibtexImportMetadataSourceServiceImpl.java @@ -117,4 +117,9 @@ public void setMetadataFieldMap(@SuppressWarnings("rawtypes") Map metadataFieldM super.setMetadataFieldMap(metadataFieldMap); } + @Override + public boolean canImportMultipleRecords() { + return true; + } + } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java b/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java index e2cb24f4b578..b686169f9a21 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java @@ -11,7 +11,9 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; +import org.dspace.content.MetadataFieldName; import org.dspace.importer.external.metadatamapping.MetadatumDTO; /** @@ -94,6 +96,17 @@ public Collection getValue(String schema, String element, String q return values; } + public Optional getSingleValue(String field) { + MetadataFieldName metadataFieldName = new MetadataFieldName(field); + return getSingleValue(metadataFieldName.schema, metadataFieldName.element, metadataFieldName.qualifier); + } + + public Optional getSingleValue(String schema, String element, String qualifier) { + return getValue(schema, element, qualifier).stream() + .map(MetadatumDTO::getValue) + .findFirst(); + } + /** * Add a value to the valueList * diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java new file mode 100644 index 000000000000..be1910d7a521 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.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.importer.external.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +public class RorParentOrgUnitMetadataContributor extends SimpleJsonPathMetadataContributor { + + private String typeField; + + private String parentType; + + private String labelField; + + /** + * Retrieve the metadata associated with the given object. + * The toString() of the resulting object will be used. + * + * @param fullJson A class to retrieve metadata from. + * @return a collection of import records. Only the identifier of the found records may be put in the record. + */ + @Override + public Collection contributeMetadata(String fullJson) { + + Collection metadata = new ArrayList<>(); + Collection metadataValue = new ArrayList<>(); + + JsonNode jsonNode = convertStringJsonToJsonNode(fullJson); + JsonNode array = jsonNode.at(getQuery()); + if (!array.isArray()) { + return metadata; + } + + Iterator nodes = array.iterator(); + while (nodes.hasNext()) { + JsonNode node = nodes.next(); + + if (!node.has(labelField)) { + continue; + } + + String type = node.has(typeField) ? node.get(typeField).asText() : null; + String label = node.get(labelField).asText(); + + if (parentType.equalsIgnoreCase(type)) { + metadataValue.add(label); + } + + } + + for (String value : metadataValue) { + MetadatumDTO metadatumDto = new MetadatumDTO(); + metadatumDto.setValue(value); + metadatumDto.setElement(getField().getElement()); + metadatumDto.setQualifier(getField().getQualifier()); + metadatumDto.setSchema(getField().getSchema()); + metadata.add(metadatumDto); + } + return metadata; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = null; + try { + body = mapper.readTree(json); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return body; + } + + public String getTypeField() { + return typeField; + } + + public void setTypeField(String typeField) { + this.typeField = typeField; + } + + public String getLabelField() { + return labelField; + } + + public void setLabelField(String labelField) { + this.labelField = labelField; + } + + public String getParentType() { + return parentType; + } + + public void setParentType(String parentType) { + this.parentType = parentType; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java new file mode 100644 index 000000000000..5248d793e292 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java @@ -0,0 +1,38 @@ +/** + * 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.importer.external.ror.service; + +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + + +/** + * An implementation of {@link AbstractMetadataFieldMapping} + * Responsible for defining the mapping of the Scopus metadatum fields on the DSpace metadatum fields + * + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + */ +public class RorFieldMapping extends AbstractMetadataFieldMapping { + + /** + * Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + * only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + * what metadatafield is generated. + * + * @param metadataFieldMap The map containing the link between retrieve metadata and metadata that will be set to + * the item. + */ + @Override + @Resource(name = "rorMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..650e939e3dd0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java @@ -0,0 +1,285 @@ +/** + * 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.importer.external.ror.service; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; +import javax.el.MethodNotFoundException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.liveimportclient.service.LiveImportClient; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.components.QuerySource; +import org.springframework.beans.factory.annotation.Autowired; + +public class RorImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private final static Logger log = LogManager.getLogger(); + + private String url; + + private int timeout = 1000; + + @Autowired + private LiveImportClient liveImportClient; + + @Override + public String getImportSource() { + return "ror"; + } + + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + List records = retry(new SearchByIdCallable(id)); + return CollectionUtils.isEmpty(records) ? null : records.get(0); + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query, start)); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query)); + } + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + List records = retry(new SearchByIdCallable(query)); + return CollectionUtils.isEmpty(records) ? null : records.get(0); + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for ROR"); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for ROR"); + } + + @Override + public void init() throws Exception { + } + + /** + * This class is a Callable implementation to get ADS entries based on query + * object. This Callable use as query value the string queryString passed to + * constructor. If the object will be construct through Query.class instance, a + * Query's map entry with key "query" will be used. Pagination is supported too, + * using the value of the Query's map with keys "start" and "count". + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ + private class SearchByQueryCallable implements Callable> { + + private Query query; + + private SearchByQueryCallable(String queryString, int start) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("start", start); + } + + private SearchByQueryCallable(Query query) { + this.query = query; + } + + @Override + public List call() throws Exception { + return search(query.getParameterAsClass("query", String.class), + query.getParameterAsClass("start", Integer.class)); + } + } + + /** + * This class is a Callable implementation to get an ADS entry using bibcode The + * bibcode to use can be passed through the constructor as a String or as + * Query's map entry, with the key "id". + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ + private class SearchByIdCallable implements Callable> { + private Query query; + + private SearchByIdCallable(Query query) { + this.query = query; + } + + private SearchByIdCallable(String id) { + this.query = new Query(); + query.addParameter("id", id); + } + + @Override + public List call() throws Exception { + return searchById(query.getParameterAsClass("id", String.class)); + } + } + + /** + * This class is a Callable implementation to count the number of entries for an + * ADS query. This Callable use as query value to ADS the string queryString + * passed to constructor. If the object will be construct through Query.class + * instance, the value of the Query's map with the key "query" will be used. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ + private class CountByQueryCallable implements Callable { + private Query query; + + private CountByQueryCallable(String queryString) { + query = new Query(); + query.addParameter("query", queryString); + } + + private CountByQueryCallable(Query query) { + this.query = query; + } + + @Override + public Integer call() throws Exception { + return count(query.getParameterAsClass("query", String.class)); + } + } + + public Integer count(String query) { + try { + Map> params = new HashMap>(); + + URIBuilder uriBuilder = new URIBuilder(this.url); + uriBuilder.addParameter("query", query); + + String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); + if (StringUtils.isEmpty(resp)) { + return 0; + } + JsonNode jsonNode = convertStringJsonToJsonNode(resp); + return jsonNode.at("/number_of_results").asInt(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + return 0; + } + + private List searchById(String id) { + + List adsResults = new ArrayList<>(); + + id = StringUtils.removeStart(id, "https://ror.org/"); + + try { + Map> params = new HashMap>(); + + URIBuilder uriBuilder = new URIBuilder(this.url + "/" + id); + + String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); + if (StringUtils.isEmpty(resp)) { + return adsResults; + } + + JsonNode jsonNode = convertStringJsonToJsonNode(resp); + adsResults.add(transformSourceRecords(jsonNode.toString())); + + } catch (URISyntaxException e) { + e.printStackTrace(); + } + return adsResults; + } + + private List search(String query, Integer start) { + List adsResults = new ArrayList<>(); + try { + Map> params = new HashMap>(); + + URIBuilder uriBuilder = new URIBuilder(this.url); + uriBuilder.addParameter("query", query); + if (start != null) { + uriBuilder.addParameter("page", String.valueOf((start / 20) + 1)); + } + + String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); + if (StringUtils.isEmpty(resp)) { + return adsResults; + } + + JsonNode jsonNode = convertStringJsonToJsonNode(resp); + JsonNode docs = jsonNode.at("/items"); + if (docs.isArray()) { + Iterator nodes = docs.elements(); + while (nodes.hasNext()) { + JsonNode node = nodes.next(); + adsResults.add(transformSourceRecords(node.toString())); + } + } else { + adsResults.add(transformSourceRecords(docs.toString())); + } + } catch (URISyntaxException e) { + e.printStackTrace(); + } + + if (start == null) { + return adsResults; + } + + if (start % 20 == 0) { + return adsResults.stream() + .limit(10) + .collect(Collectors.toList()); + } else { + return adsResults.stream() + .skip(10) + .collect(Collectors.toList()); + } + } + + private JsonNode convertStringJsonToJsonNode(String json) { + try { + return new ObjectMapper().readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process json response.", e); + } + return null; + } + + public void setUrl(String url) { + this.url = url; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/service/ImportService.java b/dspace-api/src/main/java/org/dspace/importer/external/service/ImportService.java index 28df30b345bc..a444a3609c15 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/service/ImportService.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/service/ImportService.java @@ -311,7 +311,7 @@ public boolean canImportFromFile(String originalName) { /* * Get a collection of record from File, * The first match will be return. - * + * * @param file The file from which will read records * @param originalName The original file name or full path * @return a single record contains the metadatum @@ -319,28 +319,83 @@ public boolean canImportFromFile(String originalName) { */ public ImportRecord getRecord(File file, String originalName) throws FileMultipleOccurencesException, FileSourceException { - ImportRecord importRecords = null; - for (MetadataSource metadataSource : importSources.values()) { - try (InputStream fileInputStream = new FileInputStream(file)) { - if (metadataSource instanceof FileSource) { - FileSource fileSource = (FileSource)metadataSource; - if (fileSource.isValidSourceForFile(originalName)) { - importRecords = fileSource.getRecord(fileInputStream); - break; + try (InputStream fileInputStream = new FileInputStream(file)) { + FileSource fileSource = this.getFileSource(fileInputStream, originalName); + try { + if (fileSource.isValidSourceForFile(originalName)) { + return fileSource.getRecord(fileInputStream); + } + } catch (FileSourceException e) { + log.debug(fileSource.getImportSource() + " isn't a valid parser for file", e); + } + //catch statements is required because we could have supported format (i.e. XML) + //which fail on schema validation + } catch (FileMultipleOccurencesException e) { + log.debug("File contains multiple metadata, return with error"); + throw e; + } catch (IOException e1) { + throw new FileSourceException("File cannot be read, may be null"); + } + return null; + } + + /** + * Get a collection of record from File, + * + * @param file The file from which will read records + * @param originalName The original file name or full path + * @return records containing metdatum + * @throws FileMultipleOccurencesException if the import configured for the {@code file} + * doesn't allow multiple records import. + * @throws FileSourceException if the file cannot be read. + */ + public List getRecords(File file, String originalName) + throws FileMultipleOccurencesException, FileSourceException { + try (InputStream fileInputStream = new FileInputStream(file)) { + FileSource fileSource = this.getFileSource(fileInputStream, originalName); + try { + if (fileSource.isValidSourceForFile(originalName)) { + List records = fileSource.getRecords(fileInputStream); + if (!fileSource.canImportMultipleRecords() && records.size() > 1) { + throw new FileMultipleOccurencesException( + "Found " + records.size() + " entries in file ( " + + originalName + + " ) but import source ( " + + fileSource.getImportSource() + + " ) not allowed to import multiple records" + ); } + return records; } + } catch (FileSourceException e) { + log.debug(fileSource.getImportSource() + " isn't a valid parser for file", e); + } //catch statements is required because we could have supported format (i.e. XML) //which fail on schema validation - } catch (FileSourceException e) { - log.debug(metadataSource.getImportSource() + " isn't a valid parser for file"); - } catch (FileMultipleOccurencesException e) { - log.debug("File contains multiple metadata, return with error"); - throw e; - } catch (IOException e1) { - throw new FileSourceException("File cannot be read, may be null"); + } catch (IOException e1) { + throw new FileSourceException("File cannot be read, may be null"); + } + return null; + } + + protected FileSource getFileSource(File file, String originalName) throws FileSourceException { + try (InputStream fileInputStream = new FileInputStream(file)) { + return getFileSource(file, originalName); + } catch (IOException e1) { + throw new FileSourceException("File cannot be read, may be null"); + } + } + + protected FileSource getFileSource(InputStream fileInputStream, String originalName) { + for (MetadataSource metadataSource : importSources.values()) { + if (metadataSource instanceof FileSource) { + FileSource fileSource = (FileSource)metadataSource; + if (fileSource.isValidSourceForFile(originalName)) { + return fileSource; + } } } - return importRecords; + return null; } /** diff --git a/dspace-api/src/main/java/org/dspace/importer/external/service/components/AbstractPlainMetadataSource.java b/dspace-api/src/main/java/org/dspace/importer/external/service/components/AbstractPlainMetadataSource.java index 5d83b9a7cce4..b58f69b6665c 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/service/components/AbstractPlainMetadataSource.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/service/components/AbstractPlainMetadataSource.java @@ -41,7 +41,7 @@ public abstract class AbstractPlainMetadataSource /** * Set the file extensions supported by this metadata service - * + * * @param supportedExtensions the file extensions (xml,txt,...) supported by this service */ public void setSupportedExtensions(List supportedExtensions) { @@ -64,6 +64,9 @@ public List getSupportedExtensions() { @Override public List getRecords(InputStream is) throws FileSourceException { List datas = readData(is); + if (datas == null) { + return List.of(); + } List records = new ArrayList<>(); for (PlainMetadataSourceDto item : datas) { records.add(toRecord(item)); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/service/components/FileSource.java b/dspace-api/src/main/java/org/dspace/importer/external/service/components/FileSource.java index 801f5474bb4e..fffd476a69ee 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/service/components/FileSource.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/service/components/FileSource.java @@ -52,7 +52,7 @@ public ImportRecord getRecord(InputStream inputStream) /** * This method is used to decide if the FileSource manage the file format - * + * * @param originalName the file file original name * @return true if the FileSource can parse the file, false otherwise */ @@ -67,4 +67,13 @@ public default boolean isValidSourceForFile(String originalName) { return false; } + /** + * This method is used to determine if we can import multiple records at once placed in the same source file. + * + * @return true if allowed to import multiple records in the same file, false otherwise + */ + public default boolean canImportMultipleRecords() { + return false; + } + } 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 ad4be3ddc8b5..198d00636431 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 a1bc95b818c3..269dd6601853 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/metrics/MetricsExternalServices.java b/dspace-api/src/main/java/org/dspace/metrics/MetricsExternalServices.java index b680e43eeab7..79543f43ce1e 100644 --- a/dspace-api/src/main/java/org/dspace/metrics/MetricsExternalServices.java +++ b/dspace-api/src/main/java/org/dspace/metrics/MetricsExternalServices.java @@ -118,4 +118,8 @@ public int getFetchSize() { public void setFetchSize(int fetchSize) { this.fetchSize = fetchSize; } + + public List getLogs() { + return null; + } } diff --git a/dspace-api/src/main/java/org/dspace/metrics/UpdateCrisMetricsWithExternalSource.java b/dspace-api/src/main/java/org/dspace/metrics/UpdateCrisMetricsWithExternalSource.java index 12877252791b..82c690e62caa 100644 --- a/dspace-api/src/main/java/org/dspace/metrics/UpdateCrisMetricsWithExternalSource.java +++ b/dspace-api/src/main/java/org/dspace/metrics/UpdateCrisMetricsWithExternalSource.java @@ -13,6 +13,7 @@ import java.sql.SQLException; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.function.Function; @@ -108,6 +109,7 @@ public void internalRun() throws Exception { performUpdate(externalService); context.complete(); } catch (Exception e) { + getLogsFromMetricService(externalService); log.error(e.getMessage(), e); handler.handleException(e); context.abort(); @@ -184,6 +186,7 @@ private void performUpdateWithMultiFetch(MetricsExternalServices metricsServices long updatedItems = metricsServices.updateMetric(context, itemIterator, param); + getLogsFromMetricService(metricsServices); handler.logInfo("Updated " + updatedItems + " metrics"); handler.logInfo("Update end"); @@ -216,6 +219,7 @@ private void performUpdateWithSingleFetches(MetricsExternalServices metricsServi } context.commit(); + getLogsFromMetricService(metricsServices); handler.logInfo("Found " + countFoundItems + " items"); handler.logInfo("Updated " + countUpdatedItems + " metrics"); handler.logInfo("Update end"); @@ -240,4 +244,10 @@ private void assignSpecialGroupsInContext() throws SQLException { } } + private void getLogsFromMetricService(MetricsExternalServices metricsServices) { + List metricLogger = metricsServices.getLogs(); + if (metricLogger != null) { + metricLogger.forEach(message -> handler.logInfo(message)); + } + } } diff --git a/dspace-api/src/main/java/org/dspace/metrics/scopus/ScopusProvider.java b/dspace-api/src/main/java/org/dspace/metrics/scopus/ScopusProvider.java index cf3eb875eb74..9bafca2d5169 100644 --- a/dspace-api/src/main/java/org/dspace/metrics/scopus/ScopusProvider.java +++ b/dspace-api/src/main/java/org/dspace/metrics/scopus/ScopusProvider.java @@ -40,9 +40,15 @@ public class ScopusProvider { private static final Logger log = LogManager.getLogger(ScopusProvider.class); + private List logsCache = new ArrayList<>(); + @Autowired private ScopusRestConnector scopusRestConnector; + public List getLogs() { + return logsCache; + } + /** *

      * This methods fetch a list of metrics using the {@code id} param, @@ -54,6 +60,7 @@ public class ScopusProvider { * @return List of CrisMetrics fetched */ public List getScopusList(String id) { + logsCache = new ArrayList<>(); String scopusResponse = getRecords(id); if (StringUtils.isNotBlank(scopusResponse)) { List crisMetricList = mapToCrisMetricList(scopusResponse); @@ -66,7 +73,7 @@ public List getScopusList(String id) { } return crisMetricList; } - log.error("The query : " + id + " is wrong!"); + logAndCache("The query : " + id + " is wrong!"); return List.of(); } @@ -75,7 +82,7 @@ public CrisMetricDTO getScopusObject(String id) { if (StringUtils.isNotBlank(scopusResponse)) { return mapToCrisMetric(scopusResponse); } - log.error("The query : " + id + " is wrong!"); + logAndCache("The query : " + id + " is wrong!"); return null; } @@ -94,7 +101,7 @@ private CrisMetricDTO mapToCrisMetric(String scopusResponse) { docBuilder = docBuilderFactory.newDocumentBuilder(); parsedResponse = docBuilder.parse(new InputSource(new StringReader(scopusResponse))); } catch (ParserConfigurationException | SAXException | IOException e) { - log.error(e.getMessage(), e); + logAndCacheError(e); } return mapToCrisMetric(parsedResponse); } @@ -107,7 +114,7 @@ private List mapToCrisMetricList(String scopusResponse) { docBuilder = docBuilderFactory.newDocumentBuilder(); parsedResponse = docBuilder.parse(new InputSource(new StringReader(scopusResponse))); } catch (ParserConfigurationException | SAXException | IOException e) { - log.error(e.getMessage(), e); + logAndCacheError(e); } return mapToCrisMetricList(parsedResponse); } @@ -134,7 +141,7 @@ private String getNext(String scopusResponse) { .map(element -> element.getAttribute("href")) .orElse(null); } catch (ParserConfigurationException | SAXException | IOException e) { - log.error(e.getMessage(), e); + logAndCacheError(e); } return nextUrl; } @@ -148,7 +155,7 @@ private List mapToCrisMetricList(Document doc) { .filter(Objects::nonNull) .collect(Collectors.toList()); } catch (Exception e) { - log.error(e.getMessage(), e); + logAndCacheError(e); } return scopusCitationList; } @@ -162,7 +169,7 @@ private CrisMetricDTO mapToCrisMetric(Document doc) { .map(this::mapToCrisMetric) .orElse(null); } catch (Exception e) { - log.error(e.getMessage(), e); + logAndCacheError(e); } return scopusCitation; } @@ -170,13 +177,13 @@ private CrisMetricDTO mapToCrisMetric(Document doc) { private CrisMetricDTO mapToCrisMetric(Element dataRoot) { CrisMetricDTO scopusCitation = new CrisMetricDTO(); if (dataRoot == null) { - log.debug("No citation entry found in Scopus"); + logAndCache("No citation entry found in Scopus"); return scopusCitation; } Element errorScopusResp = XMLUtils.getSingleElement(dataRoot, "error"); if (errorScopusResp != null) { - log.debug("Error citation entry found in Scopus: " + errorScopusResp.getTextContent()); + logAndCache("Error citation entry found in Scopus: " + errorScopusResp.getTextContent()); return scopusCitation; } @@ -203,10 +210,25 @@ private CrisMetricDTO mapToCrisMetric(Element dataRoot) { try { scopusCitation.setMetricCount(Double.valueOf(numCitations)); } catch (NullPointerException | NumberFormatException ex) { - log.error("Error while trying to parse numCitations:" + numCitations); + logAndCacheErrorWithMessage("Error while trying to parse numCitations:" + numCitations, ex); } scopusCitation.setRemark(scopusCitation.buildMetricsRemark()); return scopusCitation; } + private void logAndCache(String message) { + logsCache.add("INFO: " + message); + log.debug(message); + } + + private void logAndCacheErrorWithMessage(String message, Throwable e) { + logsCache.add("ERROR: " + message + '\n' + e.getMessage()); + log.error(message, e); + } + + private void logAndCacheError(Throwable e) { + logsCache.add("ERROR: " + e.getMessage()); + log.error(e.getMessage(), e); + } + } \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/metrics/scopus/UpdateScopusMetrics.java b/dspace-api/src/main/java/org/dspace/metrics/scopus/UpdateScopusMetrics.java index bd11feb99d3b..782f6f832fb2 100644 --- a/dspace-api/src/main/java/org/dspace/metrics/scopus/UpdateScopusMetrics.java +++ b/dspace-api/src/main/java/org/dspace/metrics/scopus/UpdateScopusMetrics.java @@ -42,6 +42,8 @@ public class UpdateScopusMetrics extends MetricsExternalServices { public static final String SCOPUS_CITATION = "scopusCitation"; + private List logsCache = new ArrayList<>(); + @Autowired private ScopusProvider scopusProvider; @@ -61,6 +63,10 @@ public List getFilters() { return Arrays.asList("dspace.entity.type:Publication", "dc.identifier.doi:* OR dc.identifier.pmid:*"); } + public List getLogs() { + return logsCache; + } + @Override public boolean updateMetric(Context context, Item item, String param) { String id = buildQuery(item); @@ -76,16 +82,20 @@ public long updateMetric(Context context, Iterator itemIterator, String pa long updatedItems = 0; long foundItems = 0; long apiCalls = 0; + logsCache = new ArrayList<>(); try { while (itemIterator.hasNext()) { Map queryMap = new HashMap<>(); List itemList = new ArrayList<>(); for (int i = 0; i < fetchSize && itemIterator.hasNext(); i++) { Item item = itemIterator.next(); + logAndCache("Adding item with uuid: " + item.getID()); setLastImportMetadataValue(context, item); itemList.add(item); } foundItems += itemList.size(); + String id = this.generateQuery(queryMap, itemList); + logAndCache("Getting scopus metrics for " + id); updatedItems += scopusProvider.getScopusList(this.generateQuery(queryMap, itemList)) .stream() @@ -102,11 +112,11 @@ public long updateMetric(Context context, Iterator itemIterator, String pa context.commit(); } } catch (SQLException e) { - log.error("Error while updating scopus' metrics", e); - throw new RuntimeException(e.getMessage(), e); + logAndCacheError("Error while updating scopus' metrics", e); } finally { - log.info("Found and fetched {} with {} api calls!", foundItems, apiCalls); + logAndCache("Found and fetched " + foundItems + " with " + apiCalls + " api calls!"); } + logsCache.addAll(scopusProvider.getLogs()); return updatedItems; } @@ -213,6 +223,7 @@ private boolean updateScopusMetrics(Context context, Item currentItem, CrisMetri createNewScopusMetrics(context,currentItem, scopusMetric, deltaPeriod1, deltaPeriod2); } catch (SQLException | AuthorizeException e) { + logsCache.add(e.getMessage()); log.error(e.getMessage(), e); } return true; @@ -236,4 +247,14 @@ private Double getDeltaPeriod(CrisMetricDTO currentMetric, Optional } return null; } + + private void logAndCache(String message) { + logsCache.add("INFO: " + message); + log.info(message); + } + + private void logAndCacheError(String message, Throwable e) { + logsCache.add("ERROR: " + message + '\n' + e.getMessage()); + log.error(message, e); + } } diff --git a/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java b/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java index cec440df6d45..32915d74c0cf 100644 --- a/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java @@ -314,17 +314,20 @@ private Item createProfileItem(Context context, EPerson ePerson, Collection coll item = installItemService.installItem(context, workspaceItem); + context.uncacheEntity(workspaceItem); + if (isNewProfileNotVisibleByDefault()) { Group anonymous = groupService.findByName(context, ANONYMOUS); authorizeService.removeGroupPolicies(context, item, anonymous); } - authorizeService.addPolicy(context, item, READ, ePerson); + itemService.addResourcePolicy(context, item, READ, ePerson); if (isAdditionOfWritePolicyOnProfileEnabled()) { - authorizeService.addPolicy(context, item, WRITE, ePerson); + itemService.addResourcePolicy(context, item, WRITE, ePerson); } + return reloadItem(context, item); } diff --git a/dspace-api/src/main/java/org/dspace/script2externalservices/CreateWorkspaceItemWithExternalSource.java b/dspace-api/src/main/java/org/dspace/script2externalservices/CreateWorkspaceItemWithExternalSource.java index 2872ee19d125..7f8cf9e0772e 100644 --- a/dspace-api/src/main/java/org/dspace/script2externalservices/CreateWorkspaceItemWithExternalSource.java +++ b/dspace-api/src/main/java/org/dspace/script2externalservices/CreateWorkspaceItemWithExternalSource.java @@ -37,6 +37,7 @@ import org.dspace.content.MetadataValue; import org.dspace.content.WorkspaceItem; import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.content.packager.PackageUtils; import org.dspace.core.Context; import org.dspace.discovery.DiscoverQuery; import org.dspace.discovery.DiscoverQuery.SORT_ORDER; @@ -287,6 +288,8 @@ private int fillWorkspaceItems(Context context, int record, LiveImportDataProvid if (!exist(dataObject.getMetadata())) { WorkspaceItem wsItem = externalDataService.createWorkspaceItemFromExternalDataObject(context, dataObject, this.collection); + Item itemFromWs = wsItem.getItem(); + PackageUtils.addDepositLicense(context, null, itemFromWs, wsItem.getCollection()); for (List metadataList : metadataValueToAdd(wsItem.getItem())) { addMetadata(wsItem.getItem(), metadataList); } diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/ContentGenerator.java b/dspace-api/src/main/java/org/dspace/subscriptions/ContentGenerator.java index f60ac3c98edb..e36e2c42c79b 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/ContentGenerator.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/ContentGenerator.java @@ -11,27 +11,20 @@ import static org.apache.commons.lang.StringUtils.EMPTY; import java.io.ByteArrayOutputStream; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; -import java.util.Optional; -import javax.annotation.Resource; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.dspace.content.Item; -import org.dspace.content.crosswalk.StreamDisseminationCrosswalk; -import org.dspace.content.service.ItemService; -import org.dspace.core.Context; import org.dspace.core.Email; import org.dspace.core.I18nUtil; -import org.dspace.discovery.IndexableObject; import org.dspace.eperson.EPerson; -import org.dspace.subscriptions.service.SubscriptionGenerator; -import org.springframework.beans.factory.annotation.Autowired; - +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; /** @@ -39,31 +32,30 @@ * which will handle the logic of sending the emails * in case of 'content' subscriptionType */ -@SuppressWarnings("rawtypes") -public class ContentGenerator implements SubscriptionGenerator { +public class ContentGenerator { private final Logger log = LogManager.getLogger(ContentGenerator.class); + private final ConfigurationService configurationService = DSpaceServicesFactory.getInstance() + .getConfigurationService(); - @SuppressWarnings("unchecked") - @Resource(name = "entityDissemination") - private Map entityType2Disseminator = new HashMap(); - - @Autowired - private ItemService itemService; - @Override - public void notifyForSubscriptions(Context context, EPerson ePerson, - List indexableComm, - List indexableColl, - List indexableItems) { + public void notifyForSubscriptions(EPerson ePerson, + List indexableComm, + List indexableColl, + Map> indexableEntityByType) { try { if (Objects.nonNull(ePerson)) { Locale supportedLocale = I18nUtil.getEPersonLocale(ePerson); Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "subscriptions_content")); email.addRecipient(ePerson.getEmail()); - email.addArgument(generateBodyMail(context, indexableComm)); - email.addArgument(generateBodyMail(context, indexableColl)); - email.addArgument(generateBodyMail(context, indexableItems)); + email.addArgument(configurationService.getProperty("subscription.url")); + email.addArgument(generateBodyMail("Community", indexableComm)); + email.addArgument(generateBodyMail("Collection", indexableColl)); + email.addArgument( + indexableEntityByType.entrySet().stream() + .map(entry -> generateBodyMail(entry.getKey(), entry.getValue())) + .collect(Collectors.joining("\n\n")) + ); email.send(); } } catch (Exception e) { @@ -72,18 +64,22 @@ public void notifyForSubscriptions(Context context, EPerson ePerson, } } - private String generateBodyMail(Context context, List indexableObjects) { + private String generateBodyMail(String type, List subscriptionItems) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write("\n".getBytes(UTF_8)); - if (indexableObjects.size() > 0) { - for (IndexableObject indexableObject : indexableObjects) { + if (!subscriptionItems.isEmpty()) { + out.write(("\nYou have " + subscriptionItems.size() + " subscription(s) active to type " + type + "\n") + .getBytes(UTF_8)); + for (SubscriptionItem item : subscriptionItems) { out.write("\n".getBytes(UTF_8)); - Item item = (Item) indexableObject.getIndexedObject(); - String entityType = itemService.getEntityTypeLabel(item); - Optional.ofNullable(entityType2Disseminator.get(entityType)) - .orElseGet(() -> entityType2Disseminator.get("Item")) - .disseminate(context, item, out); + out.write("List of new content for the\n".getBytes(UTF_8)); + out.write((type + " " + item.getName() + " - " + item.getUrl() + "\n") + .getBytes(UTF_8)); + + for (Entry entry : item.getItemUrlsByItemName().entrySet()) { + out.write("\n".getBytes(UTF_8)); + out.write((entry.getKey() + " - " + entry.getValue()).getBytes(UTF_8)); + } } return out.toString(); } else { @@ -96,8 +92,4 @@ private String generateBodyMail(Context context, List indexable return EMPTY; } - public void setEntityType2Disseminator(Map entityType2Disseminator) { - this.entityType2Disseminator = entityType2Disseminator; - } - } diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/StatisticsGenerator.java b/dspace-api/src/main/java/org/dspace/subscriptions/StatisticsGenerator.java index c1f9be368e27..842ff9aa0e8f 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/StatisticsGenerator.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/StatisticsGenerator.java @@ -27,7 +27,6 @@ import org.dspace.core.Email; import org.dspace.eperson.EPerson; import org.dspace.services.ConfigurationService; -import org.dspace.subscriptions.service.SubscriptionGenerator; import org.springframework.beans.factory.annotation.Autowired; @@ -38,19 +37,16 @@ * * @author Alba Aliu */ -public class StatisticsGenerator implements SubscriptionGenerator { +public class StatisticsGenerator { private static final Logger log = LogManager.getLogger(StatisticsGenerator.class); @Autowired private ConfigurationService configurationService; - @Override - public void notifyForSubscriptions(Context c, EPerson ePerson, List crisMetricsList, - List crisMetricsList1, List crisMetricsList2) { - // find statistics for all the subscribed objects + public void notifyForSubscriptions(Context c, EPerson ePerson, List crisMetricsList) { try { // send the notification to the user - if (Objects.nonNull(ePerson) && crisMetricsList.size() > 0) { + if (Objects.nonNull(ePerson) && !crisMetricsList.isEmpty()) { Email email = new Email(); String name = configurationService.getProperty("dspace.name"); File attachment = generateExcel(crisMetricsList, c); diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java index b429ecbd46e7..cc5cac24eabb 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java @@ -48,16 +48,23 @@ public void setup() throws ParseException { public void internalRun() throws Exception { assignCurrentUserInContext(); assignSpecialGroupsInContext(); + String typeOption = commandLine.getOptionValue("t"); String frequencyOption = commandLine.getOptionValue("f"); - if (StringUtils.isBlank(frequencyOption)) { - throw new IllegalArgumentException("Option --frequency (-f) must be set"); + if (StringUtils.isBlank(frequencyOption) || StringUtils.isBlank(typeOption)) { + throw new IllegalArgumentException("Options --frequency (-f) and --type (-t) must be set"); } if (!FrequencyType.isSupportedFrequencyType(frequencyOption)) { throw new IllegalArgumentException( "Option f must be one of following values D(Day), W(Week) or M(Month)"); } - subscriptionEmailNotificationService.perform(getContext(), handler, "content", frequencyOption); + + if (!StringUtils.equalsAny(typeOption, "content", "statistics")) { + throw new IllegalArgumentException( + "Option t (type) must be one of \"content\" or \"statistics\""); + } + + subscriptionEmailNotificationService.perform(getContext(), handler, typeOption, frequencyOption); } private void assignCurrentUserInContext() throws SQLException { diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java index 52685b563d9b..d4f76a555936 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java @@ -42,6 +42,9 @@ public boolean isAllowedToExecute(Context context) { public Options getOptions() { if (Objects.isNull(options)) { Options options = new Options(); + options.addOption("t", "type", true, + "Subscription type, Valid values are \"content\" or \"statistics\""); + options.getOption("t").setRequired(true); options.addOption("f", "frequency", true, "Subscription frequency. Valid values include: D (Day), W (Week) and M (Month)"); options.getOption("f").setRequired(true); diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationService.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationService.java index 95272235095a..7a7c36491278 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationService.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationService.java @@ -7,7 +7,7 @@ */ package org.dspace.subscriptions; -import java.util.Set; +import java.util.List; import org.dspace.core.Context; import org.dspace.scripts.handler.DSpaceRunnableHandler; @@ -32,6 +32,6 @@ public interface SubscriptionEmailNotificationService { /** * returns a set of supported SubscriptionTypes */ - public Set getSupportedSubscriptionTypes(); + public List getSupportedSubscriptionTypes(); } diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java index 2a30b89af3f5..78024bfdd640 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java @@ -7,10 +7,12 @@ */ package org.dspace.subscriptions; +import static org.dspace.content.Item.ANY; import static org.dspace.core.Constants.COLLECTION; import static org.dspace.core.Constants.COMMUNITY; import static org.dspace.core.Constants.ITEM; import static org.dspace.core.Constants.READ; +import static org.dspace.subscriptions.SubscriptionItem.fromItem; import java.sql.SQLException; import java.util.ArrayList; @@ -18,16 +20,18 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.app.metrics.CrisMetrics; +import org.dspace.app.metrics.service.CrisMetricsService; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; import org.dspace.core.Context; import org.dspace.discovery.IndexableObject; import org.dspace.eperson.EPerson; @@ -36,7 +40,6 @@ import org.dspace.scripts.DSpaceRunnable; import org.dspace.scripts.handler.DSpaceRunnableHandler; import org.dspace.subscriptions.service.DSpaceObjectUpdates; -import org.dspace.subscriptions.service.SubscriptionGenerator; import org.springframework.beans.factory.annotation.Autowired; /** @@ -48,87 +51,114 @@ public class SubscriptionEmailNotificationServiceImpl implements SubscriptionEma private static final Logger log = LogManager.getLogger(SubscriptionEmailNotificationServiceImpl.class); - private Map contentUpdates = new HashMap<>(); - @SuppressWarnings("rawtypes") - private Map subscriptionType2generators = new HashMap<>(); + private final Map contentUpdates; + private final ContentGenerator contentGenerator; + private final StatisticsGenerator statisticsGenerator; + private final List supportedSubscriptionTypes; @Autowired private AuthorizeService authorizeService; @Autowired private SubscribeService subscribeService; + @Autowired + private CrisMetricsService crisMetricsService; - @SuppressWarnings("rawtypes") public SubscriptionEmailNotificationServiceImpl(Map contentUpdates, - Map subscriptionType2generators) { + ContentGenerator contentGenerator, + StatisticsGenerator statisticsGenerator, + List supportedSubscriptionTypes) { this.contentUpdates = contentUpdates; - this.subscriptionType2generators = subscriptionType2generators; + this.contentGenerator = contentGenerator; + this.statisticsGenerator = statisticsGenerator; + this.supportedSubscriptionTypes = supportedSubscriptionTypes; } - @SuppressWarnings({ "rawtypes", "unchecked" }) public void perform(Context context, DSpaceRunnableHandler handler, String subscriptionType, String frequency) { - List communityItems = new ArrayList<>(); - List collectionsItems = new ArrayList<>(); - List items = new ArrayList<>(); + // Verify if subscriptionType is "content" or "subscription" + if (supportedSubscriptionTypes.get(0).equals(subscriptionType)) { + performForContent(context, handler, subscriptionType, frequency); + } else if (supportedSubscriptionTypes.get(1).equals(subscriptionType)) { + performForStatistics(context, subscriptionType, frequency); + } else { + throw new IllegalArgumentException( + "Currently this SubscriptionType:" + subscriptionType + " is not supported!"); + } + } + + @SuppressWarnings({ "rawtypes" }) + private void performForContent(Context context, DSpaceRunnableHandler handler, + String subscriptionType, String frequency) { try { List subscriptions = - findAllSubscriptionsBySubscriptionTypeAndFrequency(context, subscriptionType, frequency); - // Here is verified if SubscriptionType is "content" Or "statistics" as them are configured - if (subscriptionType2generators.keySet().contains(subscriptionType)) { - // the list of the person who has subscribed - int iterator = 0; - for (Subscription subscription : subscriptions) { - DSpaceObject dSpaceObject = subscription.getDSpaceObject(); - EPerson ePerson = subscription.getEPerson(); - - if (!authorizeService.authorizeActionBoolean(context, ePerson, dSpaceObject, READ, true)) { - iterator++; - continue; - } + findAllSubscriptionsBySubscriptionTypeAndFrequency(context, subscriptionType, frequency); + List communityItems = new ArrayList<>(); + List collectionsItems = new ArrayList<>(); + Map> entityItemsByEntityType = new HashMap<>(); + int iterator = 0; - if (dSpaceObject.getType() == COMMUNITY) { - List indexableCommunityItems = contentUpdates - .get(Community.class.getSimpleName().toLowerCase()) - .findUpdates(context, dSpaceObject, frequency); - communityItems.addAll(getItems(context, ePerson, indexableCommunityItems)); - } else if (dSpaceObject.getType() == COLLECTION) { - List indexableCollectionItems = contentUpdates - .get(Collection.class.getSimpleName().toLowerCase()) - .findUpdates(context, dSpaceObject, frequency); - collectionsItems.addAll(getItems(context, ePerson, indexableCollectionItems)); - } else if (dSpaceObject.getType() == ITEM) { - List indexableCollectionItems = contentUpdates - .get(Item.class.getSimpleName().toLowerCase()) - .findUpdates(context, dSpaceObject, frequency); - items.addAll(getItems(context, ePerson, indexableCollectionItems)); - } else { + for (Subscription subscription : subscriptions) { + DSpaceObject dSpaceObject = subscription.getDSpaceObject(); + EPerson ePerson = subscription.getEPerson(); + + if (!authorizeService.authorizeActionBoolean(context, ePerson, dSpaceObject, READ, true)) { + iterator++; + continue; + } + + switch (dSpaceObject.getType()) { + case COMMUNITY: + List indexableCommunityItems = getItems( + context, ePerson, + contentUpdates.get(Community.class.getSimpleName().toLowerCase()) + .findUpdates(context, dSpaceObject, frequency) + ); + communityItems.add(fromItem(dSpaceObject, indexableCommunityItems)); + break; + case COLLECTION: + List indexableCollectionItems = getItems( + context, ePerson, + contentUpdates.get(Collection.class.getSimpleName().toLowerCase()) + .findUpdates(context, dSpaceObject, frequency) + ); + collectionsItems.add(fromItem(dSpaceObject, indexableCollectionItems)); + break; + case ITEM: + List indexableEntityItems = getItems( + context, ePerson, contentUpdates.get(Item.class.getSimpleName().toLowerCase()) + .findUpdates(context, dSpaceObject, frequency) + ); + String dspaceType = ContentServiceFactory + .getInstance().getDSpaceObjectService(dSpaceObject) + .getMetadataFirstValue(dSpaceObject, "dspace", "entity", "type", ANY); + + entityItemsByEntityType.computeIfAbsent(dspaceType, k -> new ArrayList<>()) + .add(fromItem(dSpaceObject, indexableEntityItems)); + break; + default: log.warn("found an invalid DSpace Object type ({}) among subscriptions to send", dSpaceObject.getType()); continue; - } + } - if (iterator < subscriptions.size() - 1) { - // as the subscriptions are ordered by eperson id, so we send them by ePerson - if (ePerson.equals(subscriptions.get(iterator + 1).getEPerson())) { - iterator++; - continue; - } else { - subscriptionType2generators.get(subscriptionType) - .notifyForSubscriptions(context, ePerson, communityItems, - collectionsItems, items); - communityItems.clear(); - collectionsItems.clear(); - } + if (iterator < subscriptions.size() - 1) { + // as the subscriptions are ordered by eperson id, so we send them by ePerson + if (ePerson.equals(subscriptions.get(iterator + 1).getEPerson())) { + iterator++; + continue; } else { - //in the end of the iteration - subscriptionType2generators.get(subscriptionType) - .notifyForSubscriptions(context, ePerson, communityItems, - collectionsItems, items); + contentGenerator.notifyForSubscriptions( + ePerson, communityItems, collectionsItems, entityItemsByEntityType + ); + communityItems.clear(); + collectionsItems.clear(); } - iterator++; + } else { + //in the end of the iteration + contentGenerator.notifyForSubscriptions( + ePerson, communityItems, collectionsItems, entityItemsByEntityType + ); } - } else { - throw new IllegalArgumentException("Currently this SubscriptionType:" + subscriptionType + - " is not supported!"); + iterator++; } } catch (Exception e) { log.error(e.getMessage(), e); @@ -137,14 +167,43 @@ public void perform(Context context, DSpaceRunnableHandler handler, String subsc } } + private void performForStatistics(Context context, String subscriptionType, String frequency) { + List subscriptions = + findAllSubscriptionsBySubscriptionTypeAndFrequency(context, subscriptionType, frequency); + List crisMetricsList = new ArrayList<>(); + int iterator = 0; + + for (Subscription subscription : subscriptions) { + EPerson ePerson = subscription.getEPerson(); + DSpaceObject dSpaceObject = subscription.getDSpaceObject(); + try { + crisMetricsList.addAll(crisMetricsService.findAllByDSO(context, dSpaceObject)); + } catch (Exception e) { + log.error(e.getMessage()); + } + if (iterator < subscriptions.size() - 1) { + if (ePerson.equals(subscriptions.get(iterator + 1).getEPerson())) { + iterator++; + continue; + } else { + statisticsGenerator.notifyForSubscriptions(context, ePerson, crisMetricsList); + } + } else { + //in the end of the iteration + statisticsGenerator.notifyForSubscriptions(context, ePerson, crisMetricsList); + } + iterator++; + } + } + @SuppressWarnings("rawtypes") private List getItems(Context context, EPerson ePerson, List indexableItems) throws SQLException { List items = new ArrayList(); - for (IndexableObject indexableitem : indexableItems) { - Item item = (Item) indexableitem.getIndexedObject(); + for (IndexableObject indexableItem : indexableItems) { + Item item = (Item) indexableItem.getIndexedObject(); if (authorizeService.authorizeActionBoolean(context, ePerson, item, READ, true)) { - items.add(indexableitem); + items.add(indexableItem); } } return items; @@ -157,25 +216,25 @@ private List getItems(Context context, EPerson ePerson, List findAllSubscriptionsBySubscriptionTypeAndFrequency(Context context, String subscriptionType, String frequency) { try { return subscribeService.findAllSubscriptionsBySubscriptionTypeAndFrequency(context, subscriptionType, - frequency) + frequency) .stream() .sorted(Comparator.comparing(s -> s.getEPerson().getID())) .collect(Collectors.toList()); } catch (SQLException e) { log.error(e.getMessage(), e); } - return new ArrayList(); + return new ArrayList<>(); } @Override - public Set getSupportedSubscriptionTypes() { - return subscriptionType2generators.keySet(); + public List getSupportedSubscriptionTypes() { + return supportedSubscriptionTypes; } } diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionItem.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionItem.java new file mode 100644 index 000000000000..3254635b015f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionItem.java @@ -0,0 +1,74 @@ +/** + * 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.subscriptions; + +import static java.util.stream.Collectors.toMap; + +import java.util.List; +import java.util.Map; + +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.discovery.IndexableObject; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; + +public class SubscriptionItem { + + private static final ConfigurationService configurationService = DSpaceServicesFactory.getInstance() + .getConfigurationService(); + + private String name; + private String url; + private Map itemUrlsByItemName; + + public SubscriptionItem(String name, String url, Map itemUrlsByItemName) { + this.name = name; + this.url = url; + this.itemUrlsByItemName = itemUrlsByItemName; + } + + @SuppressWarnings({ "rawtypes" }) + static SubscriptionItem fromItem(DSpaceObject dSpaceObject, List relatedItems) { + return new SubscriptionItem( + dSpaceObject.getName(), + buildUrlForItem(dSpaceObject.getHandle()), + relatedItems.stream() + .map(obj -> (Item) obj.getIndexedObject()) + .collect(toMap(Item::getName, item -> buildUrlForItem(item.getHandle()))) + ); + } + + private static String buildUrlForItem(String handle) { + return configurationService.getProperty("dspace.ui.url") + "/handle/" + handle; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Map getItemUrlsByItemName() { + return itemUrlsByItemName; + } + + public void setItemUrlsByItemName(Map itemUrlsByItemName) { + this.itemUrlsByItemName = itemUrlsByItemName; + } +} diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/service/SubscriptionGenerator.java b/dspace-api/src/main/java/org/dspace/subscriptions/service/SubscriptionGenerator.java deleted file mode 100644 index 994ada75b61b..000000000000 --- a/dspace-api/src/main/java/org/dspace/subscriptions/service/SubscriptionGenerator.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * 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.subscriptions.service; - -import java.util.List; - -import org.dspace.core.Context; -import org.dspace.eperson.EPerson; - -/** - * Interface Class which will be used to send email notifications to ePerson - * containing information for all list of objects. - * - * @author Alba Aliu - */ -public interface SubscriptionGenerator { - - public void notifyForSubscriptions(Context c, EPerson ePerson, List comm, List coll, List items); - -} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/util/FunctionalUtils.java b/dspace-api/src/main/java/org/dspace/util/FunctionalUtils.java index 422c2405a875..66921d041799 100644 --- a/dspace-api/src/main/java/org/dspace/util/FunctionalUtils.java +++ b/dspace-api/src/main/java/org/dspace/util/FunctionalUtils.java @@ -8,6 +8,8 @@ package org.dspace.util; import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -58,4 +60,30 @@ public static T getCheckDefaultOrBuild(Predicate defaultValueChecker, T d return builder.get(); } + public static Consumer throwingConsumerWrapper( + ThrowingConsumer throwingConsumer) { + return i -> { + try { + throwingConsumer.accept(i); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + + public static Function throwingMapperWrapper( + ThrowingMapper throwingConsumer, + R defaultValue + ) { + return i -> { + R value = defaultValue; + try { + value = throwingConsumer.accept(i); + } catch (Exception e) { + throw new RuntimeException(e); + } + return value; + }; + } + } diff --git a/dspace-api/src/main/java/org/dspace/util/ThrowingConsumer.java b/dspace-api/src/main/java/org/dspace/util/ThrowingConsumer.java new file mode 100644 index 000000000000..a04fea3ef41f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/ThrowingConsumer.java @@ -0,0 +1,12 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.util; + +public interface ThrowingConsumer { + void accept(T t) throws E; +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/util/ThrowingMapper.java b/dspace-api/src/main/java/org/dspace/util/ThrowingMapper.java new file mode 100644 index 000000000000..ac4767a85706 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/ThrowingMapper.java @@ -0,0 +1,12 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.util; + +public interface ThrowingMapper { + R accept(T t) throws E; +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java b/dspace-api/src/main/java/org/dspace/validation/MetadataValidator.java index 3d50ddf66cd8..cd9c2dd3c2fb 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 000000000000..6b4994b6644e --- /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 000000000000..69e2d34b4b4e --- /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/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index e5b943c5c20f..a20d47c30e83 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -157,6 +157,12 @@ + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-doi-json.template b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-doi-json.template new file mode 100644 index 000000000000..841a6a03fbd3 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-doi-json.template @@ -0,0 +1,4 @@ +{ + "primary-doi": "@virtual.primary-doi.dc-identifier-doi@", + "alternative-doi": "@virtual.alternative-doi.dc-identifier-doi@", +} \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-with-vocabulary-xml.template b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-with-vocabulary-xml.template new file mode 100644 index 000000000000..a8ca0b6f5b82 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-with-vocabulary-xml.template @@ -0,0 +1,7 @@ + + @dspace.entity.type@ + @dc.title@ + @virtual.vocabulary_i18n.dc-type.publication-coar-types@ + @virtual.vocabulary_i18n.dc-language-iso.common_iso_languages@ + @virtual.vocabulary_i18n.organization-address-addressCountry.common_iso_countries@ + \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-xml.template b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-xml.template new file mode 100644 index 000000000000..099f285a32e2 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/crosswalks/template/virtual-field-vocabulary_18n-publication-xml.template @@ -0,0 +1,6 @@ + + @dspace.entity.type@ + @dc.title@ + @virtual.vocabulary_i18n.dc-type@ + @virtual.vocabulary_i18n.dc-language-iso@ + \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 08eb98710584..fb8b2863506e 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -157,11 +157,11 @@ wos.apiKey = submission.lookup.epo.consumerKey= submission.lookup.epo.consumerSecretKey= -event.dispatcher.default.consumers = versioning, discovery, eperson, dedup, crisconsumer, audit, nbeventsdelete, referenceresolver, orcidwebhook, iiif, itemenhancer, customurl, reciprocal +event.dispatcher.default.consumers = versioning, discovery, eperson, dedup, crisconsumer, audit, nbeventsdelete, referenceresolver, orcidwebhook, iiif, itemenhancer, customurl, reciprocal, filetypemetadataenhancer # setup a dispatcher also with the cris consumer event.dispatcher.cris-default.class = org.dspace.event.BasicDispatcher -event.dispatcher.cris-default.consumers = versioning, discovery, eperson, dedup, crisconsumer, orcidqueue, audit, referenceresolver, orcidwebhook, itemenhancer, customurl +event.dispatcher.cris-default.consumers = versioning, discovery, eperson, dedup, crisconsumer, orcidqueue, audit, referenceresolver, orcidwebhook, itemenhancer, customurl, filetypemetadataenhancer # Enable a test authority control on dc.language.iso field choices.plugin.dc.language.iso = common_iso_languages @@ -213,4 +213,4 @@ logging.server.include-stacktrace-for-httpcode = 400, 401, 404, 403, 422 # Configuration required for thorough testing of browse links webui.browse.link.1 = author:dc.contributor.* -webui.browse.link.2 = subject:dc.subject.* \ No newline at end of file +webui.browse.link.2 = subject:dc.subject.* diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml index 0697423578bc..fdd7886c477b 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml @@ -155,5 +155,10 @@ + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml index 87cc17de18a9..3b7f8829bfcb 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/bulkedit/BulkImportIT.java b/dspace-api/src/test/java/org/dspace/app/bulkedit/BulkImportIT.java index e03c414a034c..e1d04f314c68 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkedit/BulkImportIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkedit/BulkImportIT.java @@ -133,7 +133,8 @@ public void beforeTests() throws SQLException, AuthorizeException { public void testEmptyImport() throws InstantiationException, IllegalAccessException { String fileLocation = getXlsFilePath("empty.xls"); - String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -149,7 +150,8 @@ public void testEmptyImport() throws InstantiationException, IllegalAccessExcept public void testEmptyHeadersImport() throws InstantiationException, IllegalAccessException { String fileLocation = getXlsFilePath("empty-headers.xls"); - String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -165,7 +167,8 @@ public void testEmptyHeadersImport() throws InstantiationException, IllegalAcces public void testOneHeaderEmptyImport() throws InstantiationException, IllegalAccessException { String fileLocation = getXlsFilePath("one-header-empty.xls"); - String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -182,7 +185,8 @@ public void testOneHeaderEmptyImport() throws InstantiationException, IllegalAcc public void testWithoutHeadersImport() throws InstantiationException, IllegalAccessException { String fileLocation = getXlsFilePath("without-headers.xls"); - String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -198,7 +202,8 @@ public void testWithoutHeadersImport() throws InstantiationException, IllegalAcc public void testInvalidHeadersImport() throws InstantiationException, IllegalAccessException { String fileLocation = getXlsFilePath("invalid-headers.xls"); - String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -216,7 +221,8 @@ public void testInvalidHeadersImport() throws InstantiationException, IllegalAcc public void testInvalidSheetNameImport() throws InstantiationException, IllegalAccessException { String fileLocation = getXlsFilePath("invalid-sheet-name.xlsx"); - String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -241,7 +247,8 @@ public void testMetadataGroupRowWithManyValuesImport() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("metadata-group-row-with-many-values.xlsx"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -275,7 +282,8 @@ public void testHeadersDuplicatedImport() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("headers-duplicated.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -300,7 +308,8 @@ public void testCreatePatent() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("create-patent.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -351,7 +360,8 @@ public void testUpdatePatent() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-patent.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -388,7 +398,8 @@ public void testCreatePublicationWithAuthority() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("create-publication-with-authority.xlsx"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -439,7 +450,8 @@ public void testManyPublicationImport() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("many-publications.xls"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -515,7 +527,8 @@ public void testManyPublicationImportWithErrorAndNotAbortOnError() throws Except context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("many-publications.xls"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -575,7 +588,8 @@ public void testManyPublicationImportWithErrorAndAbortOnError() throws Exception context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("many-publications.xls"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, "-e" }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail(), "-er"}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -630,7 +644,8 @@ public void testCreatePublicationWithOneInvalidAuthorityAndNoAbortOnError() thro context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("create-publication-with-one-invalid-authority.xlsx"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -675,7 +690,8 @@ public void testCreatePublicationWithOneInvalidAuthorityAndAbortOnError() throws context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("create-publication-with-one-invalid-authority.xlsx"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, "-e" }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail(), "-er"}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -714,7 +730,8 @@ public void testCreatePublicationWithWillBeGeneratedAuthority() throws Exception String publicationCollectionId = publications.getID().toString(); String fileLocation = getXlsFilePath("create-publication-with-will-be-generated-authority.xls"); - String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, "-e" }; + String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, + "-e", eperson.getEmail(), "-er"}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, admin); @@ -760,7 +777,8 @@ public void testCreatePublicationWithWillBeGeneratedAuthorityAndNoRelatedItemFou String publicationCollectionId = publications.getID().toString(); String fileLocation = getXlsFilePath("create-publication-with-will-be-generated-authority.xls"); - String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, "-e" }; + String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, + "-e", eperson.getEmail(), "-er"}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, admin); @@ -810,7 +828,8 @@ public void testCreatePublicationWithWillBeReferencedAuthority() throws Exceptio String publicationCollectionId = publications.getID().toString(); String fileLocation = getXlsFilePath("create-publication-with-will-be-referenced-authority.xls"); - String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, "-e" }; + String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, + "-e", eperson.getEmail(), "-er"}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, admin); @@ -856,7 +875,8 @@ public void testCreatePublicationWithWillBeReferencedAuthorityAndNoRelatedItemFo String publicationCollectionId = publications.getID().toString(); String fileLocation = getXlsFilePath("create-publication-with-will-be-referenced-authority.xls"); - String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, "-e" }; + String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, + "-e", eperson.getEmail(), "-er"}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, admin); @@ -898,7 +918,8 @@ public void testCreatePublicationInWorkspace() throws Exception { String publicationCollectionId = publications.getID().toString(); String fileLocation = getXlsFilePath("create-workspace-publication.xls"); - String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, "-e" }; + String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, + "-e", eperson.getEmail(), "-er"}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -942,7 +963,8 @@ public void testCreateArchivedPublication() throws Exception { String publicationCollectionId = publications.getID().toString(); String fileLocation = getXlsFilePath("create-archived-publication.xls"); - String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, "-e" }; + String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, + "-e", eperson.getEmail(), "-er"}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, admin); @@ -995,7 +1017,8 @@ public void testUpdateWorkflowPatentWithValidWorkspaceItem() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-workflow-patent.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1045,7 +1068,8 @@ public void testUpdateWorkflowPatentWithInvalidWorkspaceItem() throws Exception context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-workflow-patent.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1097,7 +1121,8 @@ public void testUpdateWorkflowPatentWithoutWorkspaceItem() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-workflow-patent.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1146,7 +1171,8 @@ public void testUpdateArchivePatentWithWorkspaceItem() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-archive-patent.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1195,7 +1221,8 @@ public void testUpdateArchivePatentWithWorkflowItem() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-archive-patent.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1244,7 +1271,8 @@ public void testUpdateArchivePatentWithAlreadyArchivedItem() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-archive-patent.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1295,7 +1323,8 @@ public void testAutomaticReferenceResolution() throws Exception { String publicationCollectionId = publications.getID().toString(); String fileLocation = getXlsFilePath("create-publication-with-will-be-referenced-authority.xls"); - String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, "-e" }; + String[] args = new String[] { "bulk-import", "-c", publicationCollectionId, "-f", fileLocation, + "-e" , eperson.getEmail(), "-er"}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, admin); @@ -1316,7 +1345,8 @@ public void testAutomaticReferenceResolution() throws Exception { String personsCollectionId = persons.getID().toString(); fileLocation = getXlsFilePath("create-person.xls"); - args = new String[] { "bulk-import", "-c", personsCollectionId, "-f", fileLocation, "-e" }; + args = new String[] { "bulk-import", "-c", personsCollectionId, "-f", fileLocation, + "-e" , eperson.getEmail(), "-er"}; handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, admin); @@ -1350,7 +1380,8 @@ public void testUploadSingleBitstream() throws Exception { String fileLocation = getXlsFilePath("add-bitstream-to-item.xls"); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1404,7 +1435,8 @@ public void testUploadMultipleBitstreams() throws Exception { String fileLocation = getXlsFilePath("add-multiple-bitstreams-to-items.xls"); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1458,7 +1490,8 @@ public void testUploadMultipleBitstreamWithPathTraversal() throws Exception { String fileLocation = getXlsFilePath("add-multiple-bitstreams-with-path-traversal-to-items.xls"); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1509,7 +1542,8 @@ public void testUploadSingleBitstreamUpdate() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("add-bitstream-to-item-update.xls"); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1555,7 +1589,8 @@ public void testUploadMultipleBitstreamsUpdateMultiple() throws Exception { String fileName = "add-bitstream-to-multiple-items-update.xls"; String fileLocation = getXlsFilePath(fileName); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1602,7 +1637,8 @@ public void testUploadSingleBitstreamUpdateWithExistingBundle() throws Exception String fileName = "add-bitstream-to-item-bundle.xls"; String fileLocation = getXlsFilePath(fileName); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1639,7 +1675,8 @@ public void testCreatePublicationInWorkspaceItemsAndItemHasLicense() throws Exce context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("items-with-bitstreams.xlsx"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1702,7 +1739,8 @@ public void testCreatePublicationInWorkspaceItemsWithBitstreams() throws Excepti String fileName = "items-with-bitstreams.xlsx"; String fileLocation = getXlsFilePath(fileName); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1786,7 +1824,8 @@ public void testUpdateAndDeleteBitstreamsOfItems() throws Exception { String fileName = "update-delete-bitstreams-of-items.xls"; String fileLocation = getXlsFilePath(fileName); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1855,7 +1894,8 @@ public void testBitstreamUpdateAndDeleteWithWrongPosition() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-delete-bitstreams-of-items.xls"); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1918,7 +1958,8 @@ public void testBitstreamUpdateWithAdditionalConditionSetToFalse() throws Except context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-bitstream-policies-without-additional-ac.xls"); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -1981,7 +2022,8 @@ public void testUpdateItems() throws Exception { // start test String fileLocation = getXlsFilePath("update-items.xls"); - String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publication.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); assertThat(handler.getErrorMessages(), empty()); @@ -2031,7 +2073,8 @@ public void testCreatePublicationWithSecurityLevel() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("create-publication-with-security-level.xlsx"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -2081,7 +2124,8 @@ public void testUpdatePublicationWithSecurityLevel() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("update-publication-with-security-level.xlsx"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -2121,7 +2165,8 @@ public void testWorkbookWithoutActionColumn() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("without-action-column.xls"); - String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -2182,7 +2227,8 @@ public void testWorkbookWithDiscoverableColumn() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("publications_with_discoverable_column.xlsx"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -2237,7 +2283,8 @@ public void testWorkbookWithInvalidOptionalColumnPosition() throws Exception { context.restoreAuthSystemState(); String fileLocation = getXlsFilePath("invalid-optional-column-position.xlsx"); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); @@ -2246,6 +2293,28 @@ public void testWorkbookWithInvalidOptionalColumnPosition() throws Exception { + "must be placed before the metadata fields")); } + @Test + public void testCreatePatentByNotCollectionAdmin() throws Exception { + context.turnOffAuthorisationSystem(); + Collection patents = createCollection(context, community) + .withSubmissionDefinition("patent") + .withAdminGroup(admin) + .build(); + context.commit(); + context.restoreAuthSystemState(); + + String fileLocation = getXlsFilePath("create-patent.xls"); + String[] args = new String[] { "bulk-import", "-c", patents.getID().toString(), "-f", fileLocation, + "-e", eperson.getEmail()}; + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); + + List errorMessages = handler.getErrorMessages(); + assertThat("Expected 1 error message", errorMessages, hasSize(1)); + assertThat(errorMessages.get(0), containsString("The user is not an admin of the given collection")); + } + private WorkspaceItem findWorkspaceItem(Item item) throws SQLException { return workspaceItemService.findByItem(context, item); } diff --git a/dspace-api/src/test/java/org/dspace/app/bulkimport/service/BulkImportWorkbookBuilderIT.java b/dspace-api/src/test/java/org/dspace/app/bulkimport/service/BulkImportWorkbookBuilderIT.java index eed2826ea67a..a76642790704 100644 --- a/dspace-api/src/test/java/org/dspace/app/bulkimport/service/BulkImportWorkbookBuilderIT.java +++ b/dspace-api/src/test/java/org/dspace/app/bulkimport/service/BulkImportWorkbookBuilderIT.java @@ -176,7 +176,8 @@ public void testWorkbookBuildingFromItemDtos() throws Exception { String tempLocation = storeInTempLocation(workbook); - String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", tempLocation }; + String[] args = new String[] { "bulk-import", "-c", publications.getID().toString(), "-f", tempLocation, + "-e", admin.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl, eperson); diff --git a/dspace-api/src/test/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumerIT.java b/dspace-api/src/test/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumerIT.java new file mode 100644 index 000000000000..bfa29ab330d4 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/filetype/consumer/FileTypeMetadataEnhancerConsumerIT.java @@ -0,0 +1,432 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.filetype.consumer; + +import static org.dspace.app.matcher.MetadataValueMatcher.with; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.sql.SQLException; +import java.text.ParseException; +import java.util.function.Predicate; + +import org.apache.commons.codec.binary.StringUtils; +import org.apache.tools.ant.filters.StringInputStream; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.ResourcePolicyBuilder; +import org.dspace.content.Bitstream; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.MetadataFieldName; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class FileTypeMetadataEnhancerConsumerIT extends AbstractIntegrationTestWithDatabase { + + private Collection collection; + + private final BitstreamService bitstreamService = ContentServiceFactory.getInstance() + .getBitstreamService(); + private final ItemService itemService = ContentServiceFactory.getInstance() + .getItemService(); + + @Before + public void setup() { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + + collection = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + + context.restoreAuthSystemState(); + } + + @Test + public void testWithoutBitstreams() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection).build(); + context.restoreAuthSystemState(); + context.commit(); + + item = context.reloadEntity(item); + + assertThat(item.getMetadata(), not(hasItem(with("dc.type", null)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file_type", null)))); + + context.turnOffAuthorisationSystem(); + this.itemService.update(context, item); + context.restoreAuthSystemState(); + + item = context.reloadEntity(item); + + assertThat(item.getMetadata(), not(hasItem(with("dc.type", null)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", null)))); + } + + @Test + public void testWithoutEntityType() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection).build(); + Bitstream bitstream = BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), not(hasItem(with("dc.type", null)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", null)))); + } + + @Test + public void testWithEntityTypeDelete() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + context.turnOffAuthorisationSystem(); + Item item = ItemBuilder.createItem(context, collection).build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .build(); + + ResourcePolicyBuilder + .createResourcePolicy(context) + .withDspaceObject(bitstream) + .withAction(Constants.READ) + .withUser(admin) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + context.turnOffAuthorisationSystem(); + + this.bitstreamService.delete(context, bitstream); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), not(hasItem(with("dc.type", null)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", null)))); + } + + @Test + public void testWithEntityType() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + final String type = "Publication"; + context.turnOffAuthorisationSystem(); + final Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type))); + } + + @Test + public void testWithTypeEdited() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + String type = "Publication"; + context.turnOffAuthorisationSystem(); + Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + + context.turnOffAuthorisationSystem(); + + type = "Thesis"; + this.bitstreamService.setMetadataSingleValue(context, bitstream, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata, null, type); + this.bitstreamService.update(context, bitstream); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + } + + @Test + public void testWithTypeDeleted() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + final String type = "Publication"; + context.turnOffAuthorisationSystem(); + Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + final MetadataValue entityType = bitstream.getMetadata() + .stream() + .filter(metadataFilter(FileTypeMetadataEnhancerConsumer.entityTypeMetadata)) + .findFirst() + .orElseThrow(); + bitstream.getMetadata().remove(entityType); + context.turnOffAuthorisationSystem(); + + this.bitstreamService.update(context, bitstream); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), not(hasItem(with("dc.type", Mockito.any())))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", Mockito.any())))); + } + + @Test + public void testWithMultipleEntityType() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + final String type = "Publication"; + final String type1 = "Thesis"; + context.turnOffAuthorisationSystem(); + final Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + final Bitstream bitstream1 = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type1) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type, null, 0, -1))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 1, -1))); + } + + @Test + public void testWithMultipleEntityTypeEdited() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + String type = "Publication"; + String type1 = "Thesis"; + context.turnOffAuthorisationSystem(); + Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + Bitstream bitstream1 = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type1) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + bitstream1 = context.reloadEntity(bitstream1); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type, null, 0, -1))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 1, -1))); + + context.turnOffAuthorisationSystem(); + + type = "Journal"; + this.bitstreamService.setMetadataSingleValue( + context, + bitstream, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata, + null, + type + ); + this.bitstreamService.update(context, bitstream); + + type1 = "Journal Article"; + this.bitstreamService.setMetadataSingleValue( + context, + bitstream1, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata, + null, + type1 + ); + this.bitstreamService.update(context, bitstream1); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + bitstream1 = context.reloadEntity(bitstream1); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type, null, 0, -1))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 1, -1))); + } + + @Test + public void testWithMultipleEntityTypeDelete() + throws FileNotFoundException, SQLException, AuthorizeException, IOException, ParseException { + final String type = "Publication"; + final String type1 = "Thesis"; + context.turnOffAuthorisationSystem(); + Item item = + ItemBuilder + .createItem(context, collection) + .build(); + Bitstream bitstream = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type) + .build(); + Bitstream bitstream1 = + BitstreamBuilder + .createBitstream(context, item, new StringInputStream("test")) + .withType(type1) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + bitstream1 = context.reloadEntity(bitstream1); + + assertThat(bitstream.getMetadata(), hasItem(with("dc.type", type))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type, null, 0, -1))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 1, -1))); + + context.turnOffAuthorisationSystem(); + + this.bitstreamService.clearMetadata( + context, + bitstream, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata.schema, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata.element, + FileTypeMetadataEnhancerConsumer.entityTypeMetadata.qualifier, + null + ); + this.bitstreamService.update(context, bitstream); + + context.restoreAuthSystemState(); + context.commit(); + + bitstream = context.reloadEntity(bitstream); + bitstream1 = context.reloadEntity(bitstream1); + item = context.reloadEntity(item); + + assertThat(bitstream.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(bitstream.getMetadata(), not(hasItem(with("dspace.file.type", type)))); + assertThat(bitstream1.getMetadata(), hasItem(with("dc.type", type1))); + assertThat(bitstream1.getMetadata(), not(hasItem(with("dspace.file.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type)))); + assertThat(item.getMetadata(), not(hasItem(with("dc.type", type1)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", type, null, 0, -1)))); + assertThat(item.getMetadata(), not(hasItem(with("dspace.file.type", type1, null, 1, -1)))); + assertThat(item.getMetadata(), hasItem(with("dspace.file.type", type1, null, 0, -1))); + } + + private Predicate metadataFilter(MetadataFieldName metadataField) { + return metadata -> + StringUtils.equals(metadataField.schema, metadata.getSchema()) && + StringUtils.equals(metadataField.element, metadata.getElement()) && + StringUtils.equals(metadataField.qualifier, metadata.getQualifier()); + } +} diff --git a/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java b/dspace-api/src/test/java/org/dspace/app/metadata/export/MetadataSchemaExportScriptIT.java new file mode 100644 index 000000000000..6ed2279bb1fa --- /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/authority/orcid/MockOrcid.java b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java index 562aa86a585e..88c29fd23344 100644 --- a/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java +++ b/dspace-api/src/test/java/org/dspace/authority/orcid/MockOrcid.java @@ -51,6 +51,14 @@ public InputStream answer(InvocationOnMock invocation) { } }); + when(orcidRestConnector.get(ArgumentMatchers.matches("^\\d{4}-\\d{4}-\\d{4}-\\d{4}$"), ArgumentMatchers.any())) + .thenAnswer(new Answer() { + @Override + public InputStream answer(InvocationOnMock invocation) { + return this.getClass().getResourceAsStream("orcid-record.xml"); + } + }); + setOrcidRestConnector(orcidRestConnector); } diff --git a/dspace-api/src/test/java/org/dspace/content/enhancer/consumer/ItemEnhancerConsumerIT.java b/dspace-api/src/test/java/org/dspace/content/enhancer/consumer/ItemEnhancerConsumerIT.java index 5e95c28f65b7..eee35a81d045 100644 --- a/dspace-api/src/test/java/org/dspace/content/enhancer/consumer/ItemEnhancerConsumerIT.java +++ b/dspace-api/src/test/java/org/dspace/content/enhancer/consumer/ItemEnhancerConsumerIT.java @@ -10,6 +10,7 @@ import static org.dspace.app.matcher.MetadataValueMatcher.with; import static org.dspace.core.CrisConstants.PLACEHOLDER_PARENT_METADATA_VALUE; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; @@ -290,6 +291,75 @@ public void testWithWorkspaceItem() throws Exception { } + @Test + @SuppressWarnings("unchecked") + public void testEnhancementAfterItemUpdate() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item person = ItemBuilder.createItem(context, collection) + .withTitle("Walter White") + .withOrcidIdentifier("0000-0000-1111-2222") + .build(); + + String personId = person.getID().toString(); + + Item publication = ItemBuilder.createItem(context, collection) + .withTitle("Test publication") + .withEntityType("Publication") + .withAuthor("Jesse Pinkman") + .withAuthor("Saul Goodman") + .withAuthor("Walter White", person.getID().toString()) + .withAuthor("Gus Fring") + .build(); + + context.restoreAuthSystemState(); + publication = commitAndReload(publication); + + assertThat(getMetadataValues(publication, "dc.contributor.author"), contains( + with("dc.contributor.author", "Jesse Pinkman"), + with("dc.contributor.author", "Saul Goodman", 1), + with("dc.contributor.author", "Walter White", personId, 2, 600), + with("dc.contributor.author", "Gus Fring", 3))); + + assertThat(getMetadataValues(publication, "cris.virtual.author-orcid"), contains( + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE), + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 1), + with("cris.virtual.author-orcid", "0000-0000-1111-2222", 2), + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 3))); + + assertThat(getMetadataValues(publication, "cris.virtualsource.author-orcid"), contains( + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE), + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 1), + with("cris.virtualsource.author-orcid", personId, 2), + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 3))); + + context.turnOffAuthorisationSystem(); + itemService.addMetadata(context, publication, "dc", "title", "alternative", null, "Other name"); + itemService.update(context, publication); + context.restoreAuthSystemState(); + publication = commitAndReload(publication); + + assertThat(getMetadataValues(publication, "dc.contributor.author"), contains( + with("dc.contributor.author", "Jesse Pinkman"), + with("dc.contributor.author", "Saul Goodman", 1), + with("dc.contributor.author", "Walter White", personId, 2, 600), + with("dc.contributor.author", "Gus Fring", 3))); + + assertThat(getMetadataValues(publication, "cris.virtual.author-orcid"), contains( + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE), + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 1), + with("cris.virtual.author-orcid", "0000-0000-1111-2222", 2), + with("cris.virtual.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 3))); + + assertThat(getMetadataValues(publication, "cris.virtualsource.author-orcid"), contains( + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE), + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 1), + with("cris.virtualsource.author-orcid", personId, 2), + with("cris.virtualsource.author-orcid", PLACEHOLDER_PARENT_METADATA_VALUE, 3))); + + } + private MetadataValue getFirstMetadataValue(Item item, String metadataField) { return getMetadataValues(item, metadataField).get(0); } diff --git a/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java index b4ecc73a0c46..cfe0aa60663e 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/integration/crosswalks/XlsCollectionCrosswalkIT.java b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/XlsCollectionCrosswalkIT.java index 8929e4d65116..6ed7d8ba3aa7 100644 --- a/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/XlsCollectionCrosswalkIT.java +++ b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/XlsCollectionCrosswalkIT.java @@ -154,7 +154,7 @@ public void testBulkImportOfCollectionDisseminate() throws Exception { } String[] args = new String[] { "bulk-import", "-c", collection.getID().toString(), - "-f", tempWorkbookFile.getAbsolutePath() }; + "-f", tempWorkbookFile.getAbsolutePath(), "-e", admin.getEmail()}; TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); diff --git a/dspace-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java b/dspace-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java index 854ab0fa300f..2fc14dbf0346 100644 --- a/dspace-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/content/security/CrisSecurityServiceIT.java @@ -24,7 +24,10 @@ import org.dspace.builder.ItemBuilder; import org.dspace.content.Collection; import org.dspace.content.Item; +import org.dspace.content.logic.Filter; +import org.dspace.content.logic.LogicalStatementException; import org.dspace.content.security.service.CrisSecurityService; +import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.utils.DSpace; @@ -368,6 +371,92 @@ public void testHasAccessWithGroupConfig() throws SQLException, AuthorizeExcepti assertThat(crisSecurityService.hasAccess(context, item, fourthUser, accessMode), is(true)); } + @Test + public void testHasAccessWithGroupConfigAndAdditionalFilter() throws SQLException, AuthorizeException { + + context.turnOffAuthorisationSystem(); + + Group firstGroup = GroupBuilder.createGroup(context) + .withName("Group 1") + .build(); + + Group secondGroup = GroupBuilder.createGroup(context) + .withName("Group 2") + .build(); + + Group thirdGroup = GroupBuilder.createGroup(context) + .withName("Group 3") + .build(); + + EPerson firstUser = EPersonBuilder.createEPerson(context) + .withEmail("user@mail.it") + .withGroupMembership(firstGroup) + .build(); + + EPerson secondUser = EPersonBuilder.createEPerson(context) + .withEmail("user2@mail.it") + .withGroupMembership(secondGroup) + .build(); + + EPerson thirdUser = EPersonBuilder.createEPerson(context) + .withEmail("user3@mail.it") + .withGroupMembership(thirdGroup) + .build(); + + EPerson fourthUser = EPersonBuilder.createEPerson(context) + .withEmail("user4@mail.it") + .withGroupMembership(thirdGroup) + .build(); + + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Test item") + .withDspaceObjectOwner("Owner", owner.getID().toString()) + .build(); + + Item itemNotAccessible = ItemBuilder.createItem(context, collection) + .withTitle("Test item not accessible") + .withDspaceObjectOwner("Owner", owner.getID().toString()) + .build(); + + context.restoreAuthSystemState(); + + AccessItemMode accessMode = buildAccessItemMode(CrisSecurity.GROUP); + when(accessMode.getGroups()).thenReturn(List.of("Group 1", thirdGroup.getID().toString())); + // filter valid only on first item + when(accessMode.getAdditionalFilter()).thenReturn(new Filter() { + @Override + public Boolean getResult(Context context, Item item) throws LogicalStatementException { + return item.getName().equals("Test item"); + } + + @Override + public String getName() { + return null; + } + + @Override + public void setBeanName(String s) {} + }); + + assertThat(crisSecurityService.hasAccess(context, item, eperson, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, admin, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, owner, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, collectionAdmin, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, communityAdmin, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, submitter, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, anotherSubmitter, accessMode), is(false)); + + assertThat(crisSecurityService.hasAccess(context, item, firstUser, accessMode), is(true)); + assertThat(crisSecurityService.hasAccess(context, item, secondUser, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, item, thirdUser, accessMode), is(true)); + assertThat(crisSecurityService.hasAccess(context, item, fourthUser, accessMode), is(true)); + + assertThat(crisSecurityService.hasAccess(context, itemNotAccessible, firstUser, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, itemNotAccessible, secondUser, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, itemNotAccessible, thirdUser, accessMode), is(false)); + assertThat(crisSecurityService.hasAccess(context, itemNotAccessible, fourthUser, accessMode), is(false)); + } + private AccessItemMode buildAccessItemMode(CrisSecurity... securities) { AccessItemMode mode = mock(AccessItemMode.class); when(mode.getSecurities()).thenReturn(List.of(securities)); diff --git a/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java b/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java index db2be516ae49..4241ba26f223 100644 --- a/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java @@ -187,9 +187,9 @@ private Item newItem() provider.delete(context, item); List metadata = itemService.getMetadata(item, - DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); List remainder = new ArrayList<>(); @@ -200,13 +200,13 @@ private Item newItem() } itemService.clearMetadata(context, item, - DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); - itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + itemService.addMetadata(context, item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null, remainder); @@ -252,9 +252,9 @@ public String createDOI(Item item, Integer status, boolean metadata, String doi) doiService.update(context, doiRow); if (metadata) { - itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + itemService.addMetadata(context, item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null, doiService.DOIToExternalForm(doi)); itemService.update(context, item); @@ -315,9 +315,9 @@ public void testStore_DOI_as_item_metadata() provider.saveDOIToObject(context, item, doi); context.restoreAuthSystemState(); - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean result = false; for (MetadataValue id : metadata) { @@ -337,9 +337,9 @@ public void testGet_DOI_out_of_item_metadata() + Long.toHexString(new Date().getTime()); context.turnOffAuthorisationSystem(); - itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + itemService.addMetadata(context, item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null, doiService.DOIToExternalForm(doi)); itemService.update(context, item); @@ -358,9 +358,9 @@ public void testRemove_DOI_from_item_metadata() + Long.toHexString(new Date().getTime()); context.turnOffAuthorisationSystem(); - itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + itemService.addMetadata(context, item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null, doiService.DOIToExternalForm(doi)); itemService.update(context, item); @@ -368,9 +368,9 @@ public void testRemove_DOI_from_item_metadata() provider.removeDOIFromObject(context, item, doi); context.restoreAuthSystemState(); - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean foundDOI = false; for (MetadataValue id : metadata) { @@ -456,9 +456,9 @@ public void testRemove_two_DOIs_from_item_metadata() context.restoreAuthSystemState(); // assure that the right one was removed - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean foundDOI1 = false; boolean foundDOI2 = false; @@ -480,9 +480,9 @@ public void testRemove_two_DOIs_from_item_metadata() context.restoreAuthSystemState(); // check it - metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); foundDOI1 = false; foundDOI2 = false; @@ -691,9 +691,9 @@ public void testDelete_specified_DOI() context.restoreAuthSystemState(); // assure that the right one was removed - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean foundDOI1 = false; boolean foundDOI2 = false; @@ -733,9 +733,9 @@ public void testDelete_all_DOIs() context.restoreAuthSystemState(); // assure that the right one was removed - List metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA, - DOIIdentifierProvider.DOI_ELEMENT, - DOIIdentifierProvider.DOI_QUALIFIER, + List metadata = itemService.getMetadata(item, provider.MD_SCHEMA, + provider.DOI_ELEMENT, + provider.DOI_QUALIFIER, null); boolean foundDOI1 = false; boolean foundDOI2 = false; diff --git a/dspace-api/src/test/resources/org/dspace/authority/orcid/orcid-record.xml b/dspace-api/src/test/resources/org/dspace/authority/orcid/orcid-record.xml new file mode 100644 index 000000000000..7672e980c8bd --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/authority/orcid/orcid-record.xml @@ -0,0 +1,270 @@ + + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + + en + + + Direct + 2023-09-19T12:25:43.445Z + 2023-10-12T14:19:06.983Z + true + true + true + + + 2023-10-12T13:28:14.550Z + + 2023-09-19T12:25:43.736Z + 2023-09-19T12:25:43.736Z + Andrea + Bollini + + + 2023-10-12T13:28:14.550Z + + 2023-10-05T07:56:29.001Z + 2023-10-12T13:28:14.550Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + isco + + + 2023-10-12T13:28:14.541Z + 2023-10-12T13:28:14.541Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + Bollini, Andrea + + + + 2023-10-12T13:27:57.187Z + + 2023-10-12T10:35:14.406Z + 2023-10-12T13:27:57.187Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + Linkedin + https://it.linkedin.com/in/andreabollini + + + 2023-10-12T13:27:57.183Z + 2023-10-12T13:27:57.183Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + 4science + https://www.4science.it/ + + + + 2023-10-12T10:38:48.105Z + + 2023-10-12T10:33:21.077Z + 2023-10-12T10:38:48.105Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + test-orcid@mailinator.com + + + + + + 2023-09-04T09:04:52.121Z + + 2023-01-13T11:20:13.803Z + 2023-01-13T11:48:02.979Z + + + https://sandbox.orcid.org/client/0000-0002-3609-4817 + 0000-0002-3609-4817 + sandbox.orcid.org + + Scopus Wizard + + Scopus Author ID + 57432999200 + http://www.scopus.com/inward/authorDetails.url?authorID=57432999200&partnerID=MN8TOARS + self + + + 2023-01-19T14:25:14.512Z + 2023-01-19T14:25:14.512Z + + + https://sandbox.orcid.org/client/0000-0002-3609-4817 + 0000-0002-3609-4817 + sandbox.orcid.org + + Scopus Wizard + + Scopus Author ID + 35233141600 + http://www.scopus.com/inward/authorDetails.url?authorID=35233141600&partnerID=MN8TOARS + self + + + + + 2023-10-12T14:19:06.992Z + + + + 2023-10-12T10:52:26.965Z + + 2023-10-12T10:52:26.965Z + + + 2023-10-12T10:52:26.965Z + 2023-10-12T10:52:26.965Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + + Dspace + + Milan + IT + + + + + + 2023-10-12T10:35:49.079Z + + + 2023-10-12T10:34:17.514Z + 2023-10-12T10:35:49.079Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + + 4Science + + Milan + IT + + + https://ror.org/03vb2cr34 + ROR + + + + + + + + + + + + + + 2023-10-12T14:19:06.992Z + + 2023-10-12T14:19:06.992Z + + + doi + 10.1016/j.procs.2014.06.008 + 10.1016/j.procs.2014.06.008 + https://doi.org/10.1016/j.procs.2014.06.008 + self + + + eid + 55484808800 + 55484808800 + self + + + + 2023-10-12T14:09:25.415Z + 2023-10-12T14:19:06.992Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + + Publication Metadata in CERIF: Inspiration by FRBR + + + + doi + 10.1016/j.procs.2014.06.008 + 10.1016/j.procs.2014.06.008 + https://doi.org/10.1016/j.procs.2014.06.008 + self + + + issn + 1877-0509 + 1877-0509 + https://portal.issn.org/resource/ISSN/1877-0509 + part-of + + + eid + 55484808800 + 55484808800 + self + + + http://dx.doi.org/10.1016/j.procs.2014.06.008 + journal-article + + 2014 + + Procedia Computer Science + + + + + \ No newline at end of file diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 97704a14dfef..9a2da1377f9b 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 5c6050d48a4a..8758aa67fe2d 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 000000000000..5c40465f5908 --- /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-oai/src/main/java/org/dspace/xoai/app/XOAI.java b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java index 2db53bff9be5..1f608ab01794 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/app/XOAI.java @@ -400,8 +400,11 @@ private SolrInputDocument index(Item item) doc.addField("item.id", item.getID().toString()); String legacyOaiId = itemService.getMetadataFirstValue(item, "dspace", "legacy", "oai-identifier", Item.ANY); - String handle = StringUtils.isNotEmpty(legacyOaiId) ? legacyOaiId.split(":")[2] : item.getHandle(); - doc.addField("item.handle", handle); + String handle = item.getHandle(); + doc.addField("item.handle", item.getHandle()); + if (StringUtils.isNotEmpty(legacyOaiId)) { + doc.addField("item.legacyoaiidentifier", legacyOaiId.split(":")[2]); + } boolean isEmbargoed = !this.isPublic(item); boolean isCurrentlyVisible = this.checkIfVisibleInOAI(item); diff --git a/dspace-oai/src/main/java/org/dspace/xoai/data/DSpaceItem.java b/dspace-oai/src/main/java/org/dspace/xoai/data/DSpaceItem.java index d1f78ee67152..b179229987da 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/data/DSpaceItem.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/data/DSpaceItem.java @@ -105,7 +105,8 @@ public List getAbout() { @Override public String getIdentifier() { - return buildIdentifier(getHandle()); + return !getMetadata("dspace","legacy", "oai-identifier").isEmpty() ? + getMetadata("dspace","legacy", "oai-identifier").get(0) : buildIdentifier(getHandle()); } private static class MetadataNamePredicate implements Predicate { diff --git a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceItemSolrRepository.java b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceItemSolrRepository.java index 281095e01aab..fc08fb6fb413 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceItemSolrRepository.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/xoai/DSpaceItemSolrRepository.java @@ -59,8 +59,13 @@ public Item getItem(String identifier) throws IdDoesNotExistException { String parts[] = identifier.split(Pattern.quote(":")); if (parts.length == 3) { try { - SolrQuery params = new SolrQuery("item.handle:" + parts[2]); - return new DSpaceSolrItem(DSpaceSolrSearch.querySingle(server, params)); + try { + SolrQuery params = new SolrQuery("item.legacyoaiidentifier:" + parts[2]); + return new DSpaceSolrItem(DSpaceSolrSearch.querySingle(server, params)); + } catch (SolrSearchEmptyException ex) { + SolrQuery altParams = new SolrQuery("item.handle:" + parts[2]); + return new DSpaceSolrItem(DSpaceSolrSearch.querySingle(server, altParams)); + } } catch (SolrSearchEmptyException | IOException ex) { throw new IdDoesNotExistException(ex); } diff --git a/dspace-rdf/pom.xml b/dspace-rdf/pom.xml index c6d887b773c0..d36c9d236ec9 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 6a5945560682..b17b22057943 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 5ad15c1097b5..6c0e8b5cf61a 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 8e098d28d2e7..ec101bf6d7f8 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonGroupRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonGroupRestController.java @@ -42,6 +42,18 @@ public class EPersonGroupRestController implements InitializingBean { private ConverterService converter; @Autowired private CollectionRestRepository collectionRestRepository; + + /** + * This request can be used to join a user to a target group by using a registration data token will be replaced + * by the {@link EPersonRegistrationRestController} features. + * + * @param context + * @param uuid + * @param token + * @return + * @throws Exception + */ + @Deprecated @RequestMapping(method = RequestMethod.POST, value = EPersonRest.CATEGORY + "/" + EPersonRest.PLURAL_NAME + "/{uuid}/" + EPersonRest.GROUPS) public ResponseEntity> joinUserToGroups(Context context, diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonRegistrationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/EPersonRegistrationRestController.java new file mode 100644 index 000000000000..87b402df9d22 --- /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 e772aa0abe18..b02869962156 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyEPersonReplaceRestController.java @@ -23,7 +23,9 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.content.Bitstream; import org.dspace.content.DSpaceObject; +import org.dspace.content.service.BitstreamService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +53,8 @@ public class ResourcePolicyEPersonReplaceRestController { private Utils utils; @Autowired private ResourcePolicyService resourcePolicyService; + @Autowired + private BitstreamService bitstreamService; @PreAuthorize("hasPermission(#id, 'resourcepolicy', 'ADMIN')") @RequestMapping(method = PUT, consumes = {"text/uri-list"}) @@ -75,6 +79,11 @@ public ResponseEntity> replaceEPersonOfResourcePolicy(@Pa } EPerson newEPerson = (EPerson) dsoList.get(0); resourcePolicy.setEPerson(newEPerson); + + if (bitstreamService.isOriginalBitstream(resourcePolicy.getdSpaceObject())) { + bitstreamService.updateThumbnailResourcePolicies(context, (Bitstream) resourcePolicy.getdSpaceObject()); + } + context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java index e9ba0dff4429..40a82068dbce 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ResourcePolicyGroupReplaceRestController.java @@ -23,7 +23,9 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.content.Bitstream; import org.dspace.content.DSpaceObject; +import org.dspace.content.service.BitstreamService; import org.dspace.core.Context; import org.dspace.eperson.Group; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +53,8 @@ public class ResourcePolicyGroupReplaceRestController { private Utils utils; @Autowired private ResourcePolicyService resourcePolicyService; + @Autowired + private BitstreamService bitstreamService; @PreAuthorize("hasPermission(#id, 'resourcepolicy', 'ADMIN')") @RequestMapping(method = PUT, consumes = {"text/uri-list"}) @@ -75,6 +79,11 @@ public ResponseEntity> replaceGroupOfResourcePolicy(@Path Group newGroup = (Group) dsoList.get(0); resourcePolicy.setGroup(newGroup); + + if (bitstreamService.isOriginalBitstream(resourcePolicy.getdSpaceObject())) { + bitstreamService.updateThumbnailResourcePolicies(context, (Bitstream) resourcePolicy.getdSpaceObject()); + } + context.commit(); return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ExternalSourceEntryRestConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ExternalSourceEntryRestConverter.java index 585de2a99a57..35921c16d254 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ExternalSourceEntryRestConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ExternalSourceEntryRestConverter.java @@ -7,9 +7,19 @@ */ package org.dspace.app.rest.converter; +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + import org.dspace.app.rest.model.ExternalSourceEntryRest; +import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.projection.Projection; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; import org.dspace.external.model.ExternalDataObject; +import org.dspace.web.ContextUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -22,6 +32,12 @@ public class ExternalSourceEntryRestConverter implements DSpaceConverter convertToItemRests(List uuids, Projection projection) { + + if (uuids == null) { + return List.of(); + } + + Context context = ContextUtil.obtainCurrentRequestContext(); + return uuids.stream() + .map(uuid -> { + try { + return itemService.find(context, uuid); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }) + .filter(item -> Objects.nonNull(item)) + .map(item -> itemConverter.convert(item, projection)) + .collect(Collectors.toList()); + } + public Class getModelClass() { return ExternalDataObject.class; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java index 76aca4be231d..da47f3d8b659 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java @@ -35,7 +35,7 @@ * Converter to translate between lists of domain {@link MetadataValue}s and {@link MetadataRest} representations. */ @Component -public class MetadataConverter implements DSpaceConverter { +public class MetadataConverter implements DSpaceConverter> { @Autowired private ContentServiceFactory contentServiceFactory; @@ -46,7 +46,7 @@ public class MetadataConverter implements DSpaceConverter convert(MetadataValueList metadataValues, Projection projection) { // Convert each value to a DTO while retaining place order in a map of key -> SortedSet Map> mapOfSortedSets = new HashMap<>(); @@ -60,7 +60,7 @@ public MetadataRest convert(MetadataValueList metadataValues, set.add(converter.toRest(metadataValue, projection)); } - MetadataRest metadataRest = new MetadataRest(); + MetadataRest metadataRest = new MetadataRest<>(); // Populate MetadataRest's map of key -> List while respecting SortedSet's order Map> mapOfLists = metadataRest.getMap(); @@ -80,14 +80,14 @@ public Class getModelClass() { * Sets a DSpace object's domain metadata values from a rest representation. * Any existing metadata value is deleted or overwritten. * - * @param context the context to use. - * @param dso the DSpace object. + * @param context the context to use. + * @param dso the DSpace object. * @param metadataRest the rest representation of the new metadata. - * @throws SQLException if a database error occurs. + * @throws SQLException if a database error occurs. * @throws AuthorizeException if an authorization error occurs. */ public void setMetadata(Context context, T dso, MetadataRest metadataRest) - throws SQLException, AuthorizeException { + throws SQLException, AuthorizeException { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); dsoService.clearMetadata(context, dso, Item.ANY, Item.ANY, Item.ANY, Item.ANY); persistMetadataRest(context, dso, metadataRest, dsoService); @@ -97,14 +97,14 @@ public void setMetadata(Context context, T dso, Metadat * Add to a DSpace object's domain metadata values from a rest representation. * Any existing metadata value is preserved. * - * @param context the context to use. - * @param dso the DSpace object. + * @param context the context to use. + * @param dso the DSpace object. * @param metadataRest the rest representation of the new metadata. - * @throws SQLException if a database error occurs. + * @throws SQLException if a database error occurs. * @throws AuthorizeException if an authorization error occurs. */ public void addMetadata(Context context, T dso, MetadataRest metadataRest) - throws SQLException, AuthorizeException { + throws SQLException, AuthorizeException { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); persistMetadataRest(context, dso, metadataRest, dsoService); } @@ -113,33 +113,34 @@ public void addMetadata(Context context, T dso, Metadat * Merge into a DSpace object's domain metadata values from a rest representation. * Any existing metadata value is preserved or overwritten with the new ones * - * @param context the context to use. - * @param dso the DSpace object. + * @param context the context to use. + * @param dso the DSpace object. * @param metadataRest the rest representation of the new metadata. - * @throws SQLException if a database error occurs. + * @throws SQLException if a database error occurs. * @throws AuthorizeException if an authorization error occurs. */ - public void mergeMetadata(Context context, T dso, MetadataRest metadataRest) - throws SQLException, AuthorizeException { + public void mergeMetadata( + Context context, T dso, MetadataRest metadataRest + ) throws SQLException, AuthorizeException { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); - for (Map.Entry> entry: metadataRest.getMap().entrySet()) { + for (Map.Entry> entry : metadataRest.getMap().entrySet()) { List metadataByMetadataString = dsoService.getMetadataByMetadataString(dso, entry.getKey()); dsoService.removeMetadataValues(context, dso, metadataByMetadataString); } persistMetadataRest(context, dso, metadataRest, dsoService); } - private void persistMetadataRest(Context context, T dso, MetadataRest metadataRest, - DSpaceObjectService dsoService) - throws SQLException, AuthorizeException { - for (Map.Entry> entry: metadataRest.getMap().entrySet()) { + private void persistMetadataRest( + Context context, T dso, MetadataRest metadataRest, DSpaceObjectService dsoService + ) throws SQLException, AuthorizeException { + for (Map.Entry> entry : metadataRest.getMap().entrySet()) { String[] seq = entry.getKey().split("\\."); String schema = seq[0]; String element = seq[1]; String qualifier = seq.length == 3 ? seq[2] : null; - for (MetadataValueRest mvr: entry.getValue()) { + for (MetadataValueRest mvr : entry.getValue()) { dsoService.addMetadata(context, dso, schema, element, qualifier, mvr.getLanguage(), - mvr.getValue(), mvr.getAuthority(), mvr.getConfidence()); + mvr.getValue(), mvr.getAuthority(), mvr.getConfidence()); } } dsoService.update(context, dso); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.java new file mode 100644 index 000000000000..3ec5bfbf533c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.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.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()); + if (ePerson == null && registrationData.getRegistrationType().equals(RegistrationTypeEnum.ORCID)) { + ePerson = context.getCurrentUser(); + } + } 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/converter/RootConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RootConverter.java index 61f18a5b3c9c..1d81e308e39f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RootConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RootConverter.java @@ -10,6 +10,7 @@ import static org.dspace.app.util.Util.getSourceVersion; import org.dspace.app.rest.model.RootRest; +import org.dspace.core.CrisConstants; import org.dspace.services.ConfigurationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -28,7 +29,8 @@ public RootRest convert() { rootRest.setDspaceName(configurationService.getProperty("dspace.name")); rootRest.setDspaceUI(configurationService.getProperty("dspace.ui.url")); rootRest.setDspaceServer(configurationService.getProperty("dspace.server.url")); - rootRest.setDspaceVersion("DSpace " + getSourceVersion()); + rootRest.setDspaceVersion(CrisConstants.DSPACE_BASE_VERSION); + rootRest.setCrisVersion(getSourceVersion()); return rootRest; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java index 1b71eb8957a2..e7b43ebe33c2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java @@ -20,7 +20,7 @@ public abstract class DSpaceObjectRest extends BaseObjectRest { private String name; private String handle; - MetadataRest metadata = new MetadataRest(); + MetadataRest metadata = new MetadataRest<>(); @Override public String getId() { @@ -56,11 +56,11 @@ public void setHandle(String handle) { * * @return the metadata. */ - public MetadataRest getMetadata() { + public MetadataRest getMetadata() { return metadata; } - public void setMetadata(MetadataRest metadata) { + public void setMetadata(MetadataRest metadata) { this.metadata = metadata; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ExternalSourceEntryRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ExternalSourceEntryRest.java index 06af7e222713..4e578c313870 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ExternalSourceEntryRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ExternalSourceEntryRest.java @@ -7,6 +7,8 @@ */ package org.dspace.app.rest.model; +import java.util.List; + import org.dspace.app.rest.ExternalSourcesRestController; /** @@ -38,6 +40,7 @@ public String getType() { private String value; private String externalSource; private MetadataRest metadata = new MetadataRest(); + private List matchObjects; /** * Generic getter for the id @@ -118,4 +121,12 @@ public MetadataRest getMetadata() { public void setMetadata(MetadataRest metadata) { this.metadata = metadata; } + + public List getMatchObjects() { + return matchObjects; + } + + public void setMatchObjects(List matchObjects) { + this.matchObjects = matchObjects; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java index d1367c8fea82..072acbcfd71e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java @@ -19,10 +19,10 @@ /** * Rest representation of a map of metadata keys to ordered lists of values. */ -public class MetadataRest { +public class MetadataRest { @JsonAnySetter - private SortedMap> map = new TreeMap(); + private SortedMap> map = new TreeMap(); /** * Gets the map. @@ -30,7 +30,7 @@ public class MetadataRest { * @return the map of keys to ordered values. */ @JsonAnyGetter - public SortedMap> getMap() { + public SortedMap> getMap() { return map; } @@ -44,16 +44,16 @@ public SortedMap> getMap() { * they are passed to this method. * @return this instance, to support chaining calls for easy initialization. */ - public MetadataRest put(String key, MetadataValueRest... values) { + public MetadataRest put(String key, T... values) { // determine highest explicitly ordered value int highest = -1; - for (MetadataValueRest value : values) { + for (T value : values) { if (value.getPlace() > highest) { highest = value.getPlace(); } } // add any non-explicitly ordered values after highest - for (MetadataValueRest value : values) { + for (T value : values) { if (value.getPlace() < 0) { highest++; value.setPlace(highest); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java new file mode 100644 index 000000000000..370bd9027f62 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java @@ -0,0 +1,37 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationMetadataRest extends MetadataValueRest { + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String overrides; + + public RegistrationMetadataRest(String value, String overrides) { + super(); + this.value = value; + this.overrides = overrides; + } + + public RegistrationMetadataRest(String value) { + this(value, null); + } + + public String getOverrides() { + return overrides; + } + + public void setOverrides(String overrides) { + this.overrides = overrides; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java index 191aec88a414..7285a01a4a24 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java @@ -27,14 +27,28 @@ public class RegistrationRest extends RestAddressableModel { public static final String NAME_PLURAL = "registrations"; public static final String CATEGORY = EPERSON; + private Integer id; private String email; private UUID user; + private String registrationType; + private String netId; + @JsonInclude(JsonInclude.Include.NON_NULL) + private MetadataRest registrationMetadata; @JsonInclude(JsonInclude.Include.NON_NULL) private List groupNames = Collections.emptyList(); - private List groups = Collections.emptyList(); + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + /** * Generic getter for the email + * * @return the email value of this RegisterRest */ public String getEmail() { @@ -43,7 +57,8 @@ public String getEmail() { /** * Generic setter for the email - * @param email The email to be set on this RegisterRest + * + * @param email The email to be set on this RegisterRest */ public void setEmail(String email) { this.email = email; @@ -51,6 +66,7 @@ public void setEmail(String email) { /** * Generic getter for the user + * * @return the user value of this RegisterRest */ public UUID getUser() { @@ -59,12 +75,38 @@ public UUID getUser() { /** * Generic setter for the user - * @param user The user to be set on this RegisterRest + * + * @param user The user to be set on this RegisterRest */ public void setUser(UUID user) { this.user = user; } + public String getRegistrationType() { + return registrationType; + } + + public void setRegistrationType(String registrationType) { + this.registrationType = registrationType; + } + + public String getNetId() { + return netId; + } + + public void setNetId(String netId) { + this.netId = netId; + } + + public MetadataRest getRegistrationMetadata() { + return registrationMetadata; + } + + public void setRegistrationMetadata( + MetadataRest registrationMetadata) { + this.registrationMetadata = registrationMetadata; + } + public List getGroups() { return groups; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RootRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RootRest.java index cef8965601ca..9fd6a1263423 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RootRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RootRest.java @@ -21,6 +21,7 @@ public class RootRest extends RestAddressableModel { private String dspaceName; private String dspaceServer; private String dspaceVersion; + private String crisVersion; public String getCategory() { return CATEGORY; @@ -67,6 +68,14 @@ public void setDspaceVersion(String dspaceVersion) { this.dspaceVersion = dspaceVersion; } + public String getCrisVersion() { + return crisVersion; + } + + public void setCrisVersion(String crisVersion) { + this.crisVersion = crisVersion; + } + @Override public boolean equals(Object object) { return (object instanceof RootRest && @@ -76,6 +85,7 @@ public boolean equals(Object object) { .append(this.getDspaceUI(), ((RootRest) object).getDspaceUI()) .append(this.getDspaceName(), ((RootRest) object).getDspaceName()) .append(this.getDspaceServer(), ((RootRest) object).getDspaceServer()) + .append(this.getCrisVersion(), ((RootRest)object).getCrisVersion()) .isEquals()); } @@ -88,6 +98,7 @@ public int hashCode() { .append(this.getDspaceName()) .append(this.getDspaceUI()) .append(this.getDspaceServer()) + .append(this.getCrisVersion()) .toHashCode(); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index 566917854532..ae689807e2d2 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 3fbd6c9d9163..3cc2ecb468c8 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 0b77f96b9b5f..72ca3f254256 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ResourcePolicyRestRepository.java @@ -30,7 +30,9 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.content.Bitstream; import org.dspace.content.DSpaceObject; +import org.dspace.content.service.BitstreamService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -76,6 +78,9 @@ public class ResourcePolicyRestRepository extends DSpaceRestRepository recordsFound = importService.getRecords(file, mpFile.getOriginalFilename()); + if (recordsFound != null && !recordsFound.isEmpty()) { + records.addAll(recordsFound); break; } } catch (Exception e) { @@ -334,11 +334,15 @@ public Iterable upload(Context context, HttpServletRequest re } catch (Exception e) { log.error("Error importing metadata", e); } - WorkspaceItem source = submissionService. - createWorkspaceItem(context, getRequestService().getCurrentRequest()); - merge(context, records, source); - result = new ArrayList<>(); - result.add(source); + result = new ArrayList<>(records.size()); + for (ImportRecord importRecord : records) { + WorkspaceItem source = submissionService. + createWorkspaceItem(context, getRequestService().getCurrentRequest()); + + merge(context, importRecord, source); + + result.add(source); + } //perform upload of bitstream if there is exact one result and convert workspaceitem to entity rest if (!result.isEmpty()) { @@ -348,18 +352,17 @@ public Iterable upload(Context context, HttpServletRequest re //load bitstream into bundle ORIGINAL only if there is one result (approximately this is the // right behaviour for pdf file but not for other bibliographic format e.g. bibtex) if (result.size() == 1) { + ClassLoader loader = this.getClass().getClassLoader(); for (int i = 0; i < submissionConfig.getNumberOfSteps(); i++) { SubmissionStepConfig stepConfig = submissionConfig.getStep(i); - ClassLoader loader = this.getClass().getClassLoader(); - Class stepClass; try { - stepClass = loader.loadClass(stepConfig.getProcessingClassName()); - Object stepInstance = stepClass.newInstance(); + Class stepClass = loader.loadClass(stepConfig.getProcessingClassName()); + Object stepInstance = stepClass.getConstructor().newInstance(); if (UploadableStep.class.isAssignableFrom(stepClass)) { UploadableStep uploadableStep = (UploadableStep) stepInstance; for (MultipartFile mpFile : uploadfiles) { - ErrorRest err = uploadableStep.upload(context, - submissionService, stepConfig, wi, mpFile); + ErrorRest err = + uploadableStep.upload(context, submissionService, stepConfig, wi, mpFile); if (err != null) { errors.add(err); } @@ -449,7 +452,7 @@ private BaseObjectRest findItemRestById(Context context, String itemId) throw return authorizationRestUtil.getObject(context, objectId); } - private void merge(Context context, List records, WorkspaceItem item) throws SQLException { + private void merge(Context context, ImportRecord record, WorkspaceItem item) throws SQLException { for (MetadataValue metadataValue : itemService.getMetadata( item.getItem(), Item.ANY, Item.ANY, Item.ANY, Item.ANY)) { itemService.clearMetadata(context, item.getItem(), @@ -458,13 +461,11 @@ private void merge(Context context, List records, WorkspaceItem it metadataValue.getMetadataField().getQualifier(), metadataValue.getLanguage()); } - for (ImportRecord record : records) { - if (record != null && record.getValueList() != null) { - for (MetadatumDTO metadataValue : record.getValueList()) { - itemService.addMetadata(context, item.getItem(), metadataValue.getSchema(), - metadataValue.getElement(), metadataValue.getQualifier(), null, - metadataValue.getValue()); - } + if (record != null && record.getValueList() != null) { + for (MetadatumDTO metadataValue : record.getValueList()) { + itemService.addMetadata(context, item.getItem(), metadataValue.getSchema(), + metadataValue.getElement(), metadataValue.getQualifier(), null, + metadataValue.getValue()); } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/RegistrationEmailPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/RegistrationEmailPatchOperation.java new file mode 100644 index 000000000000..e4bbd45a3f34 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/RegistrationEmailPatchOperation.java @@ -0,0 +1,166 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation; + +import java.sql.SQLException; +import java.text.MessageFormat; +import java.util.Optional; + +import com.fasterxml.jackson.databind.JsonNode; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.JsonValueEvaluator; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; +import org.dspace.eperson.dto.RegistrationDataChanges; +import org.dspace.eperson.dto.RegistrationDataPatch; +import org.dspace.eperson.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for RegistrationData email patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/eperson/registration/<:registration-id>?token=<:token> -H " + * Content-Type: application/json" -d '[{ "op": "replace", "path": "/email", "value": "new@email"]' + * + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +@Component +public class RegistrationEmailPatchOperation extends PatchOperation { + + /** + * Path in json body of patch that uses this operation + */ + private static final String OPERATION_PATH_EMAIL = "/email"; + + @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 9fdef6b050f7..0a50fec20803 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidLoginFilter.java @@ -7,7 +7,12 @@ */ package org.dspace.app.rest.security; +import static org.dspace.authenticate.OrcidAuthenticationBean.ORCID_AUTH_ATTRIBUTE; +import static org.dspace.authenticate.OrcidAuthenticationBean.ORCID_DEFAULT_REGISTRATION_URL; +import static org.dspace.authenticate.OrcidAuthenticationBean.ORCID_REGISTRATION_TOKEN; + import java.io.IOException; +import java.text.MessageFormat; import java.util.ArrayList; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -43,10 +48,11 @@ public class OrcidLoginFilter extends StatelessLoginFilter { private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); private OrcidAuthenticationBean orcidAuthentication = new DSpace().getServiceManager() - .getServiceByName("orcidAuthentication", OrcidAuthenticationBean.class); + .getServiceByName("orcidAuthentication", + OrcidAuthenticationBean.class); public OrcidLoginFilter(String url, AuthenticationManager authenticationManager, - RestAuthenticationService restAuthenticationService) { + RestAuthenticationService restAuthenticationService) { super(url, authenticationManager, restAuthenticationService); } @@ -64,13 +70,13 @@ public Authentication attemptAuthentication(HttpServletRequest req, HttpServletR @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, - Authentication auth) throws IOException, ServletException { + Authentication auth) throws IOException, ServletException { DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth; log.debug("Orcid authentication successful for EPerson {}. Sending back temporary auth cookie", - dSpaceAuthentication.getName()); + dSpaceAuthentication.getName()); restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, true); @@ -79,26 +85,41 @@ protected void successfulAuthentication(HttpServletRequest req, HttpServletRespo @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, - AuthenticationException failed) throws IOException, ServletException { + AuthenticationException failed) throws IOException, ServletException { Context context = ContextUtil.obtainContext(request); - if (orcidAuthentication.isUsed(context, request)) { - String baseRediredirectUrl = configurationService.getProperty("dspace.ui.url"); - String redirectUrl = baseRediredirectUrl + "/error?status=401&code=orcid.generic-error"; - response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection] - } else { + if (!orcidAuthentication.isUsed(context, request)) { super.unsuccessfulAuthentication(request, response, failed); + return; + } + + String baseRediredirectUrl = configurationService.getProperty("dspace.ui.url"); + String redirectUrl = baseRediredirectUrl + "/error?status=401&code=orcid.generic-error"; + Object registrationToken = request.getAttribute(ORCID_REGISTRATION_TOKEN); + if (registrationToken != null) { + final String orcidRegistrationDataUrl = + configurationService.getProperty("orcid.registration-data.url", ORCID_DEFAULT_REGISTRATION_URL); + redirectUrl = baseRediredirectUrl + MessageFormat.format(orcidRegistrationDataUrl, registrationToken); + if (log.isDebugEnabled()) { + log.debug( + "Orcid authentication failed for user with ORCID {}.", + request.getAttribute(ORCID_AUTH_ATTRIBUTE) + ); + log.debug("Redirecting to {} for registration completion.", redirectUrl); + } } + response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection] } /** * After successful login, redirect to the DSpace URL specified by this Orcid * request (in the "redirectUrl" request parameter). If that 'redirectUrl' is * not valid or trusted for this DSpace site, then return a 400 error. - * @param request - * @param response + * + * @param request + * @param response * @throws IOException */ private void redirectAfterSuccess(HttpServletRequest request, HttpServletResponse response) throws IOException { @@ -126,9 +147,9 @@ private void redirectAfterSuccess(HttpServletRequest request, HttpServletRespons response.sendRedirect(redirectUrl); } else { log.error("Invalid Orcid redirectURL=" + redirectUrl + - ". URL doesn't match hostname of server or UI!"); + ". URL doesn't match hostname of server or UI!"); response.sendError(HttpServletResponse.SC_BAD_REQUEST, - "Invalid redirectURL! Must match server or ui hostname."); + "Invalid redirectURL! Must match server or ui hostname."); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java index 940773547da4..23c8f99e5857 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractUsageReportGenerator.java @@ -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 3e366f7cc9de..8fef2b35853a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/StatisticsReportsConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/StatisticsReportsConfiguration.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Optional; +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.model.UsageReportCategoryRest; import org.dspace.content.Bitstream; import org.dspace.content.Collection; @@ -40,6 +41,13 @@ public List getCategories(DSpaceObject dso) { } else if (dso instanceof Community) { return mapping.get("community"); } else if (dso instanceof Collection) { + String entityType = getEntityType(dso); + if (StringUtils.isNotEmpty(entityType)) { + List result = mapping.get("collection-" + entityType); + if (result != null) { + return result; + } + } return mapping.get("collection"); } else if (dso instanceof Item) { Item item = (Item) dso; @@ -59,6 +67,16 @@ public List getCategories(DSpaceObject dso) { return null; } + private String getEntityType(DSpaceObject dso) { + return dso.getMetadata() + .stream() + .filter(metadataValue -> + "dspace.entity.type".equals(metadataValue.getMetadataField().toString('.'))) + .map(MetadataValue::getValue) + .findFirst() + .orElse(""); + } + public UsageReportGenerator getReportGenerator(DSpaceObject dso, String reportId) { List categories = getCategories(dso); Optional cat = categories.stream().filter(x -> x.getReports().containsKey(reportId)) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java index 39d8a1730c06..fbba8f902ee1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopCategoriesGenerator.java @@ -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 fa2dc320b87b..e43a060bdf81 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 67983ba8f007..f5ec4428aa18 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 000000000000..49ceeba0dc9c --- /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/CreateWorkspaceItemFromExternalServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CreateWorkspaceItemFromExternalServiceIT.java index 79253dcc6dee..19100f722333 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CreateWorkspaceItemFromExternalServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CreateWorkspaceItemFromExternalServiceIT.java @@ -8,6 +8,7 @@ package org.dspace.app.rest; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; @@ -199,6 +200,8 @@ public void creatingWorkspaceItemImportedFromScopusTest() throws Exception { + ".traditionalpageone['dc.identifier.scopus'][0].value", is(scopus2R.getValue()))) .andExpect(jsonPath("$._embedded.workflowitems[1].sections" + ".traditionalpageone['dc.identifier.doi'][0].value", is(doi2R.getValue()))) + .andExpect(jsonPath("$._embedded.workflowitems[0].sections.license.url", + containsString("/api/core/bitstreams/"))) .andExpect(jsonPath("$.page.totalElements", is(2))); } 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 000000000000..cfff06d501f7 --- /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 fab9dffa4616..9fa507226ef4 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/ExternalSourcesRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java index 565a4d003f78..7c396e803537 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java @@ -19,15 +19,18 @@ import org.dspace.app.rest.matcher.EntityTypeMatcher; import org.dspace.app.rest.matcher.ExternalSourceEntryMatcher; import org.dspace.app.rest.matcher.ExternalSourceMatcher; +import org.dspace.app.rest.matcher.ItemMatcher; import org.dspace.app.rest.matcher.PageMatcher; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.EntityTypeBuilder; +import org.dspace.builder.ItemBuilder; import org.dspace.builder.WorkflowItemBuilder; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.EntityType; +import org.dspace.content.Item; import org.dspace.core.CrisConstants; import org.dspace.external.provider.AbstractExternalDataProvider; import org.dspace.external.provider.ExternalDataProvider; @@ -485,4 +488,60 @@ public void findSupportedEntityTypesOfAnExternalDataProviderPaginationTest() thr } } + @Test + public void findOneExternalSourceEntriesDuplicationTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + // create item withDoiIdentifier equals 10.1016/j.procs.2017.03.031 + Item itemOne = ItemBuilder.createItem(context, col1) + .withFullName("Public item one") + .withIssueDate("2023-10-17") + .withDoiIdentifier("10.1016/j.procs.2017.03.031") + .withEntityType("Publication") + .build(); + + // create another item withDoiIdentifier equals 10.1016/j.procs.2017.03.031 + Item itemTwo = ItemBuilder.createItem(context, col1) + .withFullName("Public item two") + .withIssueDate("2023-10-17") + .withDoiIdentifier("10.1016/j.procs.2017.03.031") + .withEntityType("Publication") + .build(); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/integration/externalsources/mock/entries") + .param("query", "one").param("size", "1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.externalSourceEntries", Matchers.hasItem( + ExternalSourceEntryMatcher.matchExternalSourceEntry("onetwo", "onetwo", "onetwo", "mock") + ))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].matchObjects", containsInAnyOrder( + ItemMatcher.matchItemProperties(itemOne), + ItemMatcher.matchItemProperties(itemTwo) + ))) + .andExpect(jsonPath("$.page", PageMatcher.pageEntryWithTotalPagesAndElements(0, 1, 2, 2))); + + getClient().perform(get("/api/integration/externalsources/mock/entries") + .param("query", "one").param("size", "1").param("page", "1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.externalSourceEntries", Matchers.hasItem( + ExternalSourceEntryMatcher.matchExternalSourceEntry("one", "one", "one", "mock") + ))) + .andExpect(jsonPath("$._embedded.externalSourceEntries[0].matchObjects", containsInAnyOrder( + ItemMatcher.matchItemProperties(itemOne), + ItemMatcher.matchItemProperties(itemTwo) + ))) + .andExpect(jsonPath("$.page", PageMatcher.pageEntryWithTotalPagesAndElements(1, 1, 2, 2))); + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemAuthorityIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemAuthorityIT.java index 9e0ea90afc69..cdfbf2f29b13 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemAuthorityIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemAuthorityIT.java @@ -127,19 +127,25 @@ public void singleItemAuthorityTest() throws Exception { Map.of("data-oairecerif_author_affiliation", "OrgUnit_1::" + orgUnit_1.getID(), "oairecerif_author_affiliation", "OrgUnit_1::" - + orgUnit_1.getID())), + + orgUnit_1.getID(), + "data-person_identifier_orcid", "", + "person_identifier_orcid", "")), ItemAuthorityMatcher.matchItemAuthorityWithOtherInformations(author_2.getID().toString(), "Author 2", "Author 2", "vocabularyEntry", Map.of("data-oairecerif_author_affiliation", "OrgUnit_1::" + orgUnit_1.getID(), "oairecerif_author_affiliation", "OrgUnit_1::" - + orgUnit_1.getID())), + + orgUnit_1.getID(), + "data-person_identifier_orcid", "", + "person_identifier_orcid", "")), ItemAuthorityMatcher.matchItemAuthorityWithOtherInformations(author_3.getID().toString(), "Author 3", "Author 3", "vocabularyEntry", Map.of("data-oairecerif_author_affiliation", "OrgUnit_2::" + orgUnit_2.getID(), "oairecerif_author_affiliation", "OrgUnit_2::" - + orgUnit_2.getID())) + + orgUnit_2.getID(), + "data-person_identifier_orcid", "", + "person_identifier_orcid", "")) ))) .andExpect(jsonPath("$.page.totalElements", Matchers.is(3))); } @@ -216,13 +222,19 @@ public void multiItemAuthorityTest() throws Exception { "Author 2(OrgUnit_2)", "Author 2", "vocabularyEntry", Map.of("data-oairecerif_author_affiliation", "OrgUnit_2::" + orgUnit_2.getID(), "oairecerif_author_affiliation", "OrgUnit_2::" + orgUnit_2.getID())), + ItemAuthorityMatcher.matchItemAuthorityWithOtherInformations(author_1.getID().toString(), + "Author 1", "Author 1", "vocabularyEntry", + Map.of("data-person_identifier_orcid", "", "person_identifier_orcid", "")), + ItemAuthorityMatcher.matchItemAuthorityWithOtherInformations(author_2.getID().toString(), + "Author 2", "Author 2", "vocabularyEntry", + Map.of("data-person_identifier_orcid", "", "person_identifier_orcid", "")), // filled with EditorAuthority extra metadata generator - ItemAuthorityMatcher.matchItemAuthorityProperties(author_1.getID().toString(), - "Author 1", "Author 1", "vocabularyEntry"), - ItemAuthorityMatcher.matchItemAuthorityProperties(author_2.getID().toString(), - "Author 2", "Author 2", "vocabularyEntry") + ItemAuthorityMatcher.matchItemAuthorityWithOtherInformations(author_1.getID().toString(), + "Author 1", "Author 1", "vocabularyEntry", Map.of()), + ItemAuthorityMatcher.matchItemAuthorityWithOtherInformations(author_2.getID().toString(), + "Author 2", "Author 2", "vocabularyEntry", Map.of()) ))) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(5))); + .andExpect(jsonPath("$.page.totalElements", Matchers.is(7))); } @Test @@ -250,7 +262,9 @@ public void singleItemAuthorityWithoutOrgUnitTest() throws Exception { .andExpect(jsonPath("$._embedded.entries", Matchers.contains( ItemAuthorityMatcher.matchItemAuthorityWithOtherInformations(author_1.getID().toString(), "Author 1", "Author 1", "vocabularyEntry", - Map.of("data-oairecerif_author_affiliation", "", "oairecerif_author_affiliation", "")) + Map.of("data-oairecerif_author_affiliation", "", "oairecerif_author_affiliation", "", + "data-person_identifier_orcid", "", + "person_identifier_orcid", "")) ))) .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidAuthorityIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidAuthorityIT.java index 2eb9cae64ae9..33bb24f8b029 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidAuthorityIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidAuthorityIT.java @@ -56,8 +56,8 @@ */ public class OrcidAuthorityIT extends AbstractControllerIntegrationTest { - private static final String ORCID_INFO = OrcidAuthority.ORCID_EXTRA; - private static final String ORCID_INSTITUTION = OrcidAuthority.INSTITUTION_EXTRA; + private static final String ORCID_INFO = OrcidAuthority.DEFAULT_ORCID_KEY; + private static final String ORCID_INSTITUTION = OrcidAuthority.DEFAULT_INSTITUTION_KEY; private static final String READ_PUBLIC_TOKEN = "062d9f30-7e11-47ef-bd95-eaa2f2452565"; @@ -600,8 +600,8 @@ public void testWithAffiliationExtra() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.entries", containsInAnyOrder( orcidEntry("Author From Orcid 1", REFERENCE, "0000-1111-2222-3333"), - orcidEntryWithInstitution("Author From Orcid 2", REFERENCE, "0000-2222-3333-4444", "Org1, Org2"), - orcidEntryWithInstitution("Author From Orcid 3", REFERENCE, "0000-5555-6666-7777", "Organization")))) + orcidEntryWithAffiliation("Author From Orcid 2", REFERENCE, "0000-2222-3333-4444", "Org1, Org2"), + orcidEntryWithAffiliation("Author From Orcid 3", REFERENCE, "0000-5555-6666-7777", "Organization")))) .andExpect(jsonPath("$.page.size", Matchers.is(20))) .andExpect(jsonPath("$.page.totalPages", Matchers.is(1))) .andExpect(jsonPath("$.page.totalElements", Matchers.is(3))); @@ -669,8 +669,11 @@ private Item buildPerson(String title, Item affiliation) { private Matcher affiliationEntry(Item item, String title, String otherInfoValue) { return ItemAuthorityMatcher.matchItemAuthorityWithOtherInformations(id(item), title, - title, "vocabularyEntry", Map.of("data-oairecerif_author_affiliation", otherInfoValue, - "oairecerif_author_affiliation", otherInfoValue)); + title, "vocabularyEntry", Map.of( + "data-oairecerif_author_affiliation", otherInfoValue, + "oairecerif_author_affiliation", otherInfoValue, + "data-" + ORCID_INFO, "", + ORCID_INFO, "")); } private Matcher orcidEntry(String title, String authorityPrefix, String orcid) { @@ -679,11 +682,18 @@ private Matcher orcidEntry(String title, String authorityPrefix, title, "vocabularyEntry", ORCID_INFO, orcid); } - private Matcher orcidEntryWithInstitution(String title, String authorityPrefix, - String orcid, String institutions) { + private Matcher orcidEntryWithAffiliation(String title, String authorityPrefix, + String orcid, String affiliation) { String authority = authorityPrefix + "ORCID::" + orcid; - return ItemAuthorityMatcher.matchItemAuthorityWithTwoMetadataInOtherInformations(authority, title, - title, "vocabularyEntry", ORCID_INFO, orcid, ORCID_INSTITUTION, institutions); + return ItemAuthorityMatcher.matchItemAuthorityWithTwoMetadataInOtherInformations( + authority, title, title, "vocabularyEntry", + Map.of( + "data-" + ORCID_INFO, orcid, + ORCID_INFO, orcid, + "data-oairecerif_author_affiliation", affiliation, + "oairecerif_author_affiliation", affiliation + ) + ); } private String id(Item item) { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidExternalSourcesIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidExternalSourcesIT.java index 04592c17da2d..2e797f9ce4ab 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidExternalSourcesIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidExternalSourcesIT.java @@ -150,10 +150,10 @@ public void findOneExternalSourcesMockitoTest() throws Exception { OrcidRestConnector orcidConnector = Mockito.mock(OrcidRestConnector.class); OrcidRestConnector realConnector = orcidV3AuthorDataProvider.getOrcidRestConnector(); orcidV3AuthorDataProvider.setOrcidRestConnector(orcidConnector); - when(orcidConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) + when(orcidConnector.get(ArgumentMatchers.matches("^\\d{4}-\\d{4}-\\d{4}-\\d{4}$"), ArgumentMatchers.any())) .thenAnswer(new Answer() { public InputStream answer(InvocationOnMock invocation) { - return getClass().getResourceAsStream("orcid-person-record.xml"); + return getClass().getResourceAsStream("orcid-record.xml"); } }); @@ -193,10 +193,10 @@ public InputStream answer(InvocationOnMock invocation) { return getClass().getResourceAsStream("orcid-search.xml"); } }); - when(orcidConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) + when(orcidConnector.get(ArgumentMatchers.matches("^\\d{4}-\\d{4}-\\d{4}-\\d{4}$"), ArgumentMatchers.any())) .thenAnswer(new Answer() { public InputStream answer(InvocationOnMock invocation) { - return getClass().getResourceAsStream("orcid-person-record.xml"); + return getClass().getResourceAsStream("orcid-record.xml"); } }); String q = "orcid:0000-0002-9029-1854"; @@ -246,10 +246,10 @@ public InputStream answer(InvocationOnMock invocation) { return getClass().getResourceAsStream("orcid-search.xml"); } }); - when(orcidConnector.get(ArgumentMatchers.endsWith("/person"), ArgumentMatchers.any())) + when(orcidConnector.get(ArgumentMatchers.matches("^\\d{4}-\\d{4}-\\d{4}-\\d{4}$"), ArgumentMatchers.any())) .thenAnswer(new Answer() { public InputStream answer(InvocationOnMock invocation) { - return getClass().getResourceAsStream("orcid-person-record.xml"); + return getClass().getResourceAsStream("orcid-record.xml"); } }); String q = "family-name:bollini AND given-names:andrea"; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidLoginFilterIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OrcidLoginFilterIT.java index 4b441b1bc8fc..5b167050780f 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 d597b68a550f..93d963db2c8f 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/RelationshipsPositionIndexingIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipsPositionIndexingIT.java index e0ee540c383d..b6652993e0d0 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipsPositionIndexingIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipsPositionIndexingIT.java @@ -65,10 +65,14 @@ public class RelationshipsPositionIndexingIT extends AbstractEntityIntegrationTe private Item publication2; private Item publication3; private Item publication4; + + private Item publication5; private Item project1; private Item project2; private RelationshipType selectedResearchOutputByAuthor; + + private RelationshipType hiddenResearchOutputByAuthor; private RelationshipType selectedResearchOutputByProject; @Autowired @@ -152,6 +156,12 @@ public void setUp() throws Exception { .withIssueDate("2017-08-01") .build(); + publication5 = ItemBuilder.createItem(context, patentCollection) + .withTitle("Publication 5") + .withAuthor("Testzz, Foo") + .withIssueDate("2017-08-01") + .build(); + project1 = ItemBuilder.createItem(context, projectCollection) .withTitle("Project 1") .build(); @@ -170,6 +180,16 @@ public void setUp() throws Exception { 0, null, 0, null).build(); + selectedResearchOutputByAuthor = RelationshipTypeBuilder + .createRelationshipTypeBuilder( + context, + null, + personEntity, + "isResearchoutputsHiddenFor", + "notDisplayingResearchoutputs", + 0, null, + 0, null).build(); + selectedResearchOutputByProject = RelationshipTypeBuilder .createRelationshipTypeBuilder( context, @@ -443,6 +463,42 @@ public void relationPlacesIndexed() throws Exception { configurationService.setProperty("relationship.places.onlyright", ""); } + @Test + public void relationPlacesIndexedShouldFail() throws Exception { + configurationService.setProperty("relationship.places.onlyright", + "null::Person::isResearchoutputsSelectedFor::hasSelectedResearchoutputs"); + context.turnOffAuthorisationSystem(); + final Relationship author1ToPublication1 = + RelationshipBuilder.createRelationshipBuilder(context, publication1, + author1, selectedResearchOutputByAuthor, -1, -1) + .build(); + final Relationship author1ToPublication2 = + RelationshipBuilder.createRelationshipBuilder(context, publication2, author1, + selectedResearchOutputByAuthor, + -1, -1) + .build(); + + final Relationship author1ToPublication3 = + RelationshipBuilder.createRelationshipBuilder(context, publication3, author1, + hiddenResearchOutputByAuthor, + -1, -1) + .build(); + + final Relationship author1ToPublication4 = + RelationshipBuilder.createRelationshipBuilder(context, publication4, author1, + hiddenResearchOutputByAuthor, + -1, -1) + .build(); + context.commit(); + final Relationship author1ToPublication5 = + RelationshipBuilder.createRelationshipBuilder(context, publication5, author1, + selectedResearchOutputByAuthor, + -1, -1) + .build(); + context.commit(); + context.restoreAuthSystemState(); + } + /** * Test to verify scenario when an item is in a relation of the same name * with different entities (i.e. a Publication selected from both a couple of researchers diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java new file mode 100644 index 000000000000..4f8e56f98054 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java @@ -0,0 +1,137 @@ +/** + * 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.dspace.app.matcher.LambdaMatcher.matches; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Optional; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.liveimportclient.service.LiveImportClientImpl; +import org.dspace.importer.external.ror.service.RorImportMetadataSourceServiceImpl; +import org.hamcrest.Matcher; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +public class RorImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { + + @Autowired + private LiveImportClientImpl liveImportClient; + + @Autowired + private RorImportMetadataSourceServiceImpl rorServiceImpl; + + @Test + public void tesGetRecords() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClient.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream file = getClass().getResourceAsStream("ror-records.json")) { + + String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClient.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(jsonResponse, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Collection recordsImported = rorServiceImpl.getRecords("test query", 0, 2); + assertThat(recordsImported, hasSize(10)); + + ImportRecord record = recordsImported.iterator().next(); + + assertThat(record.getValueList(), hasSize(11)); + + assertThat(record.getSingleValue("dc.title"), is("The University of Texas")); + assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/02f6dcw23")); + assertThat(record.getSingleValue("oairecerif.acronym"), is("UTHSCSA")); + assertThat(record.getSingleValue("oairecerif.identifier.url"), is("http://www.uthscsa.edu/")); + assertThat(record.getSingleValue("dc.type"), is("Education")); + assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); + assertThat(record.getSingleValue("organization.foundingDate"), is("1959")); + assertThat(record.getValue("organization", "identifier", "crossrefid"), hasSize(2)); + assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0001 0629 5880")); + assertThat(record.getSingleValue("organization.parentOrganization"), is("The University of Texas System")); + + } finally { + liveImportClient.setHttpClient(originalHttpClient); + } + } + + @Test + public void tesCount() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClient.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream file = getClass().getResourceAsStream("ror-records.json")) { + + String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClient.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(jsonResponse, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Integer count = rorServiceImpl.count("test"); + assertThat(count, equalTo(200)); + } finally { + liveImportClient.setHttpClient(originalHttpClient); + } + } + + @Test + public void tesGetRecord() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClient.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream file = getClass().getResourceAsStream("ror-record.json")) { + + String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClient.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(jsonResponse, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + ImportRecord record = rorServiceImpl.getRecord("https://ror.org/01sps7q28"); + assertThat(record.getValueList(), hasSize(9)); + assertThat(record.getSingleValue("dc.title"), is("The University of Texas Health Science Center at Tyler")); + assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/01sps7q28")); + assertThat(record.getSingleValue("oairecerif.acronym"), is("UTHSCT")); + assertThat(record.getSingleValue("oairecerif.identifier.url"), + is("https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler")); + assertThat(record.getSingleValue("dc.type"), is("Healthcare")); + assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); + assertThat(record.getSingleValue("organization.foundingDate"), is("1947")); + assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0000 9704 5790")); + assertThat(record.getSingleValue("organization.parentOrganization"), is("The University of Texas System")); + + } finally { + liveImportClient.setHttpClient(originalHttpClient); + } + } + + private Matcher> is(String value) { + return matches(optionalValue -> optionalValue.isPresent() && optionalValue.get().equals(value)); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorOrgUnitAuthorityIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorOrgUnitAuthorityIT.java new file mode 100644 index 000000000000..e9a42e78ec38 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorOrgUnitAuthorityIT.java @@ -0,0 +1,47 @@ +/** + * 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.dspace.app.rest.matcher.ItemAuthorityMatcher.matchItemAuthorityWithOtherInformations; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.HashMap; +import java.util.Map; + +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.junit.Test; + +public class RorOrgUnitAuthorityIT extends AbstractControllerIntegrationTest { + + @Test + public void testAuthority() throws Exception { + + Map expectedExtras = new HashMap<>(); + expectedExtras.put("data-ror_orgunit_id", "https://ror.org/02z02cv32"); + expectedExtras.put("ror_orgunit_id", "https://ror.org/02z02cv32"); + expectedExtras.put("data-ror_orgunit_type", "Nonprofit"); + expectedExtras.put("ror_orgunit_type", "Nonprofit"); + expectedExtras.put("data-ror_orgunit_acronym", "WEICan, IEEC"); + expectedExtras.put("ror_orgunit_acronym", "WEICan, IEEC"); + + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/submission/vocabularies/OrgUnitAuthority/entries") + .param("filter", "test")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.entries", hasSize(10))) + .andExpect(jsonPath("$._embedded.entries", + hasItem(matchItemAuthorityWithOtherInformations("will be referenced::ROR-ID::https://ror.org/02z02cv32", + "Wind Energy Institute of Canada", "Wind Energy Institute of Canada", "vocabularyEntry", + expectedExtras)))); + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java index cbcf970547f7..3dc0bbb05098 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java @@ -8,6 +8,7 @@ package org.dspace.app.rest; import static org.dspace.builder.ItemBuilder.createItem; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; @@ -16,6 +17,7 @@ import javax.servlet.ServletException; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.content.Collection; @@ -38,10 +40,22 @@ public class SitemapRestControllerIT extends AbstractControllerIntegrationTest { @Autowired ConfigurationService configurationService; + @Autowired + ResourcePolicyService policyService; + private final static String SITEMAPS_ENDPOINT = "sitemaps"; private Item item1; private Item item2; + private Item itemRestricted; + private Item itemUndiscoverable; + private Item entityPublication; + private Item entityPublicationRestricted; + private Item entityPublicationUndiscoverable; + private Community community; + private Community communityRestricted; + private Collection collection; + private Collection collectionRestricted; @Before @Override @@ -52,8 +66,13 @@ public void setUp() throws Exception { context.turnOffAuthorisationSystem(); - Community community = CommunityBuilder.createCommunity(context).build(); - Collection collection = CollectionBuilder.createCollection(context, community).build(); + community = CommunityBuilder.createCommunity(context).build(); + communityRestricted = CommunityBuilder.createCommunity(context).build(); + policyService.removeAllPolicies(context, communityRestricted); + collection = CollectionBuilder.createCollection(context, community).build(); + collectionRestricted = CollectionBuilder.createCollection(context, community).build(); + policyService.removeAllPolicies(context, collectionRestricted); + this.item1 = createItem(context, collection) .withTitle("Test 1") .withIssueDate("2010-10-17") @@ -62,6 +81,33 @@ public void setUp() throws Exception { .withTitle("Test 2") .withIssueDate("2015-8-3") .build(); + this.itemRestricted = createItem(context, collection) + .withTitle("Test 3") + .withIssueDate("2015-8-3") + .build(); + policyService.removeAllPolicies(context, itemRestricted); + this.itemUndiscoverable = createItem(context, collection) + .withTitle("Test 4") + .withIssueDate("2015-8-3") + .makeUnDiscoverable() + .build(); + this.entityPublication = createItem(context, collection) + .withTitle("Item Publication") + .withEntityType("Publication") + .withIssueDate("2015-8-3") + .build(); + this.entityPublicationRestricted = createItem(context, collection) + .withTitle("Item Publication Restricted") + .withEntityType("Publication") + .withIssueDate("2015-8-3") + .build(); + policyService.removeAllPolicies(context, entityPublicationRestricted); + this.entityPublicationUndiscoverable = createItem(context, collection) + .withTitle("Item Publication") + .withEntityType("Publication") + .withIssueDate("2015-8-3") + .makeUnDiscoverable() + .build(); runDSpaceScript("generate-sitemaps"); @@ -127,9 +173,39 @@ public void testSitemap_sitemap0Html() throws Exception { .andReturn(); String response = result.getResponse().getContentAsString(); + // contains a link to communities: [dspace.ui.url]/communities/ + assertTrue(response + .contains(configurationService.getProperty("dspace.ui.url") + "/communities/" + community.getID())); + // contains a link to collections: [dspace.ui.url]/collections/ + assertTrue(response + .contains(configurationService.getProperty("dspace.ui.url") + "/collections/" + collection.getID())); // contains a link to items: [dspace.ui.url]/items/ assertTrue(response.contains(configurationService.getProperty("dspace.ui.url") + "/items/" + item1.getID())); assertTrue(response.contains(configurationService.getProperty("dspace.ui.url") + "/items/" + item2.getID())); + // contains proper link to entities items + assertTrue(response.contains(configurationService.getProperty("dspace.ui.url") + "/entities/publication/" + + entityPublication.getID())); + assertFalse(response + .contains(configurationService.getProperty("dspace.ui.url") + "/items/" + entityPublication.getID())); + // does not contain links to restricted content + assertFalse(response.contains( + configurationService.getProperty("dspace.ui.url") + "/communities/" + communityRestricted.getID())); + assertFalse(response.contains( + configurationService.getProperty("dspace.ui.url") + "/collections/" + collectionRestricted.getID())); + assertFalse(response + .contains(configurationService.getProperty("dspace.ui.url") + "/items/" + itemRestricted.getID())); + assertFalse(response.contains(configurationService.getProperty("dspace.ui.url") + "/entities/publication/" + + entityPublicationRestricted.getID())); + assertFalse(response.contains( + configurationService.getProperty("dspace.ui.url") + "/items/" + entityPublicationRestricted.getID())); + // does not contain links to undiscoverable content + assertFalse(response + .contains(configurationService.getProperty("dspace.ui.url") + "/items/" + itemUndiscoverable.getID())); + assertFalse(response.contains(configurationService.getProperty("dspace.ui.url") + "/entities/publication/" + + entityPublicationUndiscoverable.getID())); + assertFalse(response.contains(configurationService.getProperty("dspace.ui.url") + "/items/" + + entityPublicationUndiscoverable.getID())); + } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java index 68dee1555f72..b0d740142c9d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java @@ -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-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index c7725805687d..d9d2c0fcf708 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -60,9 +60,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.matchers.JsonPathMatchers; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; +import org.apache.commons.text.StringEscapeUtils; import org.dspace.app.rest.matcher.CollectionMatcher; import org.dspace.app.rest.matcher.ItemMatcher; import org.dspace.app.rest.matcher.MetadataMatcher; @@ -2027,27 +2027,109 @@ public void createSingleWorkspaceItemsFromSingleFileWithMultipleEntriesTest() th Collection col1 = CollectionBuilder.createCollection(context, child1) .withName("Collection 1") .withSubmitterGroup(eperson) + .withEntityType("Publication") + .withSubmissionDefinition("traditional") .build(); Collection col2 = CollectionBuilder.createCollection(context, child1) .withName("Collection 2") .withSubmitterGroup(eperson) + .withEntityType("Publication") + .withSubmissionDefinition("traditional") .build(); - InputStream bibtex = getClass().getResourceAsStream("bibtex-test-3-entries.bib"); - final MockMultipartFile bibtexFile = new MockMultipartFile("file", "bibtex-test-3-entries.bib", - "application/x-bibtex", - bibtex); + try (InputStream bibtex = getClass().getResourceAsStream("bibtex-test-3-entries.bib")) { + final MockMultipartFile bibtexFile = + new MockMultipartFile( + "file", "bibtex-test-3-entries.bib", + "application/x-bibtex", bibtex + ); - context.restoreAuthSystemState(); + context.restoreAuthSystemState(); - String authToken = getAuthToken(eperson.getEmail(), password); - // create a workspaceitem from a single bibliographic entry file explicitly in the default collection (col1) - getClient(authToken).perform(multipart("/api/submission/workspaceitems") - .file(bibtexFile)) - // create should return return a 422 because we don't allow/support bibliographic files - // that have multiple metadata records - .andExpect(status().is(422)); - bibtex.close(); + String authToken = getAuthToken(eperson.getEmail(), password); + // create a workspaceitem from a single bibliographic entry file explicitly in the default collection (col1) + getClient(authToken) + .perform( + multipart("/api/submission/workspaceitems").file(bibtexFile) + ) + // bulk create should return 200, 201 (created) is better for single resource + .andExpect(status().isOk()) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[0].sections.traditionalpageone['dc.title'][0].value", + is("My Article") + ) + ) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[0]._embedded.collection.id", + is(col1.getID().toString()) + ) + ) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[1].sections.traditionalpageone['dc.title'][0].value", + is("My Article 2") + ) + ) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[1]._embedded.collection.id", + is(col1.getID().toString()) + ) + ) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[2].sections.traditionalpageone['dc.title'][0].value", + is("My Article 3") + ) + ) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[2]._embedded.collection.id", + is(col1.getID().toString()) + ) + ) + .andExpect( + jsonPath("$._embedded.workspaceitems[*]._embedded.upload").doesNotExist()); + getClient(authToken) + .perform( + multipart("/api/submission/workspaceitems") + .file(bibtexFile) + .param("owningCollection", col2.getID().toString()) + ) + .andExpect(status().isOk()) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[0].sections.traditionalpageone['dc.title'][0].value", + is("My Article") + ) + ) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[0]._embedded.collection.id", + is(col2.getID().toString()) + ) + ) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[1].sections.traditionalpageone['dc.title'][0].value", + is("My Article 2") + ) + ) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[1]._embedded.collection.id", + is(col2.getID().toString()) + ) + ) + .andExpect( + jsonPath( + "$._embedded.workspaceitems[2].sections.traditionalpageone['dc.title'][0].value", + is("My Article 3") + ) + ); + } } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/converter/RootConverterTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/converter/RootConverterTest.java index acb3b0c263d0..73b12848e790 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/converter/RootConverterTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/converter/RootConverterTest.java @@ -13,6 +13,7 @@ import org.dspace.app.rest.model.RootRest; import org.dspace.app.util.Util; +import org.dspace.core.CrisConstants; import org.dspace.services.ConfigurationService; import org.junit.Before; import org.junit.Test; @@ -52,7 +53,8 @@ public void testCorrectPropertiesSetFromConfigurationService() throws Exception assertEquals("dspaceurl", rootRest.getDspaceUI()); assertEquals("dspacename", rootRest.getDspaceName()); assertEquals(restUrl, rootRest.getDspaceServer()); - assertEquals("DSpace " + Util.getSourceVersion(), rootRest.getDspaceVersion()); + assertEquals(CrisConstants.DSPACE_BASE_VERSION, rootRest.getDspaceVersion()); + assertEquals(Util.getSourceVersion(), rootRest.getCrisVersion()); } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/layout/CrisLayoutTabRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/layout/CrisLayoutTabRestRepositoryIT.java index 80ed84453088..c911253a6124 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/layout/CrisLayoutTabRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/layout/CrisLayoutTabRestRepositoryIT.java @@ -12,8 +12,10 @@ import static org.dspace.app.rest.matcher.CrisLayoutBoxMatcher.matchBox; import static org.dspace.app.rest.matcher.CrisLayoutTabMatcher.matchRest; import static org.dspace.app.rest.matcher.CrisLayoutTabMatcher.matchTab; +import static org.dspace.builder.RelationshipTypeBuilder.createRelationshipTypeBuilder; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; @@ -56,6 +58,7 @@ import org.dspace.builder.EntityTypeBuilder; import org.dspace.builder.GroupBuilder; import org.dspace.builder.ItemBuilder; +import org.dspace.builder.RelationshipBuilder; import org.dspace.content.Bundle; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -63,9 +66,13 @@ import org.dspace.content.Item; import org.dspace.content.MetadataField; import org.dspace.content.MetadataSchema; +import org.dspace.content.Relationship; +import org.dspace.content.RelationshipType; +import org.dspace.content.service.EntityTypeService; import org.dspace.content.service.ItemService; import org.dspace.content.service.MetadataFieldService; import org.dspace.content.service.MetadataSchemaService; +import org.dspace.content.service.RelationshipService; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.layout.CrisLayoutBox; @@ -104,6 +111,12 @@ public class CrisLayoutTabRestRepositoryIT extends AbstractControllerIntegration @Autowired private CrisLayoutTabService crisLayoutTabService; + @Autowired + protected EntityTypeService entityTypeService; + + @Autowired + protected RelationshipService relationshipService; + private final String METADATASECURITY_URL = "http://localhost:8080/api/core/metadatafield/"; /** @@ -1730,6 +1743,196 @@ public void findByItemTabsWithCustomSecurityLayoutAnonynousTest() throws Excepti .andExpect(jsonPath("$._embedded.tabs[0].rows[1].cells[0].boxes", contains(matchBox(box2)))); } + @Test + public void findByItemTabsWithHiddenRelationshipsTest() throws Exception { + context.turnOffAuthorisationSystem(); + + EntityType eType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build(); + + EPerson userA = + EPersonBuilder.createEPerson(context) + .withNameInMetadata("Mecca", "Vincenzo") + .withEmail("vins@4science.com") + .withPassword(password) + .build(); + + Community community = + CommunityBuilder.createCommunity(context) + .withName("Test Community") + .withTitle("Title test community") + .build(); + + Collection col1 = + CollectionBuilder.createCollection(context, community) + .withName("Test Publications") + .build(); + + Collection people = + CollectionBuilder.createCollection(context, community) + .withName("People") + .withEntityType("Person") + .build(); + + Item firstPerson = + ItemBuilder.createItem(context, people) + .withTitle("4Science, Vins") + .build(); + + // RELATION.Person.researchoutputs + CrisLayoutBoxBuilder.createBuilder(context, eType, CrisLayoutBoxTypes.RELATION.name(), true, true) + .withShortname("box-shortname-one") + .build(); + + CrisLayoutBox box1 = + CrisLayoutBoxBuilder.createBuilder(context, eType, CrisLayoutBoxTypes.RELATION.name(), true, true) + .withShortname("researchoutputs") + .withHeader("Publications") + .withSecurity(LayoutSecurity.PUBLIC) + .withType(CrisLayoutBoxTypes.RELATION.name()) + .build(); + + + CrisLayoutBox box2 = + CrisLayoutBoxBuilder.createBuilder(context, eType, true, true) + .withShortname("box-shortname-two") + .withSecurity(LayoutSecurity.PUBLIC) + .build(); + + CrisLayoutFieldBuilder.createMetadataField(context, "dc.title", 0, 0) + .withLabel("LABEL TITLE") + .withRendering("RENDERIGN TITLE") + .withRowStyle("STYLE") + .withBox(box2) + .build(); + + CrisLayoutTab tab = + CrisLayoutTabBuilder.createTab(context, eType, 0) + .withShortName("details") + .withHeader("Profile") + .addBoxIntoNewRow(box2) + .withSecurity(LayoutSecurity.PUBLIC) + .build(); + + CrisLayoutTab tab1 = + CrisLayoutTabBuilder.createTab(context, eType, 0) + .withShortName("publications") + .withHeader("Publications") + .addBoxIntoNewRow(box1) + .withSecurity(LayoutSecurity.PUBLIC) + .build(); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/layout/tabs/search/findByItem") + .param("uuid", firstPerson.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))) + .andExpect(jsonPath("$._embedded.tabs", contains(matchTab(tab)))) + .andExpect(jsonPath("$._embedded.tabs", not(contains(matchTab(tab1))))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", hasSize(1))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", contains(matchBox(box2)))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[1]").doesNotExist()); + + String tokenUserA = getAuthToken(userA.getEmail(), password); + getClient(tokenUserA).perform(get("/api/layout/tabs/search/findByItem") + .param("uuid", firstPerson.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))) + .andExpect(jsonPath("$._embedded.tabs", contains(matchTab(tab)))) + .andExpect(jsonPath("$._embedded.tabs", not(contains(matchTab(tab1))))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", hasSize(1))) + .andExpect( + jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", contains(matchBox(box2))) + ) + .andExpect(jsonPath("$._embedded.tabs[0].rows[1]").doesNotExist()); + + context.turnOffAuthorisationSystem(); + + Item publication1 = + ItemBuilder.createItem(context, col1) + .withTitle("Title Of Item") + .withIssueDate("2015-06-25") + .withAuthor("4Science, Vins", firstPerson.getID().toString()) + .withEntityType("Publication") + .build(); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/layout/tabs/search/findByItem") + .param("uuid", firstPerson.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(2))) + .andExpect(jsonPath("$._embedded.tabs", containsInAnyOrder(matchTab(tab), matchTab(tab1)))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", hasSize(1))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", contains(matchBox(box2)))) + .andExpect(jsonPath("$._embedded.tabs[1].rows[0].cells[0].boxes", hasSize(1))) + .andExpect(jsonPath("$._embedded.tabs[1].rows[0].cells[0].boxes", contains(matchBox(box1)))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[1]").doesNotExist()) + .andExpect(jsonPath("$._embedded.tabs[1].rows[1]").doesNotExist()); + + getClient(tokenUserA).perform(get("/api/layout/tabs/search/findByItem") + .param("uuid", firstPerson.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(2))) + .andExpect(jsonPath("$._embedded.tabs", containsInAnyOrder(matchTab(tab), matchTab(tab1)))) + .andExpect( + jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", hasSize(1))) + .andExpect( + jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", contains(matchBox(box2))) + ) + .andExpect(jsonPath("$._embedded.tabs[1].rows[0].cells[0].boxes", hasSize(1))) + .andExpect( + jsonPath("$._embedded.tabs[1].rows[0].cells[0].boxes", contains(matchBox(box1))) + ) + .andExpect(jsonPath("$._embedded.tabs[0].rows[1]").doesNotExist()) + .andExpect(jsonPath("$._embedded.tabs[1].rows[1]").doesNotExist()); + + context.turnOffAuthorisationSystem(); + + RelationshipType hiddenResearchOutput = + createRelationshipTypeBuilder( + context, null, entityTypeService.findByEntityType(context, "Person"), "isResearchoutputsHiddenFor", + "notDisplayingResearchoutputs", 0, null, 0, null + ).build(); + + final Relationship publicationOneHiddenByFirstPerson = + RelationshipBuilder.createRelationshipBuilder( + context, publication1, firstPerson, hiddenResearchOutput + ).build(); + + context.restoreAuthSystemState(); + try { + getClient().perform(get("/api/layout/tabs/search/findByItem") + .param("uuid", firstPerson.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))) + .andExpect(jsonPath("$._embedded.tabs", not(contains(matchTab(tab1))))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", hasSize(1))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", contains(matchBox(box2)))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[1]").doesNotExist()); + + getClient(tokenUserA).perform(get("/api/layout/tabs/search/findByItem") + .param("uuid", firstPerson.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.page.totalElements", Matchers.is(1))) + .andExpect(jsonPath("$._embedded.tabs", not(contains(matchTab(tab1))))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", hasSize(1))) + .andExpect( + jsonPath("$._embedded.tabs[0].rows[0].cells[0].boxes", contains(matchBox(box2)))) + .andExpect(jsonPath("$._embedded.tabs[0].rows[1]").doesNotExist()); + + } finally { + RelationshipBuilder.deleteRelationship(publicationOneHiddenByFirstPerson.getID()); + } + + } + @Test public void findThumbnailUsingLayoutTabBoxConfiguration() throws Exception { context.turnOffAuthorisationSystem(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ItemAuthorityMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ItemAuthorityMatcher.java index df20e0f65e1f..27e9ffafc1a4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ItemAuthorityMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ItemAuthorityMatcher.java @@ -42,8 +42,9 @@ public static Matcher matchItemAuthorityWithOtherInformations(St hasJsonPath("$.display", is(display)), hasJsonPath("$.value", is(value)), hasJsonPath("$.type", is(type)), - hasJsonPath("$.otherInformation", aMapWithSize(1)), - hasJsonPath("$.otherInformation['" + otherInfMetadata + "']", is(metadataValue)) + hasJsonPath("$.otherInformation", aMapWithSize(2)), + hasJsonPath("$.otherInformation['" + otherInfMetadata + "']", is(metadataValue)), + hasJsonPath("$.otherInformation['" + "data-" + otherInfMetadata + "']", is(metadataValue)) ); } @@ -73,4 +74,18 @@ public static Matcher matchItemAuthorityWithTwoMetadataInOtherIn ) ); } + + public static Matcher matchItemAuthorityWithTwoMetadataInOtherInformations(String authority, + String display, String value, String type, Map orcidAndAffiliation) { + return allOf( + hasJsonPath("$.authority", is(authority)), + hasJsonPath("$.display", is(display)), + hasJsonPath("$.value", is(value)), + hasJsonPath("$.type", is(type)), + hasJsonPath("$.otherInformation", aMapWithSize(4)), + allOf ( + hasJsonPath("$.otherInformation", is(orcidAndAffiliation)) + ) + ); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/authority/CrisConsumerIT.java b/dspace-server-webapp/src/test/java/org/dspace/authority/CrisConsumerIT.java index 0cceb70bb218..3cbbe6850dc9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/authority/CrisConsumerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/authority/CrisConsumerIT.java @@ -22,6 +22,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -92,7 +93,7 @@ public class CrisConsumerIT extends AbstractControllerIntegrationTest { @Value("classpath:org/dspace/app/rest/simple-article.pdf") private Resource simpleArticle; - @Value("classpath:org/dspace/authority/orcid/orcid-person-record.xml") + @Value("classpath:org/dspace/authority/orcid/orcid-record.xml") private Resource orcidPersonRecord; private EPerson submitter; @@ -1058,7 +1059,7 @@ public void testOrcidImportFiller() throws Exception { String orcid = "0000-0002-9029-1854"; - when(mockOrcidConnector.get(eq(orcid + "/person"), any())) + when(mockOrcidConnector.get(matches("^\\d{4}-\\d{4}-\\d{4}-\\d{4}$"), any())) .thenAnswer(i -> orcidPersonRecord.getInputStream()); try { @@ -1076,7 +1077,7 @@ public void testOrcidImportFiller() throws Exception { context.restoreAuthSystemState(); - verify(mockOrcidConnector).get(eq(orcid + "/person"), any()); + verify(mockOrcidConnector).get(eq(orcid), any()); verifyNoMoreInteractions(mockOrcidConnector); String authToken = getAuthToken(submitter.getEmail(), password); diff --git a/dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockDataProvider.java b/dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockDataProvider.java index 894b8e409a4f..0a0b4f062d31 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockDataProvider.java +++ b/dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockDataProvider.java @@ -85,6 +85,7 @@ public void init() throws IOException { externalDataObject.setDisplayValue(id); List list = new LinkedList<>(); list.add(new MetadataValueDTO("dc", "contributor", "author", null, "Donald, Smith")); + list.add(new MetadataValueDTO("dc", "identifier", "doi", null, "10.1016/j.procs.2017.03.031")); externalDataObject.setMetadata(list); mockLookupMap.put(id, externalDataObject); diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/orcid-record.xml b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/orcid-record.xml new file mode 100644 index 000000000000..7672e980c8bd --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/orcid-record.xml @@ -0,0 +1,270 @@ + + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + + en + + + Direct + 2023-09-19T12:25:43.445Z + 2023-10-12T14:19:06.983Z + true + true + true + + + 2023-10-12T13:28:14.550Z + + 2023-09-19T12:25:43.736Z + 2023-09-19T12:25:43.736Z + Andrea + Bollini + + + 2023-10-12T13:28:14.550Z + + 2023-10-05T07:56:29.001Z + 2023-10-12T13:28:14.550Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + isco + + + 2023-10-12T13:28:14.541Z + 2023-10-12T13:28:14.541Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + Bollini, Andrea + + + + 2023-10-12T13:27:57.187Z + + 2023-10-12T10:35:14.406Z + 2023-10-12T13:27:57.187Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + Linkedin + https://it.linkedin.com/in/andreabollini + + + 2023-10-12T13:27:57.183Z + 2023-10-12T13:27:57.183Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + 4science + https://www.4science.it/ + + + + 2023-10-12T10:38:48.105Z + + 2023-10-12T10:33:21.077Z + 2023-10-12T10:38:48.105Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + test-orcid@mailinator.com + + + + + + 2023-09-04T09:04:52.121Z + + 2023-01-13T11:20:13.803Z + 2023-01-13T11:48:02.979Z + + + https://sandbox.orcid.org/client/0000-0002-3609-4817 + 0000-0002-3609-4817 + sandbox.orcid.org + + Scopus Wizard + + Scopus Author ID + 57432999200 + http://www.scopus.com/inward/authorDetails.url?authorID=57432999200&partnerID=MN8TOARS + self + + + 2023-01-19T14:25:14.512Z + 2023-01-19T14:25:14.512Z + + + https://sandbox.orcid.org/client/0000-0002-3609-4817 + 0000-0002-3609-4817 + sandbox.orcid.org + + Scopus Wizard + + Scopus Author ID + 35233141600 + http://www.scopus.com/inward/authorDetails.url?authorID=35233141600&partnerID=MN8TOARS + self + + + + + 2023-10-12T14:19:06.992Z + + + + 2023-10-12T10:52:26.965Z + + 2023-10-12T10:52:26.965Z + + + 2023-10-12T10:52:26.965Z + 2023-10-12T10:52:26.965Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + + Dspace + + Milan + IT + + + + + + 2023-10-12T10:35:49.079Z + + + 2023-10-12T10:34:17.514Z + 2023-10-12T10:35:49.079Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + + 4Science + + Milan + IT + + + https://ror.org/03vb2cr34 + ROR + + + + + + + + + + + + + + 2023-10-12T14:19:06.992Z + + 2023-10-12T14:19:06.992Z + + + doi + 10.1016/j.procs.2014.06.008 + 10.1016/j.procs.2014.06.008 + https://doi.org/10.1016/j.procs.2014.06.008 + self + + + eid + 55484808800 + 55484808800 + self + + + + 2023-10-12T14:09:25.415Z + 2023-10-12T14:19:06.992Z + + + https://sandbox.orcid.org/0000-0002-9029-1854 + 0000-0002-9029-1854 + sandbox.orcid.org + + Andrea Bollini + + + Publication Metadata in CERIF: Inspiration by FRBR + + + + doi + 10.1016/j.procs.2014.06.008 + 10.1016/j.procs.2014.06.008 + https://doi.org/10.1016/j.procs.2014.06.008 + self + + + issn + 1877-0509 + 1877-0509 + https://portal.issn.org/resource/ISSN/1877-0509 + part-of + + + eid + 55484808800 + 55484808800 + self + + + http://dx.doi.org/10.1016/j.procs.2014.06.008 + journal-article + + 2014 + + Procedia Computer Science + + + + + \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json new file mode 100644 index 000000000000..51924485b347 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json @@ -0,0 +1,107 @@ +{ + "id": "https://ror.org/01sps7q28", + "name": "The University of Texas Health Science Center at Tyler", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1947, + "types": [ + "Healthcare" + ], + "relationships": [ + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 32.426014, + "lng": -95.212728, + "state": "Texas", + "state_code": "US-TX", + "city": "Tyler", + "geonames_city": { + "id": 4738214, + "city": "Tyler", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Smith County", + "id": 4729130, + "ascii_name": "Smith County", + "code": "US.TX.423" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler" + ], + "aliases": [ + "East Texas Tuberculosis Sanitarium", + "UT Health Northeast" + ], + "acronyms": [ + "UTHSCT" + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_Tyler", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9704 5790" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "3446655" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q7896437" + ] + }, + "GRID": { + "preferred": "grid.267310.1", + "all": "grid.267310.1" + } + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json new file mode 100644 index 000000000000..91ce8d33e084 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json @@ -0,0 +1,2383 @@ +{ + "number_of_results": 200, + "time_taken": 12, + "items": [ + { + "id": "https://ror.org/02f6dcw23", + "name": "The University of Texas", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1959, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "Audie L. Murphy Memorial VA Hospital", + "type": "Related", + "id": "https://ror.org/035xhk118" + }, + { + "label": "San Antonio Military Medical Center", + "type": "Related", + "id": "https://ror.org/00m1mwc36" + }, + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 29.508129, + "lng": -98.574025, + "state": "Texas", + "state_code": "US-TX", + "city": "San Antonio", + "geonames_city": { + "id": 4726206, + "city": "San Antonio", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Bexar County", + "id": 4674023, + "ascii_name": "Bexar County", + "code": "US.TX.029" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uthscsa.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UTHSCSA" + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_San_Antonio", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 0629 5880" + ] + }, + "FundRef": { + "preferred": "100008635", + "all": [ + "100008635", + "100008636" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "1593427" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q4005868" + ] + }, + "GRID": { + "preferred": "grid.267309.9", + "all": "grid.267309.9" + } + } + }, + { + "id": "https://ror.org/01sps7q28", + "name": "The University of Texas Health Science Center at Tyler", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1947, + "types": [ + "Healthcare" + ], + "relationships": [ + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 32.426014, + "lng": -95.212728, + "state": "Texas", + "state_code": "US-TX", + "city": "Tyler", + "geonames_city": { + "id": 4738214, + "city": "Tyler", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Smith County", + "id": 4729130, + "ascii_name": "Smith County", + "code": "US.TX.423" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler" + ], + "aliases": [ + "East Texas Tuberculosis Sanitarium", + "UT Health Northeast" + ], + "acronyms": [ + "UTHSCT" + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_Tyler", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9704 5790" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "3446655" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q7896437" + ] + }, + "GRID": { + "preferred": "grid.267310.1", + "all": "grid.267310.1" + } + } + }, + { + "id": "https://ror.org/05byvp690", + "name": "The University of Texas Southwestern Medical Center", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1943, + "types": [ + "Healthcare" + ], + "relationships": [ + { + "label": "Children's Medical Center", + "type": "Related", + "id": "https://ror.org/02ndk3y82" + }, + { + "label": "Parkland Memorial Hospital", + "type": "Related", + "id": "https://ror.org/0208r0146" + }, + { + "label": "VA North Texas Health Care System", + "type": "Related", + "id": "https://ror.org/01nzxq896" + }, + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + }, + { + "label": "Institute for Exercise and Environmental Medicine", + "type": "Child", + "id": "https://ror.org/03gqc7y13" + }, + { + "label": "Texas Health Dallas", + "type": "Child", + "id": "https://ror.org/05k07p323" + } + ], + "addresses": [ + { + "lat": 32.812185, + "lng": -96.840174, + "state": "Texas", + "state_code": "US-TX", + "city": "Dallas", + "geonames_city": { + "id": 4684888, + "city": "Dallas", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Dallas County", + "id": 4684904, + "ascii_name": "Dallas County", + "code": "US.TX.113" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.utsouthwestern.edu/" + ], + "aliases": [ + "UT Southwestern" + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Southwestern_Medical_Center", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9482 7121" + ] + }, + "FundRef": { + "preferred": "100007914", + "all": [ + "100007914", + "100010487", + "100008260" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "617906" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2725999" + ] + }, + "GRID": { + "preferred": "grid.267313.2", + "all": "grid.267313.2" + } + } + }, + { + "id": "https://ror.org/019kgqr73", + "name": "The University of Texas at Arlington", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1895, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "VA North Texas Health Care System", + "type": "Related", + "id": "https://ror.org/01nzxq896" + }, + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 32.731, + "lng": -97.115, + "state": "Texas", + "state_code": "US-TX", + "city": "Arlington", + "geonames_city": { + "id": 4671240, + "city": "Arlington", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Tarrant County", + "id": 4735638, + "ascii_name": "Tarrant County", + "code": "US.TX.439" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uta.edu/uta/" + ], + "aliases": [ + "UT Arlington" + ], + "acronyms": [ + "UTA" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Texas_at_Arlington", + "labels": [ + { + "label": "Université du Texas à Arlington", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2181 9515" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100009497" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "906409" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q1230739" + ] + }, + "GRID": { + "preferred": "grid.267315.4", + "all": "grid.267315.4" + } + } + }, + { + "id": "https://ror.org/051smbs96", + "name": "The University of Texas of the Permian Basin", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1973, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 31.889444, + "lng": -102.329531, + "state": "Texas", + "state_code": "US-TX", + "city": "Odessa", + "geonames_city": { + "id": 5527554, + "city": "Odessa", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Ector County", + "id": 5520910, + "ascii_name": "Ector County", + "code": "US.TX.135" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.utpb.edu/" + ], + "aliases": [ + "UT Permian Basin" + ], + "acronyms": [ + "UTPB" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Texas_of_the_Permian_Basin", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9140 1491" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "1419441" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2495935" + ] + }, + "GRID": { + "preferred": "grid.267328.a", + "all": "grid.267328.a" + } + } + }, + { + "id": "https://ror.org/044vy1d05", + "name": "Tokushima University", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1949, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "Tokushima University Hospital", + "type": "Related", + "id": "https://ror.org/021ph5e41" + } + ], + "addresses": [ + { + "lat": 34.07, + "lng": 134.56, + "state": null, + "state_code": null, + "city": "Tokushima", + "geonames_city": { + "id": 1850158, + "city": "Tokushima", + "geonames_admin1": { + "name": "Tokushima", + "id": 1850157, + "ascii_name": "Tokushima", + "code": "JP.39" + }, + "geonames_admin2": { + "name": "Tokushima Shi", + "id": 1850156, + "ascii_name": "Tokushima Shi", + "code": "JP.39.1850156" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 1861060 + } + ], + "links": [ + "https://www.tokushima-u.ac.jp/" + ], + "aliases": [ + "Tokushima Daigaku", + "University of Tokushima" + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Tokushima", + "labels": [ + { + "label": "徳島大学", + "iso639": "ja" + } + ], + "country": { + "country_name": "Japan", + "country_code": "JP" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 1092 3579" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "501100005623" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "15696836" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q1150231" + ] + }, + "GRID": { + "preferred": "grid.267335.6", + "all": "grid.267335.6" + } + } + }, + { + "id": "https://ror.org/03np13864", + "name": "University of Trinidad and Tobago", + "email_address": null, + "ip_addresses": [ + + ], + "established": 2004, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 10.616667, + "lng": -61.216667, + "state": null, + "state_code": null, + "city": "Arima", + "geonames_city": { + "id": 3575051, + "city": "Arima", + "geonames_admin1": { + "name": "Borough of Arima", + "id": 3575052, + "ascii_name": "Borough of Arima", + "code": "TT.01" + }, + "geonames_admin2": { + "name": null, + "id": null, + "ascii_name": null, + "code": null + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 3573591 + } + ], + "links": [ + "https://utt.edu.tt/" + ], + "aliases": [ + + ], + "acronyms": [ + "UTT" + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Trinidad_and_Tobago", + "labels": [ + { + "label": "Universidad de Trinidad y Tobago", + "iso639": "es" + } + ], + "country": { + "country_name": "Trinidad and Tobago", + "country_code": "TT" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9490 0886" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "8706288" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q648244" + ] + }, + "GRID": { + "preferred": "grid.267355.0", + "all": "grid.267355.0" + } + } + }, + { + "id": "https://ror.org/04wn28048", + "name": "University of Tulsa", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1894, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 36.152222, + "lng": -95.946389, + "state": "Oklahoma", + "state_code": "US-OK", + "city": "Tulsa", + "geonames_city": { + "id": 4553433, + "city": "Tulsa", + "geonames_admin1": { + "name": "Oklahoma", + "id": 4544379, + "ascii_name": "Oklahoma", + "code": "US.OK" + }, + "geonames_admin2": { + "name": "Tulsa County", + "id": 4553440, + "ascii_name": "Tulsa County", + "code": "US.OK.143" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://utulsa.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "TU" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Tulsa", + "labels": [ + { + "label": "Université de tulsa", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2160 264X" + ] + }, + "FundRef": { + "preferred": "100007147", + "all": [ + "100007147", + "100006455" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "32043" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q1848657" + ] + }, + "GRID": { + "preferred": "grid.267360.6", + "all": "grid.267360.6" + } + } + }, + { + "id": "https://ror.org/04scfb908", + "name": "Alfred Health", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1871, + "types": [ + "Healthcare" + ], + "relationships": [ + { + "label": "Caulfield Hospital", + "type": "Child", + "id": "https://ror.org/01fcxf261" + }, + { + "label": "Melbourne Sexual Health Centre", + "type": "Child", + "id": "https://ror.org/013fdz725" + }, + { + "label": "National Trauma Research Institute", + "type": "Child", + "id": "https://ror.org/048t93218" + }, + { + "label": "The Alfred Hospital", + "type": "Child", + "id": "https://ror.org/01wddqe20" + } + ], + "addresses": [ + { + "lat": -37.845542, + "lng": 144.981632, + "state": "Victoria", + "state_code": "AU-VIC", + "city": "Melbourne", + "geonames_city": { + "id": 2158177, + "city": "Melbourne", + "geonames_admin1": { + "name": "Victoria", + "id": 2145234, + "ascii_name": "Victoria", + "code": "AU.07" + }, + "geonames_admin2": { + "name": "Melbourne", + "id": 7839805, + "ascii_name": "Melbourne", + "code": "AU.07.24600" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 2077456 + } + ], + "links": [ + "http://www.alfred.org.au/" + ], + "aliases": [ + + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "", + "labels": [ + + ], + "country": { + "country_name": "Australia", + "country_code": "AU" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0004 0432 5259" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "501100002716" + ] + }, + "GRID": { + "preferred": "grid.267362.4", + "all": "grid.267362.4" + } + } + }, + { + "id": "https://ror.org/02c2f8975", + "name": "University of Ulsan", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1970, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "Ulsan University Hospital", + "type": "Related", + "id": "https://ror.org/03sab2a45" + } + ], + "addresses": [ + { + "lat": 35.542772, + "lng": 129.256725, + "state": null, + "state_code": null, + "city": "Ulsan", + "geonames_city": { + "id": 1833747, + "city": "Ulsan", + "geonames_admin1": { + "name": "Ulsan", + "id": 1833742, + "ascii_name": "Ulsan", + "code": "KR.21" + }, + "geonames_admin2": { + "name": null, + "id": null, + "ascii_name": null, + "code": null + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 1835841 + } + ], + "links": [ + "http://en.ulsan.ac.kr/contents/main/" + ], + "aliases": [ + + ], + "acronyms": [ + "UOU" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Ulsan", + "labels": [ + { + "label": "울산대학교", + "iso639": "ko" + } + ], + "country": { + "country_name": "South Korea", + "country_code": "KR" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0004 0533 4667" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "501100002568" + ] + }, + "OrgRef": { + "preferred": "10458246", + "all": [ + "10458246", + "15162872" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q491717" + ] + }, + "GRID": { + "preferred": "grid.267370.7", + "all": "grid.267370.7" + } + } + }, + { + "id": "https://ror.org/010acrp16", + "name": "University of West Alabama", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1835, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 32.59, + "lng": -88.186, + "state": "Alabama", + "state_code": "US-AL", + "city": "Livingston", + "geonames_city": { + "id": 4073383, + "city": "Livingston", + "geonames_admin1": { + "name": "Alabama", + "id": 4829764, + "ascii_name": "Alabama", + "code": "US.AL" + }, + "geonames_admin2": { + "name": "Sumter County", + "id": 4092386, + "ascii_name": "Sumter County", + "code": "US.AL.119" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uwa.edu/" + ], + "aliases": [ + "Livingston Female Academy" + ], + "acronyms": [ + "UWA" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Alabama", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9963 9197" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "2425212" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q637346" + ] + }, + "GRID": { + "preferred": "grid.267434.0", + "all": "grid.267434.0" + } + } + }, + { + "id": "https://ror.org/002w4zy91", + "name": "University of West Florida", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1963, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "State University System of Florida", + "type": "Parent", + "id": "https://ror.org/05sqd3t97" + } + ], + "addresses": [ + { + "lat": 30.549493, + "lng": -87.21812, + "state": "Florida", + "state_code": "US-FL", + "city": "Pensacola", + "geonames_city": { + "id": 4168228, + "city": "Pensacola", + "geonames_admin1": { + "name": "Florida", + "id": 4155751, + "ascii_name": "Florida", + "code": "US.FL" + }, + "geonames_admin2": { + "name": "Escambia County", + "id": 4154550, + "ascii_name": "Escambia County", + "code": "US.FL.033" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://uwf.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UWF" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Florida", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2112 2427" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100009842" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "750756" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q659255" + ] + }, + "GRID": { + "preferred": "grid.267436.2", + "all": "grid.267436.2" + } + } + }, + { + "id": "https://ror.org/01cqxk816", + "name": "University of West Georgia", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1906, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "University System of Georgia", + "type": "Parent", + "id": "https://ror.org/017wcm924" + } + ], + "addresses": [ + { + "lat": 33.573357, + "lng": -85.099593, + "state": "Georgia", + "state_code": "US-GA", + "city": "Carrollton", + "geonames_city": { + "id": 4186416, + "city": "Carrollton", + "geonames_admin1": { + "name": "Georgia", + "id": 4197000, + "ascii_name": "Georgia", + "code": "US.GA" + }, + "geonames_admin2": { + "name": "Carroll County", + "id": 4186396, + "ascii_name": "Carroll County", + "code": "US.GA.045" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.westga.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UWG" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Georgia", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2223 6696" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100007922" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "595315" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2495945" + ] + }, + "GRID": { + "preferred": "grid.267437.3", + "all": "grid.267437.3" + } + } + }, + { + "id": "https://ror.org/03c8vvr84", + "name": "University of Western States", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1904, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 45.543351, + "lng": -122.523973, + "state": "Oregon", + "state_code": "US-OR", + "city": "Portland", + "geonames_city": { + "id": 5746545, + "city": "Portland", + "geonames_admin1": { + "name": "Oregon", + "id": 5744337, + "ascii_name": "Oregon", + "code": "US.OR" + }, + "geonames_admin2": { + "name": "Multnomah County", + "id": 5742126, + "ascii_name": "Multnomah County", + "code": "US.OR.051" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uws.edu/" + ], + "aliases": [ + "Western States Chiropractic College" + ], + "acronyms": [ + "UWS" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Western_States", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0004 0455 9493" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "1655050" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q7896612" + ] + }, + "GRID": { + "preferred": "grid.267451.3", + "all": "grid.267451.3" + } + } + }, + { + "id": "https://ror.org/03fmjzx88", + "name": "University of Winchester", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1840, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 51.060338, + "lng": -1.325418, + "state": null, + "state_code": null, + "city": "Winchester", + "geonames_city": { + "id": 2633858, + "city": "Winchester", + "geonames_admin1": { + "name": "England", + "id": 6269131, + "ascii_name": "England", + "code": "GB.ENG" + }, + "geonames_admin2": { + "name": "Hampshire", + "id": 2647554, + "ascii_name": "Hampshire", + "code": "GB.ENG.F2" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": "SOUTH EAST (ENGLAND)", + "code": "UKJ" + }, + "nuts_level2": { + "name": "Hampshire and Isle of Wight", + "code": "UKJ3" + }, + "nuts_level3": { + "name": "Central Hampshire", + "code": "UKJ36" + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 2635167 + } + ], + "links": [ + "http://www.winchester.ac.uk/pages/home.aspx" + ], + "aliases": [ + + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Winchester", + "labels": [ + + ], + "country": { + "country_name": "United Kingdom", + "country_code": "GB" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9422 2878" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100010057" + ] + }, + "HESA": { + "preferred": null, + "all": [ + "0021" + ] + }, + "UCAS": { + "preferred": null, + "all": [ + "W76" + ] + }, + "UKPRN": { + "preferred": null, + "all": [ + "10003614" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "3140939" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q3551690" + ] + }, + "GRID": { + "preferred": "grid.267454.6", + "all": "grid.267454.6" + } + } + }, + { + "id": "https://ror.org/01gw3d370", + "name": "University of Windsor", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1857, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 42.305196, + "lng": -83.067483, + "state": "Ontario", + "state_code": "CA-ON", + "city": "Windsor", + "geonames_city": { + "id": 6182962, + "city": "Windsor", + "geonames_admin1": { + "name": "Ontario", + "id": 6093943, + "ascii_name": "Ontario", + "code": "CA.08" + }, + "geonames_admin2": { + "name": null, + "id": null, + "ascii_name": null, + "code": null + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6251999 + } + ], + "links": [ + "http://www.uwindsor.ca/" + ], + "aliases": [ + "UWindsor", + "Assumption University of Windsor" + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Windsor", + "labels": [ + { + "label": "Université de windsor", + "iso639": "fr" + } + ], + "country": { + "country_name": "Canada", + "country_code": "CA" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0004 1936 9596" + ] + }, + "FundRef": { + "preferred": "100009154", + "all": [ + "100009154", + "501100000083" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "342733" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2065769" + ] + }, + "GRID": { + "preferred": "grid.267455.7", + "all": "grid.267455.7" + } + } + }, + { + "id": "https://ror.org/02gdzyx04", + "name": "University of Winnipeg", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1871, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "Winnipeg Institute for Theoretical Physics", + "type": "Child", + "id": "https://ror.org/010tw2j24" + } + ], + "addresses": [ + { + "lat": 49.890122, + "lng": -97.153367, + "state": "Manitoba", + "state_code": "CA-MB", + "city": "Winnipeg", + "geonames_city": { + "id": 6183235, + "city": "Winnipeg", + "geonames_admin1": { + "name": "Manitoba", + "id": 6065171, + "ascii_name": "Manitoba", + "code": "CA.03" + }, + "geonames_admin2": { + "name": null, + "id": null, + "ascii_name": null, + "code": null + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6251999 + } + ], + "links": [ + "http://www.uwinnipeg.ca/" + ], + "aliases": [ + + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Winnipeg", + "labels": [ + { + "label": "Université de winnipeg", + "iso639": "fr" + } + ], + "country": { + "country_name": "Canada", + "country_code": "CA" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 1703 4731" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100009367" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "587404" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q472167" + ] + }, + "GRID": { + "preferred": "grid.267457.5", + "all": "grid.267457.5" + } + } + }, + { + "id": "https://ror.org/03mnm0t94", + "name": "University of Wisconsin–Eau Claire", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1916, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "University of Wisconsin System", + "type": "Parent", + "id": "https://ror.org/03ydkyb10" + } + ], + "addresses": [ + { + "lat": 44.79895, + "lng": -91.499346, + "state": "Wisconsin", + "state_code": "US-WI", + "city": "Eau Claire", + "geonames_city": { + "id": 5251436, + "city": "Eau Claire", + "geonames_admin1": { + "name": "Wisconsin", + "id": 5279468, + "ascii_name": "Wisconsin", + "code": "US.WI" + }, + "geonames_admin2": { + "name": "Eau Claire County", + "id": 5251439, + "ascii_name": "Eau Claire County", + "code": "US.WI.035" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uwec.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UWEC" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93Eau_Claire", + "labels": [ + { + "label": "Université du Wisconsin à Eau Claire", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2227 2494" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100010315" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "496729" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q3551771" + ] + }, + "GRID": { + "preferred": "grid.267460.1", + "all": "grid.267460.1" + } + } + }, + { + "id": "https://ror.org/05hbexn54", + "name": "University of Wisconsin–Green Bay", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1965, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "University of Wisconsin System", + "type": "Parent", + "id": "https://ror.org/03ydkyb10" + } + ], + "addresses": [ + { + "lat": 44.533203, + "lng": -87.921521, + "state": "Wisconsin", + "state_code": "US-WI", + "city": "Green Bay", + "geonames_city": { + "id": 5254962, + "city": "Green Bay", + "geonames_admin1": { + "name": "Wisconsin", + "id": 5279468, + "ascii_name": "Wisconsin", + "code": "US.WI" + }, + "geonames_admin2": { + "name": "Brown County", + "id": 5246898, + "ascii_name": "Brown County", + "code": "US.WI.009" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uwgb.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UWGB" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93Green_Bay", + "labels": [ + { + "label": "Université du Wisconsin–Green Bay", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 0559 7692" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "1513886" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2378091" + ] + }, + "GRID": { + "preferred": "grid.267461.0", + "all": "grid.267461.0" + } + } + }, + { + "id": "https://ror.org/00x8ccz20", + "name": "University of Wisconsin–La Crosse", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1909, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "University of Wisconsin System", + "type": "Parent", + "id": "https://ror.org/03ydkyb10" + } + ], + "addresses": [ + { + "lat": 43.815576, + "lng": -91.233517, + "state": "Wisconsin", + "state_code": "US-WI", + "city": "La Crosse", + "geonames_city": { + "id": 5258957, + "city": "La Crosse", + "geonames_admin1": { + "name": "Wisconsin", + "id": 5279468, + "ascii_name": "Wisconsin", + "code": "US.WI" + }, + "geonames_admin2": { + "name": "La Crosse County", + "id": 5258961, + "ascii_name": "La Crosse County", + "code": "US.WI.063" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uwlax.edu/Home/Future-Students/" + ], + "aliases": [ + + ], + "acronyms": [ + "UW–L" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93La_Crosse", + "labels": [ + { + "label": "Université du Wisconsin–La Crosse", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2169 5137" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "2422287" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2688358" + ] + }, + "GRID": { + "preferred": "grid.267462.3", + "all": "grid.267462.3" + } + } + } + ], + "meta": { + "types": [ + { + "id": "company", + "title": "Company", + "count": 29790 + }, + { + "id": "education", + "title": "Education", + "count": 20325 + }, + { + "id": "nonprofit", + "title": "Nonprofit", + "count": 14187 + }, + { + "id": "healthcare", + "title": "Healthcare", + "count": 13107 + }, + { + "id": "facility", + "title": "Facility", + "count": 10080 + }, + { + "id": "other", + "title": "Other", + "count": 8369 + }, + { + "id": "government", + "title": "Government", + "count": 6511 + }, + { + "id": "archive", + "title": "Archive", + "count": 2967 + } + ], + "countries": [ + { + "id": "us", + "title": "United States", + "count": 31196 + }, + { + "id": "gb", + "title": "United Kingdom", + "count": 7410 + }, + { + "id": "de", + "title": "Germany", + "count": 5189 + }, + { + "id": "cn", + "title": "China", + "count": 4846 + }, + { + "id": "fr", + "title": "France", + "count": 4344 + }, + { + "id": "jp", + "title": "Japan", + "count": 3940 + }, + { + "id": "ca", + "title": "Canada", + "count": 3392 + }, + { + "id": "in", + "title": "India", + "count": 3075 + }, + { + "id": "cz", + "title": "Czech Republic", + "count": 2780 + }, + { + "id": "ru", + "title": "Russia", + "count": 2109 + } + ], + "statuses": [ + { + "id": "active", + "title": "active", + "count": 105336 + } + ] + } +} \ No newline at end of file diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index 8cb88f45781d..eab37034a510 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 16f8a396fbee..eb2ae6288a22 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 b35fb7388a15..908252119458 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 35e759cd6860..33aa97437dda 100644 --- a/dspace/config/crosswalks/mapConverter-scopusToCoarPublicationTypes.properties +++ b/dspace/config/crosswalks/mapConverter-scopusToCoarPublicationTypes.properties @@ -1,14 +1,14 @@ ar = Resource Types::text::journal::journal article er = Resource Types::text::journal::journal article::corrigendum re = Resource Types::text::journal::journal article::review article -cp = Resource Types::text::conference outputs::conference proceedings::conference paper +cp = Resource Types::text::conference output::conference proceedings::conference paper bk = Resource Types::text::book ch = Resource Types::text::book chapter ed = Resource Types::text::journal::editorial le = Resource Types::text::letter -cr = Conference Review -ab = Abstract Report -bz = Business Article -no = Note -pr = Press Release -sh = Short Survey \ No newline at end of file +cr = Resource Types::text::review +ab = Resource Types::text::report +bz = Resource Types::text::journal::journal article +no = Resource Types::text +pr = Resource Types::text +sh = Resource Types::text \ No newline at end of file diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 7b66eaf04372..3bc1867277ab 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 22b400e96926..9f31693eb4e2 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 50224aa35cca..414527ee3378 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 22b400e96926..9f31693eb4e2 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 7db34a16b978..ffec78c450a2 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -47,6 +47,9 @@ dspace.name = DSpace at My University # Default language for metadata values default.language = en_US +# Url of subscriptions page +subscription.url = ${dspace.ui.url}/subscriptions + # Solr server/webapp. # DSpace uses Solr for all search/browse capability (and for usage statistics). # Since DSpace 7, SOLR must be installed as a stand-alone service. @@ -281,6 +284,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 @@ -794,7 +801,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 +881,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 +1869,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 +1999,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 000000000000..f2cd1f50c02c --- /dev/null +++ b/dspace/config/emails/orcid @@ -0,0 +1,22 @@ +## E-mail sent to DSpace users when they try to register with an ORCID account +## +## Parameters: {0} is expanded to a special registration URL +## +## See org.dspace.core.Email for information on the format of this file. +## +#set($subject = "${config.get('dspace.name')} Account Registration") +#set($phone = ${config.get('mail.message.helpdesk.telephone')}) +To complete registration for a DSpace account, please click the link +below: + + ${params[0]} + +If you need assistance with your account, please email + + ${config.get("mail.helpdesk")} +#if( $phone ) + +or call us at ${phone}. +#end + +The DSpace-CRIS Team diff --git a/dspace/config/emails/subscriptions_content b/dspace/config/emails/subscriptions_content index fc186abbb0b9..d29f6d3debb4 100644 --- a/dspace/config/emails/subscriptions_content +++ b/dspace/config/emails/subscriptions_content @@ -1,19 +1,23 @@ ## E-mail sent to designated address about updates on subscribed items ## -## Parameters: {0} Collections updates -## {1} Communities updates -## {2} Items updates +## Parameters: {0} Link to subscriptions page +## {1} Collections updates block +## {2} Communities updates block +## {3} Entity updates block This email is sent from DSpace-CRIS based on the chosen subscription preferences. +You can manage your subscription preferences from ${params[0]} Communities ------------ -List of changed items : ${params[0]} +------------------- +${params[1]} + Collections ------------ -List of changed items : ${params[1]} +------------------- +${params[2]} + -Items ------ -List of changed items : ${params[2]} \ No newline at end of file +Entities +------------------- +${params[3]} \ No newline at end of file diff --git a/dspace/config/emails/validation_orcid b/dspace/config/emails/validation_orcid new file mode 100644 index 000000000000..ec11b708ec5d --- /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 563fd86735bc..8597bedbc34a 100644 --- a/dspace/config/hibernate.cfg.xml +++ b/dspace/config/hibernate.cfg.xml @@ -69,6 +69,7 @@ +