diff --git a/.dockerignore b/.dockerignore index 670bcb2e25..762ee391c2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,3 +3,4 @@ .settings/ */target/ dspace/modules/*/target/ +Dockerfile.* \ No newline at end of file diff --git a/Dockerfile.dependencies b/Dockerfile.dependencies new file mode 100644 index 0000000000..a7107d46b5 --- /dev/null +++ b/Dockerfile.dependencies @@ -0,0 +1,24 @@ +# This image will be published as dspace/dspace-dependencies +# The purpose of this image is to make the build for dspace/dspace run faster + +# Step 1 - Run Maven Build +FROM maven:3-jdk-8 as build +ARG TARGET_DIR=dspace-installer +WORKDIR /app + +RUN useradd dspace \ + && mkdir /home/dspace \ + && chown -Rv dspace: /home/dspace +USER dspace + +# Copy the DSpace source code into the workdir (excluding .dockerignore contents) +ADD --chown=dspace . /app/ +COPY dspace/src/main/docker/local.cfg /app/local.cfg + +# Trigger the installation of all maven dependencies +# Clean up the built artifacts in the same step to keep the docker image small +RUN mvn package && mvn clean + +# Clear the contents of the /app directory so no artifacts are left when dspace:dspace is built +USER root +RUN rm -rf /app/* \ No newline at end of file diff --git a/Dockerfile.jdk8 b/Dockerfile.jdk8 index ec7084f49c..afe2285f49 100644 --- a/Dockerfile.jdk8 +++ b/Dockerfile.jdk8 @@ -9,20 +9,31 @@ # - default tag for branch: dspace/dspace: dspace/dspace:dspace-7_x-jdk8 # Step 1 - Run Maven Build -FROM maven:3-jdk-8 as build +FROM dspace/dspace-dependencies:dspace-7_x as build +ARG TARGET_DIR=dspace-installer WORKDIR /app +# The dspace-install directory will be written to /install +RUN mkdir /install \ + && chown -Rv dspace: /install \ + && chown -Rv dspace: /app + +USER dspace + # Copy the DSpace source code into the workdir (excluding .dockerignore contents) -ADD . /app/ +ADD --chown=dspace . /app/ COPY dspace/src/main/docker/local.cfg /app/local.cfg -RUN mvn package +# Build DSpace. Copy the dspace-install directory to /install. Clean up the build to keep the docker image small +RUN mvn package && \ + mv /app/dspace/target/${TARGET_DIR}/* /install && \ + mvn clean # Step 2 - Run Ant Deploy FROM tomcat:8-jre8 as ant_build ARG TARGET_DIR=dspace-installer -COPY --from=build /app /dspace-src -WORKDIR /dspace-src/dspace/target/${TARGET_DIR} +COPY --from=build /install /dspace-src +WORKDIR /dspace-src # Create the initial install deployment using ANT ENV ANT_VERSION 1.10.5 @@ -32,23 +43,15 @@ ENV PATH $ANT_HOME/bin:$PATH RUN mkdir $ANT_HOME && \ wget -qO- "https://www.apache.org/dist/ant/binaries/apache-ant-$ANT_VERSION-bin.tar.gz" | tar -zx --strip-components=1 -C $ANT_HOME -RUN ant update_configs update_code update_webapps update_solr_indexes +RUN ant init_installation update_configs update_code update_webapps update_solr_indexes # Step 3 - Run tomcat # Create a new tomcat image that does not retain the the build directory contents FROM tomcat:8-jre8 -COPY --from=ant_build /dspace /dspace +ENV DSPACE_INSTALL=/dspace +COPY --from=ant_build /dspace $DSPACE_INSTALL EXPOSE 8080 8009 -# Ant will be embedded in the final container to allow additional deployments -ENV ANT_VERSION 1.10.5 -ENV ANT_HOME /tmp/ant-$ANT_VERSION -ENV PATH $ANT_HOME/bin:$PATH - -RUN mkdir $ANT_HOME && \ - wget -qO- "https://www.apache.org/dist/ant/binaries/apache-ant-$ANT_VERSION-bin.tar.gz" | tar -zx --strip-components=1 -C $ANT_HOME - -ENV DSPACE_INSTALL=/dspace ENV JAVA_OPTS=-Xmx2000m RUN ln -s $DSPACE_INSTALL/webapps/solr /usr/local/tomcat/webapps/solr && \ diff --git a/Dockerfile.jdk8-test b/Dockerfile.jdk8-test index 94d85a9e8f..01697a045c 100644 --- a/Dockerfile.jdk8-test +++ b/Dockerfile.jdk8-test @@ -5,28 +5,35 @@ # - tomcat:8-jre8 # - ANT 1.10.5 # - maven:3-jdk-8 -# - note: expose /solr to any host; provide /rest over http +# - note: # - default tag for branch: dspace/dspace: dspace/dspace:dspace-7_x-jdk8-test # Step 1 - Run Maven Build -FROM maven:3-jdk-8 as build +FROM dspace/dspace-dependencies:dspace-7_x as build +ARG TARGET_DIR=dspace-installer WORKDIR /app +# The dspace-install directory will be written to /install +RUN mkdir /install \ + && chown -Rv dspace: /install \ + && chown -Rv dspace: /app + +USER dspace + # Copy the DSpace source code into the workdir (excluding .dockerignore contents) -ADD . /app/ +ADD --chown=dspace . /app/ COPY dspace/src/main/docker/local.cfg /app/local.cfg -# Provide web.xml overrides to make webapps easier to test -COPY dspace/src/main/docker/test/solr_web.xml /app/dspace-solr/src/main/webapp/WEB-INF/web.xml -COPY dspace/src/main/docker/test/rest_web.xml /app/dspace-rest/src/main/webapp/WEB-INF/web.xml - -RUN mvn package +# Build DSpace. Copy the dspace-install directory to /install. Clean up the build to keep the docker image small +RUN mvn package && \ + mv /app/dspace/target/${TARGET_DIR}/* /install && \ + mvn clean # Step 2 - Run Ant Deploy FROM tomcat:8-jre8 as ant_build ARG TARGET_DIR=dspace-installer -COPY --from=build /app /dspace-src -WORKDIR /dspace-src/dspace/target/${TARGET_DIR} +COPY --from=build /install /dspace-src +WORKDIR /dspace-src # Create the initial install deployment using ANT ENV ANT_VERSION 1.10.5 @@ -36,23 +43,15 @@ ENV PATH $ANT_HOME/bin:$PATH RUN mkdir $ANT_HOME && \ wget -qO- "https://www.apache.org/dist/ant/binaries/apache-ant-$ANT_VERSION-bin.tar.gz" | tar -zx --strip-components=1 -C $ANT_HOME -RUN ant update_configs update_code update_webapps update_solr_indexes +RUN ant init_installation update_configs update_code update_webapps update_solr_indexes # Step 3 - Run tomcat # Create a new tomcat image that does not retain the the build directory contents FROM tomcat:8-jre8 -COPY --from=ant_build /dspace /dspace +ENV DSPACE_INSTALL=/dspace +COPY --from=ant_build /dspace $DSPACE_INSTALL EXPOSE 8080 8009 -# Ant will be embedded in the final container to allow additional deployments -ENV ANT_VERSION 1.10.5 -ENV ANT_HOME /tmp/ant-$ANT_VERSION -ENV PATH $ANT_HOME/bin:$PATH - -RUN mkdir $ANT_HOME && \ - wget -qO- "https://www.apache.org/dist/ant/binaries/apache-ant-$ANT_VERSION-bin.tar.gz" | tar -zx --strip-components=1 -C $ANT_HOME - -ENV DSPACE_INSTALL=/dspace ENV JAVA_OPTS=-Xmx2000m RUN ln -s $DSPACE_INSTALL/webapps/solr /usr/local/tomcat/webapps/solr && \ @@ -62,3 +61,9 @@ RUN ln -s $DSPACE_INSTALL/webapps/solr /usr/local/tomcat/webapps/solr ln -s $DSPACE_INSTALL/webapps/rdf /usr/local/tomcat/webapps/rdf && \ ln -s $DSPACE_INSTALL/webapps/sword /usr/local/tomcat/webapps/sword && \ ln -s $DSPACE_INSTALL/webapps/swordv2 /usr/local/tomcat/webapps/swordv2 + +COPY dspace/src/main/docker/test/solr_web.xml $DSPACE_INSTALL/webapps/solr/WEB-INF/web.xml +COPY dspace/src/main/docker/test/rest_web.xml $DSPACE_INSTALL/webapps/rest/WEB-INF/web.xml + +RUN sed -i -e "s|\${dspace.dir}|$DSPACE_INSTALL|" $DSPACE_INSTALL/webapps/solr/WEB-INF/web.xml && \ + sed -i -e "s|\${dspace.dir}|$DSPACE_INSTALL|" $DSPACE_INSTALL/webapps/rest/WEB-INF/web.xml diff --git a/README.md b/README.md index a62fc1943d..aac6b7e6cc 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,57 @@ install, upgrade, customize or host DSpace, then we recommend getting in touch w The DSpace Issue Tracker can be found at: https://jira.duraspace.org/projects/DS/summary +## Testing + +### Running Tests + +By default, in DSpace, Unit Tests and Integration Tests are disabled. However, they are +run automatically by [Travis CI](https://travis-ci.org/DSpace/DSpace/) for all Pull Requests and code commits. + +* How to run both Unit Tests (via `maven-surefire-plugin`) and Integration Tests (via `maven-failsafe-plugin`): + ``` + # NOTE: while "mvn test" runs Unit Tests, + # Integration Tests only run for "verify" or "install" phases + mvn clean install -Dmaven.test.skip=false -DskipITs=false + ``` +* How to run just Unit Tests: + ``` + mvn clean test -Dmaven.test.skip=false + ``` +* How to run a *single* Unit Test + ``` + # Run all tests in a specific test class + # NOTE: testClassName is just the class name, do not include package + mvn clean test -Dmaven.test.skip=false -Dtest=[testClassName] + + # Run one test method in a specific test class + mvn clean test -Dmaven.test.skip=false -Dtest=[testClassName]#[testMethodName] + ``` +* How to run Integration Tests (requires running Unit tests too) + ``` + mvn clean verify -Dmaven.test.skip=false -DskipITs=false + ``` +* How to run a *single* Integration Test (requires running Unit tests too) + ``` + # Run all integration tests in a specific test class + # NOTE: Integration Tests only run for "verify" or "install" phases + # NOTE: testClassName is just the class name, do not include package + mvn clean verify -Dmaven.test.skip=false -DskipITs=false -Dit.test=[testClassName] + + # Run one test method in a specific test class + mvn clean verify -Dmaven.test.skip=false -DskipITs=false -Dit.test=[testClassName]#[testMethodName] + ``` +* How to run only tests of a specific DSpace module + ``` + # Before you can run only one module's tests, other modules may need installing into your ~/.m2 + cd [dspace-src] + mvn clean install + + # Then, move into a module subdirectory, and run the test command + cd [dspace-src]/dspace-spring-rest + # Choose your test command from the lists above + ``` + ## License DSpace source code is freely available under a standard [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause). diff --git a/dspace-api/src/main/java/org/dspace/content/MetadataSchemaServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/MetadataSchemaServiceImpl.java index 185addfb01..d5c2c22f88 100644 --- a/dspace-api/src/main/java/org/dspace/content/MetadataSchemaServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/MetadataSchemaServiceImpl.java @@ -14,6 +14,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.dao.MetadataSchemaDAO; +import org.dspace.content.service.MetadataFieldService; import org.dspace.content.service.MetadataSchemaService; import org.dspace.core.Context; import org.dspace.core.LogManager; @@ -33,6 +34,9 @@ public class MetadataSchemaServiceImpl implements MetadataSchemaService { */ private static Logger log = org.apache.logging.log4j.LogManager.getLogger(MetadataSchemaServiceImpl.class); + @Autowired + protected MetadataFieldService metadataFieldService; + @Autowired(required = true) protected AuthorizeService authorizeService; @@ -115,10 +119,14 @@ public class MetadataSchemaServiceImpl implements MetadataSchemaService { "Only administrators may modify the metadata registry"); } - log.info(LogManager.getHeader(context, "delete_metadata_schema", - "metadata_schema_id=" + metadataSchema.getID())); + for (MetadataField metadataField : metadataFieldService.findAllInSchema(context, metadataSchema)) { + metadataFieldService.delete(context, metadataField); + } metadataSchemaDAO.delete(context, metadataSchema); + + log.info(LogManager.getHeader(context, "delete_metadata_schema", + "metadata_schema_id=" + metadataSchema.getID())); } @Override diff --git a/dspace-api/src/main/java/org/dspace/ctask/test/WorkflowReportTest.java b/dspace-api/src/main/java/org/dspace/ctask/test/WorkflowReportTest.java new file mode 100644 index 0000000000..21ffdd0260 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/ctask/test/WorkflowReportTest.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.ctask.test; + +import java.io.IOException; + +import org.dspace.content.DSpaceObject; +import org.dspace.curate.AbstractCurationTask; +import org.dspace.curate.Curator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Curation task which simply reports its invocation without changing anything. + * Meant for testing. + * + * @author mhwood + */ +public class WorkflowReportTest + extends AbstractCurationTask { + private static final Logger LOG = LoggerFactory.getLogger(WorkflowReportTest.class); + + @Override + public int perform(DSpaceObject dso) + throws IOException { + LOG.info("Class {} as task {} received 'perform' for object {}", + WorkflowReportTest.class.getSimpleName(), taskId, dso); + curator.report(String.format( + "Class %s as task %s received 'perform' for object %s%n", + WorkflowReportTest.class.getSimpleName(), taskId, dso)); + return Curator.CURATE_SUCCESS; + } +} diff --git a/dspace-api/src/main/java/org/dspace/curate/CurationCli.java b/dspace-api/src/main/java/org/dspace/curate/CurationCli.java index d5577cf368..3832ddf3ec 100644 --- a/dspace-api/src/main/java/org/dspace/curate/CurationCli.java +++ b/dspace-api/src/main/java/org/dspace/curate/CurationCli.java @@ -9,6 +9,10 @@ package org.dspace.curate; import java.io.BufferedReader; import java.io.FileReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.Writer; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -18,6 +22,7 @@ import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.apache.commons.cli.PosixParser; +import org.apache.commons.io.output.NullOutputStream; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.core.Context; import org.dspace.core.factory.CoreServiceFactory; @@ -57,7 +62,9 @@ public class CurationCli { options.addOption("e", "eperson", true, "email address of curating eperson"); options.addOption("r", "reporter", true, - "reporter to manage results - use '-' to report to console. If absent, no reporting"); + "relative or absolute path to the desired report file. " + + "Use '-' to report to console. " + + "If absent, no reporting"); options.addOption("s", "scope", true, "transaction scope to impose: use 'object', 'curation', or 'open'. If absent, 'open' " + "applies"); @@ -165,9 +172,17 @@ public class CurationCli { } Curator curator = new Curator(); - if (reporterName != null) { - curator.setReporter(reporterName); + OutputStream reporter; + if (null == reporterName) { + reporter = new NullOutputStream(); + } else if ("-".equals(reporterName)) { + reporter = System.out; + } else { + reporter = new PrintStream(reporterName); } + Writer reportWriter = new OutputStreamWriter(reporter); + curator.setReporter(reportWriter); + if (scope != null) { Curator.TxScope txScope = Curator.TxScope.valueOf(scope.toUpperCase()); curator.setTransactionScope(txScope); diff --git a/dspace-api/src/main/java/org/dspace/curate/Curator.java b/dspace-api/src/main/java/org/dspace/curate/Curator.java index 1a78544bc0..44733174df 100644 --- a/dspace-api/src/main/java/org/dspace/curate/Curator.java +++ b/dspace-api/src/main/java/org/dspace/curate/Curator.java @@ -15,6 +15,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -69,16 +70,12 @@ public class Curator { INTERACTIVE, BATCH, ANY } - ; - // transaction scopes public static enum TxScope { OBJECT, CURATION, OPEN } - ; - - private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(Curator.class); + private static final Logger log = LogManager.getLogger(); protected static final ThreadLocal curationCtx = new ThreadLocal<>(); @@ -86,7 +83,7 @@ public class Curator { protected Map trMap = new HashMap<>(); protected List perfList = new ArrayList<>(); protected TaskQueue taskQ = null; - protected String reporter = null; + protected Appendable reporter = null; protected Invoked iMode = null; protected TaskResolver resolver = new TaskResolver(); protected TxScope txScope = TxScope.OPEN; @@ -193,7 +190,7 @@ public class Curator { * causes reporting to standard out. * @return return self (Curator instance) with reporter set */ - public Curator setReporter(String reporter) { + public Curator setReporter(Appendable reporter) { this.reporter = reporter; return this; } @@ -346,9 +343,10 @@ public class Curator { * @param message the message to output to the reporting stream. */ public void report(String message) { - // Stub for now - if ("-".equals(reporter)) { - System.out.println(message); + try { + reporter.append(message); + } catch (IOException ex) { + log.error("Task reporting failure", ex); } } diff --git a/dspace-api/src/main/java/org/dspace/curate/FileReporter.java b/dspace-api/src/main/java/org/dspace/curate/FileReporter.java new file mode 100644 index 0000000000..796b35eff5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/curate/FileReporter.java @@ -0,0 +1,88 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.curate; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.GregorianCalendar; + +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; + +/** + * Save a curation report to a unique file in the reports directory. + * Reports are named by the date and time of day, for example: + * "curation-20180916T113903045.report". + * + * @author mhwood + */ +public class FileReporter + implements Reporter { + private final Writer writer; + + /** + * Open a writer to a file in a directory named by the configuration + * property {@code report.dir}, or in {@code [DSpace]/reports} if not + * configured. + * + * @throws IOException if there is a problem with the file path. + */ + public FileReporter() + throws IOException { + // Calculate a unique(?) file name. + Date now = GregorianCalendar.getInstance().getTime(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd'T'hhmmssSSS"); + String filename = String.format("curation-%s.report", sdf.format(now)); + + // Build a path to the directory which is to receive the file. + ConfigurationService cfg = new DSpace().getConfigurationService(); + String reportDir = cfg.getProperty("report.dir"); + Path reportPath; + if (null == reportDir) { + reportPath = Paths.get(cfg.getProperty("dspace.dir"), + "reports", + filename); + } else { + reportPath = Paths.get(reportDir, filename); + } + + // Open the file. + writer = new FileWriter(reportPath.toFile()); + } + + @Override + public Appendable append(CharSequence cs) + throws IOException { + writer.append(cs); + return this; + } + + @Override + public Appendable append(CharSequence cs, int i, int i1) + throws IOException { + writer.append(cs, i, i1); + return this; + } + + @Override + public Appendable append(char c) throws IOException { + writer.append(c); + return this; + } + + @Override + public void close() throws Exception { + writer.close(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/curate/LogReporter.java b/dspace-api/src/main/java/org/dspace/curate/LogReporter.java new file mode 100644 index 0000000000..bd3ee3cffb --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/curate/LogReporter.java @@ -0,0 +1,62 @@ +/** + * 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.curate; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Write curation report records through the logging framework. + * Whole lines (strings ending in '\n') are written to the log category "curation". + * Any partial line is flushed when the reporter is {@code close()}d. + * + * @author mhwood + */ +public class LogReporter + implements Reporter { + private static final Logger LOG = LoggerFactory.getLogger("curation"); + private final StringBuilder buffer = new StringBuilder(); + + @Override + public Appendable append(CharSequence cs) + throws IOException { + for (int pos = 0; pos < cs.length(); pos++) { + char c = cs.charAt(pos); + if (c == '\n') { + LOG.info(buffer.toString()); + buffer.delete(0, buffer.length()); // Clear the buffer + } else { + buffer.append(c); + } + } + return this; + } + + @Override + public Appendable append(CharSequence cs, int i, int i1) + throws IOException { + return append(cs.subSequence(i, i1)); + } + + @Override + public Appendable append(char c) + throws IOException { + return append(String.valueOf(c)); + } + + @Override + public void close() + throws Exception { + if (buffer.length() > 0) { + LOG.info(buffer.toString()); + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/curate/Reporter.java b/dspace-api/src/main/java/org/dspace/curate/Reporter.java new file mode 100644 index 0000000000..c4c58f75cf --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/curate/Reporter.java @@ -0,0 +1,18 @@ +/** + * 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.curate; + +/** + * A marker interface needed to make curation reporter classes into plugins. + * + * @author mhwood + */ +public interface Reporter + extends Appendable, AutoCloseable { +} diff --git a/dspace-api/src/main/java/org/dspace/curate/WorkflowCuratorServiceImpl.java b/dspace-api/src/main/java/org/dspace/curate/WorkflowCuratorServiceImpl.java index 5cecc13b89..a3272c61b4 100644 --- a/dspace-api/src/main/java/org/dspace/curate/WorkflowCuratorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/curate/WorkflowCuratorServiceImpl.java @@ -30,6 +30,8 @@ import org.dspace.content.Item; import org.dspace.content.service.CollectionService; import org.dspace.core.Context; import org.dspace.core.LogManager; +import org.dspace.core.factory.CoreServiceFactory; +import org.dspace.core.service.PluginService; import org.dspace.curate.service.WorkflowCuratorService; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; @@ -56,9 +58,10 @@ import org.springframework.beans.factory.annotation.Autowired; public class WorkflowCuratorServiceImpl implements WorkflowCuratorService { /** - * log4j logger + * Logging category */ - private Logger log = org.apache.logging.log4j.LogManager.getLogger(WorkflowCuratorServiceImpl.class); + private static final Logger log + = org.apache.logging.log4j.LogManager.getLogger(); protected Map tsMap = new HashMap(); @@ -118,6 +121,7 @@ public class WorkflowCuratorServiceImpl implements WorkflowCuratorService { Curator curator = new Curator(); // are we going to perform, or just put on queue? if (step.queue != null) { + // The queue runner will call setReporter for (Task task : step.tasks) { curator.addTask(task.name); } @@ -125,7 +129,18 @@ public class WorkflowCuratorServiceImpl implements WorkflowCuratorService { basicWorkflowItemService.update(c, wfi); return false; } else { - return curate(curator, c, wfi); + PluginService plugins = CoreServiceFactory.getInstance() + .getPluginService(); + try (Reporter reporter + = (Reporter) plugins + .getSinglePlugin(Reporter.class);) { + curator.setReporter(reporter); + boolean status = curate(curator, c, wfi); + reporter.close(); + return status; + } catch (Exception e) { + log.error("Failed to close report", e); + } } } return true; diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/DatabaseUtils.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/DatabaseUtils.java index a8ca129e85..f4b0936c6f 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/DatabaseUtils.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/DatabaseUtils.java @@ -10,6 +10,7 @@ package org.dspace.storage.rdbms; import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -23,6 +24,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.sql.DataSource; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.core.Context; @@ -88,7 +90,8 @@ public class DatabaseUtils { // Usage checks if (argv.length < 1) { System.out.println("\nDatabase action argument is missing."); - System.out.println("Valid actions: 'test', 'info', 'migrate', 'repair', 'validate' or 'clean'"); + System.out.println("Valid actions: 'test', 'info', 'migrate', 'repair', 'validate', " + + "'update-sequences' or 'clean'"); System.out.println("\nOr, type 'database help' for more information.\n"); System.exit(1); } @@ -328,24 +331,49 @@ public class DatabaseUtils { e.printStackTrace(); System.exit(1); } + } else if (argv[0].equalsIgnoreCase("update-sequences")) { + try (Connection connection = dataSource.getConnection()) { + String dbType = getDbType(connection); + String sqlfile = "org/dspace/storage/rdbms/sqlmigration/" + dbType + + "/update-sequences.sql"; + InputStream sqlstream = DatabaseUtils.class.getClassLoader().getResourceAsStream(sqlfile); + if (sqlstream != null) { + String s = IOUtils.toString(sqlstream, "UTF-8"); + if (!s.isEmpty()) { + System.out.println("Running " + sqlfile); + connection.createStatement().execute(s); + System.out.println("update-sequences complete"); + } else { + System.err.println(sqlfile + " contains no SQL to execute"); + } + } else { + System.err.println(sqlfile + " not found"); + } + } } else { System.out.println("\nUsage: database [action]"); - System.out.println("Valid actions: 'test', 'info', 'migrate', 'repair' or 'clean'"); + System.out.println("Valid actions: 'test', 'info', 'migrate', 'repair', " + + "'update-sequences' or 'clean'"); System.out.println( - " - test = Performs a test connection to database to validate connection settings"); + " - test = Performs a test connection to database to " + + "validate connection settings"); System.out.println( - " - info / status = Describe basic info/status about database, including validating the " + - "compatibility of this database"); - System.out.println(" - migrate = Migrate the database to the latest version"); + " - info / status = Describe basic info/status about database, including validating the " + + "compatibility of this database"); System.out.println( - " - repair = Attempt to repair any previously failed database migrations or checksum " + - "mismatches (via Flyway repair)"); + " - migrate = Migrate the database to the latest version"); System.out.println( - " - validate = Validate current database's migration status (via Flyway validate), " + - "validating all migration checksums."); + " - repair = Attempt to repair any previously failed database " + + "migrations or checksum mismatches (via Flyway repair)"); System.out.println( - " - clean = DESTROY all data and tables in database (WARNING there is no going back!). " + - "Requires 'db.cleanDisabled=false' setting in config."); + " - validate = Validate current database's migration status (via Flyway validate), " + + "validating all migration checksums."); + System.out.println( + " - update-sequences = Update database sequences after running AIP ingest."); + System.out.println( + " - clean = DESTROY all data and tables in database " + + "(WARNING there is no going back!). " + + "Requires 'db.cleanDisabled=false' setting in config."); System.out.println(""); System.exit(0); } diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/update-sequences.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/update-sequences.sql new file mode 100644 index 0000000000..fb76d68762 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/update-sequences.sql @@ -0,0 +1,79 @@ +-- +-- 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/ +-- + +-- SQL code to update the ID (primary key) generating sequences, if some +-- import operation has set explicit IDs. +-- +-- Sequences are used to generate IDs for new rows in the database. If a +-- bulk import operation, such as an SQL dump, specifies primary keys for +-- imported data explicitly, the sequences are out of sync and need updating. +-- This SQL code does just that. +-- +-- This should rarely be needed; any bulk import should be performed using the +-- org.dspace.content API which is safe to use concurrently and in multiple +-- JVMs. The SQL code below will typically only be required after a direct +-- SQL data dump from a backup or somesuch. + +-- The 'updateseq' procedure was derived from incseq.sql found at: +-- http://www.akadia.com/services/scripts/incseq.sql + +DECLARE + PROCEDURE updateseq ( seq IN VARCHAR, + tbl IN VARCHAR, + attr IN VARCHAR, + cond IN VARCHAR DEFAULT '' ) IS + curr NUMBER := 0; + BEGIN + EXECUTE IMMEDIATE 'SELECT max(' || attr + || ') FROM ' || tbl + || ' ' || cond + INTO curr; + curr := curr + 1; + EXECUTE IMMEDIATE 'DROP SEQUENCE ' || seq; + EXECUTE IMMEDIATE 'CREATE SEQUENCE ' + || seq + || ' START WITH ' + || NVL(curr, 1); + END updateseq; + +BEGIN + updateseq('bitstreamformatregistry_seq', 'bitstreamformatregistry', + 'bitstream_format_id'); + updateseq('fileextension_seq', 'fileextension', 'file_extension_id'); + updateseq('resourcepolicy_seq', 'resourcepolicy', 'policy_id'); + updateseq('workspaceitem_seq', 'workspaceitem', 'workspace_item_id'); + updateseq('workflowitem_seq', 'workflowitem', 'workflow_id'); + updateseq('tasklistitem_seq', 'tasklistitem', 'tasklist_id'); + updateseq('registrationdata_seq', 'registrationdata', + 'registrationdata_id'); + updateseq('subscription_seq', 'subscription', 'subscription_id'); + updateseq('metadatafieldregistry_seq', 'metadatafieldregistry', + 'metadata_field_id'); + updateseq('metadatavalue_seq', 'metadatavalue', 'metadata_value_id'); + updateseq('metadataschemaregistry_seq', 'metadataschemaregistry', + 'metadata_schema_id'); + updateseq('harvested_collection_seq', 'harvested_collection', 'id'); + updateseq('harvested_item_seq', 'harvested_item', 'id'); + updateseq('webapp_seq', 'webapp', 'webapp_id'); + updateseq('requestitem_seq', 'requestitem', 'requestitem_id'); + updateseq('handle_id_seq', 'handle', 'handle_id'); + + -- Handle Sequence is a special case. Since Handles minted by DSpace + -- use the 'handle_seq', we need to ensure the next assigned handle + -- will *always* be unique. So, 'handle_seq' always needs to be set + -- to the value of the *largest* handle suffix. That way when the + -- next handle is assigned, it will use the next largest number. This + -- query does the following: + -- For all 'handle' values which have a number in their suffix + -- (after '/'), find the maximum suffix value, convert it to a + -- number, and set the 'handle_seq' to start at the next value (see + -- updateseq above for more). + updateseq('handle_seq', 'handle', + q'{to_number(regexp_replace(handle, '.*/', ''), '999999999999')}', + q'{WHERE REGEXP_LIKE(handle, '^.*/[0123456789]*$')}'); +END; \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/README.md b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/README.md index 8f549b9c45..72eb279912 100644 --- a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/README.md +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/README.md @@ -17,4 +17,16 @@ not realize you manually ran one or more scripts. Please see the Flyway Documentation for more information: http://flywaydb.org/ +## Using the update-sequences.sql script + +The `update-sequences.sql` script in this directory may still be used to update +your internal database counts if you feel they have gotten out of "sync". This +may sometimes occur after large restores of content (e.g. when using the DSpace +[AIP Backup and Restore](https://wiki.duraspace.org/display/DSDOC5x/AIP+Backup+and+Restore) +feature). + +This `update-sequences.sql` script can be executed by running +"dspace database update-sequences". It will not harm your +database (or its contents) in any way. It just ensures all database counts (i.e. +sequences) are properly set to the next available value. diff --git a/dspace/etc/postgres/update-sequences.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/update-sequences.sql similarity index 63% rename from dspace/etc/postgres/update-sequences.sql rename to dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/update-sequences.sql index 9928dbf319..c1aaadce86 100644 --- a/dspace/etc/postgres/update-sequences.sql +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/update-sequences.sql @@ -1,35 +1,10 @@ -- --- update-sequences.sql +-- 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/ -- --- Copyright (c) 2002-2016, The DSpace Foundation. All rights reserved. --- --- Redistribution and use in source and binary forms, with or without --- modification, are permitted provided that the following conditions are --- met: --- --- - Redistributions of source code must retain the above copyright --- notice, this list of conditions and the following disclaimer. --- --- - Redistributions in binary form must reproduce the above copyright --- notice, this list of conditions and the following disclaimer in the --- documentation and/or other materials provided with the distribution. --- --- Neither the name of the DSpace Foundation nor the names of its --- contributors may be used to endorse or promote products derived from --- this software without specific prior written permission. --- --- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS --- ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT --- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR --- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT --- HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, --- INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, --- BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS --- OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND --- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR --- TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE --- USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH --- DAMAGE. -- SQL code to update the ID (primary key) generating sequences, if some -- import operation has set explicit IDs. diff --git a/dspace-api/src/test/java/org/dspace/curate/CuratorTest.java b/dspace-api/src/test/java/org/dspace/curate/CuratorTest.java index 5761ee7ec7..8ca6b6c172 100644 --- a/dspace-api/src/test/java/org/dspace/curate/CuratorTest.java +++ b/dspace-api/src/test/java/org/dspace/curate/CuratorTest.java @@ -58,7 +58,7 @@ public class CuratorTest // Get and configure a Curator. Curator instance = new Curator(); - instance.setReporter("-"); // Send any report to standard out. FIXME when DS-3989 is merged + instance.setReporter(System.out); // Send any report to standard out. instance.addTask(TASK_NAME); // Configure the run. diff --git a/dspace-api/src/test/java/org/dspace/curate/ITCurator.java b/dspace-api/src/test/java/org/dspace/curate/ITCurator.java new file mode 100644 index 0000000000..ecb2667f08 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/curate/ITCurator.java @@ -0,0 +1,202 @@ +/** + * 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.curate; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.dspace.AbstractUnitTest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Site; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.services.ConfigurationService; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Drive the Curator and check results. + * + * @author mhwood + */ +public class ITCurator + extends AbstractUnitTest { + Logger LOG = LoggerFactory.getLogger(ITCurator.class); + + public ITCurator() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + /** + * The report should contain contributions from all tasks and all curated objects. + * + * @throws SQLException passed through. + * @throws IOException passed through. + * @throws AuthorizeException passed through. + */ + @Test + public void testCurate_Reporting() + throws SQLException, IOException, AuthorizeException { + // Configure for testing. + ConfigurationService cfg = kernelImpl.getConfigurationService(); + cfg.setProperty("plugin.named.org.dspace.curate.CurationTask", + Task1.class.getName() + " = task1"); + cfg.addPropertyValue("plugin.named.org.dspace.curate.CurationTask", + Task2.class.getName() + " = task2"); + + // Create some structure. + context.turnOffAuthorisationSystem(); + Site site = ContentServiceFactory.getInstance() + .getSiteService() + .findSite(context); + Community community = ContentServiceFactory.getInstance() + .getCommunityService() + .create(null, context); + + // Run some tasks. + ListReporter reporter = new ListReporter(); + Curator curator = new Curator(); + curator.setReporter(reporter); + curator.addTask("task1"); + curator.addTask("task2"); + curator.curate(context, site); + + // Validate the results. + List report = reporter.getReport(); + for (String aReport : report) { + LOG.info("Report: {}", aReport); + } + Pattern pattern; + pattern = Pattern.compile(String.format("task1.*%s", site.getHandle())); + Assert.assertTrue("A report should mention 'task1' and site's handle", + reportMatcher(report, pattern)); + pattern = Pattern.compile(String.format("task1.*%s", community.getHandle())); + Assert.assertTrue("A report should mention 'task1' and the community's handle", + reportMatcher(report, pattern)); + pattern = Pattern.compile(String.format("task2.*%s", site.getHandle())); + Assert.assertTrue("A report should mention 'task2' and the Site's handle", + reportMatcher(report, pattern)); + pattern = Pattern.compile(String.format("task2.*%s", community.getHandle())); + Assert.assertTrue("A report should mention 'task2' and the community's handle", + reportMatcher(report, pattern)); + } + + /** + * Match a collection of strings against a regular expression.\ + * + * @param reports strings to be searched. + * @param pattern expression to be matched. + * @return true if at least one string matches the expression. + */ + private boolean reportMatcher(List reports, Pattern pattern) { + for (String aReport : reports) { + if (pattern.matcher(aReport).find()) { + return true; + } + } + return false; + } + + /** + * Dummy curation task for testing. Reports how it was invoked. + * + * @author mhwood + */ + public static class Task1 extends AbstractCurationTask { + public Task1() { + } + + @Override + public int perform(DSpaceObject dso) + throws IOException { + curator.report(String.format( + "Task1 received 'perform' on taskId '%s' for object '%s'%n", + taskId, dso.getHandle())); + return Curator.CURATE_SUCCESS; + } + } + + /** + * Dummy curation task for testing. Reports how it was invoked. + * + * @author mhwood + */ + public static class Task2 extends AbstractCurationTask { + public Task2() { + } + + @Override + public int perform(DSpaceObject dso) throws IOException { + curator.report(String.format( + "Task2 received 'perform' on taskId '%s' for object '%s'%n", + taskId, dso.getHandle())); + return Curator.CURATE_SUCCESS; + } + } + + /** + * Absorb report strings into a sequential collection. + */ + class ListReporter + implements Appendable { + private final List report = new ArrayList<>(); + + /** + * Get the content of the report accumulator. + * @return accumulated reports. + */ + List getReport() { + return report; + } + + @Override + public Appendable append(CharSequence cs) + throws IOException { + report.add(cs.toString()); + return this; + } + + @Override + public Appendable append(CharSequence cs, int i, int i1) + throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public Appendable append(char c) + throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + } +} 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 eae538b50b..32fb2dbca5 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 @@ -54,15 +54,15 @@ import org.dspace.content.MetadataField; import org.dspace.content.MetadataValue; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.ItemService; -import org.dspace.core.ConfigurationManager; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.xoai.exceptions.CompilingException; import org.dspace.xoai.services.api.CollectionsService; import org.dspace.xoai.services.api.cache.XOAICacheService; import org.dspace.xoai.services.api.cache.XOAIItemCacheService; import org.dspace.xoai.services.api.cache.XOAILastCompilationCacheService; -import org.dspace.xoai.services.api.config.ConfigurationService; import org.dspace.xoai.services.api.solr.SolrServerResolver; import org.dspace.xoai.solr.DSpaceSolrSearch; import org.dspace.xoai.solr.exceptions.DSpaceSolrException; @@ -94,6 +94,8 @@ public class XOAI { private final AuthorizeService authorizeService; private final ItemService itemService; + private final static ConfigurationService configurationService = DSpaceServicesFactory + .getInstance().getConfigurationService(); private List getFileFormats(Item item) { List formats = new ArrayList<>(); @@ -283,14 +285,16 @@ public class XOAI { throws DSpaceSolrIndexerException { try { int i = 0; + int batchSize = configurationService.getIntProperty("oai.import.batch.size", 1000); SolrServer server = solrServerResolver.getServer(); + ArrayList list = new ArrayList<>(); while (iterator.hasNext()) { try { Item item = iterator.next(); if (item.getHandle() == null) { log.warn("Skipped item without handle: " + item.getID()); } else { - server.add(this.index(item)); + list.add(this.index(item)); } //Uncache the item to keep memory consumption low context.uncacheEntity(item); @@ -300,12 +304,22 @@ public class XOAI { log.error(ex.getMessage(), ex); } i++; - if (i % 100 == 0) { + if (i % 1000 == 0 && batchSize != 1000) { System.out.println(i + " items imported so far..."); } + if (i % batchSize == 0) { + System.out.println(i + " items imported so far..."); + server.add(list); + server.commit(); + list.clear(); + } } System.out.println("Total: " + i + " items"); - server.commit(); + if (i > 0) { + server.add(list); + server.commit(true, true); + list.clear(); + } return i; } catch (SolrServerException | IOException ex) { throw new DSpaceSolrIndexerException(ex.getMessage(), ex); @@ -334,6 +348,7 @@ public class XOAI { dates.add(policy.getEndDate()); } } + context.uncacheEntity(policy); } dates.add(item.getLastModified()); Collections.sort(dates); @@ -458,6 +473,7 @@ public class XOAI { return true; } } + context.uncacheEntity(policy); } return false; } @@ -477,8 +493,8 @@ public class XOAI { private static boolean getKnownExplanation(Throwable t) { if (t instanceof ConnectException) { System.err.println("Solr server (" - + ConfigurationManager.getProperty("oai", "solr.url") - + ") is down, turn it on."); + + configurationService.getProperty("oai.solr.url", "") + + ") is down, turn it on."); return true; } @@ -525,7 +541,6 @@ public class XOAI { BasicConfiguration.class }); - ConfigurationService configurationService = applicationContext.getBean(ConfigurationService.class); XOAICacheService cacheService = applicationContext.getBean(XOAICacheService.class); XOAIItemCacheService itemCacheService = applicationContext.getBean(XOAIItemCacheService.class); @@ -547,7 +562,7 @@ public class XOAI { boolean solr = true; // Assuming solr by default - solr = !("database").equals(configurationService.getProperty("oai", "storage")); + solr = !("database").equals(configurationService.getProperty("oai.storage", "solr")); boolean run = false; @@ -652,7 +667,7 @@ public class XOAI { private static void usage() { boolean solr = true; // Assuming solr by default - solr = !("database").equals(ConfigurationManager.getProperty("oai", "storage")); + solr = !("database").equals(configurationService.getProperty("oai.storage","solr")); if (solr) { System.out.println("OAI Manager Script"); diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java index 3d511a1e67..8bf037cbe3 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java @@ -346,6 +346,26 @@ public final class DSpaceConfigurationService implements ConfigurationService { } } + @Override + public synchronized boolean addPropertyValue(String name, Object value) { + if (name == null) { + throw new IllegalArgumentException("name cannot be null for setting configuration"); + } + if (value == null) { + throw new IllegalArgumentException("configuration value may not be null"); + } + + // If the value is a type of String, trim any leading/trailing spaces before saving it. + if (String.class.isInstance(value)) { + value = ((String) value).trim(); + } + + Configuration configuration = getConfiguration(); + boolean isNew = !configuration.containsKey(name); + configuration.addProperty(name, value); + return isNew; + } + /* (non-Javadoc) * @see org.dspace.services.ConfigurationService#setProperty(java.lang.String, java.lang.Object) */ diff --git a/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java b/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java index 050e4c089c..526a518a09 100644 --- a/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java +++ b/dspace-services/src/main/java/org/dspace/services/ConfigurationService.java @@ -237,6 +237,16 @@ public interface ConfigurationService { */ public boolean hasProperty(String name); + /** + * Add a value to a configuration property. + * + * @param name the property name. May not be null. + * @param value the property value. May not be null. + * @return true if a new property was created. + * @throws IllegalArgumentException if the name or value is null. + */ + public boolean addPropertyValue(String name, Object value); + /** * Set a configuration property (setting) in the system. * Type is not important here since conversion happens automatically diff --git a/dspace-spring-rest/pom.xml b/dspace-spring-rest/pom.xml index 3ea878747b..0e9e03f034 100644 --- a/dspace-spring-rest/pom.xml +++ b/dspace-spring-rest/pom.xml @@ -226,6 +226,12 @@ test + + com.flipkart.zjsonpatch + zjsonpatch + 0.4.6 + + org.springframework.data diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/OpenSearchController.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/OpenSearchController.java index ebba89fff9..37664d0f51 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/OpenSearchController.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/OpenSearchController.java @@ -43,7 +43,6 @@ import org.dspace.discovery.SearchUtils; import org.dspace.discovery.configuration.DiscoveryConfiguration; import org.dspace.discovery.configuration.DiscoverySearchFilter; -import org.springframework.boot.autoconfigure.web.ErrorController; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -60,7 +59,7 @@ import org.w3c.dom.Document; */ @Controller @RequestMapping("/opensearch") -public class OpenSearchController implements ErrorController { +public class OpenSearchController { private static final Logger log = Logger.getLogger(ScopeResolver.class); private static final String errorpath = "/error"; @@ -192,16 +191,6 @@ public class OpenSearchController implements ErrorController { } } - @RequestMapping(value = errorpath) - public String error() { - return "Error handling"; - } - - @Override - public String getErrorPath() { - return errorpath; - } - /** * Internal method for controller initialization */ diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/RestResourceController.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/RestResourceController.java index 91b679737d..4a9ca49705 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/RestResourceController.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/RestResourceController.java @@ -1047,6 +1047,33 @@ public class RestResourceController implements InitializingBean { return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } + /** + * Execute a PUT request for an entity with id of type UUID; + * + * curl -X PUT http:///api/{apiCategory}/{model}/{uuid} + * + * Example: + *
+     * {@code
+     *      curl -X PUT http:///api/core/collection/8b632938-77c2-487c-81f0-e804f63e68e6
+     * }
+     * 
+ * + * @param request the http request + * @param apiCategory the API category e.g. "core" + * @param model the DSpace model e.g. "collection" + * @param uuid the ID of the target REST object + * @param jsonNode the part of the request body representing the updated rest object + * @return the relevant REST resource + */ + @RequestMapping(method = RequestMethod.PUT, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_UUID) + public DSpaceResource put(HttpServletRequest request, + @PathVariable String apiCategory, @PathVariable String model, + @PathVariable UUID uuid, + @RequestBody JsonNode jsonNode) { + return putOneJsonInternal(request, apiCategory, model, uuid, jsonNode); + } + /** * Execute a PUT request for an entity with id of type Integer; * @@ -1150,4 +1177,4 @@ public class RestResourceController implements InitializingBean { return result; } -} +} \ No newline at end of file diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/CollectionConverter.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/CollectionConverter.java index a4c368d2b4..c1a13686db 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/CollectionConverter.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/CollectionConverter.java @@ -25,6 +25,7 @@ import org.dspace.content.service.CollectionService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.services.RequestService; +import org.dspace.services.model.Request; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -71,12 +72,17 @@ public class CollectionConverter private List getDefaultBitstreamPoliciesForCollection(UUID uuid) { - HttpServletRequest request = requestService.getCurrentRequest().getHttpServletRequest(); Context context = null; + Request currentRequest = requestService.getCurrentRequest(); + if (currentRequest != null) { + HttpServletRequest request = currentRequest.getHttpServletRequest(); + context = ContextUtil.obtainContext(request); + } else { + context = new Context(); + } Collection collection = null; List defaultCollectionPolicies = null; try { - context = ContextUtil.obtainContext(request); collection = collectionService.find(context, uuid); defaultCollectionPolicies = authorizeService.getPoliciesActionFilter(context, collection, Constants.DEFAULT_BITSTREAM_READ); diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/DSpaceObjectConverter.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/DSpaceObjectConverter.java index 76e23c31ea..462768de4b 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/DSpaceObjectConverter.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/DSpaceObjectConverter.java @@ -7,12 +7,8 @@ */ package org.dspace.app.rest.converter; -import java.util.ArrayList; -import java.util.List; - -import org.dspace.app.rest.model.MetadataEntryRest; import org.dspace.content.DSpaceObject; -import org.dspace.content.MetadataValue; +import org.springframework.beans.factory.annotation.Autowired; /** * This is the base converter from/to objects in the DSpace API data model and @@ -26,6 +22,9 @@ public abstract class DSpaceObjectConverter extends DSpaceConverter { + @Autowired(required = true) + private MetadataConverter metadataConverter; + @Override public R fromModel(M obj) { R resource = newInstance(); @@ -34,27 +33,10 @@ public abstract class DSpaceObjectConverter fullList = obj.getMetadata(); - List metadata = convertMetadataToRest(fullList); - resource.setMetadata(metadata); + resource.setMetadata(metadataConverter.convert(obj.getMetadata())); return resource; } - public List convertMetadataToRest(List fullList) { - List metadata = new ArrayList(); - for (MetadataValue mv : fullList) { - MetadataEntryRest me = new MetadataEntryRest(); - me.setKey(mv.getMetadataField().toString('.')); - me.setValue(mv.getValue()); - me.setLanguage(mv.getLanguage()); - me.setPlace(mv.getPlace()); - me.setAuthority(mv.getAuthority()); - me.setConfidence(mv.getConfidence()); - metadata.add(me); - } - return metadata; - } - @Override public M toModel(R obj) { return null; diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/ItemConverter.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/ItemConverter.java index 06a3beab15..33a083ff64 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/ItemConverter.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/ItemConverter.java @@ -15,7 +15,6 @@ import java.util.List; import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.ItemRest; -import org.dspace.app.rest.model.MetadataEntryRest; import org.dspace.app.rest.model.RelationshipRest; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; @@ -47,6 +46,8 @@ public class ItemConverter extends DSpaceObjectConverter fullList = new LinkedList<>(); fullList = itemService.getMetadata(obj, Item.ANY, Item.ANY, Item.ANY, Item.ANY, true); - List metadata = super.convertMetadataToRest(fullList); - item.setMetadata(metadata); + item.setMetadata(metadataConverter.convert(fullList)); return item; diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/JsonPatchConverter.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/JsonPatchConverter.java index c9d387b591..aba4d6a995 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/JsonPatchConverter.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/JsonPatchConverter.java @@ -116,7 +116,8 @@ public class JsonPatchConverter implements PatchConverter { Object value = operation.getValue(); if (value != null) { - opNode.set("value", mapper.valueToTree(value)); + opNode.set("value", value instanceof JsonValueEvaluator ? ((JsonValueEvaluator) value).getValueNode() + : mapper.valueToTree(value)); } patchNode.add(opNode); diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java new file mode 100644 index 0000000000..51710f45af --- /dev/null +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/MetadataConverter.java @@ -0,0 +1,100 @@ +/** + * 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.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import org.dspace.app.rest.model.MetadataRest; +import org.dspace.app.rest.model.MetadataValueRest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.DSpaceObjectService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +/** + * Converter to translate between lists of domain {@link MetadataValue}s and {@link MetadataRest} representations. + */ +@Component +public class MetadataConverter implements Converter, MetadataRest> { + + @Autowired + private ContentServiceFactory contentServiceFactory; + + @Autowired + private MetadataValueConverter valueConverter; + + /** + * Gets a rest representation of the given list of domain metadata values. + * + * @param metadataValueList the domain values. + * @return the rest representation. + */ + @Override + public MetadataRest convert(List metadataValueList) { + // Convert each value to a DTO while retaining place order in a map of key -> SortedSet + Map> mapOfSortedSets = new HashMap<>(); + for (MetadataValue metadataValue : metadataValueList) { + String key = metadataValue.getMetadataField().toString('.'); + SortedSet set = mapOfSortedSets.get(key); + if (set == null) { + set = new TreeSet<>(Comparator.comparingInt(MetadataValueRest::getPlace)); + mapOfSortedSets.put(key, set); + } + set.add(valueConverter.convert(metadataValue)); + } + + MetadataRest metadataRest = new MetadataRest(); + + // Populate MetadataRest's map of key -> List while respecting SortedSet's order + Map> mapOfLists = metadataRest.getMap(); + for (Map.Entry> entry : mapOfSortedSets.entrySet()) { + mapOfLists.put(entry.getKey(), entry.getValue().stream().collect(Collectors.toList())); + } + + return metadataRest; + } + + /** + * Sets a DSpace object's domain metadata values from a rest representation. + * + * @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 AuthorizeException if an authorization error occurs. + */ + public void setMetadata(Context context, DSpaceObject dso, MetadataRest metadataRest) + throws SQLException, AuthorizeException { + DSpaceObjectService dsoService = contentServiceFactory.getDSpaceObjectService(dso); + dsoService.clearMetadata(context, dso, Item.ANY, Item.ANY, Item.ANY, Item.ANY); + 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()) { + dsoService.addMetadata(context, dso, schema, element, qualifier, mvr.getLanguage(), + mvr.getValue(), mvr.getAuthority(), mvr.getConfidence()); + } + } + dsoService.update(context, dso); + } +} diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/MetadataValueConverter.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/MetadataValueConverter.java new file mode 100644 index 0000000000..1095e1870a --- /dev/null +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/converter/MetadataValueConverter.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.converter; + +import org.dspace.app.rest.model.MetadataValueRest; +import org.dspace.content.MetadataValue; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +/** + * Converter to translate between domain {@link MetadataValue}s and {@link MetadataValueRest} representations. + */ +@Component +public class MetadataValueConverter implements Converter { + + /** + * Gets a rest representation of the given domain metadata value. + * + * @param metadataValue the domain value. + * @return the rest representation. + */ + @Override + public MetadataValueRest convert(MetadataValue metadataValue) { + MetadataValueRest metadataValueRest = new MetadataValueRest(); + metadataValueRest.setValue(metadataValue.getValue()); + metadataValueRest.setLanguage(metadataValue.getLanguage()); + metadataValueRest.setAuthority(metadataValue.getAuthority()); + metadataValueRest.setConfidence(metadataValue.getConfidence()); + metadataValueRest.setPlace(metadataValue.getPlace()); + return metadataValueRest; + } +} diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/exception/PatchBadRequestException.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/exception/PatchBadRequestException.java index 5c3328a1dd..2000684fb2 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/exception/PatchBadRequestException.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/exception/PatchBadRequestException.java @@ -22,7 +22,10 @@ import org.springframework.web.bind.annotation.ResponseStatus; public class PatchBadRequestException extends RuntimeException { public PatchBadRequestException(String message) { - super(message); + this(message, null); } + public PatchBadRequestException(String message, Exception e) { + super(message, e); + } } diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/exception/UnprocessableEntityException.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/exception/UnprocessableEntityException.java index aa2a9106a3..8bbd7c3909 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/exception/UnprocessableEntityException.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/exception/UnprocessableEntityException.java @@ -27,6 +27,10 @@ import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY, reason = "Unprocessable request") public class UnprocessableEntityException extends RuntimeException { + public UnprocessableEntityException(String message, Throwable cause) { + super(message, cause); + } + public UnprocessableEntityException(String message) { super(message); } diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/CollectionRest.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/CollectionRest.java index f5dde2c043..77bb992004 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/CollectionRest.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/CollectionRest.java @@ -10,6 +10,7 @@ package org.dspace.app.rest.model; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; /** * The Collection REST Resource @@ -45,6 +46,7 @@ public class CollectionRest extends DSpaceObjectRest { } @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getType() { return NAME; } @@ -58,4 +60,5 @@ public class CollectionRest extends DSpaceObjectRest { public void setDefaultAccessConditions(List defaultAccessConditions) { this.defaultAccessConditions = defaultAccessConditions; } + } diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java index bf89cd7a61..1b71eb8957 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/DSpaceObjectRest.java @@ -7,8 +7,6 @@ */ package org.dspace.app.rest.model; -import java.util.List; - import org.dspace.app.rest.RestResourceController; /** @@ -22,7 +20,7 @@ public abstract class DSpaceObjectRest extends BaseObjectRest { private String name; private String handle; - List metadata; + MetadataRest metadata = new MetadataRest(); @Override public String getId() { @@ -53,11 +51,16 @@ public abstract class DSpaceObjectRest extends BaseObjectRest { this.handle = handle; } - public List getMetadata() { + /** + * Gets the rest representation of all metadata of the DSpace object. + * + * @return the metadata. + */ + public MetadataRest getMetadata() { return metadata; } - public void setMetadata(List metadata) { + public void setMetadata(MetadataRest metadata) { this.metadata = metadata; } diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/ItemRest.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/ItemRest.java index af02574f09..abe1d3473f 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/ItemRest.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/ItemRest.java @@ -11,6 +11,7 @@ import java.util.Date; import java.util.List; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; /** * The Item REST Resource @@ -28,8 +29,6 @@ public class ItemRest extends DSpaceObjectRest { private CollectionRest owningCollection; @JsonIgnore private CollectionRest templateItemOf; - //private EPerson submitter; - List bitstreams; List relationships; @@ -40,6 +39,7 @@ public class ItemRest extends DSpaceObjectRest { } @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getType() { return NAME; } diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataEntryRest.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataEntryRest.java deleted file mode 100644 index 8b6fc31612..0000000000 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataEntryRest.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.app.rest.model; - -/** - * An embeddable representation of the Metadata to use in with DSpace REST - * Resource - * - * @author Andrea Bollini (andrea.bollini at 4science.it) - */ -public class MetadataEntryRest { - String key; - - String value; - - String language; - int place; - String authority; - int confidence; - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getLanguage() { - return language; - } - - public void setLanguage(String language) { - this.language = language; - } - - public int getPlace() { - return place; - } - - public void setPlace(int place) { - this.place = place; - } - - public String getAuthority() { - return authority; - } - - public void setAuthority(String authority) { - this.authority = authority; - } - - public int getConfidence() { - return confidence; - } - - public void setConfidence(int confidence) { - this.confidence = confidence; - } -} diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataFieldRest.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataFieldRest.java index 51fafd52d5..7e6eabc4d0 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataFieldRest.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataFieldRest.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import org.dspace.app.rest.RestResourceController; /** @@ -61,6 +62,7 @@ public class MetadataFieldRest extends BaseObjectRest { } @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getType() { return NAME; } diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataRest.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataRest.java new file mode 100644 index 0000000000..23474e793e --- /dev/null +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataRest.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.app.rest.model; + +import java.util.Arrays; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; + +/** + * Rest representation of a map of metadata keys to ordered lists of values. + */ +public class MetadataRest { + + @JsonAnySetter + private SortedMap> map = new TreeMap(); + + /** + * Gets the map. + * + * @return the map of keys to ordered values. + */ + @JsonAnyGetter + public SortedMap> getMap() { + return map; + } + + /** + * Sets the metadata values for a given key. + * + * @param key the key. + * @param values the values. The values will be ordered according to their {@code place} value, if + * nonnegative. Values that are negative (the default is -1) are assumed to be non-explicitly + * set and will will be ordered at the end of any explicitly ordered values, in the order + * they are passed to this method. + * @return this instance, to support chaining calls for easy initialization. + */ + public MetadataRest put(String key, MetadataValueRest... values) { + // determine highest explicitly ordered value + int highest = -1; + for (MetadataValueRest value : values) { + if (value.getPlace() > highest) { + highest = value.getPlace(); + } + } + // add any non-explicitly ordered values after highest + for (MetadataValueRest value : values) { + if (value.getPlace() < 0) { + highest++; + value.setPlace(highest); + } + } + map.put(key, Arrays.asList(values)); + return this; + } + + @Override + public boolean equals(Object object) { + return object instanceof MetadataRest && ((MetadataRest) object).getMap().equals(map); + } +} \ No newline at end of file diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataSchemaRest.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataSchemaRest.java index c6244af87e..27229ea429 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataSchemaRest.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataSchemaRest.java @@ -7,6 +7,7 @@ */ package org.dspace.app.rest.model; +import com.fasterxml.jackson.annotation.JsonProperty; import org.dspace.app.rest.RestResourceController; /** @@ -39,6 +40,7 @@ public class MetadataSchemaRest extends BaseObjectRest { } @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getType() { return NAME; } diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataValueRest.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataValueRest.java index 4dc7be11e9..013e235847 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataValueRest.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/MetadataValueRest.java @@ -7,8 +7,10 @@ */ package org.dspace.app.rest.model; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonProperty.Access; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.dspace.app.rest.converter.MetadataConverter; /** * An embeddable representation of the Metadata to use in with DSpace REST @@ -26,8 +28,25 @@ public class MetadataValueRest { int confidence; - @JsonProperty(access = Access.READ_ONLY) - int place; + /** + * The order of this metadata value with respect to others in the same DSO with the same key. + * + * In the REST representation, all values of the same key are given as a json array that expresses + * their relative order, so there is no need to expose the exact numeric value publicly. The numeric + * value is only used at this level to ensure the intended order is respected when converting to/from json. + * + * @see MetadataConverter#convert(List) + * @see MetadataRest#put(String, MetadataValueRest...) + */ + @JsonIgnore + int place = -1; + + public MetadataValueRest() { + } + + public MetadataValueRest(String value) { + this.value = value; + } public String getValue() { return value; @@ -68,5 +87,4 @@ public class MetadataValueRest { public void setPlace(int place) { this.place = place; } - } diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/patch/JsonValueEvaluator.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/patch/JsonValueEvaluator.java index 596fa3049b..76802d9aa0 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/patch/JsonValueEvaluator.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/model/patch/JsonValueEvaluator.java @@ -45,4 +45,7 @@ public class JsonValueEvaluator implements LateObjectEvaluator { } } + public JsonNode getValueNode() { + return this.valueNode; + } } diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java index 18233a3906..d1ce46a719 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/BitstreamRestRepository.java @@ -14,11 +14,14 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.UUID; +import javax.servlet.http.HttpServletRequest; import org.dspace.app.rest.converter.BitstreamConverter; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.hateoas.BitstreamResource; +import org.dspace.app.rest.model.patch.Patch; +import org.dspace.app.rest.repository.patch.DSpaceObjectPatch; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.service.BitstreamService; @@ -39,16 +42,15 @@ import org.springframework.stereotype.Component; */ @Component(BitstreamRest.CATEGORY + "." + BitstreamRest.NAME) -public class BitstreamRestRepository extends DSpaceRestRepository { +public class BitstreamRestRepository extends DSpaceObjectRestRepository { + + private final BitstreamService bs; @Autowired - BitstreamService bs; - - @Autowired - BitstreamConverter converter; - - public BitstreamRestRepository() { - System.out.println("Repository initialized by Spring"); + public BitstreamRestRepository(BitstreamService dsoService, + BitstreamConverter dsoConverter) { + super(dsoService, dsoConverter, new DSpaceObjectPatch() { }); + this.bs = dsoService; } @Override @@ -70,7 +72,7 @@ public class BitstreamRestRepository extends DSpaceRestRepository page = new PageImpl(bit, pageable, total).map(converter); + Page page = new PageImpl(bit, pageable, total).map(dsoConverter); return page; } + @Override + @PreAuthorize("hasPermission(#id, 'BITSTREAM', 'WRITE')") + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID id, + Patch patch) throws AuthorizeException, SQLException { + patchDSpaceObject(apiCategory, model, id, patch); + } + @Override public Class getDomainClass() { return BitstreamRest.class; diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java index 66474c6029..a7ba1a5552 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/CollectionRestRepository.java @@ -7,23 +7,38 @@ */ package org.dspace.app.rest.repository; +import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.BadRequestException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.Parameter; import org.dspace.app.rest.SearchRestMethod; import org.dspace.app.rest.converter.CollectionConverter; +import org.dspace.app.rest.converter.MetadataConverter; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; +import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.CollectionRest; import org.dspace.app.rest.model.CommunityRest; import org.dspace.app.rest.model.hateoas.CollectionResource; +import org.dspace.app.rest.model.patch.Patch; +import org.dspace.app.rest.repository.patch.DSpaceObjectPatch; +import org.dspace.app.rest.utils.CollectionRestEqualityUtils; +import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.util.UUIDUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -39,20 +54,27 @@ import org.springframework.stereotype.Component; */ @Component(CollectionRest.CATEGORY + "." + CollectionRest.NAME) -public class CollectionRestRepository extends DSpaceRestRepository { +public class CollectionRestRepository extends DSpaceObjectRestRepository { + + private final CollectionService cs; @Autowired CommunityService communityService; - @Autowired - CollectionService cs; - @Autowired CollectionConverter converter; + @Autowired + MetadataConverter metadataConverter; - public CollectionRestRepository() { - System.out.println("Repository initialized by Spring"); + @Autowired + CollectionRestEqualityUtils collectionRestEqualityUtils; + + + public CollectionRestRepository(CollectionService dsoService, + CollectionConverter dsoConverter) { + super(dsoService, dsoConverter, new DSpaceObjectPatch() {}); + this.cs = dsoService; } @Override @@ -67,7 +89,7 @@ public class CollectionRestRepository extends DSpaceRestRepository page = new PageImpl(collections, pageable, total).map(converter); + Page page = new PageImpl(collections, pageable, total).map(dsoConverter); return page; } @@ -108,7 +130,7 @@ public class CollectionRestRepository extends DSpaceRestRepository page = utils.getPage(collections, pageable).map(converter); + Page page = utils.getPage(collections, pageable).map(dsoConverter); return page; } @@ -125,10 +147,17 @@ public class CollectionRestRepository extends DSpaceRestRepository page = utils.getPage(collections, pageable).map(converter); + Page page = utils.getPage(collections, pageable).map(dsoConverter); return page; } + @Override + @PreAuthorize("hasPermission(#id, 'COLLECTION', 'WRITE')") + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID id, + Patch patch) throws AuthorizeException, SQLException { + patchDSpaceObject(apiCategory, model, id, patch); + } + @Override public Class getDomainClass() { return CollectionRest.class; @@ -139,4 +168,97 @@ public class CollectionRestRepository extends DSpaceRestRepository { +public class CommunityRestRepository extends DSpaceObjectRestRepository { - @Autowired - CommunityService cs; + private final CommunityService cs; @Autowired CommunityConverter converter; - public CommunityRestRepository() { - System.out.println("Repository initialized by Spring"); + @Autowired + MetadataConverter metadataConverter; + + @Autowired + CommunityRestEqualityUtils communityRestEqualityUtils; + + public CommunityRestRepository(CommunityService dsoService, + CommunityConverter dsoConverter) { + super(dsoService, dsoConverter, new DSpaceObjectPatch() {}); + this.cs = dsoService; } @Override @@ -60,7 +74,7 @@ public class CommunityRestRepository extends DSpaceRestRepository page = new PageImpl(communities, pageable, total).map(converter); + Page page = new PageImpl(communities, pageable, total).map(dsoConverter); return page; } @@ -130,7 +155,7 @@ public class CommunityRestRepository extends DSpaceRestRepository page = utils.getPage(topCommunities, pageable).map(converter); + Page page = utils.getPage(topCommunities, pageable).map(dsoConverter); return page; } @@ -151,10 +176,17 @@ public class CommunityRestRepository extends DSpaceRestRepository page = utils.getPage(subCommunities, pageable).map(converter); + Page page = utils.getPage(subCommunities, pageable).map(dsoConverter); return page; } + @Override + @PreAuthorize("hasPermission(#id, 'COMMUNITY', 'WRITE')") + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID id, + Patch patch) throws AuthorizeException, SQLException { + patchDSpaceObject(apiCategory, model, id, patch); + } + @Override public Class getDomainClass() { return CommunityRest.class; @@ -165,4 +197,49 @@ public class CommunityRestRepository extends DSpaceRestRepository the specific type of DSpaceObject. + * @param the corresponding DSpaceObjectRest. + */ +public abstract class DSpaceObjectRestRepository + extends DSpaceRestRepository { + + final DSpaceObjectService dsoService; + final DSpaceObjectPatch dsoPatch; + final DSpaceObjectConverter dsoConverter; + + @Autowired + MetadataConverter metadataConverter; + + DSpaceObjectRestRepository(DSpaceObjectService dsoService, + DSpaceObjectConverter dsoConverter, + DSpaceObjectPatch dsoPatch) { + this.dsoService = dsoService; + this.dsoPatch = dsoPatch; + this.dsoConverter = dsoConverter; + } + + /** + * Updates the DSpaceObject according to the given Patch. + * + * @param apiCategory the api category. + * @param model the api model. + * @param id the id of the DSpaceObject. + * @param patch the patch to apply. + * @throws AuthorizeException if the action is unauthorized. + * @throws ResourceNotFoundException if the DSpace object was not found. + * @throws SQLException if a database error occurs. + * @throws UnprocessableEntityException if the patch attempts to modify an unmodifiable attribute of the object. + */ + protected void patchDSpaceObject(String apiCategory, String model, UUID id, Patch patch) + throws AuthorizeException, ResourceNotFoundException, SQLException, UnprocessableEntityException { + M dso = dsoService.find(obtainContext(), id); + if (dso == null) { + throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + id + " not found"); + } + R dsoRest = dsoPatch.patch(findOne(id), patch.getOperations()); + updateDSpaceObject(dso, dsoRest); + } + + /** + * Applies the changes in the given rest DSpace object to the model DSpace object. + * The default implementation updates metadata if needed. Subclasses should extend + * to support updates of additional properties. + * + * @param dso the dso to apply changes to. + * @param dsoRest the rest representation of the new desired state. + */ + protected void updateDSpaceObject(M dso, R dsoRest) + throws AuthorizeException, SQLException { + R origDsoRest = dsoConverter.fromModel(dso); + if (!origDsoRest.getMetadata().equals(dsoRest.getMetadata())) { + metadataConverter.setMetadata(obtainContext(), dso, dsoRest.getMetadata()); + } + } +} diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java index 67ee9556a4..101025996d 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/DSpaceRestRepository.java @@ -27,6 +27,7 @@ import org.dspace.app.rest.model.patch.Patch; import org.dspace.app.util.DCInputsReaderException; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; +import org.dspace.content.service.MetadataFieldService; import org.dspace.core.Context; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -56,6 +57,9 @@ public abstract class DSpaceRestRepository thisRepository; + @Autowired + private MetadataFieldService metadataFieldService; + @Override public S save(S entity) { Context context = null; @@ -73,7 +77,7 @@ public abstract class DSpaceRestRepository { - EPersonService es = EPersonServiceFactory.getInstance().getEPersonService(); +public class EPersonRestRepository extends DSpaceObjectRestRepository { @Autowired AuthorizeService authorizeService; + private final EPersonService es; + @Autowired - EPersonConverter converter; + MetadataConverter metadataConverter; @Autowired EPersonPatch epersonPatch; + public EPersonRestRepository(EPersonService dsoService, + EPersonConverter dsoConverter, + EPersonPatch dsoPatch) { + super(dsoService, dsoConverter, dsoPatch); + this.es = dsoService; + } + @Override protected EPersonRest createAndReturn(Context context) throws AuthorizeException { @@ -89,18 +92,12 @@ public class EPersonRestRepository extends DSpaceRestRepository page = new PageImpl(epersons, pageable, total).map(converter); + Page page = new PageImpl(epersons, pageable, total).map(dsoConverter); return page; } @@ -159,7 +156,7 @@ public class EPersonRestRepository extends DSpaceRestRepository page = new PageImpl(epersons, pageable, total).map(converter); + Page page = new PageImpl(epersons, pageable, total).map(dsoConverter); return page; } @@ -185,42 +182,22 @@ public class EPersonRestRepository extends DSpaceRestRepository operations = patch.getOperations(); - EPersonRest ePersonRest = findOne(context, uuid); - EPersonRest patchedModel = (EPersonRest) epersonPatch.patch(ePersonRest, operations); - updatePatchedValues(context, patchedModel, eperson); - - } catch (SQLException e) { - throw new RuntimeException(e.getMessage(), e); - } + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID uuid, + Patch patch) throws AuthorizeException, SQLException { + patchDSpaceObject(apiCategory, model, uuid, patch); } - /** - * Applies changes in the rest model. - * @param context - * @param ePersonRest the updated eperson rest - * @param ePerson the eperson content object - * @throws SQLException - * @throws AuthorizeException - */ - private void updatePatchedValues(Context context, EPersonRest ePersonRest, EPerson ePerson) - throws SQLException, AuthorizeException { + @Override + protected void updateDSpaceObject(EPerson ePerson, EPersonRest ePersonRest) + throws AuthorizeException, SQLException { + super.updateDSpaceObject(ePerson, ePersonRest); + Context context = obtainContext(); if (ePersonRest.getPassword() != null) { es.setPassword(ePerson, ePersonRest.getPassword()); } @@ -235,7 +212,6 @@ public class EPersonRestRepository extends DSpaceRestRepository { +public class GroupRestRepository extends DSpaceObjectRestRepository { @Autowired GroupService gs; @Autowired - GroupConverter converter; + GroupRestRepository(GroupService dsoService, + GroupConverter dsoConverter) { + super(dsoService, dsoConverter, new DSpaceObjectPatch() {}); + this.gs = dsoService; + } + + @Autowired + MetadataConverter metadataConverter; @Override @PreAuthorize("hasAuthority('ADMIN')") @@ -65,19 +74,12 @@ public class GroupRestRepository extends DSpaceRestRepository { group = gs.create(context); gs.setName(group, groupRest.getName()); gs.update(context, group); - - if (groupRest.getMetadata() != null) { - for (MetadataEntryRest mer: groupRest.getMetadata()) { - String[] metadatakey = mer.getKey().split("\\."); - gs.addMetadata(context, group, metadatakey[0], metadatakey[1], - metadatakey.length == 3 ? metadatakey[2] : null, mer.getLanguage(), mer.getValue()); - } - } + metadataConverter.setMetadata(context, group, groupRest.getMetadata()); } catch (SQLException excSQL) { throw new RuntimeException(excSQL.getMessage(), excSQL); } - return converter.convert(group); + return dsoConverter.convert(group); } @Override @@ -92,7 +94,7 @@ public class GroupRestRepository extends DSpaceRestRepository { if (group == null) { return null; } - return converter.fromModel(group); + return dsoConverter.fromModel(group); } @PreAuthorize("hasAuthority('ADMIN')") @@ -106,10 +108,17 @@ public class GroupRestRepository extends DSpaceRestRepository { } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); } - Page page = new PageImpl(groups, pageable, total).map(converter); + Page page = new PageImpl(groups, pageable, total).map(dsoConverter); return page; } + @Override + @PreAuthorize("hasPermission(#id, 'GROUP', 'WRITE')") + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID id, + Patch patch) throws AuthorizeException, SQLException { + patchDSpaceObject(apiCategory, model, id, patch); + } + @Override public Class getDomainClass() { return GroupRest.class; diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java index f8d19a6ce6..514c200333 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/ItemRestRepository.java @@ -13,21 +13,32 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.UUID; +import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.BadRequestException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.dspace.app.rest.converter.ItemConverter; -import org.dspace.app.rest.exception.PatchBadRequestException; +import org.dspace.app.rest.converter.MetadataConverter; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.ItemRest; import org.dspace.app.rest.model.hateoas.ItemResource; -import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.patch.Patch; import org.dspace.app.rest.repository.patch.ItemPatch; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Collection; import org.dspace.content.Item; +import org.dspace.content.WorkspaceItem; +import org.dspace.content.service.CollectionService; +import org.dspace.content.service.InstallItemService; import org.dspace.content.service.ItemService; +import org.dspace.content.service.WorkspaceItemService; import org.dspace.core.Context; +import org.dspace.util.UUIDUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -43,22 +54,35 @@ import org.springframework.stereotype.Component; */ @Component(ItemRest.CATEGORY + "." + ItemRest.NAME) -public class ItemRestRepository extends DSpaceRestRepository { +public class ItemRestRepository extends DSpaceObjectRestRepository { private static final Logger log = Logger.getLogger(ItemRestRepository.class); - @Autowired - ItemService is; + private final ItemService is; @Autowired - ItemConverter converter; + MetadataConverter metadataConverter; @Autowired ItemPatch itemPatch; + @Autowired + WorkspaceItemService workspaceItemService; - public ItemRestRepository() { - System.out.println("Repository initialized by Spring"); + @Autowired + ItemService itemService; + + @Autowired + CollectionService collectionService; + + @Autowired + InstallItemService installItemService; + + public ItemRestRepository(ItemService dsoService, + ItemConverter dsoConverter, + ItemPatch dsoPatch) { + super(dsoService, dsoConverter, dsoPatch); + this.is = dsoService; } @Override @@ -73,7 +97,7 @@ public class ItemRestRepository extends DSpaceRestRepository { if (item == null) { return null; } - return converter.fromModel(item); + return dsoConverter.fromModel(item); } @Override @@ -92,55 +116,33 @@ public class ItemRestRepository extends DSpaceRestRepository { } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); } - Page page = new PageImpl(items, pageable, total).map(converter); + Page page = new PageImpl(items, pageable, total).map(dsoConverter); return page; } @Override - public void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID uuid, - Patch patch) - throws UnprocessableEntityException, PatchBadRequestException, SQLException, AuthorizeException, - ResourceNotFoundException { - - Item item = is.find(context, uuid); - - if (item == null) { - throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + uuid + " not found"); - } - - List operations = patch.getOperations(); - ItemRest itemRest = findOne(uuid); - - ItemRest patchedModel = (ItemRest) itemPatch.patch(itemRest, operations); - updatePatchedValues(context, patchedModel, item); + @PreAuthorize("hasPermission(#id, 'ITEM', 'WRITE')") + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID id, + Patch patch) throws AuthorizeException, SQLException { + patchDSpaceObject(apiCategory, model, id, patch); } - /** - * Persists changes to the rest model. - * @param context - * @param itemRest the updated item rest resource - * @param item the item content object - * @throws SQLException - * @throws AuthorizeException - */ - private void updatePatchedValues(Context context, ItemRest itemRest, Item item) - throws SQLException, AuthorizeException { + @Override + protected void updateDSpaceObject(Item item, ItemRest itemRest) + throws AuthorizeException, SQLException { + super.updateDSpaceObject(item, itemRest); - try { - if (itemRest.getWithdrawn() != item.isWithdrawn()) { - if (itemRest.getWithdrawn()) { - is.withdraw(context, item); - } else { - is.reinstate(context, item); - } + Context context = obtainContext(); + if (itemRest.getWithdrawn() != item.isWithdrawn()) { + if (itemRest.getWithdrawn()) { + is.withdraw(context, item); + } else { + is.reinstate(context, item); } - if (itemRest.getDiscoverable() != item.isDiscoverable()) { - item.setDiscoverable(itemRest.getDiscoverable()); - is.update(context, item); - } - } catch (SQLException | AuthorizeException e) { - e.printStackTrace(); - throw e; + } + if (itemRest.getDiscoverable() != item.isDiscoverable()) { + item.setDiscoverable(itemRest.getDiscoverable()); + is.update(context, item); } } @@ -155,10 +157,15 @@ public class ItemRestRepository extends DSpaceRestRepository { } @Override + @PreAuthorize("hasAuthority('ADMIN')") protected void delete(Context context, UUID id) throws AuthorizeException { Item item = null; try { item = is.find(context, id); + if (item == null) { + throw new ResourceNotFoundException(ItemRest.CATEGORY + "." + ItemRest.NAME + + " with id: " + id + " not found"); + } if (is.isInProgressSubmission(context, item)) { throw new UnprocessableEntityException("The item cannot be deleted. " + "It's part of a in-progress submission."); @@ -177,4 +184,68 @@ public class ItemRestRepository extends DSpaceRestRepository { } } + @Override + @PreAuthorize("hasAuthority('ADMIN')") + protected ItemRest createAndReturn(Context context) throws AuthorizeException, SQLException { + HttpServletRequest req = getRequestService().getCurrentRequest().getHttpServletRequest(); + String owningCollectionUuidString = req.getParameter("owningCollection"); + ObjectMapper mapper = new ObjectMapper(); + ItemRest itemRest = null; + try { + ServletInputStream input = req.getInputStream(); + itemRest = mapper.readValue(input, ItemRest.class); + } catch (IOException e1) { + throw new UnprocessableEntityException("Error parsing request body", e1); + } + + if (itemRest.getInArchive() == false) { + throw new BadRequestException("InArchive attribute should not be set to false for the create"); + } + UUID owningCollectionUuid = UUIDUtils.fromString(owningCollectionUuidString); + Collection collection = collectionService.find(context, owningCollectionUuid); + if (collection == null) { + throw new BadRequestException("The given owningCollection parameter is invalid: " + + owningCollectionUuid); + } + WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, false); + Item item = workspaceItem.getItem(); + item.setArchived(true); + item.setOwningCollection(collection); + item.setDiscoverable(itemRest.getDiscoverable()); + item.setLastModified(itemRest.getLastModified()); + metadataConverter.setMetadata(context, item, itemRest.getMetadata()); + + Item itemToReturn = installItemService.installItem(context, workspaceItem); + + return dsoConverter.fromModel(itemToReturn); + } + + @Override + @PreAuthorize("hasPermission(#id, 'ITEM', 'WRITE')") + protected ItemRest put(Context context, HttpServletRequest request, String apiCategory, String model, UUID uuid, + JsonNode jsonNode) + throws RepositoryMethodNotImplementedException, SQLException, AuthorizeException { + HttpServletRequest req = getRequestService().getCurrentRequest().getHttpServletRequest(); + ObjectMapper mapper = new ObjectMapper(); + ItemRest itemRest = null; + try { + itemRest = mapper.readValue(jsonNode.toString(), ItemRest.class); + } catch (IOException e1) { + throw new UnprocessableEntityException("Error parsing request body", e1); + } + + Item item = itemService.find(context, uuid); + if (item == null) { + throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + uuid + " not found"); + } + + if (StringUtils.equals(uuid.toString(), itemRest.getId())) { + metadataConverter.setMetadata(context, item, itemRest.getMetadata()); + } else { + throw new IllegalArgumentException("The UUID in the Json and the UUID in the url do not match: " + + uuid + ", " + + itemRest.getId()); + } + return dsoConverter.fromModel(item); + } } \ No newline at end of file diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java index dddb62b5da..3b67acaf7c 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/MetadataFieldRestRepository.java @@ -7,22 +7,37 @@ */ package org.dspace.app.rest.repository; +import static java.lang.Integer.parseInt; +import static org.apache.commons.lang3.StringUtils.isBlank; + +import java.io.IOException; import java.sql.SQLException; import java.util.List; +import java.util.Objects; +import javax.servlet.http.HttpServletRequest; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; import org.dspace.app.rest.Parameter; import org.dspace.app.rest.SearchRestMethod; import org.dspace.app.rest.converter.MetadataFieldConverter; +import org.dspace.app.rest.exception.PatchBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.MetadataFieldRest; import org.dspace.app.rest.model.hateoas.MetadataFieldResource; +import org.dspace.authorize.AuthorizeException; import org.dspace.content.MetadataField; import org.dspace.content.MetadataSchema; +import org.dspace.content.NonUniqueMetadataException; import org.dspace.content.service.MetadataFieldService; import org.dspace.content.service.MetadataSchemaService; import org.dspace.core.Context; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; /** @@ -34,7 +49,7 @@ import org.springframework.stereotype.Component; public class MetadataFieldRestRepository extends DSpaceRestRepository { @Autowired - MetadataFieldService metaFieldService; + MetadataFieldService metadataFieldService; @Autowired MetadataSchemaService metadataSchemaService; @@ -49,7 +64,7 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository findAll(Context context, Pageable pageable) { List metadataField = null; try { - metadataField = metaFieldService.findAll(context); + metadataField = metadataFieldService.findAll(context); } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); } @@ -73,7 +88,7 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository findBySchema(@Parameter(value = "schema", required = true) String schemaName, - Pageable pageable) { + Pageable pageable) { Context context = obtainContext(); List metadataFields = null; try { @@ -81,7 +96,7 @@ public class MetadataFieldRestRepository extends DSpaceRestRepository { @Autowired - MetadataSchemaService metaScemaService; + MetadataSchemaService metadataSchemaService; @Autowired MetadataSchemaConverter converter; @@ -42,7 +56,7 @@ public class MetadataSchemaRestRepository extends DSpaceRestRepository findAll(Context context, Pageable pageable) { List metadataSchema = null; try { - metadataSchema = metaScemaService.findAll(context); + metadataSchema = metadataSchemaService.findAll(context); } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); } @@ -73,4 +87,100 @@ public class MetadataSchemaRestRepository extends DSpaceRestRepository { +public class SiteRestRepository extends DSpaceObjectRestRepository { + + private final SiteService sitesv; @Autowired - SiteService sitesv; - - @Autowired - SiteConverter converter; - - - public SiteRestRepository() { + public SiteRestRepository(SiteService dsoService, + SiteConverter dsoConverter) { + super(dsoService, dsoConverter, new DSpaceObjectPatch() {}); + this.sitesv = dsoService; } @Override @@ -54,7 +58,7 @@ public class SiteRestRepository extends DSpaceRestRepository { if (site == null) { return null; } - return converter.fromModel(site); + return dsoConverter.fromModel(site); } @Override @@ -66,10 +70,17 @@ public class SiteRestRepository extends DSpaceRestRepository { } catch (SQLException e) { throw new RuntimeException(e.getMessage(), e); } - Page page = new PageImpl(sites, pageable, total).map(converter); + Page page = new PageImpl(sites, pageable, total).map(dsoConverter); return page; } + @Override + @PreAuthorize("hasAuthority('ADMIN')") + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, UUID id, + Patch patch) throws AuthorizeException, SQLException { + patchDSpaceObject(apiCategory, model, id, patch); + } + @Override public Class getDomainClass() { return SiteRest.class; diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/AbstractResourcePatch.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/AbstractResourcePatch.java index 4033718941..547e33842d 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/AbstractResourcePatch.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/AbstractResourcePatch.java @@ -30,7 +30,7 @@ public abstract class AbstractResourcePatch { * @throws UnprocessableEntityException * @throws PatchBadRequestException */ - public RestModel patch(R restModel, List operations) { + public R patch(R restModel, List operations) { // Note: the list of possible operations is taken from JsonPatchConverter class. Does not implement // test https://tools.ietf.org/html/rfc6902#section-4.6 diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/DSpaceObjectPatch.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/DSpaceObjectPatch.java new file mode 100644 index 0000000000..e0e520496f --- /dev/null +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/DSpaceObjectPatch.java @@ -0,0 +1,81 @@ +/** + * 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; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.flipkart.zjsonpatch.JsonPatch; +import org.dspace.app.rest.converter.JsonPatchConverter; +import org.dspace.app.rest.model.DSpaceObjectRest; +import org.dspace.app.rest.model.MetadataRest; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.patch.Patch; + +/** + * Base class for DSpaceObject-based PATCH operations, providing common functionality. + * + * @param the type of DSpaceObjectRest object the class is applicable to. + */ +public abstract class DSpaceObjectPatch extends AbstractResourcePatch { + + private static final String METADATA_PATH = "/metadata"; + + private ObjectMapper objectMapper = new ObjectMapper(); + + private JsonPatchConverter jsonPatchConverter = new JsonPatchConverter(objectMapper); + + /** + * Applies the given patch operations to the given DSpaceObjectRest instance. + * + * This extends the default implementation by first applying metadata-based patch operations, + * then applying any others. + * + * @param dsoRest the instance to apply the changes to. + * @param operations the list of patch operations. + * @return the modified DSpaceObectRest instance. + */ + @Override + public R patch(R dsoRest, List operations) { + List metadataOperations = new ArrayList<>(); + List otherOperations = new ArrayList<>(); + + for (Operation operation : operations) { + String path = operation.getPath(); + if (path.equals(METADATA_PATH) || path.startsWith(METADATA_PATH + "/")) { + metadataOperations.add(operation); + } else { + otherOperations.add(operation); + } + } + + if (!metadataOperations.isEmpty()) { + dsoRest.setMetadata(applyMetadataPatch( + jsonPatchConverter.convert(new Patch(metadataOperations)), + dsoRest.getMetadata())); + } + + return super.patch(dsoRest, otherOperations); + } + + private MetadataRest applyMetadataPatch(JsonNode patch, MetadataRest metadataRest) { + try { + ObjectNode objectNode = objectMapper.createObjectNode(); + JsonNode metadataNode = objectMapper.valueToTree(metadataRest); + objectNode.replace("metadata", metadataNode); + JsonPatch.applyInPlace(patch, objectNode); + return objectMapper.treeToValue(objectNode.get("metadata"), MetadataRest.class); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/EPersonPatch.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/EPersonPatch.java index 2204c98cc8..bb9ce69090 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/EPersonPatch.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/EPersonPatch.java @@ -20,7 +20,7 @@ import org.springframework.stereotype.Component; * Provides patch operations for eperson updates. */ @Component -public class EPersonPatch extends AbstractResourcePatch { +public class EPersonPatch extends DSpaceObjectPatch { @Autowired EPersonOperationFactory patchFactory; diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/ItemPatch.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/ItemPatch.java index d68edbf444..b34a62d1ea 100644 --- a/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/ItemPatch.java +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/repository/patch/ItemPatch.java @@ -20,7 +20,7 @@ import org.springframework.stereotype.Component; * Provides PATCH operations for item updates. */ @Component -public class ItemPatch extends AbstractResourcePatch { +public class ItemPatch extends DSpaceObjectPatch { @Autowired ItemOperationFactory patchFactory; diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/utils/CollectionRestEqualityUtils.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/utils/CollectionRestEqualityUtils.java new file mode 100644 index 0000000000..0cb55ce212 --- /dev/null +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/utils/CollectionRestEqualityUtils.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.app.rest.utils; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.CollectionRest; +import org.springframework.stereotype.Component; + +/** + * This class will contain methods that can define in what way CollectionRest objects are equal + */ +@Component +public class CollectionRestEqualityUtils extends DSpaceObjectRestEqualityUtils { + + /** + * This method will return a boolean indicating whether the given CollectionRest objects are equal + * through comparing their attributes + * @param original The original CollectionRest object + * @param updated The CollectionRest object that has to be checked for equality + * @return A boolean indicating whether they're equal or not + */ + public boolean isCollectionRestEqualWithoutMetadata(CollectionRest original, CollectionRest updated) { + return super.isDSpaceObjectEqualsWithoutMetadata(original, updated) && + StringUtils.equals(original.getCategory(), updated.getCategory()) && + StringUtils.equals(original.getType(), updated.getType()); + + } +} diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/utils/CommunityRestEqualityUtils.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/utils/CommunityRestEqualityUtils.java new file mode 100644 index 0000000000..97f4a4ee3a --- /dev/null +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/utils/CommunityRestEqualityUtils.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.app.rest.utils; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.CommunityRest; +import org.springframework.stereotype.Component; + +/** + * This class will contain methods that can define in what way CommunityRest objects are equal + */ +@Component +public class CommunityRestEqualityUtils extends DSpaceObjectRestEqualityUtils { + + /** + * This method will return a boolean indicating whether the given CommunityRest objects are equal + * through comparing their attributes + * @param original The original CommunityRest object + * @param updated The CommunityRest object that has to be checked for equality + * @return A boolean indicating whether they're equal or not + */ + public boolean isCommunityRestEqualWithoutMetadata(CommunityRest original, CommunityRest updated) { + return super.isDSpaceObjectEqualsWithoutMetadata(original, updated) && + StringUtils.equals(original.getCategory(), updated.getCategory()) && + StringUtils.equals(original.getType(), updated.getType()); + + } +} diff --git a/dspace-spring-rest/src/main/java/org/dspace/app/rest/utils/DSpaceObjectRestEqualityUtils.java b/dspace-spring-rest/src/main/java/org/dspace/app/rest/utils/DSpaceObjectRestEqualityUtils.java new file mode 100644 index 0000000000..c71eb5bf85 --- /dev/null +++ b/dspace-spring-rest/src/main/java/org/dspace/app/rest/utils/DSpaceObjectRestEqualityUtils.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.app.rest.utils; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.DSpaceObjectRest; +import org.springframework.stereotype.Component; + +/** + * This class will contain methods that can define in what way DSpaceObjectRest objects are equal + */ +@Component +public class DSpaceObjectRestEqualityUtils { + + /** + * This method will return a boolean indicating whether the given DSpaceObjectRest objects are equal + * through comparing their attributes + * @param original The original DSpaceObjectRest object + * @param updated The DSpaceObjectRest object that has to be checked for equality + * @return A boolean indicating whether they're equal or not + */ + public boolean isDSpaceObjectEqualsWithoutMetadata(DSpaceObjectRest original, DSpaceObjectRest updated) { + return StringUtils.equals(original.getId(), updated.getId()) && + StringUtils.equals(original.getCategory(), updated.getCategory()) && + StringUtils.equals(original.getHandle(), updated.getHandle()); + + } +} diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java index 49cc5f2bdd..c581b08ebc 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/BitstreamRestRepositoryIT.java @@ -27,11 +27,13 @@ import org.dspace.app.rest.builder.ItemBuilder; import org.dspace.app.rest.matcher.BitstreamFormatMatcher; import org.dspace.app.rest.matcher.BitstreamMatcher; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.test.MetadataPatchSuite; import org.dspace.content.Bitstream; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.content.service.BitstreamService; +import org.dspace.eperson.EPerson; import org.hamcrest.Matchers; import org.junit.Ignore; import org.junit.Test; @@ -587,4 +589,25 @@ public class BitstreamRestRepositoryIT extends AbstractControllerIntegrationTest getClient(token).perform(delete("/api/core/bitstreams/" + col.getLogo().getID())) .andExpect(status().is(422)); } + + @Test + public void patchBitstreamMetadataAuthorized() throws Exception { + runPatchMetadataTests(admin, 200); + } + + @Test + public void patchBitstreamMetadataUnauthorized() throws Exception { + runPatchMetadataTests(eperson, 403); + } + + private void runPatchMetadataTests(EPerson asUser, int expectedStatus) throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Community").withLogo("logo_community") + .build(); + context.restoreAuthSystemState(); + String token = getAuthToken(asUser.getEmail(), password); + + new MetadataPatchSuite().runWith(getClient(token), "/api/core/bitstreams/" + + parentCommunity.getLogo().getID(), expectedStatus); + } } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java index beacc1b838..980604a8ae 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/BrowsesResourceControllerIT.java @@ -7,10 +7,10 @@ */ package org.dspace.app.rest; +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -31,6 +31,7 @@ import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.eperson.Group; +import org.hamcrest.Matchers; import org.junit.Test; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; @@ -380,14 +381,10 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe "An embargoed publication", "2017-08-10")))) - //The private item must not be present - .andExpect(jsonPath("$._embedded.items[*].metadata[?(@.key=='dc.title')].value", - not(hasItem("This is a private item")))) - - //The internal item must not be present - .andExpect(jsonPath("$._embedded.items[*].metadata[?(@.key=='dc.title')].value", - not(hasItem("Internal publication")))) - ; + //The private and internal items must not be present + .andExpect(jsonPath("$._embedded.items[*].metadata", Matchers.allOf( + not(matchMetadata("dc.title", "This is a private item")), + not(matchMetadata("dc.title", "Internal publication"))))); } @Test @@ -883,4 +880,4 @@ public class BrowsesResourceControllerIT extends AbstractControllerIntegrationTe ))); } -} \ No newline at end of file +} diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java index e9de5c69d6..0a85afaab8 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/CollectionRestRepositoryIT.java @@ -7,25 +7,48 @@ */ package org.dspace.app.rest; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 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.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.UUID; +import com.fasterxml.jackson.databind.ObjectMapper; import org.dspace.app.rest.builder.CollectionBuilder; import org.dspace.app.rest.builder.CommunityBuilder; +import org.dspace.app.rest.converter.CollectionConverter; import org.dspace.app.rest.matcher.CollectionMatcher; +import org.dspace.app.rest.matcher.MetadataMatcher; +import org.dspace.app.rest.model.CollectionRest; +import org.dspace.app.rest.model.MetadataRest; +import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Collection; import org.dspace.content.Community; +import org.dspace.core.Constants; +import org.dspace.eperson.EPerson; import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; public class CollectionRestRepositoryIT extends AbstractControllerIntegrationTest { + @Autowired + CollectionConverter collectionConverter; + + @Autowired + AuthorizeService authorizeService; @Test public void findAllTest() throws Exception { @@ -276,4 +299,351 @@ public class CollectionRestRepositoryIT extends AbstractControllerIntegrationTes .andExpect(status().isNotFound()); } + @Test + public void findCollectionWithParentCommunity() throws Exception { + + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Community child2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community Two") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + Collection col2 = CollectionBuilder.createCollection(context, child2).withName("Collection 2").build(); + + getClient().perform(get("/api/core/collections/" + col1.getID())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", is( + CollectionMatcher.matchCollectionEntry(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$", Matchers.not( + is( + CollectionMatcher.matchCollectionEntry(col2.getName(), col2.getID(), col2.getHandle()) + )))); + } + + @Test + public void updateTest() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + getClient().perform(get("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CollectionMatcher.matchCollectionEntry(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/collections"))) + ; + + String token = getAuthToken(admin.getEmail(), password); + + ObjectMapper mapper = new ObjectMapper(); + + CollectionRest collectionRest = collectionConverter.fromModel(col1); + + collectionRest.setMetadata(new MetadataRest() + .put("dc.title", new MetadataValueRest("Electronic theses and dissertations"))); + + getClient(token).perform(put("/api/core/collections/" + col1.getID().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(collectionRest))) + .andExpect(status().isOk()) + ; + + getClient().perform(get("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CollectionMatcher.matchCollectionEntry("Electronic theses and dissertations", + col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/core/collections"))) + ; + } + + @Test + public void deleteTest() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .withLogo("ThisIsSomeDummyText") + .build(); + + Community parentCommunity2 = CommunityBuilder.createCommunity(context) + .withName("Parent Community 2") + .withLogo("SomeTest") + .build(); + + Community parentCommunityChild1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunityChild1) + .withName("Collection 1") + .build(); + + String token = getAuthToken(admin.getEmail(), password); + + getClient(token).perform(get("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CollectionMatcher.matchCollectionEntry(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/core/collections"))) ; + getClient(token).perform(delete("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isNoContent()) + ; + getClient(token).perform(get("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isNotFound()) + ; + } + + @Test + public void deleteTestUnAuthorized() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .withLogo("ThisIsSomeDummyText") + .build(); + + Community parentCommunity2 = CommunityBuilder.createCommunity(context) + .withName("Parent Community 2") + .withLogo("SomeTest") + .build(); + + Community parentCommunityChild1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunityChild1) + .withName("Collection 1") + .build(); + + getClient().perform(get("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CollectionMatcher.matchCollectionEntry(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/core/collections"))) ; + getClient().perform(delete("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isUnauthorized()) + ; + } + + @Test + public void createTest() throws Exception { + context.turnOffAuthorisationSystem(); + + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .withLogo("ThisIsSomeDummyText") + .build(); + + ObjectMapper mapper = new ObjectMapper(); + CollectionRest collectionRest = new CollectionRest(); + // We send a name but the created collection should set this to the title + collectionRest.setName("Collection"); + + collectionRest.setMetadata(new MetadataRest() + .put("dc.description", + new MetadataValueRest("

Some cool HTML code here

")) + .put("dc.description.abstract", + new MetadataValueRest("Sample top-level community created via the REST API")) + .put("dc.description.tableofcontents", + new MetadataValueRest("

HTML News

")) + .put("dc.rights", + new MetadataValueRest("Custom Copyright Text")) + .put("dc.title", + new MetadataValueRest("Title Text"))); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(post("/api/core/collections") + .content(mapper.writeValueAsBytes(collectionRest)) + .param("parent", parentCommunity.getID().toString()) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", not(empty())), + hasJsonPath("$.uuid", not(empty())), + hasJsonPath("$.name", is("Title Text")), + hasJsonPath("$.handle", not(empty())), + hasJsonPath("$.type", is("collection")), + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("dc.description", + "

Some cool HTML code here

"), + MetadataMatcher.matchMetadata("dc.description.abstract", + "Sample top-level community created via the REST API"), + MetadataMatcher.matchMetadata("dc.description.tableofcontents", + "

HTML News

"), + MetadataMatcher.matchMetadata("dc.rights", + "Custom Copyright Text"), + MetadataMatcher.matchMetadata("dc.title", + "Title Text") + ))))); + + } + + + @Test + public void deleteCollectionEpersonWithDeleteRightsTest() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .withLogo("ThisIsSomeDummyText") + .build(); + + Community parentCommunity2 = CommunityBuilder.createCommunity(context) + .withName("Parent Community 2") + .withLogo("SomeTest") + .build(); + + Community parentCommunityChild1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunityChild1) + .withName("Collection 1") + .build(); + + + context.setCurrentUser(eperson); + authorizeService.addPolicy(context, col1, Constants.DELETE, eperson); + authorizeService.addPolicy(context, col1, Constants.WRITE, eperson); + + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token).perform(get("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CollectionMatcher.matchCollectionEntry(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/core/collections"))) ; + getClient(token).perform(delete("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isNoContent()) + ; + getClient(token).perform(get("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isNotFound()) + ; + + authorizeService.removePoliciesActionFilter(context, eperson, Constants.DELETE); + authorizeService.removePoliciesActionFilter(context, eperson, Constants.WRITE); + + } + + @Test + public void updateCollectionEpersonWithWriteRightsTest() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + getClient().perform(get("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CollectionMatcher.matchCollectionEntry(col1.getName(), col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/collections"))) + ; + + + context.setCurrentUser(eperson); + authorizeService.addPolicy(context, col1, Constants.WRITE, eperson); + + String token = getAuthToken(eperson.getEmail(), password); + ObjectMapper mapper = new ObjectMapper(); + + CollectionRest collectionRest = collectionConverter.fromModel(col1); + + collectionRest.setMetadata(new MetadataRest() + .put("dc.title", new MetadataValueRest("Electronic theses and dissertations"))); + + getClient(token).perform(put("/api/core/collections/" + col1.getID().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(collectionRest))) + .andExpect(status().isOk()) + ; + + getClient().perform(get("/api/core/collections/" + col1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CollectionMatcher.matchCollectionEntry("Electronic theses and dissertations", + col1.getID(), col1.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/core/collections"))) + ; + + authorizeService.removePoliciesActionFilter(context, eperson, Constants.WRITE); + } + + public void patchCollectionMetadataAuthorized() throws Exception { + runPatchMetadataTests(admin, 200); + } + + @Test + public void patchCollectionMetadataUnauthorized() throws Exception { + runPatchMetadataTests(eperson, 403); + } + + private void runPatchMetadataTests(EPerson asUser, int expectedStatus) throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Community").build(); + Collection col = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection").build(); + context.restoreAuthSystemState(); + String token = getAuthToken(asUser.getEmail(), password); + + new MetadataPatchSuite().runWith(getClient(token), "/api/core/collections/" + col.getID(), expectedStatus); + } } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java index d1f77fcdd1..3b3c12c720 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/CommunityRestRepositoryIT.java @@ -8,34 +8,50 @@ package org.dspace.app.rest; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 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.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import java.util.Arrays; import java.util.UUID; import com.fasterxml.jackson.databind.ObjectMapper; import org.dspace.app.rest.builder.CollectionBuilder; import org.dspace.app.rest.builder.CommunityBuilder; +import org.dspace.app.rest.converter.CommunityConverter; import org.dspace.app.rest.matcher.CommunityMatcher; -import org.dspace.app.rest.matcher.CommunityMetadataMatcher; +import org.dspace.app.rest.matcher.MetadataMatcher; import org.dspace.app.rest.model.CommunityRest; -import org.dspace.app.rest.model.MetadataEntryRest; +import org.dspace.app.rest.model.MetadataRest; +import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Collection; import org.dspace.content.Community; +import org.dspace.core.Constants; +import org.dspace.eperson.EPerson; import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; public class CommunityRestRepositoryIT extends AbstractControllerIntegrationTest { + @Autowired + CommunityConverter communityConverter; + + @Autowired + AuthorizeService authorizeService; + @Test public void createTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -45,31 +61,29 @@ public class CommunityRestRepositoryIT extends AbstractControllerIntegrationTest // We send a name but the created community should set this to the title comm.setName("Test Top-Level Community"); - MetadataEntryRest description = new MetadataEntryRest(); - description.setKey("dc.description"); + MetadataRest metadataRest = new MetadataRest(); + + MetadataValueRest description = new MetadataValueRest(); description.setValue("

Some cool HTML code here

"); + metadataRest.put("dc.description", description); - MetadataEntryRest abs = new MetadataEntryRest(); - abs.setKey("dc.description.abstract"); + MetadataValueRest abs = new MetadataValueRest(); abs.setValue("Sample top-level community created via the REST API"); + metadataRest.put("dc.description.abstract", abs); - MetadataEntryRest contents = new MetadataEntryRest(); - contents.setKey("dc.description.tableofcontents"); + MetadataValueRest contents = new MetadataValueRest(); contents.setValue("

HTML News

"); + metadataRest.put("dc.description.tableofcontents", contents); - MetadataEntryRest copyright = new MetadataEntryRest(); - copyright.setKey("dc.rights"); + MetadataValueRest copyright = new MetadataValueRest(); copyright.setValue("Custom Copyright Text"); + metadataRest.put("dc.rights", copyright); - MetadataEntryRest title = new MetadataEntryRest(); - title.setKey("dc.title"); + MetadataValueRest title = new MetadataValueRest(); title.setValue("Title Text"); + metadataRest.put("dc.title", title); - comm.setMetadata(Arrays.asList(description, - abs, - contents, - copyright, - title)); + comm.setMetadata(metadataRest); String authToken = getAuthToken(admin.getEmail(), password); getClient(authToken).perform(post("/api/core/communities") @@ -87,21 +101,79 @@ public class CommunityRestRepositoryIT extends AbstractControllerIntegrationTest hasJsonPath("$._links.logo.href", not(empty())), hasJsonPath("$._links.subcommunities.href", not(empty())), hasJsonPath("$._links.self.href", not(empty())), - hasJsonPath("$.metadata", Matchers.containsInAnyOrder( - CommunityMetadataMatcher.matchMetadata("dc.description", - "

Some cool HTML code here

"), - CommunityMetadataMatcher.matchMetadata("dc.description.abstract", - "Sample top-level community created via the REST API"), - CommunityMetadataMatcher.matchMetadata("dc.description.tableofcontents", - "

HTML News

"), - CommunityMetadataMatcher.matchMetadata("dc.rights", - "Custom Copyright Text"), - CommunityMetadataMatcher.matchMetadata("dc.title", - "Title Text") + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("dc.description", "

Some cool HTML code here

"), + matchMetadata("dc.description.abstract", + "Sample top-level community created via the REST API"), + matchMetadata("dc.description.tableofcontents", "

HTML News

"), + matchMetadata("dc.rights", "Custom Copyright Text"), + matchMetadata("dc.title", "Title Text") ))))); + } + @Test + public void createWithParentTest() throws Exception { + context.turnOffAuthorisationSystem(); + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + ObjectMapper mapper = new ObjectMapper(); + CommunityRest comm = new CommunityRest(); + // We send a name but the created community should set this to the title + comm.setName("Test Sub-Level Community"); + + comm.setMetadata(new MetadataRest() + .put("dc.description", + new MetadataValueRest("

Some cool HTML code here

")) + .put("dc.description.abstract", + new MetadataValueRest("Sample top-level community created via the REST API")) + .put("dc.description.tableofcontents", + new MetadataValueRest("

HTML News

")) + .put("dc.rights", + new MetadataValueRest("Custom Copyright Text")) + .put("dc.title", + new MetadataValueRest("Title Text"))); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(post("/api/core/communities") + .content(mapper.writeValueAsBytes(comm)) + .param("parent", parentCommunity.getID().toString()) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", not(empty())), + hasJsonPath("$.uuid", not(empty())), + hasJsonPath("$.name", is("Title Text")), + hasJsonPath("$.handle", not(empty())), + hasJsonPath("$.type", is("community")), + hasJsonPath("$._links.collections.href", not(empty())), + hasJsonPath("$._links.logo.href", not(empty())), + hasJsonPath("$._links.subcommunities.href", not(empty())), + hasJsonPath("$._links.self.href", not(empty())), + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("dc.description", + "

Some cool HTML code here

"), + MetadataMatcher.matchMetadata("dc.description.abstract", + "Sample top-level community created via the REST API"), + MetadataMatcher.matchMetadata("dc.description.tableofcontents", + "

HTML News

"), + MetadataMatcher.matchMetadata("dc.rights", + "Custom Copyright Text"), + MetadataMatcher.matchMetadata("dc.title", + "Title Text") + ))))); + + } + @Test public void createUnauthorizedTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -110,11 +182,13 @@ public class CommunityRestRepositoryIT extends AbstractControllerIntegrationTest CommunityRest comm = new CommunityRest(); comm.setName("Test Top-Level Community"); - MetadataEntryRest title = new MetadataEntryRest(); - title.setKey("dc.title"); - title.setValue("Title Text"); + MetadataRest metadataRest = new MetadataRest(); - comm.setMetadata(Arrays.asList(title)); + MetadataValueRest title = new MetadataValueRest(); + title.setValue("Title Text"); + metadataRest.put("dc.title", title); + + comm.setMetadata(metadataRest); // Anonymous user tries to create a community. // Should fail because user is not authenticated. Error 401. @@ -511,4 +585,291 @@ public class CommunityRestRepositoryIT extends AbstractControllerIntegrationTest getClient().perform(get("/api/core/communities/" + UUID.randomUUID())).andExpect(status().isNotFound()); } + + @Test + public void updateTest() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + getClient().perform(get("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CommunityMatcher.matchCommunityEntry(parentCommunity.getName(), parentCommunity.getID(), + parentCommunity.getHandle()) + ))) + .andExpect(jsonPath("$", Matchers.not( + Matchers.is( + CommunityMatcher.matchCommunityEntry(child1.getName(), child1.getID(), child1.getHandle()) + ) + ))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/communities"))) + ; + + String token = getAuthToken(admin.getEmail(), password); + + ObjectMapper mapper = new ObjectMapper(); + + CommunityRest communityRest = communityConverter.fromModel(parentCommunity); + + communityRest.setMetadata(new MetadataRest() + .put("dc.title", new MetadataValueRest("Electronic theses and dissertations"))); + + getClient(token).perform(put("/api/core/communities/" + parentCommunity.getID().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(communityRest))) + .andExpect(status().isOk()) + ; + + getClient().perform(get("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CommunityMatcher.matchCommunityEntry("Electronic theses and dissertations", + parentCommunity.getID(), + parentCommunity.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/communities"))) + ; + } + + @Test + public void deleteTest() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .withLogo("ThisIsSomeDummyText") + .build(); + + Community parentCommunity2 = CommunityBuilder.createCommunity(context) + .withName("Parent Community 2") + .withLogo("SomeTest") + .build(); + + Community parentCommunityChild1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Community parentCommunityChild2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community2") + .build(); + + Community parentCommunityChild2Child1 = CommunityBuilder.createSubCommunity(context, parentCommunityChild2) + .withName("Sub Sub Community") + .build(); + + + Community parentCommunity2Child1 = CommunityBuilder.createSubCommunity(context, parentCommunity2) + .withName("Sub2 Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunityChild1) + .withName("Collection 1") + .build(); + + String token = getAuthToken(admin.getEmail(), password); + + getClient(token).perform(get("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CommunityMatcher.matchCommunityEntry(parentCommunity.getName(), parentCommunity.getID(), + parentCommunity.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/core/communities"))) ; + getClient(token).perform(delete("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isNoContent()) + ; + getClient(token).perform(get("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isNotFound()) + ; + + getClient(token).perform(get("/api/core/communities/" + parentCommunityChild1.getID().toString())) + .andExpect(status().isNotFound()) + ; + } + + @Test + public void deleteTestUnAuthorized() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .withLogo("ThisIsSomeDummyText") + .build(); + + Community parentCommunity2 = CommunityBuilder.createCommunity(context) + .withName("Parent Community 2") + .withLogo("SomeTest") + .build(); + + Community parentCommunityChild1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Community parentCommunityChild2 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community2") + .build(); + + Community parentCommunityChild2Child1 = CommunityBuilder.createSubCommunity(context, parentCommunityChild2) + .withName("Sub Sub Community") + .build(); + + + Community parentCommunity2Child1 = CommunityBuilder.createSubCommunity(context, parentCommunity2) + .withName("Sub2 Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunityChild1) + .withName("Collection 1") + .build(); + + + getClient().perform(get("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CommunityMatcher.matchCommunityEntry(parentCommunity.getName(), parentCommunity.getID(), + parentCommunity.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/core/communities"))) ; + getClient().perform(delete("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isUnauthorized()) + ; + } + + @Test + public void deleteCommunityEpersonWithDeleteRightsTest() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + context.setCurrentUser(eperson); + authorizeService.addPolicy(context, parentCommunity, Constants.DELETE, eperson); + + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token).perform(get("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CommunityMatcher.matchCommunityEntry(parentCommunity.getName(), parentCommunity.getID(), + parentCommunity.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/core/communities"))) ; + getClient(token).perform(delete("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isNoContent()) + ; + getClient(token).perform(get("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isNotFound()) + ; + + authorizeService.removePoliciesActionFilter(context, eperson, Constants.DELETE); + } + + @Test + public void updateCommunityEpersonWithWriteRightsTest() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + getClient().perform(get("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CommunityMatcher.matchCommunityEntry(parentCommunity.getName(), parentCommunity.getID(), + parentCommunity.getHandle()) + ))) + .andExpect(jsonPath("$", Matchers.not( + Matchers.is( + CommunityMatcher.matchCommunityEntry(child1.getName(), child1.getID(), child1.getHandle()) + ) + ))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/communities"))) + ; + + ObjectMapper mapper = new ObjectMapper(); + + CommunityRest communityRest = communityConverter.fromModel(parentCommunity); + + communityRest.setMetadata(new MetadataRest() + .put("dc.title", new MetadataValueRest("Electronic theses and dissertations"))); + + context.setCurrentUser(eperson); + authorizeService.addPolicy(context, parentCommunity, Constants.WRITE, eperson); + + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token).perform(put("/api/core/communities/" + parentCommunity.getID().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsBytes(communityRest))) + .andExpect(status().isOk()) + ; + + getClient().perform(get("/api/core/communities/" + parentCommunity.getID().toString())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + CommunityMatcher.matchCommunityEntry("Electronic theses and dissertations", + parentCommunity.getID(), + parentCommunity.getHandle()) + ))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/communities"))) + ; + + authorizeService.removePoliciesActionFilter(context, eperson, Constants.DELETE); + + } + + public void patchCommunityMetadataAuthorized() throws Exception { + runPatchMetadataTests(admin, 200); + } + + @Test + public void patchCommunityMetadataUnauthorized() throws Exception { + runPatchMetadataTests(eperson, 403); + } + + private void runPatchMetadataTests(EPerson asUser, int expectedStatus) throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Community").build(); + context.restoreAuthSystemState(); + String token = getAuthToken(asUser.getEmail(), password); + + new MetadataPatchSuite().runWith(getClient(token), "/api/core/communities/" + + parentCommunity.getID(), expectedStatus); + } } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java index ff8043b14b..0f40159801 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java @@ -8,6 +8,7 @@ package org.dspace.app.rest; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -21,7 +22,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.UUID; import javax.ws.rs.core.MediaType; @@ -32,12 +32,13 @@ import org.dspace.app.rest.builder.CommunityBuilder; import org.dspace.app.rest.builder.EPersonBuilder; import org.dspace.app.rest.builder.ItemBuilder; import org.dspace.app.rest.matcher.EPersonMatcher; -import org.dspace.app.rest.matcher.EPersonMetadataMatcher; import org.dspace.app.rest.model.EPersonRest; -import org.dspace.app.rest.model.MetadataEntryRest; +import org.dspace.app.rest.model.MetadataRest; +import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.patch.Operation; 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.content.Collection; import org.dspace.content.Item; import org.dspace.eperson.EPerson; @@ -54,15 +55,16 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { // we should check how to get it from Spring ObjectMapper mapper = new ObjectMapper(); EPersonRest data = new EPersonRest(); + MetadataRest metadataRest = new MetadataRest(); data.setEmail("createtest@fake-email.com"); data.setCanLogIn(true); - MetadataEntryRest surname = new MetadataEntryRest(); - surname.setKey("eperson.lastname"); + MetadataValueRest surname = new MetadataValueRest(); surname.setValue("Doe"); - MetadataEntryRest firstname = new MetadataEntryRest(); - firstname.setKey("eperson.firstname"); + metadataRest.put("eperson.lastname", surname); + MetadataValueRest firstname = new MetadataValueRest(); firstname.setValue("John"); - data.setMetadata(Arrays.asList(surname, firstname)); + metadataRest.put("eperson.firstname", firstname); + data.setMetadata(metadataRest); String authToken = getAuthToken(admin.getEmail(), password); getClient(authToken).perform(post("/api/eperson/epersons") @@ -78,9 +80,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { hasJsonPath("$.canLogIn", is(true)), hasJsonPath("$.requireCertificate", is(false)), hasJsonPath("$._links.self.href", not(empty())), - hasJsonPath("$.metadata", Matchers.containsInAnyOrder( - EPersonMetadataMatcher.matchFirstName("John"), - EPersonMetadataMatcher.matchLastName("Doe") + hasJsonPath("$.metadata", Matchers.allOf( + matchMetadata("eperson.firstname", "John"), + matchMetadata("eperson.lastname", "Doe") ))))); // TODO cleanup the context!!! } @@ -1030,4 +1032,23 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest { } + @Test + public void patchEPersonMetadataAuthorized() throws Exception { + runPatchMetadataTests(admin, 200); + } + + @Test + public void patchEPersonMetadataUnauthorized() throws Exception { + runPatchMetadataTests(eperson, 403); + } + + private void runPatchMetadataTests(EPerson asUser, int expectedStatus) throws Exception { + context.turnOffAuthorisationSystem(); + EPerson ePerson = EPersonBuilder.createEPerson(context).withEmail("user@test.com").build(); + context.restoreAuthSystemState(); + String token = getAuthToken(asUser.getEmail(), password); + + new MetadataPatchSuite().runWith(getClient(token), "/api/eperson/epersons/" + ePerson.getID(), expectedStatus); + } + } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/GroupRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/GroupRestRepositoryIT.java index b75c9cf7bb..259961c12d 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/GroupRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/GroupRestRepositoryIT.java @@ -22,6 +22,8 @@ import org.dspace.app.rest.builder.GroupBuilder; import org.dspace.app.rest.matcher.GroupMatcher; import org.dspace.app.rest.model.GroupRest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.test.MetadataPatchSuite; +import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.hamcrest.Matchers; import org.junit.Test; @@ -234,4 +236,22 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest { ; } + @Test + public void patchGroupMetadataAuthorized() throws Exception { + runPatchMetadataTests(admin, 200); + } + + @Test + public void patchGroupMetadataUnauthorized() throws Exception { + runPatchMetadataTests(eperson, 403); + } + + private void runPatchMetadataTests(EPerson asUser, int expectedStatus) throws Exception { + context.turnOffAuthorisationSystem(); + Group group = GroupBuilder.createGroup(context).withName("Group").build(); + context.restoreAuthSystemState(); + String token = getAuthToken(asUser.getEmail(), password); + + new MetadataPatchSuite().runWith(getClient(token), "/api/eperson/groups/" + group.getID(), expectedStatus); + } } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java index 55de139f29..09e4cd1f0f 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java @@ -7,10 +7,13 @@ */ package org.dspace.app.rest; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 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.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -18,9 +21,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.io.InputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; import javax.ws.rs.core.MediaType; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharEncoding; import org.dspace.app.rest.builder.BitstreamBuilder; @@ -31,9 +36,14 @@ import org.dspace.app.rest.builder.GroupBuilder; import org.dspace.app.rest.builder.ItemBuilder; import org.dspace.app.rest.builder.WorkspaceItemBuilder; import org.dspace.app.rest.matcher.ItemMatcher; +import org.dspace.app.rest.matcher.MetadataMatcher; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.model.MetadataRest; +import org.dspace.app.rest.model.MetadataValueRest; import org.dspace.app.rest.model.patch.Operation; 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.content.Bitstream; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -43,6 +53,7 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.test.web.servlet.MvcResult; public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest { @@ -909,7 +920,6 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest { } - @Test public void useStringForBooleanTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -1414,4 +1424,348 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest { Matchers.containsString("/api/core/items"))); } + @Test + public void testCreateItem() throws Exception { + + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + + ObjectMapper mapper = new ObjectMapper(); + ItemRest itemRest = new ItemRest(); + itemRest.setName("Practices of research data curation in institutional repositories:" + + " A qualitative view from repository staff"); + itemRest.setInArchive(true); + itemRest.setDiscoverable(true); + itemRest.setWithdrawn(false); + + itemRest.setMetadata(new MetadataRest() + .put("dc.description", new MetadataValueRest("

Some cool HTML code here

")) + .put("dc.description.abstract", new MetadataValueRest("Sample item created via the REST API")) + .put("dc.description.tableofcontents", new MetadataValueRest("

HTML News

")) + .put("dc.rights", new MetadataValueRest("Custom Copyright Text")) + .put("dc.title", new MetadataValueRest("Title Text"))); + + String token = getAuthToken(admin.getEmail(), password); + MvcResult mvcResult = getClient(token).perform(post("/api/core/items?owningCollection=" + + col1.getID().toString()) + .content(mapper.writeValueAsBytes(itemRest)).contentType(contentType)) + .andExpect(status().isCreated()) + .andReturn(); + + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + String itemUuidString = String.valueOf(map.get("uuid")); + String itemHandleString = String.valueOf(map.get("handle")); + + //TODO Refactor this to use the converter to Item instead of checking every property separately + getClient(token).perform(get("/api/core/items/" + itemUuidString)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", is(itemUuidString)), + hasJsonPath("$.uuid", is(itemUuidString)), + hasJsonPath("$.name", is("Title Text")), + hasJsonPath("$.handle", is(itemHandleString)), + hasJsonPath("$.type", is("item")), + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("dc.description", + "

Some cool HTML code here

"), + MetadataMatcher.matchMetadata("dc.description.abstract", + "Sample item created via the REST API"), + MetadataMatcher.matchMetadata("dc.description.tableofcontents", + "

HTML News

"), + MetadataMatcher.matchMetadata("dc.rights", + "Custom Copyright Text"), + MetadataMatcher.matchMetadata("dc.title", + "Title Text") + ))))); + } + + @Test + public void updateTest() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + ObjectMapper mapper = new ObjectMapper(); + ItemRest itemRest = new ItemRest(); + itemRest.setName("Practices of research data curation in institutional repositories:" + + " A qualitative view from repository staff"); + itemRest.setInArchive(true); + itemRest.setDiscoverable(true); + itemRest.setWithdrawn(false); + + + String token = getAuthToken(admin.getEmail(), password); + MvcResult mvcResult = getClient(token).perform(post("/api/core/items?owningCollection=" + + col1.getID().toString()) + .content(mapper.writeValueAsBytes(itemRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andReturn(); + + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + String itemUuidString = String.valueOf(map.get("uuid")); + String itemHandleString = String.valueOf(map.get("handle")); + + itemRest.setMetadata(new MetadataRest() + .put("dc.description", new MetadataValueRest("

Some cool HTML code here

")) + .put("dc.description.abstract", new MetadataValueRest("Sample item created via the REST API")) + .put("dc.description.tableofcontents", new MetadataValueRest("

HTML News

")) + .put("dc.rights", new MetadataValueRest("New Custom Copyright Text")) + .put("dc.title", new MetadataValueRest("New title"))); + + itemRest.setUuid(itemUuidString); + itemRest.setHandle(itemHandleString); + + mvcResult = getClient(token).perform(put("/api/core/items/" + itemUuidString) + .content(mapper.writeValueAsBytes(itemRest)) + .contentType(contentType)) + .andExpect(status().isOk()) + .andReturn(); + map = mapper.readValue(content, Map.class); + itemUuidString = String.valueOf(map.get("uuid")); + itemHandleString = String.valueOf(map.get("handle")); + + //TODO Refactor this to use the converter to Item instead of checking every property separately + getClient(token).perform(get("/api/core/items/" + itemUuidString)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", is(itemUuidString)), + hasJsonPath("$.uuid", is(itemUuidString)), + hasJsonPath("$.name", is("New title")), + hasJsonPath("$.handle", is(itemHandleString)), + hasJsonPath("$.type", is("item")), + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("dc.description", + "

Some cool HTML code here

"), + MetadataMatcher.matchMetadata("dc.description.abstract", + "Sample item created via the REST API"), + MetadataMatcher.matchMetadata("dc.description.tableofcontents", + "

HTML News

"), + MetadataMatcher.matchMetadata("dc.rights", + "New Custom Copyright Text"), + MetadataMatcher.matchMetadata("dc.title", + "New title") + ))))); + } + + + @Test + public void testDeleteItem() throws Exception { + + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + + ObjectMapper mapper = new ObjectMapper(); + ItemRest itemRest = new ItemRest(); + itemRest.setName("Practices of research data curation in institutional repositories:" + + " A qualitative view from repository staff"); + itemRest.setInArchive(true); + itemRest.setDiscoverable(true); + itemRest.setWithdrawn(false); + + itemRest.setMetadata(new MetadataRest() + .put("dc.description", new MetadataValueRest("

Some cool HTML code here

")) + .put("dc.description.abstract", new MetadataValueRest("Sample item created via the REST API")) + .put("dc.description.tableofcontents", new MetadataValueRest("

HTML News

")) + .put("dc.rights", new MetadataValueRest("Custom Copyright Text")) + .put("dc.title", new MetadataValueRest("Title Text"))); + + String token = getAuthToken(admin.getEmail(), password); + MvcResult mvcResult = getClient(token).perform(post("/api/core/items?owningCollection=" + + col1.getID().toString()) + .content(mapper.writeValueAsBytes(itemRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andReturn(); + + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + String itemUuidString = String.valueOf(map.get("uuid")); + String itemHandleString = String.valueOf(map.get("handle")); + + //TODO Refactor this to use the converter to Item instead of checking every property separately + getClient(token).perform(get("/api/core/items/" + itemUuidString)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", is(itemUuidString)), + hasJsonPath("$.uuid", is(itemUuidString)), + hasJsonPath("$.name", is("Title Text")), + hasJsonPath("$.handle", is(itemHandleString)), + hasJsonPath("$.type", is("item")), + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("dc.description", + "

Some cool HTML code here

"), + MetadataMatcher.matchMetadata("dc.description.abstract", + "Sample item created via the REST API"), + MetadataMatcher.matchMetadata("dc.description.tableofcontents", + "

HTML News

"), + MetadataMatcher.matchMetadata("dc.rights", + "Custom Copyright Text"), + MetadataMatcher.matchMetadata("dc.title", + "Title Text") + ))))); + + getClient(token).perform(delete("/api/core/items/" + itemUuidString)) + .andExpect(status().isNoContent()); + + getClient(token).perform(get("/api/core/items/" + itemUuidString)) + .andExpect(status().isNotFound()); + } + + @Test + public void testDeleteItemUnauthorized() throws Exception { + + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + + ObjectMapper mapper = new ObjectMapper(); + ItemRest itemRest = new ItemRest(); + itemRest.setName("Practices of research data curation in institutional repositories:" + + " A qualitative view from repository staff"); + itemRest.setInArchive(true); + itemRest.setDiscoverable(true); + itemRest.setWithdrawn(false); + + itemRest.setMetadata(new MetadataRest() + .put("dc.description", new MetadataValueRest("

Some cool HTML code here

")) + .put("dc.description.abstract", new MetadataValueRest("Sample item created via the REST API")) + .put("dc.description.tableofcontents", new MetadataValueRest("

HTML News

")) + .put("dc.rights", new MetadataValueRest("Custom Copyright Text")) + .put("dc.title", new MetadataValueRest("Title Text"))); + + String token = getAuthToken(admin.getEmail(), password); + MvcResult mvcResult = getClient(token).perform(post("/api/core/items?owningCollection=" + + col1.getID().toString()) + .content(mapper.writeValueAsBytes(itemRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andReturn(); + + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + String itemUuidString = String.valueOf(map.get("uuid")); + String itemHandleString = String.valueOf(map.get("handle")); + + //TODO Refactor this to use the converter to Item instead of checking every property separately + getClient(token).perform(get("/api/core/items/" + itemUuidString)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", is(itemUuidString)), + hasJsonPath("$.uuid", is(itemUuidString)), + hasJsonPath("$.name", is("Title Text")), + hasJsonPath("$.handle", is(itemHandleString)), + hasJsonPath("$.type", is("item")), + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("dc.description", + "

Some cool HTML code here

"), + MetadataMatcher.matchMetadata("dc.description.abstract", + "Sample item created via the REST API"), + MetadataMatcher.matchMetadata("dc.description.tableofcontents", + "

HTML News

"), + MetadataMatcher.matchMetadata("dc.rights", + "Custom Copyright Text"), + MetadataMatcher.matchMetadata("dc.title", + "Title Text") + ))))); + + getClient().perform(delete("/api/core/items/" + itemUuidString)) + .andExpect(status().isUnauthorized()); + + getClient(token).perform(get("/api/core/items/" + itemUuidString)) + .andExpect(status().isOk()); + } + + @Test + public void deleteOneWrongUuidResourceNotFoundTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community with one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder + .createCollection(context, parentCommunity).withName("Collection 1").build(); + + //2. One public item, one workspace item and one template item. + Item publicItem = ItemBuilder.createItem(context, col1) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .withSubject("ExtraEntry") + .build(); + + + String token = getAuthToken(admin.getEmail(), password); + + //Delete public item + getClient(token).perform(delete("/api/core/items/" + parentCommunity.getID())) + .andExpect(status().is(404)); + } + + public void patchItemMetadataAuthorized() throws Exception { + runPatchMetadataTests(admin, 200); + } + + @Test + public void patchItemMetadataUnauthorized() throws Exception { + runPatchMetadataTests(eperson, 403); + } + + private void runPatchMetadataTests(EPerson asUser, int expectedStatus) throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + Item item = ItemBuilder.createItem(context, col1).build(); + context.restoreAuthSystemState(); + String token = getAuthToken(asUser.getEmail(), password); + + new MetadataPatchSuite().runWith(getClient(token), "/api/core/items/" + item.getID(), expectedStatus); + } + } \ No newline at end of file diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/MetadataSchemaRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/MetadataSchemaRestRepositoryIT.java index 1d7b60a8d0..8ffcd4df67 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/MetadataSchemaRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/MetadataSchemaRestRepositoryIT.java @@ -7,50 +7,258 @@ */ package org.dspace.app.rest; +import static com.jayway.jsonpath.JsonPath.read; +import static org.dspace.app.rest.matcher.MetadataschemaMatcher.matchEntry; import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 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.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.concurrent.atomic.AtomicReference; + +import com.fasterxml.jackson.databind.ObjectMapper; import org.dspace.app.rest.builder.MetadataSchemaBuilder; +import org.dspace.app.rest.converter.MetadataSchemaConverter; import org.dspace.app.rest.matcher.MetadataschemaMatcher; +import org.dspace.app.rest.model.MetadataSchemaRest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.content.MetadataSchema; import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +/** + * Integration tests for the {@link org.dspace.app.rest.repository.MetadataSchemaRestRepository} + * This class will include all the tests for the logic with regards to the + * {@link org.dspace.app.rest.repository.MetadataSchemaRestRepository} + */ public class MetadataSchemaRestRepositoryIT extends AbstractControllerIntegrationTest { + private static final String TEST_NAME = "testSchemaName"; + private static final String TEST_NAMESPACE = "testSchemaNameSpace"; + + private static final String TEST_NAME_UPDATED = "testSchemaNameUpdated"; + private static final String TEST_NAMESPACE_UPDATED = "testSchemaNameSpaceUpdated"; + + @Autowired + MetadataSchemaConverter metadataSchemaConverter; + @Test public void findAll() throws Exception { context.turnOffAuthorisationSystem(); MetadataSchema metadataSchema = MetadataSchemaBuilder.createMetadataSchema(context, "ATest", "ANamespace") - .build(); + .build(); + context.restoreAuthSystemState(); getClient().perform(get("/api/core/metadataschemas")) - .andExpect(status().isOk()) - .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.metadataschemas", Matchers.hasItem( - MetadataschemaMatcher.matchEntry() - ))) - .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/metadataschemas"))) - .andExpect(jsonPath("$.page.size", is(20))); + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.metadataschemas", Matchers.hasItem( + matchEntry() + ))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/metadataschemas"))) + .andExpect(jsonPath("$.page.size", is(20))); } - @Test public void findOne() throws Exception { context.turnOffAuthorisationSystem(); + MetadataSchema metadataSchema = MetadataSchemaBuilder.createMetadataSchema(context, "ATest", "ANamespace") + .build(); + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/metadataschemas/" + metadataSchema.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is( + matchEntry(metadataSchema) + ))); + } + + @Test + public void createSuccess() throws Exception { + context.turnOffAuthorisationSystem(); + MetadataSchema metadataSchema = MetadataSchemaBuilder.createMetadataSchema(context, "ATest", "ANamespace") .build(); + context.restoreAuthSystemState(); + + MetadataSchemaRest metadataSchemaRest = metadataSchemaConverter.fromModel(metadataSchema); + metadataSchemaRest.setPrefix(TEST_NAME); + metadataSchemaRest.setNamespace(TEST_NAMESPACE); + + String authToken = getAuthToken(admin.getEmail(), password); + AtomicReference idRef = new AtomicReference<>(); + + + getClient(authToken) + .perform(post("/api/core/metadataschemas") + .content(new ObjectMapper().writeValueAsBytes(metadataSchemaRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + getClient().perform(get("/api/core/metadataschemas/" + idRef.get())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", MetadataschemaMatcher.matchEntry(TEST_NAME, TEST_NAMESPACE))); + } + + @Test + public void createUnauthorizedTest() + throws Exception { + MetadataSchemaRest metadataSchemaRest = new MetadataSchemaRest(); + metadataSchemaRest.setPrefix(TEST_NAME); + metadataSchemaRest.setNamespace(TEST_NAMESPACE); + + getClient() + .perform(post("/api/core/metadataschemas") + .content(new ObjectMapper().writeValueAsBytes(metadataSchemaRest)) + .contentType(contentType)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void deleteSuccess() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataSchema metadataSchema = MetadataSchemaBuilder.createMetadataSchema(context, "ATest", "A namespace") + .build(); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/metadataschemas/" + metadataSchema.getID())) + .andExpect(status().isOk()); + + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(delete("/api/core/metadataschemas/" + metadataSchema.getID())) + .andExpect(status().isNoContent()); + + getClient().perform(get("/api/core/metadataschemas/" + metadataSchema.getID())) + .andExpect(status().isNotFound()); + + } + + @Test + public void deleteUnauthorized() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataSchema metadataSchema = MetadataSchemaBuilder.createMetadataSchema(context, TEST_NAME, TEST_NAMESPACE) + .build(); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/metadataschemas/" + metadataSchema.getID())).andExpect(status().isOk()); + + getClient() + .perform(delete("/api/core/metadataschemas/" + metadataSchema.getID())) + .andExpect(status().isUnauthorized()); + + getClient().perform(get("/api/core/metadataschemas/" + metadataSchema.getID())).andExpect(status().isOk()); + + } + + @Test + public void deleteNonExisting() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataSchema metadataSchema = MetadataSchemaBuilder.createMetadataSchema(context, "A name", "A namespace") + .build(); + + context.restoreAuthSystemState(); + + Integer id = metadataSchema.getID(); + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(delete("/api/core/metadataschemas/" + id)) + .andExpect(status().isNoContent()); + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(delete("/api/core/metadataschemas/" + id)) + .andExpect(status().isNotFound()); + } + + @Test + public void update() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataSchema metadataSchema = MetadataSchemaBuilder.createMetadataSchema(context, TEST_NAME, TEST_NAMESPACE) + .build(); + + context.restoreAuthSystemState(); + + MetadataSchemaRest metadataSchemaRest = new MetadataSchemaRest(); + metadataSchemaRest.setId(metadataSchema.getID()); + metadataSchemaRest.setPrefix(TEST_NAME_UPDATED); + metadataSchemaRest.setNamespace(TEST_NAMESPACE_UPDATED); + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(put("/api/core/metadataschemas/" + metadataSchema.getID()) + .content(new ObjectMapper().writeValueAsBytes(metadataSchemaRest)) + .contentType(contentType)) + .andExpect(status().isOk()); getClient().perform(get("/api/core/metadataschemas/" + metadataSchema.getID())) .andExpect(status().isOk()) - .andExpect(jsonPath("$", is( - MetadataschemaMatcher.matchEntry(metadataSchema) - ))); + .andExpect(jsonPath("$", MetadataschemaMatcher + .matchEntry(TEST_NAME_UPDATED, TEST_NAMESPACE_UPDATED))); } + + @Test + public void updateUnauthorized() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataSchema metadataSchema = MetadataSchemaBuilder.createMetadataSchema(context, TEST_NAME, TEST_NAMESPACE) + .build(); + + context.restoreAuthSystemState(); + + MetadataSchemaRest metadataSchemaRest = new MetadataSchemaRest(); + metadataSchemaRest.setId(metadataSchema.getID()); + metadataSchemaRest.setPrefix(TEST_NAME_UPDATED); + metadataSchemaRest.setNamespace(TEST_NAMESPACE_UPDATED); + + getClient() + .perform(put("/api/core/metadataschemas/" + metadataSchema.getID()) + .content(new ObjectMapper().writeValueAsBytes(metadataSchemaRest)) + .contentType(contentType)) + .andExpect(status().isUnauthorized()); + + getClient().perform(get("/api/core/metadataschemas/" + metadataSchema.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", MetadataschemaMatcher + .matchEntry(TEST_NAME, TEST_NAMESPACE))); + } + + @Test + public void updateWrongRights() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataSchema metadataSchema = MetadataSchemaBuilder.createMetadataSchema(context, TEST_NAME, TEST_NAMESPACE) + .build(); + + context.restoreAuthSystemState(); + + MetadataSchemaRest metadataSchemaRest = new MetadataSchemaRest(); + metadataSchemaRest.setId(metadataSchema.getID()); + metadataSchemaRest.setPrefix(TEST_NAME_UPDATED); + metadataSchemaRest.setNamespace(TEST_NAMESPACE_UPDATED); + + getClient(getAuthToken(eperson.getEmail(), password)) + .perform(put("/api/core/metadataschemas/" + metadataSchema.getID()) + .content(new ObjectMapper().writeValueAsBytes(metadataSchemaRest)) + .contentType(contentType)) + .andExpect(status().isForbidden()); + + getClient().perform(get("/api/core/metadataschemas/" + metadataSchema.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", MetadataschemaMatcher + .matchEntry(TEST_NAME, TEST_NAMESPACE))); + } + } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/MetadatafieldRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/MetadatafieldRestRepositoryIT.java index 7217030ab3..49a5995be7 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/MetadatafieldRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/MetadatafieldRestRepositoryIT.java @@ -7,23 +7,63 @@ */ package org.dspace.app.rest; +import static com.jayway.jsonpath.JsonPath.read; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 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.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.concurrent.atomic.AtomicReference; + +import com.fasterxml.jackson.databind.ObjectMapper; import org.dspace.app.rest.builder.MetadataFieldBuilder; import org.dspace.app.rest.builder.MetadataSchemaBuilder; import org.dspace.app.rest.matcher.MetadataFieldMatcher; +import org.dspace.app.rest.model.MetadataFieldRest; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.content.MetadataField; +import org.dspace.content.MetadataFieldServiceImpl; import org.dspace.content.MetadataSchema; +import org.dspace.content.service.MetadataSchemaService; import org.hamcrest.Matchers; +import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +/** + * Integration tests for the {@link org.dspace.app.rest.repository.MetadataFieldRestRepository} + * This class will include all the tests for the logic with regards to the + * {@link org.dspace.app.rest.repository.MetadataFieldRestRepository} + */ public class MetadatafieldRestRepositoryIT extends AbstractControllerIntegrationTest { + private static final String ELEMENT = "test element"; + private static final String QUALIFIER = "test qualifier"; + private static final String SCOPE_NOTE = "test scope_note"; + + private static final String ELEMENT_UPDATED = "test element updated"; + private static final String QUALIFIER_UPDATED = "test qualifier updated"; + private static final String SCOPE_NOTE_UPDATED = "test scope_note updated"; + + private MetadataSchema metadataSchema; + + @Autowired + private MetadataSchemaService metadataSchemaService; + + @Autowired + private MetadataFieldServiceImpl metadataFieldService; + + @Before + public void setup() throws Exception { + metadataSchema = metadataSchemaService.findAll(context).get(0); + } @Test public void findAll() throws Exception { @@ -31,14 +71,15 @@ public class MetadatafieldRestRepositoryIT extends AbstractControllerIntegration context.turnOffAuthorisationSystem(); MetadataField metadataField = MetadataFieldBuilder .createMetadataField(context, "AnElement", "AQualifier", "AScopeNote").build(); + context.restoreAuthSystemState(); getClient().perform(get("/api/core/metadatafields") - .param("size", String.valueOf(100))) + .param("size", String.valueOf(100))) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$._embedded.metadatafields", Matchers.hasItems( - MetadataFieldMatcher.matchMetadataFieldByKeys("dc","title", null), - MetadataFieldMatcher.matchMetadataFieldByKeys("dc","date", "issued")) + MetadataFieldMatcher.matchMetadataFieldByKeys("dc", "title", null), + MetadataFieldMatcher.matchMetadataFieldByKeys("dc", "date", "issued")) )) .andExpect(jsonPath("$._links.first.href", Matchers.containsString("/api/core/metadatafields"))) .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/core/metadatafields"))) @@ -54,6 +95,7 @@ public class MetadatafieldRestRepositoryIT extends AbstractControllerIntegration context.turnOffAuthorisationSystem(); MetadataField metadataField = MetadataFieldBuilder .createMetadataField(context, "AnElement", "AQualifier", "AScopeNote").build(); + context.restoreAuthSystemState(); getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) .andExpect(status().isOk()) @@ -65,13 +107,14 @@ public class MetadatafieldRestRepositoryIT extends AbstractControllerIntegration @Test public void searchMethodsExist() throws Exception { + getClient().perform(get("/api/core/metadatafields")) - .andExpect(jsonPath("$._links.search.href", Matchers.notNullValue())); + .andExpect(jsonPath("$._links.search.href", notNullValue())); getClient().perform(get("/api/core/metadatafields/search")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._links.bySchema", Matchers.notNullValue())); + .andExpect(jsonPath("$._links.bySchema", notNullValue())); } @Test @@ -79,38 +122,39 @@ public class MetadatafieldRestRepositoryIT extends AbstractControllerIntegration context.turnOffAuthorisationSystem(); MetadataSchema schema = MetadataSchemaBuilder.createMetadataSchema(context, "ASchema", - "http://www.dspace.org/ns/aschema").build(); + "http://www.dspace.org/ns/aschema").build(); MetadataField metadataField = MetadataFieldBuilder .createMetadataField(context, schema, "AnElement", "AQualifier", "AScopeNote").build(); + context.restoreAuthSystemState(); getClient().perform(get("/api/core/metadatafields/search/bySchema") - .param("schema", "dc") - .param("size", String.valueOf(100))) + .param("schema", "dc") + .param("size", String.valueOf(100))) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$._embedded.metadatafields", Matchers.hasItems( - MetadataFieldMatcher.matchMetadataFieldByKeys("dc","title", null), - MetadataFieldMatcher.matchMetadataFieldByKeys("dc","date", "issued")) + MetadataFieldMatcher.matchMetadataFieldByKeys("dc", "title", null), + MetadataFieldMatcher.matchMetadataFieldByKeys("dc", "date", "issued")) )) .andExpect(jsonPath("$.page.size", is(100))); getClient().perform(get("/api/core/metadatafields/search/bySchema") - .param("schema", schema.getName())) - .andExpect(status().isOk()) - .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.metadatafields", Matchers.hasItem( - MetadataFieldMatcher.matchMetadataField(metadataField)) - )) - .andExpect(jsonPath("$.page.size", is(20))) - .andExpect(jsonPath("$.page.totalElements", is(1))); + .param("schema", schema.getName())) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.metadatafields", Matchers.hasItem( + MetadataFieldMatcher.matchMetadataField(metadataField)) + )) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); } @Test public void findByUndefinedSchema() throws Exception { getClient().perform(get("/api/core/metadatafields/search/bySchema") - .param("schema", "undefined")) + .param("schema", "undefined")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$.page.size", is(20))) @@ -124,4 +168,236 @@ public class MetadatafieldRestRepositoryIT extends AbstractControllerIntegration .andExpect(status().isUnprocessableEntity()); } + @Test + public void createSuccess() throws Exception { + + MetadataFieldRest metadataFieldRest = new MetadataFieldRest(); + metadataFieldRest.setElement("testElementForCreate"); + metadataFieldRest.setQualifier("testQualifierForCreate"); + metadataFieldRest.setScopeNote(SCOPE_NOTE); + + String authToken = getAuthToken(admin.getEmail(), password); + AtomicReference idRef = new AtomicReference<>(); + + assertThat(metadataFieldService.findByElement(context, metadataSchema, ELEMENT, QUALIFIER), nullValue()); + + getClient(authToken) + .perform(post("/api/core/metadatafields") + .param("schemaId", metadataSchema.getID() + "") + .content(new ObjectMapper().writeValueAsBytes(metadataFieldRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + getClient(authToken).perform(get("/api/core/metadatafields/" + idRef.get())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", MetadataFieldMatcher.matchMetadataFieldByKeys( + metadataSchema.getName(), "testElementForCreate", "testQualifierForCreate"))); + } + + @Test + public void createUnauthorized() throws Exception { + + MetadataFieldRest metadataFieldRest = new MetadataFieldRest(); + metadataFieldRest.setElement(ELEMENT); + metadataFieldRest.setQualifier(QUALIFIER); + metadataFieldRest.setScopeNote(SCOPE_NOTE); + + getClient() + .perform(post("/api/core/metadatafields") + .param("schemaId", metadataSchema.getID() + "") + .content(new ObjectMapper().writeValueAsBytes(metadataFieldRest)) + .contentType(contentType)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void createUnauthorizedEPersonNoAdminRights() throws Exception { + + MetadataFieldRest metadataFieldRest = new MetadataFieldRest(); + metadataFieldRest.setElement(ELEMENT); + metadataFieldRest.setQualifier(QUALIFIER); + metadataFieldRest.setScopeNote(SCOPE_NOTE); + + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token) + .perform(post("/api/core/metadatafields") + .param("schemaId", metadataSchema.getID() + "") + .content(new ObjectMapper().writeValueAsBytes(metadataFieldRest)) + .contentType(contentType)) + .andExpect(status().isForbidden()); + } + + @Test + public void deleteSuccess() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataField metadataField = MetadataFieldBuilder.createMetadataField(context, ELEMENT, QUALIFIER, SCOPE_NOTE) + .build(); + context.restoreAuthSystemState(); + + + getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isOk()); + getClient(getAuthToken(admin.getEmail(), password)) + .perform(delete("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isNoContent()); + + getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteUnauthorized() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataField metadataField = MetadataFieldBuilder.createMetadataField(context, ELEMENT, QUALIFIER, SCOPE_NOTE) + .build(); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isOk()); + getClient() + .perform(delete("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isUnauthorized()); + + getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isOk()); + } + + @Test + public void deleteUnauthorizedEPersonNoAdminRights() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataField metadataField = MetadataFieldBuilder.createMetadataField(context, ELEMENT, QUALIFIER, SCOPE_NOTE) + .build(); + + context.restoreAuthSystemState(); + + String token = getAuthToken(eperson.getEmail(), password); + + + getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isOk()); + getClient(token) + .perform(delete("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isForbidden()); + + getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isOk()); + } + + + @Test + public void deleteNonExisting() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataField metadataField = MetadataFieldBuilder.createMetadataField(context, ELEMENT, QUALIFIER, SCOPE_NOTE) + .build(); + + context.restoreAuthSystemState(); + + Integer id = metadataField.getID(); + getClient(getAuthToken(admin.getEmail(), password)) + .perform(delete("/api/core/metadatafields/" + id)) + .andExpect(status().isNoContent()); + + assertThat(metadataFieldService.find(context, id), nullValue()); + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(delete("/api/core/metadatafields/" + id)) + .andExpect(status().isNotFound()); + } + + @Test + public void update() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataField metadataField = MetadataFieldBuilder.createMetadataField(context, ELEMENT, QUALIFIER, SCOPE_NOTE) + .build(); + + context.restoreAuthSystemState(); + + MetadataFieldRest metadataFieldRest = new MetadataFieldRest(); + metadataFieldRest.setId(metadataField.getID()); + metadataFieldRest.setElement(ELEMENT_UPDATED); + metadataFieldRest.setQualifier(QUALIFIER_UPDATED); + metadataFieldRest.setScopeNote(SCOPE_NOTE_UPDATED); + + getClient(getAuthToken(admin.getEmail(), password)) + .perform(put("/api/core/metadatafields/" + metadataField.getID()) + .content(new ObjectMapper().writeValueAsBytes(metadataFieldRest)) + .contentType(contentType)) + .andExpect(status().isOk()); + + getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", MetadataFieldMatcher.matchMetadataFieldByKeys( + metadataSchema.getName(), ELEMENT_UPDATED, QUALIFIER_UPDATED) + )); + } + + @Test + public void updateUnauthorized() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataField metadataField = MetadataFieldBuilder.createMetadataField(context, ELEMENT, QUALIFIER, SCOPE_NOTE) + .build(); + + context.restoreAuthSystemState(); + + MetadataFieldRest metadataFieldRest = new MetadataFieldRest(); + metadataFieldRest.setId(metadataField.getID()); + metadataFieldRest.setElement(ELEMENT_UPDATED); + metadataFieldRest.setQualifier(QUALIFIER_UPDATED); + metadataFieldRest.setScopeNote(SCOPE_NOTE_UPDATED); + + getClient() + .perform(put("/api/core/metadatafields/" + metadataField.getID()) + .content(new ObjectMapper().writeValueAsBytes(metadataFieldRest)) + .contentType(contentType)) + .andExpect(status().isUnauthorized()); + + getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", MetadataFieldMatcher.matchMetadataFieldByKeys( + metadataSchema.getName(), ELEMENT, QUALIFIER) + )); + + + } + + @Test + public void updateWrongRights() throws Exception { + context.turnOffAuthorisationSystem(); + + MetadataField metadataField = MetadataFieldBuilder.createMetadataField(context, ELEMENT, QUALIFIER, SCOPE_NOTE) + .build(); + + context.restoreAuthSystemState(); + + MetadataFieldRest metadataFieldRest = new MetadataFieldRest(); + metadataFieldRest.setId(metadataField.getID()); + metadataFieldRest.setElement(ELEMENT_UPDATED); + metadataFieldRest.setQualifier(QUALIFIER_UPDATED); + metadataFieldRest.setScopeNote(SCOPE_NOTE_UPDATED); + + getClient(getAuthToken(eperson.getEmail(), password)) + .perform(put("/api/core/metadatafields/" + metadataField.getID()) + .content(new ObjectMapper().writeValueAsBytes(metadataFieldRest)) + .contentType(contentType)) + .andExpect(status().isForbidden()); + + getClient().perform(get("/api/core/metadatafields/" + metadataField.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", MetadataFieldMatcher.matchMetadataFieldByKeys( + metadataSchema.getName(), ELEMENT, QUALIFIER) + )); + + + } + + } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/SiteRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/SiteRestRepositoryIT.java index 33ada54c97..e3b1f23559 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/SiteRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/SiteRestRepositoryIT.java @@ -17,7 +17,9 @@ import java.util.UUID; import org.dspace.app.rest.builder.SiteBuilder; import org.dspace.app.rest.matcher.SiteMatcher; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.test.MetadataPatchSuite; import org.dspace.content.Site; +import org.dspace.eperson.EPerson; import org.hamcrest.Matchers; import org.junit.Test; @@ -67,4 +69,23 @@ public class SiteRestRepositoryIT extends AbstractControllerIntegrationTest { .andExpect(status().isNotFound()); } + + @Test + public void patchSiteMetadataAuthorized() throws Exception { + runPatchMetadataTests(admin, 200); + } + + @Test + public void patchSiteMetadataUnauthorized() throws Exception { + runPatchMetadataTests(eperson, 403); + } + + private void runPatchMetadataTests(EPerson asUser, int expectedStatus) throws Exception { + context.turnOffAuthorisationSystem(); + Site site = SiteBuilder.createSite(context).build(); + context.restoreAuthSystemState(); + String token = getAuthToken(asUser.getEmail(), password); + + new MetadataPatchSuite().runWith(getClient(token), "/api/core/sites/" + site.getID(), expectedStatus); + } } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index c1b5b6eef1..c0c2f59081 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -61,7 +61,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * All the workspaceitem should be returned regardless of the collection where they were created - * + * * @throws Exception */ public void findAllTest() throws Exception { @@ -114,7 +114,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * The workspaceitem endpoint must provide proper pagination - * + * * @throws Exception */ public void findAllWithPaginationTest() throws Exception { @@ -179,7 +179,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * The workspaceitem resource endpoint must expose the proper structure - * + * * @throws Exception */ public void findOneTest() throws Exception { @@ -212,7 +212,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * The workspaceitem resource endpoint must expose the proper structure - * + * * @throws Exception */ public void findOneRelsTest() throws Exception { @@ -239,7 +239,8 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration getClient().perform(get("/api/submission/workspaceitems/" + witem.getID() + "/collection")) .andExpect(status().isOk()) .andExpect(jsonPath("$", Matchers - .is(CollectionMatcher.matchCollectionEntry(col1.getName(), col1.getID(), col1.getHandle())))); + .is(CollectionMatcher.matchCollectionEntry(col1.getName(), col1.getID(), col1.getHandle())) + )); getClient().perform(get("/api/submission/workspaceitems/" + witem.getID() + "/item")).andExpect(status().isOk()) .andExpect(jsonPath("$", Matchers.is(ItemMatcher.matchItemWithTitleAndDateIssued(witem.getItem(), @@ -254,7 +255,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Check the response code for unexistent workspaceitem - * + * * @throws Exception */ public void findOneWrongUUIDTest() throws Exception { @@ -267,7 +268,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Removing a workspaceitem should result in delete of all the underline resources (item and bitstreams) - * + * * @throws Exception */ public void deleteOneTest() throws Exception { @@ -321,7 +322,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration /** * Create three workspaceitem with two different submitter and verify that the findBySubmitter return the proper * list of workspaceitem for each submitter also paginating - * + * * @throws Exception */ public void findBySubmitterTest() throws Exception { @@ -424,7 +425,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration /** * Test the creation of workspaceitem POSTing to the resource collection endpoint. It should respect the collection * param if present or use a default if it is not used - * + * * @throws Exception */ public void createEmptyWorkspateItemTest() throws Exception { @@ -469,7 +470,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Test the creation of workspaceitems POSTing to the resource collection endpoint a bibtex file - * + * * @throws Exception */ public void createMultipleWorkspaceItemFromFileTest() throws Exception { @@ -541,7 +542,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration /** * Test the creation of a workspaceitem POSTing to the resource collection endpoint a PDF file. As a single item * will be created we expect to have the pdf file stored as a bitstream - * + * * @throws Exception */ public void createWorkspaceItemFromPDFFileTest() throws Exception { @@ -588,7 +589,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration /** * Test the exposition of validation error for missing required metadata both at the creation time than on existent * workspaceitems - * + * * @throws Exception */ public void validationErrorsRequiredMetadataTest() throws Exception { @@ -650,7 +651,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Test the update of metadata - * + * * @throws Exception */ public void patchUpdateMetadataTest() throws Exception { @@ -707,7 +708,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Test delete of a metadata - * + * * @throws Exception */ public void patchDeleteMetadataTest() throws Exception { @@ -900,7 +901,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Test the addition of metadata - * + * * @throws Exception */ public void patchAddMetadataTest() throws Exception { @@ -959,7 +960,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Test the addition of metadata - * + * * @throws Exception */ public void patchAddMultipleMetadataValuesTest() throws Exception { @@ -1166,7 +1167,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Test the acceptance of the deposit license - * + * * @throws Exception */ public void patchAcceptLicenseTest() throws Exception { @@ -1328,7 +1329,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Test the reject of the deposit license - * + * * @throws Exception */ public void patchRejectLicenseTest() throws Exception { @@ -1495,7 +1496,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Test update of bitstream metadata in the upload section - * + * * @throws Exception */ public void patchUploadTest() throws Exception { @@ -1622,7 +1623,7 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Test /** * Test the upload of files in the upload over section - * + * * @throws Exception */ public void uploadTest() throws Exception { diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java index db9caa857c..9ff71928e5 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java @@ -8,8 +8,8 @@ package org.dspace.app.rest.matcher; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; @@ -30,9 +30,9 @@ public class BitstreamMatcher { hasJsonPath("$.uuid", is(bitstream.getID().toString())), hasJsonPath("$.name", is(bitstream.getName())), hasJsonPath("$.bundleName", is("ORIGINAL")), - hasJsonPath("$.metadata", containsInAnyOrder( - BitstreamMetadataMatcher.matchTitle(bitstream.getName()), - BitstreamMetadataMatcher.matchDescription(bitstream.getDescription()) + hasJsonPath("$.metadata", allOf( + matchMetadata("dc.title", bitstream.getName()), + matchMetadata("dc.description", bitstream.getDescription()) )), hasJsonPath("$.sizeBytes", is((int) bitstream.getSizeBytes())), hasJsonPath("$.checkSum", matchChecksum()), diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/BitstreamMetadataMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/BitstreamMetadataMatcher.java deleted file mode 100644 index 1fba985560..0000000000 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/BitstreamMetadataMatcher.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.app.rest.matcher; - -import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.is; - -import org.hamcrest.Matcher; - -public class BitstreamMetadataMatcher { - - private BitstreamMetadataMatcher() { } - - public static Matcher matchTitle(String title) { - return allOf( - hasJsonPath("$.key", is("dc.title")), - hasJsonPath("$.value", is(title)) - ); - } - - public static Matcher matchDescription(String description) { - return allOf( - hasJsonPath("$.key", is("dc.description")), - hasJsonPath("$.value", is(description)) - ); - } -} diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CollectionMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CollectionMatcher.java index 9986a1072a..8e8579c64c 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CollectionMatcher.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CollectionMatcher.java @@ -32,8 +32,8 @@ public class CollectionMatcher { hasJsonPath("$.name", is(name)), hasJsonPath("$.handle", is(handle)), hasJsonPath("$.type", is("collection")), - hasJsonPath("$.metadata", Matchers.contains( - CollectionMetadataMatcher.matchTitle(name) + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("dc.title", name) )), matchLinks(uuid), matchLogo(logo) diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CollectionMetadataMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CollectionMetadataMatcher.java deleted file mode 100644 index 3f5bbefe59..0000000000 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CollectionMetadataMatcher.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.app.rest.matcher; - -import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.allOf; - -import org.hamcrest.Matcher; - -public class CollectionMetadataMatcher { - - private CollectionMetadataMatcher() { } - - public static Matcher matchTitle(String title) { - return allOf( - hasJsonPath("$.key", is("dc.title")), - hasJsonPath("$.value", is(title)) - ); - } -} diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CommunityMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CommunityMatcher.java index 585d955c2f..f1fffaa5c3 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CommunityMatcher.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CommunityMatcher.java @@ -36,8 +36,8 @@ public class CommunityMatcher { hasJsonPath("$.name", is(name)), hasJsonPath("$.handle", is(handle)), hasJsonPath("$.type", is("community")), - hasJsonPath("$.metadata", Matchers.contains( - CommunityMetadataMatcher.matchMetadata("dc.title", name) + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("dc.title", name) )) ); } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CommunityMetadataMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CommunityMetadataMatcher.java deleted file mode 100644 index ec251e86ea..0000000000 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/CommunityMetadataMatcher.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.app.rest.matcher; - -import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.is; - -import org.hamcrest.Matcher; - -public class CommunityMetadataMatcher { - - private CommunityMetadataMatcher() { } - - public static Matcher matchMetadata(String key, String value) { - return allOf( - hasJsonPath("$.key", is(key)), - hasJsonPath("$.value", is(value)) - ); - } -} diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/EPersonMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/EPersonMatcher.java index 421af32dbe..819f1dccf6 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/EPersonMatcher.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/EPersonMatcher.java @@ -29,9 +29,9 @@ public class EPersonMatcher { hasJsonPath("$.type", is("eperson")), hasJsonPath("$.canLogIn", not(empty())), hasJsonPath("$._links.self.href", containsString("/api/eperson/epersons/" + ePerson.getID().toString())), - hasJsonPath("$.metadata", Matchers.hasItems( - EPersonMetadataMatcher.matchFirstName(ePerson.getFirstName()), - EPersonMetadataMatcher.matchLastName(ePerson.getLastName()) + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("eperson.firstname", ePerson.getFirstName()), + MetadataMatcher.matchMetadata("eperson.lastname", ePerson.getLastName()) )) ); } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/EPersonMetadataMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/EPersonMetadataMatcher.java deleted file mode 100644 index 74ff800632..0000000000 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/EPersonMetadataMatcher.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.app.rest.matcher; - -import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.is; - -import org.hamcrest.Matcher; - -public class EPersonMetadataMatcher { - - private EPersonMetadataMatcher() { } - - public static Matcher matchFirstName(String firstName) { - return allOf( - hasJsonPath("$.key", is("eperson.firstname")), - hasJsonPath("$.value", is(firstName)) - ); - } - - public static Matcher matchLastName(String lastName) { - return allOf( - hasJsonPath("$.key", is("eperson.lastname")), - hasJsonPath("$.value", is(lastName)) - ); - } - - public static Matcher matchLanguage(String language) { - return allOf( - hasJsonPath("$.key", is("eperson.language")), - hasJsonPath("$.value", is(language)) - ); - } -} diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/ItemMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/ItemMatcher.java index e5176eea26..0f6eb553f4 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/ItemMatcher.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/ItemMatcher.java @@ -8,9 +8,9 @@ package org.dspace.app.rest.matcher; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata; import static org.dspace.app.rest.test.AbstractControllerIntegrationTest.REST_SERVER_URL; import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; @@ -34,8 +34,9 @@ public class ItemMatcher { matchItemProperties(item), //Check core metadata (the JSON Path expression evaluates to a collection so we have to use contains) - hasJsonPath("$.metadata[?(@.key=='dc.title')].value", contains(title)), - hasJsonPath("$.metadata[?(@.key=='dc.date.issued')].value", contains(dateIssued)), + hasJsonPath("$.metadata", allOf( + matchMetadata("dc.title", title), + matchMetadata("dc.date.issued", dateIssued))), //Check links matchItemLinks(item) diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/MetadataMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/MetadataMatcher.java new file mode 100644 index 0000000000..ed821d1f60 --- /dev/null +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/MetadataMatcher.java @@ -0,0 +1,45 @@ +/** + * 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.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; + +import org.hamcrest.Matcher; + +/** + * Utility class to provide convenient matchers for metadata. + */ +public class MetadataMatcher { + + private MetadataMatcher() { } + + /** + * Gets a matcher to ensure a given value is present among all values for a given metadata key. + * + * @param key the metadata key. + * @param value the value that must be present. + * @return the matcher. + */ + public static Matcher matchMetadata(String key, String value) { + return hasJsonPath("$.['" + key + "'][*].value", contains(value)); + } + + /** + * Gets a matcher to ensure a given value is present at a specific position in the list of values for a given key. + * + * @param key the metadata key. + * @param value the value that must be present. + * @param position the position it must be present at. + * @return the matcher. + */ + public static Matcher matchMetadata(String key, String value, int position) { + return hasJsonPath("$.['" + key + "'][" + position + "].value", is(value)); + } +} diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/MetadataschemaMatcher.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/MetadataschemaMatcher.java index 18e8a5ad8d..42b70810a5 100644 --- a/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/MetadataschemaMatcher.java +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/matcher/MetadataschemaMatcher.java @@ -29,11 +29,15 @@ public class MetadataschemaMatcher { } public static Matcher matchEntry(MetadataSchema metadataSchema) { + return matchEntry(metadataSchema.getName(), metadataSchema.getNamespace()); + } + + public static Matcher matchEntry(String name, String nameSpace) { return allOf( - hasJsonPath("$.prefix", is(metadataSchema.getName())), - hasJsonPath("$.namespace", is(metadataSchema.getNamespace())), - hasJsonPath("$.type", is("metadataschema")), - hasJsonPath("$._links.self.href", Matchers.containsString("/api/core/metadataschemas")) + hasJsonPath("$.prefix", is(name)), + hasJsonPath("$.namespace", is(nameSpace)), + hasJsonPath("$.type", is("metadataschema")), + hasJsonPath("$._links.self.href", Matchers.containsString("/api/core/metadataschemas")) ); } } diff --git a/dspace-spring-rest/src/test/java/org/dspace/app/rest/test/MetadataPatchSuite.java b/dspace-spring-rest/src/test/java/org/dspace/app/rest/test/MetadataPatchSuite.java new file mode 100644 index 0000000000..423a4cbe35 --- /dev/null +++ b/dspace-spring-rest/src/test/java/org/dspace/app/rest/test/MetadataPatchSuite.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.app.rest.test; + +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.result.MockMvcResultMatchers.status; + +import javax.ws.rs.core.MediaType; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +/** + * Utility class for performing metadata patch tests sourced from a common json file (see constructor). + */ +public class MetadataPatchSuite { + private final ObjectMapper objectMapper = new ObjectMapper(); + private final JsonNode suite; + + /** + * Initializes the suite by parsing the json file of tests. + * + * @throws Exception if there is an error reading the file. + */ + public MetadataPatchSuite() throws Exception { + suite = objectMapper.readTree(getClass().getResourceAsStream("metadata-patch-suite.json")); + } + + /** + * Runs all tests in the file using the given client and url, expecting the given status. + * + * @param client the client to use. + * @param url the url to issue the patch against. + * @param expectedStatus the expected http status code. If this does not match the actual code, the test fails. + */ + public void runWith(MockMvc client, String url, int expectedStatus) { + for (JsonNode testNode: suite.get("tests")) { + String requestBody = testNode.get("patch").toString(); + String expectedMetadata = testNode.get("expect").toString(); + try { + System.out.println("Running patch test: " + testNode.get("name") + "\nRequest: " + requestBody); + checkResponse("PATCH", client, patch(url).content(requestBody), expectedMetadata, expectedStatus); + if (expectedStatus >= 200 && expectedStatus < 300) { + checkResponse("GET", client, get(url), expectedMetadata, expectedStatus); + } + } catch (Throwable t) { + Assert.fail("Metadata patch test '" + testNode.get("name") + "' failed.\n" + "Request body: " + + requestBody + "\n" + "Error: " + (t instanceof AssertionError ? "" : t.getClass().getName()) + + t.getMessage()); + } + } + } + + /** + * Issues a PATCH or GET request and checks that the body and response code match what is expected. + * + * @param verb the http verb (PATCH or GET). + * @param client the client to use. + * @param requestBuilder the request builder that has been pre-seeded with the request url and request body. + * @param expectedMetadata the expected metadata as a minimal (no extra spaces) json string. Note: This will + * only be checked if the expectedStatus is in the 200 range. + * @param expectedStatus the expected http response status. + * @throws Exception if any checked error occurs, signifying test failure. + */ + private void checkResponse(String verb, MockMvc client, MockHttpServletRequestBuilder requestBuilder, + String expectedMetadata, int expectedStatus) throws Exception { + ResultActions resultActions = client.perform(requestBuilder + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().is(expectedStatus)); + if (expectedStatus >= 200 && expectedStatus < 300) { + String responseBody = resultActions.andReturn().getResponse().getContentAsString(); + JsonNode responseJson = objectMapper.readTree(responseBody); + String responseMetadata = responseJson.get("metadata").toString(); + if (!responseMetadata.equals(expectedMetadata)) { + Assert.fail("Expected metadata in " + verb + " response: " + expectedMetadata + + "\nGot metadata in " + verb + " response: " + responseMetadata); + } + } + } +} diff --git a/dspace-spring-rest/src/test/resources/org/dspace/app/rest/test/metadata-patch-suite.json b/dspace-spring-rest/src/test/resources/org/dspace/app/rest/test/metadata-patch-suite.json new file mode 100644 index 0000000000..cff452fe91 --- /dev/null +++ b/dspace-spring-rest/src/test/resources/org/dspace/app/rest/test/metadata-patch-suite.json @@ -0,0 +1,152 @@ +{ + "tests": [ + { + "name": "clear metadata", + "patch": [ + { "op": "replace", + "path": "/metadata", + "value": {} + } + ], + "expect": {} + }, + { + "name": "add first title", + "patch": [ + { + "op": "add", + "path": "/metadata/dc.title", + "value": [ + { "value": "title 1" } + ] + } + ], + "expect": { + "dc.title": [ + { "value": "title 1", "language": null, "authority": null, "confidence": -1 } + ] + } + }, + { + "name": "add second title", + "patch": [ + { + "op": "add", + "path": "/metadata/dc.title/-", + "value": { "value": "最後のタイトル", "language": "ja_JP" } + } + ], + "expect": { + "dc.title": [ + { "value": "title 1", "language": null, "authority": null, "confidence": -1 }, + { "value": "最後のタイトル", "language": "ja_JP", "authority": null, "confidence": -1 } + ] + } + }, + { + "name": "insert zeroth title", + "patch": [ + { + "op": "add", + "path": "/metadata/dc.title/0", + "value": { + "value": "title 0" + } + } + ], + "expect": { + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 }, + { "value": "最後のタイトル", "language": "ja_JP", "authority": null, "confidence": -1 } + ] + } + }, + { + "name": "move last title up one", + "patch": [ + { + "op": "move", + "from": "/metadata/dc.title/2", + "path": "/metadata/dc.title/1" + } + ], + "expect": { + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 }, + { "value": "最後のタイトル", "language": "ja_JP", "authority": null, "confidence": -1 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 } + ] + } + }, + { + "name": "replace title 2 value and language in two operations", + "patch": [ + { + "op": "replace", + "path": "/metadata/dc.title/1/value", + "value": "title A" + }, + { + "op": "replace", + "path": "/metadata/dc.title/1/language", + "value": "en_US" + } + ], + "expect": { + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 }, + { "value": "title A", "language": "en_US", "authority": null, "confidence": -1 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 } + ] + } + }, + { + "name": "copy title A to end of list", + "patch": [ + { + "op": "copy", + "from": "/metadata/dc.title/1", + "path": "/metadata/dc.title/-" + } + ], + "expect": { + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 }, + { "value": "title A", "language": "en_US", "authority": null, "confidence": -1 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 }, + { "value": "title A", "language": "en_US", "authority": null, "confidence": -1 } + ] + } + }, + { + "name": "remove both title A copies", + "patch": [ + { + "op": "remove", + "path": "/metadata/dc.title/1" + }, + { + "op": "remove", + "path": "/metadata/dc.title/2" + } + ], + "expect": { + "dc.title": [ + { "value": "title 0", "language": null, "authority": null, "confidence": -1 }, + { "value": "title 1", "language": null, "authority": null, "confidence": -1 } + ] + } + }, + { + "name": "remove all titles", + "patch": [ + { + "op": "remove", + "path": "/metadata/dc.title" + } + ], + "expect": {} + } + ] +} diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index af9306359e..da22a95716 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -37,7 +37,7 @@ dspace.url = ${dspace.baseUrl} # This is the URL that will be used for the REST endpoints to be served on. # This will typically be followed by /api to determine the root endpoints. -dspace.restUrl = ${dspace.baseUrl}/rest +dspace.restUrl = ${dspace.baseUrl}/spring-rest # Optional: DSpace URL for mobile access # This diff --git a/dspace/config/modules/oai.cfg b/dspace/config/modules/oai.cfg index 5fbce8e880..493eb2510e 100644 --- a/dspace/config/modules/oai.cfg +++ b/dspace/config/modules/oai.cfg @@ -31,6 +31,13 @@ oai.cache.enabled = true # Base Cache Directory oai.cache.dir = ${dspace.dir}/var/oai +#---------------------------------------------------------------# +#--------------OAI IMPORT CONFIGURATION ------------------------# +#---------------------------------------------------------------# + +# Size of batches to commit to solr at a time +oai.import.batch.size = 1000 + #---------------------------------------------------------------# #--------------OAI HARVESTING CONFIGURATIONS--------------------# #---------------------------------------------------------------# diff --git a/dspace/config/modules/submission-curation.cfg b/dspace/config/modules/submission-curation.cfg index b69ba71cbb..60442a9955 100644 --- a/dspace/config/modules/submission-curation.cfg +++ b/dspace/config/modules/submission-curation.cfg @@ -2,7 +2,15 @@ #------------SUBMISSION CURATION CONFIGURATIONS-----------------# #---------------------------------------------------------------# # This file contains configuration properties solely relating # -# to the scheduling of curation tasks during submission. # +# to the scheduling of curation tasks during submission -- that # +# is: when tasks are attached to a workflow. # #---------------------------------------------------------------# # Scan for viruses -submission-curation.virus-scan = false \ No newline at end of file +submission-curation.virus-scan = false + +# Report serializer plugin, to capture submission task reports. +# Uncomment exactly one, or configure your own. +# FileReporter writes reports to ${report.dir}/curation-yyyyMMddThhmmssSSS.report +plugin.single.org.dspace.curate.Reporter = org.dspace.curate.FileReporter +# LogReporter writes report lines to the DSpace log. +#plugin.single.org.dspace.curate.Reporter = org.dspace.curate.LogReporter diff --git a/dspace/etc/oracle/README.md b/dspace/etc/oracle/README.md deleted file mode 100644 index 0ed2a66aa9..0000000000 --- a/dspace/etc/oracle/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# DSpace Database Now Upgrades Automatically - -AS OF DSPACE 5, the DSpace database now upgrades itself AUTOMATICALLY. - -Therefore, all `database_schema*.sql` files have been removed. Starting -with DSpace 4.x -> 5.0 upgrade, you will no longer need to manually run any -SQL scripts to upgrade your database. - -Please see the [5.0 Upgrade Instructions](https://wiki.duraspace.org/display/DSDOC5x/Upgrading+DSpace) -for more information on upgrading to DSpace 5. - - -## More info on automatic database upgrades - -As of DSpace 5.0, we now use [Flyway DB](http://flywaydb.org/) along with the -SQL scripts embedded in the `dspace-api.jar` to automatically keep your DSpace -database up-to-date. These scripts are now located in the source code at: -`[dspace-src]/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle` - -As Flyway automates the upgrade process, you should NEVER run these SQL scripts -manually. For more information, please see the `README.md` in the scripts directory. - -## Using the update-sequences.sql script - -The `update-sequences.sql` script in this directory may still be used to update -your internal database counts if you feel they have gotten out of "sync". This -may sometimes occur after large restores of content (e.g. when using the DSpace -[AIP Backup and Restore](https://wiki.duraspace.org/display/DSDOC5x/AIP+Backup+and+Restore) -feature). - -This `update-sequences.sql` script can be run manually. It will not harm your -database (or its contents) in any way. It just ensures all database counts (i.e. -sequences) are properly set to the next available value. diff --git a/dspace/etc/oracle/update-sequences.sql b/dspace/etc/oracle/update-sequences.sql deleted file mode 100644 index d40f7665a7..0000000000 --- a/dspace/etc/oracle/update-sequences.sql +++ /dev/null @@ -1,75 +0,0 @@ --- --- update-sequences.sql --- --- Copyright (c) 2002-2016, The DSpace Foundation. All rights reserved. --- --- Redistribution and use in source and binary forms, with or without --- modification, are permitted provided that the following conditions are --- met: --- --- - Redistributions of source code must retain the above copyright --- notice, this list of conditions and the following disclaimer. --- --- - Redistributions in binary form must reproduce the above copyright --- notice, this list of conditions and the following disclaimer in the --- documentation and/or other materials provided with the distribution. --- --- - Neither the name of the DSpace Foundation nor the names of its --- contributors may be used to endorse or promote products derived from --- this software without specific prior written permission. --- --- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS --- ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT --- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR --- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT --- HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, --- INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, --- BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS --- OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND --- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR --- TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE --- USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH --- DAMAGE. - --- SQL code to update the ID (primary key) generating sequences, if some --- import operation has set explicit IDs. --- --- Sequences are used to generate IDs for new rows in the database. If a --- bulk import operation, such as an SQL dump, specifies primary keys for --- imported data explicitly, the sequences are out of sync and need updating. --- This SQL code does just that. --- --- This should rarely be needed; any bulk import should be performed using the --- org.dspace.content API which is safe to use concurrently and in multiple --- JVMs. The SQL code below will typically only be required after a direct --- SQL data dump from a backup or somesuch. - --- Depends on being run from sqlplus with incseq.sql in the current path --- you can find incseq.sql at: http://www.akadia.com/services/scripts/incseq.sql --- Here that script was renamed to updateseq.sql. - -@updateseq.sql bitstreamformatregistry_seq bitstreamformatregistry bitstream_format_id "" -@updateseq.sql fileextension_seq fileextension file_extension_id "" -@updateseq.sql resourcepolicy_seq resourcepolicy policy_id "" -@updateseq.sql workspaceitem_seq workspaceitem workspace_item_id "" -@updateseq.sql workflowitem_seq workflowitem workflow_id "" -@updateseq.sql tasklistitem_seq tasklistitem tasklist_id "" -@updateseq.sql registrationdata_seq registrationdata registrationdata_id "" -@updateseq.sql subscription_seq subscription subscription_id "" -@updateseq.sql metadatafieldregistry_seq metadatafieldregistry metadata_field_id "" -@updateseq.sql metadatavalue_seq metadatavalue metadata_value_id "" -@updateseq.sql metadataschemaregistry_seq metadataschemaregistry metadata_schema_id "" -@updateseq.sql harvested_collection_seq harvested_collection id "" -@updateseq.sql harvested_item_seq harvested_item id "" -@updateseq.sql webapp_seq webapp webapp_id "" -@updateseq.sql requestitem_seq requestitem requestitem_id "" -@updateseq.sql handle_id_seq handle handle_id "" - --- Handle Sequence is a special case. Since Handles minted by DSpace use the 'handle_seq', --- we need to ensure the next assigned handle will *always* be unique. So, 'handle_seq' --- always needs to be set to the value of the *largest* handle suffix. That way when the --- next handle is assigned, it will use the next largest number. This query does the following: --- For all 'handle' values which have a number in their suffix (after '/'), find the maximum --- suffix value, convert it to a number, and set the 'handle_seq' to start at the next value --- (see updateseq.sql script for more) -@updateseq.sql handle_seq handle "to_number(regexp_replace(handle, '.*/', ''), '999999999999')" "WHERE REGEXP_LIKE(handle, '^.*/[0123456789]*$')" diff --git a/dspace/etc/oracle/updateseq.sql b/dspace/etc/oracle/updateseq.sql deleted file mode 100644 index 49d3701cd2..0000000000 --- a/dspace/etc/oracle/updateseq.sql +++ /dev/null @@ -1,30 +0,0 @@ --- ############################################################################################# --- --- %Purpose: Set a sequence to the max value of a given attribute --- --- ############################################################################################# --- --- Paramters: --- 1: sequence name --- 2: table name --- 3: attribute name --- --- Sample usage: --- @updateseq.sql my_sequence my_table my_attribute where-clause --- --------------------------------------------------------------------------------- --- -SET SERVEROUTPUT ON SIZE 1000000; --- -DECLARE - curr NUMBER := 0; -BEGIN - SELECT max(&3) INTO curr FROM &2 &4; - - curr := curr + 1; - - EXECUTE IMMEDIATE 'DROP SEQUENCE &1'; - - EXECUTE IMMEDIATE 'CREATE SEQUENCE &1 START WITH ' || NVL(curr,1); -END; -/ diff --git a/dspace/etc/postgres/README.md b/dspace/etc/postgres/README.md deleted file mode 100644 index 7d4a9ad2da..0000000000 --- a/dspace/etc/postgres/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# DSpace Database Now Upgrades Automatically - -AS OF DSPACE 5, the DSpace database now upgrades itself AUTOMATICALLY. - -Therefore, all `database_schema*.sql` files have been removed. Starting -with DSpace 4.x -> 5.0 upgrade, you will no longer need to manually run any -SQL scripts to upgrade your database. - -Please see the [5.0 Upgrade Instructions](https://wiki.duraspace.org/display/DSDOC5x/Upgrading+DSpace) -for more information on upgrading to DSpace 5. - - -## More info on automatic database upgrades - -As of DSpace 5.0, we now use [Flyway DB](http://flywaydb.org/) along with the -SQL scripts embedded in the `dspace-api.jar` to automatically keep your DSpace -database up-to-date. These scripts are now located in the source code at: -`[dspace-src]/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres` - -As Flyway automates the upgrade process, you should NEVER run these SQL scripts -manually. For more information, please see the `README.md` in the scripts directory. - -## Using the update-sequences.sql script - -The `update-sequences.sql` script in this directory may still be used to update -your internal database counts if you feel they have gotten out of "sync". This -may sometimes occur after large restores of content (e.g. when using the DSpace -[AIP Backup and Restore](https://wiki.duraspace.org/display/DSDOC5x/AIP+Backup+and+Restore) -feature). - -This `update-sequences.sql` script can be run manually. It will not harm your -database (or its contents) in any way. It just ensures all database counts (i.e. -sequences) are properly set to the next available value. diff --git a/dspace/src/main/config/build.xml b/dspace/src/main/config/build.xml index ab13191561..8b4f588dd0 100644 --- a/dspace/src/main/config/build.xml +++ b/dspace/src/main/config/build.xml @@ -569,16 +569,6 @@ Common usage: - - - - - - - - - - @@ -593,10 +583,6 @@ Common usage: ${dspace.dir}/lib.bak-${build.date} - ${dspace.dir}/etc was backed up to - - ${dspace.dir}/etc.bak-${build.date} - Please review these directories and delete if no longer needed. ==================================================================== @@ -728,8 +714,6 @@ Common usage: - - @@ -821,17 +805,6 @@ Common usage: - - - - - - - - - diff --git a/dspace/src/main/docker/test/solr_web.xml b/dspace/src/main/docker/test/solr_web.xml index 4329317c53..50a8bd5b9a 100644 --- a/dspace/src/main/docker/test/solr_web.xml +++ b/dspace/src/main/docker/test/solr_web.xml @@ -1,6 +1,4 @@ - - + - - - - solr/home ${dspace.dir}/solr java.lang.String - + - log4j.configuration - ${dspace.dir}/config/log4j-solr.properties - URL locating a Log4J configuration file (properties or XML). + + URL locating a Log4J configuration file (properties or XML). + + log4jConfiguration + ${dspace.dir}/config/log4j-solr.xml + + org.apache.logging.log4j.web.Log4jServletContextListener + + + + Activate logging + log4jServletFilter + org.apache.logging.log4j.web.Log4jServletFilter + + LocalHostRestrictionFilter @@ -87,7 +92,16 @@ --> - - - org.dspace.solr.filters.ConfigureLog4jListener - - Zookeeper org.apache.solr.servlet.ZookeeperInfoServlet