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 fbb904f8084e..6b1e6d42a095 100644 --- a/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImport.java +++ b/dspace-api/src/main/java/org/dspace/app/bulkedit/BulkImport.java @@ -13,7 +13,7 @@ import static org.apache.commons.lang3.StringUtils.isAllBlank; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; -import static org.apache.commons.lang3.StringUtils.split; +import static org.apache.commons.lang3.StringUtils.splitByWholeSeparator; import static org.apache.commons.lang3.StringUtils.startsWith; import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage; import static org.apache.commons.lang3.math.NumberUtils.isCreatable; @@ -609,7 +609,8 @@ private boolean areMetadataValuesValid(Row row, boolean manyMetadataValuesAllowe for (int index = firstMetadataIndex; index < row.getLastCellNum(); index++) { String cellValue = WorkbookUtils.getCellValue(row, index); - String[] values = isNotBlank(cellValue) ? split(cellValue, METADATA_SEPARATOR) : new String[] { "" }; + String[] values = isNotBlank(cellValue) ? splitByWholeSeparator(cellValue, METADATA_SEPARATOR) + : new String[] { "" }; if (values.length > 1 && !manyMetadataValuesAllowed) { handleValidationErrorOnRow(row, "Multiple metadata value on the same cell not allowed " + "in the metadata group sheets: " + cellValue); @@ -743,7 +744,7 @@ private List validateAccessConditions(Row row) { Map accessConditionOptions = getUploadAccessConditions(); return Arrays.stream(getAccessConditionValues(row)) - .map(accessCondition -> split(accessCondition, ACCESS_CONDITION_ATTRIBUTES_SEPARATOR)[0]) + .map(accessCondition -> splitByWholeSeparator(accessCondition, ACCESS_CONDITION_ATTRIBUTES_SEPARATOR)[0]) .filter(accessConditionName -> !accessConditionOptions.containsKey(accessConditionName)) .collect(Collectors.toList()); } @@ -788,14 +789,14 @@ private List buildAccessConditions(Row row, String[] accessCond } return Arrays.stream(accessConditions) - .map(accessCondition -> split(accessCondition, ACCESS_CONDITION_ATTRIBUTES_SEPARATOR)) + .map(accessCondition -> splitByWholeSeparator(accessCondition, ACCESS_CONDITION_ATTRIBUTES_SEPARATOR)) .map(accessConditionAttributes -> buildAccessCondition(accessConditionAttributes)) .collect(Collectors.toList()); } private String[] getAccessConditionValues(Row row) { String accessConditionCellValue = getCellValue(row, ACCESS_CONDITION_HEADER); - return split(accessConditionCellValue, METADATA_SEPARATOR); + return splitByWholeSeparator(accessConditionCellValue, METADATA_SEPARATOR); } private AccessCondition buildAccessCondition(String[] accessCondition) { @@ -1158,9 +1159,6 @@ private Item updateItem(EntityRow entityRow, Item item) handler.logInfo("Row " + entityRow.getRow() + " - Item updated successfully - ID: " + item.getID()); switch (entityRow.getAction()) { - case UPDATE: - itemService.update(context, item); - break; case UPDATE_WORKFLOW: startWorkflow(entityRow, item); break; @@ -1168,6 +1166,7 @@ private Item updateItem(EntityRow entityRow, Item item) installItem(entityRow, item); break; default: + itemService.update(context, item); break; } @@ -1308,12 +1307,13 @@ private void removeSingleMetadata(DSpaceObject dso, MetadataField field, String } private String getMetadataField(String field) { - return field.contains(LANGUAGE_SEPARATOR_PREFIX) ? split(field, LANGUAGE_SEPARATOR_PREFIX)[0] : field; + return field.contains(LANGUAGE_SEPARATOR_PREFIX) ? splitByWholeSeparator(field, LANGUAGE_SEPARATOR_PREFIX)[0] + : field; } private String getMetadataLanguage(String field) { if (field.contains(LANGUAGE_SEPARATOR_PREFIX)) { - return split(field, LANGUAGE_SEPARATOR_PREFIX)[1].replace(LANGUAGE_SEPARATOR_SUFFIX, ""); + return splitByWholeSeparator(field, LANGUAGE_SEPARATOR_PREFIX)[1].replace(LANGUAGE_SEPARATOR_SUFFIX, ""); } return null; } @@ -1366,7 +1366,8 @@ private MultiValuedMap getMetadataFromRow(Row row, Map< if (index >= firstMetadataIndex) { String cellValue = WorkbookUtils.getCellValue(row, index); - String[] values = isNotBlank(cellValue) ? split(cellValue, METADATA_SEPARATOR) : new String[] { "" }; + String[] values = isNotBlank(cellValue) ? splitByWholeSeparator(cellValue, METADATA_SEPARATOR) + : new String[] { "" }; List metadataValues = Arrays.stream(values) .map(value -> buildMetadataValueVO(row, value, isMetadataGroupsSheet)) diff --git a/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java b/dspace-api/src/main/java/org/dspace/app/bulkimport/model/BulkImportSheet.java index 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/mediafilter/BrandedPreviewJPEGFilter.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java index 7b082c6c21a4..7073f4f09776 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/BrandedPreviewJPEGFilter.java @@ -22,7 +22,7 @@ * * @author Jason Sherman jsherman@usao.edu */ -public class BrandedPreviewJPEGFilter extends MediaFilter { +public class BrandedPreviewJPEGFilter extends JPEGFilter { @Override public String getFilteredName(String oldFilename) { return oldFilename + ".preview.jpg"; @@ -36,14 +36,6 @@ public String getBundleName() { return "BRANDED_PREVIEW"; } - /** - * @return String bitstreamformat - */ - @Override - public String getFormatString() { - return "JPEG"; - } - /** * @return String description */ @@ -81,9 +73,7 @@ public InputStream getDestinationStream(Item currentItem, InputStream source, bo String brandFont = configurationService.getProperty("webui.preview.brand.font"); int brandFontPoint = configurationService.getIntProperty("webui.preview.brand.fontpoint"); - JPEGFilter jpegFilter = new JPEGFilter(); - return jpegFilter - .getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, + return getThumbDim(currentItem, buf, verbose, xmax, ymax, blurring, hqscaling, brandHeight, brandFontPoint, brandFont); } } 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/nbevent/service/impl/NBEventServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/nbevent/service/impl/NBEventServiceImpl.java index 4df11b054e67..e07fb16c72ca 100644 --- a/dspace-api/src/main/java/org/dspace/app/nbevent/service/impl/NBEventServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/nbevent/service/impl/NBEventServiceImpl.java @@ -8,14 +8,17 @@ package org.dspace.app.nbevent.service.impl; import java.io.IOException; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Date; +import java.util.Iterator; import java.util.List; import java.util.UUID; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.solr.client.solrj.SolrClient; @@ -33,6 +36,7 @@ import org.dspace.app.nbevent.NBTopic; import org.dspace.app.nbevent.dao.impl.NBEventsDaoImpl; import org.dspace.app.nbevent.service.NBEventService; +import org.dspace.authorize.AuthorizeException; import org.dspace.content.Item; import org.dspace.content.NBEvent; import org.dspace.content.service.ItemService; @@ -316,18 +320,38 @@ private String getResourceUUID(Context context, String originalId) throws Except String id = getHandleFromOriginalId(originalId); if (id != null) { Item item = (Item) handleService.resolveToObject(context, id); + if (item != null) { final String itemUuid = item.getID().toString(); context.uncacheEntity(item); return itemUuid; } else { - return null; + item = fromLegacyIdentifier(context, originalId); + return item == null ? null : item.getID().toString(); } } else { throw new RuntimeException("Malformed originalId " + originalId); } } + private Item fromLegacyIdentifier(Context context, String legacyIdentifier) { + if (StringUtils.isBlank(legacyIdentifier)) { + return null; + } + try { + Iterator + iterator = itemService.findUnfilteredByMetadataField( + context, "dspace", "legacy", "oai-identifier", + legacyIdentifier); + if (!iterator.hasNext()) { + return null; + } + return iterator.next(); + } catch (AuthorizeException | SQLException e) { + throw new RuntimeException(e); + } + } + // oai:www.openstarts.units.it:10077/21486 private String getHandleFromOriginalId(String originalId) { Integer startPosition = originalId.lastIndexOf(':'); diff --git a/dspace-api/src/main/java/org/dspace/app/util/SubmissionStepConfig.java b/dspace-api/src/main/java/org/dspace/app/util/SubmissionStepConfig.java index 7f29e6d79e52..03d5dc6b7657 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/SubmissionStepConfig.java +++ b/dspace-api/src/main/java/org/dspace/app/util/SubmissionStepConfig.java @@ -11,6 +11,9 @@ import java.util.Map; import org.apache.commons.lang3.BooleanUtils; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.WorkspaceItem; +import org.hibernate.proxy.HibernateProxyHelper; /** * Class representing configuration for a single step within an Item Submission @@ -179,6 +182,45 @@ public String getVisibilityOutside() { return visibilityOutside; } + public boolean isHiddenForInProgressSubmission(InProgressSubmission obj) { + + String scopeToCheck = getScope(obj); + + if (scope == null || scopeToCheck == null) { + return false; + } + + String visibility = getVisibility(); + String visibilityOutside = getVisibilityOutside(); + + if (scope.equalsIgnoreCase(scopeToCheck)) { + return "hidden".equalsIgnoreCase(visibility); + } else { + return visibilityOutside == null || "hidden".equalsIgnoreCase(visibilityOutside); + } + + } + + public boolean isReadOnlyForInProgressSubmission(InProgressSubmission obj) { + + String scopeToCheck = getScope(obj); + + if (scope == null || scopeToCheck == null) { + return false; + } + + String visibility = scope.equalsIgnoreCase(scopeToCheck) ? getVisibility() : getVisibilityOutside(); + return "read-only".equalsIgnoreCase(visibility); + + } + + private String getScope(InProgressSubmission obj) { + if (HibernateProxyHelper.getClassWithoutInitializingProxy(obj).equals(WorkspaceItem.class)) { + return "submission"; + } + return "workflow"; + } + /** * Get the number of this step in the current Submission process config. * Step numbers start with #0 (although step #0 is ALWAYS the special 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/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/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/RorOrgUnitAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/RorOrgUnitAuthority.java new file mode 100644 index 000000000000..09f7330b62fe --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/authority/RorOrgUnitAuthority.java @@ -0,0 +1,86 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.content.authority; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.dspace.content.authority.factory.ItemAuthorityServiceFactory; +import org.dspace.ror.ROROrgUnitDTO; +import org.dspace.ror.service.RORApiService; +import org.dspace.ror.service.RORApiServiceImpl; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; + +public class RorOrgUnitAuthority extends ItemAuthority { + + private final RORApiService rorApiService = dspace.getSingletonService(RORApiServiceImpl.class); + private final ItemAuthorityServiceFactory itemAuthorityServiceFactory = + dspace.getServiceManager().getServiceByName("itemAuthorityServiceFactory", ItemAuthorityServiceFactory.class); + private final ConfigurationService configurationService = + DSpaceServicesFactory.getInstance().getConfigurationService(); + + private String authorityName; + + @Override + public Choices getMatches(String text, int start, int limit, String locale) { + super.setPluginInstanceName(authorityName); + Choices solrChoices = super.getMatches(text, start, limit, locale); + + return solrChoices.values.length == 0 ? getRORApiMatches(text, start, limit) : solrChoices; + } + + private Choices getRORApiMatches(String text, int start, int limit) { + Choice[] rorApiChoices = getChoiceFromRORQueryResults( + rorApiService.getOrgUnits(text).stream() + .filter(ou -> "active".equals(ou.getStatus())) + .collect(Collectors.toList()) + ).toArray(new Choice[0]); + + int confidenceValue = itemAuthorityServiceFactory.getInstance(authorityName) + .getConfidenceForChoices(rorApiChoices); + + return new Choices(rorApiChoices, start, rorApiChoices.length, confidenceValue, + rorApiChoices.length > (start + limit), 0); + } + + private List getChoiceFromRORQueryResults(List orgUnits) { + return orgUnits + .stream() + .map(orgUnit -> new Choice(composeAuthorityValue(orgUnit.getIdentifier()), orgUnit.getName(), + orgUnit.getName(), buildExtras(orgUnit))) + .collect(Collectors.toList()); + } + + private Map buildExtras(ROROrgUnitDTO orgUnit) { + return new HashMap<>(); + } + + private String composeAuthorityValue(String rorId) { + String prefix = configurationService.getProperty("ror.authority.prefix", "will be referenced::ROR-ID::"); + return prefix + rorId; + } + + @Override + public String getLinkedEntityType() { + return configurationService.getProperty("cris.ItemAuthority." + authorityName + ".entityType"); + } + + @Override + public void setPluginInstanceName(String name) { + authorityName = name; + } + + @Override + public String getPluginInstanceName() { + return authorityName; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/dto/MetadataValueDTO.java b/dspace-api/src/main/java/org/dspace/content/dto/MetadataValueDTO.java index 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/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/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/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/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/RorOrgUnitDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/RorOrgUnitDataProvider.java new file mode 100644 index 000000000000..76a38796d5f6 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/RorOrgUnitDataProvider.java @@ -0,0 +1,69 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.external.provider.impl; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.external.model.ExternalDataObject; +import org.dspace.external.provider.AbstractExternalDataProvider; +import org.dspace.ror.ROROrgUnitDTO; +import org.dspace.ror.service.RORApiService; +import org.springframework.beans.factory.annotation.Autowired; + +public class RorOrgUnitDataProvider extends AbstractExternalDataProvider { + + @Autowired + private RORApiService rorApiService; + + private String sourceIdentifier; + + @Override + public Optional getExternalDataObject(String id) { + return rorApiService.getOrgUnit(id) + .map(this::convertToExternalDataObject); + } + + @Override + public List searchExternalDataObjects(String query, int start, int limit) { + return rorApiService.getOrgUnits(query).stream() + .map(this::convertToExternalDataObject) + .collect(Collectors.toList()); + } + + private ExternalDataObject convertToExternalDataObject(ROROrgUnitDTO orgUnit) { + ExternalDataObject object = new ExternalDataObject(sourceIdentifier); + object.setId(orgUnit.getIdentifier()); + object.setValue(orgUnit.getName()); + object.setDisplayValue(orgUnit.getName()); + object.setMetadata(rorApiService.getMetadataValues(orgUnit)); + return object; + } + + @Override + public boolean supports(String source) { + return StringUtils.equals(sourceIdentifier, source); + } + + @Override + public int getNumberOfResults(String query) { + return searchExternalDataObjects(query, 0, -1).size(); + } + + public void setSourceIdentifier(String sourceIdentifier) { + this.sourceIdentifier = sourceIdentifier; + } + + @Override + public String getSourceIdentifier() { + return sourceIdentifier; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java index 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/identifier/doi/DataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java index 57136d6143bb..43882918cd4a 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java @@ -8,13 +8,14 @@ package org.dspace.identifier.doi; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.sql.SQLException; -import java.util.HashMap; import java.util.Iterator; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; @@ -35,13 +36,14 @@ import org.apache.http.util.EntityUtils; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; import org.dspace.content.crosswalk.CrosswalkException; -import org.dspace.content.crosswalk.DisseminationCrosswalk; -import org.dspace.content.crosswalk.ParameterizedDisseminationCrosswalk; +import org.dspace.content.crosswalk.StreamDisseminationCrosswalk; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.DSpaceObjectService; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; import org.dspace.core.Context; -import org.dspace.core.factory.CoreServiceFactory; import org.dspace.handle.service.HandleService; import org.dspace.identifier.DOI; import org.dspace.services.ConfigurationService; @@ -99,18 +101,6 @@ public class DataCiteConnector * dependency injection. */ protected String METADATA_PATH; - /** - * Name of crosswalk to convert metadata into DataCite Metadata Scheme. Set - * by spring dependency injection. - */ - protected String CROSSWALK_NAME; - /** - * DisseminationCrosswalk to map local metadata into DataCite metadata. - * The name of the crosswalk is set by spring dependency injection using - * {@link #setDisseminationCrosswalkName(String) setDisseminationCrosswalkName} which - * instantiates the crosswalk. - */ - protected ParameterizedDisseminationCrosswalk xwalk; protected ConfigurationService configurationService; @@ -119,8 +109,12 @@ public class DataCiteConnector @Autowired protected HandleService handleService; + @Autowired + private ItemService itemService; + + private Map disseminationCrosswalkByEntityType; + public DataCiteConnector() { - this.xwalk = null; this.USERNAME = null; this.PASSWORD = null; } @@ -189,34 +183,6 @@ public void setConfigurationService(ConfigurationService configurationService) { this.configurationService = configurationService; } - /** - * Set the name of the dissemination crosswalk used to convert the metadata - * into DataCite Metadata Schema. Used by spring dependency injection. - * - * @param CROSSWALK_NAME The name of the dissemination crosswalk to use. This - * crosswalk must be configured in dspace.cfg. - */ - @Autowired(required = true) - public void setDisseminationCrosswalkName(String CROSSWALK_NAME) { - this.CROSSWALK_NAME = CROSSWALK_NAME; - } - - protected void prepareXwalk() { - if (null != this.xwalk) { - return; - } - - this.xwalk = (ParameterizedDisseminationCrosswalk) CoreServiceFactory.getInstance().getPluginService() - .getNamedPlugin( - DisseminationCrosswalk.class, - this.CROSSWALK_NAME); - - if (this.xwalk == null) { - throw new RuntimeException("Can't find crosswalk '" - + CROSSWALK_NAME + "'!"); - } - } - protected String getUsername() { if (null == this.USERNAME) { this.USERNAME = this.configurationService.getProperty(CFG_USER); @@ -350,64 +316,43 @@ public void deleteDOI(Context context, String doi) @Override public void reserveDOI(Context context, DSpaceObject dso, String doi) throws DOIIdentifierException { - this.prepareXwalk(); DSpaceObjectService dSpaceObjectService = ContentServiceFactory.getInstance() .getDSpaceObjectService(dso); - if (!this.xwalk.canDisseminate(dso)) { - log.error("Crosswalk " + this.CROSSWALK_NAME - + " cannot disseminate DSO with type " + dso.getType() - + " and ID " + dso.getID() + ". Giving up reserving the DOI " - + doi + "."); - throw new DOIIdentifierException("Cannot disseminate " - + dSpaceObjectService.getTypeText(dso) + "/" + dso.getID() - + " using crosswalk " + this.CROSSWALK_NAME + ".", - DOIIdentifierException.CONVERSION_ERROR); - } + StreamDisseminationCrosswalk xwalk = getStreamDisseminationCrosswalkByDso(dso); - // Set the transform's parameters. - // XXX Should the actual list be configurable? - Map parameters = new HashMap<>(); - if (configurationService.hasProperty(CFG_PREFIX)) { - parameters.put("prefix", - configurationService.getProperty(CFG_PREFIX)); - } - if (configurationService.hasProperty(CFG_PUBLISHER)) { - parameters.put("publisher", - configurationService.getProperty(CFG_PUBLISHER)); - } - if (configurationService.hasProperty(CFG_DATAMANAGER)) { - parameters.put("datamanager", - configurationService.getProperty(CFG_DATAMANAGER)); - } - if (configurationService.hasProperty(CFG_HOSTINGINSTITUTION)) { - parameters.put("hostinginstitution", - configurationService.getProperty(CFG_HOSTINGINSTITUTION)); + if (xwalk == null) { + log.error("No crosswalk found for DSO with type " + dso.getType() + + " and ID " + dso.getID() + ". Giving up reserving the DOI " + + doi + "."); + throw new DOIIdentifierException("Cannot disseminate " + + dSpaceObjectService.getTypeText(dso) + "/" + dso.getID() + ".", + DOIIdentifierException.CONVERSION_ERROR); } Element root = null; try { - root = xwalk.disseminateElement(context, dso, parameters); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + xwalk.disseminate(context, dso, baos); + SAXBuilder builder = new SAXBuilder(); + Document document = builder.build(new ByteArrayInputStream(baos.toByteArray())); + root = document.getRootElement(); } catch (AuthorizeException ae) { log.error("Caught an AuthorizeException while disseminating DSO " + "with type " + dso.getType() + " and ID " + dso.getID() + ". Giving up to reserve DOI " + doi + ".", ae); throw new DOIIdentifierException("AuthorizeException occured while " - + "converting " + dSpaceObjectService.getTypeText(dso) + "/" + dso - .getID() - + " using crosswalk " + this.CROSSWALK_NAME + ".", ae, + + "converting " + dSpaceObjectService.getTypeText(dso) + "/" + dso + ".", ae, DOIIdentifierException.CONVERSION_ERROR); } catch (CrosswalkException ce) { log.error("Caught an CrosswalkException while reserving a DOI (" + doi + ") for DSO with type " + dso.getType() + " and ID " + dso.getID() + ". Won't reserve the doi.", ce); throw new DOIIdentifierException("CrosswalkException occured while " - + "converting " + dSpaceObjectService.getTypeText(dso) + "/" + dso - .getID() - + " using crosswalk " + this.CROSSWALK_NAME + ".", ce, + + "converting " + dSpaceObjectService.getTypeText(dso) + "/" + dso + ".", ce, DOIIdentifierException.CONVERSION_ERROR); - } catch (IOException | SQLException ex) { + } catch (IOException | SQLException | JDOMException ex) { throw new RuntimeException(ex); } @@ -462,6 +407,21 @@ public void reserveDOI(Context context, DSpaceObject dso, String doi) } } + private StreamDisseminationCrosswalk getStreamDisseminationCrosswalkByDso(DSpaceObject dso) { + + if (dso.getType() != Constants.ITEM) { + return null; + } + + String entityType = itemService.getEntityType((Item) dso); + if (StringUtils.isBlank(entityType)) { + entityType = "Publication"; + } + + return disseminationCrosswalkByEntityType.get(entityType); + + } + @Override public void registerDOI(Context context, DSpaceObject dso, String doi) throws DOIIdentifierException { @@ -631,7 +591,7 @@ protected DataCiteResponse sendMetadataPostRequest(String doi, Element metadataR Format format = Format.getCompactFormat(); format.setEncoding("UTF-8"); XMLOutputter xout = new XMLOutputter(format); - return sendMetadataPostRequest(doi, xout.outputString(new Document(metadataRoot))); + return sendMetadataPostRequest(doi, xout.outputString(metadataRoot.getDocument())); } protected DataCiteResponse sendMetadataPostRequest(String doi, String metadata) @@ -842,12 +802,21 @@ protected Element addDOI(String doi, Element root) { } Element identifier = new Element("identifier", configurationService.getProperty(CFG_NAMESPACE, - "http://datacite.org/schema/kernel-3")); + "http://datacite.org/schema/kernel-4")); identifier.setAttribute("identifierType", "DOI"); identifier.addContent(doi.substring(DOI.SCHEME.length())); return root.addContent(0, identifier); } + public Map getDisseminationCrosswalkByEntityType() { + return disseminationCrosswalkByEntityType; + } + + public void setDisseminationCrosswalkByEntityType( + Map disseminationCrosswalkByEntityType) { + this.disseminationCrosswalkByEntityType = disseminationCrosswalkByEntityType; + } + protected class DataCiteResponse { private final int statusCode; private final String content; diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java index 8fbe4ef2cf57..da59472c45a6 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/ads/ADSImportMetadataSourceServiceImpl.java @@ -273,6 +273,9 @@ public Integer count(String query, String token) { uriBuilder.addParameter("fl", this.resultFieldList); String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); + if (StringUtils.isEmpty(resp)) { + return 0; + } JsonNode jsonNode = convertStringJsonToJsonNode(resp); return jsonNode.at("/response/numFound").asInt(); } catch (URISyntaxException e) { @@ -296,6 +299,9 @@ public List search(String query, Integer start, Integer count, Str uriBuilder.addParameter("fl", this.resultFieldList); String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); + if (StringUtils.isEmpty(resp)) { + return adsResults; + } JsonNode jsonNode = convertStringJsonToJsonNode(resp); JsonNode docs = jsonNode.at("/response/docs"); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java index 5eff46c790e4..53230d960831 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/cinii/CiniiImportMetadataSourceServiceImpl.java @@ -289,6 +289,9 @@ protected List search(String id, String appId) URIBuilder uriBuilder = new URIBuilder(this.url + id + ".rdf?appid=" + appId); Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); + if (StringUtils.isBlank(response)) { + return records; + } List elements = splitToRecords(response); for (Element record : elements) { records.add(transformSourceRecords(record)); @@ -416,6 +419,9 @@ private Integer countCiniiElement(String appId, Integer maxResult, String author Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); + if (StringUtils.isEmpty(response)) { + return 0; + } SAXBuilder saxBuilder = new SAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java index 7dde330b27ec..5c4c49deaec9 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java @@ -158,6 +158,9 @@ public List call() throws Exception { } Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); + if (StringUtils.isEmpty(response)) { + return results; + } JsonNode jsonNode = convertStringJsonToJsonNode(response); Iterator nodes = jsonNode.at("/message/items").iterator(); while (nodes.hasNext()) { @@ -194,6 +197,9 @@ public List call() throws Exception { URIBuilder uriBuilder = new URIBuilder(url + "/" + ID); Map> params = new HashMap>(); String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); + if (StringUtils.isEmpty(responseString)) { + return results; + } JsonNode jsonNode = convertStringJsonToJsonNode(responseString); JsonNode messageNode = jsonNode.at("/message"); results.add(transformSourceRecords(messageNode.toString())); @@ -246,6 +252,9 @@ public List call() throws Exception { } Map> params = new HashMap>(); String resp = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); + if (StringUtils.isEmpty(resp)) { + return results; + } JsonNode jsonNode = convertStringJsonToJsonNode(resp); Iterator nodes = jsonNode.at("/message/items").iterator(); while (nodes.hasNext()) { @@ -284,6 +293,9 @@ public Integer call() throws Exception { uriBuilder.addParameter("query", query.getParameterAsClass("query", String.class)); Map> params = new HashMap>(); String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); + if (StringUtils.isEmpty(responseString)) { + return 0; + } JsonNode jsonNode = convertStringJsonToJsonNode(responseString); return jsonNode.at("/message/total-results").asInt(); } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java index 395d6b48c987..77e34ba9e41b 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/epo/service/EpoImportMetadataSourceServiceImpl.java @@ -59,6 +59,7 @@ * Implements a data source for querying EPO * * @author Pasquale Cavallo (pasquale.cavallo at 4Science dot it) + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4Science.com) */ public class EpoImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService implements QuerySource { @@ -147,6 +148,9 @@ protected String login() throws IOException, HttpException { Map> params = getLoginParams(); String entity = "grant_type=client_credentials"; String json = liveImportClient.executeHttpPostRequest(this.authUrl, params, entity); + if (StringUtils.isBlank(json)) { + return json; + } ObjectMapper mapper = new ObjectMapper(new JsonFactory()); JsonNode rootNode = mapper.readTree(json); JsonNode accessTokenNode = rootNode.get("access_token"); @@ -190,7 +194,8 @@ public int getRecordsCount(Query query) throws MetadataSourceException { String bearer = login(); return retry(new CountRecordsCallable(query, bearer)); } catch (IOException | HttpException e) { - e.printStackTrace(); + log.warn(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); } } return 0; @@ -204,7 +209,7 @@ public Collection getRecords(String query, int start, String bearer = login(); return retry(new SearchByQueryCallable(query, bearer, start, count)); } catch (IOException | HttpException e) { - log.warn(e.getMessage()); + log.warn(e.getMessage(), e); throw new RuntimeException(e.getMessage(), e); } } @@ -247,14 +252,12 @@ public ImportRecord getRecord(Query query) throws MetadataSourceException { } @Override - public Collection findMatchingRecords(Item item) - throws MetadataSourceException { + public Collection findMatchingRecords(Item item) throws MetadataSourceException { return null; } @Override - public Collection findMatchingRecords(Query query) - throws MetadataSourceException { + public Collection findMatchingRecords(Query query) throws MetadataSourceException { return null; } @@ -303,7 +306,13 @@ private SearchByIdCallable(String id, String bearer) { } public List call() throws Exception { - if (id.contains(APP_NO_DATE_SEPARATOR)) { + int positionToSplit = id.indexOf(":"); + String docType = EpoDocumentId.EPODOC; + String idS = id; + if (positionToSplit != -1) { + docType = id.substring(0, positionToSplit); + idS = id.substring(positionToSplit + 1, id.length()); + } else if (id.contains(APP_NO_DATE_SEPARATOR)) { // special case the id is the combination of the applicationnumber and date filed String query = "applicationnumber=" + id.split(APP_NO_DATE_SEPARATOR_REGEX)[0]; SearchByQueryCallable search = new SearchByQueryCallable(query, bearer, 0, 10); @@ -316,12 +325,7 @@ public List call() throws Exception { return records; } // search by Patent Number - String[] identifier = id.split(":"); - String patentIdentifier = identifier.length == 2 ? identifier[1] : id; - List records = retry(new SearchByQueryCallable(patentIdentifier, bearer, null, null)); - if (records.size() > 1) { - log.warn("More record are returned with Patent Number: " + id); - } + List records = searchDocument(bearer, idS, docType); return records; } } @@ -375,7 +379,7 @@ public List call() throws Exception { private Integer countDocument(String bearer, String query) { if (StringUtils.isBlank(bearer)) { - return null; + return 0; } try { Map> params = new HashMap>(); @@ -388,6 +392,9 @@ private Integer countDocument(String bearer, String query) { uriBuilder.addParameter("q", query); String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); + if (StringUtils.isBlank(response)) { + return 0; + } SAXBuilder saxBuilder = new SAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); @@ -402,7 +409,7 @@ private Integer countDocument(String bearer, String query) { return Integer.parseInt(totalRes); } catch (JDOMException | IOException | URISyntaxException | JaxenException e) { log.error(e.getMessage(), e); - return null; + return 0; } } @@ -425,6 +432,9 @@ private List searchDocumentIds(String bearer, String query, int s uriBuilder.addParameter("q", query); String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); + if (StringUtils.isBlank(response)) { + return results; + } SAXBuilder saxBuilder = new SAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); @@ -465,6 +475,9 @@ private List searchDocument(String bearer, String id, String docTy String url = this.url.replace("$(doctype)", docType).replace("$(id)", id); String response = liveImportClient.executeHttpGetRequest(1000, url, params); + if (StringUtils.isBlank(response)) { + return results; + } List elements = splitToRecords(response); for (Element element : elements) { results.add(transformSourceRecords(element)); @@ -481,7 +494,7 @@ private List splitToRecords(String recordsSrc) { Document document = saxBuilder.build(new StringReader(recordsSrc)); Element root = document.getRootElement(); List namespaces = Arrays.asList(Namespace.getNamespace("ns", "http://www.epo.org/exchange")); - XPathExpression xpath = XPathFactory.instance().compile("//ns:exchange-document", + XPathExpression xpath = XPathFactory.instance().compile("//ns:exchange-documents", Filters.element(), null, namespaces); List recordsList = xpath.evaluate(root); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/AuthorMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/AuthorMetadataContributor.java index c28c229188a7..b2d2ea22b0c6 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/AuthorMetadataContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/AuthorMetadataContributor.java @@ -111,10 +111,16 @@ private List getMetadataOfAuthors(Element element) throws JaxenExc addMetadatum(metadatums, getMetadata(getElementValue(surname) + ", " + getElementValue(givenName), this.authname)); - addMetadatum(metadatums, getMetadata(getElementValue(scopusId), this.scopusId)); - addMetadatum(metadatums, getMetadata(getElementValue(orcid), this.orcid)); - addMetadatum(metadatums, getMetadata(StringUtils.isNotBlank(afid.getValue()) - ? this.affId2affName.get(afid.getValue()) : null, this.affiliation)); + if (this.scopusId != null) { + addMetadatum(metadatums, getMetadata(getElementValue(scopusId), this.scopusId)); + } + if (this.orcid != null) { + addMetadatum(metadatums, getMetadata(getElementValue(orcid), this.orcid)); + } + if (this.affiliation != null) { + addMetadatum(metadatums, getMetadata(StringUtils.isNotBlank(afid.getValue()) + ? this.affId2affName.get(afid.getValue()) : null, this.affiliation)); + } return metadatums; } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeAndSubNodeContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeAndSubNodeContributor.java new file mode 100644 index 000000000000..aae07b1ff263 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeAndSubNodeContributor.java @@ -0,0 +1,87 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jdom2.Element; +import org.jdom2.Namespace; +import org.jdom2.filter.Filters; +import org.jdom2.xpath.XPathExpression; +import org.jdom2.xpath.XPathFactory; + +/** + * Metadata contributor that takes multiple value of the some nome. + * Can fileter also nedes by attribute element value. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class SimpleXpathMetadatumAndAttributeAndSubNodeContributor extends SimpleXpathMetadatumAndAttributeContributor { + + private String attributeValue; + private String queryToSubNode; + + @Override + public Collection contributeMetadata(Element t) { + List values = new LinkedList<>(); + List namespaces = new ArrayList(); + for (String ns : prefixToNamespaceMapping.keySet()) { + namespaces.add(Namespace.getNamespace(prefixToNamespaceMapping.get(ns), ns)); + } + + List nodes = getNodes(t, query, namespaces); + List subNodes = getSubNodes(namespaces, nodes); + for (Object el : subNodes) { + if (el instanceof Element) { + values.add(metadataFieldMapping.toDCValue(this.field, extractValue(el))); + } + } + return values; + } + + private List getSubNodes(List namespaces, List nodes) { + List allNodes = new ArrayList(); + for (Object el : nodes) { + if (el instanceof Element) { + List elements = ((Element) el).getChildren(); + for (Element element : elements) { + String attributeValue = element.getAttributeValue(this.attribute); + if (StringUtils.equals(attributeValue, this.attributeValue)) { + List subNodes = getNodes(element, queryToSubNode, namespaces); + allNodes.addAll(subNodes); + } + } + } + } + return allNodes; + } + + private List getNodes(Element t, String query, List namespaces) { + XPathExpression xpath = XPathFactory.instance().compile(query, Filters.fpassthrough(),null, namespaces); + return xpath.evaluate(t); + } + + private String extractValue(Object el) { + String value = ((Element) el).getText(); + return StringUtils.isNotBlank(value) ? value : ((Element) el).getValue().trim(); + } + + public void setAttributeValue(String attributeValue) { + this.attributeValue = attributeValue; + } + + public void setQueryToSubNode(String queryToSubNode) { + this.queryToSubNode = queryToSubNode; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeContributor.java index dea840d15b38..1fd9d168338d 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumAndAttributeContributor.java @@ -33,7 +33,7 @@ public class SimpleXpathMetadatumAndAttributeContributor extends SimpleXpathMeta private final static Logger log = LogManager.getLogger(); - private String attribute; + protected String attribute; @Override public Collection contributeMetadata(Element t) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java index 1ec0da74206e..ba95e1e9045a 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/pubmedeurope/PubmedEuropeMetadataSourceServiceImpl.java @@ -292,6 +292,9 @@ public Integer count(String query) throws URISyntaxException, ClientProtocolExce try { Map> params = new HashMap>(); String response = liveImportClient.executeHttpGetRequest(1000, buildURI(1, query), params); + if (StringUtils.isEmpty(response)) { + return 0; + } SAXBuilder saxBuilder = new SAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java index d0c2fb078a2c..36cf8ae2dc49 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/scopus/service/ScopusImportMetadataSourceServiceImpl.java @@ -200,6 +200,9 @@ public Integer call() throws Exception { Map requestParams = getRequestParameters(query, null, null, null); params.put(URI_PARAMETERS, requestParams); String response = liveImportClient.executeHttpGetRequest(timeout, url, params); + if (StringUtils.isEmpty(response)) { + return 0; + } SAXBuilder saxBuilder = new SAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); @@ -243,6 +246,9 @@ public List call() throws Exception { Map requestParams = getRequestParameters(queryString, viewMode, null, null); params.put(URI_PARAMETERS, requestParams); String response = liveImportClient.executeHttpGetRequest(timeout, url, params); + if (StringUtils.isEmpty(response)) { + return results; + } List elements = splitToRecords(response); for (Element record : elements) { results.add(transformSourceRecords(record)); @@ -302,6 +308,9 @@ public List call() throws Exception { Map requestParams = getRequestParameters(queryString, viewMode, start, count); params.put(URI_PARAMETERS, requestParams); String response = liveImportClient.executeHttpGetRequest(timeout, url, params); + if (StringUtils.isEmpty(response)) { + return results; + } List elements = splitToRecords(response); for (Element record : elements) { results.add(transformSourceRecords(record)); @@ -347,6 +356,9 @@ public List call() throws Exception { Map requestParams = getRequestParameters(queryString, viewMode, start, count); params.put(URI_PARAMETERS, requestParams); String response = liveImportClient.executeHttpGetRequest(timeout, url, params); + if (StringUtils.isEmpty(response)) { + return results; + } List elements = splitToRecords(response); for (Element record : elements) { results.add(transformSourceRecords(record)); diff --git a/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java index a4f90fa5ba61..1b942a7f1525 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java @@ -144,6 +144,9 @@ public Integer call() throws Exception { uriBuilder.addParameter("lookfor", query.getParameterAsClass("query", String.class)); Map> params = new HashMap>(); String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); + if (StringUtils.isEmpty(responseString)) { + return 0; + } JsonNode node = convertStringJsonToJsonNode(responseString); JsonNode resultCountNode = node.get("resultCount"); return resultCountNode.intValue(); @@ -182,8 +185,7 @@ public String call() throws Exception { } } Map> params = new HashMap>(); - String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); - return response; + return liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), params); } } @@ -309,6 +311,9 @@ private JsonNode convertStringJsonToJsonNode(String json) { private List extractMetadataFromRecordList(String records) { List recordsResult = new ArrayList<>(); + if (StringUtils.isEmpty(records)) { + return recordsResult; + } JsonNode jsonNode = convertStringJsonToJsonNode(records); JsonNode node = jsonNode.get("records"); if (Objects.nonNull(node) && node.isArray()) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java index 2ccdc12b8db2..b669cd860078 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/wos/service/WOSImportMetadataSourceServiceImpl.java @@ -145,6 +145,9 @@ public Integer call() throws Exception { Map> params = new HashMap>(); params.put(HEADER_PARAMETERS, getRequestParameters()); String response = liveImportClient.executeHttpGetRequest(timeout, url, params); + if (StringUtils.isEmpty(response)) { + return 0; + } SAXBuilder saxBuilder = new SAXBuilder(); Document document = saxBuilder.build(new StringReader(response)); @@ -179,6 +182,9 @@ public List call() throws Exception { Map> params = new HashMap>(); params.put(HEADER_PARAMETERS, getRequestParameters()); String response = liveImportClient.executeHttpGetRequest(timeout, urlString, params); + if (StringUtils.isEmpty(response)) { + return results; + } List elements = splitToRecords(response); for (Element record : elements) { @@ -226,6 +232,9 @@ public List call() throws Exception { String url = urlSearch + URLEncoder.encode(queryString, StandardCharsets.UTF_8) + "&count=" + count + "&firstRecord=" + (start + 1); String response = liveImportClient.executeHttpGetRequest(timeout, url, params); + if (StringUtils.isEmpty(response)) { + return results; + } List omElements = splitToRecords(response); for (Element el : omElements) { 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/ror/ROROrgUnitDTO.java b/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitDTO.java new file mode 100644 index 000000000000..9eac4494fc59 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitDTO.java @@ -0,0 +1,87 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.ror; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.StringUtils; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ROROrgUnitDTO { + + @JsonProperty("id") + private String url; + + private String name; + + private String[] acronyms; + + private String[] aliases; + + private String status; + + private String[] types; + + public String getIdentifier() { + if (StringUtils.isBlank(url)) { + return null; + } + + String[] splittedUrl = url.split("/"); + return splittedUrl[splittedUrl.length - 1]; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String[] getAcronyms() { + return acronyms; + } + + public void setAcronyms(String[] acronyms) { + this.acronyms = acronyms; + } + + public String[] getAliases() { + return aliases; + } + + public void setAliases(String[] aliases) { + this.aliases = aliases; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String[] getTypes() { + return types; + } + + public void setTypes(String[] types) { + this.types = types; + } +} diff --git a/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitListDTO.java b/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitListDTO.java new file mode 100644 index 000000000000..1b3b5c7c9593 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/ROROrgUnitListDTO.java @@ -0,0 +1,36 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.ror; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ROROrgUnitListDTO { + private ROROrgUnitDTO[] items; + + @JsonProperty(value = "number_of_results") + private int total; + + public ROROrgUnitDTO[] getItems() { + return items; + } + + public void setItems(ROROrgUnitDTO[] items) { + this.items = items; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } +} diff --git a/dspace-api/src/main/java/org/dspace/ror/client/RORApiClient.java b/dspace-api/src/main/java/org/dspace/ror/client/RORApiClient.java new file mode 100644 index 000000000000..5e2682c3a002 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/client/RORApiClient.java @@ -0,0 +1,21 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.ror.client; + +import java.util.List; +import java.util.Optional; + +import org.dspace.ror.ROROrgUnitDTO; + +public interface RORApiClient { + + List searchOrganizations(String text); + + Optional findOrganizationByRORId(String rorId); +} diff --git a/dspace-api/src/main/java/org/dspace/ror/client/RORApiClientImpl.java b/dspace-api/src/main/java/org/dspace/ror/client/RORApiClientImpl.java new file mode 100644 index 000000000000..c6934de86d00 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/client/RORApiClientImpl.java @@ -0,0 +1,163 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.ror.client; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.http.client.methods.RequestBuilder.get; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.dspace.ror.ROROrgUnitDTO; +import org.dspace.ror.ROROrgUnitListDTO; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +public class RORApiClientImpl implements RORApiClient { + + public static final int TIMEOUT_MS = 15 * 1000; + + @Autowired + private ConfigurationService configurationService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(TIMEOUT_MS) + .setConnectionRequestTimeout(TIMEOUT_MS) + .setSocketTimeout(TIMEOUT_MS) + .build(); + + @Override + public List searchOrganizations(String text) { + RorResponse response = performGetRequest(buildGetWithQueryExact(getRORApiUrl(), text.trim())); + + if (isNotFound(response)) { + return Collections.emptyList(); + } + + if (isNotSuccessful(response)) { + String message = "ROR API request was not successful. " + + "Status: " + response.getStatusCode() + " - Content: " + response.getContent(); + throw new RuntimeException(message); + } + + ROROrgUnitListDTO orgUnits = parseResponse(response, ROROrgUnitListDTO.class); + + return List.of(orgUnits.getItems()); + } + + @Override + public Optional findOrganizationByRORId(String rorId) { + RorResponse response = performGetRequest(buildGetWithRORId(getRORApiUrl(), rorId)); + + if (isNotFound(response)) { + return Optional.empty(); + } + + if (isNotSuccessful(response)) { + String message = "ROR API request was not successful. " + + "Status: " + response.getStatusCode() + " - Content: " + response.getContent(); + throw new RuntimeException(message); + } + + ROROrgUnitDTO orgUnit = parseResponse(response, ROROrgUnitDTO.class); + + return Optional.ofNullable(orgUnit); + } + + private RorResponse performGetRequest(HttpUriRequest request) { + try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { + CloseableHttpResponse httpResponse = httpClient.execute(request); + int statusCode = getStatusCode(httpResponse); + HttpEntity entity = httpResponse.getEntity(); + return new RorResponse(statusCode, getContent(httpResponse)); +// return httpResponse; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private HttpUriRequest buildGetWithRORId(String url, String rorId) { + return get(url + "/" + rorId).setConfig(requestConfig).build(); + } + + private HttpUriRequest buildGetWithQuery(String url, String value) { + return get(url).addParameter("query", value).setConfig(requestConfig).build(); + } + + private HttpUriRequest buildGetWithQueryExact(String url, String value) { + return get(url).addParameter("query", "\"" + value + "\"").setConfig(requestConfig).build(); + } + + private T parseResponse(RorResponse response, Class clazz) { + try { + return objectMapper.readValue(response.getContent(), clazz); + } catch (UnsupportedOperationException | IOException e) { + throw new RuntimeException(e); + } + } + + private String getContent(HttpResponse response) { + try { + HttpEntity entity = response.getEntity(); + return entity != null ? IOUtils.toString(entity.getContent(), UTF_8) : null; + } catch (UnsupportedOperationException | IOException e) { + throw new RuntimeException(e); + } + } + + private boolean isNotSuccessful(RorResponse response) { + int statusCode = response.getStatusCode(); + return statusCode < 200 || statusCode > 299; + } + + private boolean isNotFound(RorResponse response) { + return response.getStatusCode() == HttpStatus.SC_NOT_FOUND; + } + + private int getStatusCode(HttpResponse response) { + return response.getStatusLine().getStatusCode(); + } + + private String getRORApiUrl() { + return configurationService.getProperty("ror.orgunit-import.api-url"); + } + + private static class RorResponse { + private final int statusCode; + private final String content; + + public RorResponse(int statusCode, String content) { + + this.statusCode = statusCode; + this.content = content; + } + + public int getStatusCode() { + return statusCode; + } + + public String getContent() { + return content; + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/ror/service/RORApiService.java b/dspace-api/src/main/java/org/dspace/ror/service/RORApiService.java new file mode 100644 index 000000000000..fffaa14ee777 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/service/RORApiService.java @@ -0,0 +1,28 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.ror.service; +import java.util.List; +import java.util.Optional; + +import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.ror.ROROrgUnitDTO; + +public interface RORApiService { + + public List getOrgUnits(String query); + + public Optional getOrgUnit(String rorId); + + public List getMetadataValues(ROROrgUnitDTO orgUnit); + + public List getMetadataValues(String rorId); + + public List getMetadataFields(); + +} diff --git a/dspace-api/src/main/java/org/dspace/ror/service/RORApiServiceImpl.java b/dspace-api/src/main/java/org/dspace/ror/service/RORApiServiceImpl.java new file mode 100644 index 000000000000..cff214fe3f11 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ror/service/RORApiServiceImpl.java @@ -0,0 +1,123 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.ror.service; + +import static java.util.Optional.ofNullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.ror.ROROrgUnitDTO; +import org.dspace.ror.client.RORApiClient; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +public class RORApiServiceImpl implements RORApiService { + + private static final String ORGUNIT_MAPPING_PREFIX = "ror.orgunit-import.api.metadata-field."; + + @Autowired + private RORApiClient apiClient; + + @Autowired + private ConfigurationService configurationService; + + @Override + public List getOrgUnits(String query) { + return apiClient.searchOrganizations(query); + } + + @Override + public Optional getOrgUnit(String rorId) { + return apiClient.findOrganizationByRORId(rorId); + } + + @Override + public List getMetadataValues(String rorId) { + return getOrgUnit(rorId) + .map(this::getMetadataValues) + .orElse(getInactiveMetadataField()); + } + + @Override + public List getMetadataFields() { + return configurationService.getPropertyKeys(ORGUNIT_MAPPING_PREFIX).stream() + .map(key -> configurationService.getProperty(key)) + .filter(this::isMetadataField) + .collect(Collectors.toList()); + } + + @Override + public List getMetadataValues(ROROrgUnitDTO orgUnit) { + + List metadataValues = new ArrayList<>(); + + getPersonMetadataField("name") + .flatMap(field -> getMetadataValue(orgUnit.getName(), field)) + .ifPresent(metadataValues::add); + + getPersonMetadataField("acronym") + .flatMap(field -> getMetadataArrayValue(orgUnit.getAcronyms(), field)) + .ifPresent(metadataValues::add); + + getPersonMetadataField("url") + .flatMap(field -> getMetadataValue(orgUnit.getUrl(), field)) + .ifPresent(metadataValues::add); + + getPersonMetadataField("identifier") + .flatMap(field -> getMetadataValue(orgUnit.getIdentifier(), field)) + .ifPresent(metadataValues::add); + + return metadataValues; + + } + + private List getInactiveMetadataField() { + return getPersonMetadataField("active") + .flatMap(field -> getMetadataValue("false", field)) + .map(List::of) + .orElse(List.of()); + } + + private Optional getMetadataValue(String value, String field) { + return Optional.ofNullable(value) + .filter(StringUtils::isNotBlank) + .map(metadataValue -> new MetadataValueDTO(field, metadataValue)); + } + + private Optional getMetadataArrayValue(String[] values, String field) { + String joinedAcronym = Arrays.stream(values) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining("/")); + return StringUtils.isNotEmpty(joinedAcronym) + ? Optional.of(new MetadataValueDTO(field, joinedAcronym)) + : Optional.empty(); + } + + private boolean isMetadataField(String property) { + return property != null && property.contains("."); + } + + private Optional getPersonMetadataField(String fieldName) { + return ofNullable(configurationService.getProperty(ORGUNIT_MAPPING_PREFIX + fieldName)); + } + + public RORApiClient getApiClient() { + return apiClient; + } + + public void setApiClient(RORApiClient apiClient) { + this.apiClient = apiClient; + } +} diff --git a/dspace-api/src/main/java/org/dspace/statistics/content/StatisticsDatasetDisplay.java b/dspace-api/src/main/java/org/dspace/statistics/content/StatisticsDatasetDisplay.java index 941d4a33d314..75e18ec49e1f 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/content/StatisticsDatasetDisplay.java +++ b/dspace-api/src/main/java/org/dspace/statistics/content/StatisticsDatasetDisplay.java @@ -278,9 +278,14 @@ public String composeFilterQuery(String startDate, String endDate, boolean relat } //Creates query for usage raport generator - public String composeQueryWithInverseRelation(DSpaceObject dSpaceObject, List default_queries ) { + public String composeQueryWithInverseRelation(DSpaceObject dSpaceObject, List default_queries, int type) { StringBuilder query = new StringBuilder(); - query.append("{!join from=search.resourceid to=id fromIndex="); + if (type == Constants.BITSTREAM) { + query.append("{!join from=search.resourceid to=owningItem fromIndex="); + } else { + query.append("{!join from=search.resourceid to=id fromIndex="); + } + query.append(configurationService.getProperty("solr.multicorePrefix")); query.append("search} "); boolean isFirstDefaultQuery = true; 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 a913f2504a50..f60ac3c98edb 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/ContentGenerator.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/ContentGenerator.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import javax.annotation.Resource; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -31,6 +32,8 @@ import org.dspace.subscriptions.service.SubscriptionGenerator; import org.springframework.beans.factory.annotation.Autowired; + + /** * Implementation class of SubscriptionGenerator * which will handle the logic of sending the emails @@ -42,6 +45,7 @@ public class ContentGenerator implements SubscriptionGenerator private final Logger log = LogManager.getLogger(ContentGenerator.class); @SuppressWarnings("unchecked") + @Resource(name = "entityDissemination") private Map entityType2Disseminator = new HashMap(); @Autowired @@ -50,7 +54,8 @@ public class ContentGenerator implements SubscriptionGenerator @Override public void notifyForSubscriptions(Context context, EPerson ePerson, List indexableComm, - List indexableColl) { + List indexableColl, + List indexableItems) { try { if (Objects.nonNull(ePerson)) { Locale supportedLocale = I18nUtil.getEPersonLocale(ePerson); @@ -58,6 +63,7 @@ public void notifyForSubscriptions(Context context, EPerson ePerson, email.addRecipient(ePerson.getEmail()); email.addArgument(generateBodyMail(context, indexableComm)); email.addArgument(generateBodyMail(context, indexableColl)); + email.addArgument(generateBodyMail(context, indexableItems)); email.send(); } } catch (Exception e) { 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 ae5fd931da76..c1f9be368e27 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/StatisticsGenerator.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/StatisticsGenerator.java @@ -46,7 +46,7 @@ public class StatisticsGenerator implements SubscriptionGenerator { @Override public void notifyForSubscriptions(Context c, EPerson ePerson, List crisMetricsList, - List crisMetricsList1) { + List crisMetricsList1, List crisMetricsList2) { // find statistics for all the subscribed objects try { // send the notification to the user 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/SubscriptionEmailNotificationServiceImpl.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java index 8fb01cd36e92..2a30b89af3f5 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java @@ -9,6 +9,7 @@ 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 java.sql.SQLException; @@ -67,6 +68,7 @@ public SubscriptionEmailNotificationServiceImpl(Map public void perform(Context context, DSpaceRunnableHandler handler, String subscriptionType, String frequency) { List communityItems = new ArrayList<>(); List collectionsItems = new ArrayList<>(); + List items = new ArrayList<>(); try { List subscriptions = findAllSubscriptionsBySubscriptionTypeAndFrequency(context, subscriptionType, frequency); @@ -93,6 +95,11 @@ public void perform(Context context, DSpaceRunnableHandler handler, String subsc .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 { log.warn("found an invalid DSpace Object type ({}) among subscriptions to send", dSpaceObject.getType()); @@ -106,14 +113,16 @@ public void perform(Context context, DSpaceRunnableHandler handler, String subsc continue; } else { subscriptionType2generators.get(subscriptionType) - .notifyForSubscriptions(context, ePerson, communityItems, collectionsItems); + .notifyForSubscriptions(context, ePerson, communityItems, + collectionsItems, items); communityItems.clear(); collectionsItems.clear(); } } else { //in the end of the iteration subscriptionType2generators.get(subscriptionType) - .notifyForSubscriptions(context, ePerson, communityItems, collectionsItems); + .notifyForSubscriptions(context, ePerson, communityItems, + collectionsItems, items); } iterator++; } diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/dSpaceObjectsUpdates/ItemsUpdates.java b/dspace-api/src/main/java/org/dspace/subscriptions/dSpaceObjectsUpdates/ItemsUpdates.java index bf0c1ab28e93..40fcd81dafa5 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/dSpaceObjectsUpdates/ItemsUpdates.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/dSpaceObjectsUpdates/ItemsUpdates.java @@ -40,6 +40,7 @@ import org.dspace.discovery.indexobject.IndexableItem; import org.dspace.subscriptions.ContentGenerator; import org.dspace.subscriptions.service.DSpaceObjectUpdates; +import org.springframework.beans.factory.annotation.Autowired; /** @@ -49,10 +50,20 @@ * @author Alba Aliu */ public class ItemsUpdates implements DSpaceObjectUpdates { - private final CollectionService collectionService; - private final CommunityService communityService; - private final ItemService itemService; + + @Autowired + private CollectionService collectionService; + + @Autowired + private CommunityService communityService; + + @Autowired + private ItemService itemService; + + @Autowired private DiscoveryConfigurationService searchConfigurationService; + + @Autowired private SearchService searchService; private final Logger log = org.apache.logging.log4j.LogManager.getLogger(ContentGenerator.class); @@ -189,12 +200,4 @@ private DiscoverQuery buildBaseQuery(DiscoveryConfiguration discoveryConfigurati return discoverQuery; } - public ItemsUpdates(CollectionService collectionService, CommunityService communityService, ItemService itemService, - DiscoveryConfigurationService searchConfigurationService, SearchService searchService) { - this.collectionService = collectionService; - this.communityService = communityService; - this.itemService = itemService; - this.searchConfigurationService = searchConfigurationService; - this.searchService = searchService; - } } 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 index 1790513b9b79..994ada75b61b 100644 --- a/dspace-api/src/main/java/org/dspace/subscriptions/service/SubscriptionGenerator.java +++ b/dspace-api/src/main/java/org/dspace/subscriptions/service/SubscriptionGenerator.java @@ -20,6 +20,6 @@ */ public interface SubscriptionGenerator { - public void notifyForSubscriptions(Context c, EPerson ePerson, List comm, List coll); + 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/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/java/org/dspace/validation/service/impl/ValidationServiceImpl.java b/dspace-api/src/main/java/org/dspace/validation/service/impl/ValidationServiceImpl.java index 10081bb914b5..b4c9b4bc4c1a 100644 --- a/dspace-api/src/main/java/org/dspace/validation/service/impl/ValidationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/validation/service/impl/ValidationServiceImpl.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.annotation.PostConstruct; import org.dspace.app.util.SubmissionConfig; @@ -54,23 +55,51 @@ private void setup() throws SubmissionConfigReaderException { @Override public List validate(Context context, InProgressSubmission obj) { + SubmissionConfig submissionConfig = submissionConfigReader.getSubmissionConfigByInProgressSubmission(obj); + List errors = new ArrayList(); - SubmissionConfig submissionConfig = submissionConfigReader.getSubmissionConfigByInProgressSubmission(obj); + errors.addAll(notHiddenStepsValidations(context, obj, submissionConfig)); + errors.addAll(globalValidations(context, obj, submissionConfig)); + + return errors; + + } + + private List notHiddenStepsValidations(Context context, InProgressSubmission obj, + SubmissionConfig submissionConfig) { + + List errors = new ArrayList(); for (SubmissionStepConfig stepConfig : submissionConfig) { + + if (isStepHiddenOrReadOnly(stepConfig, obj)) { + continue; + } + stepValidators.stream() .filter(validation -> validation.getName().equals(stepConfig.getType())) .flatMap(validation -> validation.validate(context, obj, stepConfig).stream()) .forEach(errors::add); + } - globalValidators.stream() + return errors; + + } + + private List globalValidations(Context context, InProgressSubmission obj, + SubmissionConfig submissionConfig) { + + return globalValidators.stream() .flatMap(validator -> validator.validate(context, obj, submissionConfig).stream()) - .forEach(errors::add); + .collect(Collectors.toList()); - return errors; + } + private boolean isStepHiddenOrReadOnly(SubmissionStepConfig stepConfig, InProgressSubmission obj) { + return stepConfig.isHiddenForInProgressSubmission(obj) || stepConfig.isReadOnlyForInProgressSubmission(obj); } + } diff --git a/dspace-api/src/main/java/org/dspace/versioning/ItemCorrectionProvider.java b/dspace-api/src/main/java/org/dspace/versioning/ItemCorrectionProvider.java index 0a031ede318f..a65ac8af97e1 100644 --- a/dspace-api/src/main/java/org/dspace/versioning/ItemCorrectionProvider.java +++ b/dspace-api/src/main/java/org/dspace/versioning/ItemCorrectionProvider.java @@ -193,10 +193,25 @@ protected void updateBundlesAndBitstreams(Context c, Item itemNew, Item nativeIt List nativeBundles = nativeItem.getBundles(bundleName); List correctedBundles = itemNew.getBundles(bundleName); - if (CollectionUtils.isEmpty(nativeBundles) || CollectionUtils.isEmpty(correctedBundles)) { + if (CollectionUtils.isEmpty(nativeBundles) && CollectionUtils.isEmpty(correctedBundles)) { continue; } - updateBundleAndBitstreams(c, nativeBundles.get(0), correctedBundles.get(0)); + + Bundle nativeBundle; + if (CollectionUtils.isEmpty(nativeBundles)) { + nativeBundle = bundleService.create(c, nativeItem, bundleName); + } else { + nativeBundle = nativeBundles.get(0); + } + + Bundle correctedBundle; + if (CollectionUtils.isEmpty(correctedBundles)) { + correctedBundle = bundleService.create(c, nativeItem, bundleName); + } else { + correctedBundle = correctedBundles.get(0); + } + + updateBundleAndBitstreams(c, nativeBundle, correctedBundle); } } @@ -246,7 +261,26 @@ protected void updateBundleAndBitstreams(Context c, Bundle nativeBundle, Bundle } } + deleteBitstreams(nativeBundle, correctedBundle); bundleService.update(c, nativeBundle); + if (nativeBundle.getItems().isEmpty()) { + bundleService.delete(c, nativeBundle); + } + } + + private void deleteBitstreams(Bundle nativeBundle, Bundle correctedBundle) { + for (Bitstream bitstream : nativeBundle.getBitstreams()) { + if (contains(correctedBundle, bitstream)) { + continue; + } + nativeBundle.removeBitstream(bitstream); + } + } + + private boolean contains(Bundle bundle, Bitstream bitstream) { + return bundle.getBitstreams().stream() + .map(Bitstream::getChecksum) + .anyMatch(cs -> bitstream.getChecksum().equals(cs)); } protected Bitstream findBitstreamByChecksum(Bundle bundle, String bitstreamChecksum) { 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 c00b82260ea4..e5b943c5c20f 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 @@ -144,7 +144,7 @@ - + diff --git a/dspace-api/src/test/data/dspaceFolder/assetstore/bulk-import/update-items.xls b/dspace-api/src/test/data/dspaceFolder/assetstore/bulk-import/update-items.xls index 01d923643cf0..2d30ccfd275f 100644 Binary files a/dspace-api/src/test/data/dspaceFolder/assetstore/bulk-import/update-items.xls and b/dspace-api/src/test/data/dspaceFolder/assetstore/bulk-import/update-items.xls differ 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/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index 34acf16fa41d..0b7def31ca3a 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -57,7 +57,6 @@ org.dspace.app.rest.submit.step.CollectionStep collection - submission submit.progressbar.describe.stepone @@ -245,18 +244,46 @@ correction workflow - + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form - + submit.progressbar.identifiers org.dspace.app.rest.submit.step.ShowIdentifiersStep identifiers + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + workflow + + + + submit.progressbar.describe.stepone + org.dspace.app.rest.submit.step.DescribeStep + submission-form + submission + + + + + org.dspace.app.rest.submit.step.CollectionStep + collection + + + + + org.dspace.app.rest.submit.step.CollectionStep + collection + submission + + @@ -320,6 +347,13 @@ + + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/edititem-service.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/edititem-service.xml index 95f341fac20c..787bb5ebdd5a 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/edititem-service.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/edititem-service.xml @@ -90,6 +90,15 @@ + + + + + ADMIN + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/identifier-service.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/identifier-service.xml index 206b801d0842..12a3b4e9565d 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/identifier-service.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/identifier-service.xml @@ -41,6 +41,12 @@ - + + + + + + + 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..8daa7cd4ae02 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/test-beans.xml @@ -22,6 +22,16 @@ + + + + + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml b/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml index 025561e92377..8ebe20c34f27 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml @@ -1955,7 +1955,7 @@ You can leave out the day and/or month if they aren't applicable. - +
      @@ -1996,7 +1996,35 @@ You can leave out the day and/or month if they aren't applicable.
      - + +
      + + + dc + title + + false + + onebox + Field required + + +
      + +
      + + + dc + type + + false + + onebox + Field required + + +
      + 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/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..bd01d3519bc5 100644 --- a/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java +++ b/dspace-api/src/test/java/org/dspace/content/integration/crosswalks/ReferCrosswalkIT.java @@ -69,6 +69,7 @@ import org.dspace.eperson.EPerson; import org.dspace.layout.CrisLayoutBox; import org.dspace.layout.LayoutSecurity; +import org.dspace.services.ConfigurationService; import org.dspace.utils.DSpace; import org.json.JSONObject; import org.junit.After; @@ -82,6 +83,7 @@ * */ public class ReferCrosswalkIT extends AbstractIntegrationTestWithDatabase { + static final String CFG_PREFIX = "identifier.doi.prefix"; private static final String BASE_OUTPUT_DIR_PATH = "./target/testing/dspace/assetstore/crosswalk/"; @@ -99,6 +101,8 @@ public class ReferCrosswalkIT extends AbstractIntegrationTestWithDatabase { private VirtualField virtualFieldId; + private ConfigurationService configurationService; + @Before public void setup() throws SQLException, AuthorizeException { @@ -117,6 +121,8 @@ public void setup() throws SQLException, AuthorizeException { when(mockedVirtualFieldId.getMetadata(any(), any(), any())).thenReturn(new String[] { "mock-id" }); this.virtualFieldMapper.setVirtualField("id", mockedVirtualFieldId); + this.configurationService = new DSpace().getSingletonService(ConfigurationService.class); + context.turnOffAuthorisationSystem(); community = createCommunity(context).build(); collection = createCollection(context, community).withAdminGroup(eperson).build(); @@ -2530,6 +2536,82 @@ public void testVirtualBitstreamFieldWithProject() throws Exception { assertThat(resultLines[54].trim(), equalTo("")); } + @Test + public void testExportToDataciteFormatItemWithThreeDOI() throws Exception { + String prefix; + prefix = this.configurationService.getProperty(CFG_PREFIX); + if (null == prefix) { + throw new RuntimeException("Unable to load DOI prefix from " + + "configuration. Cannot find property " + + CFG_PREFIX + "."); + } + + context.turnOffAuthorisationSystem(); + + Item publication = createItem(context, collection) + .withEntityType("Publication") + .withTitle("publication title") + .withDoiIdentifier("test doi") + .withDoiIdentifier("test doi2") + .withDoiIdentifier("test" + prefix + "test") + .build(); + + context.restoreAuthSystemState(); + + ReferCrosswalk referCrosswalk = new DSpace().getServiceManager() + .getServiceByName("referCrosswalkVirtualFieldDOI", ReferCrosswalk.class); + assertThat(referCrosswalk, notNullValue()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publication, out); + + String[] resultLines = out.toString().split("\n"); + + assertThat(resultLines.length, is(5)); + assertThat(resultLines[0].trim(), is("{")); + assertThat(resultLines[1].trim(), is("\"primary-doi\": \"test" + prefix + "test\",")); + assertThat(resultLines[2].trim(), is("\"alternative-doi\": \"test doi\",")); + assertThat(resultLines[3].trim(), is("\"alternative-doi\": \"test doi2\"")); + assertThat(resultLines[4].trim(), is("}")); + } + + @Test + public void testExportToDataciteFormatItemWithSingleDOINotMatchingPrefix() throws Exception { + String prefix; + prefix = this.configurationService.getProperty(CFG_PREFIX); + if (null == prefix) { + throw new RuntimeException("Unable to load DOI prefix from " + + "configuration. Cannot find property " + + CFG_PREFIX + "."); + } + + context.turnOffAuthorisationSystem(); + + Item publication = createItem(context, collection) + .withEntityType("Publication") + .withTitle("publication title") + .withDoiIdentifier("test doi") + .build(); + + context.restoreAuthSystemState(); + + ReferCrosswalk referCrosswalk = new DSpace().getServiceManager() + .getServiceByName("referCrosswalkVirtualFieldDOI", ReferCrosswalk.class); + assertThat(referCrosswalk, notNullValue()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + referCrosswalk.disseminate(context, publication, out); + + String[] resultLines = out.toString().split("\n"); + + assertThat(resultLines.length, is(3)); + assertThat(resultLines[0].trim(), is("{")); + assertThat(resultLines[1].trim(), is("\"primary-doi\": \"test doi\"")); + assertThat(resultLines[2].trim(), is("}")); + } + + + private void createSelectedRelationship(Item author, Item publication, RelationshipType selectedRelationshipType) { createRelationshipBuilder(context, publication, author, selectedRelationshipType, -1, -1).build(); diff --git a/dspace-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-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 e27a3ee947cb..2db53bff9be5 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 @@ -36,6 +36,7 @@ import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Options; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.solr.client.solrj.SolrClient; @@ -81,7 +82,7 @@ */ @SuppressWarnings("deprecation") public class XOAI { - private static Logger log = LogManager.getLogger(XOAI.class); + private static final Logger log = LogManager.getLogger(XOAI.class); // needed because the solr query only returns 10 rows by default private final Context context; @@ -104,7 +105,7 @@ public class XOAI { private final static ConfigurationService configurationService = DSpaceServicesFactory.getInstance() .getConfigurationService(); - private List extensionPlugins; + private final List extensionPlugins; private List getFileFormats(Item item) { List formats = new ArrayList<>(); @@ -151,9 +152,9 @@ private void println(String line) { } public int index() throws DSpaceSolrIndexerException { - int result = 0; - try { + int result; + try { if (clean) { clearIndex(); System.out.println("Using full import."); @@ -169,8 +170,8 @@ public int index() throws DSpaceSolrIndexerException { } else { result = this.index((Date) results.get(0).getFieldValue("item.lastmodified")); } - } + solrServerResolver.getServer().commit(); if (optimize) { @@ -214,7 +215,7 @@ private int index(Date last) throws DSpaceSolrIndexerException, IOException { * @param last maximum date for an item to be considered for an update * @return Iterator over list of items which might have changed their visibility * since the last update. - * @throws DSpaceSolrIndexerException + * @throws DSpaceSolrIndexerException e */ private Iterator getItemsWithPossibleChangesBefore(Date last) throws DSpaceSolrIndexerException, IOException { try { @@ -365,7 +366,7 @@ private int index(Iterator iterator) throws DSpaceSolrIndexerException { * * @param item Item * @return date - * @throws SQLException + * @throws SQLException e */ private Date getMostRecentModificationDate(Item item) throws SQLException { List dates = new LinkedList<>(); @@ -398,7 +399,8 @@ private SolrInputDocument index(Item item) SolrInputDocument doc = new SolrInputDocument(); doc.addField("item.id", item.getID().toString()); - String handle = item.getHandle(); + 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); boolean isEmbargoed = !this.isPublic(item); @@ -418,7 +420,7 @@ private SolrInputDocument index(Item item) * future will be marked as such. */ - boolean isPublic = isEmbargoed ? (isIndexed ? isCurrentlyVisible : false) : true; + boolean isPublic = !isEmbargoed || (isIndexed && isCurrentlyVisible); doc.addField("item.public", isPublic); // if the visibility of the item will change in the future due to an @@ -433,8 +435,7 @@ private SolrInputDocument index(Item item) * because this will override the item.public flag. */ - doc.addField("item.deleted", - (item.isWithdrawn() || !item.isDiscoverable() || (isEmbargoed ? isPublic : false))); + doc.addField("item.deleted", (item.isWithdrawn() || !item.isDiscoverable() || (isEmbargoed && isPublic))); /* * An item that is embargoed will potentially not be harvested by incremental @@ -574,8 +575,8 @@ private static void cleanCache(XOAIItemCacheService xoaiItemCacheService, XOAICa public static void main(String[] argv) throws IOException, ConfigurationException { - AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext( - new Class[] { BasicConfiguration.class }); + AnnotationConfigApplicationContext applicationContext = + new AnnotationConfigApplicationContext(BasicConfiguration.class); XOAICacheService cacheService = applicationContext.getBean(XOAICacheService.class); XOAIItemCacheService itemCacheService = applicationContext.getBean(XOAIItemCacheService.class); @@ -596,10 +597,9 @@ public static void main(String[] argv) throws IOException, ConfigurationExceptio String[] validDatabaseCommands = { COMMAND_CLEAN_CACHE, COMMAND_COMPILE_ITEMS, COMMAND_ERASE_COMPILED_ITEMS }; - boolean solr = true; // Assuming solr by default - solr = !("database").equals(configurationService.getProperty("oai.storage", "solr")); - + boolean solr = !("database").equals(configurationService.getProperty("oai.storage", "solr")); boolean run = false; + if (line.getArgs().length > 0) { if (solr) { if (Arrays.asList(validSolrCommands).contains(line.getArgs()[0])) { diff --git a/dspace-oai/src/main/java/org/dspace/xoai/filter/DSpaceAuthorizationFilter.java b/dspace-oai/src/main/java/org/dspace/xoai/filter/DSpaceAuthorizationFilter.java index ebb19c84b5e2..baf8552a6029 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/filter/DSpaceAuthorizationFilter.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/filter/DSpaceAuthorizationFilter.java @@ -9,12 +9,17 @@ package org.dspace.xoai.filter; import java.sql.SQLException; +import java.util.Iterator; +import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.handle.factory.HandleServiceFactory; import org.dspace.handle.service.HandleService; @@ -33,6 +38,9 @@ public class DSpaceAuthorizationFilter extends DSpaceFilter { private static final HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); + private static final ItemService itemService + = ContentServiceFactory.getInstance().getItemService(); + @Override public boolean isShown(DSpaceItem item) { boolean pub = false; @@ -43,6 +51,11 @@ public boolean isShown(DSpaceItem item) { return false; } Item dspaceItem = (Item) handleService.resolveToObject(context, handle); + + if (dspaceItem == null) { + dspaceItem = fromLegacyIdentifier(item); + } + if (dspaceItem == null) { return false; } @@ -55,6 +68,25 @@ public boolean isShown(DSpaceItem item) { return pub; } + private Item fromLegacyIdentifier(DSpaceItem item) { + List legacyIdentifier = item.getMetadata("dspace.legacy.oai-identifier"); + if (legacyIdentifier.isEmpty()) { + return null; + } + try { + Iterator + iterator = itemService.findUnfilteredByMetadataField( + context, "dspace", "legacy", "oai-identifier", + legacyIdentifier.get(0)); + if (!iterator.hasNext()) { + return null; + } + return iterator.next(); + } catch (AuthorizeException | SQLException e) { + throw new RuntimeException(e); + } + } + @Override public SolrFilterResult buildSolrQuery() { return new SolrFilterResult("item.public:true"); 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/Application.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java index c85efed835bf..02fd8ad0166a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/Application.java @@ -12,6 +12,7 @@ import java.util.List; import javax.servlet.Filter; +import org.apache.commons.lang3.ArrayUtils; import org.dspace.app.rest.filter.DSpaceRequestContextFilter; import org.dspace.app.rest.model.hateoas.DSpaceLinkRelationProvider; import org.dspace.app.rest.parameter.resolver.SearchFilterResolver; @@ -187,9 +188,28 @@ public void addCorsMappings(@NonNull CorsRegistry registry) { .getCorsAllowedOrigins(configuration.getCorsAllowedOriginsConfig()); String[] iiifAllowedOrigins = configuration .getCorsAllowedOrigins(configuration.getIiifAllowedOriginsConfig()); + String[] bitstreamAllowedOrigins = configuration + .getCorsAllowedOrigins(configuration.getBitstreamAllowedOriginsConfig()); boolean corsAllowCredentials = configuration.getCorsAllowCredentials(); boolean iiifAllowCredentials = configuration.getIiifAllowCredentials(); + boolean bitstreamAllowCredentials = configuration.getBitstreamsAllowCredentials(); + + if (ArrayUtils.isEmpty(bitstreamAllowedOrigins)) { + bitstreamAllowedOrigins = corsAllowedOrigins; + } + if (!ArrayUtils.isEmpty(bitstreamAllowedOrigins)) { + registry.addMapping("/api/core/bitstreams/**").allowedMethods(CorsConfiguration.ALL) + // Set Access-Control-Allow-Credentials to "true" and specify which origins are valid + // for our Access-Control-Allow-Origin header + .allowCredentials(bitstreamAllowCredentials).allowedOrigins(bitstreamAllowedOrigins) + // Allow list of request preflight headers allowed to be sent to us from the client + .allowedHeaders("Accept", "Authorization", "Content-Type", "Origin", "X-On-Behalf-Of", + "X-Requested-With", "X-XSRF-TOKEN", "X-CORRELATION-ID", "X-REFERRER", + "x-recaptcha-token", "Access-Control-Allow-Origin") + // Allow list of response headers allowed to be sent by us (the server) to the client + .exposedHeaders("Authorization", "DSPACE-XSRF-TOKEN", "Location", "WWW-Authenticate"); + } if (corsAllowedOrigins != null) { registry.addMapping("/api/**").allowedMethods(CorsConfiguration.ALL) // Set Access-Control-Allow-Credentials to "true" and specify which origins are valid 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/authorization/impl/CanSubscribeFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSubscribeFeature.java index 2e0e27b05751..fb1398448691 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSubscribeFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSubscribeFeature.java @@ -17,6 +17,7 @@ import org.dspace.app.rest.model.BaseObjectRest; import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.CommunityRest; +import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.utils.Utils; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.DSpaceObject; @@ -55,7 +56,8 @@ public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLEx public String[] getSupportedTypes() { return new String[]{ CommunityRest.CATEGORY + "." + CommunityRest.NAME, - CollectionRest.CATEGORY + "." + CollectionRest.NAME + CollectionRest.CATEGORY + "." + CollectionRest.NAME, + ItemRest.CATEGORY + "." + ItemRest.NAME }; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java index 74a3c7264e75..248342e1b19b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java @@ -101,6 +101,10 @@ protected void fillFromModel(T obj, R witem, Projection projection) { for (SubmissionSectionRest sections : def.getPanels()) { SubmissionStepConfig stepConfig = submissionSectionConverter.toModel(sections); + if (stepConfig.isHiddenForInProgressSubmission(obj)) { + continue; + } + /* * First, load the step processing class (using the current * class loader) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/EditItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/EditItemConverter.java index eb2dedef3e62..24c8761268c5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/EditItemConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/EditItemConverter.java @@ -101,6 +101,10 @@ protected void fillFromModel(EditItem obj, EditItemRest rest, Projection project for (SubmissionSectionRest sections : def.getPanels()) { SubmissionStepConfig stepConfig = submissionSectionConverter.toModel(sections); + if (stepConfig.isHiddenForInProgressSubmission(obj)) { + continue; + } + /* * First, load the step processing class (using the current * class loader) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java index 76aca4be231d..da47f3d8b659 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java @@ -35,7 +35,7 @@ * Converter to translate between lists of domain {@link MetadataValue}s and {@link MetadataRest} representations. */ @Component -public class MetadataConverter implements DSpaceConverter { +public class MetadataConverter implements DSpaceConverter> { @Autowired private ContentServiceFactory contentServiceFactory; @@ -46,7 +46,7 @@ public class MetadataConverter implements DSpaceConverter convert(MetadataValueList metadataValues, Projection projection) { // Convert each value to a DTO while retaining place order in a map of key -> SortedSet Map> mapOfSortedSets = new HashMap<>(); @@ -60,7 +60,7 @@ public MetadataRest convert(MetadataValueList metadataValues, set.add(converter.toRest(metadataValue, projection)); } - MetadataRest metadataRest = new MetadataRest(); + MetadataRest metadataRest = new MetadataRest<>(); // Populate MetadataRest's map of key -> List while respecting SortedSet's order Map> mapOfLists = metadataRest.getMap(); @@ -80,14 +80,14 @@ public Class getModelClass() { * Sets a DSpace object's domain metadata values from a rest representation. * Any existing metadata value is deleted or overwritten. * - * @param context the context to use. - * @param dso the DSpace object. + * @param context the context to use. + * @param dso the DSpace object. * @param metadataRest the rest representation of the new metadata. - * @throws SQLException if a database error occurs. + * @throws SQLException if a database error occurs. * @throws AuthorizeException if an authorization error occurs. */ public void setMetadata(Context context, T dso, MetadataRest metadataRest) - throws SQLException, AuthorizeException { + throws SQLException, AuthorizeException { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); dsoService.clearMetadata(context, dso, Item.ANY, Item.ANY, Item.ANY, Item.ANY); persistMetadataRest(context, dso, metadataRest, dsoService); @@ -97,14 +97,14 @@ public void setMetadata(Context context, T dso, Metadat * Add to a DSpace object's domain metadata values from a rest representation. * Any existing metadata value is preserved. * - * @param context the context to use. - * @param dso the DSpace object. + * @param context the context to use. + * @param dso the DSpace object. * @param metadataRest the rest representation of the new metadata. - * @throws SQLException if a database error occurs. + * @throws SQLException if a database error occurs. * @throws AuthorizeException if an authorization error occurs. */ public void addMetadata(Context context, T dso, MetadataRest metadataRest) - throws SQLException, AuthorizeException { + throws SQLException, AuthorizeException { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); persistMetadataRest(context, dso, metadataRest, dsoService); } @@ -113,33 +113,34 @@ public void addMetadata(Context context, T dso, Metadat * Merge into a DSpace object's domain metadata values from a rest representation. * Any existing metadata value is preserved or overwritten with the new ones * - * @param context the context to use. - * @param dso the DSpace object. + * @param context the context to use. + * @param dso the DSpace object. * @param metadataRest the rest representation of the new metadata. - * @throws SQLException if a database error occurs. + * @throws SQLException if a database error occurs. * @throws AuthorizeException if an authorization error occurs. */ - public void mergeMetadata(Context context, T dso, MetadataRest metadataRest) - throws SQLException, AuthorizeException { + public void mergeMetadata( + Context context, T dso, MetadataRest metadataRest + ) throws SQLException, AuthorizeException { DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); - for (Map.Entry> entry: metadataRest.getMap().entrySet()) { + for (Map.Entry> entry : metadataRest.getMap().entrySet()) { List metadataByMetadataString = dsoService.getMetadataByMetadataString(dso, entry.getKey()); dsoService.removeMetadataValues(context, dso, metadataByMetadataString); } persistMetadataRest(context, dso, metadataRest, dsoService); } - private void persistMetadataRest(Context context, T dso, MetadataRest metadataRest, - DSpaceObjectService dsoService) - throws SQLException, AuthorizeException { - for (Map.Entry> entry: metadataRest.getMap().entrySet()) { + private void persistMetadataRest( + Context context, T dso, MetadataRest metadataRest, DSpaceObjectService dsoService + ) throws SQLException, AuthorizeException { + for (Map.Entry> entry : metadataRest.getMap().entrySet()) { String[] seq = entry.getKey().split("\\."); String schema = seq[0]; String element = seq[1]; String qualifier = seq.length == 3 ? seq[2] : null; - for (MetadataValueRest mvr: entry.getValue()) { + for (MetadataValueRest mvr : entry.getValue()) { dsoService.addMetadata(context, dso, schema, element, qualifier, mvr.getLanguage(), - mvr.getValue(), mvr.getAuthority(), mvr.getConfidence()); + mvr.getValue(), mvr.getAuthority(), mvr.getConfidence()); } } dsoService.update(context, dso); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.java new file mode 100644 index 000000000000..5b742366b582 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/RegistrationDataConverter.java @@ -0,0 +1,140 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.converter; + +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.MetadataRest; +import org.dspace.app.rest.model.RegistrationMetadataRest; +import org.dspace.app.rest.model.RegistrationRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.MetadataValue; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.RegistrationData; +import org.dspace.eperson.RegistrationTypeEnum; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.AccountService; +import org.dspace.eperson.service.RegistrationDataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +@Component +public class RegistrationDataConverter implements DSpaceConverter { + + @Autowired + private HttpServletRequest request; + + @Autowired + private RegistrationDataService registrationDataService; + + @Override + public RegistrationRest convert(RegistrationData registrationData, Projection projection) { + + if (registrationData == null) { + return null; + } + + Context context = ContextUtil.obtainContext(request); + + AccountService accountService = EPersonServiceFactory.getInstance().getAccountService(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setId(registrationData.getID()); + registrationRest.setEmail(registrationData.getEmail()); + registrationRest.setNetId(registrationData.getNetId()); + registrationRest.setRegistrationType( + Optional.ofNullable(registrationData.getRegistrationType()) + .map(RegistrationTypeEnum::toString) + .orElse(null) + ); + + EPerson ePerson = null; + try { + ePerson = accountService.getEPerson(context, registrationData.getToken()); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e); + } + + if (ePerson != null) { + registrationRest.setUser(ePerson.getID()); + try { + MetadataRest metadataRest = getMetadataRest(ePerson, registrationData); + if (registrationData.getEmail() != null) { + metadataRest.put( + "email", + new RegistrationMetadataRest(registrationData.getEmail(), ePerson.getEmail()) + ); + } + registrationRest.setRegistrationMetadata(metadataRest); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } else { + registrationRest.setRegistrationMetadata(getMetadataRest(registrationData)); + } + + registrationRest.setGroupNames(getGroupNames(registrationData)); + registrationRest.setGroups( + registrationData.getGroups().stream().map(Group::getID).collect(Collectors.toList()) + ); + return registrationRest; + } + + + private MetadataRest getMetadataRest(EPerson ePerson, RegistrationData registrationData) + throws SQLException { + return registrationDataService.groupEpersonMetadataByRegistrationData(ePerson, registrationData) + .reduce( + new MetadataRest<>(), + (map, entry) -> map.put( + entry.getKey().getMetadataField().toString('.'), + new RegistrationMetadataRest( + entry.getKey().getValue(), + entry.getValue().map(MetadataValue::getValue).orElse(null) + ) + ), + (m1, m2) -> { + m1.getMap().putAll(m2.getMap()); + return m1; + } + ); + } + + private MetadataRest getMetadataRest(RegistrationData registrationData) { + MetadataRest metadataRest = new MetadataRest<>(); + registrationData.getMetadata().forEach( + (m) -> metadataRest.put( + m.getMetadataField().toString('.'), + new RegistrationMetadataRest(m.getValue()) + ) + ); + return metadataRest; + } + + private List getGroupNames(RegistrationData registrationData) { + return registrationData.getGroups().stream() + .map(Group::getName) + .collect(Collectors.toList()); + } + + @Override + public Class getModelClass() { + return RegistrationData.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index edefd7434e76..72133e3de530 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -137,7 +137,7 @@ protected void handleUnprocessableEntityException(HttpServletRequest request, Ht Exception ex) throws IOException { //422 is not defined in HttpServletResponse. Its meaning is "Unprocessable Entity". //Using the value from HttpStatus. - sendErrorResponse(request, response, null, + sendErrorResponse(request, response, ex, "Unprocessable or invalid entity", HttpStatus.UNPROCESSABLE_ENTITY.value()); } @@ -145,7 +145,7 @@ protected void handleUnprocessableEntityException(HttpServletRequest request, Ht @ExceptionHandler( {InvalidSearchRequestException.class}) protected void handleInvalidSearchRequestException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { - sendErrorResponse(request, response, null, + sendErrorResponse(request, response, ex, "Invalid search request", HttpStatus.UNPROCESSABLE_ENTITY.value()); } @@ -184,7 +184,7 @@ protected void handleCustomUnprocessableEntityException(HttpServletRequest reque TranslatableException ex) throws IOException { Context context = ContextUtil.obtainContext(request); sendErrorResponse( - request, response, null, ex.getLocalizedMessage(context), HttpStatus.UNPROCESSABLE_ENTITY.value() + request, response, (Exception) ex, ex.getLocalizedMessage(context), HttpStatus.UNPROCESSABLE_ENTITY.value() ); } @@ -200,7 +200,7 @@ protected ResponseEntity handleCustomUnprocessableEditException(HttpServ protected void ParameterConversionException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { // we want the 400 status for missing parameters, see https://jira.lyrasis.org/browse/DS-4428 - sendErrorResponse(request, response, null, + sendErrorResponse(request, response, ex, "A required parameter is invalid", HttpStatus.BAD_REQUEST.value()); } @@ -209,7 +209,7 @@ protected void ParameterConversionException(HttpServletRequest request, HttpServ protected void MissingParameterException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { // we want the 400 status for missing parameters, see https://jira.lyrasis.org/browse/DS-4428 - sendErrorResponse(request, response, null, + sendErrorResponse(request, response, ex, "A required parameter is missing", HttpStatus.BAD_REQUEST.value()); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java index 1b71eb8957a2..e7b43ebe33c2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java @@ -20,7 +20,7 @@ public abstract class DSpaceObjectRest extends BaseObjectRest { private String name; private String handle; - MetadataRest metadata = new MetadataRest(); + MetadataRest metadata = new MetadataRest<>(); @Override public String getId() { @@ -56,11 +56,11 @@ public void setHandle(String handle) { * * @return the metadata. */ - public MetadataRest getMetadata() { + public MetadataRest getMetadata() { return metadata; } - public void setMetadata(MetadataRest metadata) { + public void setMetadata(MetadataRest metadata) { this.metadata = metadata; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java index d1367c8fea82..072acbcfd71e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/MetadataRest.java @@ -19,10 +19,10 @@ /** * Rest representation of a map of metadata keys to ordered lists of values. */ -public class MetadataRest { +public class MetadataRest { @JsonAnySetter - private SortedMap> map = new TreeMap(); + private SortedMap> map = new TreeMap(); /** * Gets the map. @@ -30,7 +30,7 @@ public class MetadataRest { * @return the map of keys to ordered values. */ @JsonAnyGetter - public SortedMap> getMap() { + public SortedMap> getMap() { return map; } @@ -44,16 +44,16 @@ public SortedMap> getMap() { * they are passed to this method. * @return this instance, to support chaining calls for easy initialization. */ - public MetadataRest put(String key, MetadataValueRest... values) { + public MetadataRest put(String key, T... values) { // determine highest explicitly ordered value int highest = -1; - for (MetadataValueRest value : values) { + for (T value : values) { if (value.getPlace() > highest) { highest = value.getPlace(); } } // add any non-explicitly ordered values after highest - for (MetadataValueRest value : values) { + for (T value : values) { if (value.getPlace() < 0) { highest++; value.setPlace(highest); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java new file mode 100644 index 000000000000..370bd9027f62 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationMetadataRest.java @@ -0,0 +1,37 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.model; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class RegistrationMetadataRest extends MetadataValueRest { + + @JsonInclude(JsonInclude.Include.NON_NULL) + private String overrides; + + public RegistrationMetadataRest(String value, String overrides) { + super(); + this.value = value; + this.overrides = overrides; + } + + public RegistrationMetadataRest(String value) { + this(value, null); + } + + public String getOverrides() { + return overrides; + } + + public void setOverrides(String overrides) { + this.overrides = overrides; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java index 191aec88a414..7285a01a4a24 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RegistrationRest.java @@ -27,14 +27,28 @@ public class RegistrationRest extends RestAddressableModel { public static final String NAME_PLURAL = "registrations"; public static final String CATEGORY = EPERSON; + private Integer id; private String email; private UUID user; + private String registrationType; + private String netId; + @JsonInclude(JsonInclude.Include.NON_NULL) + private MetadataRest registrationMetadata; @JsonInclude(JsonInclude.Include.NON_NULL) private List groupNames = Collections.emptyList(); - private List groups = Collections.emptyList(); + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + /** * Generic getter for the email + * * @return the email value of this RegisterRest */ public String getEmail() { @@ -43,7 +57,8 @@ public String getEmail() { /** * Generic setter for the email - * @param email The email to be set on this RegisterRest + * + * @param email The email to be set on this RegisterRest */ public void setEmail(String email) { this.email = email; @@ -51,6 +66,7 @@ public void setEmail(String email) { /** * Generic getter for the user + * * @return the user value of this RegisterRest */ public UUID getUser() { @@ -59,12 +75,38 @@ public UUID getUser() { /** * Generic setter for the user - * @param user The user to be set on this RegisterRest + * + * @param user The user to be set on this RegisterRest */ public void setUser(UUID user) { this.user = user; } + public String getRegistrationType() { + return registrationType; + } + + public void setRegistrationType(String registrationType) { + this.registrationType = registrationType; + } + + public String getNetId() { + return netId; + } + + public void setNetId(String netId) { + this.netId = netId; + } + + public MetadataRest getRegistrationMetadata() { + return registrationMetadata; + } + + public void setRegistrationMetadata( + MetadataRest registrationMetadata) { + this.registrationMetadata = registrationMetadata; + } + public List getGroups() { return groups; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java index 566917854532..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 subscriptionParameterList = subscriptionRest.getSubscriptionParameterList(); + if (CollectionUtils.isNotEmpty(subscriptionParameterList)) { + List subscriptionParameters = new ArrayList<>(); + validateParameters(subscriptionRest, subscriptionParameterList, subscriptionParameters); + subscription = subscribeService.subscribe(context, ePerson, dSpaceObject, subscriptionParameters, + subscriptionRest.getSubscriptionType()); } + context.commit(); + return converter.toRest(subscription, utils.obtainProjection()); } catch (SQLException sqlException) { throw new SQLException(sqlException.getMessage(), sqlException); } catch (IOException ioException) { 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/AbstractTopSolrStatsFieldGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractTopSolrStatsFieldGenerator.java index 9d04803c71ea..6d8242ca9fb1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractTopSolrStatsFieldGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/AbstractTopSolrStatsFieldGenerator.java @@ -66,8 +66,8 @@ Dataset getTypeStatsDataset(Context context, DSpaceObject dso, String typeAxisSt } else { hasValidRelation = true; - query = statisticsDatasetDisplay - .composeQueryWithInverseRelation(dso, discoveryConfiguration.getDefaultFilterQueries()); + query = statisticsDatasetDisplay.composeQueryWithInverseRelation( + dso, discoveryConfiguration.getDefaultFilterQueries(), getDsoType(dso)); } } 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 58532e46bf0f..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); } @@ -93,7 +98,8 @@ private int getCategoryCount(DSpaceObject dso, DiscoveryConfiguration discoveryC private String composeCategoryQuery(DSpaceObject dso, DiscoveryConfiguration configuration, String categoryQuery) { List defaultFilterQueries = configuration.getDefaultFilterQueries(); - String query = new StatisticsDatasetDisplay().composeQueryWithInverseRelation(dso, defaultFilterQueries); + String query = new StatisticsDatasetDisplay().composeQueryWithInverseRelation(dso, + defaultFilterQueries, dso.getType()); if (categoryQuery.equals(OTHER_CATEGORY)) { return query + " AND " + getAllCategoryQueriesReverted(); @@ -104,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 ")); @@ -129,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/statistics/TopItemsGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopItemsGenerator.java index d0805c68de36..1e67b0cac66c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopItemsGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TopItemsGenerator.java @@ -81,9 +81,8 @@ public UsageReportRest createUsageReport(Context context, DSpaceObject root, Str } else { hasValidRelation = true; - query = statisticsDatasetDisplay - .composeQueryWithInverseRelation(root, - discoveryConfiguration.getDefaultFilterQueries()); + query = statisticsDatasetDisplay.composeQueryWithInverseRelation(root, + discoveryConfiguration.getDefaultFilterQueries(), getDsoType()); } } if (!hasValidRelation) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalDownloadsAndVisitsGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalDownloadsAndVisitsGenerator.java index 2a1d95e0c291..620333ce61b9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalDownloadsAndVisitsGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalDownloadsAndVisitsGenerator.java @@ -59,9 +59,8 @@ public UsageReportRest createUsageReport(Context context, DSpaceObject dso, Stri } else { hasValidRelation = true; - query = statisticsDatasetDisplay. - composeQueryWithInverseRelation( - dso, discoveryConfiguration.getDefaultFilterQueries()); + query = statisticsDatasetDisplay.composeQueryWithInverseRelation(dso, + discoveryConfiguration.getDefaultFilterQueries(), dso.getType()); } } if (!hasValidRelation) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalVisitGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalVisitGenerator.java index 98ee393b5cd3..e419ac660fd6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalVisitGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalVisitGenerator.java @@ -118,7 +118,7 @@ Dataset getDSOStatsDataset(Context context, DSpaceObject dso, int dsoType, Strin } else { hasValidRelation = true; query = statisticsDatasetDisplay.composeQueryWithInverseRelation( - dso, discoveryConfiguration.getDefaultFilterQueries()); + dso, discoveryConfiguration.getDefaultFilterQueries(), dso.getType()); type_of_dso = dso.getType(); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalVisitPerPeriodGenerator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalVisitPerPeriodGenerator.java index 5f7f53898abe..465f1022372a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalVisitPerPeriodGenerator.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/statistics/TotalVisitPerPeriodGenerator.java @@ -97,7 +97,8 @@ public UsageReportRest createUsageReport(Context context, DSpaceObject dso, Stri statisticsDatasetDisplay .composeQueryWithInverseRelation( dso, - discoveryConfiguration.getDefaultFilterQueries() + discoveryConfiguration.getDefaultFilterQueries(), + getDsoType(dso) ) ); } 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 4b2dddd6b707..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 @@ -36,6 +36,11 @@ public class ApplicationConfig { @Value("${iiif.cors.allowed-origins}") private String[] iiifCorsAllowedOrigins; + // Allowed IIIF CORS origins ("Access-Control-Allow-Origin" header) + // Can be overridden in DSpace configuration + @Value("${rest.cors.bitstream-allowed-origins}") + private String[] bitstreamCorsAllowedOrigins; + // Whether to allow credentials (cookies) in CORS requests ("Access-Control-Allow-Credentials" header) // Defaults to true. Can be overridden in DSpace configuration @Value("${rest.cors.allow-credentials:true}") @@ -46,6 +51,11 @@ public class ApplicationConfig { @Value("${iiif.cors.allow-credentials:true}") private boolean iiifCorsAllowCredentials; + // Whether to allow credentials (cookies) in CORS requests ("Access-Control-Allow-Credentials" header) + // Defaults to true. Can be overridden in DSpace configuration + @Value("${rest.cors.bitstream-allow-credentials:true}") + private boolean bitstreamsCorsAllowCredentials; + // Configured User Interface URL (default: http://localhost:4000) @Value("${dspace.ui.url:http://localhost:4000}") private String uiURL; @@ -91,6 +101,14 @@ public String[] getIiifAllowedOriginsConfig() { return this.iiifCorsAllowedOrigins; } + /** + * Returns the bitstream.cors.allowed-origins (for Bitstream access) defined in DSpace configuration. + * @return allowed origins + */ + public String[] getBitstreamAllowedOriginsConfig() { + return this.bitstreamCorsAllowedOrigins; + } + /** * Return whether to allow credentials (cookies) on CORS requests. This is used to set the * CORS "Access-Control-Allow-Credentials" header in Application class. @@ -108,4 +126,13 @@ public boolean getCorsAllowCredentials() { public boolean getIiifAllowCredentials() { return iiifCorsAllowCredentials; } + + /** + * Return whether to allow credentials (cookies) on IIIF requests. This is used to set the + * CORS "Access-Control-Allow-Credentials" header in Application class. Defaults to false. + * @return true or false + */ + public boolean getBitstreamsAllowCredentials() { + return bitstreamsCorsAllowCredentials; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java index 90f7ec252542..47dbcef1749d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/UsageReportUtils.java @@ -101,6 +101,12 @@ public class UsageReportUtils { public static final String TOP_DOWNLOAD_COUNTRIES_REPORT_ID = "TopDownloadsCountries"; public static final String TOP_DOWNLOAD_CITIES_REPORT_ID = "TopDownloadsCities"; public static final String TOTAL_DOWNLOAD_PER_MONTH_REPORT_ID = "TotalDownloadsPerMonth"; + public static final String TOP_ITEMS_CITIES_REPORT_ID = "TopItemsCities"; + public static final String TOP_ITEMS_CONTINENTS_REPORT_ID = "TopItemsContinents"; + public static final String TOP_ITEMS_COUNTRIES_REPORT_ID = "TopItemsCountries"; + public static final String TOP_ITEMS_CATEGORIES_REPORT_ID = "TopItemsCategories"; + public static final String TOTAL_ITEMS_VISITS_REPORT_ID = "TotalItemsVisits"; + public static final String TOTAL_ITEMS_VISITS_PER_MONTH_REPORT_ID = "TotalItemsVisitsPerMonth"; /** * Get list of usage reports that are applicable to the DSO (of given UUID) 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/AuthorizationRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorizationRestRepositoryIT.java index 350e471f49c5..67185f2cdab2 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorizationRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthorizationRestRepositoryIT.java @@ -25,7 +25,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.io.IOException; import java.io.Serializable; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -64,6 +66,7 @@ import org.dspace.app.rest.projection.DefaultProjection; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.AuthorizeException; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.EPersonBuilder; @@ -81,8 +84,13 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.services.ConfigurationService; +import org.dspace.xmlworkflow.storedcomponents.PoolTask; +import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; +import org.dspace.xmlworkflow.storedcomponents.service.PoolTaskService; +import org.dspace.xmlworkflow.storedcomponents.service.XmlWorkflowItemService; import org.hamcrest.Matcher; import org.hamcrest.Matchers; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -117,6 +125,12 @@ public class AuthorizationRestRepositoryIT extends AbstractControllerIntegration @Autowired private ItemConverter itemConverter; + @Autowired + private PoolTaskService poolTaskService; + + @Autowired + private XmlWorkflowItemService xmlWorkflowItemService; + @Autowired private Utils utils; private SiteService siteService; @@ -175,6 +189,14 @@ public void setUp() throws Exception { configurationService.setProperty("webui.user.assumelogin", true); } + @After + public void cleanUp() throws Exception { + context.turnOffAuthorisationSystem(); + poolTaskService.findAll(context).forEach(this::deletePoolTask); + xmlWorkflowItemService.findAll(context).forEach(this::deleteWorkflowItem); + context.restoreAuthSystemState(); + } + @Test /** * This method is not implemented @@ -2824,7 +2846,17 @@ public void verifySpecialGroupForNonAdministrativeUsersTest() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.errors").doesNotExist()); - getClient(epersonToken).perform(get("/api/submission/workspaceitems/" + workspaceItemIdRef.get())) + AtomicReference workflowItemIdRef = new AtomicReference(); + + getClient(epersonToken).perform(post("/api/workflow/workflowitems") + .content("/api/submission/workspaceitems/" + workspaceItemIdRef.get()) + .contentType(textUriContentType)) + .andExpect(status().isCreated()) + .andDo(result -> workflowItemIdRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + + getClient(tokenAdmin).perform(get("/api/workflow/workflowitems/" + workflowItemIdRef.get())) .andExpect(status().isOk()) .andExpect(jsonPath("$.sections.correction.metadata", hasSize(equalTo(3)))) .andExpect(jsonPath("$.sections.correction.empty", is(false))) @@ -2931,5 +2963,20 @@ private String getAuthorizationID(String epersonUuid, String featureName, String + id.toString(); } + private void deletePoolTask(PoolTask poolTask) { + try { + poolTaskService.delete(context, poolTask); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e); + } + } + + private void deleteWorkflowItem(XmlWorkflowItem workflowItem) { + try { + xmlWorkflowItemService.delete(context, workflowItem); + } catch (SQLException | AuthorizeException | IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CorrectionStepIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CorrectionStepIT.java index 05b3db7a4ac6..34665592823e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CorrectionStepIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CorrectionStepIT.java @@ -9,9 +9,9 @@ import static com.jayway.jsonpath.JsonPath.read; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -20,6 +20,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.Date; @@ -40,21 +41,17 @@ import org.dspace.builder.EntityTypeBuilder; import org.dspace.builder.ItemBuilder; import org.dspace.builder.RelationshipTypeBuilder; -import org.dspace.builder.WorkspaceItemBuilder; import org.dspace.content.Collection; import org.dspace.content.EntityType; import org.dspace.content.Item; -import org.dspace.content.MetadataSchemaEnum; import org.dspace.content.Relationship; import org.dspace.content.RelationshipType; import org.dspace.content.WorkspaceItem; import org.dspace.content.service.EntityTypeService; -import org.dspace.content.service.ItemService; import org.dspace.content.service.RelationshipService; import org.dspace.content.service.RelationshipTypeService; import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Constants; -import org.dspace.eperson.EPerson; import org.dspace.services.ConfigurationService; import org.dspace.xmlworkflow.storedcomponents.PoolTask; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; @@ -68,12 +65,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; -import org.springframework.data.rest.webmvc.RestMediaTypes; /** * Integration tests for {@link CorrectionStep}. * - * @author Giuseppe Digilio (luca.giamminonni at 4science.it) + * @author Giuseppe Digilio (giuseppe.digilio at 4science.it) * */ public class CorrectionStepIT extends AbstractControllerIntegrationTest { @@ -84,9 +80,6 @@ public class CorrectionStepIT extends AbstractControllerIntegrationTest { @Autowired private EntityTypeService entityTypeService; - @Autowired - private ItemService itemService; - @Autowired private PoolTaskService poolTaskService; @@ -138,7 +131,7 @@ public void setup() throws Exception { date = "2020-02-20"; subject = "ExtraEntry"; - title = "Title " + (new Date().getTime()); + title = "Title " + new Date().getTime(); type = "text"; itemToBeCorrected = ItemBuilder.createItem(context, collection) @@ -160,6 +153,7 @@ public void setup() throws Exception { context.restoreAuthSystemState(); } + @Override @After public void destroy() throws Exception { //Clean up the database for the next test @@ -191,14 +185,12 @@ public void destroy() throws Exception { } } - if (workspaceItemIdRef.get() != null) { - WorkspaceItemBuilder.deleteWorkspaceItem(workspaceItemIdRef.get()); - } poolTaskService.findAll(context).forEach(this::deletePoolTask); - super.destroy(); + xmlWorkflowItemService.findAll(context).forEach(this::deleteWorkflowItem); super.destroy(); } + @Test public void checkCorrection() throws Exception { @@ -213,16 +205,15 @@ public void checkCorrection() throws Exception { .andExpect(status().isCreated()) .andDo(result -> workspaceItemIdRef.set(read(result.getResponse().getContentAsString(), "$.id"))); - Integer workspaceItemId = workspaceItemIdRef.get(); - List relationshipList = relationshipService.findByItem(context, itemToBeCorrected); - assert (relationshipList.size() > 0); + assert relationshipList.size() > 0; Item correctedItem = relationshipList.get(0).getLeftItem(); WorkspaceItem newWorkspaceItem = workspaceItemService.findByItem(context,correctedItem); //make a change on the title Map value = new HashMap(); - value.put("value", "New Title"); + final String newTitle = "New Title"; + value.put("value", newTitle); List addGrant = new ArrayList(); addGrant.add(new ReplaceOperation("/sections/traditionalpageone/dc.title/0", value)); String patchBody = getPatchContent(addGrant); @@ -241,9 +232,11 @@ public void checkCorrection() throws Exception { .contentType("application/json-patch+json")) .andExpect(status().isOk()) .andExpect(jsonPath("$.errors").doesNotExist()); + //add an asbtract description - Map addValue = new HashMap(); - addValue.put("value","Description Test"); + Map addValue = new HashMap(); + final String newDescription = "New Description"; + addValue.put("value", newDescription); addGrant = new ArrayList(); addGrant.add(new AddOperation("/sections/traditionalpagetwo/dc.description.abstract", List.of(addValue))); patchBody = getPatchContent(addGrant); @@ -252,17 +245,32 @@ public void checkCorrection() throws Exception { .contentType("application/json-patch+json")) .andExpect(status().isOk()) .andExpect(jsonPath("$.errors").doesNotExist()); - //check if the correction is present + getClient(tokenSubmitter).perform(get("/api/submission/workspaceitems/" + newWorkspaceItem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.correction.metadata").doesNotExist()); + + AtomicReference workflowItemIdRef = new AtomicReference(); + + getClient(tokenSubmitter).perform(post("/api/workflow/workflowitems") + .content("/api/submission/workspaceitems/" + newWorkspaceItem.getID()) + .contentType(textUriContentType)) + .andExpect(status().isCreated()) + .andDo(result -> workflowItemIdRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + + //check if the correction is present + final String extraEntry = "ExtraEntry"; + getClient(tokenAdmin).perform(get("/api/workflow/workflowitems/" + workflowItemIdRef.get())) //The status has to be 200 OK .andExpect(status().isOk()) //The array of browse index should have a size equals to 4 .andExpect(jsonPath("$.sections.correction.metadata", hasSize(equalTo(3)))) .andExpect(jsonPath("$.sections.correction.empty", is(false))) - .andExpect(jsonPath("$.sections.correction.metadata", - containsInAnyOrder(matchMetadataCorrection("New Title"), - matchMetadataCorrection("Description Test"), - matchMetadataCorrection("ExtraEntry")))); + .andExpect(jsonPath("$.sections.correction.metadata",hasItem(matchMetadataCorrection(newTitle)))) + .andExpect(jsonPath("$.sections.correction.metadata",hasItem(matchMetadataCorrection(newDescription)))) + .andExpect(jsonPath("$.sections.correction.metadata",hasItem(matchMetadataCorrection(extraEntry)))); } @@ -279,14 +287,23 @@ public void checkEmptyCorrection() throws Exception { .andExpect(status().isCreated()) .andDo(result -> workspaceItemIdRef.set(read(result.getResponse().getContentAsString(), "$.id"))); - Integer workspaceItemId = workspaceItemIdRef.get(); List relationshipList = relationshipService.findByItem(context, itemToBeCorrected); - assert (relationshipList.size() > 0); + assert relationshipList.size() > 0; Item correctedItem = relationshipList.get(0).getLeftItem(); WorkspaceItem newWorkspaceItem = workspaceItemService.findByItem(context, correctedItem); + AtomicReference workflowItemIdRef = new AtomicReference(); + + getClient(tokenSubmitter).perform(post("/api/workflow/workflowitems") + .content("/api/submission/workspaceitems/" + newWorkspaceItem.getID()) + .contentType(textUriContentType)) + .andExpect(status().isCreated()) + .andDo(result -> workflowItemIdRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + //check if the correction section is empty on relation item - getClient(tokenSubmitter).perform(get("/api/submission/workspaceitems/" + newWorkspaceItem.getID())) + getClient(tokenAdmin).perform(get("/api/workflow/workflowitems/" + workflowItemIdRef.get())) //The status has to be 200 OK .andExpect(status().isOk()) //The array of browse index should have a size greater or equals to 1 @@ -295,72 +312,10 @@ public void checkEmptyCorrection() throws Exception { } - private static Matcher matchMetadataCorrection(String value) { + private static Matcher matchMetadataCorrection(String value) { return Matchers.anyOf( - // Check workspaceitem properties - hasJsonPath("$.newValues[0]", is(value)), - hasJsonPath("$.oldValues[0]", is(value))); - } - - private void claimTaskAndCheckResponse(String authToken, Integer poolTaskId) throws SQLException, Exception { - getClient(authToken).perform(post("/api/workflow/claimedtasks") - .contentType(RestMediaTypes.TEXT_URI_LIST) - .content("/api/workflow/pooltasks/" + poolTaskId)) - .andExpect(status().isCreated()) - .andExpect(jsonPath("$", Matchers.allOf(hasJsonPath("$.type", is("claimedtask"))))); - } - - private XmlWorkflowItem setSubmission(EPerson user, String title, String date) - throws Exception { - - context.setCurrentUser(user); - - AtomicReference idRef = new AtomicReference(); - String tokenSubmitter = getAuthToken(user.getEmail(), password); - // create empty workSpaceItem - getClient(tokenSubmitter).perform(post("/api/submission/workspaceitems") - .param("owningCollection", collection.getID().toString()) - .contentType(org.springframework.http.MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andDo((result -> idRef - .set(read(result.getResponse().getContentAsString(), "$.id")))); - - WorkspaceItem witem = workspaceItemService.find(context, idRef.get()); - Item item = witem.getItem(); - - // add metadata - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "title", null, null, title); - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "date", "issued", null, date); - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "subject", null, null, "ExtraEntry"); - itemService.addMetadata(context, item, MetadataSchemaEnum.DC.getName(), - "type", null, null, "text"); - // accept license - List addGrant = new ArrayList(); - addGrant.add(new AddOperation("/sections/license/granted", true)); - String patchBody = getPatchContent(addGrant); - getClient(tokenSubmitter).perform(patch("/api/submission/workspaceitems/" + witem.getID()) - .content(patchBody) - .contentType("application/json-patch+json")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.errors").doesNotExist()) - .andExpect(jsonPath("$.sections.license.granted", - is(true))) - .andExpect(jsonPath("$.sections.license.acceptanceDate").isNotEmpty()) - .andExpect(jsonPath("$.sections.license.url").isNotEmpty()); - - //deposit workSpaceItem, so it become workFlowItem - getClient(tokenSubmitter).perform(post(BASE_REST_SERVER_URL + "/api/workflow/workflowitems") - .content("/api/submission/workspaceitems/" + witem.getID()) - .contentType(textUriContentType)) - .andExpect(status().isCreated()) - .andDo((result -> idRef - .set(read(result.getResponse().getContentAsString(), "$.id")))); - - XmlWorkflowItem xmlWorkFlowItem = xmlWorkflowItemService.find(context, idRef.get()); - return xmlWorkFlowItem; + hasJsonPath("$.newValues[0]", equalTo(value)), + hasJsonPath("$.oldValues[0]", equalTo(value))); } private void deletePoolTask(PoolTask poolTask) { @@ -371,5 +326,12 @@ private void deletePoolTask(PoolTask poolTask) { } } + private void deleteWorkflowItem(XmlWorkflowItem workflowItem) { + try { + xmlWorkflowItemService.delete(context, workflowItem); + } catch (SQLException | AuthorizeException | IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java index 9a0d39225c3d..1b17215054ad 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java @@ -150,7 +150,7 @@ private ArrayList getRecords() { MetadatumDTO ispartof = createMetadatumDTO("dc", "relation", "ispartof", "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu"); MetadatumDTO doi = createMetadatumDTO("dc", "identifier", "doi", "10.26693/jmbs01.02.184"); - MetadatumDTO issn = createMetadatumDTO("dc", "identifier", "issn", "2415-3060"); + MetadatumDTO issn = createMetadatumDTO("dc", "relation", "issn", "2415-3060"); MetadatumDTO volume = createMetadatumDTO("oaire", "citation", "volume", "1"); MetadatumDTO issue = createMetadatumDTO("oaire", "citation", "issue", "2"); @@ -176,7 +176,7 @@ private ArrayList getRecords() { MetadatumDTO ispartof2 = createMetadatumDTO("dc", "relation", "ispartof", "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu"); MetadatumDTO doi2 = createMetadatumDTO("dc", "identifier", "doi", "10.26693/jmbs01.02.105"); - MetadatumDTO issn2 = createMetadatumDTO("dc", "identifier", "issn", "2415-3060"); + MetadatumDTO issn2 = createMetadatumDTO("dc", "relation", "issn", "2415-3060"); MetadatumDTO volume2 = createMetadatumDTO("oaire", "citation", "volume", "1"); MetadatumDTO issue2 = createMetadatumDTO("oaire", "citation", "issue", "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/EditItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EditItemRestRepositoryIT.java index b2cc8e5ea7c6..7a511f662fc3 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EditItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EditItemRestRepositoryIT.java @@ -15,6 +15,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -1906,6 +1907,50 @@ public void testEditItemModeConfigurationWithEntityTypeAndSubmission() throws Ex } + @Test + public void testEditWithHiddenSections() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .withEntityType("Publication") + .build(); + Item item = ItemBuilder.createItem(context, collection) + .withTitle("Item") + .build(); + context.restoreAuthSystemState(); + getClient(getAuthToken(admin.getEmail(), password)) + .perform(get("/api/core/edititems/" + item.getID() + ":MODE-TEST-HIDDEN")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.test-outside-workflow-hidden").exists()) + .andExpect(jsonPath("$.sections.test-outside-submission-hidden").doesNotExist()) + .andExpect(jsonPath("$.sections.test-never-hidden").exists()) + .andExpect(jsonPath("$.sections.test-always-hidden").doesNotExist()); + } + + @Test + public void testValidationWithHiddenSteps() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .withEntityType("Publication") + .build(); + Item item = ItemBuilder.createItem(context, collection) + .build(); + context.restoreAuthSystemState(); + getClient(getAuthToken(admin.getEmail(), password)) + .perform(get("/api/core/edititems/" + item.getID() + ":MODE-TEST-HIDDEN")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.errors", hasSize(1))) + .andExpect(jsonPath("$.errors[0].message", is("error.validation.required"))) + .andExpect(jsonPath("$.errors[0].paths", contains("/sections/test-outside-workflow-hidden/dc.title"))); + } + private Bitstream getBitstream(Item item, String name) throws SQLException { return bitstreamService.getBitstreamByName(item, "ORIGINAL", name); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EpoImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EpoImportMetadataSourceServiceIT.java index d86dd1875d8d..f6b944d34264 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EpoImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EpoImportMetadataSourceServiceIT.java @@ -148,28 +148,35 @@ private ArrayList getRecords() { List metadatums = new ArrayList(); MetadatumDTO identifierOther = createMetadatumDTO("dc", "identifier", "other", "epodoc:ES2902749T"); MetadatumDTO patentno = createMetadatumDTO("dc", "identifier", "patentno", "ES2902749T"); + MetadatumDTO kind = createMetadatumDTO("crispatent", "kind", null, "T3"); MetadatumDTO identifier = createMetadatumDTO("dc", "identifier", "applicationnumber", "18705153"); MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2022-03-29"); MetadatumDTO dateSubmitted = createMetadatumDTO("dcterms", "dateSubmitted", null, "2018-02-19"); - MetadatumDTO applicant = createMetadatumDTO("dc", "contributor", null, "PANKA BLOOD TEST GMBH"); - MetadatumDTO applicant2 = createMetadatumDTO("dc", "contributor", null, "Panka Blood Test GmbH"); + MetadatumDTO applicant = createMetadatumDTO("dc", "contributor", null, "Panka Blood Test GmbH"); MetadatumDTO author = createMetadatumDTO("dc", "contributor", "author", "PANTEL, Klaus, "); MetadatumDTO author2 = createMetadatumDTO("dc", "contributor", "author", "BARTKOWIAK, Kai"); MetadatumDTO title = createMetadatumDTO("dc", "title", null, "Método para el diagnóstico del cáncer de mama"); MetadatumDTO subject = createMetadatumDTO("dc", "subject", null, "G01N 33/ 574 A I "); + MetadatumDTO kindCodeInline = createMetadatumDTO("crispatent", "document", "kind", "T3"); + MetadatumDTO issueDateInline = createMetadatumDTO("crispatent", "document", "issueDate", "2022-03-29"); + MetadatumDTO titleInline = createMetadatumDTO("crispatent", "document", "title", + "Método para el diagnóstico del cáncer de mama"); metadatums.add(identifierOther); metadatums.add(patentno); + metadatums.add(kind); metadatums.add(identifier); metadatums.add(date); metadatums.add(dateSubmitted); metadatums.add(applicant); - metadatums.add(applicant2); metadatums.add(author); metadatums.add(author2); metadatums.add(title); metadatums.add(subject); + metadatums.add(kindCodeInline); + metadatums.add(issueDateInline); + metadatums.add(titleInline); ImportRecord firstrRecord = new ImportRecord(metadatums); @@ -177,11 +184,11 @@ private ArrayList getRecords() { List metadatums2 = new ArrayList(); MetadatumDTO identifierOther2 = createMetadatumDTO("dc", "identifier", "other", "epodoc:TW202202864"); MetadatumDTO patentno2 = createMetadatumDTO("dc", "identifier", "patentno", "TW202202864"); + MetadatumDTO kind2 = createMetadatumDTO("crispatent", "kind", null, "A"); MetadatumDTO identifier2 = createMetadatumDTO("dc", "identifier", "applicationnumber", "109122801"); MetadatumDTO date2 = createMetadatumDTO("dc", "date", "issued", "2022-01-16"); MetadatumDTO dateSubmitted2 = createMetadatumDTO("dcterms", "dateSubmitted", null, "2020-07-06"); - MetadatumDTO applicant3 = createMetadatumDTO("dc", "contributor", null, "ADVANTEST CORP [JP]"); - MetadatumDTO applicant4 = createMetadatumDTO("dc", "contributor", null, "ADVANTEST CORPORATION"); + MetadatumDTO applicant2 = createMetadatumDTO("dc", "contributor", null, "ADVANTEST CORPORATION"); MetadatumDTO author5 = createMetadatumDTO("dc", "contributor", "author", "POEPPE, OLAF, "); MetadatumDTO author6 = createMetadatumDTO("dc", "contributor", "author", "HILLIGES, KLAUS-DIETER, "); MetadatumDTO author7 = createMetadatumDTO("dc", "contributor", "author", "KRECH, ALAN"); @@ -192,19 +199,29 @@ private ArrayList getRecords() { "G01R 31/ 319 A I "); MetadatumDTO subject3 = createMetadatumDTO("dc", "subject", null, "G01R 31/ 3193 A I "); + MetadatumDTO kindCodeInline2 = createMetadatumDTO("crispatent", "document", "kind", "A"); + MetadatumDTO issueDateInline2 = createMetadatumDTO("crispatent", "document", "issueDate", "2022-01-16"); + MetadatumDTO titleInline2 = createMetadatumDTO("crispatent", "document", "title", + "Automated test equipment for testing one or more devices under test," + + " method for automated testing of one or more devices under test," + + " and computer program using a buffer memory"); + metadatums2.add(identifierOther2); metadatums2.add(patentno2); + metadatums2.add(kind2); metadatums2.add(identifier2); metadatums2.add(date2); metadatums2.add(dateSubmitted2); - metadatums2.add(applicant3); - metadatums2.add(applicant4); + metadatums2.add(applicant2); metadatums2.add(author5); metadatums2.add(author6); metadatums2.add(author7); metadatums2.add(title2); metadatums2.add(subject2); metadatums2.add(subject3); + metadatums2.add(kindCodeInline2); + metadatums2.add(issueDateInline2); + metadatums2.add(titleInline2); ImportRecord secondRecord = new ImportRecord(metadatums2); records.add(firstrRecord); 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/StatisticsRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/StatisticsRestRepositoryIT.java index 0f7996a765f3..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 @@ -25,24 +25,27 @@ import static org.dspace.app.rest.utils.UsageReportUtils.TOP_DOWNLOAD_CITIES_REPORT_ID; import static org.dspace.app.rest.utils.UsageReportUtils.TOP_DOWNLOAD_CONTINENTS_REPORT_ID; import static org.dspace.app.rest.utils.UsageReportUtils.TOP_DOWNLOAD_COUNTRIES_REPORT_ID; +import static org.dspace.app.rest.utils.UsageReportUtils.TOP_ITEMS_CATEGORIES_REPORT_ID; +import static org.dspace.app.rest.utils.UsageReportUtils.TOP_ITEMS_CITIES_REPORT_ID; +import static org.dspace.app.rest.utils.UsageReportUtils.TOP_ITEMS_CONTINENTS_REPORT_ID; +import static org.dspace.app.rest.utils.UsageReportUtils.TOP_ITEMS_COUNTRIES_REPORT_ID; import static org.dspace.app.rest.utils.UsageReportUtils.TOP_ITEMS_REPORT_ID; import static org.dspace.app.rest.utils.UsageReportUtils.TOP_ITEMS_REPORT_RELATION_ORGUNIT_RP_RESEARCHOUTPUTS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_DOWNLOADS_REPORT_ID; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_DOWNLOADS_REPORT_ID_RELATION_ORGUNIT_RP_RESEARCHOUTPUTS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_DOWNLOAD_PER_MONTH_REPORT_ID; -//import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_DOWNLOADS_REPORT_ID_RELATION_PERSON_RESEARCHOUTPUTS; +import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_ITEMS_VISITS_PER_MONTH_REPORT_ID; +import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_ITEMS_VISITS_REPORT_ID; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_PER_MONTH_REPORT_ID; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_PER_MONTH_REPORT_ID_RELATION_ORGUNIT_RP_RESEARCHOUTPUTS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_PER_MONTH_REPORT_ID_RELATION_PERSON_PROJECTS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_PER_MONTH_REPORT_ID_RELATION_PERSON_RESEARCHOUTPUTS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_REPORT_ID; -//import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_REPORT_ID_RELATION_ORGUNIT_PROJECTS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_REPORT_ID_RELATION_ORGUNIT_RP_RESEARCHOUTPUTS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_REPORT_ID_RELATION_PERSON_PROJECTS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_REPORT_ID_RELATION_PERSON_RESEARCHOUTPUTS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_TOTAL_DOWNLOADS; import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_TOTAL_DOWNLOADS_RELATION_ORGUNIT_RP_RESEARCHOUTPUTS; -//import static org.dspace.app.rest.utils.UsageReportUtils.TOTAL_VISITS_TOTAL_DOWNLOADS_RELATION_PERSON_RESEARCHOUTPUTS; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -921,10 +924,10 @@ public void TotalDownloadsReport_Item_NotVisited() throws Exception { } @Test - public void TotalDownloadsReport_NotSupportedDSO_Collection() throws Exception { + public void TotalDownloadsReport_SupportedDSO_Collection() throws Exception { getClient(adminToken) .perform(get("/api/statistics/usagereports/" + collectionVisited.getID() + "_" + TOTAL_DOWNLOADS_REPORT_ID)) - .andExpect(status().isNotFound()); + .andExpect(status().isOk()); } /** @@ -1293,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(); @@ -1392,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) @@ -1575,7 +1594,8 @@ public void usageReportsSearch_Community_Visited() throws Exception { // And request the community usage reports getClient(adminToken) - .perform(get("/api/statistics/usagereports/search/object?uri=http://localhost:8080/server/api/core" + + .perform(get("/api/statistics/usagereports/search/object?category=community-mainReports" + + "&uri=http://localhost:8080/server/api/core" + "/communities/" + communityVisited.getID())) // ** THEN ** .andExpect(status().isOk()) @@ -1616,7 +1636,8 @@ public void usageReportsSearch_Collection_NotVisited() throws Exception { // Collection is not visited // And request the collection's usage reports getClient(adminToken) - .perform(get("/api/statistics/usagereports/search/object?uri=http://localhost:8080/server/api/core" + + .perform(get("/api/statistics/usagereports/search/object?category=collection-mainReports" + + "&uri=http://localhost:8080/server/api/core" + "/collections/" + collectionNotVisited.getID())) // ** THEN ** .andExpect(status().isOk()) @@ -1951,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"); @@ -2035,7 +2074,8 @@ public void usageReportsSearch_Community_VisitedAtTime() throws Exception { String endDate = dateFormat.format(cal.getTime()); // And request the community usage reports getClient(adminToken) - .perform(get("/api/statistics/usagereports/search/object?uri=http://localhost:8080/server/api/core" + + .perform(get("/api/statistics/usagereports/search/object?category=community-mainReports" + + "&uri=http://localhost:8080/server/api/core" + "/communities/" + communityVisited.getID() + "&startDate=2019-06-01&endDate=" + endDate)) // ** THEN ** .andExpect(status().isOk()) @@ -2435,6 +2475,535 @@ public void usageReportsSearch_OrgUnitWithPublicationVisited() throws Exception ))); } + @Test + 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") + .build(); + Item item2 = ItemBuilder.createItem(context, collectionNotVisited) + .withTitle("My item 2") + .withType("Controlled Vocabulary for Resource Type Genres::thesis") + .build(); + Item item3 = ItemBuilder.createItem(context, collectionNotVisited) + .withTitle("My item 3") + .withType("Controlled Vocabulary for Resource Type Genres::thesis::bachelor thesis") + .build(); + Item item4 = ItemBuilder.createItem(context, collectionNotVisited) + .withTitle("My item 4") + .withType("Controlled Vocabulary for Resource Type Genres::text::periodical::" + + "journal::contribution to journal::journal article") + .build(); + context.restoreAuthSystemState(); + + ObjectMapper mapper = new ObjectMapper(); + + ViewEventRest viewEventRest = new ViewEventRest(); + viewEventRest.setTargetType("item"); + viewEventRest.setTargetId(item.getID()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + ViewEventRest viewEventRest2 = new ViewEventRest(); + viewEventRest2.setTargetType("item"); + viewEventRest2.setTargetId(item2.getID()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest2)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest2)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + ViewEventRest viewEventRest3 = new ViewEventRest(); + viewEventRest3.setTargetType("item"); + viewEventRest3.setTargetId(item3.getID()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest3)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + ViewEventRest viewEventRest4 = new ViewEventRest(); + viewEventRest4.setTargetType("item"); + viewEventRest4.setTargetId(item4.getID()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest4)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + UsageReportPointDsoTotalVisitsRest expectedPoint1 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint1.addValue("views", 1); + expectedPoint1.setType("item"); + expectedPoint1.setLabel("My item"); + expectedPoint1.setId(item.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint2 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint2.addValue("views", 2); + expectedPoint2.setType("item"); + expectedPoint2.setLabel("My item 2"); + expectedPoint2.setId(item2.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint3 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint3.addValue("views", 1); + expectedPoint3.setType("item"); + expectedPoint3.setLabel("My item 3"); + expectedPoint3.setId(item3.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint4 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint4.addValue("views", 1); + expectedPoint4.setType("item"); + expectedPoint4.setLabel("My item 4"); + expectedPoint4.setId(item4.getID().toString()); + + List points = List.of(expectedPoint1, expectedPoint2, expectedPoint3, expectedPoint4); + + UsageReportPointCityRest pointCity = new UsageReportPointCityRest(); + pointCity.addValue("views", 5); + pointCity.setId("New York"); + + UsageReportPointContinentRest pointContinent = new UsageReportPointContinentRest(); + pointContinent.addValue("views", 5); + pointContinent.setId("North America"); + + UsageReportPointCountryRest pointCountry = new UsageReportPointCountryRest(); + 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 thesisCategory = new UsageReportPointCategoryRest(); + thesisCategory.addValue("views", 3); + thesisCategory.setId("thesis"); + + UsageReportPointCategoryRest otherCategory = new UsageReportPointCategoryRest(); + otherCategory.addValue("views", 1); + otherCategory.setId("other"); + + UsageReportPointCategoryRest bookCategory = new UsageReportPointCategoryRest(); + bookCategory.addValue("views", 0); + bookCategory.setId("book"); + + UsageReportPointCategoryRest bookChapterCategory = new UsageReportPointCategoryRest(); + bookChapterCategory.addValue("views", 0); + bookChapterCategory.setId("bookChapter"); + + UsageReportPointCategoryRest datasetCategory = new UsageReportPointCategoryRest(); + datasetCategory.addValue("views", 0); + datasetCategory.setId("dataset"); + + List categories = List.of(articleCategory, thesisCategory, otherCategory, bookCategory, + bookChapterCategory, datasetCategory); + + // And request the collections global usage report (show top most popular items) + getClient(adminToken) + .perform(get("/api/statistics/usagereports/search/object") + .param("category", "publicationCollection-itemReports") + .param("uri", "http://localhost:8080/server/api/core/collections/" + collectionNotVisited.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.usagereports", not(empty()))) + .andExpect(jsonPath("$._embedded.usagereports", Matchers.containsInAnyOrder( + matchUsageReport(collectionNotVisited.getID() + "_" + TOTAL_ITEMS_VISITS_REPORT_ID, + TOP_ITEMS_REPORT_ID, points), + matchUsageReport(collectionNotVisited.getID() + "_" + TOP_ITEMS_CITIES_REPORT_ID, + TOP_CITIES_REPORT_ID, List.of(pointCity)), + matchUsageReport(collectionNotVisited.getID() + "_" + TOTAL_ITEMS_VISITS_PER_MONTH_REPORT_ID, + TOTAL_VISITS_PER_MONTH_REPORT_ID, getLastMonthVisitPoints(5)), + matchUsageReport(collectionNotVisited.getID() + "_" + TOP_ITEMS_CONTINENTS_REPORT_ID, + TOP_CONTINENTS_REPORT_ID, List.of(pointContinent)), + matchUsageReport(collectionNotVisited.getID() + "_" + TOP_ITEMS_CATEGORIES_REPORT_ID, + TOP_CATEGORIES_REPORT_ID, categories), + matchUsageReport(collectionNotVisited.getID() + "_" + TOP_ITEMS_COUNTRIES_REPORT_ID, + TOP_COUNTRIES_REPORT_ID, List.of(pointCountry))))); + } + + @Test + public void usageReportsSearch_Collection_DownloadReports() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item item1 = ItemBuilder.createItem(context, collectionNotVisited) + .withTitle("Item 1") + .build(); + + Item item2 = ItemBuilder.createItem(context, collectionNotVisited) + .withTitle("Item 2") + .build(); + + Item item3 = ItemBuilder.createItem(context, collectionNotVisited) + .withTitle("Item 3") + .build(); + + Bitstream bitstream1 = createBitstream(item1, "Bitstream 1"); + Bitstream bitstream2 = createBitstream(item1, "Bitstream 2"); + Bitstream bitstream3 = createBitstream(item2, "Bitstream 3"); + Bitstream bitstream4 = createBitstream(item3, "Bitstream 4"); + + getClient().perform(get("/api/core/bitstreams/" + bitstream1.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream1.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream2.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream4.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream4.getID() + "/content")) + .andExpect(status().isOk()); + + context.restoreAuthSystemState(); + + UsageReportPointDsoTotalVisitsRest expectedPoint1 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint1.addValue("views", 3); + expectedPoint1.setType("item"); + expectedPoint1.setLabel("Item 1"); + expectedPoint1.setId(item1.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint2 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint2.addValue("views", 3); + expectedPoint2.setType("item"); + expectedPoint2.setLabel("Item 2"); + expectedPoint2.setId(item2.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint3 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint3.addValue("views", 2); + expectedPoint3.setType("item"); + expectedPoint3.setLabel("Item 3"); + expectedPoint3.setId(item3.getID().toString()); + + List points = List.of(expectedPoint1, expectedPoint2, expectedPoint3); + + UsageReportPointCityRest pointCity = new UsageReportPointCityRest(); + pointCity.addValue("views", 8); + pointCity.setId("New York"); + + UsageReportPointContinentRest pointContinent = new UsageReportPointContinentRest(); + pointContinent.addValue("views", 8); + pointContinent.setId("North America"); + + UsageReportPointCountryRest pointCountry = new UsageReportPointCountryRest(); + pointCountry.addValue("views", 8); + pointCountry.setIdAndLabel(Locale.US.getCountry(), Locale.US.getDisplayCountry(context.getCurrentLocale())); + + getClient(adminToken) + .perform(get("/api/statistics/usagereports/search/object") + .param("category", "collection-downloadReports") + .param("uri", "http://localhost:8080/server/api/core/collections/" + collectionNotVisited.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.usagereports", not(empty()))) + .andExpect(jsonPath("$._embedded.usagereports", Matchers.containsInAnyOrder( + matchUsageReport(collectionNotVisited.getID() + "_" + TOTAL_DOWNLOADS_REPORT_ID, + TOP_ITEMS_REPORT_ID, points), + matchUsageReport(collectionNotVisited.getID() + "_" + TOP_DOWNLOAD_CITIES_REPORT_ID, + TOP_CITIES_REPORT_ID, List.of(pointCity)), + matchUsageReport(collectionNotVisited.getID() + "_" + TOTAL_DOWNLOAD_PER_MONTH_REPORT_ID, + TOTAL_VISITS_PER_MONTH_REPORT_ID, getLastMonthVisitPoints(8)), + matchUsageReport(collectionNotVisited.getID() + "_" + TOP_DOWNLOAD_CONTINENTS_REPORT_ID, + TOP_CONTINENTS_REPORT_ID, List.of(pointContinent)), + matchUsageReport(collectionNotVisited.getID() + "_" + TOP_DOWNLOAD_COUNTRIES_REPORT_ID, + TOP_COUNTRIES_REPORT_ID, List.of(pointCountry))))); + } + + @Test + public void usageReportsSearch_Community_ItemReports() throws Exception { + context.turnOffAuthorisationSystem(); + + Community community = CommunityBuilder.createCommunity(context).build(); + collectionNotVisited = CollectionBuilder.createCollection(context, community).build(); + + Item item = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Publication") + .withTitle("My item") + .build(); + Item item2 = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Patent") + .withTitle("My item 2") + .build(); + Item item3 = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Funding") + .withTitle("My item 3") + .build(); + Item item4 = ItemBuilder.createItem(context, collectionNotVisited) + .withEntityType("Project") + .withTitle("My item 4") + .build(); + context.restoreAuthSystemState(); + + ObjectMapper mapper = new ObjectMapper(); + + ViewEventRest viewEventRest = new ViewEventRest(); + viewEventRest.setTargetType("item"); + viewEventRest.setTargetId(item.getID()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + ViewEventRest viewEventRest2 = new ViewEventRest(); + viewEventRest2.setTargetType("item"); + viewEventRest2.setTargetId(item2.getID()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest2)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest2)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + ViewEventRest viewEventRest3 = new ViewEventRest(); + viewEventRest3.setTargetType("item"); + viewEventRest3.setTargetId(item3.getID()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest3)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + ViewEventRest viewEventRest4 = new ViewEventRest(); + viewEventRest4.setTargetType("item"); + viewEventRest4.setTargetId(item4.getID()); + + getClient().perform(post("/api/statistics/viewevents") + .content(mapper.writeValueAsBytes(viewEventRest4)) + .contentType(contentType)) + .andExpect(status().isCreated()); + + UsageReportPointDsoTotalVisitsRest expectedPoint1 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint1.addValue("views", 1); + expectedPoint1.setType("item"); + expectedPoint1.setLabel("My item"); + expectedPoint1.setId(item.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint2 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint2.addValue("views", 2); + expectedPoint2.setType("item"); + expectedPoint2.setLabel("My item 2"); + expectedPoint2.setId(item2.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint3 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint3.addValue("views", 1); + expectedPoint3.setType("item"); + expectedPoint3.setLabel("My item 3"); + expectedPoint3.setId(item3.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint4 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint4.addValue("views", 1); + expectedPoint4.setType("item"); + expectedPoint4.setLabel("My item 4"); + expectedPoint4.setId(item4.getID().toString()); + + List points = List.of(expectedPoint1, expectedPoint2, expectedPoint3, expectedPoint4); + + UsageReportPointCityRest pointCity = new UsageReportPointCityRest(); + pointCity.addValue("views", 5); + pointCity.setId("New York"); + + UsageReportPointContinentRest pointContinent = new UsageReportPointContinentRest(); + pointContinent.addValue("views", 5); + pointContinent.setId("North America"); + + UsageReportPointCountryRest pointCountry = new UsageReportPointCountryRest(); + pointCountry.addValue("views", 5); + pointCountry.setIdAndLabel(Locale.US.getCountry(), Locale.US.getDisplayCountry(context.getCurrentLocale())); + + UsageReportPointCategoryRest publicationCategory = new UsageReportPointCategoryRest(); + publicationCategory.addValue("views", 1); + publicationCategory.setId("publication"); + + UsageReportPointCategoryRest patentCategory = new UsageReportPointCategoryRest(); + patentCategory.addValue("views", 2); + patentCategory.setId("patent"); + + UsageReportPointCategoryRest fundingCategory = new UsageReportPointCategoryRest(); + fundingCategory.addValue("views", 1); + fundingCategory.setId("funding"); + + UsageReportPointCategoryRest projectCategory = new UsageReportPointCategoryRest(); + projectCategory.addValue("views", 1); + projectCategory.setId("project"); + + UsageReportPointCategoryRest productCategory = new UsageReportPointCategoryRest(); + productCategory.addValue("views", 0); + productCategory.setId("product"); + + 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); + // And request the collections global usage report (show top most popular items) + getClient(adminToken) + .perform(get("/api/statistics/usagereports/search/object") + .param("category", "community-itemReports") + .param("uri", "http://localhost:8080/server/api/core/communities/" + community.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.usagereports", not(empty()))) + .andExpect(jsonPath("$._embedded.usagereports", Matchers.containsInAnyOrder( + matchUsageReport(community.getID() + "_" + TOTAL_ITEMS_VISITS_REPORT_ID, + TOP_ITEMS_REPORT_ID, points), + matchUsageReport(community.getID() + "_" + TOP_ITEMS_CITIES_REPORT_ID, + TOP_CITIES_REPORT_ID, List.of(pointCity)), + matchUsageReport(community.getID() + "_" + TOTAL_ITEMS_VISITS_PER_MONTH_REPORT_ID, + TOTAL_VISITS_PER_MONTH_REPORT_ID, getLastMonthVisitPoints(5)), + matchUsageReport(community.getID() + "_" + TOP_ITEMS_CONTINENTS_REPORT_ID, + TOP_CONTINENTS_REPORT_ID, List.of(pointContinent)), + matchUsageReport(community.getID() + "_" + TOP_ITEMS_CATEGORIES_REPORT_ID, + TOP_CATEGORIES_REPORT_ID, categories), + matchUsageReport(community.getID() + "_" + TOP_ITEMS_COUNTRIES_REPORT_ID, + TOP_COUNTRIES_REPORT_ID, List.of(pointCountry))))); + } + + @Test + public void usageReportsSearch_Community_DownloadReports() throws Exception { + + context.turnOffAuthorisationSystem(); + + Community community = CommunityBuilder.createCommunity(context).build(); + collectionNotVisited = CollectionBuilder.createCollection(context, community).build(); + + Item item1 = ItemBuilder.createItem(context, collectionNotVisited) + .withTitle("Item 1") + .build(); + + Item item2 = ItemBuilder.createItem(context, collectionNotVisited) + .withTitle("Item 2") + .build(); + + Item item3 = ItemBuilder.createItem(context, collectionNotVisited) + .withTitle("Item 3") + .build(); + + Bitstream bitstream1 = createBitstream(item1, "Bitstream 1"); + Bitstream bitstream2 = createBitstream(item1, "Bitstream 2"); + Bitstream bitstream3 = createBitstream(item2, "Bitstream 3"); + Bitstream bitstream4 = createBitstream(item3, "Bitstream 4"); + + getClient().perform(get("/api/core/bitstreams/" + bitstream1.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream1.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream2.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream3.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream4.getID() + "/content")) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/bitstreams/" + bitstream4.getID() + "/content")) + .andExpect(status().isOk()); + + context.restoreAuthSystemState(); + + UsageReportPointDsoTotalVisitsRest expectedPoint1 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint1.addValue("views", 3); + expectedPoint1.setType("item"); + expectedPoint1.setLabel("Item 1"); + expectedPoint1.setId(item1.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint2 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint2.addValue("views", 3); + expectedPoint2.setType("item"); + expectedPoint2.setLabel("Item 2"); + expectedPoint2.setId(item2.getID().toString()); + + UsageReportPointDsoTotalVisitsRest expectedPoint3 = new UsageReportPointDsoTotalVisitsRest(); + expectedPoint3.addValue("views", 2); + expectedPoint3.setType("item"); + expectedPoint3.setLabel("Item 3"); + expectedPoint3.setId(item3.getID().toString()); + + List points = List.of(expectedPoint1, expectedPoint2, expectedPoint3); + + UsageReportPointCityRest pointCity = new UsageReportPointCityRest(); + pointCity.addValue("views", 8); + pointCity.setId("New York"); + + UsageReportPointContinentRest pointContinent = new UsageReportPointContinentRest(); + pointContinent.addValue("views", 8); + pointContinent.setId("North America"); + + UsageReportPointCountryRest pointCountry = new UsageReportPointCountryRest(); + pointCountry.addValue("views", 8); + pointCountry.setIdAndLabel(Locale.US.getCountry(), Locale.US.getDisplayCountry(context.getCurrentLocale())); + + getClient(adminToken) + .perform(get("/api/statistics/usagereports/search/object") + .param("category", "community-downloadReports") + .param("uri", "http://localhost:8080/server/api/core/communities/" + community.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.usagereports", not(empty()))) + .andExpect(jsonPath("$._embedded.usagereports", Matchers.containsInAnyOrder( + matchUsageReport(community.getID() + "_" + TOTAL_DOWNLOADS_REPORT_ID, TOP_ITEMS_REPORT_ID, points), + matchUsageReport(community.getID() + "_" + TOP_DOWNLOAD_CITIES_REPORT_ID, + TOP_CITIES_REPORT_ID, List.of(pointCity)), + matchUsageReport(community.getID() + "_" + TOTAL_DOWNLOAD_PER_MONTH_REPORT_ID, + TOTAL_VISITS_PER_MONTH_REPORT_ID, getLastMonthVisitPoints(8)), + matchUsageReport(community.getID() + "_" + TOP_DOWNLOAD_CONTINENTS_REPORT_ID, + TOP_CONTINENTS_REPORT_ID, List.of(pointContinent)), + matchUsageReport(community.getID() + "_" + TOP_DOWNLOAD_COUNTRIES_REPORT_ID, + TOP_COUNTRIES_REPORT_ID, List.of(pointCountry))))); + } + private List getLastMonthVisitPoints(int viewsLastMonth) { return getListOfVisitsPerMonthsPoints(viewsLastMonth, 0); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index d3a830a5eb5b..a0343d67e93d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -310,7 +310,7 @@ public void findAllPaginationTest() throws Exception { .param("page", "0")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("patent"))) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("test-hidden"))) .andExpect(jsonPath("$._links.first.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=0"), Matchers.containsString("size=1")))) @@ -322,10 +322,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=1"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=15"), Matchers.containsString("size=1")))) + Matchers.containsString("page=16"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(16))) - .andExpect(jsonPath("$.page.totalPages", is(16))) + .andExpect(jsonPath("$.page.totalElements", is(17))) + .andExpect(jsonPath("$.page.totalPages", is(17))) .andExpect(jsonPath("$.page.number", is(0))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -333,7 +333,7 @@ public void findAllPaginationTest() throws Exception { .param("page", "1")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("accessConditionNotDiscoverable"))) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("patent"))) .andExpect(jsonPath("$._links.first.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=0"), Matchers.containsString("size=1")))) @@ -348,10 +348,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=1"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=15"), Matchers.containsString("size=1")))) + Matchers.containsString("page=16"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(16))) - .andExpect(jsonPath("$.page.totalPages", is(16))) + .andExpect(jsonPath("$.page.totalElements", is(17))) + .andExpect(jsonPath("$.page.totalPages", is(17))) .andExpect(jsonPath("$.page.number", is(1))); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java index 43f80037b16b..9236e7c4ce25 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java @@ -51,8 +51,8 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe @Autowired private ChoiceAuthorityService cas; - private final static int PAGE_TOTAL_ELEMENTS = 31; - private final static int PAGE_TOTAL_PAGES = 16; + private final static int PAGE_TOTAL_ELEMENTS = 33; + private final static int PAGE_TOTAL_PAGES = 17; @Test public void findAll() throws Exception { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionSectionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionSectionsControllerIT.java index 8ff9ff19217f..5fd5ba9fba50 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionSectionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionSectionsControllerIT.java @@ -68,8 +68,7 @@ public void testFindOne() throws Exception { getClient(token).perform(get("/api/config/submissionsections/collection")) .andExpect(status().isOk()) - .andExpect(jsonPath("$", matches("collection", true, "collection", - of("submission", "hidden", "workflow", "hidden", "edit", "hidden")))); + .andExpect(jsonPath("$", matches("collection", true, "collection"))); getClient(token).perform(get("/api/config/submissionsections/traditionalpageone")) .andExpect(status().isOk()) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubscriptionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubscriptionRestRepositoryIT.java index 038acf7e73a6..6a086cc6b75d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubscriptionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubscriptionRestRepositoryIT.java @@ -572,13 +572,25 @@ public void createSubscriptionForItemByEPersonTest() throws Exception { context.restoreAuthSystemState(); + AtomicReference idRef = new AtomicReference(); String tokenEPerson = getAuthToken(eperson.getEmail(), password); - getClient(tokenEPerson).perform(post("/api/core/subscriptions") - .param("resource", item1.getID().toString()) - .param("eperson_id", eperson.getID().toString()) - .content(new ObjectMapper().writeValueAsString(map)) - .contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isBadRequest()); + + try { + getClient(tokenEPerson).perform(post("/api/core/subscriptions") + .param("resource", item1.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.subscriptionType", is("content"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].name", is("frequency"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].value", is("W"))) + .andExpect(jsonPath("$._links.eperson.href", Matchers.endsWith("eperson"))) + .andExpect(jsonPath("$._links.resource.href", Matchers.endsWith("resource"))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + } finally { + SubscribeBuilder.deleteSubscription(idRef.get()); + } } @Test @@ -600,13 +612,25 @@ public void createSubscriptionForItemByAdminTest() throws Exception { context.restoreAuthSystemState(); + AtomicReference idRef = new AtomicReference(); String tokenAdmin = getAuthToken(admin.getEmail(), password); - getClient(tokenAdmin).perform(post("/api/core/subscriptions") - .param("resource", item1.getID().toString()) - .param("eperson_id", eperson.getID().toString()) - .content(new ObjectMapper().writeValueAsString(map)) - .contentType(MediaType.APPLICATION_JSON_VALUE)) - .andExpect(status().isBadRequest()); + + try { + getClient(tokenAdmin).perform(post("/api/core/subscriptions") + .param("resource", item1.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.subscriptionType", is("content"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].name", is("frequency"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].value", is("W"))) + .andExpect(jsonPath("$._links.eperson.href", Matchers.endsWith("eperson"))) + .andExpect(jsonPath("$._links.resource.href", Matchers.endsWith("resource"))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + } finally { + SubscribeBuilder.deleteSubscription(idRef.get()); + } } @Test diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java index 1f26477910a7..026a8d956544 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java @@ -69,6 +69,7 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.services.ConfigurationService; +import org.dspace.workflow.WorkflowItem; import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; import org.dspace.xmlworkflow.state.Step; import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; @@ -2646,4 +2647,64 @@ public void whenWorkspaceitemBecomeWorkflowitemWithAccessConditionsTheItemMustBe WorkflowItemBuilder.deleteWorkflowItem(idRef.get()); } } + + @Test + public void testWorkflowWithHiddenSections() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .withSubmissionDefinition("test-hidden") + .withWorkflowGroup(1, eperson) + .build(); + + WorkflowItem workflowItem = WorkflowItemBuilder.createWorkflowItem(context, collection) + .withTitle("Workflow Item") + .build(); + + context.restoreAuthSystemState(); + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(get("/api/workflow/workflowitems/" + workflowItem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.test-outside-workflow-hidden").exists()) + .andExpect(jsonPath("$.sections.test-outside-submission-hidden").doesNotExist()) + .andExpect(jsonPath("$.sections.test-never-hidden").exists()) + .andExpect(jsonPath("$.sections.test-always-hidden").doesNotExist()); + + } + + @Test + public void testValidationWithHiddenSteps() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .withSubmissionDefinition("test-hidden") + .withWorkflowGroup(1, eperson) + .build(); + + WorkflowItem workflowItem = WorkflowItemBuilder.createWorkflowItem(context, collection) + .build(); + + context.restoreAuthSystemState(); + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(get("/api/workflow/workflowitems/" + workflowItem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.errors", hasSize(1))) + .andExpect(jsonPath("$.errors[0].message", is("error.validation.required"))) + .andExpect(jsonPath("$.errors[0].paths", contains("/sections/test-outside-workflow-hidden/dc.title"))); + } + } 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 cc7dd26e1d6b..c7725805687d 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 @@ -9849,6 +9849,64 @@ public void supervisionOrdersEndpointTest() throws Exception { )); } + @Test + public void testSubmissionWithHiddenSections() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .withSubmissionDefinition("test-hidden") + .build(); + + WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Workspace Item") + .withIssueDate("2023-01-01") + .withType("Type") + .build(); + + context.restoreAuthSystemState(); + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(get("/api/submission/workspaceitems/" + workspaceItem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.test-outside-workflow-hidden").doesNotExist()) + .andExpect(jsonPath("$.sections.test-outside-submission-hidden").exists()) + .andExpect(jsonPath("$.sections.test-never-hidden").exists()) + .andExpect(jsonPath("$.sections.test-always-hidden").doesNotExist()); + } + + @Test + public void testValidationWithHiddenSteps() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .withSubmissionDefinition("test-hidden") + .build(); + + WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .build(); + + context.restoreAuthSystemState(); + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(get("/api/submission/workspaceitems/" + workspaceItem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.errors", hasSize(1))) + .andExpect(jsonPath("$.errors[0].message", is("error.validation.required"))) + .andExpect(jsonPath("$.errors[0].paths", contains("/sections/test-outside-submission-hidden/dc.type"))); + } + @Test public void patchBySupervisorTest() throws Exception { context.turnOffAuthorisationSystem(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSubscribeFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSubscribeFeatureIT.java index 7eb0960566fc..abb52f374ba1 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSubscribeFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSubscribeFeatureIT.java @@ -175,10 +175,12 @@ public void canNotSubscribeItemTest() throws Exception { String token3 = getAuthToken(ePersonNotSubscribePermission.getEmail(), password); getClient(token1).perform(get("/api/authz/authorizations/" + epersonToItem.getID())) - .andExpect(status().isNotFound()); + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(epersonToItem)))); getClient(token2).perform(get("/api/authz/authorizations/" + adminToItem.getID())) - .andExpect(status().isNotFound()); + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is(AuthorizationMatcher.matchAuthorization(adminToItem)))); getClient(token3).perform(get("/api/authz/authorizations/" + ePersonNotSubscribePermissionToItem.getID())) .andExpect(status().isNotFound()); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java index 1d3b5b051605..a93a964d36de 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java @@ -16,7 +16,6 @@ import org.apache.commons.codec.CharEncoding; import org.apache.commons.io.IOUtils; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; -import org.dspace.authorize.ResourcePolicy; import org.dspace.builder.BitstreamBuilder; import org.dspace.builder.BundleBuilder; import org.dspace.builder.CollectionBuilder; @@ -757,7 +756,8 @@ public void testCanMoveAdmin() throws Exception { // Verify the general admin has this feature on item 1 getClient(adminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + + "http://localhost/api/core/items/" + item1.getID()) + .param("size", "30")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -798,7 +798,7 @@ public void testCanMoveAdmin() throws Exception { // grant item 1 admin REMOVE permissions on the item’s owning collection // verify item 1 admin has this feature on item 1 context.turnOffAuthorisationSystem(); - ResourcePolicy removePermission = ResourcePolicyBuilder.createResourcePolicy(context) + ResourcePolicyBuilder.createResourcePolicy(context) .withDspaceObject(collectionX) .withAction(Constants.REMOVE) .withUser(item1Writer) @@ -820,7 +820,7 @@ public void testCanMoveWriter() throws Exception { // grant item 1 write REMOVE permissions on the item’s owning collection context.turnOffAuthorisationSystem(); - ResourcePolicy removePermission = ResourcePolicyBuilder.createResourcePolicy(context) + ResourcePolicyBuilder.createResourcePolicy(context) .withDspaceObject(collectionX) .withAction(Constants.REMOVE) .withUser(item1Writer) 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-datacitePublicationLicense.properties b/dspace/config/crosswalks/mapConverter-datacitePublicationLicense.properties new file mode 100644 index 000000000000..05bcf86c28da --- /dev/null +++ b/dspace/config/crosswalks/mapConverter-datacitePublicationLicense.properties @@ -0,0 +1,8 @@ +CC\ BY = https://creativecommons.org/licenses/by/4.0/legalcode +CC\ BY\-SA = https://creativecommons.org/licenses/by-sa/4.0/legalcode +CC\ BY\-ND = https://creativecommons.org/licenses/by-nd/4.0/legalcode +CC\ BY\-NC = https://creativecommons.org/licenses/by-nc/4.0/legalcode +CC\ BY\-NC\-SA = https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode +CC\ BY\-NC\-ND = https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode +CC0 = https://creativecommons.org/share-your-work/public-domain/cc0/ +PDM = https://creativecommons.org/publicdomain/mark/1.0/ \ No newline at end of file diff --git a/dspace/config/crosswalks/mapConverter-datacitePublicationRights.properties b/dspace/config/crosswalks/mapConverter-datacitePublicationRights.properties new file mode 100644 index 000000000000..049024f7dc56 --- /dev/null +++ b/dspace/config/crosswalks/mapConverter-datacitePublicationRights.properties @@ -0,0 +1,4 @@ +openaccess = http://purl.org/coar/access_right/c_abf2 +embargoed = http://purl.org/coar/access_right/c_f1cf +restricted = http://purl.org/coar/access_right/c_16ec +metadata\ only = http://purl.org/coar/access_right/c_14cb \ No newline at end of file diff --git a/dspace/config/crosswalks/mapConverter-datacitePublicationTypes.properties b/dspace/config/crosswalks/mapConverter-datacitePublicationTypes.properties new file mode 100644 index 000000000000..0ef150d6f8f8 --- /dev/null +++ b/dspace/config/crosswalks/mapConverter-datacitePublicationTypes.properties @@ -0,0 +1,50 @@ +Resource\ Types\:\:text = Text +Resource\ Types\:\:text\:\:annotation = Text +Resource\ Types\:\:text\:\:bibliography = Text +Resource\ Types\:\:text\:\:blog\ post = Text +Resource\ Types\:\:text\:\:book = Book +Resource\ Types\:\:text\:\:book\:\:book\ part = BookChapter +Resource\ Types\:\:text\:\:conference\ output = ConferencePaper +Resource\ Types\:\:text\:\:conference\ output\:\:conference paper not in proceedings = ConferencePaper +Resource\ Types\:\:text\:\:conference\ output\:\:conference poster not in proceedings = ConferencePaper +Resource\ Types\:\:text\:\:conference\ output\:\:conference presentation = ConferenceProceeding +Resource\ Types\:\:text\:\:conference\ output\:\:conference proceedings = ConferenceProceeding +Resource\ Types\:\:text\:\:journal = Journal +Resource\ Types\:\:text\:\:journal\:\:editorial = Journal +Resource\ Types\:\:text\:\:journal\:\:journal\ article = JournalArticle +Resource\ Types\:\:text\:\:journal\:\:journal\ article\:\:corrigendum = JournalArticle +Resource\ Types\:\:text\:\:journal\:\:journal\ article\:\:data\ paper = JournalArticle +Resource\ Types\:\:text\:\:journal\:\:journal\ article\:\:research\ article = JournalArticle +Resource\ Types\:\:text\:\:journal\:\:journal\ article\:\:review\ article = JournalArticle +Resource\ Types\:\:text\:\:journal\:\:journal\ article\:\:software\ paper = JournalArticle +Resource\ Types\:\:text\:\:journal\:\:letter\ to\ the\ editor = Journal +Resource\ Types\:\:text\:\:lecture = Text +Resource\ Types\:\:text\:\:letter = Text +Resource\ Types\:\:text\:\:magazine = Text +Resource\ Types\:\:text\:\:manuscript = Text +Resource\ Types\:\:text\:\:musical\ notation = Sound +Resource\ Types\:\:text\:\:newspaper = Text +Resource\ Types\:\:text\:\:newspaper\:\:newspaper\ article = Text +Resource\ Types\:\:text\:\:other\ periodical = Text +Resource\ Types\:\:text\:\:preprint = Preprint +Resource\ Types\:\:text\:\:report = Report +Resource\ Types\:\:text\:\:report\:\:clinical\ study = Report +Resource\ Types\:\:text\:\:report\:\:data\ management\ plan = OutputManagementPlan +Resource\ Types\:\:text\:\:report\:\:memorandum = Report +Resource\ Types\:\:text\:\:report\:\:policy\ report = Report +Resource\ Types\:\:text\:\:report\:\:project\ deliverable = Report +Resource\ Types\:\:text\:\:report\:\:research\ protocol = Report +Resource\ Types\:\:text\:\:report\:\:research\ report = Report +Resource\ Types\:\:text\:\:report\:\:technical\ report = Report +Resource\ Types\:\:text\:\:research\ proposal = Text +Resource\ Types\:\:text\:\:review = PeerReview +Resource\ Types\:\:text\:\:review\:\:book\ review = PeerReview +Resource\ Types\:\:text\:\:review\:\:commentary = PeerReview +Resource\ Types\:\:text\:\:review\:\:peer\ review = PeerReview +Resource\ Types\:\:text\:\:technical\ documentation = Text +Resource\ Types\:\:text\:\:thesis = Dissertation +Resource\ Types\:\:text\:\:thesis\:\:bachelor\ thesis = Dissertation +Resource\ Types\:\:text\:\:thesis\:\:doctoral\ thesis = Dissertation +Resource\ Types\:\:text\:\:thesis\:\:master\ thesis = Dissertation +Resource\ Types\:\:text\:\:transcription = Text +Resource\ Types\:\:text\:\:working\ paper = Preprint 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 new file mode 100644 index 000000000000..9f31693eb4e2 --- /dev/null +++ b/dspace/config/crosswalks/template/patent-datacite-xml.template @@ -0,0 +1,44 @@ + + + @virtual.primary-doi.dc-identifier-doi@ + + @group.dc-contributor-author.start@ + + @dc.contributor.author@ + @relation.dc-contributor-author.start@ + @person.identifier.orcid@ + @relation.dc-contributor-author.end@ + @oairecerif.author.affiliation@ + + @group.dc-contributor-author.end@ + + + @dc.title@ + + @dc.publisher@ + @virtual.date.dc-date-issued.YYYY@ + + @dc.subject@ + + + @dc.date.issued@ + @datacite.available@ + + @dc.language.iso@ + + + @dc.identifier.uri@ + @virtual.alternative-doi.dc-identifier-doi@ + + @dc.description.version@ + + + @oaire.licenseCondition@ + + @datacite.rights@ + + + @dc.description.abstract@ + @dc.description@ + + \ No newline at end of file diff --git a/dspace/config/crosswalks/template/product-datacite-xml.template b/dspace/config/crosswalks/template/product-datacite-xml.template new file mode 100644 index 000000000000..414527ee3378 --- /dev/null +++ b/dspace/config/crosswalks/template/product-datacite-xml.template @@ -0,0 +1,43 @@ + + + @virtual.primary-doi.dc-identifier-doi@ + + @group.dc-contributor-author.start@ + + @dc.contributor.author@ + @relation.dc-contributor-author.start@ + @person.identifier.orcid@ + @relation.dc-contributor-author.end@ + @oairecerif.author.affiliation@ + + @group.dc-contributor-author.end@ + + + @dc.title@ + + @dc.publisher@ + @virtual.date.dc-date-issued.YYYY@ + + @dc.subject@ + + + @dc.date.issued@ + + @dc.language.iso@ + + + @dc.identifier.uri@ + @virtual.alternative-doi.dc-identifier-doi@ + + @dc.description.version@ + + + @oaire.licenseCondition@ + + @datacite.rights@ + + + @dc.description.abstract@ + @dc.description@ + + \ No newline at end of file diff --git a/dspace/config/crosswalks/template/publication-datacite-xml.template b/dspace/config/crosswalks/template/publication-datacite-xml.template new file mode 100644 index 000000000000..9f31693eb4e2 --- /dev/null +++ b/dspace/config/crosswalks/template/publication-datacite-xml.template @@ -0,0 +1,44 @@ + + + @virtual.primary-doi.dc-identifier-doi@ + + @group.dc-contributor-author.start@ + + @dc.contributor.author@ + @relation.dc-contributor-author.start@ + @person.identifier.orcid@ + @relation.dc-contributor-author.end@ + @oairecerif.author.affiliation@ + + @group.dc-contributor-author.end@ + + + @dc.title@ + + @dc.publisher@ + @virtual.date.dc-date-issued.YYYY@ + + @dc.subject@ + + + @dc.date.issued@ + @datacite.available@ + + @dc.language.iso@ + + + @dc.identifier.uri@ + @virtual.alternative-doi.dc-identifier-doi@ + + @dc.description.version@ + + + @oaire.licenseCondition@ + + @datacite.rights@ + + + @dc.description.abstract@ + @dc.description@ + + \ No newline at end of file diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 65405e795af8..190ea8bb1a80 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -281,6 +281,10 @@ identifier.doi.prefix = 10.5072 # it from other services also minting DOIs under your prefix? identifier.doi.namespaceseparator = dspace/ +# if you want, you can specify custom metadata field for doi identifier +# if nothing specified, then will be used dc.identifier.doi as default +identifier.doi.metadata = dc.identifier.doi + ##### Edit Item configurations ##### # This configuration allows to set a group that will able to # use edit metadata mode @@ -466,6 +470,7 @@ useProxies = true filter.plugins = Text Extractor filter.plugins = JPEG Thumbnail filter.plugins = PDFBox JPEG Thumbnail +filter.plugins = Branded Preview JPEG # [To enable Branded Preview]: uncomment and insert the following into the plugin list @@ -620,7 +625,7 @@ crosswalk.dissemination.DataCite.preferList = false crosswalk.dissemination.DataCite.publisher = My University #crosswalk.dissemination.DataCite.dataManager = # defaults to publisher #crosswalk.dissemination.DataCite.hostingInstitution = # defaults to publisher -crosswalk.dissemination.DataCite.namespace = http://datacite.org/schema/kernel-3 +crosswalk.dissemination.DataCite.namespace = http://datacite.org/schema/kernel-4 # Crosswalk Plugin Configuration: # The purpose of Crosswalks is to translate an external metadata format to/from @@ -1110,8 +1115,8 @@ thumbnail.hqscaling = true #### Settings for BrandedPreviewJPEGFilter #### # max dimensions of the preview image -webui.preview.maxwidth = 600 -webui.preview.maxheight = 600 +webui.preview.maxwidth = 1000 +webui.preview.maxheight = 1000 # Blur before scaling. A little blur before scaling does wonders for keeping # moire in check. @@ -1857,6 +1862,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--------------------------# #------------------------------------------------------------------# @@ -1960,3 +1992,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 2b8289af278c..fc186abbb0b9 100644 --- a/dspace/config/emails/subscriptions_content +++ b/dspace/config/emails/subscriptions_content @@ -2,7 +2,7 @@ ## ## Parameters: {0} Collections updates ## {1} Communities updates -#set($subject = 'DSpace: Your Content Subscriptions') +## {2} Items updates This email is sent from DSpace-CRIS based on the chosen subscription preferences. @@ -14,3 +14,6 @@ Collections ----------- List of changed items : ${params[1]} +Items +----- +List of changed items : ${params[2]} \ 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 @@ +