From cb05748ff4bcbeca9abdf0cc733d12698b9bd618 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 29 Mar 2022 16:16:41 +0200 Subject: [PATCH 001/118] [CST-5249] Added suggestions from openaire --- ...blicationLoaderCliScriptConfiguration.java | 29 + .../OAIREPublicationLoaderRunnable.java | 113 + .../OAIREPublicationLoaderRunnableCli.java | 36 + ...EPublicationLoaderScriptConfiguration.java | 62 + .../suggestion/SolrSuggestionProvider.java | 140 ++ .../SolrSuggestionStorageService.java | 191 ++ .../SolrSuggestionStorageServiceImpl.java | 347 +++ .../org/dspace/app/suggestion/Suggestion.java | 89 + .../app/suggestion/SuggestionEvidence.java | 56 + .../app/suggestion/SuggestionProvider.java | 34 + .../app/suggestion/SuggestionService.java | 48 + .../app/suggestion/SuggestionServiceImpl.java | 191 ++ .../app/suggestion/SuggestionSource.java | 46 + .../app/suggestion/SuggestionTarget.java | 71 + .../app/suggestion/SuggestionUtils.java | 111 + .../suggestion/oaire/AuthorNamesScorer.java | 147 ++ .../app/suggestion/oaire/DateScorer.java | 200 ++ .../app/suggestion/oaire/EvidenceScorer.java | 37 + .../oaire/OAIREPublicationLoader.java | 240 ++ .../service/impl/ExternalDataServiceImpl.java | 15 + ...pleXpathDateFormatMetadataContributor.java | 84 + .../SimpleXpathMetadatumContributor.java | 8 +- .../OpenAIREPublicationFieldMapping.java | 29 + ...enAireImportMetadataSourceServiceImpl.java | 337 +++ .../spring-dspace-addon-import-services.xml | 16 +- ...pring-dspace-addon-suggestion-services.xml | 23 + .../config/spring/api/external-services.xml | 2 + .../config/spring/api/solr-services.xml | 3 + .../config/spring/api/suggestions.xml | 33 + .../MockSolrSuggestionProvider.java | 20 + .../MockSolrSuggestionStorageService.java | 38 + .../MockSuggestionExternalDataSource.java | 67 + .../org/dspace/builder/AbstractBuilder.java | 4 + .../java/org/dspace/builder/ItemBuilder.java | 4 + .../builder/SuggestionTargetBuilder.java | 161 ++ .../app/rest/RestResourceController.java | 7 + .../rest/converter/SuggestionConverter.java | 52 + .../converter/SuggestionSourceConverter.java | 39 + .../converter/SuggestionTargetConverter.java | 41 + .../dspace/app/rest/model/SuggestionRest.java | 110 + .../app/rest/model/SuggestionSourceRest.java | 51 + .../app/rest/model/SuggestionTargetRest.java | 73 + .../model/hateoas/SuggestionResource.java | 25 + .../hateoas/SuggestionSourceResource.java | 25 + .../hateoas/SuggestionTargetResource.java | 25 + .../repository/SuggestionRestRepository.java | 88 + .../SuggestionSourceRestRepository.java | 64 + .../SuggestionTargetRestRepository.java | 92 + .../SuggestionTargetTargetLinkRepository.java | 70 + ...ggestionRestPermissionEvaluatorPlugin.java | 94 + ...onTargetRestPermissionEvaluatorPlugin.java | 95 + .../rest/ExternalSourcesRestControllerIT.java | 1 + .../app/rest/SuggestionRestRepositoryIT.java | 473 ++++ .../SuggestionSourceRestRepositoryIT.java | 168 ++ .../SuggestionTargetRestRepositoryIT.java | 597 +++++ .../app/rest/matcher/SuggestionMatcher.java | 57 + .../rest/matcher/SuggestionSourceMatcher.java | 28 + .../rest/matcher/SuggestionTargetMatcher.java | 29 + dspace/config/dspace.cfg | 2 + dspace/config/modules/authority.cfg | 13 + dspace/config/modules/suggestion.cfg | 7 + dspace/config/registries/dspace-types.xml | 8 + .../config/spring/api/external-services.xml | 12 + .../spring/api/openaire-integration.xml | 226 ++ dspace/config/spring/api/scripts.xml | 6 + dspace/config/spring/api/solr-services.xml | 3 + dspace/config/spring/api/suggestions.xml | 68 + dspace/solr/suggestion/conf/admin-extra.html | 31 + dspace/solr/suggestion/conf/elevate.xml | 36 + dspace/solr/suggestion/conf/protwords.txt | 21 + dspace/solr/suggestion/conf/schema.xml | 547 +++++ dspace/solr/suggestion/conf/scripts.conf | 24 + dspace/solr/suggestion/conf/solrconfig.xml | 1943 +++++++++++++++++ dspace/solr/suggestion/conf/spellings.txt | 2 + dspace/solr/suggestion/conf/stopwords.txt | 57 + dspace/solr/suggestion/conf/synonyms.txt | 31 + dspace/solr/suggestion/core.properties | 0 77 files changed, 8365 insertions(+), 8 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderCliScriptConfiguration.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnable.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnableCli.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderScriptConfiguration.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionProvider.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageService.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/Suggestion.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionProvider.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionService.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionUtils.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/oaire/AuthorNamesScorer.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/oaire/DateScorer.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/oaire/EvidenceScorer.java create mode 100644 dspace-api/src/main/java/org/dspace/app/suggestion/oaire/OAIREPublicationLoader.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathDateFormatMetadataContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/openaire/metadatamapping/OpenAIREPublicationFieldMapping.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java create mode 100644 dspace-api/src/main/resources/spring/spring-dspace-addon-suggestion-services.xml create mode 100644 dspace-api/src/test/data/dspaceFolder/config/spring/api/suggestions.xml create mode 100644 dspace-api/src/test/java/org/dspace/app/suggestion/MockSolrSuggestionProvider.java create mode 100644 dspace-api/src/test/java/org/dspace/app/suggestion/MockSolrSuggestionStorageService.java create mode 100644 dspace-api/src/test/java/org/dspace/app/suggestion/MockSuggestionExternalDataSource.java create mode 100644 dspace-api/src/test/java/org/dspace/builder/SuggestionTargetBuilder.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionSourceConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionTargetConverter.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionSourceRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionTargetRest.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionResource.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionSourceResource.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionTargetResource.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionRestRepository.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionSourceRestRepository.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionTargetRestRepository.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionTargetTargetLinkRepository.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SuggestionRestPermissionEvaluatorPlugin.java create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SuggestionTargetRestPermissionEvaluatorPlugin.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionRestRepositoryIT.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionSourceRestRepositoryIT.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionTargetRestRepositoryIT.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionMatcher.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionSourceMatcher.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionTargetMatcher.java create mode 100644 dspace/config/modules/authority.cfg create mode 100644 dspace/config/modules/suggestion.cfg create mode 100644 dspace/config/spring/api/openaire-integration.xml create mode 100644 dspace/config/spring/api/suggestions.xml create mode 100644 dspace/solr/suggestion/conf/admin-extra.html create mode 100644 dspace/solr/suggestion/conf/elevate.xml create mode 100644 dspace/solr/suggestion/conf/protwords.txt create mode 100644 dspace/solr/suggestion/conf/schema.xml create mode 100644 dspace/solr/suggestion/conf/scripts.conf create mode 100644 dspace/solr/suggestion/conf/solrconfig.xml create mode 100644 dspace/solr/suggestion/conf/spellings.txt create mode 100644 dspace/solr/suggestion/conf/stopwords.txt create mode 100644 dspace/solr/suggestion/conf/synonyms.txt create mode 100644 dspace/solr/suggestion/core.properties diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderCliScriptConfiguration.java new file mode 100644 index 000000000000..aeb034da0636 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderCliScriptConfiguration.java @@ -0,0 +1,29 @@ +/** + * 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.suggestion; + +import org.apache.commons.cli.Options; + +/** + * Extension of {@link OAIREPublicationLoaderScriptConfiguration} for CLI. + * + * @author Alessandro Martelli (alessandro.martelli at 4science.it) + */ +public class OAIREPublicationLoaderCliScriptConfiguration + extends OAIREPublicationLoaderScriptConfiguration { + + @Override + public Options getOptions() { + Options options = super.getOptions(); + options.addOption("h", "help", false, "help"); + options.getOption("h").setType(boolean.class); + super.options = options; + return options; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnable.java b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnable.java new file mode 100644 index 000000000000..1349a1a40caa --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnable.java @@ -0,0 +1,113 @@ +/** + * 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.suggestion; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.cli.ParseException; +import org.dspace.app.suggestion.oaire.OAIREPublicationLoader; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.discovery.IndexableObject; +import org.dspace.discovery.SearchService; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.utils.DSpace; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Runner responsible to import metadata about authors from OpenAIRE to Solr. + * This runner works in two ways: + * If -s parameter with a valid UUID is received, then the specific researcher + * with this UUID will be used. + * Invocation without any parameter results in massive import, processing all + * authors registered in DSpace. + * + * @author Alessandro Martelli (alessandro.martelli at 4science.it) + */ +public class OAIREPublicationLoaderRunnable + extends DSpaceRunnable> { + + private static final Logger LOGGER = LoggerFactory.getLogger(OAIREPublicationLoaderRunnable.class); + + private OAIREPublicationLoader oairePublicationLoader = null; + + protected Context context; + + protected String profile; + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public OAIREPublicationLoaderScriptConfiguration getScriptConfiguration() { + OAIREPublicationLoaderScriptConfiguration configuration = new DSpace().getServiceManager() + .getServiceByName("import-oaire-suggestions", OAIREPublicationLoaderScriptConfiguration.class); + return configuration; + } + + @Override + public void setup() throws ParseException { + + oairePublicationLoader = new DSpace().getServiceManager().getServiceByName( + "OAIREPublicationLoader", OAIREPublicationLoader.class); + + profile = commandLine.getOptionValue("s"); + if (profile == null) { + LOGGER.info("No argument for -s, process all profile"); + } else { + LOGGER.info("Process eperson item with UUID " + profile); + } + } + + @Override + public void internalRun() throws Exception { + + context = new Context(); + + List researchers = getResearchers(profile); + + for (Item researcher : researchers) { + + oairePublicationLoader.importAuthorRecords(context, researcher); + } + + } + + /** + * Get the Item(s) which map a researcher from Solr. If the uuid is specified, + * the researcher with this UUID will be chosen. If the uuid doesn't match any + * researcher, the method returns an empty array list. If uuid is null, all + * research will be return. + * + * @param profile uuid of the researcher. If null, all researcher will be + * returned. + * @return the researcher with specified UUID or all researchers + */ + @SuppressWarnings("rawtypes") + private List getResearchers(String profileUUID) { + final UUID uuid = profileUUID != null ? UUID.fromString(profileUUID) : null; + SearchService searchService = new DSpace().getSingletonService(SearchService.class); + List objects = null; + if (uuid != null) { + objects = searchService.search(context, "search.resourceid:" + uuid.toString(), + "lastModified", false, 0, 1000, "search.resourcetype:Item", "dspace.entity.type:Person"); + } else { + objects = searchService.search(context, "*:*", "lastModified", false, 0, 1000, "search.resourcetype:Item", + "dspace.entity.type:Person"); + } + List items = new ArrayList(); + if (objects != null) { + for (IndexableObject o : objects) { + items.add((Item) o.getIndexedObject()); + } + } + LOGGER.info("Found " + items.size() + " researcher(s)"); + return items; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnableCli.java b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnableCli.java new file mode 100644 index 000000000000..b0f850577958 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnableCli.java @@ -0,0 +1,36 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.suggestion; + +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.ParseException; +import org.dspace.utils.DSpace; + +public class OAIREPublicationLoaderRunnableCli extends OAIREPublicationLoaderRunnable { + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public OAIREPublicationLoaderCliScriptConfiguration getScriptConfiguration() { + OAIREPublicationLoaderCliScriptConfiguration configuration = new DSpace().getServiceManager() + .getServiceByName("import-oaire-suggestions", OAIREPublicationLoaderCliScriptConfiguration.class); + return configuration; + } + + @Override + public void setup() throws ParseException { + super.setup(); + + // in case of CLI we show the help prompt + if (commandLine.hasOption('h')) { + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("Import Readearchers Suggestions", getScriptConfiguration().getOptions()); + System.exit(0); + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderScriptConfiguration.java new file mode 100644 index 000000000000..594ab4ce3123 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderScriptConfiguration.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.app.suggestion; + +import java.sql.SQLException; + +import org.apache.commons.cli.Options; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.dspace.scripts.configuration.ScriptConfiguration; +import org.springframework.beans.factory.annotation.Autowired; + +public class OAIREPublicationLoaderScriptConfiguration + extends ScriptConfiguration { + + @Autowired + private AuthorizeService authorizeService; + + private Class dspaceRunnableClass; + + @Override + public Class getDspaceRunnableClass() { + return dspaceRunnableClass; + } + + /** + * Generic setter for the dspaceRunnableClass + * @param dspaceRunnableClass The dspaceRunnableClass to be set on this OAIREPublicationLoaderScriptConfiguration + */ + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + + @Override + public boolean isAllowedToExecute(Context context) { + try { + return authorizeService.isAdmin(context); + } catch (SQLException e) { + throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); + } + } + + @Override + public Options getOptions() { + if (options == null) { + Options options = new Options(); + + options.addOption("s", "single-researcher", true, "Single researcher UUID"); + options.getOption("s").setType(String.class); + + super.options = options; + } + return options; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionProvider.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionProvider.java new file mode 100644 index 000000000000..e4573ebcd3ca --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionProvider.java @@ -0,0 +1,140 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.suggestion; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import org.apache.logging.log4j.Logger; +import org.apache.solr.client.solrj.SolrServerException; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.external.model.ExternalDataObject; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Suggestion provider that read the suggestion from the local suggestion solr + * core + * + * @author Andrea Bollini (andrea.bollini at 4science dot it) + * + */ +public abstract class SolrSuggestionProvider implements SuggestionProvider { + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SolrSuggestionProvider.class); + + @Autowired + protected ItemService itemService; + + @Autowired + protected SolrSuggestionStorageService solrSuggestionStorageService; + + private String sourceName; + + public String getSourceName() { + return sourceName; + } + + public void setSourceName(String sourceName) { + this.sourceName = sourceName; + } + + public void setItemService(ItemService itemService) { + this.itemService = itemService; + } + + @Override + public long countAllTargets(Context context) { + try { + return this.solrSuggestionStorageService.countAllTargets(context, sourceName); + } catch (SolrServerException | IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public long countUnprocessedSuggestionByTarget(Context context, UUID target) { + try { + return this.solrSuggestionStorageService.countUnprocessedSuggestionByTarget(context, sourceName, target); + } catch (SolrServerException | IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public List findAllUnprocessedSuggestions(Context context, UUID target, int pageSize, long offset, + boolean ascending) { + + try { + return this.solrSuggestionStorageService.findAllUnprocessedSuggestions(context, sourceName, + target, pageSize, offset, ascending); + } catch (SolrServerException | IOException e) { + throw new RuntimeException(e); + } + + } + + @Override + public List findAllTargets(Context context, int pageSize, long offset) { + try { + return this.solrSuggestionStorageService.findAllTargets(context, sourceName, pageSize, offset); + } catch (SolrServerException | IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Suggestion findUnprocessedSuggestion(Context context, UUID target, String id) { + try { + return this.solrSuggestionStorageService.findUnprocessedSuggestion(context, sourceName, target, id); + } catch (SolrServerException | IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public SuggestionTarget findTarget(Context context, UUID target) { + try { + return this.solrSuggestionStorageService.findTarget(context, sourceName, target); + } catch (SolrServerException | IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void rejectSuggestion(Context context, UUID target, String idPart) { + Suggestion suggestion = findUnprocessedSuggestion(context, target, idPart); + try { + solrSuggestionStorageService.flagSuggestionAsProcessed(suggestion); + } catch (SolrServerException | IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void flagRelatedSuggestionsAsProcessed(Context context, ExternalDataObject externalDataObject) { + if (!isExternalDataObjectPotentiallySuggested(context, externalDataObject)) { + return; + } + try { + solrSuggestionStorageService.flagAllSuggestionAsProcessed(sourceName, externalDataObject.getId()); + } catch (SolrServerException | IOException e) { + log.error(e.getMessage(), e); + } + } + + /** + * + * @param context + * @param externalDataObject + * @return true if the externalDataObject could be suggested by this provider + * (i.e. it comes from a DataProvider used by this suggestor) + */ + protected abstract boolean isExternalDataObjectPotentiallySuggested(Context context, + ExternalDataObject externalDataObject); +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageService.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageService.java new file mode 100644 index 000000000000..b7de6146f27e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageService.java @@ -0,0 +1,191 @@ +/** + * 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.suggestion; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import org.apache.solr.client.solrj.SolrServerException; +import org.dspace.core.Context; + +/** + * Service to deal with the local suggestion solr core used by the + * SolrSuggestionProvider(s) + * + * @author Andrea Bollini (andrea.bollini at 4science dot it) + * @author Luca Giamminonni (luca.giamminonni at 4science dot it) + * + */ +public interface SolrSuggestionStorageService { + public static final String SOURCE = "source"; + /** This is the URI Part of the suggestion source:target:id */ + public static final String SUGGESTION_FULLID = "suggestion_fullid"; + public static final String SUGGESTION_ID = "suggestion_id"; + public static final String TARGET_ID = "target_id"; + public static final String TITLE = "title"; + public static final String DATE = "date"; + public static final String DISPLAY = "display"; + public static final String CONTRIBUTORS = "contributors"; + public static final String ABSTRACT = "abstract"; + public static final String CATEGORY = "category"; + public static final String EXTERNAL_URI = "external-uri"; + public static final String PROCESSED = "processed"; + public static final String SCORE = "trust"; + public static final String EVIDENCES = "evidences"; + + /** + * Add a new suggestion to SOLR + * + * @param suggestion + * @param force true if the suggestion must be reindexed + * @param commit + * @throws IOException + * @throws SolrServerException + */ + public void addSuggestion(Suggestion suggestion, boolean force, boolean commit) + throws SolrServerException, IOException; + + /** + * Return true if the suggestion is already in SOLR and flagged as processed + * + * @param suggestion + * @return true if the suggestion is already in SOLR and flagged as processed + * @throws IOException + * @throws SolrServerException + */ + public boolean exist(Suggestion suggestion) throws SolrServerException, IOException; + + /** + * Delete a suggestion from SOLR if any + * + * @param suggestion + * @throws IOException + * @throws SolrServerException + */ + public void deleteSuggestion(Suggestion suggestion) throws SolrServerException, IOException; + + /** + * Flag a suggestion as processed in SOLR if any + * + * @param suggestion + * @throws IOException + * @throws SolrServerException + */ + public void flagSuggestionAsProcessed(Suggestion suggestion) throws SolrServerException, IOException; + + /** + * Delete all the suggestions from SOLR if any related to a specific target + * + * @param target + * @throws IOException + * @throws SolrServerException + */ + public void deleteTarget(SuggestionTarget target) throws SolrServerException, IOException; + + /** + * Performs an explicit commit, causing pending documents to be committed for + * indexing. + * + * @throws SolrServerException + * @throws IOException + */ + void commit() throws SolrServerException, IOException; + + /** + * Flag all the suggestion related to the given source and id as processed. + * + * @param source the source name + * @param idPart the id's last part + * @throws SolrServerException + * @throws IOException + */ + void flagAllSuggestionAsProcessed(String source, String idPart) throws SolrServerException, IOException; + + /** + * Count all the targets related to the given source. + * + * @param source the source name + * @return the target's count + * @throws IOException + * @throws SolrServerException + */ + long countAllTargets(Context context, String source) throws SolrServerException, IOException; + + /** + * Count all the unprocessed suggestions related to the given source and target. + * + * @param context the DSpace Context + * @param source the source name + * @param target the target id + * @return the suggestion count + * @throws SolrServerException + * @throws IOException + */ + long countUnprocessedSuggestionByTarget(Context context, String source, UUID target) + throws SolrServerException, IOException; + + /** + * Find all the unprocessed suggestions related to the given source and target. + * + * @param context the DSpace Context + * @param source the source name + * @param target the target id + * @param pageSize the page size + * @param offset the page offset + * @param ascending true to retrieve the suggestions ordered by score + * ascending + * @return the found suggestions + * @throws SolrServerException + * @throws IOException + */ + List findAllUnprocessedSuggestions(Context context, String source, UUID target, + int pageSize, long offset, boolean ascending) throws SolrServerException, IOException; + + /** + * + * Find all the suggestion targets related to the given source. + * + * @param context the DSpace Context + * @param source the source name + * @param pageSize the page size + * @param offset the page offset + * @return the found suggestion targets + * @throws SolrServerException + * @throws IOException + */ + List findAllTargets(Context context, String source, int pageSize, long offset) + throws SolrServerException, IOException; + + /** + * Find an unprocessed suggestion by the given source, target id and suggestion + * id. + * + * @param context the DSpace Context + * @param source the source name + * @param target the target id + * @param id the suggestion id + * @return the suggestion, if any + * @throws SolrServerException + * @throws IOException + */ + Suggestion findUnprocessedSuggestion(Context context, String source, UUID target, String id) + throws SolrServerException, IOException; + + /** + * Find a suggestion target by the given source and target. + * + * @param context the DSpace Context + * @param source the source name + * @param target the target id + * @return the suggestion target, if any + * @throws SolrServerException + * @throws IOException + */ + SuggestionTarget findTarget(Context context, String source, UUID target) throws SolrServerException, IOException; +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java new file mode 100644 index 000000000000..9d77fc2886c6 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java @@ -0,0 +1,347 @@ +/** + * 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.suggestion; + +import static org.apache.commons.collections.CollectionUtils.isEmpty; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import org.apache.commons.lang3.StringUtils; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.SolrQuery.SortClause; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.HttpSolrClient; +import org.apache.solr.client.solrj.response.FacetField; +import org.apache.solr.client.solrj.response.FacetField.Count; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.SolrDocument; +import org.apache.solr.common.SolrDocumentList; +import org.apache.solr.common.SolrInputDocument; +import org.dspace.content.Item; +import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.util.UUIDUtils; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service to deal with the local suggestion solr core used by the + * SolrSuggestionProvider(s) + * + * @author Andrea Bollini (andrea.bollini at 4science dot it) + * + */ +public class SolrSuggestionStorageServiceImpl implements SolrSuggestionStorageService { + + protected SolrClient solrSuggestionClient; + + @Autowired + private ItemService itemService; + + /** + * Get solr client which use suggestion core + * + * @return solr client + */ + protected SolrClient getSolr() { + if (solrSuggestionClient == null) { + String solrService = DSpaceServicesFactory.getInstance().getConfigurationService() + .getProperty("suggestion.solr.server", "http://localhost:8983/solr/suggestion"); + solrSuggestionClient = new HttpSolrClient.Builder(solrService).build(); + } + return solrSuggestionClient; + } + + @Override + public void addSuggestion(Suggestion suggestion, boolean force, boolean commit) + throws SolrServerException, IOException { + if (force || !exist(suggestion)) { + Gson gson = new Gson(); + SolrInputDocument document = new SolrInputDocument(); + document.addField(SOURCE, suggestion.getSource()); + String suggestionFullID = suggestion.getID(); + document.addField(SUGGESTION_FULLID, suggestionFullID); + document.addField(SUGGESTION_ID, suggestionFullID.split(":", 3)[2]); + document.addField(TARGET_ID, suggestion.getTarget().getID().toString()); + document.addField(DISPLAY, suggestion.getDisplay()); + document.addField(TITLE, getFirstValue(suggestion, "dc", "title", null)); + document.addField(DATE, getFirstValue(suggestion, "dc", "date", "issued")); + document.addField(CONTRIBUTORS, getAllValues(suggestion, "dc", "contributor", "author")); + document.addField(ABSTRACT, getFirstValue(suggestion, "dc", "description", "abstract")); + document.addField(CATEGORY, getAllValues(suggestion, "dc", "source", null)); + document.addField(EXTERNAL_URI, suggestion.getExternalSourceUri()); + document.addField(SCORE, suggestion.getScore()); + document.addField(PROCESSED, false); + document.addField(EVIDENCES, gson.toJson(suggestion.getEvidences())); + getSolr().add(document); + if (commit) { + getSolr().commit(); + } + } + } + + @Override + public void commit() throws SolrServerException, IOException { + getSolr().commit(); + } + + private List getAllValues(Suggestion suggestion, String schema, String element, String qualifier) { + return suggestion.getMetadata().stream() + .filter(st -> StringUtils.isNotBlank(st.getValue()) && StringUtils.equals(st.getSchema(), schema) + && StringUtils.equals(st.getElement(), element) + && StringUtils.equals(st.getQualifier(), qualifier)) + .map(st -> st.getValue()).collect(Collectors.toList()); + } + + private String getFirstValue(Suggestion suggestion, String schema, String element, String qualifier) { + return suggestion.getMetadata().stream() + .filter(st -> StringUtils.isNotBlank(st.getValue()) + && StringUtils.equals(st.getSchema(), schema) + && StringUtils.equals(st.getElement(), element) + && StringUtils.equals(st.getQualifier(), qualifier)) + .map(st -> st.getValue()).findFirst().orElse(null); + } + + @Override + public boolean exist(Suggestion suggestion) throws SolrServerException, IOException { + SolrQuery query = new SolrQuery( + SUGGESTION_FULLID + ":\"" + suggestion.getID() + "\" AND " + PROCESSED + ":true"); + return getSolr().query(query).getResults().getNumFound() == 1; + } + + @Override + public void deleteSuggestion(Suggestion suggestion) throws SolrServerException, IOException { + getSolr().deleteById(suggestion.getID()); + getSolr().commit(); + } + + @Override + public void flagSuggestionAsProcessed(Suggestion suggestion) throws SolrServerException, IOException { + SolrInputDocument sdoc = new SolrInputDocument(); + sdoc.addField(SUGGESTION_FULLID, suggestion.getID()); + Map fieldModifier = new HashMap<>(1); + fieldModifier.put("set", true); + sdoc.addField(PROCESSED, fieldModifier); // add the map as the field value + getSolr().add(sdoc); + getSolr().commit(); + } + + @Override + public void flagAllSuggestionAsProcessed(String source, String idPart) throws SolrServerException, IOException { + SolrQuery query = new SolrQuery(SOURCE + ":" + source + " AND " + SUGGESTION_ID + ":\"" + idPart + "\""); + query.setRows(Integer.MAX_VALUE); + query.setFields(SUGGESTION_FULLID); + SolrDocumentList results = getSolr().query(query).getResults(); + if (results.getNumFound() > 0) { + for (SolrDocument rDoc : results) { + SolrInputDocument sdoc = new SolrInputDocument(); + sdoc.addField(SUGGESTION_FULLID, rDoc.getFieldValue(SUGGESTION_FULLID)); + Map fieldModifier = new HashMap<>(1); + fieldModifier.put("set", true); + sdoc.addField(PROCESSED, fieldModifier); // add the map as the field value + getSolr().add(sdoc); + } + } + getSolr().commit(); + } + + @Override + public void deleteTarget(SuggestionTarget target) throws SolrServerException, IOException { + getSolr().deleteByQuery( + SOURCE + ":" + target.getSource() + " AND " + TARGET_ID + ":" + target.getTarget().getID().toString()); + getSolr().commit(); + } + + @Override + public long countAllTargets(Context context, String source) throws SolrServerException, IOException { + SolrQuery solrQuery = new SolrQuery(); + solrQuery.setRows(0); + solrQuery.setQuery(SOURCE + ":" + source); + solrQuery.addFilterQuery(PROCESSED + ":false"); + solrQuery.setFacet(true); + solrQuery.setFacetMinCount(1); + solrQuery.addFacetField(TARGET_ID); + solrQuery.setFacetLimit(Integer.MAX_VALUE); + QueryResponse response = getSolr().query(solrQuery); + return response.getFacetField(TARGET_ID).getValueCount(); + } + + @Override + public long countUnprocessedSuggestionByTarget(Context context, String source, UUID target) + throws SolrServerException, IOException { + SolrQuery solrQuery = new SolrQuery(); + solrQuery.setRows(0); + solrQuery.setQuery("*:*"); + solrQuery.addFilterQuery( + SOURCE + ":" + source, + TARGET_ID + ":" + target.toString(), + PROCESSED + ":false"); + + QueryResponse response = getSolr().query(solrQuery); + return response.getResults().getNumFound(); + } + + @Override + public List findAllUnprocessedSuggestions(Context context, String source, UUID target, + int pageSize, long offset, boolean ascending) throws SolrServerException, IOException { + + SolrQuery solrQuery = new SolrQuery(); + solrQuery.setRows(pageSize); + solrQuery.setStart((int) offset); + solrQuery.setQuery("*:*"); + solrQuery.addFilterQuery( + SOURCE + ":" + source, + TARGET_ID + ":" + target.toString(), + PROCESSED + ":false"); + + if (ascending) { + solrQuery.addSort(SortClause.asc("trust")); + } else { + solrQuery.addSort(SortClause.desc("trust")); + } + + solrQuery.addSort(SortClause.desc("date")); + solrQuery.addSort(SortClause.asc("title")); + + QueryResponse response = getSolr().query(solrQuery); + List suggestions = new ArrayList(); + for (SolrDocument solrDoc : response.getResults()) { + suggestions.add(convertSolrDoc(context, solrDoc, source)); + } + return suggestions; + + } + + @Override + public List findAllTargets(Context context, String source, int pageSize, long offset) + throws SolrServerException, IOException { + + SolrQuery solrQuery = new SolrQuery(); + solrQuery.setRows(0); + solrQuery.setQuery(SOURCE + ":" + source); + solrQuery.addFilterQuery(PROCESSED + ":false"); + solrQuery.setFacet(true); + solrQuery.setFacetMinCount(1); + solrQuery.addFacetField(TARGET_ID); + solrQuery.setFacetLimit((int) (pageSize + offset)); + QueryResponse response = getSolr().query(solrQuery); + FacetField facetField = response.getFacetField(TARGET_ID); + List suggestionTargets = new ArrayList(); + int idx = 0; + for (Count c : facetField.getValues()) { + if (idx < offset) { + idx++; + continue; + } + SuggestionTarget target = new SuggestionTarget(); + target.setSource(source); + target.setTotal((int) c.getCount()); + target.setTarget(findItem(context, c.getName())); + suggestionTargets.add(target); + idx++; + } + return suggestionTargets; + + } + + @Override + public Suggestion findUnprocessedSuggestion(Context context, String source, UUID target, String id) + throws SolrServerException, IOException { + + SolrQuery solrQuery = new SolrQuery(); + solrQuery.setRows(1); + solrQuery.setQuery("*:*"); + solrQuery.addFilterQuery( + SOURCE + ":" + source, + TARGET_ID + ":" + target.toString(), + SUGGESTION_ID + ":\"" + id + "\"", + PROCESSED + ":false"); + + SolrDocumentList results = getSolr().query(solrQuery).getResults(); + return isEmpty(results) ? null : convertSolrDoc(context, results.get(0), source); + } + + @Override + public SuggestionTarget findTarget(Context context, String source, UUID target) + throws SolrServerException, IOException { + SolrQuery solrQuery = new SolrQuery(); + solrQuery.setRows(0); + solrQuery.setQuery(SOURCE + ":" + source); + solrQuery.addFilterQuery( + TARGET_ID + ":" + target.toString(), + PROCESSED + ":false"); + QueryResponse response = getSolr().query(solrQuery); + SuggestionTarget sTarget = new SuggestionTarget(); + sTarget.setSource(source); + sTarget.setTotal((int) response.getResults().getNumFound()); + Item itemTarget = findItem(context, target); + if (itemTarget != null) { + sTarget.setTarget(itemTarget); + } else { + return null; + } + return sTarget; + } + + private Suggestion convertSolrDoc(Context context, SolrDocument solrDoc, String sourceName) { + Item target = findItem(context, (String) solrDoc.getFieldValue(TARGET_ID)); + + Suggestion suggestion = new Suggestion(sourceName, target, (String) solrDoc.getFieldValue(SUGGESTION_ID)); + suggestion.setDisplay((String) solrDoc.getFieldValue(DISPLAY)); + suggestion.getMetadata() + .add(new MetadataValueDTO("dc", "title", null, null, (String) solrDoc.getFieldValue(TITLE))); + suggestion.getMetadata() + .add(new MetadataValueDTO("dc", "date", "issued", null, (String) solrDoc.getFieldValue(DATE))); + suggestion.getMetadata().add( + new MetadataValueDTO("dc", "description", "abstract", null, (String) solrDoc.getFieldValue(ABSTRACT))); + + suggestion.setExternalSourceUri((String) solrDoc.getFieldValue(EXTERNAL_URI)); + if (solrDoc.containsKey(CATEGORY)) { + for (Object o : solrDoc.getFieldValues(CATEGORY)) { + suggestion.getMetadata().add( + new MetadataValueDTO("dc", "source", null, null, (String) o)); + } + } + if (solrDoc.containsKey(CONTRIBUTORS)) { + for (Object o : solrDoc.getFieldValues(CONTRIBUTORS)) { + suggestion.getMetadata().add( + new MetadataValueDTO("dc", "contributor", "author", null, (String) o)); + } + } + String evidencesJson = (String) solrDoc.getFieldValue(EVIDENCES); + Type listType = new TypeToken>() { + }.getType(); + List evidences = new Gson().fromJson(evidencesJson, listType); + suggestion.getEvidences().addAll(evidences); + return suggestion; + } + + private Item findItem(Context context, UUID itemId) { + try { + return itemService.find(context, itemId); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private Item findItem(Context context, String itemId) { + return findItem(context, UUIDUtils.fromString(itemId)); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/Suggestion.java b/dspace-api/src/main/java/org/dspace/app/suggestion/Suggestion.java new file mode 100644 index 000000000000..3629b508ab93 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/Suggestion.java @@ -0,0 +1,89 @@ +/** + * 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.suggestion; + +import java.util.LinkedList; +import java.util.List; + +import org.dspace.content.Item; +import org.dspace.content.dto.MetadataValueDTO; + +/** + * + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +public class Suggestion { + + private String id; + + private String display; + + private String source; + + private String externalSourceUri; + + private Item target; + + private List evidences = new LinkedList(); + + private List metadata = new LinkedList(); + + public Suggestion(String source, Item target, String idPart) { + this.source = source; + this.target = target; + this.id = source + ":" + target.getID().toString() + ":" + idPart; + } + + public String getDisplay() { + return display; + } + + public void setDisplay(String display) { + this.display = display; + } + + public String getSource() { + return source; + } + + public String getExternalSourceUri() { + return externalSourceUri; + } + + public void setExternalSourceUri(String externalSourceUri) { + this.externalSourceUri = externalSourceUri; + } + + public List getEvidences() { + return evidences; + } + + public List getMetadata() { + return metadata; + } + + public Item getTarget() { + return target; + } + + public String getID() { + return id; + } + + public Double getScore() { + if (evidences != null && evidences.size() > 0) { + double score = 0; + for (SuggestionEvidence evidence : evidences) { + score += evidence.getScore(); + } + return score; + } + return null; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java new file mode 100644 index 000000000000..d1129837bc9a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java @@ -0,0 +1,56 @@ +/** + * 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.suggestion; + +/** + * + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +public class SuggestionEvidence { + + private String name; + + private double score; + + private String notes; + + public SuggestionEvidence() { + } + + public SuggestionEvidence(String name, double score, String notes) { + this.name = name; + this.score = score; + this.notes = notes; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getScore() { + return score; + } + + public void setScore(double score) { + this.score = score; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionProvider.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionProvider.java new file mode 100644 index 000000000000..c7ae8e8025fc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionProvider.java @@ -0,0 +1,34 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.suggestion; + +import java.util.List; +import java.util.UUID; + +import org.dspace.core.Context; +import org.dspace.external.model.ExternalDataObject; + +public interface SuggestionProvider { + public List findAllTargets(Context context, int pageSize, long offset); + + public long countAllTargets(Context context); + + public SuggestionTarget findTarget(Context context, UUID target); + + public List findAllUnprocessedSuggestions(Context context, UUID target, int pageSize, long offset, + boolean ascending); + + public long countUnprocessedSuggestionByTarget(Context context, UUID target); + + public Suggestion findUnprocessedSuggestion(Context context, UUID target, String id); + + public void rejectSuggestion(Context context, UUID target, String idPart); + + public void flagRelatedSuggestionsAsProcessed(Context context, ExternalDataObject externalDataObject); + +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionService.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionService.java new file mode 100644 index 000000000000..c52a9bb41c01 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionService.java @@ -0,0 +1,48 @@ +/** + * 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.suggestion; + +import java.util.List; +import java.util.UUID; + +import org.dspace.core.Context; + +/** + * + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +public interface SuggestionService { + + public SuggestionTarget find(Context context, String source, UUID id); + + public long countAll(Context context, String source); + + public List findAllTargets(Context context, String source, int pageSize, long offset); + + public long countAllByTarget(Context context, UUID target); + + public List findByTarget(Context context, UUID target, int pageSize, long offset); + + public SuggestionSource findSource(Context context, String source); + + public long countSources(Context context); + + public List findAllSources(Context context, int pageSize, long offset); + + public Suggestion findUnprocessedSuggestion(Context context, String id); + + public void rejectSuggestion(Context context, String id); + + public List findByTargetAndSource(Context context, UUID target, String source, int pageSize, + long offset, boolean ascending); + + public long countAllByTargetAndSource(Context context, String source, UUID target); + + public List getSuggestionProviders(); +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java new file mode 100644 index 000000000000..4ac804ddaf84 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java @@ -0,0 +1,191 @@ +/** + * 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.suggestion; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import javax.annotation.Resource; + +import org.apache.logging.log4j.Logger; +import org.dspace.core.Context; +import org.springframework.stereotype.Service; + +@Service +public class SuggestionServiceImpl implements SuggestionService { + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SuggestionServiceImpl.class); + + @Resource(name = "suggestionProviders") + private Map providersMap; + + @Override + public List getSuggestionProviders() { + if (providersMap != null) { + return providersMap.values().stream().collect(Collectors.toList()); + } + return null; + } + + @Override + public SuggestionTarget find(Context context, String source, UUID id) { + if (providersMap.containsKey(source)) { + return providersMap.get(source).findTarget(context, id); + } else { + return null; + } + } + + @Override + public long countAll(Context context, String source) { + if (providersMap.containsKey(source)) { + return providersMap.get(source).countAllTargets(context); + } else { + return 0; + } + } + + @Override + public List findAllTargets(Context context, String source, int pageSize, long offset) { + if (providersMap.containsKey(source)) { + return providersMap.get(source).findAllTargets(context, pageSize, offset); + } else { + return null; + } + } + + @Override + public long countAllByTarget(Context context, UUID target) { + int count = 0; + for (String provider : providersMap.keySet()) { + if (providersMap.get(provider).countUnprocessedSuggestionByTarget(context, target) > 0) { + count++; + } + } + return count; + } + + @Override + public List findByTarget(Context context, UUID target, int pageSize, long offset) { + List fullSourceTargets = new ArrayList(); + for (String source : providersMap.keySet()) { + SuggestionTarget sTarget = providersMap.get(source).findTarget(context, target); + if (sTarget != null && sTarget.getTotal() > 0) { + fullSourceTargets.add(sTarget); + } + } + fullSourceTargets.sort(new Comparator() { + @Override + public int compare(SuggestionTarget arg0, SuggestionTarget arg1) { + return -(arg0.getTotal() - arg1.getTotal()); + } + } + ); + return fullSourceTargets.stream().skip(offset).limit(pageSize).collect(Collectors.toList()); + } + + @Override + public long countSources(Context context) { + return providersMap.size(); + } + + @Override + public SuggestionSource findSource(Context context, String source) { + if (providersMap.containsKey(source)) { + SuggestionSource ssource = new SuggestionSource(source); + ssource.setTotal((int) providersMap.get(source).countAllTargets(context)); + return ssource; + } else { + return null; + } + } + + @Override + public List findAllSources(Context context, int pageSize, long offset) { + List fullSources = getSources(context).stream().skip(offset).limit(pageSize) + .collect(Collectors.toList()); + return fullSources; + } + + private List getSources(Context context) { + List results = new ArrayList(); + for (String source : providersMap.keySet()) { + SuggestionSource ssource = new SuggestionSource(source); + ssource.setTotal((int) providersMap.get(source).countAllTargets(context)); + results.add(ssource); + } + return results; + } + + @Override + public long countAllByTargetAndSource(Context context, String source, UUID target) { + if (providersMap.containsKey(source)) { + return providersMap.get(source).countUnprocessedSuggestionByTarget(context, target); + } + return 0; + } + + @Override + public List findByTargetAndSource(Context context, UUID target, String source, int pageSize, + long offset, boolean ascending) { + if (providersMap.containsKey(source)) { + return providersMap.get(source).findAllUnprocessedSuggestions(context, target, pageSize, offset, ascending); + } + return null; + } + + @Override + public Suggestion findUnprocessedSuggestion(Context context, String id) { + String source = null; + UUID target = null; + String idPart = null; + String[] split; + try { + split = id.split(":", 3); + source = split[0]; + target = UUID.fromString(split[1]); + idPart = split[2]; + } catch (Exception e) { + log.warn("findSuggestion got an invalid id " + id + ", return null"); + return null; + } + if (split.length != 3) { + return null; + } + if (providersMap.containsKey(source)) { + return providersMap.get(source).findUnprocessedSuggestion(context, target, idPart); + } + return null; + } + + @Override + public void rejectSuggestion(Context context, String id) { + String source = null; + UUID target = null; + String idPart = null; + String[] split; + try { + split = id.split(":", 3); + source = split[0]; + target = UUID.fromString(split[1]); + idPart = split[2]; + } catch (Exception e) { + log.warn("rejectSuggestion got an invalid id " + id + ", doing nothing"); + return; + } + if (split.length != 3) { + return; + } + if (providersMap.containsKey(source)) { + providersMap.get(source).rejectSuggestion(context, target, idPart); + } + + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java new file mode 100644 index 000000000000..b9df687dec32 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java @@ -0,0 +1,46 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.suggestion; + +/** + * + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +public class SuggestionSource { + + private String name; + + private int total; + + public SuggestionSource() { + } + + /** + * Summarize the available suggestions from a source. + * + * @param the name must be not null + */ + public SuggestionSource(String name) { + super(); + this.name = name; + } + + public String getID() { + return name; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java new file mode 100644 index 000000000000..985d398d712a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java @@ -0,0 +1,71 @@ +/** + * 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.suggestion; + +import org.dspace.content.Item; + +/** + * + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +public class SuggestionTarget { + + private Item target; + + private String source; + + private int total; + + public SuggestionTarget() { + } + + /** + * Wrap a target person into a suggestion target. + * + * @param item must be not null + */ + public SuggestionTarget(Item item) { + super(); + this.target = item; + } + + /** + * The suggestion target uses the concatenation of the source and target uuid separated by colon as id + * + * @return the source:uuid of the wrapped item + */ + public String getID() { + return source + ":" + target.getID(); + } + + public Item getTarget() { + return target; + } + + public void setTarget(Item target) { + this.target = target; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionUtils.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionUtils.java new file mode 100644 index 000000000000..30ced75fc914 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionUtils.java @@ -0,0 +1,111 @@ +/** + * 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.suggestion; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.external.model.ExternalDataObject; + +/** + * This utility class provides convenient methods to deal with the + * {@link ExternalDataObject} for the purpose of the Suggestion framework + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +public class SuggestionUtils { + private SuggestionUtils() { + } + /** + * This method receive an ExternalDataObject and a metadatum key. + * It return only the values of the Metadata associated with the key. + * + * @param record the ExternalDataObject to extract metadata from + * @param schema schema of the searching record + * @param element element of the searching record + * @param qualifier qualifier of the searching record + * @return value of the first matching record + */ + public static List getAllEntriesByMetadatum(ExternalDataObject record, String schema, String element, + String qualifier) { + return record.getMetadata().stream() + .filter(x -> + StringUtils.equals(x.getSchema(), schema) + && StringUtils.equals(x.getElement(), element) + && StringUtils.equals(x.getQualifier(), qualifier)) + .map(x -> x.getValue()).collect(Collectors.toList()); + } + + /** + * This method receive an ExternalDataObject and a metadatum key. + * It return only the values of the Metadata associated with the key. + * + * @param record the ExternalDataObject to extract metadata from + * @param metadataFieldKey the metadata field key (i.e. dc.title or dc.contributor.author), + * the jolly char is not supported + * @return value of the first matching record + */ + public static List getAllEntriesByMetadatum(ExternalDataObject record, String metadataFieldKey) { + if (metadataFieldKey == null) { + return Collections.EMPTY_LIST; + } + String[] fields = metadataFieldKey.split("\\."); + String schema = fields[0]; + String element = fields[1]; + String qualifier = null; + if (fields.length == 3) { + qualifier = fields[2]; + } + return getAllEntriesByMetadatum(record, schema, element, qualifier); + } + + /** + * This method receive and ExternalDataObject and a metadatum key. + * It return only the value of the first Metadatum from the list associated with the key. + * + * @param record the ExternalDataObject to extract metadata from + * @param schema schema of the searching record + * @param element element of the searching record + * @param qualifier qualifier of the searching record + * @return value of the first matching record + */ + public static String getFirstEntryByMetadatum(ExternalDataObject record, String schema, String element, + String qualifier) { + return record.getMetadata().stream() + .filter(x -> + StringUtils.equals(x.getSchema(), schema) + && StringUtils.equals(x.getElement(), element) + && StringUtils.equals(x.getQualifier(), qualifier)) + .map(x -> x.getValue()).findFirst().orElse(null); + } + + /** + * This method receive and ExternalDataObject and a metadatum key. + * It return only the value of the first Metadatum from the list associated with the key. + * + * @param record the ExternalDataObject to extract metadata from + * @param metadataFieldKey the metadata field key (i.e. dc.title or dc.contributor.author), + * the jolly char is not supported + * @return value of the first matching record + */ + public static String getFirstEntryByMetadatum(ExternalDataObject record, String metadataFieldKey) { + if (metadataFieldKey == null) { + return null; + } + String[] fields = metadataFieldKey.split("\\."); + String schema = fields[0]; + String element = fields[1]; + String qualifier = null; + if (fields.length == 3) { + qualifier = fields[2]; + } + return getFirstEntryByMetadatum(record, schema, element, qualifier); + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/AuthorNamesScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/AuthorNamesScorer.java new file mode 100644 index 000000000000..f429ae017ce9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/AuthorNamesScorer.java @@ -0,0 +1,147 @@ +/** + * 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.suggestion.oaire; + +import static org.dspace.app.suggestion.SuggestionUtils.getAllEntriesByMetadatum; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.ibm.icu.text.CharsetDetector; +import com.ibm.icu.text.CharsetMatch; +import com.ibm.icu.text.Normalizer; +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.suggestion.SuggestionEvidence; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.dspace.external.model.ExternalDataObject; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@see org.dspace.app.suggestion.oaire.EvidenceScorer} which evaluate ImportRecords + * based on Author's name. + * + * @author Andrea Bollini (andrea.bollini at 4science dot it) + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + * + */ +public class AuthorNamesScorer implements EvidenceScorer { + + private List contributorMetadata; + + private List names; + + @Autowired + private ItemService itemService; + + /** + * returns the metadata key of the Item which to base the filter on + * @return metadata key + */ + public List getContributorMetadata() { + return contributorMetadata; + } + + /** + * set the metadata key of the Item which to base the filter on + * @return metadata key + */ + public void setContributorMetadata(List contributorMetadata) { + this.contributorMetadata = contributorMetadata; + } + + /** + * return the metadata key of ImportRecord which to base the filter on + * @return + */ + public List getNames() { + return names; + } + + /** + * set the metadata key of ImportRecord which to base the filter on + */ + public void setNames(List names) { + this.names = names; + } + + /** + * Method which is responsible to evaluate ImportRecord based on authors name. + * This method extract the researcher name from Item using contributorMetadata fields + * and try to match them with values extract from ImportRecord using metadata keys defined + * in names. + * ImportRecords which don't match will be discarded. + * + * @param importRecord the import record to check + * @param researcher DSpace item + * @return the generated evidence or null if the record must be discarded + */ + @Override + public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecord) { + List names = searchMetadataValues(researcher); + int maxNameLenght = names.stream().mapToInt(n -> n[0].length()).max().orElse(1); + List metadataAuthors = new ArrayList<>(); + for (String contributorMetadatum : contributorMetadata) { + metadataAuthors.addAll(getAllEntriesByMetadatum(importRecord, contributorMetadatum)); + } + List normalizedMetadataAuthors = metadataAuthors.stream().map(x -> normalize(x)) + .collect(Collectors.toList()); + int idx = 0; + for (String nMetadataAuthor : normalizedMetadataAuthors) { + Optional found = names.stream() + .filter(a -> StringUtils.equalsIgnoreCase(a[0], nMetadataAuthor)).findFirst(); + if (found.isPresent()) { + return new SuggestionEvidence(this.getClass().getSimpleName(), + 100 * ((double) nMetadataAuthor.length() / (double) maxNameLenght), + "The author " + metadataAuthors.get(idx) + " at position " + (idx + 1) + + " in the authors list matches the name " + found.get()[1] + + " in the researcher profile"); + } + idx++; + } + return null; + } + + /** + * Return list of Item metadata values starting from metadata keys defined in class level variable names. + * + * @param researcher DSpace item + * @return list of metadata values + */ + private List searchMetadataValues(Item researcher) { + List authors = new ArrayList(); + for (String name : names) { + List values = itemService.getMetadataByMetadataString(researcher, name); + if (values != null) { + for (MetadataValue v : values) { + authors.add(new String[] {normalize(v.getValue()), v.getValue()}); + } + } + } + return authors; + } + + private String normalize(String value) { + String norm = Normalizer.normalize(value, Normalizer.NFD); + CharsetDetector cd = new CharsetDetector(); + cd.setText(value.getBytes()); + CharsetMatch detect = cd.detect(); + if (detect != null && detect.getLanguage() != null) { + norm = norm.replaceAll("[^\\p{L}]", " ").toLowerCase(new Locale(detect.getLanguage())); + } else { + norm = norm.replaceAll("[^\\p{L}]", " ").toLowerCase(); + } + return Arrays.asList(norm.split("\\s+")).stream().sorted().collect(Collectors.joining()); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/DateScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/DateScorer.java new file mode 100644 index 000000000000..2a5f37e12a6b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/DateScorer.java @@ -0,0 +1,200 @@ +/** + * 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.suggestion.oaire; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; + +import org.dspace.app.suggestion.SuggestionEvidence; +import org.dspace.app.suggestion.SuggestionUtils; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.dspace.external.model.ExternalDataObject; +import org.dspace.util.MultiFormatDateParser; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@see org.dspace.app.suggestion.oaire.EvidenceScorer} which evaluate ImportRecords + * based on the distance from a date extracted from the ResearcherProfile (birthday / graduation date) + * + * @author Andrea Bollini (andrea.bollini at 4science dot it) + * + */ +public class DateScorer implements EvidenceScorer { + + private String birthDateMetadata; + + private String educationDateMetadata; + + private String minDateMetadata; + + private String maxDateMetadata; + + private int birthDateDelta = 20; + private int birthDateRange = 50; + + private int educationDateDelta = -3; + private int educationDateRange = 50; + + @Autowired + private ItemService itemService; + + private String publicationDateMetadata; + + public void setItemService(ItemService itemService) { + this.itemService = itemService; + } + + public void setBirthDateMetadata(String birthDate) { + this.birthDateMetadata = birthDate; + } + + public String getBirthDateMetadata() { + return birthDateMetadata; + } + + public void setEducationDateMetadata(String educationDate) { + this.educationDateMetadata = educationDate; + } + + public String getEducationDateMetadata() { + return educationDateMetadata; + } + + public void setBirthDateDelta(int birthDateDelta) { + this.birthDateDelta = birthDateDelta; + } + + public void setBirthDateRange(int birthDateRange) { + this.birthDateRange = birthDateRange; + } + + public void setEducationDateDelta(int educationDateDelta) { + this.educationDateDelta = educationDateDelta; + } + + public void setEducationDateRange(int educationDateRange) { + this.educationDateRange = educationDateRange; + } + + public void setMaxDateMetadata(String maxDateMetadata) { + this.maxDateMetadata = maxDateMetadata; + } + + public void setMinDateMetadata(String minDateMetadata) { + this.minDateMetadata = minDateMetadata; + } + + public void setPublicationDateMetadata(String publicationDateMetadata) { + this.publicationDateMetadata = publicationDateMetadata; + } + + /** + * Method which is responsible to evaluate ImportRecord based on the publication date. + * ImportRecords which have a date outside the defined or calculated expected range will be discarded. + * + * @param importRecord the ExternalDataObject to check + * @param researcher DSpace item + * @return the generated evidence or null if the record must be discarded + */ + @Override + public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecord) { + Integer[] range = calculateRange(researcher); + if (range == null) { + return new SuggestionEvidence(this.getClass().getSimpleName(), + 0, + "No assumption was possible about the publication year range. " + + "Please consider to set a min/max date in the profile, specify the birthday " + + "or education achievements"); + } else { + String optDate = SuggestionUtils.getFirstEntryByMetadatum(importRecord, publicationDateMetadata); + int year = getYear(optDate); + if (year > 0) { + if ((range[0] == null || year >= range[0]) && + (range[1] == null || year <= range[1])) { + return new SuggestionEvidence(this.getClass().getSimpleName(), + 10, + "The publication date is within the expected range [" + range[0] + ", " + + range[1] + "]"); + } else { + // outside the range, discard the suggestion + return null; + } + } else { + return new SuggestionEvidence(this.getClass().getSimpleName(), + 0, + "No assumption was possible as the publication date is " + (optDate != null + ? "unprocessable [" + optDate + "]" + : "unknown")); + } + } + } + + private Integer[] calculateRange(Item researcher) { + String minDateStr = getSingleValue(researcher, minDateMetadata); + int minYear = getYear(minDateStr); + String maxDateStr = getSingleValue(researcher, maxDateMetadata); + int maxYear = getYear(maxDateStr); + if (minYear > 0 && maxYear > 0) { + return new Integer[] { minYear, maxYear }; + } else { + String birthDateStr = getSingleValue(researcher, birthDateMetadata); + int birthDateYear = getYear(birthDateStr); + int educationDateYear = getListMetadataValues(researcher, educationDateMetadata) + .stream() + .mapToInt(x -> getYear(x.getValue())) + .filter(d -> d > 0) + .min().orElse(-1); + if (educationDateYear > 0) { + return new Integer[] { + minYear > 0 ? minYear : educationDateYear + educationDateDelta, + maxYear > 0 ? maxYear : educationDateYear + educationDateDelta + educationDateRange + }; + } else if (birthDateYear > 0) { + return new Integer[] { + minYear > 0 ? minYear : birthDateYear + birthDateDelta, + maxYear > 0 ? maxYear : birthDateYear + birthDateDelta + birthDateRange + }; + } else { + return null; + } + } + } + + private List getListMetadataValues(Item researcher, String metadataKey) { + if (metadataKey != null) { + return itemService.getMetadataByMetadataString(researcher, metadataKey); + } else { + return Collections.EMPTY_LIST; + } + } + + private String getSingleValue(Item researcher, String metadataKey) { + if (metadataKey != null) { + return itemService.getMetadata(researcher, metadataKey); + } + return null; + } + + private int getYear(String birthDateStr) { + int birthDateYear = -1; + if (birthDateStr != null) { + Date birthDate = MultiFormatDateParser.parse(birthDateStr); + if (birthDate != null) { + Calendar calendar = new GregorianCalendar(); + calendar.setTime(birthDate); + birthDateYear = calendar.get(Calendar.YEAR); + } + } + return birthDateYear; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/EvidenceScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/EvidenceScorer.java new file mode 100644 index 000000000000..9df7621b46bb --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/EvidenceScorer.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.suggestion.oaire; + +import org.dspace.app.suggestion.SuggestionEvidence; +import org.dspace.content.Item; +import org.dspace.external.model.ExternalDataObject; + +/** + * Interface used in {@see org.dspace.app.suggestion.oaire.OAIREPublicationApproverServiceImpl} + * to construct filtering pipeline. + * + * For each EvidenceScorer, the service call computeEvidence method. + * + * @author Andrea Bollini (andrea.bollini at 4science dot it) + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + * + */ +public interface EvidenceScorer { + + /** + * Method to compute the suggestion evidence of an ImportRecord, a null evidence + * would lead the record to be discarded. + * + * @param researcher DSpace item + * @param importRecord the record to evaluate + * @return the generated suggestion evidence or null if the record should be + * discarded + */ + public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecords); + +} diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/OAIREPublicationLoader.java b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/OAIREPublicationLoader.java new file mode 100644 index 000000000000..d8a20ed3a8bd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/OAIREPublicationLoader.java @@ -0,0 +1,240 @@ +/** + * 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.suggestion.oaire; + +import static org.dspace.app.suggestion.SuggestionUtils.getAllEntriesByMetadatum; +import static org.dspace.app.suggestion.SuggestionUtils.getFirstEntryByMetadatum; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.solr.client.solrj.SolrServerException; +import org.dspace.app.suggestion.SolrSuggestionProvider; +import org.dspace.app.suggestion.Suggestion; +import org.dspace.app.suggestion.SuggestionEvidence; +import org.dspace.content.Item; +import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.core.Context; +import org.dspace.external.model.ExternalDataObject; +import org.dspace.external.provider.ExternalDataProvider; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Class responsible to load and manage ImportRecords from OpenAIRE + * + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + * + */ +public class OAIREPublicationLoader extends SolrSuggestionProvider { + + private List names; + + private ExternalDataProvider primaryProvider; + + private List otherProviders; + + @Autowired + private ConfigurationService configurationService; + + private List pipeline; + + public void setPrimaryProvider(ExternalDataProvider primaryProvider) { + this.primaryProvider = primaryProvider; + } + + public void setOtherProviders(List otherProviders) { + this.otherProviders = otherProviders; + } + + /** + * Set the pipeline of Approver + * @param pipeline list Approver + */ + public void setPipeline(List pipeline) { + this.pipeline = pipeline; + } + + /** + * This method filter a list of ImportRecords using a pipeline of AuthorNamesApprover + * and return a filtered list of ImportRecords. + * + * @see org.dspace.app.suggestion.oaire.AuthorNamesScorer + * @param researcher the researcher Item + * @param importRecords List of import record + * @return a list of filtered import records + */ + public List reduceAndTransform(Item researcher, List importRecords) { + List results = new ArrayList<>(); + for (ExternalDataObject r : importRecords) { + boolean skip = false; + List evidences = new ArrayList(); + for (EvidenceScorer authorNameApprover : pipeline) { + SuggestionEvidence evidence = authorNameApprover.computeEvidence(researcher, r); + if (evidence != null) { + evidences.add(evidence); + } else { + skip = true; + break; + } + } + if (!skip) { + Suggestion suggestion = translateImportRecordToSuggestion(researcher, r); + suggestion.getEvidences().addAll(evidences); + results.add(suggestion); + } + } + return results; + } + + /** + * Save a List of ImportRecord into Solr. + * ImportRecord will be translate into a SolrDocument by the method translateImportRecordToSolrDocument. + * + * @param context the DSpace Context + * @param researcher a DSpace Item + * @throws SolrServerException + * @throws IOException + */ + public void importAuthorRecords(Context context, Item researcher) + throws SolrServerException, IOException { + List metadata = getImportRecords(researcher); + List records = reduceAndTransform(researcher, metadata); + for (Suggestion record : records) { + solrSuggestionStorageService.addSuggestion(record, false, false); + } + solrSuggestionStorageService.commit(); + } + + /** + * Translate an ImportRecord into a Suggestion + * @param item DSpace item + * @param record ImportRecord + * @return Suggestion + */ + private Suggestion translateImportRecordToSuggestion(Item item, ExternalDataObject record) { + String openAireId = record.getId(); + Suggestion suggestion = new Suggestion(getSourceName(), item, openAireId); + suggestion.setDisplay(getFirstEntryByMetadatum(record, "dc", "title", null)); + suggestion.getMetadata().add( + new MetadataValueDTO("dc", "title", null, null, getFirstEntryByMetadatum(record, "dc", "title", null))); + suggestion.getMetadata().add(new MetadataValueDTO("dc", "date", "issued", null, + getFirstEntryByMetadatum(record, "dc", "date", "issued"))); + suggestion.getMetadata().add(new MetadataValueDTO("dc", "description", "abstract", null, + getFirstEntryByMetadatum(record, "dc", "description", "abstract"))); + suggestion.setExternalSourceUri(configurationService.getProperty("dspace.server.url") + + "/api/integration/externalsources/" + primaryProvider.getSourceIdentifier() + "/entryValues/" + + openAireId); + for (String o : getAllEntriesByMetadatum(record, "dc", "source", null)) { + suggestion.getMetadata().add(new MetadataValueDTO("dc", "source", null, null, o)); + } + for (String o : getAllEntriesByMetadatum(record, "dc", "contributor", "author")) { + suggestion.getMetadata().add(new MetadataValueDTO("dc", "contributor", "author", null, o)); + } + return suggestion; + } + + public List getNames() { + return names; + } + + public void setNames(List names) { + this.names = names; + } + + /** + * Load metadata from OpenAIRE using the import service. The service use the value + * get from metadata key defined in class level variable names as author to query OpenAIRE. + * + * @see org.dspace.importer.external.openaire.service.OpenAireImportMetadataSourceServiceImpl + * @param researcher item to extract metadata from + * @return list of ImportRecord + */ + private List getImportRecords(Item researcher) { + List searchValues = searchMetadataValues(researcher); + List matchingRecords = new ArrayList<>(); + for (String searchValue : searchValues) { + matchingRecords.addAll(primaryProvider.searchExternalDataObjects(searchValue, 0, 9999)); + } + List toReturn = removeDuplicates(matchingRecords); + return toReturn; + } + + /** + * This method remove duplicates from importRecords list. + * An element is a duplicate if in the list exist another element + * with the same value of the metadatum 'dc.identifier.other' + * + * @param importRecords list of ImportRecord + * @return list of ImportRecords without duplicates + */ + private List removeDuplicates(List importRecords) { + List filteredRecords = new ArrayList<>(); + for (ExternalDataObject currentRecord : importRecords) { + if (!isDuplicate(currentRecord, filteredRecords)) { + filteredRecords.add(currentRecord); + } + } + return filteredRecords; + } + + + /** + * Check if the ImportRecord is already present in the list. + * The comparison is made on the value of metadatum with key 'dc.identifier.other' + * + * @param dto An importRecord instance + * @param importRecords a list of importRecord + * @return true if dto is already present in importRecords, false otherwise + */ + private boolean isDuplicate(ExternalDataObject dto, List importRecords) { + String currentItemId = dto.getId(); + if (currentItemId == null) { + return true; + } + for (ExternalDataObject importRecord : importRecords) { + if (currentItemId.equals(importRecord.getId())) { + return true; + } + } + return false; + } + + + /** + * Return list of Item metadata values starting from metadata keys defined in class level variable names. + * + * @param researcher DSpace item + * @return list of metadata values + */ + private List searchMetadataValues(Item researcher) { + List authors = new ArrayList(); + for (String name : names) { + String value = itemService.getMetadata(researcher, name); + if (value != null) { + authors.add(value); + } + } + return authors; + } + + @Override + protected boolean isExternalDataObjectPotentiallySuggested(Context context, ExternalDataObject externalDataObject) { + if (StringUtils.equals(externalDataObject.getSource(), primaryProvider.getSourceIdentifier())) { + return true; + } else if (otherProviders != null) { + return otherProviders.stream() + .anyMatch(x -> StringUtils.equals(externalDataObject.getSource(), x.getSourceIdentifier())); + } else { + return false; + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java b/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java index f91ea00cac4a..59cbe4f9d087 100644 --- a/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/external/service/impl/ExternalDataServiceImpl.java @@ -13,6 +13,8 @@ import java.util.stream.Collectors; import org.apache.logging.log4j.Logger; +import org.dspace.app.suggestion.SuggestionProvider; +import org.dspace.app.suggestion.SuggestionService; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Item; @@ -44,6 +46,9 @@ public class ExternalDataServiceImpl implements ExternalDataService { @Autowired private WorkspaceItemService workspaceItemService; + @Autowired + private SuggestionService suggestionService; + @Override public Optional getExternalDataObject(String source, String id) { ExternalDataProvider provider = getExternalDataProvider(source); @@ -105,6 +110,16 @@ public WorkspaceItem createWorkspaceItemFromExternalDataObject(Context context, log.info(LogHelper.getHeader(context, "create_item_from_externalDataObject", "Created item" + "with id: " + item.getID() + " from source: " + externalDataObject.getSource() + " with identifier: " + externalDataObject.getId())); + try { + List providers = suggestionService.getSuggestionProviders(); + if (providers != null) { + for (SuggestionProvider p : providers) { + p.flagRelatedSuggestionsAsProcessed(context, externalDataObject); + } + } + } catch (Exception e) { + log.error("Got problems with the solr suggestion storage service: " + e.getMessage(), e); + } return workspaceItem; } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathDateFormatMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathDateFormatMetadataContributor.java new file mode 100644 index 000000000000..bbb4e7311ee4 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathDateFormatMetadataContributor.java @@ -0,0 +1,84 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.metadatamapping.contributor; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import org.apache.axiom.om.OMAttribute; +import org.apache.axiom.om.OMElement; +import org.apache.axiom.om.OMText; +import org.apache.axiom.om.xpath.AXIOMXPath; +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.jaxen.JaxenException; + + +public class SimpleXpathDateFormatMetadataContributor extends SimpleXpathMetadatumContributor { + + + private DateFormat dateFormatFrom; + private DateFormat dateFormatTo; + + public void setDateFormatFrom(String dateFormatFrom) { + this.dateFormatFrom = new SimpleDateFormat(dateFormatFrom); + } + + public void setDateFormatTo(String dateFormatTo) { + this.dateFormatTo = new SimpleDateFormat(dateFormatTo); + } + + @Override + public Collection contributeMetadata(OMElement t) { + List values = new LinkedList<>(); + try { + AXIOMXPath xpath = new AXIOMXPath(query); + for (String ns : prefixToNamespaceMapping.keySet()) { + xpath.addNamespace(prefixToNamespaceMapping.get(ns), ns); + } + List nodes = xpath.selectNodes(t); + for (Object el : nodes) { + if (el instanceof OMElement) { + values.add(getMetadatum(field, ((OMElement) el).getText())); + } else if (el instanceof OMAttribute) { + values.add(getMetadatum(field, ((OMAttribute) el).getAttributeValue())); + } else if (el instanceof String) { + values.add(getMetadatum(field, (String) el)); + } else if (el instanceof OMText) { + values.add(metadataFieldMapping.toDCValue(field, ((OMText) el).getText())); + } else { + System.err.println("node of type: " + el.getClass()); + } + } + return values; + } catch (JaxenException e) { + System.err.println(query); + throw new RuntimeException(e); + } + } + + private MetadatumDTO getMetadatum(MetadataFieldConfig field, String value) { + MetadatumDTO dcValue = new MetadatumDTO(); + if (field == null) { + return null; + } + try { + dcValue.setValue(dateFormatTo.format(dateFormatFrom.parse(value))); + } catch (ParseException e) { + dcValue.setValue(value); + } + dcValue.setElement(field.getElement()); + dcValue.setQualifier(field.getQualifier()); + dcValue.setSchema(field.getSchema()); + return dcValue; + } +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java index 87cdbfa6ed04..8b4c959543bc 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleXpathMetadatumContributor.java @@ -31,7 +31,7 @@ * @author Roeland Dillen (roeland at atmire dot com) */ public class SimpleXpathMetadatumContributor implements MetadataContributor { - private MetadataFieldConfig field; + protected MetadataFieldConfig field; private static final Logger log = LoggerFactory.getLogger(SimpleXpathMetadatumContributor.class); @@ -44,7 +44,7 @@ public Map getPrefixToNamespaceMapping() { return prefixToNamespaceMapping; } - private MetadataFieldMapping> metadataFieldMapping; + protected MetadataFieldMapping> metadataFieldMapping; /** * Return metadataFieldMapping @@ -76,7 +76,7 @@ public void setPrefixToNamespaceMapping(Map prefixToNamespaceMap this.prefixToNamespaceMapping = prefixToNamespaceMapping; } - private Map prefixToNamespaceMapping; + protected Map prefixToNamespaceMapping; /** * Initialize SimpleXpathMetadatumContributor with a query, prefixToNamespaceMapping and MetadataFieldConfig @@ -100,7 +100,7 @@ public SimpleXpathMetadatumContributor() { } - private String query; + protected String query; /** * Return the MetadataFieldConfig used while retrieving MetadatumDTO diff --git a/dspace-api/src/main/java/org/dspace/importer/external/openaire/metadatamapping/OpenAIREPublicationFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/openaire/metadatamapping/OpenAIREPublicationFieldMapping.java new file mode 100644 index 000000000000..d58ffc8ca9d5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/openaire/metadatamapping/OpenAIREPublicationFieldMapping.java @@ -0,0 +1,29 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.openaire.metadatamapping; + +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + +/** + * An implementation of {@link AbstractMetadataFieldMapping} responsible for + * defining the mapping of the OpenAIRE metadatum fields on the DSpace metadatum + * fields + * + * @author Mykhaylo Boychuk (4science.it) + */ +public class OpenAIREPublicationFieldMapping extends AbstractMetadataFieldMapping { + + @Override + @Resource(name = "openairePublicationsMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..2bae0aa27533 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java @@ -0,0 +1,337 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.openaire.service; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import javax.el.MethodNotFoundException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.apache.axiom.om.OMElement; +import org.apache.axiom.om.OMXMLBuilderFactory; +import org.apache.axiom.om.OMXMLParserWrapper; +import org.apache.axiom.om.xpath.AXIOMXPath; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.components.QuerySource; +import org.jaxen.JaxenException; + +/** + * Implements a data source for querying OpenAIRE + * + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + */ +public class OpenAireImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private String baseAddress; + + private WebTarget webTarget; + + private String queryParam; + + @Override + public String getImportSource() { + return "openaire"; + } + + /** + * The string that identifies this import implementation. Preferable a URI + * + * @return the identifying uri + */ + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + return retry(new SearchByIdCallable(id)); + } + + /** + * The string that identifies this import implementation. Preferable a URI + * + * @return the identifying uri + */ + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + return retry(new SearchByIdCallable(query)); + } + + + /** + * Find the number of records matching a query; + * + * @param query a query string to base the search on. + * @return the sum of the matching records over this import source + * @throws MetadataSourceException if the underlying methods throw any exception. + */ + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + /** + * Find the number of records matching a query; + * + * @param query a query object to base the search on. + * @return the sum of the matching records over this import source + * @throws MetadataSourceException if the underlying methods throw any exception. + */ + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + /** + * Find the number of records matching a string query. Supports pagination + * + * @param query a query string to base the search on. + * @param start offset to start at + * @param count number of records to retrieve. + * @return a set of records. Fully transformed. + * @throws MetadataSourceException if the underlying methods throw any exception. + */ + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query, start, count)); + } + + + /** + * Find records based on a object query. + * + * @param query a query object to base the search on. + * @return a set of records. Fully transformed. + * @throws MetadataSourceException if the underlying methods throw any exception. + */ + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query)); + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for OpenAIRE"); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for OpenAIRE"); + } + + /** + * Set the baseAddress to this object + * + * @param baseAddress The String object that represents the baseAddress of this object + */ + public void setBaseAddress(String baseAddress) { + this.baseAddress = baseAddress; + } + + /** + * Return the baseAddress set to this object + * + * @return The String object that represents the baseAddress of this object + */ + public String getBaseAddress() { + return baseAddress; + } + + /** + * Set the name of the query param, this correspond to the index used (title, author) + * + * @param queryParam on which index make the query + */ + public void setQueryParam(String queryParam) { + this.queryParam = queryParam; + } + + /** + * Get the name of the query param for the rest call + * + * @return the name of the query param, i.e. the index (title, author) to use + */ + public String getQueryParam() { + return queryParam; + } + /** + * Initialize the class + * + * @throws Exception on generic exception + */ + @Override + public void init() throws Exception { + Client client = ClientBuilder.newClient(); + if (baseAddress == null) { + baseAddress = "http://api.openaire.eu/search/publications"; + } + if (queryParam == null) { + queryParam = "title"; + } + webTarget = client.target(baseAddress); + } + + public class SearchByIdCallable implements Callable { + + String id = null; + + public SearchByIdCallable(String id) { + this.id = id; + } + + public SearchByIdCallable(Query query) { + this.id = query.getParameterAsClass("id", String.class); + } + + @Override + public ImportRecord call() throws Exception { + List results = new ArrayList(); + WebTarget localTarget = webTarget.queryParam("openairePublicationID", id); + Invocation.Builder invocationBuilder = localTarget.request(); + Response response = invocationBuilder.get(); + if (response.getStatus() == 200) { + String responseString = response.readEntity(String.class); + List omElements = splitToRecords(responseString); + if (omElements != null) { + for (OMElement record : omElements) { + results.add(filterMultipleTitles(transformSourceRecords(record))); + } + } + return results != null ? results.get(0) : null; + } else { + return null; + } + } + } + + public class CountByQueryCallable implements Callable { + + String q; + + public CountByQueryCallable(String query) { + q = query; + } + + public CountByQueryCallable(Query query) { + q = query.getParameterAsClass("query", String.class); + } + + @Override + public Integer call() throws Exception { + WebTarget localTarget = webTarget.queryParam(queryParam, q); + Invocation.Builder invocationBuilder = localTarget.request(); + Response response = invocationBuilder.get(); + if (response.getStatus() == 200) { + String responseString = response.readEntity(String.class); + OMXMLParserWrapper records = OMXMLBuilderFactory.createOMBuilder(new StringReader(responseString)); + OMElement element = records.getDocumentElement(); + AXIOMXPath xpath = null; + try { + xpath = new AXIOMXPath("/response/header/total"); + OMElement totalItem = (OMElement) xpath.selectSingleNode(element); + return totalItem != null ? Integer.parseInt(totalItem.getText()) : null; + } catch (JaxenException e) { + return 0; + } + } else { + return 0; + } + } + } + + public class SearchByQueryCallable implements Callable> { + + String q; + int page; + int count; + + public SearchByQueryCallable(String query, int start, int count) { + this.q = query; + this.page = start / count; + this.count = count; + } + + public SearchByQueryCallable(Query query) { + this.q = query.getParameterAsClass("query", String.class); + this.page = query.getParameterAsClass("start", Integer.class) / + query.getParameterAsClass("count", Integer.class); + this.count = query.getParameterAsClass("count", Integer.class); + } + + @Override + public List call() throws Exception { + WebTarget localTarget = webTarget.queryParam(queryParam, q); + localTarget = localTarget.queryParam("page", page + 1); + localTarget = localTarget.queryParam("size", count); + List results = new ArrayList(); + Invocation.Builder invocationBuilder = localTarget.request(); + Response response = invocationBuilder.get(); + if (response.getStatus() == 200) { + String responseString = response.readEntity(String.class); + List omElements = splitToRecords(responseString); + if (omElements != null) { + for (OMElement record : omElements) { + results.add(filterMultipleTitles(transformSourceRecords(record))); + } + } + } + return results; + } + } + + /** + * This method remove multiple titles occurrences + * + * @param transformSourceRecords + * @return ImportRecord with one or zero title + */ + private ImportRecord filterMultipleTitles(ImportRecord transformSourceRecords) { + List metadata = (List)transformSourceRecords.getValueList(); + ArrayList nextSourceRecord = new ArrayList<>(); + boolean found = false; + for (MetadatumDTO dto : metadata) { + if ("dc".equals(dto.getSchema()) && "title".equals(dto.getElement()) && dto.getQualifier() == null) { + if (!found) { + nextSourceRecord.add(dto); + found = true; + } + } else { + nextSourceRecord.add(dto); + } + } + return new ImportRecord(nextSourceRecord); + } + + private List splitToRecords(String recordsSrc) { + OMXMLParserWrapper records = OMXMLBuilderFactory.createOMBuilder(new StringReader(recordsSrc)); + OMElement element = records.getDocumentElement(); + AXIOMXPath xpath = null; + try { + xpath = new AXIOMXPath("/response/results/result"); + xpath.addNamespace("dri", "http://www.driver-repository.eu/namespace/dri"); + xpath.addNamespace("oaf", "http://namespace.openaire.eu/oaf"); + xpath.addNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance"); + List recordsList = xpath.selectNodes(element); + return recordsList; + } catch (JaxenException e) { + return null; + } + } + + + +} diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index 5e69ee9c4282..00900f4bda65 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -115,10 +115,18 @@ - - - - + + + + + + + + + diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-suggestion-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-suggestion-services.xml new file mode 100644 index 000000000000..fb720137c440 --- /dev/null +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-suggestion-services.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml index ac163d35811d..8a5f585335bb 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml @@ -90,6 +90,8 @@ + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml index 5f86c7359890..611a32ad7960 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml @@ -47,5 +47,8 @@ + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/suggestions.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/suggestions.xml new file mode 100644 index 000000000000..a3ae1cb875e6 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/suggestions.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace-api/src/test/java/org/dspace/app/suggestion/MockSolrSuggestionProvider.java b/dspace-api/src/test/java/org/dspace/app/suggestion/MockSolrSuggestionProvider.java new file mode 100644 index 000000000000..af890da45541 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/suggestion/MockSolrSuggestionProvider.java @@ -0,0 +1,20 @@ +/** + * 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.suggestion; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.core.Context; +import org.dspace.external.model.ExternalDataObject; + +public class MockSolrSuggestionProvider extends SolrSuggestionProvider { + + @Override + protected boolean isExternalDataObjectPotentiallySuggested(Context context, ExternalDataObject externalDataObject) { + return StringUtils.equals(MockSuggestionExternalDataSource.NAME, externalDataObject.getSource()); + } +} diff --git a/dspace-api/src/test/java/org/dspace/app/suggestion/MockSolrSuggestionStorageService.java b/dspace-api/src/test/java/org/dspace/app/suggestion/MockSolrSuggestionStorageService.java new file mode 100644 index 000000000000..1c843026d415 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/suggestion/MockSolrSuggestionStorageService.java @@ -0,0 +1,38 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.suggestion; + +import org.dspace.solr.MockSolrServer; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +/** + * Mock SOLR service for the suggestion Core. + */ +@Service +public class MockSolrSuggestionStorageService extends SolrSuggestionStorageServiceImpl + implements InitializingBean, DisposableBean { + private MockSolrServer mockSolrServer; + + @Override + public void afterPropertiesSet() throws Exception { + mockSolrServer = new MockSolrServer("suggestion"); + solrSuggestionClient = mockSolrServer.getSolrServer(); + } + + /** Clear all records from the search core. */ + public void reset() { + mockSolrServer.reset(); + } + + @Override + public void destroy() throws Exception { + mockSolrServer.destroy(); + } +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/app/suggestion/MockSuggestionExternalDataSource.java b/dspace-api/src/test/java/org/dspace/app/suggestion/MockSuggestionExternalDataSource.java new file mode 100644 index 000000000000..cf0303debd3d --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/suggestion/MockSuggestionExternalDataSource.java @@ -0,0 +1,67 @@ +/** + * 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.suggestion; + +import java.util.List; +import java.util.Optional; + +import org.apache.commons.codec.binary.StringUtils; +import org.dspace.core.Context; +import org.dspace.external.model.ExternalDataObject; +import org.dspace.external.provider.AbstractExternalDataProvider; +import org.dspace.services.RequestService; +import org.dspace.services.model.Request; +import org.dspace.utils.DSpace; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MockSuggestionExternalDataSource extends AbstractExternalDataProvider { + public static final String NAME = "suggestion"; + + @Autowired + private SuggestionService suggestionService; + + @Override + public String getSourceIdentifier() { + return NAME; + } + + @Override + public Optional getExternalDataObject(String id) { + RequestService requestService = new DSpace().getRequestService(); + Request currentRequest = requestService.getCurrentRequest(); + Context context = (Context) currentRequest.getAttribute("dspace.context"); + Suggestion suggestion = suggestionService.findUnprocessedSuggestion(context, id); + if (suggestion != null) { + ExternalDataObject extDataObj = new ExternalDataObject(NAME); + extDataObj.setDisplayValue(suggestion.getDisplay()); + extDataObj.setId(suggestion.getExternalSourceUri() + .substring(suggestion.getExternalSourceUri().lastIndexOf("/") + 1)); + extDataObj.setMetadata(suggestion.getMetadata()); + return Optional.of(extDataObj); + } + return null; + } + + @Override + public List searchExternalDataObjects(String query, int start, int limit) { + return null; + } + + @Override + public boolean supports(String source) { + return StringUtils.equals(NAME, source); + } + + @Override + public int getNumberOfResults(String query) { + return 0; + } + +} diff --git a/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java b/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java index 06deacaca473..56a0356df715 100644 --- a/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java @@ -15,6 +15,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.app.requestitem.factory.RequestItemServiceFactory; import org.dspace.app.requestitem.service.RequestItemService; +import org.dspace.app.suggestion.SolrSuggestionStorageService; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.AuthorizeService; @@ -45,6 +46,7 @@ import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.service.ProcessService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.utils.DSpace; import org.dspace.versioning.factory.VersionServiceFactory; import org.dspace.versioning.service.VersionHistoryService; import org.dspace.versioning.service.VersioningService; @@ -95,6 +97,7 @@ public abstract class AbstractBuilder { static ProcessService processService; static RequestItemService requestItemService; static VersioningService versioningService; + static SolrSuggestionStorageService solrSuggestionService; protected Context context; @@ -151,6 +154,7 @@ public static void init() { inProgressUserService = XmlWorkflowServiceFactory.getInstance().getInProgressUserService(); poolTaskService = XmlWorkflowServiceFactory.getInstance().getPoolTaskService(); workflowItemRoleService = XmlWorkflowServiceFactory.getInstance().getWorkflowItemRoleService(); + solrSuggestionService = new DSpace().getSingletonService(SolrSuggestionStorageService.class); } diff --git a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java index aad0e86b1e90..519321c5d547 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java @@ -139,6 +139,10 @@ public ItemBuilder withIIIFCanvasHeight(int i) { return addMetadataValue(item, "iiif", "image", "height", String.valueOf(i)); } + public ItemBuilder withDSpaceObjectOwner(String name, String authority) { + return addMetadataValue(item, "dspace", "object", "owner", null, name, authority, 600); + } + public ItemBuilder withMetadata(final String schema, final String element, final String qualifier, final String value) { return addMetadataValue(item, schema, element, qualifier, value); diff --git a/dspace-api/src/test/java/org/dspace/builder/SuggestionTargetBuilder.java b/dspace-api/src/test/java/org/dspace/builder/SuggestionTargetBuilder.java new file mode 100644 index 000000000000..f9671bba60fb --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/builder/SuggestionTargetBuilder.java @@ -0,0 +1,161 @@ +/** + * 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.builder; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.solr.client.solrj.SolrServerException; +import org.dspace.app.suggestion.MockSuggestionExternalDataSource; +import org.dspace.app.suggestion.SolrSuggestionStorageService; +import org.dspace.app.suggestion.Suggestion; +import org.dspace.app.suggestion.SuggestionEvidence; +import org.dspace.app.suggestion.SuggestionTarget; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.dto.MetadataValueDTO; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; + +/** + * Builder to construct Item objects + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +public class SuggestionTargetBuilder extends AbstractBuilder { + public final static String EVIDENCE_MOCK_NAME = "MockEvidence"; + public final static String EVIDENCE_MOCK_NOTE = "Generated for testing purpose..."; + private Item item; + private SuggestionTarget target; + private List suggestions; + private String source; + private int total; + + protected SuggestionTargetBuilder(Context context) { + super(context); + } + + public static SuggestionTargetBuilder createTarget(final Context context, final Collection col, final String name) { + return createTarget(context, col, name, null); + } + + public static SuggestionTargetBuilder createTarget(final Context context, final Collection col, final String name, + final EPerson eperson) { + SuggestionTargetBuilder builder = new SuggestionTargetBuilder(context); + return builder.create(context, col, name, eperson); + } + + public static SuggestionTargetBuilder createTarget(final Context context, final Item item) { + SuggestionTargetBuilder builder = new SuggestionTargetBuilder(context); + return builder.create(context, item); + } + + private SuggestionTargetBuilder create(final Context context, final Collection col, final String name) { + return create(context, col, name, null); + } + + private SuggestionTargetBuilder create(final Context context, final Collection col, final String name, + final EPerson eperson) { + this.context = context; + + try { + ItemBuilder itemBuilder = ItemBuilder.createItem(context, col).withTitle(name); + if (eperson != null) { + itemBuilder = itemBuilder.withDSpaceObjectOwner(eperson.getFullName(), eperson.getID().toString()); + } + item = itemBuilder.build(); + context.dispatchEvents(); + indexingService.commit(); + } catch (Exception e) { + return handleException(e); + } + return this; + } + + private SuggestionTargetBuilder create(final Context context, final Item item) { + this.context = context; + this.item = item; + return this; + } + + public SuggestionTargetBuilder withSuggestionCount(final String source, final int total) { + this.source = source; + this.total = total; + return this; + } + + @Override + public SuggestionTarget build() { + target = new SuggestionTarget(item); + target.setTotal(total); + target.setSource(source); + suggestions = generateAllSuggestion(); + try { + for (Suggestion s : suggestions) { + solrSuggestionService.addSuggestion(s, false, false); + } + solrSuggestionService.commit(); + } catch (SolrServerException | IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + return target; + } + + @Override + public void cleanup() throws Exception { + solrSuggestionService.deleteTarget(target); + } + + @Override + protected SolrSuggestionStorageService getService() { + return solrSuggestionService; + } + + @Override + public void delete(Context c, SuggestionTarget dso) throws Exception { + solrSuggestionService.deleteTarget(dso); + } + + private List generateAllSuggestion() { + List allSuggestions = new ArrayList(); + for (int idx = 0; idx < target.getTotal(); idx++) { + String idPartStr = String.valueOf(idx + 1); + Suggestion sug = new Suggestion(source, item, idPartStr); + sug.setDisplay("Suggestion " + source + " " + idPartStr); + MetadataValueDTO mTitle = new MetadataValueDTO(); + mTitle.setSchema("dc"); + mTitle.setElement("title"); + mTitle.setValue("Title Suggestion " + idPartStr); + + MetadataValueDTO mSource1 = new MetadataValueDTO(); + mSource1.setSchema("dc"); + mSource1.setElement("source"); + mSource1.setValue("Source 1"); + + MetadataValueDTO mSource2 = new MetadataValueDTO(); + mSource2.setSchema("dc"); + mSource2.setElement("source"); + mSource2.setValue("Source 2"); + + sug.getMetadata().add(mTitle); + sug.getMetadata().add(mSource1); + sug.getMetadata().add(mSource2); + + sug.setExternalSourceUri( + "http://localhost/api/integration/externalsources/" + MockSuggestionExternalDataSource.NAME + + "/entryValues/" + idPartStr); + sug.getEvidences().add(new SuggestionEvidence(EVIDENCE_MOCK_NAME, + idx % 2 == 0 ? 100 - idx : (double) idx / 2, EVIDENCE_MOCK_NOTE)); + allSuggestions.add(sug); + } + return allSuggestions; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java index 7c79a857017b..57c6052c277f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java @@ -1067,6 +1067,13 @@ private ResponseEntity> deleteI return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } + @RequestMapping(method = RequestMethod.DELETE, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG) + public ResponseEntity> delete(HttpServletRequest request, @PathVariable String apiCategory, + @PathVariable String model, @PathVariable String id) + throws HttpRequestMethodNotSupportedException { + return deleteInternal(apiCategory, model, id); + } + /** * Execute a PUT request for an entity with id of type UUID; * diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionConverter.java new file mode 100644 index 000000000000..8eed5fb78ac8 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionConverter.java @@ -0,0 +1,52 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.converter; + +import org.dspace.app.rest.model.SuggestionRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.suggestion.Suggestion; +import org.dspace.app.suggestion.SuggestionEvidence; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * This class provides the method to convert a Suggestion to its REST representation, the + * SuggestionRest + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@Component +public class SuggestionConverter + implements DSpaceConverter { + + @Autowired + private MetadataValueDTOListConverter metadataConverter; + + @Override + public SuggestionRest convert(Suggestion target, Projection projection) { + SuggestionRest targetRest = new SuggestionRest(); + targetRest.setProjection(projection); + targetRest.setId(target.getID()); + targetRest.setDisplay(target.getDisplay()); + targetRest.setExternalSourceUri(target.getExternalSourceUri()); + targetRest.setSource(target.getSource()); + targetRest.setScore(String.format("%.2f", target.getScore())); + for (SuggestionEvidence se : target.getEvidences()) { + targetRest.getEvidences().put(se.getName(), + new SuggestionRest.EvidenceRest(String.format("%.2f", se.getScore()), se.getNotes())); + } + targetRest.setMetadata(metadataConverter.convert(target.getMetadata())); + return targetRest; + } + + @Override + public Class getModelClass() { + return Suggestion.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionSourceConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionSourceConverter.java new file mode 100644 index 000000000000..3506133b6f57 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionSourceConverter.java @@ -0,0 +1,39 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.converter; + +import org.dspace.app.rest.model.SuggestionSourceRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.suggestion.SuggestionSource; +import org.springframework.stereotype.Component; + +/** + * This class provides the method to convert a SuggestionSource to its REST representation, the + * SuggestionSourceRest + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@Component +public class SuggestionSourceConverter + implements DSpaceConverter { + + @Override + public SuggestionSourceRest convert(SuggestionSource target, Projection projection) { + SuggestionSourceRest targetRest = new SuggestionSourceRest(); + targetRest.setProjection(projection); + targetRest.setId(target.getID()); + targetRest.setTotal(target.getTotal()); + return targetRest; + } + + @Override + public Class getModelClass() { + return SuggestionSource.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionTargetConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionTargetConverter.java new file mode 100644 index 000000000000..4bf4be72263a --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionTargetConverter.java @@ -0,0 +1,41 @@ +/** + * 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.SuggestionTargetRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.suggestion.SuggestionTarget; +import org.springframework.stereotype.Component; + +/** + * This class provides the method to convert a SuggestionTarget to its REST representation, the + * SuggestionTargetRest + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@Component +public class SuggestionTargetConverter + implements DSpaceConverter { + + @Override + public SuggestionTargetRest convert(SuggestionTarget target, Projection projection) { + SuggestionTargetRest targetRest = new SuggestionTargetRest(); + targetRest.setProjection(projection); + targetRest.setId(target.getID()); + targetRest.setDisplay(target.getTarget().getName()); + targetRest.setTotal(target.getTotal()); + targetRest.setSource(target.getSource()); + return targetRest; + } + + @Override + public Class getModelClass() { + return SuggestionTarget.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionRest.java new file mode 100644 index 000000000000..461aff70e8c9 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionRest.java @@ -0,0 +1,110 @@ +/** + * 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.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonProperty.Access; +import org.dspace.app.rest.RestResourceController; + +/** + * The Suggestion REST Resource. A suggestion is an object, usually a + * publication, proposed by a source related to a specific Person (target) to be + * imported in the system. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@LinksRest(links = { @LinkRest(name = SuggestionRest.TARGET, method = "getTarget") }) +public class SuggestionRest extends BaseObjectRest { + private static final long serialVersionUID = 1L; + public static final String NAME = "suggestion"; + public static final String TARGET = "target"; + public static final String CATEGORY = RestAddressableModel.INTEGRATION; + + private String display; + private String source; + private String externalSourceUri; + private String score; + private Map evidences = new HashMap(); + private MetadataRest metadata = new MetadataRest(); + + @Override + @JsonProperty(access = Access.READ_ONLY) + public String getType() { + return NAME; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + public String getDisplay() { + return display; + } + + public void setDisplay(String display) { + this.display = display; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getExternalSourceUri() { + return externalSourceUri; + } + + public void setExternalSourceUri(String externalSourceUri) { + this.externalSourceUri = externalSourceUri; + } + + public void setScore(String score) { + this.score = score; + } + + public String getScore() { + return score; + } + + public Map getEvidences() { + return evidences; + } + + public void setEvidences(Map evidences) { + this.evidences = evidences; + } + + public MetadataRest getMetadata() { + return metadata; + } + + public void setMetadata(MetadataRest metadata) { + this.metadata = metadata; + } + + public static class EvidenceRest { + public String score; + public String notes; + public EvidenceRest(String score, String notes) { + this.score = score; + this.notes = notes; + } + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionSourceRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionSourceRest.java new file mode 100644 index 000000000000..9c2aa80e82e5 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionSourceRest.java @@ -0,0 +1,51 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonProperty.Access; +import org.dspace.app.rest.RestResourceController; + +/** + * The Suggestion Source REST Resource. A suggestion source is a connector to an + * external system that provides suggestion for a target object of related + * objects to be imported in the system. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +public class SuggestionSourceRest extends BaseObjectRest { + private static final long serialVersionUID = 1L; + public static final String CATEGORY = RestAddressableModel.INTEGRATION; + public static final String NAME = "suggestionsource"; + + private int total; + + @Override + @JsonProperty(access = Access.READ_ONLY) + public String getType() { + return NAME; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionTargetRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionTargetRest.java new file mode 100644 index 000000000000..ba93ab4e52b7 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SuggestionTargetRest.java @@ -0,0 +1,73 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonProperty.Access; +import org.dspace.app.rest.RestResourceController; + +/** + * The Suggestion Target REST Resource. A suggestion target is a Person to whom + * one or more suggester sources have found related objects to be importe in the + * system. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@LinksRest(links = { + @LinkRest(name = SuggestionTargetRest.TARGET, method = "getTarget") +}) +public class SuggestionTargetRest extends BaseObjectRest { + private static final long serialVersionUID = 1L; + public static final String NAME = "suggestiontarget"; + public static final String TARGET = "target"; + public static final String CATEGORY = RestAddressableModel.INTEGRATION; + + private String display; + private String source; + private int total; + + @Override + @JsonProperty(access = Access.READ_ONLY) + public String getType() { + return NAME; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + public String getDisplay() { + return display; + } + + public void setDisplay(String display) { + this.display = display; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionResource.java new file mode 100644 index 000000000000..66165f86983b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionResource.java @@ -0,0 +1,25 @@ +/** + * 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.hateoas; + +import org.dspace.app.rest.model.SuggestionRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * Suggestion Rest HAL Resource. The HAL Resource wraps the REST Resource + * adding support for the links and embedded resources + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@RelNameDSpaceResource(SuggestionRest.NAME) +public class SuggestionResource extends DSpaceResource { + public SuggestionResource(SuggestionRest target, Utils utils) { + super(target, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionSourceResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionSourceResource.java new file mode 100644 index 000000000000..1f01f27d862e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionSourceResource.java @@ -0,0 +1,25 @@ +/** + * 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.hateoas; + +import org.dspace.app.rest.model.SuggestionSourceRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * Suggestion Source Rest HAL Resource. The HAL Resource wraps the REST Resource + * adding support for the links and embedded resources + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@RelNameDSpaceResource(SuggestionSourceRest.NAME) +public class SuggestionSourceResource extends DSpaceResource { + public SuggestionSourceResource(SuggestionSourceRest target, Utils utils) { + super(target, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionTargetResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionTargetResource.java new file mode 100644 index 000000000000..26cd7c3c34e2 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SuggestionTargetResource.java @@ -0,0 +1,25 @@ +/** + * 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.hateoas; + +import org.dspace.app.rest.model.SuggestionTargetRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * Suggestion Target Rest HAL Resource. The HAL Resource wraps the REST Resource + * adding support for the links and embedded resources + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@RelNameDSpaceResource(SuggestionTargetRest.NAME) +public class SuggestionTargetResource extends DSpaceResource { + public SuggestionTargetResource(SuggestionTargetRest target, Utils utils) { + super(target, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionRestRepository.java new file mode 100644 index 000000000000..e2e1c3ce7ccb --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionRestRepository.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.app.rest.repository; + +import java.util.List; +import java.util.UUID; + +import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; +import org.dspace.app.rest.model.SuggestionRest; +import org.dspace.app.rest.model.SuggestionTargetRest; +import org.dspace.app.suggestion.Suggestion; +import org.dspace.app.suggestion.SuggestionService; +import org.dspace.authorize.AuthorizeException; +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.domain.Sort.Direction; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * This is the repository responsible to manage Suggestion Target Rest object + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ + +@Component(SuggestionRest.CATEGORY + "." + SuggestionRest.NAME) +public class SuggestionRestRepository extends DSpaceRestRepository { + private final static String ORDER_FIELD = "trust"; + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SuggestionRestRepository.class); + + @Autowired + private SuggestionService suggestionService; + + @Override + @PreAuthorize("hasPermission(#id, 'SUGGESTION', 'READ')") + public SuggestionRest findOne(Context context, String id) { + Suggestion suggestion = suggestionService.findUnprocessedSuggestion(context, id); + if (suggestion == null) { + return null; + } + return converter.toRest(suggestion, utils.obtainProjection()); + } + + @Override + @PreAuthorize("permitAll()") + public Page findAll(Context context, Pageable pageable) { + throw new RepositoryMethodNotImplementedException(SuggestionTargetRest.NAME, "findAll"); + } + + @PreAuthorize("hasPermission(#target, 'SUGGESTION.TARGET', 'READ')") + @SearchRestMethod(name = "findByTargetAndSource") + public Page findByTargetAndSource( + @Parameter(required = true, value = "source") String source, + @Parameter(required = true, value = "target") UUID target, Pageable pageable) { + Context context = obtainContext(); + boolean ascending = false; + if (pageable.getSort() != null && pageable.getSort().getOrderFor(ORDER_FIELD) != null) { + ascending = pageable.getSort().getOrderFor(ORDER_FIELD).getDirection() == Direction.ASC; + } + List suggestions = suggestionService.findByTargetAndSource(context, target, source, + pageable.getPageSize(), pageable.getOffset(), ascending); + long tot = suggestionService.countAllByTargetAndSource(context, source, target); + return converter.toRestPage(suggestions, pageable, tot, utils.obtainProjection()); + } + + @Override + @PreAuthorize("hasPermission(#id, 'SUGGESTION', 'DELETE')") + protected void delete(Context context, String id) + throws AuthorizeException, RepositoryMethodNotImplementedException { + suggestionService.rejectSuggestion(context, id); + } + + @Override + public Class getDomainClass() { + return SuggestionRest.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionSourceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionSourceRestRepository.java new file mode 100644 index 000000000000..6bc251749bce --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionSourceRestRepository.java @@ -0,0 +1,64 @@ +/** + * 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; + +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.model.SuggestionSourceRest; +import org.dspace.app.suggestion.SuggestionService; +import org.dspace.app.suggestion.SuggestionSource; +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.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * This is the repository responsible to manage Suggestion Target Rest object + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ + +@Component(SuggestionSourceRest.CATEGORY + "." + SuggestionSourceRest.NAME) +public class SuggestionSourceRestRepository extends DSpaceRestRepository { + + private static final Logger log = org.apache.logging.log4j.LogManager + .getLogger(SuggestionSourceRestRepository.class); + + @Autowired + private SuggestionService suggestionService; + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public SuggestionSourceRest findOne(Context context, String source) { + SuggestionSource suggestionSource = suggestionService.findSource(context, source); + if (suggestionSource == null) { + return null; + } + return converter.toRest(suggestionSource, utils.obtainProjection()); + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public Page findAll(Context context, Pageable pageable) { + List suggestionSources = suggestionService.findAllSources(context, pageable.getPageSize(), + pageable.getOffset()); + long count = suggestionService.countSources(context); + if (suggestionSources == null) { + return null; + } + return converter.toRestPage(suggestionSources, pageable, count, utils.obtainProjection()); + } + + @Override + public Class getDomainClass() { + return SuggestionSourceRest.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionTargetRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionTargetRestRepository.java new file mode 100644 index 000000000000..aadeb4da94ec --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionTargetRestRepository.java @@ -0,0 +1,92 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository; + +import java.util.List; +import java.util.UUID; + +import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; +import org.dspace.app.rest.model.SuggestionTargetRest; +import org.dspace.app.suggestion.SuggestionService; +import org.dspace.app.suggestion.SuggestionTarget; +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.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * This is the repository responsible to manage Suggestion Target Rest object + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ + +@Component(SuggestionTargetRest.CATEGORY + "." + SuggestionTargetRest.NAME) +public class SuggestionTargetRestRepository extends DSpaceRestRepository { + + private static final Logger log = org.apache.logging.log4j.LogManager + .getLogger(SuggestionTargetRestRepository.class); + + @Autowired + private SuggestionService suggestionService; + + @Override + @PreAuthorize("hasPermission(#id, 'SUGGESTIONTARGET', 'READ')") + public SuggestionTargetRest findOne(Context context, String id) { + String source = null; + UUID uuid = null; + try { + source = id.split(":")[0]; + uuid = UUID.fromString(id.split(":")[1]); + } catch (Exception e) { + return null; + } + SuggestionTarget suggestionTarget = suggestionService.find(context, source, uuid); + if (suggestionTarget == null) { + return null; + } + return converter.toRest(suggestionTarget, utils.obtainProjection()); + } + + @Override + @PreAuthorize("permitAll()") + public Page findAll(Context context, Pageable pageable) { + throw new RepositoryMethodNotImplementedException(SuggestionTargetRest.NAME, "findAll"); + } + + @PreAuthorize("hasAuthority('ADMIN')") + @SearchRestMethod(name = "findBySource") + public Page findBySource(@Parameter(required = true, value = "source") String source, + Pageable pageable) { + Context context = obtainContext(); + List suggestionTargets = suggestionService.findAllTargets(context, source, + pageable.getPageSize(), pageable.getOffset()); + long tot = suggestionService.countAll(context, source); + return converter.toRestPage(suggestionTargets, pageable, tot, utils.obtainProjection()); + } + + @PreAuthorize("hasPermission(#target, 'SUGGESTIONTARGET.TARGET', 'READ')") + @SearchRestMethod(name = "findByTarget") + public Page findByTarget(@Parameter(required = true, value = "target") UUID target, + Pageable pageable) { + Context context = obtainContext(); + List suggestionTargets = suggestionService.findByTarget(context, target, + pageable.getPageSize(), pageable.getOffset()); + long tot = suggestionService.countAllByTarget(context, target); + return converter.toRestPage(suggestionTargets, pageable, tot, utils.obtainProjection()); + } + + @Override + public Class getDomainClass() { + return SuggestionTargetRest.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionTargetTargetLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionTargetTargetLinkRepository.java new file mode 100644 index 000000000000..50c6e4d48e27 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SuggestionTargetTargetLinkRepository.java @@ -0,0 +1,70 @@ +/** + * 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; + +import java.sql.SQLException; +import java.util.UUID; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.model.SuggestionTargetRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.content.Item; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +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; + +/** + * Link repository for "target" subresource of an suggestion target. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component(SuggestionTargetRest.CATEGORY + "." + SuggestionTargetRest.NAME + "." + SuggestionTargetRest.TARGET) +public class SuggestionTargetTargetLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private ItemService itemService; + + /** + * Returns the item related to the suggestion target with the given id. + * + * @param request the http servlet request + * @param id the suggestion target UUID + * @param pageable the optional pageable + * @param projection the projection object + * @return the target item rest representation + */ + @PreAuthorize("hasPermission(#id, 'SUGGESTIONTARGET', 'READ')") + public ItemRest getTarget(@Nullable HttpServletRequest request, String id, + @Nullable Pageable pageable, Projection projection) { + String source = id.split(":")[0]; + UUID uuid = UUID.fromString(id.split(":")[1]); + if (StringUtils.isBlank(source) || uuid == null) { + throw new ResourceNotFoundException("No such item related to a suggestion target with UUID: " + id); + } + try { + Context context = obtainContext(); + Item profile = itemService.find(context, uuid); + if (profile == null) { + throw new ResourceNotFoundException("No such item related to a suggestion target with UUID: " + id); + } + + return converter.toRest(profile, projection); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SuggestionRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SuggestionRestPermissionEvaluatorPlugin.java new file mode 100644 index 000000000000..2f095a9abcdd --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SuggestionRestPermissionEvaluatorPlugin.java @@ -0,0 +1,94 @@ +/** + * 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.security; + +import static org.dspace.app.rest.security.DSpaceRestPermission.DELETE; +import static org.dspace.app.rest.security.DSpaceRestPermission.READ; +import static org.dspace.app.rest.security.DSpaceRestPermission.WRITE; + +import java.io.Serializable; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.SuggestionRest; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.util.UUIDUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +/** + * + * An authenticated user is allowed to view a suggestion for the data that his + * own. This {@link RestPermissionEvaluatorPlugin} implements that requirement. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component +public class SuggestionRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { + + @Autowired + private ItemService itemService; + + @Override + public boolean hasDSpacePermission(Authentication authentication, Serializable targetId, String targetType, + DSpaceRestPermission restPermission) { + + if (!READ.equals(restPermission) && !WRITE.equals(restPermission) && !DELETE.equals(restPermission)) { + return false; + } + + if (!StringUtils.equalsIgnoreCase(targetType, SuggestionRest.NAME) + && !StringUtils.startsWithIgnoreCase(targetType, SuggestionRest.NAME)) { + return false; + } + + Context context = ContextUtil.obtainCurrentRequestContext(); + + EPerson currentUser = context.getCurrentUser(); + if (currentUser == null) { + return false; + } + + try { + String id = targetId.toString(); + UUID uuid = null; + if (id.contains(":")) { + uuid = UUIDUtils.fromString(id.split(":", 3)[1]); + } else { + uuid = UUIDUtils.fromString(id); + } + if (uuid == null) { + return false; + } + Item item = itemService.find(context, uuid); + if (item != null) { + List mvalues = itemService.getMetadataByMetadataString(item, "dspace.object.owner"); + if (mvalues != null) { + for (MetadataValue mv : mvalues) { + if (StringUtils.equals(mv.getAuthority(), currentUser.getID().toString())) { + return true; + } + } + } + } + } catch (Exception ex) { + return false; + } + + return false; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SuggestionTargetRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SuggestionTargetRestPermissionEvaluatorPlugin.java new file mode 100644 index 000000000000..98acee30c679 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SuggestionTargetRestPermissionEvaluatorPlugin.java @@ -0,0 +1,95 @@ +/** + * 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.security; + +import static org.dspace.app.rest.security.DSpaceRestPermission.DELETE; +import static org.dspace.app.rest.security.DSpaceRestPermission.READ; +import static org.dspace.app.rest.security.DSpaceRestPermission.WRITE; + +import java.io.Serializable; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.SuggestionTargetRest; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.util.UUIDUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +/** + * + * An authenticated user is allowed to view the suggestions summary + * (SuggestionTarget) for the data that his own. This + * {@link RestPermissionEvaluatorPlugin} implements that requirement. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component +public class SuggestionTargetRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { + + @Autowired + private ItemService itemService; + + @Override + public boolean hasDSpacePermission(Authentication authentication, Serializable targetId, String targetType, + DSpaceRestPermission restPermission) { + + if (!READ.equals(restPermission) && !WRITE.equals(restPermission) && !DELETE.equals(restPermission)) { + return false; + } + + if (!StringUtils.equalsIgnoreCase(targetType, SuggestionTargetRest.NAME) + && !StringUtils.startsWithIgnoreCase(targetType, SuggestionTargetRest.NAME)) { + return false; + } + + Context context = ContextUtil.obtainCurrentRequestContext(); + + EPerson currentUser = context.getCurrentUser(); + if (currentUser == null) { + return false; + } + + try { + String id = targetId.toString(); + UUID uuid = null; + if (id.contains(":")) { + uuid = UUIDUtils.fromString(id.split(":", 2)[1]); + } else { + uuid = UUIDUtils.fromString(id); + } + if (uuid == null) { + return false; + } + Item item = itemService.find(context, uuid); + if (item != null) { + List mvalues = itemService.getMetadataByMetadataString(item, "dspace.object.owner"); + if (mvalues != null) { + for (MetadataValue mv : mvalues) { + if (StringUtils.equals(mv.getAuthority(), currentUser.getID().toString())) { + return true; + } + } + } + } + } catch (Exception ex) { + return false; + } + + return false; + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java index 9205c3b88ea4..4c610c74bc89 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java @@ -41,6 +41,7 @@ public void findAllExternalSources() throws Exception { .andExpect(jsonPath("$._embedded.externalsources", Matchers.hasItems( ExternalSourceMatcher.matchExternalSource("mock", "mock", false), ExternalSourceMatcher.matchExternalSource("orcid", "orcid", false), + ExternalSourceMatcher.matchExternalSource("suggestion", "suggestion", false), ExternalSourceMatcher.matchExternalSource( "sherpaJournalIssn", "sherpaJournalIssn", false), ExternalSourceMatcher.matchExternalSource( diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionRestRepositoryIT.java new file mode 100644 index 000000000000..16d4580594ff --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionRestRepositoryIT.java @@ -0,0 +1,473 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.dspace.app.rest.matcher.SuggestionMatcher.matchSuggestion; +import static org.dspace.builder.SuggestionTargetBuilder.EVIDENCE_MOCK_NAME; +import static org.dspace.builder.SuggestionTargetBuilder.EVIDENCE_MOCK_NOTE; +import static org.hamcrest.Matchers.is; +import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE; +import static org.springframework.http.MediaType.parseMediaType; +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.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.Map; +import java.util.UUID; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dspace.app.rest.matcher.MetadataMatcher; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.suggestion.MockSuggestionExternalDataSource; +import org.dspace.app.suggestion.SuggestionTarget; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.SuggestionTargetBuilder; +import org.dspace.builder.WorkspaceItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.springframework.test.web.servlet.MvcResult; + +/** + * Integration Tests against the /api/integration/suggestions endpoint + */ +public class SuggestionRestRepositoryIT extends AbstractControllerIntegrationTest { + private Collection colPeople; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + // We turn off the authorization system in order to create the structure as + // defined below + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + colPeople = CollectionBuilder.createCollection(context, parentCommunity).withName("People") + .withEntityType("Person").build(); + context.restoreAuthSystemState(); + } + + @Test + public void findAllTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + context.restoreAuthSystemState(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/integration/suggestions")) + .andExpect(status().isMethodNotAllowed()); + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/integration/suggestions")).andExpect(status().isMethodNotAllowed()); + getClient().perform(get("/api/integration/suggestions")).andExpect(status().isMethodNotAllowed()); + } + + @Test + public void findByTargetAndSourceTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + SuggestionTarget targetSecond = SuggestionTargetBuilder + .createTarget(context, colPeople, "Digilio, Giuseppe", eperson).withSuggestionCount("reciter", 11) + .build(); + context.restoreAuthSystemState(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken) + .perform(get("/api/integration/suggestions/search/findByTargetAndSource").param("source", "scopus") + .param("target", itemFirst.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestions", Matchers.contains( + matchSuggestion("scopus", itemFirst, "Suggestion scopus 1", "1", + 100.0, EVIDENCE_MOCK_NAME, 100.0, EVIDENCE_MOCK_NOTE), + matchSuggestion("scopus", itemFirst, "Suggestion scopus 3", "3", + 98.0, EVIDENCE_MOCK_NAME, 98.0, EVIDENCE_MOCK_NOTE), + matchSuggestion("scopus", itemFirst, "Suggestion scopus 2", "2", + 0.5, EVIDENCE_MOCK_NAME, 0.5, EVIDENCE_MOCK_NOTE)))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=scopus"), + Matchers.containsString("target=" + itemFirst.getID().toString())))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(3))); + Item itemSecond = targetSecond.getTarget(); + getClient(adminToken) + .perform(get("/api/integration/suggestions/search/findByTargetAndSource").param("source", "reciter") + .param("target", itemSecond.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestions", Matchers.containsInAnyOrder( + matchSuggestion("reciter", itemSecond, "Suggestion reciter 1", "1"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 2", "2"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 3", "3"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 4", "4"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 5", "5"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 6", "6"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 7", "7"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 8", "8"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 9", "9"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 10", "10"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 11", "11")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(11))); + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken) + .perform(get("/api/integration/suggestions/search/findByTargetAndSource").param("source", "reciter") + .param("target", itemSecond.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestions", Matchers.containsInAnyOrder( + matchSuggestion("reciter", itemSecond, "Suggestion reciter 1", "1"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 2", "2"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 3", "3"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 4", "4"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 5", "5"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 6", "6"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 7", "7"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 8", "8"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 9", "9"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 10", "10"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 11", "11")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(11))); + } + + @Test + public void findByTargetAndSourcePaginationTest() throws Exception { + context.turnOffAuthorisationSystem(); + SuggestionTarget targetSecond = SuggestionTargetBuilder + .createTarget(context, colPeople, "Digilio, Giuseppe", eperson).withSuggestionCount("reciter", 11) + .build(); + context.restoreAuthSystemState(); + String adminToken = getAuthToken(admin.getEmail(), password); + Item itemSecond = targetSecond.getTarget(); + getClient(adminToken) + .perform(get("/api/integration/suggestions/search/findByTargetAndSource").param("source", "reciter") + .param("size", "5") + .param("target", itemSecond.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestions", Matchers.contains( + matchSuggestion("reciter", itemSecond, "Suggestion reciter 1", "1"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 3", "3"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 5", "5"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 7", "7"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 9", "9")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=1"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=2"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=0"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.prev.href").doesNotExist()) + .andExpect(jsonPath("$.page.size", is(5))).andExpect(jsonPath("$.page.totalElements", is(11))); + + getClient(adminToken) + .perform(get("/api/integration/suggestions/search/findByTargetAndSource").param("source", "reciter") + .param("size", "5") + .param("page", "1") + .param("target", itemSecond.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestions", Matchers.contains( + matchSuggestion("reciter", itemSecond, "Suggestion reciter 11", "11"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 10", "10"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 8", "8"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 6", "6"), + matchSuggestion("reciter", itemSecond, "Suggestion reciter 4", "4")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=1"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=2"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=0"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=2"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=0"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$.page.size", is(5))).andExpect(jsonPath("$.page.totalElements", is(11))); + + getClient(adminToken) + .perform(get("/api/integration/suggestions/search/findByTargetAndSource").param("source", "reciter") + .param("size", "5") + .param("page", "2") + .param("target", itemSecond.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestions", Matchers.contains( + matchSuggestion("reciter", itemSecond, "Suggestion reciter 2", "2")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=2"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=1"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=2"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString( + "/api/integration/suggestions/search/findByTargetAndSource?"), + Matchers.containsString("source=reciter"), + Matchers.containsString("page=0"), + Matchers.containsString("size=5"), + Matchers.containsString("target=" + itemSecond.getID().toString())))) + .andExpect(jsonPath("$._links.next.href").doesNotExist()) + .andExpect(jsonPath("$.page.size", is(5))).andExpect(jsonPath("$.page.totalElements", is(11))); + } + + @Test + public void findByTargetAndSourceNotAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + SuggestionTarget targetSecond = SuggestionTargetBuilder + .createTarget(context, colPeople, "Digilio, Giuseppe", eperson).withSuggestionCount("reciter", 11) + .build(); + context.restoreAuthSystemState(); + // anonymous cannot access the suggestions source endpoint + getClient() + .perform(get("/api/integration/suggestions/search/findByTargetAndSource") + .param("target", UUID.randomUUID().toString()).param("source", "reciter")) + .andExpect(status().isUnauthorized()); + // nor normal user + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson) + .perform(get("/api/integration/suggestions/search/findByTargetAndSource") + .param("target", UUID.randomUUID().toString()).param("source", "reciter")) + .andExpect(status().isForbidden()); + } + + @Test + public void findOneTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + SuggestionTarget targetSecond = SuggestionTargetBuilder + .createTarget(context, colPeople, "Digilio, Giuseppe", eperson).withSuggestionCount("reciter", 11) + .build(); + context.restoreAuthSystemState(); + + String adminToken = getAuthToken(admin.getEmail(), password); + String suggestionId = "reciter:" + itemFirst.getID().toString() + ":6"; + getClient(adminToken).perform(get("/api/integration/suggestions/" + suggestionId)).andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchSuggestion("reciter", itemFirst, "Suggestion reciter 6", "6"))) + .andExpect(jsonPath("$._links.self.href", + Matchers.endsWith("/api/integration/suggestions/" + suggestionId))); + Item itemSecond = targetSecond.getTarget(); + String epersonSuggestionId = "reciter:" + itemSecond.getID().toString() + ":2"; + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/integration/suggestions/" + epersonSuggestionId)) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchSuggestion("reciter", itemSecond, "Suggestion reciter 2", "2"))) + .andExpect(jsonPath("$._links.self.href", + Matchers.endsWith("/api/integration/suggestions/" + epersonSuggestionId))); + } + + @Test + public void findOneNotAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + SuggestionTarget targetSecond = SuggestionTargetBuilder + .createTarget(context, colPeople, "Digilio, Giuseppe", eperson).withSuggestionCount("reciter", 11) + .build(); + context.restoreAuthSystemState(); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + String suggestionId = "reciter:" + itemFirst.getID().toString() + ":6"; + getClient(epersonToken).perform(get("/api/integration/suggestions/" + suggestionId)) + .andExpect(status().isForbidden()); + getClient(epersonToken).perform(get("/api/integration/suggestions/not-exist")) + .andExpect(status().isForbidden()); + getClient().perform(get("/api/integration/suggestions/" + suggestionId)).andExpect(status().isUnauthorized()); + getClient().perform(get("/api/integration/suggestions/not-exist")).andExpect(status().isUnauthorized()); + } + + @Test + public void acceptSuggestionTest() throws Exception { + context.turnOffAuthorisationSystem(); + Collection colPublications = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Publications").build(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 2).build(); + context.restoreAuthSystemState(); + + String adminToken = getAuthToken(admin.getEmail(), password); + String suggestionId = "reciter:" + itemFirst.getID().toString() + ":1"; + // the suggestion is here + getClient(adminToken).perform(get("/api/integration/suggestions/" + suggestionId)).andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchSuggestion("reciter", itemFirst, "Suggestion reciter 1", "1"))) + .andExpect(jsonPath("$._links.self.href", + Matchers.endsWith("/api/integration/suggestions/" + suggestionId))); + Integer workspaceItemId = null; + try { + ObjectMapper mapper = new ObjectMapper(); + MvcResult mvcResult = getClient(adminToken).perform( + post("/api/submission/workspaceitems?owningCollection=" + colPublications.getID().toString()) + .contentType(parseMediaType(TEXT_URI_LIST_VALUE)) + .content("http://localhost/api/integration/externalsources/" + + MockSuggestionExternalDataSource.NAME + "/entryValues/" + suggestionId)) + .andExpect(status().isCreated()).andReturn(); + String content = mvcResult.getResponse().getContentAsString(); + Map map = mapper.readValue(content, Map.class); + workspaceItemId = (Integer) map.get("id"); + String itemUuidString = String.valueOf(((Map) ((Map) map.get("_embedded")).get("item")).get("uuid")); + + getClient(adminToken).perform(get("/api/submission/workspaceitems/" + workspaceItemId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.allOf( + hasJsonPath("$.id", is(workspaceItemId)), + hasJsonPath("$.type", is("workspaceitem")), + hasJsonPath("$._embedded.item", Matchers.allOf( + hasJsonPath("$.id", is(itemUuidString)), + hasJsonPath("$.uuid", is(itemUuidString)), + hasJsonPath("$.type", is("item")), + hasJsonPath("$.metadata", Matchers.allOf( + MetadataMatcher.matchMetadata("dc.title", "Title Suggestion 1") + ))))) + )); + + getClient(adminToken).perform(get("/api/integration/suggestions/" + suggestionId)) + .andExpect(status().isNotFound()); + // 1 suggestion is still pending + getClient(adminToken) + .perform(get("/api/integration/suggestions/search/findByTargetAndSource").param("source", "reciter") + .param("target", itemFirst.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestions", Matchers.contains( + matchSuggestion("reciter", itemFirst, "Suggestion reciter 2", "2")))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + } finally { + if (workspaceItemId != null) { + WorkspaceItemBuilder.deleteWorkspaceItem(workspaceItemId); + } + } + } + + @Test + public void rejectSuggestionTest() throws Exception { + context.turnOffAuthorisationSystem(); + Collection colPublications = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Publications").build(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 2).build(); + context.restoreAuthSystemState(); + + String adminToken = getAuthToken(admin.getEmail(), password); + String suggestionId = "reciter:" + itemFirst.getID().toString() + ":1"; + // reject the suggestion + getClient(adminToken).perform(delete("/api/integration/suggestions/" + suggestionId)) + .andExpect(status().isNoContent()); + getClient(adminToken).perform(get("/api/integration/suggestions/" + suggestionId)) + .andExpect(status().isNotFound()); + // 1 suggestion is still pending + getClient(adminToken) + .perform(get("/api/integration/suggestions/search/findByTargetAndSource").param("source", "reciter") + .param("target", itemFirst.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestions", Matchers.contains( + matchSuggestion("reciter", itemFirst, "Suggestion reciter 2", "2")))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionSourceRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionSourceRestRepositoryIT.java new file mode 100644 index 000000000000..30a1779fbd80 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionSourceRestRepositoryIT.java @@ -0,0 +1,168 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +import static org.dspace.app.rest.matcher.SuggestionSourceMatcher.matchSuggestionSource; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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 org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.suggestion.SuggestionTarget; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.SuggestionTargetBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration Tests against the /api/integration/suggestionsources endpoint + */ +public class SuggestionSourceRestRepositoryIT extends AbstractControllerIntegrationTest { + private Collection colPeople; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + // We turn off the authorization system in order to create the structure as + // defined below + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + colPeople = CollectionBuilder.createCollection(context, parentCommunity).withName("People") + .withEntityType("Person").build(); + context.restoreAuthSystemState(); + } + + /** + * Build a list of suggestion target, Bollini, Andrea has suggestion from both + * sources, Digilio, Giuseppe only from reciter Test 0, 3, 6 from both sources, + * Test 1, 2, 4, 5 only from ReCiter and finally Lombardi, Corrado only from + * scopus + */ + private void buildSuggestionTargetsList() { + // We turn off the authorization system in order to create the structure as + // defined below + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + SuggestionTarget targetSecond = SuggestionTargetBuilder + .createTarget(context, colPeople, "Digilio, Giuseppe", eperson).withSuggestionCount("reciter", 11) + .build(); + for (int idx = 0; idx < 8; idx++) { + Item item = ItemBuilder.createItem(context, colPeople).withTitle("Test " + idx).build(); + SuggestionTargetBuilder.createTarget(context, item).withSuggestionCount("reciter", idx + 3).build(); + if (idx % 3 == 0) { + SuggestionTargetBuilder.createTarget(context, item).withSuggestionCount("scopus", idx + 7).build(); + } + } + Item itemLast = ItemBuilder.createItem(context, colPeople).withTitle("Lombardi, Corrado").build(); + SuggestionTarget targetLast = SuggestionTargetBuilder.createTarget(context, itemLast) + .withSuggestionCount("scopus", 3).build(); + context.restoreAuthSystemState(); + } + + @Test + public void findAllTest() throws Exception { + buildSuggestionTargetsList(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/integration/suggestionsources")).andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestionsources", + Matchers.contains(matchSuggestionSource("reciter", 10), matchSuggestionSource("scopus", 5)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(2))); + } + + @Test + public void findAllPaginationTest() throws Exception { + buildSuggestionTargetsList(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/integration/suggestionsources").param("size", "1")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestionsources", + Matchers.contains(matchSuggestionSource("reciter", 10)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/integration/suggestionsources"))) + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf(Matchers.containsString("/api/integration/suggestionsources?"), + Matchers.containsString("page=1"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf(Matchers.containsString("/api/integration/suggestionsources?"), + Matchers.containsString("page=1"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf(Matchers.containsString("/api/integration/suggestionsources?"), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href").doesNotExist()).andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + getClient(adminToken).perform(get("/api/integration/suggestionsources").param("size", "1").param("page", "1")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestionsources", + Matchers.contains(matchSuggestionSource("scopus", 5)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/integration/suggestionsources"))) + .andExpect(jsonPath("$._links.next.href").doesNotExist()) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf(Matchers.containsString("/api/integration/suggestionsources?"), + Matchers.containsString("page=1"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf(Matchers.containsString("/api/integration/suggestionsources?"), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf(Matchers.containsString("/api/integration/suggestionsources?"), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$.page.size", is(1))).andExpect(jsonPath("$.page.totalElements", is(2))); + } + + @Test + public void findAllNotAdminTest() throws Exception { + buildSuggestionTargetsList(); + // anonymous cannot access the suggestions source endpoint + getClient().perform(get("/api/integration/suggestionsources")).andExpect(status().isUnauthorized()); + // nor normal user + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson).perform(get("/api/integration/suggestionsources")).andExpect(status().isForbidden()); + + } + + @Test + public void findOneTest() throws Exception { + buildSuggestionTargetsList(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/integration/suggestionsources/reciter")).andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchSuggestionSource("reciter", 10))).andExpect(jsonPath("$._links.self.href", + Matchers.endsWith("/api/integration/suggestionsources/reciter"))); + getClient(adminToken).perform(get("/api/integration/suggestionsources/scopus")).andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchSuggestionSource("scopus", 5))).andExpect( + jsonPath("$._links.self.href", Matchers.endsWith("/api/integration/suggestionsources/scopus"))); + + } + + @Test + public void findOneNotAdminTest() throws Exception { + buildSuggestionTargetsList(); + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/integration/suggestionsources/reciter")) + .andExpect(status().isForbidden()); + getClient(epersonToken).perform(get("/api/integration/suggestionsources/not-exist")) + .andExpect(status().isForbidden()); + getClient().perform(get("/api/integration/suggestionsources/reciter")).andExpect(status().isUnauthorized()); + getClient().perform(get("/api/integration/suggestionsources/not-exist")).andExpect(status().isUnauthorized()); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionTargetRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionTargetRestRepositoryIT.java new file mode 100644 index 000000000000..21108010f56c --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SuggestionTargetRestRepositoryIT.java @@ -0,0 +1,597 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +import static org.dspace.app.rest.matcher.SuggestionTargetMatcher.matchSuggestionTarget; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +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 org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.suggestion.SuggestionTarget; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.SuggestionTargetBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.eperson.EPerson; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration Tests against the /api/integration/suggestiontargets endpoint + */ +public class SuggestionTargetRestRepositoryIT extends AbstractControllerIntegrationTest { + private Collection colPeople; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + // We turn off the authorization system in order to create the structure as + // defined below + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + colPeople = CollectionBuilder.createCollection(context, parentCommunity).withName("People") + .withEntityType("Person").build(); + context.restoreAuthSystemState(); + } + + /** + * Build a list of suggestion target, Bollini, Andrea has suggestion from both + * sources, Digilio, Giuseppe only from reciter Test 0, 3, 6 from both sources, + * Test 1, 2, 4, 5 only from ReCiter and finally Lombardi, Corrado only from + * scopus + */ + private void buildSuggestionTargetsList() { + // We turn off the authorization system in order to create the structure as + // defined below + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + SuggestionTarget targetSecond = SuggestionTargetBuilder + .createTarget(context, colPeople, "Digilio, Giuseppe", eperson).withSuggestionCount("reciter", 11) + .build(); + for (int idx = 0; idx < 8; idx++) { + Item item = ItemBuilder.createItem(context, colPeople).withTitle("Test " + idx).build(); + SuggestionTargetBuilder.createTarget(context, item).withSuggestionCount("reciter", idx + 3).build(); + if (idx % 3 == 0) { + SuggestionTargetBuilder.createTarget(context, item).withSuggestionCount("scopus", idx + 7).build(); + } + } + Item itemLast = ItemBuilder.createItem(context, colPeople).withTitle("Lombardi, Corrado").build(); + SuggestionTarget targetLast = SuggestionTargetBuilder.createTarget(context, itemLast) + .withSuggestionCount("scopus", 3).build(); + context.restoreAuthSystemState(); + } + + @Test + public void findAllTest() throws Exception { + buildSuggestionTargetsList(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/integration/suggestiontargets")) + .andExpect(status().isMethodNotAllowed()); + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/integration/suggestiontargets")).andExpect(status().isMethodNotAllowed()); + getClient().perform(get("/api/integration/suggestiontargets")).andExpect(status().isMethodNotAllowed()); + } + + @Test + public void findBySourceTest() throws Exception { + buildSuggestionTargetsList(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "reciter")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", Matchers.contains( + matchSuggestionTarget("Bollini, Andrea", "reciter", 31), + matchSuggestionTarget("Digilio, Giuseppe", "reciter", 11), + matchSuggestionTarget("Test 7", "reciter", 10), matchSuggestionTarget("Test 6", "reciter", 9), + matchSuggestionTarget("Test 5", "reciter", 8), matchSuggestionTarget("Test 4", "reciter", 7), + matchSuggestionTarget("Test 3", "reciter", 6), matchSuggestionTarget("Test 2", "reciter", 5), + matchSuggestionTarget("Test 1", "reciter", 4), matchSuggestionTarget("Test 0", "reciter", 3) + ))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString( + "/api/integration/suggestiontargets/search/findBySource?source=reciter"))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(10))); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "scopus")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.containsInAnyOrder( + matchSuggestionTarget("Test 6", "scopus", 13), + matchSuggestionTarget("Test 3", "scopus", 10), + matchSuggestionTarget("Test 0", "scopus", 7), + matchSuggestionTarget("Bollini, Andrea", "scopus", 3), + matchSuggestionTarget("Lombardi, Corrado", "scopus", 3)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString( + "/api/integration/suggestiontargets/search/findBySource?source=scopus"))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(5))); + } + + @Test + public void findBySourcePaginationTest() throws Exception { + buildSuggestionTargetsList(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findBySource") + .param("source", "reciter").param("size", "1")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.contains(matchSuggestionTarget("Bollini, Andrea", "reciter", 31)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=1"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=9"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=0"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href").doesNotExist()).andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.totalElements", is(10))); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "reciter") + .param("size", "1").param("page", "1")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.contains(matchSuggestionTarget("Digilio, Giuseppe", "reciter", 11)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=1"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=2"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=9"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=0"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=0"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$.page.size", is(1))).andExpect(jsonPath("$.page.totalElements", is(10))); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "reciter") + .param("size", "1").param("page", "9")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.contains(matchSuggestionTarget("Test 0", "reciter", 3)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=9"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.next.href").doesNotExist()) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=9"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=0"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=reciter"), Matchers.containsString("page=8"), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$.page.size", is(1))).andExpect(jsonPath("$.page.totalElements", is(10))); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "scopus") + .param("size", "3").param("page", "0")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.contains( + matchSuggestionTarget("Test 6", "scopus", 13), + matchSuggestionTarget("Test 3", "scopus", 10), + matchSuggestionTarget("Test 0", "scopus", 7)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=scopus"), Matchers.containsString("page=0"), + Matchers.containsString("size=3")))) + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=scopus"), Matchers.containsString("page=1"), + Matchers.containsString("size=3")))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=scopus"), Matchers.containsString("page=1"), + Matchers.containsString("size=3")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=scopus"), Matchers.containsString("page=0"), + Matchers.containsString("size=3")))) + .andExpect(jsonPath("$._links.prev.href").doesNotExist()).andExpect(jsonPath("$.page.size", is(3))) + .andExpect(jsonPath("$.page.totalElements", is(5))); + + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "scopus") + .param("size", "3").param("page", "1")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", Matchers.iterableWithSize(2))) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.containsInAnyOrder(matchSuggestionTarget("Bollini, Andrea", "scopus", 3), + matchSuggestionTarget("Lombardi, Corrado", "scopus", 3)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=scopus"), Matchers.containsString("page=1"), + Matchers.containsString("size=3")))) + .andExpect(jsonPath("$._links.next.href").doesNotExist()) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=scopus"), Matchers.containsString("page=1"), + Matchers.containsString("size=3")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=scopus"), Matchers.containsString("page=0"), + Matchers.containsString("size=3")))) + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findBySource?"), + Matchers.containsString("source=scopus"), Matchers.containsString("page=0"), + Matchers.containsString("size=3")))) + .andExpect(jsonPath("$.page.size", is(3))).andExpect(jsonPath("$.page.totalElements", is(5))); + } + + @Test + public void findBySourceUnAuthenticatedTest() throws Exception { + buildSuggestionTargetsList(); + // anonymous cannot access the suggestions endpoint + getClient().perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "reciter")) + .andExpect(status().isUnauthorized()); + getClient().perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "not-exist")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findBySourceForbiddenTest() throws Exception { + buildSuggestionTargetsList(); + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson) + .perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "reciter")) + .andExpect(status().isForbidden()); + getClient(tokenEperson) + .perform(get("/api/integration/suggestiontargets/search/findBySource").param("source", "not-exist")) + .andExpect(status().isForbidden()); + } + + @Test + public void findBySourceBadRequestTest() throws Exception { + String tokenEperson = getAuthToken(admin.getEmail(), password); + getClient(tokenEperson).perform(get("/api/integration/suggestiontargets/search/findBySource")) + .andExpect(status().isBadRequest()); + } + + @Test + public void findOneTest() throws Exception { + context.turnOffAuthorisationSystem(); + SuggestionTarget target = SuggestionTargetBuilder.createTarget(context, colPeople, "Bollini, Andrea") + .withSuggestionCount("scopus", 3).build(); + SuggestionTarget targetEPerson = SuggestionTargetBuilder + .createTarget(context, colPeople, "Digilio, Giuseppe", eperson).withSuggestionCount("reciter", 11) + .build(); + context.restoreAuthSystemState(); + String uuidStr = target.getID().toString(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/integration/suggestiontargets/" + uuidStr)).andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchSuggestionTarget("Bollini, Andrea", "scopus", 3))).andExpect(jsonPath( + "$._links.self.href", Matchers.endsWith("/api/integration/suggestiontargets/" + uuidStr))); + // build a person profile linked to our eperson + String uuidStrEpersonProfile = targetEPerson.getID().toString(); + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson).perform(get("/api/integration/suggestiontargets/" + uuidStrEpersonProfile)) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchSuggestionTarget("Digilio, Giuseppe", "reciter", 11))) + .andExpect(jsonPath("$._links.self.href", + Matchers.endsWith("/api/integration/suggestiontargets/" + uuidStrEpersonProfile))); + } + + @Test + public void findOneFullProjectionTest() throws Exception { + context.turnOffAuthorisationSystem(); + SuggestionTarget target = SuggestionTargetBuilder.createTarget(context, colPeople, "Bollini, Andrea") + .withSuggestionCount("scopus", 3).build(); + SuggestionTarget targetEPerson = SuggestionTargetBuilder + .createTarget(context, colPeople, "Digilio, Giuseppe", eperson).withSuggestionCount("reciter", 11) + .build(); + context.restoreAuthSystemState(); + String uuidStrTarget = target.getID().toString(); + String uuidStrProfile = target.getTarget().getID().toString(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/" + uuidStrTarget).param("projection", "full")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchSuggestionTarget("Bollini, Andrea", "scopus", 3))) + .andExpect(jsonPath("$._links.self.href", + Matchers.endsWith("/api/integration/suggestiontargets/" + uuidStrTarget))) + .andExpect(jsonPath("$._embedded.target.id", Matchers.is(uuidStrProfile))); + String uuidStrEpersonTarget = targetEPerson.getID().toString(); + String uuidStrEpersonProfile = targetEPerson.getTarget().getID().toString(); + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken) + .perform(get("/api/integration/suggestiontargets/" + uuidStrEpersonTarget).param("projection", "full")) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchSuggestionTarget("Digilio, Giuseppe", "reciter", 11))) + .andExpect(jsonPath("$._links.self.href", + Matchers.endsWith("/api/integration/suggestiontargets/" + uuidStrEpersonTarget))) + .andExpect(jsonPath("$._embedded.target.id", Matchers.is(uuidStrEpersonProfile))); + } + + @Test + public void findOneUnAuthenticatedTest() throws Exception { + context.turnOffAuthorisationSystem(); + SuggestionTarget target = SuggestionTargetBuilder.createTarget(context, colPeople, "Bollini, Andrea") + .withSuggestionCount("reciter", 31).build(); + context.restoreAuthSystemState(); + String uuidStr = target.getID().toString(); + getClient().perform(get("/api/integration/suggestiontargets/" + uuidStr)).andExpect(status().isUnauthorized()); + } + + @Test + public void findOneForbiddenTest() throws Exception { + // build a generic person profile + context.turnOffAuthorisationSystem(); + SuggestionTarget target = SuggestionTargetBuilder.createTarget(context, colPeople, "Bollini, Andrea") + .withSuggestionCount("reciter", 31).build(); + context.restoreAuthSystemState(); + String uuidStr = target.getID().toString(); + String tokenEperson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEperson).perform(get("/api/integration/suggestiontargets/" + uuidStr)) + .andExpect(status().isForbidden()); + } + + @Test + public void findOneTestWrongID() throws Exception { + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/integration/suggestiontargets/not-an-uuid")) + .andExpect(status().isNotFound()); + getClient(adminToken).perform(get("/api/integration/suggestiontargets/" + UUID.randomUUID())) + .andExpect(status().isNotFound()); + getClient(adminToken).perform(get("/api/integration/suggestiontargets/scopus:" + UUID.randomUUID())) + .andExpect(status().isNotFound()); + getClient(adminToken).perform(get("/api/integration/suggestiontargets/invalid:" + UUID.randomUUID())) + .andExpect(status().isNotFound()); + + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken).perform(get("/api/integration/suggestiontargets/not-an-uuid")) + .andExpect(status().isForbidden()); + getClient(epersonToken).perform(get("/api/integration/suggestiontargets/" + UUID.randomUUID())) + .andExpect(status().isForbidden()); + getClient(epersonToken).perform(get("/api/integration/suggestiontargets/scopus:" + UUID.randomUUID())) + .andExpect(status().isForbidden()); + getClient(epersonToken).perform(get("/api/integration/suggestiontargets/invalid:" + UUID.randomUUID())) + .andExpect(status().isForbidden()); + + getClient().perform(get("/api/integration/suggestiontargets/not-an-uuid")).andExpect(status().isUnauthorized()); + getClient().perform(get("/api/integration/suggestiontargets/" + UUID.randomUUID())) + .andExpect(status().isUnauthorized()); + getClient().perform(get("/api/integration/suggestiontargets/scopus:" + UUID.randomUUID())) + .andExpect(status().isUnauthorized()); + getClient().perform(get("/api/integration/suggestiontargets/invalid:" + UUID.randomUUID())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findByTargetTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + Item itemLast = ItemBuilder.createItem(context, colPeople).withTitle("Lombardi, Corrado") + .withDSpaceObjectOwner(eperson.getFullName(), eperson.getID().toString()).build(); + SuggestionTarget targetLast = SuggestionTargetBuilder.createTarget(context, itemLast) + .withSuggestionCount("scopus", 2).build(); + context.restoreAuthSystemState(); + + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findByTarget").param("target", + itemFirst.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.contains(matchSuggestionTarget("Bollini, Andrea", "reciter", 31), + matchSuggestionTarget("Bollini, Andrea", "scopus", 3)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?target=" + + itemFirst.getID().toString()))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(2))); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findByTarget").param("target", + itemLast.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.contains(matchSuggestionTarget("Lombardi, Corrado", "scopus", 2)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?target=" + + itemLast.getID().toString()))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken) + .perform(get("/api/integration/suggestiontargets/search/findByTarget").param("target", + itemLast.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.contains(matchSuggestionTarget("Lombardi, Corrado", "scopus", 2)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?target=" + + itemLast.getID().toString()))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void findByTargetPaginationTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + context.restoreAuthSystemState(); + + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findByTarget").param("size", "1") + .param("target", itemFirst.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.contains(matchSuggestionTarget("Bollini, Andrea", "reciter", 31)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?"), + Matchers.containsString("target=" + itemFirst.getID().toString()), + Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?"), + Matchers.containsString("target=" + itemFirst.getID().toString()), + Matchers.containsString("page=1"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?"), + Matchers.containsString("target=" + itemFirst.getID().toString()), + Matchers.containsString("page=1"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?"), + Matchers.containsString("target=" + itemFirst.getID().toString()), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href").doesNotExist()).andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.size", is(1))).andExpect(jsonPath("$.page.totalElements", is(2))); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findByTarget").param("size", "1") + .param("page", "1").param("target", itemFirst.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.suggestiontargets", + Matchers.contains(matchSuggestionTarget("Bollini, Andrea", "scopus", 3)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?"), + Matchers.containsString("target=" + itemFirst.getID().toString()), + Matchers.containsString("size=1"), Matchers.containsString("page=1")))) + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?"), + Matchers.containsString("target=" + itemFirst.getID().toString()), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?"), + Matchers.containsString("target=" + itemFirst.getID().toString()), + Matchers.containsString("page=1"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/suggestiontargets/search/findByTarget?"), + Matchers.containsString("target=" + itemFirst.getID().toString()), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.next.href").doesNotExist()).andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.size", is(1))).andExpect(jsonPath("$.page.totalElements", is(2))); + } + + @Test + public void findByTargetUnAuthenticatedTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + Item itemLast = ItemBuilder.createItem(context, colPeople).withTitle("Lombardi, Corrado") + .withDSpaceObjectOwner(eperson.getFullName(), eperson.getID().toString()).build(); + SuggestionTarget targetLast = SuggestionTargetBuilder.createTarget(context, itemLast) + .withSuggestionCount("scopus", 2).build(); + context.restoreAuthSystemState(); + + // anonymous cannot access the suggestions endpoint + getClient().perform(get("/api/integration/suggestiontargets/search/findByTarget").param("target", + itemFirst.getID().toString())).andExpect(status().isUnauthorized()); + getClient().perform(get("/api/integration/suggestiontargets/search/findByTarget").param("target", + itemLast.getID().toString())).andExpect(status().isUnauthorized()); + } + + @Test + public void findByTargetForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + Item itemFirst = ItemBuilder.createItem(context, colPeople).withTitle("Bollini, Andrea").build(); + SuggestionTarget targetFirstReciter = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("reciter", 31).build(); + SuggestionTarget targetFirstScopus = SuggestionTargetBuilder.createTarget(context, itemFirst) + .withSuggestionCount("scopus", 3).build(); + Item itemLast = ItemBuilder.createItem(context, colPeople).withTitle("Lombardi, Corrado") + .withDSpaceObjectOwner(eperson.getFullName(), eperson.getID().toString()).build(); + SuggestionTarget targetLast = SuggestionTargetBuilder.createTarget(context, itemLast) + .withSuggestionCount("scopus", 2).build(); + EPerson anotherEPerson = EPersonBuilder.createEPerson(context).withEmail("another@example.com") + .withPassword(password).withNameInMetadata("Test", "Test").build(); + context.restoreAuthSystemState(); + + String tokenAnother = getAuthToken(anotherEPerson.getEmail(), password); + getClient(tokenAnother).perform(get("/api/integration/suggestiontargets/search/findByTarget").param("target", + itemFirst.getID().toString())).andExpect(status().isForbidden()); + getClient(tokenAnother).perform(get("/api/integration/suggestiontargets/search/findByTarget").param("target", + itemLast.getID().toString())).andExpect(status().isForbidden()); + } + + @Test + public void findByTargetBadRequestTest() throws Exception { + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken) + .perform(get("/api/integration/suggestiontargets/search/findByTarget").param("target", "not-exist")) + .andExpect(status().isBadRequest()); + getClient(adminToken).perform(get("/api/integration/suggestiontargets/search/findByTarget")) + .andExpect(status().isBadRequest()); + getClient().perform(get("/api/integration/suggestiontargets/search/findByTarget").param("target", "not-exist")) + .andExpect(status().isBadRequest()); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionMatcher.java new file mode 100644 index 000000000000..38be403cb222 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionMatcher.java @@ -0,0 +1,57 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.is; + +import org.dspace.content.Item; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; + +public class SuggestionMatcher { + + private SuggestionMatcher() { } + + // Matcher for a suggestion + public static Matcher matchSuggestion(String source, Item target, String display, + String suggestionId) { + return Matchers.allOf( + hasJsonPath("$.display", is(display)), + hasJsonPath("$.source", is(source)), + hasJsonPath("$.id", is(source + ":" + target.getID().toString() + ":" + suggestionId)), + hasJsonPath("$.metadata['dc.title'][0].value", is("Title Suggestion " + suggestionId )), + hasJsonPath("$.metadata['dc.source'][0].value", is("Source 1")), + hasJsonPath("$.metadata['dc.source'][1].value", is("Source 2")), + hasJsonPath("$.score"), + hasJsonPath("$.evidences"), + hasJsonPath("$.type", is("suggestion")) + ); + } + + public static Matcher matchSuggestion(String source, Item target, String display, + String suggestionId, double score, String evidenceName, double evidenceScore, String evidenceNote) { + return Matchers.allOf( + hasJsonPath("$.display", is(display)), + hasJsonPath("$.source", is(source)), + hasJsonPath("$.id", is(source + ":" + target.getID().toString() + ":" + suggestionId)), + hasJsonPath("$.metadata['dc.title'][0].value", is("Title Suggestion " + suggestionId )), + hasJsonPath("$.metadata['dc.source'][0].value", is("Source 1")), + hasJsonPath("$.metadata['dc.source'][1].value", is("Source 2")), + hasJsonPath("$.score", is(String.format("%.2f", score))), + hasJsonPath("$.evidences." + evidenceName, Matchers.is( + hasJsonPath("$", + Matchers.allOf( + hasJsonPath("$.score", is(String.format("%.2f", evidenceScore))), + hasJsonPath("$.notes", is(evidenceNote)))) + )), + hasJsonPath("$.type", is("suggestion")) + ); + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionSourceMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionSourceMatcher.java new file mode 100644 index 000000000000..f9d70cef8681 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionSourceMatcher.java @@ -0,0 +1,28 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.is; + +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; + +public class SuggestionSourceMatcher { + + private SuggestionSourceMatcher() { } + + // Matcher for a suggestion target + public static Matcher matchSuggestionSource(String name, int total) { + return Matchers.allOf( + hasJsonPath("$.id", is(name)), + hasJsonPath("$.total", is(total)), + hasJsonPath("$.type", is("suggestionsource")) + ); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionTargetMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionTargetMatcher.java new file mode 100644 index 000000000000..b88b51020e76 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SuggestionTargetMatcher.java @@ -0,0 +1,29 @@ +/** + * 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.is; + +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; + +public class SuggestionTargetMatcher { + + private SuggestionTargetMatcher() { } + + // Matcher for a suggestion target + public static Matcher matchSuggestionTarget(String name, String source, int total) { + return Matchers.allOf( + hasJsonPath("$.display", is(name)), + hasJsonPath("$.source", is(source)), + hasJsonPath("$.total", is(total)), + hasJsonPath("$.type", is("suggestiontarget")) + ); + } +} diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d5c28da0964c..d9cce845412c 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1603,6 +1603,7 @@ include = ${module_dir}/authentication-oidc.cfg include = ${module_dir}/authentication-password.cfg include = ${module_dir}/authentication-shibboleth.cfg include = ${module_dir}/authentication-x509.cfg +include = ${module_dir}/authority.cfg include = ${module_dir}/bulkedit.cfg include = ${module_dir}/citation-page.cfg include = ${module_dir}/clamav.cfg @@ -1621,6 +1622,7 @@ include = ${module_dir}/solr-statistics.cfg include = ${module_dir}/solrauthority.cfg include = ${module_dir}/spring.cfg include = ${module_dir}/submission-curation.cfg +include = ${module_dir}/suggestion.cfg include = ${module_dir}/sword-client.cfg include = ${module_dir}/sword-server.cfg include = ${module_dir}/swordv2-server.cfg diff --git a/dspace/config/modules/authority.cfg b/dspace/config/modules/authority.cfg new file mode 100644 index 000000000000..44384f5c3f63 --- /dev/null +++ b/dspace/config/modules/authority.cfg @@ -0,0 +1,13 @@ +#---------------------------------------------------------------# +#----------------- AUTHORITY CONFIGURATIONS --------------------# +#---------------------------------------------------------------# +# These configs are used by the authority framework # +#---------------------------------------------------------------# + +##### Authority Control Settings ##### +plugin.named.org.dspace.content.authority.ChoiceAuthority = \ + org.dspace.content.authority.EPersonAuthority = EPersonAuthority + +choices.plugin.dspace.object.owner = EPersonAuthority +choices.presentation.dspace.object.owner = suggest +authority.controlled.dspace.object.owner = true diff --git a/dspace/config/modules/suggestion.cfg b/dspace/config/modules/suggestion.cfg new file mode 100644 index 000000000000..0792c46ebfac --- /dev/null +++ b/dspace/config/modules/suggestion.cfg @@ -0,0 +1,7 @@ +#---------------------------------------------------------------# +#-------------------Suggestion CONFIGURATIONS-------------------# +#---------------------------------------------------------------# +# Configuration properties used by publication claim # +# (suggestion) service # +#---------------------------------------------------------------# +suggestion.solr.server = ${solr.server}/${solr.multicorePrefix}suggestion diff --git a/dspace/config/registries/dspace-types.xml b/dspace/config/registries/dspace-types.xml index ab0c1bc40fc4..38033cd5c548 100644 --- a/dspace/config/registries/dspace-types.xml +++ b/dspace/config/registries/dspace-types.xml @@ -43,4 +43,12 @@ enabled Stores a boolean text value (true or false) to indicate if the iiif feature is enabled or not for the dspace object. If absent the value is derived from the parent dspace object + + + dspace + object + owner + + + diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 9e28e5d55985..c85f6cb368ad 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -67,6 +67,18 @@ + + + + + + + + + + + + diff --git a/dspace/config/spring/api/openaire-integration.xml b/dspace/config/spring/api/openaire-integration.xml new file mode 100644 index 000000000000..12a013af68eb --- /dev/null +++ b/dspace/config/spring/api/openaire-integration.xml @@ -0,0 +1,226 @@ + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index e7c55549c748..5aee3bd9b298 100644 --- a/dspace/config/spring/api/scripts.xml +++ b/dspace/config/spring/api/scripts.xml @@ -50,4 +50,10 @@ + + + + + + diff --git a/dspace/config/spring/api/solr-services.xml b/dspace/config/spring/api/solr-services.xml index 698a82418422..b6129a9f67de 100644 --- a/dspace/config/spring/api/solr-services.xml +++ b/dspace/config/spring/api/solr-services.xml @@ -31,5 +31,8 @@ + + + diff --git a/dspace/config/spring/api/suggestions.xml b/dspace/config/spring/api/suggestions.xml new file mode 100644 index 000000000000..03a96d1329c8 --- /dev/null +++ b/dspace/config/spring/api/suggestions.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + dc.title + crisrp.name + crisrp.name.translated + crisrp.name.variant + + + + + + + + dc.contributor.author + + + + + dc.title + crisrp.name + crisrp.name.translated + crisrp.name.variant + + + + + + + + + + + + + diff --git a/dspace/solr/suggestion/conf/admin-extra.html b/dspace/solr/suggestion/conf/admin-extra.html new file mode 100644 index 000000000000..aa739da862cf --- /dev/null +++ b/dspace/solr/suggestion/conf/admin-extra.html @@ -0,0 +1,31 @@ + + + diff --git a/dspace/solr/suggestion/conf/elevate.xml b/dspace/solr/suggestion/conf/elevate.xml new file mode 100644 index 000000000000..7630ebe20fb9 --- /dev/null +++ b/dspace/solr/suggestion/conf/elevate.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/dspace/solr/suggestion/conf/protwords.txt b/dspace/solr/suggestion/conf/protwords.txt new file mode 100644 index 000000000000..1dfc0abecbf8 --- /dev/null +++ b/dspace/solr/suggestion/conf/protwords.txt @@ -0,0 +1,21 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# Use a protected word file to protect against the stemmer reducing two +# unrelated words to the same base word. + +# Some non-words that normally won't be encountered, +# just to test that they won't be stemmed. +dontstems +zwhacky + diff --git a/dspace/solr/suggestion/conf/schema.xml b/dspace/solr/suggestion/conf/schema.xml new file mode 100644 index 000000000000..ddb995465382 --- /dev/null +++ b/dspace/solr/suggestion/conf/schema.xml @@ -0,0 +1,547 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +suggestion_fullid + + + + + diff --git a/dspace/solr/suggestion/conf/scripts.conf b/dspace/solr/suggestion/conf/scripts.conf new file mode 100644 index 000000000000..f58b262ae0c8 --- /dev/null +++ b/dspace/solr/suggestion/conf/scripts.conf @@ -0,0 +1,24 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +user= +solr_hostname=localhost +solr_port=8983 +rsyncd_port=18983 +data_dir= +webapp_name=solr +master_host= +master_data_dir= +master_status_dir= diff --git a/dspace/solr/suggestion/conf/solrconfig.xml b/dspace/solr/suggestion/conf/solrconfig.xml new file mode 100644 index 000000000000..1ffbffe57c13 --- /dev/null +++ b/dspace/solr/suggestion/conf/solrconfig.xml @@ -0,0 +1,1943 @@ + + + + + + + + + 7.7.2 + + + + + + + + + + + + + + + ${solr.data.dir:} + + + + + + + + + + + + + + + + + + + + + + + + + 32 + 1000 + + + + + + + + + + + + ${solr.lock.type:native} + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + ${solr.ulog.dir:} + + + + + 10000 + ${solr.autoCommit.maxTime:10000} + false + + + + + + ${solr.autoSoftCommit.maxTime:1000} + + + + + + + + + + + + + + + + + + + + 1024 + + + + + + + + + + + + + + + + + + + + + + false + + + + + + 20 + + + 200 + + + + + + + + + + + + static firstSearcher warming in solrconfig.xml + + + + + + false + + + 2 + + + + + + + + + + + + + + + + + + + + + + + explicit + 10 + suggestion_id + + + + + + + + + + + + + + explicit + json + true + suggestion_id + + + + + + + + true + json + true + + + + + + + + explicit + + + velocity + browse + layout + Solritas + + + edismax + + text^0.5 features^1.0 name^1.2 sku^1.5 suggestion_id^10.0 manu^1.1 cat^1.4 + title^10.0 description^5.0 keywords^5.0 author^2.0 resourcename^1.0 + + suggestion_id + 100% + *:* + 10 + *,score + + + text^0.5 features^1.0 name^1.2 sku^1.5 suggestion_id^10.0 manu^1.1 cat^1.4 + title^10.0 description^5.0 keywords^5.0 author^2.0 resourcename^1.0 + + text,features,name,sku,suggestion_id,manu,cat,title,description,keywords,author,resourcename + 3 + + + on + cat + manu_exact + content_type + author_s + ipod + GB + 1 + cat,inStock + after + price + 0 + 600 + 50 + popularity + 0 + 10 + 3 + manufacturedate_dt + NOW/YEAR-10YEARS + NOW + +1YEAR + before + after + + + on + content features title name + html + <b> + </b> + 0 + title + 0 + name + 3 + 200 + content + 750 + + + on + false + 5 + 2 + 5 + true + true + 5 + 3 + + + + + spellcheck + + + + + + + + + + + + + + + application/json + + + + + + + application/csv + + + + + + + + true + ignored_ + + + true + links + ignored_ + + + + + + + + + + + + + + + + + + + + + + solrpingquery + + + all + + + + + + + + + explicit + true + + + + + + + + + + + + + + + + textSpell + + + default + name + ./spellchecker + + + + + + + + + + + + false + + false + + 1 + + + spellcheck + + + + + + + + true + + + tvComponent + + + + + + + + + text_general + + + + + + default + suggestion_id + solr.DirectSolrSpellChecker + + internal + + 0.5 + + 2 + + 1 + + 5 + + 4 + + 0.01 + + + + + + wordbreak + solr.WordBreakSolrSpellChecker + name + true + true + 10 + + + + + + + + + + + + + + + + suggestion_id + + default + wordbreak + on + true + 10 + 5 + 5 + true + true + 10 + 5 + + + spellcheck + + + + + + + + + + suggestion_id + true + + + tvComponent + + + + + + + + + default + + + org.carrot2.clustering.lingo.LingoClusteringAlgorithm + + + 20 + + + clustering/carrot2 + + + ENGLISH + + + stc + org.carrot2.clustering.stc.STCClusteringAlgorithm + + + + + + + true + default + true + + name + suggestion_id + + features + + true + + + + false + + edismax + + text^0.5 features^1.0 name^1.2 sku^1.5 suggestion_id^10.0 manu^1.1 cat^1.4 + + *:* + 10 + *,score + + + clustering + + + + + + + + + + true + false + + + terms + + + + + + + + string + elevate.xml + + + + + + explicit + suggestion_id + + + elevator + + + + + + + + + + + 100 + + + + + + + + 70 + + 0.5 + + [-\w ,/\n\"']{20,200} + + + + + + + ]]> + ]]> + + + + + + + + + + + + + + + + + + + + + + + + ,, + ,, + ,, + ,, + ,]]> + ]]> + + + + + + 10 + .,!? + + + + + + + WORD + + + en + US + + + + + + + + + + + + + + + + + + + + + + + + text/plain; charset=UTF-8 + + + + + + + + + 5 + + + + + + + + + + + + + + + + + + *:* + + diff --git a/dspace/solr/suggestion/conf/spellings.txt b/dspace/solr/suggestion/conf/spellings.txt new file mode 100644 index 000000000000..d7ede6f5611d --- /dev/null +++ b/dspace/solr/suggestion/conf/spellings.txt @@ -0,0 +1,2 @@ +pizza +history \ No newline at end of file diff --git a/dspace/solr/suggestion/conf/stopwords.txt b/dspace/solr/suggestion/conf/stopwords.txt new file mode 100644 index 000000000000..8433c832d2cf --- /dev/null +++ b/dspace/solr/suggestion/conf/stopwords.txt @@ -0,0 +1,57 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# a couple of test stopwords to test that the words are really being +# configured from this file: +stopworda +stopwordb + +#Standard english stop words taken from Lucene's StopAnalyzer +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +s +such +t +that +the +their +then +there +these +they +this +to +was +will +with + diff --git a/dspace/solr/suggestion/conf/synonyms.txt b/dspace/solr/suggestion/conf/synonyms.txt new file mode 100644 index 000000000000..b0e31cb7ec83 --- /dev/null +++ b/dspace/solr/suggestion/conf/synonyms.txt @@ -0,0 +1,31 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaa => aaaa +bbb => bbbb1 bbbb2 +ccc => cccc1,cccc2 +a\=>a => b\=>b +a\,a => b\,b +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma + diff --git a/dspace/solr/suggestion/core.properties b/dspace/solr/suggestion/core.properties new file mode 100644 index 000000000000..e69de29bb2d1 From d59c0b72cf218b1f2b7d661d82ecf70f897b6fb2 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 30 Mar 2022 13:43:01 +0200 Subject: [PATCH 002/118] [CST-5249] Added EPersonAuthority class --- .../content/authority/EPersonAuthority.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java diff --git a/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java b/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java new file mode 100644 index 000000000000..7a1510d7214a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/authority/EPersonAuthority.java @@ -0,0 +1,93 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.content.authority; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.apache.log4j.Logger; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.EPersonService; +import org.dspace.util.UUIDUtils; + +/** + * + * @author Mykhaylo Boychuk (4science.it) + */ +public class EPersonAuthority implements ChoiceAuthority { + private static final Logger log = Logger.getLogger(EPersonAuthority.class); + + /** + * the name assigned to the specific instance by the PluginService, @see + * {@link NameAwarePlugin} + **/ + private String authorityName; + + private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + + @Override + public Choices getBestMatch(String text, String locale) { + return getMatches(text, 0, 2, locale); + } + + @Override + public Choices getMatches(String text, int start, int limit, String locale) { + Context context = null; + if (limit <= 0) { + limit = 20; + } + context = new Context(); + List ePersons = null; + try { + ePersons = ePersonService.search(context, text, start, limit); + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + List choiceList = new ArrayList(); + for (EPerson eperson : ePersons) { + choiceList.add(new Choice(eperson.getID().toString(), eperson.getFullName(), eperson.getFullName())); + } + Choice[] results = new Choice[choiceList.size()]; + results = choiceList.toArray(results); + return new Choices(results, start, ePersons.size(), Choices.CF_AMBIGUOUS, ePersons.size() > (start + limit), 0); + } + + @Override + public String getLabel(String key, String locale) { + + UUID uuid = UUIDUtils.fromString(key); + if (uuid == null) { + return null; + } + + Context context = new Context(); + try { + EPerson ePerson = ePersonService.find(context, uuid); + return ePerson != null ? ePerson.getFullName() : null; + } catch (SQLException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e.getMessage(), e); + } + + } + + @Override + public String getPluginInstanceName() { + return authorityName; + } + + @Override + public void setPluginInstanceName(String name) { + this.authorityName = name; + } +} \ No newline at end of file From 561902b123cef0e6ad62bc842282ab4cfd9041ad Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 30 Mar 2022 17:23:13 +0200 Subject: [PATCH 003/118] [CST-5249] Fixed ExternalSourcesRestControllerIT test --- .../org/dspace/app/rest/ExternalSourcesRestControllerIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java index 4c610c74bc89..0744bc950855 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java @@ -53,7 +53,7 @@ public void findAllExternalSources() throws Exception { ExternalSourceMatcher.matchExternalSource( "openAIREFunding", "openAIREFunding", false) ))) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(7))); + .andExpect(jsonPath("$.page.totalElements", Matchers.is(8))); } @Test From 4fc5a67ac82ce329ea0c986b2647d4ebbae7fbda Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 4 May 2022 13:16:57 +0200 Subject: [PATCH 004/118] [CST-5249] Fixed suggestion import --- .../service/OpenAireImportMetadataSourceServiceImpl.java | 2 +- dspace/config/spring/api/suggestions.xml | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java index 5c19b6397085..45737b8b22af 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java @@ -333,7 +333,7 @@ private List splitToRecords(String recordsSrc) { Namespace.getNamespace("dri", "http://www.driver-repository.eu/namespace/dri"), Namespace.getNamespace("oaf", "http://namespace.openaire.eu/oaf"), Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")); - XPathExpression xpath = XPathFactory.instance().compile("/results/result", + XPathExpression xpath = XPathFactory.instance().compile("//results/result", Filters.element(), null, namespaces); List recordsList = xpath.evaluate(root); diff --git a/dspace/config/spring/api/suggestions.xml b/dspace/config/spring/api/suggestions.xml index 03a96d1329c8..c576c34427ed 100644 --- a/dspace/config/spring/api/suggestions.xml +++ b/dspace/config/spring/api/suggestions.xml @@ -32,9 +32,6 @@ dc.title - crisrp.name - crisrp.name.translated - crisrp.name.variant @@ -49,9 +46,6 @@ dc.title - crisrp.name - crisrp.name.translated - crisrp.name.variant From 71946ac2213e06f7dbb97bd7a01583f0fd1b389b Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 4 May 2022 14:02:30 +0200 Subject: [PATCH 005/118] [CST-5249] Fixed EntityTypeRestRepositoryIT test --- .../java/org/dspace/app/rest/EntityTypeRestRepositoryIT.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EntityTypeRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EntityTypeRestRepositoryIT.java index 740a2c0dc388..2de61bb43dfb 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EntityTypeRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EntityTypeRestRepositoryIT.java @@ -416,6 +416,8 @@ public void findAllByAuthorizedExternalSource() throws Exception { .setSupportedEntityTypes(Arrays.asList("Publication")); ((AbstractExternalDataProvider) externalDataService.getExternalDataProvider("pubmed")) .setSupportedEntityTypes(Arrays.asList("Publication")); + ((AbstractExternalDataProvider) externalDataService.getExternalDataProvider("suggestion")) + .setSupportedEntityTypes(Arrays.asList("Publication")); // these are similar to the previous checks but now we have restricted the mock and pubmed providers // to support only publication, this mean that there are no providers suitable for funding @@ -439,6 +441,8 @@ public void findAllByAuthorizedExternalSource() throws Exception { .setSupportedEntityTypes(null); ((AbstractExternalDataProvider) externalDataService.getExternalDataProvider("pubmed")) .setSupportedEntityTypes(null); + ((AbstractExternalDataProvider) externalDataService.getExternalDataProvider("suggestion")) + .setSupportedEntityTypes(null); } } From 3de276a797de2b34d363111d8886b5ddb8bdd051 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 7 Jul 2022 15:29:03 +0200 Subject: [PATCH 006/118] [CST-5249] Fixed ExternalSourcesRestControllerIT test --- .../org/dspace/app/rest/ExternalSourcesRestControllerIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java index 00360f9a7173..4aefefdad8e1 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java @@ -54,7 +54,7 @@ public void findAllExternalSources() throws Exception { ExternalSourceMatcher.matchExternalSource( "openAIREFunding", "openAIREFunding", false) ))) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(9))); + .andExpect(jsonPath("$.page.totalElements", Matchers.is(10))); } @Test From a86d6ef2f73dd6cd13ddc152ed64befacebe6154 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 8 Jul 2022 17:26:15 +0200 Subject: [PATCH 007/118] [CST-5249] Improved QAEventProcessed test --- .../org/dspace/content/ItemServiceImpl.java | 9 ++ .../org/dspace/content/QAEventProcessed.java | 6 +- .../dspace/eperson/EPersonServiceImpl.java | 9 ++ .../qaevent/OpenaireEventsRunnableCli.java | 11 -- .../main/java/org/dspace/qaevent/QATopic.java | 3 +- .../org/dspace/qaevent/dao/QAEventsDao.java | 32 ++++- .../qaevent/dao/impl/QAEventsDaoImpl.java | 25 ++++ .../qaevent/service/QAEventService.java | 8 ++ .../service/impl/QAEventServiceImpl.java | 127 ++++++++++-------- ...java => QAEventRelatedRestController.java} | 63 +++++---- .../repository/QAEventRestRepository.java | 43 +++--- .../app/rest/QAEventRestRepositoryIT.java | 59 +++++++- 12 files changed, 274 insertions(+), 121 deletions(-) rename dspace-server-webapp/src/main/java/org/dspace/app/rest/{QAEventRestController.java => QAEventRelatedRestController.java} (71%) diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 27d0ba189cbd..620f499f53b4 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -67,6 +67,7 @@ import org.dspace.orcid.service.OrcidSynchronizationService; import org.dspace.orcid.service.OrcidTokenService; import org.dspace.profile.service.ResearcherProfileService; +import org.dspace.qaevent.dao.QAEventsDao; import org.dspace.services.ConfigurationService; import org.dspace.versioning.service.VersioningService; import org.dspace.workflow.WorkflowItemService; @@ -149,6 +150,9 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It @Autowired(required = true) private ResearcherProfileService researcherProfileService; + @Autowired + private QAEventsDao qaEventsDao; + protected ItemServiceImpl() { super(); } @@ -784,6 +788,11 @@ protected void rawDelete(Context context, Item item) throws AuthorizeException, orcidToken.setProfileItem(null); } + List qaEvents = qaEventsDao.findByItem(context, item); + for (QAEventProcessed qaEvent : qaEvents) { + qaEventsDao.delete(context, qaEvent); + } + //Only clear collections after we have removed everything else from the item item.clearCollections(); item.setOwningCollection(null); diff --git a/dspace-api/src/main/java/org/dspace/content/QAEventProcessed.java b/dspace-api/src/main/java/org/dspace/content/QAEventProcessed.java index 3657c2fdc458..3631a2ff68c6 100644 --- a/dspace-api/src/main/java/org/dspace/content/QAEventProcessed.java +++ b/dspace-api/src/main/java/org/dspace/content/QAEventProcessed.java @@ -13,7 +13,7 @@ import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.JoinColumn; -import javax.persistence.OneToOne; +import javax.persistence.ManyToOne; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; @@ -40,11 +40,11 @@ public class QAEventProcessed implements Serializable { private Date eventTimestamp; @JoinColumn(name = "eperson_uuid") - @OneToOne + @ManyToOne private EPerson eperson; @JoinColumn(name = "item_uuid") - @OneToOne + @ManyToOne private Item item; public String getEventId() { diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java index 61477995c7ed..c368e81ad681 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java @@ -33,6 +33,7 @@ import org.dspace.content.Item; import org.dspace.content.MetadataField; import org.dspace.content.MetadataValue; +import org.dspace.content.QAEventProcessed; import org.dspace.content.WorkspaceItem; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.ItemService; @@ -47,6 +48,7 @@ import org.dspace.eperson.service.SubscribeService; import org.dspace.event.Event; import org.dspace.orcid.service.OrcidTokenService; +import org.dspace.qaevent.dao.QAEventsDao; import org.dspace.util.UUIDUtils; import org.dspace.versioning.Version; import org.dspace.versioning.VersionHistory; @@ -103,6 +105,8 @@ public class EPersonServiceImpl extends DSpaceObjectServiceImpl impleme protected ClaimedTaskService claimedTaskService; @Autowired protected OrcidTokenService orcidTokenService; + @Autowired + protected QAEventsDao qaEventsDao; protected EPersonServiceImpl() { super(); @@ -391,6 +395,11 @@ public void delete(Context context, EPerson ePerson, boolean cascade) // Remove any subscriptions subscribeService.deleteByEPerson(context, ePerson); + List qaEvents = qaEventsDao.findByEPerson(context, ePerson); + for (QAEventProcessed qaEvent : qaEvents) { + qaEventsDao.delete(context, qaEvent); + } + // Remove ourself ePersonDAO.delete(context, ePerson); diff --git a/dspace-api/src/main/java/org/dspace/qaevent/OpenaireEventsRunnableCli.java b/dspace-api/src/main/java/org/dspace/qaevent/OpenaireEventsRunnableCli.java index c8f260a072c4..9da0d964ef85 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/OpenaireEventsRunnableCli.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/OpenaireEventsRunnableCli.java @@ -7,10 +7,6 @@ */ package org.dspace.qaevent; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; - import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.ParseException; import org.dspace.utils.DSpace; @@ -41,14 +37,7 @@ public void setup() throws ParseException { formatter.printHelp("Import Notification event json file", getScriptConfiguration().getOptions()); System.exit(0); } - } - /** - * Get the events input stream from a local file. - */ - @Override - protected InputStream getQAEventsInputStream() throws Exception { - return new FileInputStream(new File(fileLocation)); } } diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java b/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java index 1ce09fe45dcf..63e523b9cb5e 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java @@ -10,7 +10,8 @@ import java.util.Date; /** - * This model class represent the quality assurance broker topic concept + * This model class represent the quality assurance broker topic concept. A + * topic represents a type of event and is therefore used to group events. * * @author Andrea Bollini (andrea.bollini at 4science.it) * diff --git a/dspace-api/src/main/java/org/dspace/qaevent/dao/QAEventsDao.java b/dspace-api/src/main/java/org/dspace/qaevent/dao/QAEventsDao.java index 30a74e55baf6..9de2d124602c 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/dao/QAEventsDao.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/dao/QAEventsDao.java @@ -13,6 +13,7 @@ import org.dspace.content.Item; import org.dspace.content.QAEventProcessed; import org.dspace.core.Context; +import org.dspace.core.GenericDAO; import org.dspace.eperson.EPerson; /** @@ -21,7 +22,36 @@ * @author Andrea Bollini (andrea.bollini at 4science.it) * */ -public interface QAEventsDao { +public interface QAEventsDao extends GenericDAO { + + /** + * Returns all the stored QAEventProcessed entities. + * + * @param context the DSpace context + * @return the found entities + * @throws SQLException if an SQL error occurs + */ + public List findAll(Context context) throws SQLException; + + /** + * Returns the stored QAEventProcessed entities by item. + * + * @param context the DSpace context + * @param item the item to search for + * @return the found entities + * @throws SQLException if an SQL error occurs + */ + public List findByItem(Context context, Item item) throws SQLException; + + /** + * Returns the stored QAEventProcessed entities by eperson. + * + * @param context the DSpace context + * @param ePerson the ePerson to search for + * @return the found entities + * @throws SQLException if an SQL error occurs + */ + public List findByEPerson(Context context, EPerson ePerson) throws SQLException; /** * Search a page of quality assurance broker events by notification ID. diff --git a/dspace-api/src/main/java/org/dspace/qaevent/dao/impl/QAEventsDaoImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/dao/impl/QAEventsDaoImpl.java index 550027441b93..3263ae9de9b7 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/dao/impl/QAEventsDaoImpl.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/dao/impl/QAEventsDaoImpl.java @@ -28,6 +28,11 @@ */ public class QAEventsDaoImpl extends AbstractHibernateDAO implements QAEventsDao { + @Override + public List findAll(Context context) throws SQLException { + return findAll(context, QAEventProcessed.class); + } + @Override public boolean storeEvent(Context context, String checksum, EPerson eperson, Item item) { QAEventProcessed qaEvent = new QAEventProcessed(); @@ -62,4 +67,24 @@ public List searchByEventId(Context context, String eventId, I return findMany(context, query); } + @Override + public List findByItem(Context context, Item item) throws SQLException { + Query query = createQuery(context, "" + + " SELECT qaevent " + + " FROM QAEventProcessed qaevent " + + " WHERE qaevent.item = :item "); + query.setParameter("item", item); + return findMany(context, query); + } + + @Override + public List findByEPerson(Context context, EPerson ePerson) throws SQLException { + Query query = createQuery(context, "" + + " SELECT qaevent " + + " FROM QAEventProcessed qaevent " + + " WHERE qaevent.eperson = :eperson "); + query.setParameter("eperson", ePerson); + return findMany(context, query); + } + } diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventService.java b/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventService.java index 8e840037de9a..85fd6fa073a3 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventService.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventService.java @@ -144,4 +144,12 @@ public List findEventsByTopicAndPage(String topic, long offset, int pag */ public long countSources(); + /** + * Check if the given QA event supports a related item. + * + * @param qaevent the event to be verified + * @return true if the event supports a related item, false otherwise. + */ + public boolean isRelatedItemSupported(QAEvent qaevent); + } diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java index 04a830358c35..628a19752c0a 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java @@ -8,6 +8,8 @@ package org.dspace.qaevent.service.impl; import static java.util.Comparator.comparing; +import static org.apache.commons.lang3.StringUtils.endsWith; +import static org.dspace.content.QAEvent.OPENAIRE_SOURCE; import java.io.IOException; import java.util.ArrayList; @@ -21,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrQuery.ORDER; @@ -235,40 +238,30 @@ public List findAllTopicsBySource(String source, long offset, long coun @Override public void store(Context context, QAEvent dto) { - UpdateRequest updateRequest = new UpdateRequest(); - String topic = dto.getTopic(); if (isNotSupportedSource(dto.getSource())) { throw new IllegalArgumentException("The source of the given event is not supported: " + dto.getSource()); } - if (topic != null) { - String checksum = dto.getEventId(); - try { - if (!qaEventsDao.isEventStored(context, checksum)) { - SolrInputDocument doc = new SolrInputDocument(); - doc.addField(SOURCE, dto.getSource()); - doc.addField(EVENT_ID, checksum); - doc.addField(ORIGINAL_ID, dto.getOriginalId()); - doc.addField(TITLE, dto.getTitle()); - doc.addField(TOPIC, topic); - doc.addField(TRUST, dto.getTrust()); - doc.addField(MESSAGE, dto.getMessage()); - doc.addField(LAST_UPDATE, new Date()); - final String resourceUUID = getResourceUUID(context, dto.getOriginalId()); - if (resourceUUID == null) { - throw new IllegalArgumentException("Skipped event " + checksum + - " related to the oai record " + dto.getOriginalId() + " as the record was not found"); - } - doc.addField(RESOURCE_UUID, resourceUUID); - doc.addField(RELATED_UUID, dto.getRelated()); - updateRequest.add(doc); - updateRequest.process(getSolr()); - getSolr().commit(); - } - } catch (Exception e) { - throw new RuntimeException(e); + if (StringUtils.isBlank(dto.getTopic())) { + throw new IllegalArgumentException("A topic is mandatory for an event"); + } + + String checksum = dto.getEventId(); + try { + if (!qaEventsDao.isEventStored(context, checksum)) { + + SolrInputDocument doc = createSolrDocument(context, dto, checksum); + + UpdateRequest updateRequest = new UpdateRequest(); + + updateRequest.add(doc); + updateRequest.process(getSolr()); + + getSolr().commit(); } + } catch (Exception e) { + throw new RuntimeException(e); } } @@ -346,32 +339,6 @@ public long countEventsByTopic(String topic) { } } - private String getResourceUUID(Context context, String originalId) throws Exception { - String id = getHandleFromOriginalId(originalId); - if (id != null) { - Item item = (Item) handleService.resolveToObject(context, id); - if (item != null) { - final String itemUuid = item.getID().toString(); - context.uncacheEntity(item); - return itemUuid; - } else { - return null; - } - } else { - throw new IllegalArgumentException("Malformed originalId " + originalId); - } - } - - // oai:www.openstarts.units.it:10077/21486 - private String getHandleFromOriginalId(String originalId) { - Integer startPosition = originalId.lastIndexOf(':'); - if (startPosition != -1) { - return originalId.substring(startPosition + 1, originalId.length()); - } else { - return null; - } - } - @Override public QASource findSource(String sourceName) { @@ -425,6 +392,58 @@ public long countSources() { return getSupportedSources().length; } + @Override + public boolean isRelatedItemSupported(QAEvent qaevent) { + // Currently only PROJECT topics related to OPENAIRE supports related items + return qaevent.getSource().equals(OPENAIRE_SOURCE) && endsWith(qaevent.getTopic(), "/PROJECT"); + } + + private SolrInputDocument createSolrDocument(Context context, QAEvent dto, String checksum) throws Exception { + SolrInputDocument doc = new SolrInputDocument(); + doc.addField(SOURCE, dto.getSource()); + doc.addField(EVENT_ID, checksum); + doc.addField(ORIGINAL_ID, dto.getOriginalId()); + doc.addField(TITLE, dto.getTitle()); + doc.addField(TOPIC, dto.getTopic()); + doc.addField(TRUST, dto.getTrust()); + doc.addField(MESSAGE, dto.getMessage()); + doc.addField(LAST_UPDATE, new Date()); + final String resourceUUID = getResourceUUID(context, dto.getOriginalId()); + if (resourceUUID == null) { + throw new IllegalArgumentException("Skipped event " + checksum + + " related to the oai record " + dto.getOriginalId() + " as the record was not found"); + } + doc.addField(RESOURCE_UUID, resourceUUID); + doc.addField(RELATED_UUID, dto.getRelated()); + return doc; + } + + private String getResourceUUID(Context context, String originalId) throws Exception { + String id = getHandleFromOriginalId(originalId); + if (id != null) { + Item item = (Item) handleService.resolveToObject(context, id); + if (item != null) { + final String itemUuid = item.getID().toString(); + context.uncacheEntity(item); + return itemUuid; + } else { + return null; + } + } else { + throw new IllegalArgumentException("Malformed originalId " + originalId); + } + } + + // oai:www.openstarts.units.it:10077/21486 + private String getHandleFromOriginalId(String originalId) { + Integer startPosition = originalId.lastIndexOf(':'); + if (startPosition != -1) { + return originalId.substring(startPosition + 1, originalId.length()); + } else { + return null; + } + } + private boolean isNotSupportedSource(String source) { return !ArrayUtils.contains(getSupportedSources(), source); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/QAEventRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/QAEventRelatedRestController.java similarity index 71% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/QAEventRestController.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/QAEventRelatedRestController.java index 1584c48e65fa..8716d079374f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/QAEventRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/QAEventRelatedRestController.java @@ -12,10 +12,7 @@ import java.io.IOException; import java.sql.SQLException; import java.util.UUID; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.apache.commons.lang3.StringUtils; import org.dspace.app.rest.converter.ConverterService; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.ItemRest; @@ -37,9 +34,10 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -50,7 +48,8 @@ @RestController @RequestMapping("/api/" + QAEventRest.CATEGORY + "/qaevents" + REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG + "/related") -public class QAEventRestController { +public class QAEventRelatedRestController { + @Autowired protected Utils utils; @@ -67,42 +66,44 @@ public class QAEventRestController { * This method associate an item to a qa event * * @param qaeventId The qa event id - * @param response The current response - * @param request The current request - * @param relatedItemUUID The uuid of the related item to associate with the qa - * event + * @param relatedItemUUID The uuid of the related item to associate with the qa event * @return The related item * @throws SQLException If something goes wrong * @throws AuthorizeException If something goes wrong */ - @RequestMapping(method = RequestMethod.POST) + @PostMapping @PreAuthorize("hasAuthority('ADMIN')") - public ResponseEntity> postRelatedItem(@PathVariable(name = "id") String qaeventId, - HttpServletResponse response, HttpServletRequest request, - @RequestParam(required = true, name = "item") UUID relatedItemUUID) - throws SQLException, AuthorizeException { - Context context = ContextUtil.obtainContext(request); + public ResponseEntity> addRelatedItem(@PathVariable(name = "id") String qaeventId, + @RequestParam(name = "item") UUID relatedItemUUID) throws SQLException, AuthorizeException { + + Context context = ContextUtil.obtainCurrentRequestContext(); + QAEvent qaevent = qaEventService.findEventByEventId(qaeventId); if (qaevent == null) { throw new ResourceNotFoundException("No such qa event: " + qaeventId); } + + if (!qaEventService.isRelatedItemSupported(qaevent)) { + throw new UnprocessableEntityException("The given event does not supports a related item"); + } + if (qaevent.getRelated() != null) { - throw new UnprocessableEntityException("The qa event with ID: " + qaeventId + " already has " + - "a related item"); - } else if (!StringUtils.endsWith(qaevent.getTopic(), "/PROJECT")) { - return ControllerUtils.toEmptyResponse(HttpStatus.BAD_REQUEST); + throw new UnprocessableEntityException("The given event already has a related item"); } Item relatedItem = itemService.find(context, relatedItemUUID); - if (relatedItem != null) { - qaevent.setRelated(relatedItemUUID.toString()); - qaEventService.store(context, qaevent); - } else { + if (relatedItem == null) { throw new UnprocessableEntityException("The proposed related item was not found"); } + + qaevent.setRelated(relatedItemUUID.toString()); + qaEventService.store(context, qaevent); + ItemRest relatedItemRest = converterService.toRest(relatedItem, utils.obtainProjection()); ItemResource itemResource = converterService.toResource(relatedItemRest); + context.complete(); + return ControllerUtils.toResponseEntity(HttpStatus.CREATED, new HttpHeaders(), itemResource); } @@ -110,22 +111,26 @@ public ResponseEntity> postRelatedItem(@PathVariable(name * This method remove the association to a related item from a qa event * * @param qaeventId The qa event id - * @param response The current response - * @param request The current request * @return The related item * @throws SQLException If something goes wrong * @throws AuthorizeException If something goes wrong */ - @RequestMapping(method = RequestMethod.DELETE) + @DeleteMapping @PreAuthorize("hasAuthority('ADMIN')") - public ResponseEntity> deleteAdminGroup(@PathVariable(name = "id") String qaeventId, - HttpServletResponse response, HttpServletRequest request) + public ResponseEntity> removeRelatedItem(@PathVariable(name = "id") String qaeventId) throws SQLException, AuthorizeException, IOException { - Context context = ContextUtil.obtainContext(request); + + Context context = ContextUtil.obtainCurrentRequestContext(); QAEvent qaevent = qaEventService.findEventByEventId(qaeventId); + if (qaevent == null) { throw new ResourceNotFoundException("No such qa event: " + qaeventId); } + + if (!qaEventService.isRelatedItemSupported(qaevent)) { + throw new UnprocessableEntityException("The given event does not supports a related item"); + } + if (qaevent.getRelated() != null) { qaevent.setRelated(null); qaEventService.store(context, qaevent); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRestRepository.java index 110ebe0e1374..431c236de78d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRestRepository.java @@ -9,7 +9,6 @@ import java.sql.SQLException; import java.util.List; -import java.util.UUID; import javax.servlet.http.HttpServletRequest; import org.dspace.app.rest.Parameter; @@ -24,10 +23,9 @@ import org.dspace.content.service.ItemService; import org.dspace.core.Context; import org.dspace.eperson.EPerson; -import org.dspace.eperson.service.EPersonService; import org.dspace.qaevent.dao.QAEventsDao; import org.dspace.qaevent.service.QAEventService; -import org.slf4j.Logger; +import org.dspace.util.UUIDUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -55,20 +53,15 @@ public class QAEventRestRepository extends DSpaceRestRepository resourcePatch; - private Logger log = org.slf4j.LoggerFactory.getLogger(QAEventRestRepository.class); - @Override @PreAuthorize("hasAuthority('ADMIN')") public QAEventRest findOne(Context context, String id) { QAEvent qaEvent = qaEventService.findEventByEventId(id); if (qaEvent == null) { - // HACK check if this request is part of a patch flow + // check if this request is part of a patch flow qaEvent = (QAEvent) requestService.getCurrentRequest().getAttribute("patchedNotificationEvent"); if (qaEvent != null && qaEvent.getEventId().contentEquals(id)) { return converter.toRest(qaEvent, utils.obtainProjection()); @@ -82,7 +75,7 @@ public QAEventRest findOne(Context context, String id) { @SearchRestMethod(name = "findByTopic") @PreAuthorize("hasAuthority('ADMIN')") public Page findByTopic(Context context, @Parameter(value = "topic", required = true) String topic, - Pageable pageable) { + Pageable pageable) { List qaEvents = null; Long count = 0L; boolean ascending = false; @@ -99,16 +92,11 @@ public Page findByTopic(Context context, @Parameter(value = "topic" } @Override - protected void delete(Context context, String id) throws AuthorizeException { - Item item; - try { - item = itemService.find(context, UUID.fromString(id)); - EPerson eperson = context.getCurrentUser(); - qaEventService.deleteEventByEventId(id); - qaEventDao.storeEvent(context, id, eperson, item); - } catch (SQLException e) { - throw new RuntimeException("Unable to delete QAEvent " + id, e); - } + protected void delete(Context context, String eventId) throws AuthorizeException { + Item item = findTargetItem(context, eventId); + EPerson eperson = context.getCurrentUser(); + qaEventService.deleteEventByEventId(eventId); + qaEventDao.storeEvent(context, eventId, eperson, item); } @Override @@ -119,11 +107,24 @@ public Page findAll(Context context, Pageable pageable) { @Override @PreAuthorize("hasAuthority('ADMIN')") protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, - String id, Patch patch) throws SQLException, AuthorizeException { + String id, Patch patch) throws SQLException, AuthorizeException { QAEvent qaEvent = qaEventService.findEventByEventId(id); resourcePatch.patch(context, qaEvent, patch.getOperations()); } + private Item findTargetItem(Context context, String eventId) { + QAEvent qaEvent = qaEventService.findEventByEventId(eventId); + if (qaEvent == null) { + return null; + } + + try { + return itemService.find(context, UUIDUtils.fromString(qaEvent.getTarget())); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + @Override public Class getDomainClass() { return QAEventRest.class; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/QAEventRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/QAEventRestRepositoryIT.java index 5ccbc98d95e3..173f233de0ad 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/QAEventRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/QAEventRestRepositoryIT.java @@ -9,9 +9,13 @@ import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasNoJsonPath; +import static org.dspace.app.rest.matcher.QAEventMatcher.matchQAEventEntry; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; 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; @@ -39,8 +43,11 @@ import org.dspace.content.EntityType; import org.dspace.content.Item; import org.dspace.content.QAEvent; +import org.dspace.content.QAEventProcessed; +import org.dspace.qaevent.dao.QAEventsDao; import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; /** * Integration tests for {@link QAEventRestRepository}. @@ -50,6 +57,9 @@ */ public class QAEventRestRepositoryIT extends AbstractControllerIntegrationTest { + @Autowired + private QAEventsDao qaEventsDao; + @Test public void findAllNotImplementedTest() throws Exception { String adminToken = getAuthToken(admin.getEmail(), password); @@ -669,7 +679,7 @@ public void setInvalidRelatedTest() throws Exception { getClient(authToken) .perform(post("/api/integration/qaevents/" + event.getEventId() + "/related").param("item", funding.getID().toString())) - .andExpect(status().isBadRequest()); + .andExpect(status().isUnprocessableEntity()); // check that no related item has been added to our event getClient(authToken) .perform(get("/api/integration/qaevents/" + event.getEventId()).param("projection", "full")) @@ -712,4 +722,51 @@ public void deleteItemWithEventTest() throws Exception { QAEventMatcher.matchQAEventEntry(event2)))) .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); } + + @Test + public void testEventDeletion() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + QAEvent event = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(get("/api/integration/qaevents/" + event.getEventId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchQAEventEntry(event))); + + List processedEvents = qaEventsDao.findAll(context); + assertThat(processedEvents, empty()); + + getClient(authToken).perform(delete("/api/integration/qaevents/" + event.getEventId())) + .andExpect(status().isNoContent()); + + getClient(authToken).perform(get("/api/integration/qaevents/" + event.getEventId())) + .andExpect(status().isNotFound()); + + processedEvents = qaEventsDao.findAll(context); + assertThat(processedEvents, hasSize(1)); + + QAEventProcessed processedEvent = processedEvents.get(0); + assertThat(processedEvent.getEventId(), is(event.getEventId())); + assertThat(processedEvent.getItem(), notNullValue()); + assertThat(processedEvent.getItem().getID().toString(), is(event.getTarget())); + assertThat(processedEvent.getEventTimestamp(), notNullValue()); + assertThat(processedEvent.getEperson().getID(), is(admin.getID())); + + } } From b68a8987c7929cd18ee438a731c0982a0735e4f0 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 12 Jul 2022 12:43:17 +0200 Subject: [PATCH 008/118] [CST-5249] Fixed LGTM alerts --- .../app/suggestion/OAIREPublicationLoaderRunnable.java | 6 +++--- .../java/org/dspace/app/suggestion/SuggestionSource.java | 2 +- .../org/dspace/app/suggestion/oaire/EvidenceScorer.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnable.java b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnable.java index 1349a1a40caa..fd79ea5e9fca 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnable.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/OAIREPublicationLoaderRunnable.java @@ -85,9 +85,9 @@ public void internalRun() throws Exception { * researcher, the method returns an empty array list. If uuid is null, all * research will be return. * - * @param profile uuid of the researcher. If null, all researcher will be - * returned. - * @return the researcher with specified UUID or all researchers + * @param profileUUID uuid of the researcher. If null, all researcher will be + * returned. + * @return the researcher with specified UUID or all researchers */ @SuppressWarnings("rawtypes") private List getResearchers(String profileUUID) { diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java index b9df687dec32..ea9698d45e29 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java @@ -24,7 +24,7 @@ public SuggestionSource() { /** * Summarize the available suggestions from a source. * - * @param the name must be not null + * @param name the name must be not null */ public SuggestionSource(String name) { super(); diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/EvidenceScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/EvidenceScorer.java index 9df7621b46bb..6e00fdef0d8a 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/EvidenceScorer.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/oaire/EvidenceScorer.java @@ -32,6 +32,6 @@ public interface EvidenceScorer { * @return the generated suggestion evidence or null if the record should be * discarded */ - public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecords); + public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecord); } From d6446b15ae75028fa84a7153e96f1a4f01f6f08b Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 9 Mar 2023 18:27:32 +0100 Subject: [PATCH 009/118] [DSC-963] Create new project to run dspace with embedded tomcat --- dspace-server-webapp/pom.xml | 54 --- .../{Application.java => WebApplication.java} | 32 +- .../java/org/dspace/app/TestApplication.java | 16 + .../AbstractControllerIntegrationTest.java | 4 +- .../AbstractWebClientIntegrationTest.java | 4 +- dspace-webapp-boot/pom.xml | 135 +++++++ .../main/java/org/dspace/app/Application.java | 45 +++ .../src/main/resources/application.properties | 1 + .../src/main/resources/static}/index.html | 0 .../resources/static}/js/hal/http/client.js | 0 .../static}/js/vendor/CustomPostForm.js | 0 .../src/main/resources/static}/login.html | 0 .../src/main/resources/static}/styles.css | 0 .../app/rest/example/ExampleController.java | 0 .../app/rest/example/ExampleControllerIT.java | 0 dspace/config/log4j2-console.xml | 2 +- dspace/modules/server/pom.xml | 348 ------------------ .../modules/server/src/main/webapp/.gitignore | 0 pom.xml | 36 ++ 19 files changed, 242 insertions(+), 435 deletions(-) rename dspace-server-webapp/src/main/java/org/dspace/app/rest/{Application.java => WebApplication.java} (87%) create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/TestApplication.java create mode 100644 dspace-webapp-boot/pom.xml create mode 100644 dspace-webapp-boot/src/main/java/org/dspace/app/Application.java rename {dspace-server-webapp => dspace-webapp-boot}/src/main/resources/application.properties (99%) rename {dspace-server-webapp/src/main/webapp => dspace-webapp-boot/src/main/resources/static}/index.html (100%) rename {dspace-server-webapp/src/main/webapp => dspace-webapp-boot/src/main/resources/static}/js/hal/http/client.js (100%) rename {dspace-server-webapp/src/main/webapp => dspace-webapp-boot/src/main/resources/static}/js/vendor/CustomPostForm.js (100%) rename {dspace-server-webapp/src/main/webapp => dspace-webapp-boot/src/main/resources/static}/login.html (100%) rename {dspace-server-webapp/src/main/webapp => dspace-webapp-boot/src/main/resources/static}/styles.css (100%) rename {dspace/modules/server => dspace-webapp-boot}/src/test/java/org/dspace/app/rest/example/ExampleController.java (100%) rename {dspace/modules/server => dspace-webapp-boot}/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java (100%) delete mode 100644 dspace/modules/server/pom.xml delete mode 100644 dspace/modules/server/src/main/webapp/.gitignore diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 99aa88bebf0c..78334431df86 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -2,7 +2,6 @@ 4.0.0 org.dspace dspace-server-webapp - war DSpace Server Webapp DSpace Server Webapp (Spring Boot) @@ -25,55 +24,10 @@ @ - - org.dspace.app.rest.Application - - org.apache.maven.plugins - maven-war-plugin - - true - - true - - - - prepare-package - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - - test-jar - - - - - - - com.mycila - license-maven-plugin - - - **/src/test/resources/** - **/src/test/data/** - - src/main/webapp/index.html - src/main/webapp/login.html - src/main/webapp/styles.css - src/main/webapp/js/hal/** - src/main/webapp/js/vendor/** - - - + + org.dspace + dspace-parent + cris-2022.03.01-SNAPSHOT + .. + + + + + ${basedir}/.. + + @ + + + + + + org.dspace.modules + additions + + + org.dspace + dspace-server-webapp + + + org.apache.solr + solr-solrj + + + + + org.dspace + dspace-api + test-jar + test + + + org.dspace + dspace-server-webapp + test-jar + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + ${spring-security.version} + test + + + com.jayway.jsonpath + json-path-assert + ${json-path.version} + test + + + junit + junit + test + + + com.h2database + h2 + test + + + org.mockito + mockito-inline + test + + + + + org.apache.solr + solr-core + ${solr.client.version} + test + + + + org.apache.commons + commons-text + + + + + org.apache.lucene + lucene-analyzers-icu + test + + + + + + + + + com.mycila + license-maven-plugin + + + **/src/test/resources/** + **/src/test/data/** + + src/main/resources/static/index.html + src/main/resources/static/login.html + src/main/resources/static/styles.css + src/main/resources/static/js/hal/** + src/main/resources/static/js/vendor/** + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/dspace-webapp-boot/src/main/java/org/dspace/app/Application.java b/dspace-webapp-boot/src/main/java/org/dspace/app/Application.java new file mode 100644 index 000000000000..90039887f862 --- /dev/null +++ b/dspace-webapp-boot/src/main/java/org/dspace/app/Application.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; + +import org.dspace.app.rest.WebApplication; +import org.dspace.app.rest.utils.DSpaceConfigurationInitializer; +import org.dspace.app.rest.utils.DSpaceKernelInitializer; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication(scanBasePackageClasses = WebApplication.class) +public class Application extends SpringBootServletInitializer { + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class) + .initializers(new DSpaceKernelInitializer(), new DSpaceConfigurationInitializer()) + .run(args); + } + + /** + * Override the default SpringBootServletInitializer.configure() method, + * passing it this Application class. + *

+ * This is necessary to allow us to build a deployable WAR, rather than + * always relying on embedded Tomcat. + *

+ * See: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-create-a-deployable-war-file + * + * @param application + * @return + */ + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + // Pass this Application class, and our initializers for DSpace Kernel and Configuration + // NOTE: Kernel must be initialized before Configuration + return application.sources(Application.class) + .initializers(new DSpaceKernelInitializer(), new DSpaceConfigurationInitializer()); + } +} diff --git a/dspace-server-webapp/src/main/resources/application.properties b/dspace-webapp-boot/src/main/resources/application.properties similarity index 99% rename from dspace-server-webapp/src/main/resources/application.properties rename to dspace-webapp-boot/src/main/resources/application.properties index f6fba076c0dd..bfedd6379e97 100644 --- a/dspace-server-webapp/src/main/resources/application.properties +++ b/dspace-webapp-boot/src/main/resources/application.properties @@ -27,6 +27,7 @@ # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html # +server.servlet.context-path=/server ######################## # DSpace Settings # diff --git a/dspace-server-webapp/src/main/webapp/index.html b/dspace-webapp-boot/src/main/resources/static/index.html similarity index 100% rename from dspace-server-webapp/src/main/webapp/index.html rename to dspace-webapp-boot/src/main/resources/static/index.html diff --git a/dspace-server-webapp/src/main/webapp/js/hal/http/client.js b/dspace-webapp-boot/src/main/resources/static/js/hal/http/client.js similarity index 100% rename from dspace-server-webapp/src/main/webapp/js/hal/http/client.js rename to dspace-webapp-boot/src/main/resources/static/js/hal/http/client.js diff --git a/dspace-server-webapp/src/main/webapp/js/vendor/CustomPostForm.js b/dspace-webapp-boot/src/main/resources/static/js/vendor/CustomPostForm.js similarity index 100% rename from dspace-server-webapp/src/main/webapp/js/vendor/CustomPostForm.js rename to dspace-webapp-boot/src/main/resources/static/js/vendor/CustomPostForm.js diff --git a/dspace-server-webapp/src/main/webapp/login.html b/dspace-webapp-boot/src/main/resources/static/login.html similarity index 100% rename from dspace-server-webapp/src/main/webapp/login.html rename to dspace-webapp-boot/src/main/resources/static/login.html diff --git a/dspace-server-webapp/src/main/webapp/styles.css b/dspace-webapp-boot/src/main/resources/static/styles.css similarity index 100% rename from dspace-server-webapp/src/main/webapp/styles.css rename to dspace-webapp-boot/src/main/resources/static/styles.css diff --git a/dspace/modules/server/src/test/java/org/dspace/app/rest/example/ExampleController.java b/dspace-webapp-boot/src/test/java/org/dspace/app/rest/example/ExampleController.java similarity index 100% rename from dspace/modules/server/src/test/java/org/dspace/app/rest/example/ExampleController.java rename to dspace-webapp-boot/src/test/java/org/dspace/app/rest/example/ExampleController.java diff --git a/dspace/modules/server/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java b/dspace-webapp-boot/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java similarity index 100% rename from dspace/modules/server/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java rename to dspace-webapp-boot/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java diff --git a/dspace/config/log4j2-console.xml b/dspace/config/log4j2-console.xml index 3d51b123367b..a0322abf19d3 100644 --- a/dspace/config/log4j2-console.xml +++ b/dspace/config/log4j2-console.xml @@ -25,7 +25,7 @@ For command line / Ant scripts, we are only concerned about significant warnings/errors. For the full detail, change this to INFO and re-run Ant. --> - + diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml deleted file mode 100644 index 9b696fa0cbda..000000000000 --- a/dspace/modules/server/pom.xml +++ /dev/null @@ -1,348 +0,0 @@ - - 4.0.0 - org.dspace.modules - server - war - DSpace Server Webapp:: Local Customizations - Overlay customizations. -This is probably a temporary solution to the build problems. We like to investigate about -the possibility to remove the overlays enable a more flexible extension mechanism. -The use of web-fragment and spring mvc technology allow us to add request handlers -just adding new jar in the classloader - - - modules - org.dspace - 7.6-SNAPSHOT - .. - - - - - ${basedir}/../../.. - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - unpack - prepare-package - - unpack-dependencies - - - org.dspace.modules - additions - - ${project.build.directory}/additions - META-INF/** - - - - - - org.apache.maven.plugins - maven-war-plugin - - false - - true - - - - ${project.build.directory}/additions - WEB-INF/classes - - - - - - prepare-package - - - - - - org.codehaus.gmaven - groovy-maven-plugin - - - setproperty - initialize - - execute - - - - project.properties['agnostic.build.dir'] = project.build.directory.replace(File.separator, '/'); - log.info("Initializing Maven property 'agnostic.build.dir' to: {}", project.properties['agnostic.build.dir']); - - - - - - - - - - - - unit-test-environment - - false - - skipUnitTests - false - - - - - - - maven-dependency-plugin - - ${project.build.directory}/testing - - - org.dspace - dspace-parent - ${project.version} - zip - testEnvironment - - - - - - setupUnitTestEnvironment - generate-test-resources - - unpack - - - - - - - - maven-surefire-plugin - - - - - - ${agnostic.build.dir}/testing/dspace/ - - true - ${agnostic.build.dir}/testing/dspace/solr/ - - - - - - - - - org.dspace - dspace-server-webapp - test-jar - test - - - - - - - integration-test-environment - - false - - skipIntegrationTests - false - - - - - - - maven-dependency-plugin - - ${project.build.directory}/testing - - - org.dspace - dspace-parent - ${project.version} - zip - testEnvironment - - - - - - setupIntegrationTestEnvironment - pre-integration-test - - unpack - - - - - - - - maven-failsafe-plugin - - - - - ${agnostic.build.dir}/testing/dspace/ - - true - ${agnostic.build.dir}/testing/dspace/solr/ - - - - - - - - - org.dspace - dspace-server-webapp - test-jar - test - - - - - - oracle-support - - - db.name - oracle - - - - - com.oracle - ojdbc6 - - - - - - - - - org.dspace.modules - additions - - - org.dspace - dspace-server-webapp - classes - - - org.dspace - dspace-server-webapp - war - - - org.apache.solr - solr-solrj - ${solr.client.version} - - - - - org.dspace - dspace-api - test-jar - test - - - org.dspace - dspace-server-webapp - test-jar - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - ${spring-security.version} - test - - - com.jayway.jsonpath - json-path-assert - ${json-path.version} - test - - - junit - junit - test - - - com.h2database - h2 - test - - - org.mockito - mockito-inline - test - - - - - org.apache.solr - solr-core - ${solr.client.version} - test - - - org.apache.lucene - lucene-analyzers-icu - test - - - - - diff --git a/dspace/modules/server/src/main/webapp/.gitignore b/dspace/modules/server/src/main/webapp/.gitignore deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/pom.xml b/pom.xml index 3d9e6851d1d0..357f3f447dba 100644 --- a/pom.xml +++ b/pom.xml @@ -793,6 +793,21 @@ + + + dspace-webapp-boot + + + dspace-webapp-boot/pom.xml + + + + dspace-webapp-boot + + + @@ -1068,6 +1083,7 @@ org.dspace dspace-server-webapp +<<<<<<< HEAD test-jar 7.6-SNAPSHOT test @@ -1094,6 +1110,26 @@ dspace-server-webapp 7.6-SNAPSHOT war +======= + cris-2022.03.01-SNAPSHOT + + + org.dspace + dspace-server-webapp + test-jar + cris-2022.03.01-SNAPSHOT + test + + + org.dspace + dspace-rdf + cris-2022.03.01-SNAPSHOT + + + org.dspace + dspace-iiif + cris-2022.03.01-SNAPSHOT +>>>>>>> 0dad4dd713... [DSC-963] Create new project to run dspace with embedded tomcat From 0400b38121d07a2b1f7e10f5f106c6cfcb313904 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 10 Mar 2023 15:32:59 +0100 Subject: [PATCH 010/118] [DSC-963] Fixed test configuration --- .../src/test/resources/application.properties | 64 +++++++++++++++++++ .../src/main/resources/application.properties | 6 +- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 dspace-server-webapp/src/test/resources/application.properties diff --git a/dspace-server-webapp/src/test/resources/application.properties b/dspace-server-webapp/src/test/resources/application.properties new file mode 100644 index 000000000000..9b408d9612de --- /dev/null +++ b/dspace-server-webapp/src/test/resources/application.properties @@ -0,0 +1,64 @@ +# +# 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/ +# + +# Spring Boot's Test application.properties + +######################## +# Jackson serialization settings +# +spring.jackson.serialization.fail-on-empty-beans=false + +######################## +# Internationalization +# +# Base Path for our messages file (i18n) +spring.messages.basename=i18n/messages +spring.messages.encoding=UTF-8 + +######################## +# URI Encoding and Decoding +# +# +# Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly. +server.servlet.encoding.charset=UTF-8 +# Force the encoding to the configured charset on HTTP requests and responses. +server.servlet.encoding.force=true + +########################### +# Server Properties +# +# Error handling settings +server.error.include-stacktrace = never + +# When to include the error message in error responses (introduced in Spring 2.3.x) +server.error.include-message = always + +# Spring Boot proxy configuration (can be overridden in local.cfg). +server.forward-headers-strategy=FRAMEWORK + +###################### +# Cache Properties +# Added for IIIF cache support. +# Path to configuration file. +spring.cache.jcache.config=classpath:iiif/cache/ehcache.xml + +###################### +# Spring Boot Autoconfigure +# +# TODO: At some point we may want to investigate whether we can re-enable these and remove the custom DSpace init code +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, \ + org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, \ + org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, \ + org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration, \ + org.springframework.boot.autoconfigure.velocity.VelocityAutoConfiguration + +spring.main.allow-bean-definition-overriding = true + +######################### +# Spring Boot Logging levels +logging.config = classpath:log4j2-test.xml diff --git a/dspace-webapp-boot/src/main/resources/application.properties b/dspace-webapp-boot/src/main/resources/application.properties index bfedd6379e97..8233298ef0b0 100644 --- a/dspace-webapp-boot/src/main/resources/application.properties +++ b/dspace-webapp-boot/src/main/resources/application.properties @@ -27,7 +27,6 @@ # http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html # -server.servlet.context-path=/server ######################## # DSpace Settings # @@ -39,6 +38,11 @@ server.servlet.context-path=/server # interact with or read its configuration from dspace.cfg. dspace.dir=${dspace.dir} +######################## +# Servlet context path configuration for spring boot application running with embedded tomcat +# +server.servlet.context-path=/server + ######################## # Jackson serialization settings # From dcde7dbeeacfc508d922d4a26e7ef1dde2b3bb53 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 10 Mar 2023 15:37:57 +0100 Subject: [PATCH 011/118] [DSC-963] Fixed dspace pom --- dspace/pom.xml | 6 +++++- pom.xml | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/dspace/pom.xml b/dspace/pom.xml index 7916648e4784..8ab959999814 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -217,7 +217,11 @@ org.dspace dspace-server-webapp - war + compile + + + org.dspace + dspace-webapp-boot compile diff --git a/pom.xml b/pom.xml index 357f3f447dba..10af1fff3ead 100644 --- a/pom.xml +++ b/pom.xml @@ -1120,6 +1120,11 @@ cris-2022.03.01-SNAPSHOT test + + org.dspace + dspace-webapp-boot + cris-2022.03.01-SNAPSHOT + org.dspace dspace-rdf From 5d592df6bb6a4ec2fc9a220709c108e733e6d437 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 10 Mar 2023 16:14:50 +0100 Subject: [PATCH 012/118] [DSC-963] Improved tests configuration --- .../src/main/resources/application.properties | 0 .../src/test/resources/application.properties | 64 ------------------- 2 files changed, 64 deletions(-) rename {dspace-webapp-boot => dspace-server-webapp}/src/main/resources/application.properties (100%) delete mode 100644 dspace-server-webapp/src/test/resources/application.properties diff --git a/dspace-webapp-boot/src/main/resources/application.properties b/dspace-server-webapp/src/main/resources/application.properties similarity index 100% rename from dspace-webapp-boot/src/main/resources/application.properties rename to dspace-server-webapp/src/main/resources/application.properties diff --git a/dspace-server-webapp/src/test/resources/application.properties b/dspace-server-webapp/src/test/resources/application.properties deleted file mode 100644 index 9b408d9612de..000000000000 --- a/dspace-server-webapp/src/test/resources/application.properties +++ /dev/null @@ -1,64 +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/ -# - -# Spring Boot's Test application.properties - -######################## -# Jackson serialization settings -# -spring.jackson.serialization.fail-on-empty-beans=false - -######################## -# Internationalization -# -# Base Path for our messages file (i18n) -spring.messages.basename=i18n/messages -spring.messages.encoding=UTF-8 - -######################## -# URI Encoding and Decoding -# -# -# Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly. -server.servlet.encoding.charset=UTF-8 -# Force the encoding to the configured charset on HTTP requests and responses. -server.servlet.encoding.force=true - -########################### -# Server Properties -# -# Error handling settings -server.error.include-stacktrace = never - -# When to include the error message in error responses (introduced in Spring 2.3.x) -server.error.include-message = always - -# Spring Boot proxy configuration (can be overridden in local.cfg). -server.forward-headers-strategy=FRAMEWORK - -###################### -# Cache Properties -# Added for IIIF cache support. -# Path to configuration file. -spring.cache.jcache.config=classpath:iiif/cache/ehcache.xml - -###################### -# Spring Boot Autoconfigure -# -# TODO: At some point we may want to investigate whether we can re-enable these and remove the custom DSpace init code -spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, \ - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration, \ - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration, \ - org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration, \ - org.springframework.boot.autoconfigure.velocity.VelocityAutoConfiguration - -spring.main.allow-bean-definition-overriding = true - -######################### -# Spring Boot Logging levels -logging.config = classpath:log4j2-test.xml From 2819b6f2e42814cb7291d055f492e67e97c6bdee Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 10 Mar 2023 16:42:00 +0100 Subject: [PATCH 013/118] [DSC-963] Fixed dspace-server-webapp pom --- dspace-server-webapp/pom.xml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 78334431df86..7ccb75244e57 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -57,6 +57,25 @@ + + org.apache.maven.plugins + maven-jar-plugin + + + + true + true + + + + + + + test-jar + + + + From 1bbd478cf6b3805d988104e57c06cf957017bc19 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 10 Mar 2023 17:39:33 +0100 Subject: [PATCH 014/118] [DSC-963] Fixed Sword tests --- .../src/test/java/org/dspace/app/rdf/RdfIT.java | 2 +- .../src/test/java/org/dspace/app/sword/Swordv1IT.java | 2 +- .../src/test/java/org/dspace/app/sword2/Swordv2IT.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rdf/RdfIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rdf/RdfIT.java index 85ab3dcadd78..10f06370ad5c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rdf/RdfIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rdf/RdfIT.java @@ -47,7 +47,7 @@ */ // Ensure the RDF endpoint IS ENABLED before any tests run. // This annotation overrides default DSpace config settings loaded into Spring Context -@TestPropertySource(properties = {"rdf.enabled = true"}) +@TestPropertySource(properties = {"rdf.enabled = true", "server.servlet.context-path = /"}) public class RdfIT extends AbstractWebClientIntegrationTest { @Autowired diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java index 24244e1773e6..ffef89316b93 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java @@ -34,7 +34,7 @@ */ // Ensure the SWORD SERVER IS ENABLED before any tests run. // This annotation overrides default DSpace config settings loaded into Spring Context -@TestPropertySource(properties = {"sword-server.enabled = true"}) +@TestPropertySource(properties = { "sword-server.enabled = true", "server.servlet.context-path = /" }) public class Swordv1IT extends AbstractWebClientIntegrationTest { @Autowired diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java index 95ec76251415..f9caeead664e 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java @@ -34,7 +34,7 @@ */ // Ensure the SWORDv2 SERVER IS ENABLED before any tests run. // This annotation overrides default DSpace config settings loaded into Spring Context -@TestPropertySource(properties = {"swordv2-server.enabled = true"}) +@TestPropertySource(properties = {"swordv2-server.enabled = true", "server.servlet.context-path = /"}) public class Swordv2IT extends AbstractWebClientIntegrationTest { @Autowired From 944f4a10949df3fd48df2bcc445be91802e93bfb Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 10 Mar 2023 17:50:08 +0100 Subject: [PATCH 015/118] [DSC-963] Fixed dspace pom --- dspace-webapp-boot/pom.xml | 2 +- dspace/pom.xml | 1 + pom.xml | 31 +------------------------------ 3 files changed, 3 insertions(+), 31 deletions(-) diff --git a/dspace-webapp-boot/pom.xml b/dspace-webapp-boot/pom.xml index 1a110886cf42..4a14ef03fcf1 100644 --- a/dspace-webapp-boot/pom.xml +++ b/dspace-webapp-boot/pom.xml @@ -12,7 +12,7 @@ org.dspace dspace-parent - cris-2022.03.01-SNAPSHOT + 7.6-SNAPSHOT .. diff --git a/dspace/pom.xml b/dspace/pom.xml index 8ab959999814..9cfe7da36612 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -222,6 +222,7 @@ org.dspace dspace-webapp-boot + war compile diff --git a/pom.xml b/pom.xml index 10af1fff3ead..2da6e8aa3ca8 100644 --- a/pom.xml +++ b/pom.xml @@ -1083,7 +1083,6 @@ org.dspace dspace-server-webapp -<<<<<<< HEAD test-jar 7.6-SNAPSHOT test @@ -1102,39 +1101,11 @@ org.dspace dspace-server-webapp 7.6-SNAPSHOT - jar - classes - org.dspace - dspace-server-webapp + dspace-webapp-boot 7.6-SNAPSHOT war -======= - cris-2022.03.01-SNAPSHOT - - - org.dspace - dspace-server-webapp - test-jar - cris-2022.03.01-SNAPSHOT - test - - - org.dspace - dspace-webapp-boot - cris-2022.03.01-SNAPSHOT - - - org.dspace - dspace-rdf - cris-2022.03.01-SNAPSHOT - - - org.dspace - dspace-iiif - cris-2022.03.01-SNAPSHOT ->>>>>>> 0dad4dd713... [DSC-963] Create new project to run dspace with embedded tomcat From 882485b61528a3f240434f0d0eb448c9d75fa04e Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 10 Mar 2023 19:32:46 +0100 Subject: [PATCH 016/118] [DSC-963] Set default servlet context path on application-test.properties --- .../src/test/java/org/dspace/app/rdf/RdfIT.java | 2 +- .../src/test/java/org/dspace/app/sword/Swordv1IT.java | 2 +- .../src/test/java/org/dspace/app/sword2/Swordv2IT.java | 2 +- .../src/test/resources/application-test.properties | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rdf/RdfIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rdf/RdfIT.java index 10f06370ad5c..85ab3dcadd78 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rdf/RdfIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rdf/RdfIT.java @@ -47,7 +47,7 @@ */ // Ensure the RDF endpoint IS ENABLED before any tests run. // This annotation overrides default DSpace config settings loaded into Spring Context -@TestPropertySource(properties = {"rdf.enabled = true", "server.servlet.context-path = /"}) +@TestPropertySource(properties = {"rdf.enabled = true"}) public class RdfIT extends AbstractWebClientIntegrationTest { @Autowired diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java index ffef89316b93..24244e1773e6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword/Swordv1IT.java @@ -34,7 +34,7 @@ */ // Ensure the SWORD SERVER IS ENABLED before any tests run. // This annotation overrides default DSpace config settings loaded into Spring Context -@TestPropertySource(properties = { "sword-server.enabled = true", "server.servlet.context-path = /" }) +@TestPropertySource(properties = {"sword-server.enabled = true"}) public class Swordv1IT extends AbstractWebClientIntegrationTest { @Autowired diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java index f9caeead664e..95ec76251415 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/sword2/Swordv2IT.java @@ -34,7 +34,7 @@ */ // Ensure the SWORDv2 SERVER IS ENABLED before any tests run. // This annotation overrides default DSpace config settings loaded into Spring Context -@TestPropertySource(properties = {"swordv2-server.enabled = true", "server.servlet.context-path = /"}) +@TestPropertySource(properties = {"swordv2-server.enabled = true"}) public class Swordv2IT extends AbstractWebClientIntegrationTest { @Autowired diff --git a/dspace-server-webapp/src/test/resources/application-test.properties b/dspace-server-webapp/src/test/resources/application-test.properties index 9a396cf8e5b1..e92e1166e355 100644 --- a/dspace-server-webapp/src/test/resources/application-test.properties +++ b/dspace-server-webapp/src/test/resources/application-test.properties @@ -14,4 +14,6 @@ ## Log4j2 configuration for test environment ## This file is found on classpath at src/test/resources/log4j2-test.xml -logging.config = classpath:log4j2-test.xml \ No newline at end of file +logging.config = classpath:log4j2-test.xml + +server.servlet.context-path=/ \ No newline at end of file From c82588ab5430463bd75f804e4c83a797099c0f47 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Fri, 10 Mar 2023 20:41:07 +0100 Subject: [PATCH 017/118] [DSC-963] Improved TestApplication configuration --- .../test/java/org/dspace/app/{ => rest}/TestApplication.java | 5 ++--- .../app/rest/test/AbstractControllerIntegrationTest.java | 2 +- .../app/rest/test/AbstractWebClientIntegrationTest.java | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) rename dspace-server-webapp/src/test/java/org/dspace/app/{ => rest}/TestApplication.java (70%) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/TestApplication.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/TestApplication.java similarity index 70% rename from dspace-server-webapp/src/test/java/org/dspace/app/TestApplication.java rename to dspace-server-webapp/src/test/java/org/dspace/app/rest/TestApplication.java index 0f80e866edd7..e387e3f0024d 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/TestApplication.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/TestApplication.java @@ -5,12 +5,11 @@ * * http://www.dspace.org/license/ */ -package org.dspace.app; +package org.dspace.app.rest; -import org.dspace.app.rest.WebApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -@SpringBootApplication(scanBasePackageClasses = WebApplication.class) +@SpringBootApplication public class TestApplication { } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java index 4ec66fb00081..a27e0ab75c8c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractControllerIntegrationTest.java @@ -23,7 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.dspace.AbstractIntegrationTestWithDatabase; -import org.dspace.app.TestApplication; +import org.dspace.app.rest.TestApplication; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.utils.DSpaceConfigurationInitializer; import org.dspace.app.rest.utils.DSpaceKernelInitializer; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractWebClientIntegrationTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractWebClientIntegrationTest.java index be0a27b4ebd1..7f58a9999dd4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractWebClientIntegrationTest.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/test/AbstractWebClientIntegrationTest.java @@ -9,7 +9,7 @@ import org.apache.commons.lang3.StringUtils; import org.dspace.AbstractIntegrationTestWithDatabase; -import org.dspace.app.TestApplication; +import org.dspace.app.rest.TestApplication; import org.dspace.app.rest.utils.DSpaceConfigurationInitializer; import org.dspace.app.rest.utils.DSpaceKernelInitializer; import org.junit.runner.RunWith; From aff1de4153994eb69bc151fde159881ba830bde5 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Mon, 13 Mar 2023 10:33:06 +0100 Subject: [PATCH 018/118] [DSC-963] Added @Order on AdminRestPermissionEvaluatorPlugin --- .../app/rest/security/AdminRestPermissionEvaluatorPlugin.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AdminRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AdminRestPermissionEvaluatorPlugin.java index 0d251f6400f7..338eed4a7340 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AdminRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AdminRestPermissionEvaluatorPlugin.java @@ -20,6 +20,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -29,6 +31,7 @@ * the authenticated EPerson is allowed to perform the requested action. */ @Component +@Order(value = Ordered.HIGHEST_PRECEDENCE) public class AdminRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { private static final Logger log = LoggerFactory.getLogger(RestObjectPermissionEvaluatorPlugin.class); From b3a3acf91058f748feb8c1c3fb6d4be8bcb5d5cc Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Mon, 13 Mar 2023 11:46:20 +0100 Subject: [PATCH 019/118] [DSC-963] Minor improvements --- dspace-server-webapp/pom.xml | 31 +++++++------------ .../org/dspace/app/rest/WebApplication.java | 10 ++---- .../app/{rest => }/TestApplication.java | 11 +++++-- .../AbstractControllerIntegrationTest.java | 2 +- .../AbstractWebClientIntegrationTest.java | 2 +- .../main/java/org/dspace/app/Application.java | 13 ++++++++ dspace/config/log4j2-console.xml | 2 +- 7 files changed, 39 insertions(+), 32 deletions(-) rename dspace-server-webapp/src/test/java/org/dspace/app/{rest => }/TestApplication.java (55%) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 7ccb75244e57..fbba57301f09 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -28,6 +28,18 @@ + + org.apache.maven.plugins + maven-jar-plugin + + + + + test-jar + + + + - + From abb17db890e6bf6d6088746723d69ae80fd8d3b8 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Mon, 13 Mar 2023 17:44:07 +0100 Subject: [PATCH 020/118] [DSC-963] Fixed ItemRestRepositoryIT and GenericAuthorizationFeatureIT integration tests --- .../ExternalSourceItemUriListHandler.java | 8 +++++--- .../GenericAuthorizationFeatureIT.java | 18 ++++++++++++------ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/handler/ExternalSourceItemUriListHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/handler/ExternalSourceItemUriListHandler.java index d619100bf67a..201a7ba1633d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/handler/ExternalSourceItemUriListHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/handler/ExternalSourceItemUriListHandler.java @@ -30,16 +30,19 @@ @Component public class ExternalSourceItemUriListHandler extends ExternalSourceEntryItemUriListHandler { + private Pattern pattern = Pattern.compile("\\/api\\/core\\/items\\/(.*)"); + @Autowired private ItemService itemService; @Override @SuppressWarnings("rawtypes") public boolean supports(List uriList, String method,Class clazz) { - if (clazz != Item.class) { + if (clazz != Item.class || uriList.size() != 1) { return false; } - return true; + + return pattern.matcher(uriList.get(0)).find(); } @Override @@ -61,7 +64,6 @@ public boolean validate(Context context, HttpServletRequest request, List uriList) { Item item = null; String url = uriList.get(0); - Pattern pattern = Pattern.compile("\\/api\\/core\\/items\\/(.*)"); Matcher matcher = pattern.matcher(url); if (!matcher.find()) { throw new DSpaceBadRequestException("The uri: " + url + " doesn't resolve to an item"); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java index 1d3b5b051605..e6ccf5954c7a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java @@ -757,7 +757,8 @@ public void testCanMoveAdmin() throws Exception { // Verify the general admin has this feature on item 1 getClient(adminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + + "http://localhost/api/core/items/" + item1.getID()) + .param("size", "1000")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -765,7 +766,8 @@ public void testCanMoveAdmin() throws Exception { // Verify community A admin has this feature on item 1 getClient(communityAAdminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + + "http://localhost/api/core/items/" + item1.getID()) + .param("size", "1000")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -773,7 +775,8 @@ public void testCanMoveAdmin() throws Exception { // Verify collection X admin has this feature on item 1 getClient(collectionXAdminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + + "http://localhost/api/core/items/" + item1.getID()) + .param("size", "1000")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -781,7 +784,8 @@ public void testCanMoveAdmin() throws Exception { // Verify item 1 admin doesn’t have this feature on item 1 getClient(item1AdminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + + "http://localhost/api/core/items/" + item1.getID()) + .param("size", "1000")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -789,7 +793,8 @@ public void testCanMoveAdmin() throws Exception { // Verify community A admin doesn’t have this feature on item 2 getClient(communityAAdminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item2.getID())) + + "http://localhost/api/core/items/" + item2.getID()) + .param("size", "1000")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -808,7 +813,8 @@ public void testCanMoveAdmin() throws Exception { // verify item 1 write has this feature on item 1 getClient(item1WriterToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + + "http://localhost/api/core/items/" + item1.getID()) + .param("size", "1000")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='canMove')]") .exists()); From 08c547805e96f24282bbeec62cd579b597569a7d Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 14 Mar 2023 12:57:50 +0100 Subject: [PATCH 021/118] [DSC-963] Fixed SubmissionCCLicenseUrlRepositoryIT tests --- .../org/dspace/app/rest/link/DSpaceResourceHalLinkFactory.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/DSpaceResourceHalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/DSpaceResourceHalLinkFactory.java index c306691eb352..30404e030ab6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/DSpaceResourceHalLinkFactory.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/DSpaceResourceHalLinkFactory.java @@ -21,6 +21,8 @@ import org.dspace.app.rest.model.hateoas.DSpaceResource; import org.dspace.app.rest.utils.Utils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.data.domain.Pageable; import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.Link; @@ -33,6 +35,7 @@ * @author Tom Desair (tom dot desair at atmire dot com) */ @Component +@Order(Ordered.HIGHEST_PRECEDENCE) public class DSpaceResourceHalLinkFactory extends HalLinkFactory { @Autowired From 12cb9a82df62636ac8114ddf59ed5b27bb57554d Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 14 Mar 2023 17:12:09 +0100 Subject: [PATCH 022/118] [DSC-963] Refactoring to maintain server module --- dspace-server-webapp/pom.xml | 17 + .../src/main/resources/static/index.html | 0 .../resources/static/js/hal/http/client.js | 0 .../static/js/vendor/CustomPostForm.js | 0 .../src/main/resources/static/login.html | 0 .../src/main/resources/static/styles.css | 0 dspace/modules/pom.xml | 11 + .../modules/server-boot}/pom.xml | 30 +- .../org/dspace/app/ServerBootApplication.java | 36 ++ dspace/modules/server/pom.xml | 349 ++++++++++++++++++ .../org/dspace/app/ServerApplication.java | 10 +- .../modules/server/src/main/webapp/.gitignore | 0 .../app/rest/example/ExampleController.java | 0 .../app/rest/example/ExampleControllerIT.java | 0 dspace/pom.xml | 6 - pom.xml | 20 - 16 files changed, 420 insertions(+), 59 deletions(-) rename {dspace-webapp-boot => dspace-server-webapp}/src/main/resources/static/index.html (100%) rename {dspace-webapp-boot => dspace-server-webapp}/src/main/resources/static/js/hal/http/client.js (100%) rename {dspace-webapp-boot => dspace-server-webapp}/src/main/resources/static/js/vendor/CustomPostForm.js (100%) rename {dspace-webapp-boot => dspace-server-webapp}/src/main/resources/static/login.html (100%) rename {dspace-webapp-boot => dspace-server-webapp}/src/main/resources/static/styles.css (100%) rename {dspace-webapp-boot => dspace/modules/server-boot}/pom.xml (73%) create mode 100644 dspace/modules/server-boot/src/main/java/org/dspace/app/ServerBootApplication.java create mode 100644 dspace/modules/server/pom.xml rename dspace-webapp-boot/src/main/java/org/dspace/app/Application.java => dspace/modules/server/src/main/java/org/dspace/app/ServerApplication.java (85%) create mode 100644 dspace/modules/server/src/main/webapp/.gitignore rename {dspace-webapp-boot => dspace/modules/server}/src/test/java/org/dspace/app/rest/example/ExampleController.java (100%) rename {dspace-webapp-boot => dspace/modules/server}/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java (100%) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index fbba57301f09..57f3399f982a 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -40,6 +40,23 @@ + + + com.mycila + license-maven-plugin + + + **/src/test/resources/** + **/src/test/data/** + + src/main/resources/static/index.html + src/main/resources/static/login.html + src/main/resources/static/styles.css + src/main/resources/static/js/hal/** + src/main/resources/static/js/vendor/** + + + + modules org.dspace - dspace-parent 7.6-SNAPSHOT .. - ${basedir}/.. - - @ + ${basedir}/../../.. @@ -105,26 +102,9 @@ - + - - - com.mycila - license-maven-plugin - - - **/src/test/resources/** - **/src/test/data/** - - src/main/resources/static/index.html - src/main/resources/static/login.html - src/main/resources/static/styles.css - src/main/resources/static/js/hal/** - src/main/resources/static/js/vendor/** - - - org.springframework.boot spring-boot-maven-plugin diff --git a/dspace/modules/server-boot/src/main/java/org/dspace/app/ServerBootApplication.java b/dspace/modules/server-boot/src/main/java/org/dspace/app/ServerBootApplication.java new file mode 100644 index 000000000000..f46532ff14b5 --- /dev/null +++ b/dspace/modules/server-boot/src/main/java/org/dspace/app/ServerBootApplication.java @@ -0,0 +1,36 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app; + +import org.dspace.app.rest.WebApplication; +import org.dspace.app.rest.utils.DSpaceConfigurationInitializer; +import org.dspace.app.rest.utils.DSpaceKernelInitializer; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; + +/** + * Define the Spring Boot Application settings itself to be runned using an + * embedded application server. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@SpringBootApplication(scanBasePackageClasses = WebApplication.class) +public class ServerBootApplication { + + private ServerBootApplication() { + + } + + public static void main(String[] args) { + new SpringApplicationBuilder(ServerBootApplication.class) + .initializers(new DSpaceKernelInitializer(), new DSpaceConfigurationInitializer()) + .run(args); + } + +} diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml new file mode 100644 index 000000000000..65849295e8c4 --- /dev/null +++ b/dspace/modules/server/pom.xml @@ -0,0 +1,349 @@ + + 4.0.0 + org.dspace.modules + server + war + DSpace Server Webapp:: Local Customizations + + modules + org.dspace + cris-2022.03.01-SNAPSHOT + .. + + + + + ${basedir}/../../.. + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack + prepare-package + + unpack-dependencies + + + org.dspace.modules + additions + + ${project.build.directory}/additions + META-INF/** + + + + + + org.apache.maven.plugins + maven-war-plugin + + false + + true + + + + ${project.build.directory}/additions + WEB-INF/classes + + + + + + prepare-package + + + + + + org.codehaus.gmaven + groovy-maven-plugin + + + setproperty + initialize + + execute + + + + project.properties['agnostic.build.dir'] = project.build.directory.replace(File.separator, '/'); + log.info("Initializing Maven property 'agnostic.build.dir' to: {}", project.properties['agnostic.build.dir']); + + + + + + + + + + + + unit-test-environment + + false + + skipUnitTests + false + + + + + + + maven-dependency-plugin + + ${project.build.directory}/testing + + + org.dspace + dspace-parent + ${project.version} + zip + testEnvironment + + + + + + setupUnitTestEnvironment + generate-test-resources + + unpack + + + + + + + + maven-surefire-plugin + + + + + + ${agnostic.build.dir}/testing/dspace + + true + ${agnostic.build.dir}/testing/dspace/solr/ + + + + + + + + + org.dspace + dspace-server-webapp + test-jar + test + + + + + + + integration-test-environment + + false + + skipIntegrationTests + false + + + + + + + maven-dependency-plugin + + ${project.build.directory}/testing + + + org.dspace + dspace-parent + ${project.version} + zip + testEnvironment + + + + + + setupIntegrationTestEnvironment + pre-integration-test + + unpack + + + + + + + + maven-failsafe-plugin + + + + + ${agnostic.build.dir}/testing/dspace + + true + ${agnostic.build.dir}/testing/dspace/solr/ + + + + + + + + + org.dspace + dspace-server-webapp + test-jar + test + + + + + + oracle-support + + + db.name + oracle + + + + + com.oracle + ojdbc6 + + + + + + + + + org.dspace.modules + additions + + + org.dspace + dspace-server-webapp + + + org.springframework.boot + spring-boot-starter-tomcat + provided + ${spring-boot.version} + + + org.apache.solr + solr-solrj + ${solr.client.version} + + + + + org.dspace + dspace-api + test-jar + test + + + org.dspace + dspace-server-webapp + test-jar + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + ${spring-security.version} + test + + + com.jayway.jsonpath + json-path-assert + ${json-path.version} + test + + + junit + junit + test + + + com.h2database + h2 + test + + + org.mockito + mockito-inline + test + + + + + org.apache.solr + solr-core + ${solr.client.version} + test + + + + org.apache.commons + commons-text + + + + + org.apache.lucene + lucene-analyzers-icu + test + + + + + diff --git a/dspace-webapp-boot/src/main/java/org/dspace/app/Application.java b/dspace/modules/server/src/main/java/org/dspace/app/ServerApplication.java similarity index 85% rename from dspace-webapp-boot/src/main/java/org/dspace/app/Application.java rename to dspace/modules/server/src/main/java/org/dspace/app/ServerApplication.java index dc84b29a5620..34acc778b7f3 100644 --- a/dspace-webapp-boot/src/main/java/org/dspace/app/Application.java +++ b/dspace/modules/server/src/main/java/org/dspace/app/ServerApplication.java @@ -28,13 +28,7 @@ * */ @SpringBootApplication(scanBasePackageClasses = WebApplication.class) -public class Application extends SpringBootServletInitializer { - - public static void main(String[] args) { - new SpringApplicationBuilder(Application.class) - .initializers(new DSpaceKernelInitializer(), new DSpaceConfigurationInitializer()) - .run(args); - } +public class ServerApplication extends SpringBootServletInitializer { /** * Override the default SpringBootServletInitializer.configure() method, @@ -52,7 +46,7 @@ public static void main(String[] args) { protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { // Pass this Application class, and our initializers for DSpace Kernel and Configuration // NOTE: Kernel must be initialized before Configuration - return application.sources(Application.class) + return application.sources(ServerApplication.class) .initializers(new DSpaceKernelInitializer(), new DSpaceConfigurationInitializer()); } } diff --git a/dspace/modules/server/src/main/webapp/.gitignore b/dspace/modules/server/src/main/webapp/.gitignore new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/dspace-webapp-boot/src/test/java/org/dspace/app/rest/example/ExampleController.java b/dspace/modules/server/src/test/java/org/dspace/app/rest/example/ExampleController.java similarity index 100% rename from dspace-webapp-boot/src/test/java/org/dspace/app/rest/example/ExampleController.java rename to dspace/modules/server/src/test/java/org/dspace/app/rest/example/ExampleController.java diff --git a/dspace-webapp-boot/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java b/dspace/modules/server/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java similarity index 100% rename from dspace-webapp-boot/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java rename to dspace/modules/server/src/test/java/org/dspace/app/rest/example/ExampleControllerIT.java diff --git a/dspace/pom.xml b/dspace/pom.xml index 9cfe7da36612..b3ebbc4faeea 100644 --- a/dspace/pom.xml +++ b/dspace/pom.xml @@ -219,12 +219,6 @@ dspace-server-webapp compile - - org.dspace - dspace-webapp-boot - war - compile - org.dspace dspace-sword diff --git a/pom.xml b/pom.xml index 2da6e8aa3ca8..6e65afac32d7 100644 --- a/pom.xml +++ b/pom.xml @@ -793,21 +793,6 @@ - - - dspace-webapp-boot - - - dspace-webapp-boot/pom.xml - - - - dspace-webapp-boot - - - @@ -1101,11 +1086,6 @@ org.dspace dspace-server-webapp 7.6-SNAPSHOT - - org.dspace - dspace-webapp-boot - 7.6-SNAPSHOT - war From 4b72466d74982d5f9da43d03dc76fb16f0d90d3e Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Tue, 14 Mar 2023 18:36:50 +0100 Subject: [PATCH 023/118] [DSC-963] Configured spring boot maven plugin --- dspace/modules/server-boot/pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dspace/modules/server-boot/pom.xml b/dspace/modules/server-boot/pom.xml index 138329845a9a..fbfab5f93b63 100644 --- a/dspace/modules/server-boot/pom.xml +++ b/dspace/modules/server-boot/pom.xml @@ -108,6 +108,14 @@ org.springframework.boot spring-boot-maven-plugin + ${spring-boot.version} + + + + repackage + + + From 313a1d8d68f6721ffe4b7a13eaa220df5b9f0a21 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Wed, 15 Mar 2023 17:58:16 +0100 Subject: [PATCH 024/118] [DSC-963] Suppress checkstyle warning --- .../src/main/java/org/dspace/app/ServerBootApplication.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dspace/modules/server-boot/src/main/java/org/dspace/app/ServerBootApplication.java b/dspace/modules/server-boot/src/main/java/org/dspace/app/ServerBootApplication.java index f46532ff14b5..5efa79a02aca 100644 --- a/dspace/modules/server-boot/src/main/java/org/dspace/app/ServerBootApplication.java +++ b/dspace/modules/server-boot/src/main/java/org/dspace/app/ServerBootApplication.java @@ -20,13 +20,10 @@ * @author Luca Giamminonni (luca.giamminonni at 4science.it) * */ +@SuppressWarnings({ "checkstyle:hideutilityclassconstructor" }) @SpringBootApplication(scanBasePackageClasses = WebApplication.class) public class ServerBootApplication { - private ServerBootApplication() { - - } - public static void main(String[] args) { new SpringApplicationBuilder(ServerBootApplication.class) .initializers(new DSpaceKernelInitializer(), new DSpaceConfigurationInitializer()) From c6e00a1a302844bd52ffdaf88f3c03089eafa4b5 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 23 Mar 2023 15:06:32 +0100 Subject: [PATCH 025/118] [DSC-963] Fixed webjars classpath --- .../src/main/java/org/dspace/app/rest/WebApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java index 4f3002dd49ee..c9d859a4406f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java @@ -208,7 +208,7 @@ public void addResourceHandlers(ResourceHandlerRegistry registry) { // Make all other Webjars available off the /webjars path registry .addResourceHandler("/webjars/**") - .addResourceLocations("/webjars/"); + .addResourceLocations("/webjars/", "classpath:/META-INF/resources/webjars/"); } @Override From 3767ae8ab18fa621a1caa4461d7fd94a4d0cd799 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 20 Apr 2023 11:47:04 +0200 Subject: [PATCH 026/118] [DSC-963] Fixed porting on main --- .../GenericAuthorizationFeatureIT.java | 18 ++++++------------ dspace/modules/server/pom.xml | 13 +++---------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java index e6ccf5954c7a..1d3b5b051605 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java @@ -757,8 +757,7 @@ public void testCanMoveAdmin() throws Exception { // Verify the general admin has this feature on item 1 getClient(adminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID()) - .param("size", "1000")) + + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -766,8 +765,7 @@ public void testCanMoveAdmin() throws Exception { // Verify community A admin has this feature on item 1 getClient(communityAAdminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID()) - .param("size", "1000")) + + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -775,8 +773,7 @@ public void testCanMoveAdmin() throws Exception { // Verify collection X admin has this feature on item 1 getClient(collectionXAdminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID()) - .param("size", "1000")) + + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -784,8 +781,7 @@ public void testCanMoveAdmin() throws Exception { // Verify item 1 admin doesn’t have this feature on item 1 getClient(item1AdminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID()) - .param("size", "1000")) + + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -793,8 +789,7 @@ public void testCanMoveAdmin() throws Exception { // Verify community A admin doesn’t have this feature on item 2 getClient(communityAAdminToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item2.getID()) - .param("size", "1000")) + + "http://localhost/api/core/items/" + item2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -813,8 +808,7 @@ public void testCanMoveAdmin() throws Exception { // verify item 1 write has this feature on item 1 getClient(item1WriterToken).perform( get("/api/authz/authorizations/search/object?embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID()) - .param("size", "1000")) + + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='canMove')]") .exists()); diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index 65849295e8c4..152c905f45de 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -7,7 +7,7 @@ modules org.dspace - cris-2022.03.01-SNAPSHOT + 7.6-SNAPSHOT .. @@ -149,7 +149,7 @@ - ${agnostic.build.dir}/testing/dspace + ${agnostic.build.dir}/testing/dspace/ true ${agnostic.build.dir}/testing/dspace/solr/ @@ -218,7 +218,7 @@ - ${agnostic.build.dir}/testing/dspace + ${agnostic.build.dir}/testing/dspace/ true ${agnostic.build.dir}/testing/dspace/solr/ @@ -330,13 +330,6 @@ solr-core ${solr.client.version} test - - - - org.apache.commons - commons-text - - org.apache.lucene From fe621b37b0e340419631c95704c2a99ee8c7e4be Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 20 Apr 2023 12:06:34 +0200 Subject: [PATCH 027/118] [DSC-963] Fixed GenericAuthorizationFeatureIT tests --- .../GenericAuthorizationFeatureIT.java | 322 ++++++++++-------- 1 file changed, 182 insertions(+), 140 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java index 1d3b5b051605..d59ef00018b1 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java @@ -209,7 +209,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { String siteId = ContentServiceFactory.getInstance().getSiteService().findSite(context).getID().toString(); // Verify the general admin has this feature on the site - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/sites/" + siteId)) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -217,14 +217,14 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify community A admin doesn’t have this feature on the site getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/sites/" + siteId)) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on community A - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -232,7 +232,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify community A admin has this feature on community A getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -240,7 +240,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify community A admin has this feature on community AA getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -248,7 +248,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify collection X admin doesn’t have this feature on community A getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -256,7 +256,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify community A admin doesn’t have this feature on community B getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityB.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -264,7 +264,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify the general admin has this feature on collection X getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -272,7 +272,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify community A admin has this feature on collection X getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -280,7 +280,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify collection X admin has this feature on collection X getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -288,7 +288,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify item 1 admin doesn’t have this feature on collection X getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -296,7 +296,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify collection X admin doesn’t have this feature on collection Y getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionY.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -304,7 +304,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify the general admin has this feature on item 1 getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -312,7 +312,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify community A admin has this feature on item 1 getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -320,7 +320,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify collection X admin has this feature on item 1 getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -328,7 +328,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify item 1 admin has this feature on item 1 getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -336,7 +336,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify item 1 admin doesn’t have this feature on item 2 getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -344,7 +344,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify the general admin has this feature on the bundle in item 1 getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -352,7 +352,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify community A admin has this feature on the bundle in item 1 getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -360,7 +360,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify collection X admin has this feature on the bundle in item 1 getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -368,7 +368,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify item 1 admin has this feature on the bundle in item 1 getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -376,7 +376,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify item 1 admin doesn’t have this feature on the bundle in item 2 getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -384,7 +384,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify the general admin has this feature on the bitstream in item 1 getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -392,7 +392,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify community A admin has this feature on the bitstream in item 1 getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -400,7 +400,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify collection X admin has this feature on the bitstream in item 1 getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -408,7 +408,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify item 1 admin has this feature on the bitstream in item 1 getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -416,7 +416,7 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { // Verify item 1 admin doesn’t have this feature on the bitstream in item 2 getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -431,7 +431,7 @@ private void testAdminsHavePermissionsItem(String feature) throws Exception { // Verify the general admin has this feature on item 1 getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -439,7 +439,7 @@ private void testAdminsHavePermissionsItem(String feature) throws Exception { // Verify community A admin has this feature on item 1 getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -447,7 +447,7 @@ private void testAdminsHavePermissionsItem(String feature) throws Exception { // Verify collection X admin has this feature on item 1 getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -455,7 +455,7 @@ private void testAdminsHavePermissionsItem(String feature) throws Exception { // Verify item 1 admin has this feature on item 1 getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -463,7 +463,7 @@ private void testAdminsHavePermissionsItem(String feature) throws Exception { // Verify community A admin doesn’t have this feature on item 2 getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -480,14 +480,14 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // (or doesn’t have access otherwise) if (hasDSOAccess) { getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); } else { getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -496,7 +496,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify community A write doesn’t have this feature on community AA getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -504,7 +504,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify community A write doesn’t have this feature on collection X getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -512,7 +512,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify community A write doesn’t have this feature on item 1 getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -520,7 +520,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify community A write doesn’t have this feature on the bundle in item 1 getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -528,7 +528,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify community A write doesn’t have this feature on the bitstream in item 1 getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -536,7 +536,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify collection X write doesn’t have this feature on community A getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -544,7 +544,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify collection X write doesn’t have this feature on community AA getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -554,14 +554,14 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // (or doesn’t have access otherwise) if (hasDSOAccess) { getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); } else { getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -570,7 +570,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify collection X write doesn’t have this feature on item 1 getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -578,7 +578,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify collection X write doesn’t have this feature on the bundle in item 1 getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -586,7 +586,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify collection X write doesn’t have this feature on the bitstream in item 1 getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -594,7 +594,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify item 1 write doesn’t have this feature on community A getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -602,7 +602,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify item 1 write doesn’t have this feature on community AA getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -610,7 +610,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify item 1 write doesn’t have this feature on collection X getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -620,14 +620,14 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // (or doesn’t have access otherwise) if (hasDSOAccess) { getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); } else { getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -636,7 +636,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify item 1 write doesn’t have this feature on the bundle in item 1 getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -644,7 +644,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify item 1 write doesn’t have this feature on the bitstream in item 1 getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -652,7 +652,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify community A write doesn’t have this feature on community B getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityB.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -660,7 +660,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify collection X write doesn’t have this feature on collection Y getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionY.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -668,7 +668,7 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify item 1 write doesn’t have this feature on item 2 getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -682,7 +682,7 @@ private void testWriteUsersHavePermissionsItem(String feature, boolean hasDSOAcc // Verify community A write doesn’t have this feature on item 1 getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -690,7 +690,7 @@ private void testWriteUsersHavePermissionsItem(String feature, boolean hasDSOAcc // Verify collection X write doesn’t have this feature on item 1 getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -700,14 +700,14 @@ private void testWriteUsersHavePermissionsItem(String feature, boolean hasDSOAcc // (or doesn’t have access otherwise) if (hasDSOAccess) { getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); } else { getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -716,7 +716,7 @@ private void testWriteUsersHavePermissionsItem(String feature, boolean hasDSOAcc // Verify item 1 write doesn’t have this feature on item 2 getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -756,7 +756,7 @@ public void testCanMoveAdmin() throws Exception { // Verify the general admin has this feature on item 1 getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -764,7 +764,7 @@ public void testCanMoveAdmin() throws Exception { // Verify community A admin has this feature on item 1 getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -772,7 +772,7 @@ public void testCanMoveAdmin() throws Exception { // Verify collection X admin has this feature on item 1 getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -780,7 +780,7 @@ public void testCanMoveAdmin() throws Exception { // Verify item 1 admin doesn’t have this feature on item 1 getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -788,7 +788,7 @@ public void testCanMoveAdmin() throws Exception { // Verify community A admin doesn’t have this feature on item 2 getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -807,7 +807,7 @@ public void testCanMoveAdmin() throws Exception { // verify item 1 write has this feature on item 1 getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='canMove')]") @@ -830,7 +830,7 @@ public void testCanMoveWriter() throws Exception { String item1WriterToken = getAuthToken(item1Writer.getEmail(), password); // verify item 1 write has this feature on item 1 getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='canMove')]") @@ -867,28 +867,30 @@ public void testCanDeleteAdmin() throws Exception { final String feature = "canDelete"; // Verify the general admin doesn’t have this feature on the site - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/sites/" + siteId)) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on community A - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on community A - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on community AA - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -908,161 +910,173 @@ public void testCanDeleteAdmin() throws Exception { .build(); context.restoreAuthSystemState(); String communityAAAdminToken = getAuthToken(communityAAAdmin.getEmail(), password); - getClient(communityAAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X admin doesn’t have this feature on community A - getClient(collectionXAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify community A admin doesn’t have this feature on community B - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityB.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on collection X - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on collection X - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin doesn’t have this feature on collection X - getClient(collectionXAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 admin doesn’t have this feature on collection X - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X admin doesn’t have this feature on collection Y - getClient(collectionXAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionY.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on item 1 - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on item 1 - getClient(collectionXAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 admin doesn’t have this feature on item 2 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on the bundle in item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bundle in item 1 - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bundle in item 1 - getClient(collectionXAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bundle in item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on the bundle in item 2 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on the bitstream in item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bitstream in item 1 - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bitstream in item 1 - getClient(collectionXAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bitstream in item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on the bitstream in item 2 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1090,7 +1104,8 @@ public void testCanDeleteAdminParent() throws Exception { context.restoreAuthSystemState(); String communityAAAdminToken = getAuthToken(communityAAAdmin.getEmail(), password); //verify the community AA admin has this feature on community AA - getClient(communityAAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1105,7 +1120,8 @@ public void testCanDeleteAdminParent() throws Exception { .build(); context.restoreAuthSystemState(); // verify collection X admin has this feature on collection X - getClient(collectionXAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1120,7 +1136,7 @@ public void testCanDeleteAdminParent() throws Exception { .build(); context.restoreAuthSystemState(); // verify item 1 admin has this feature on item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1151,13 +1167,15 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String communityADeleterToken = getAuthToken(communityADeleter.getEmail(), password); // Verify the user has this feature on community A - getClient(communityADeleterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityADeleterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on community AA - getClient(communityADeleterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityADeleterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1179,19 +1197,22 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String communityARemoverToken = getAuthToken(communityARemover.getEmail(), password); // Verify the user has this feature on community AA - getClient(communityARemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityARemoverToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on community A - getClient(communityARemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityARemoverToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify this user doesn’t have this feature on collection X - getClient(communityARemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityARemoverToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1212,19 +1233,22 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String communityAARemoverToken = getAuthToken(communityAARemover.getEmail(), password); // Verify the user has this feature on collection X - getClient(communityAARemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAARemoverToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on community AA - getClient(communityAARemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAARemoverToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/communities/" + communityAA.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify this user doesn’t have this feature on item 1 - getClient(communityAARemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAARemoverToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1245,7 +1269,8 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String collectionXRemoverToken = getAuthToken(collectionXRemover.getEmail(), password); // Verify the user doesn’t have this feature on item 1 - getClient(collectionXRemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXRemoverToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1266,7 +1291,7 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String item1DeleterToken = getAuthToken(item1Deleter.getEmail(), password); // Verify the user doesn’t have this feature on item 1 - getClient(item1DeleterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1DeleterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1293,21 +1318,21 @@ public void testCanDeleteMinimalPermissions() throws Exception { String collectionXRemoverItem1DeleterToken = getAuthToken(collectionXRemoverItem1Deleter.getEmail(), password); // Verify the user has this feature on item 1 getClient(collectionXRemoverItem1DeleterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on collection X getClient(collectionXRemoverItem1DeleterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/collections/" + collectionX.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify this user doesn’t have this feature on the bundle in item 1 getClient(collectionXRemoverItem1DeleterToken).perform( - get("/api/authz/authorizations/search/object?embed=feature&uri=" + get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1328,19 +1353,19 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String item1RemoverToken = getAuthToken(item1Remover.getEmail(), password); // Verify the user has this feature on the bundle in item 1 - getClient(item1RemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1RemoverToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on item 1 - getClient(item1RemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1RemoverToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify this user doesn’t have this feature on the bitstream in item 1 - getClient(item1RemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1RemoverToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1361,7 +1386,8 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String bundle1RemoverToken = getAuthToken(bundle1Remover.getEmail(), password); // Verify the user doesn’t have this feature on the bitstream in item 1 - getClient(bundle1RemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(bundle1RemoverToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1388,7 +1414,8 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String bundle1item1RemoverToken = getAuthToken(bundle1item1Remover.getEmail(), password); // Verify the user has this feature on the bitstream in item 1 - getClient(bundle1item1RemoverToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(bundle1item1RemoverToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1404,35 +1431,38 @@ public void testCanReorderBitstreamsAdmin() throws Exception { final String feature = "canReorderBitstreams"; // Verify the general admin has this feature on the bundle in item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bundle in item 1 - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bundle in item 1 - getClient(collectionXAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bundle in item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin doesn’t have this feature on the bundle in item 2 - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1447,19 +1477,21 @@ public void testCanReorderBitstreamsWriter() throws Exception { final String feature = "canReorderBitstreams"; // Verify community A write doesn’t have this feature on the bundle in item 1 - getClient(communityAWriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAWriterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on the bundle in item 1 - getClient(collectionXWriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXWriterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on the bundle in item 1 - getClient(item1WriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1WriterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1467,7 +1499,8 @@ public void testCanReorderBitstreamsWriter() throws Exception { // Create a new user, grant WRITE permissions on the bundle in item 1 to this user // Verify the user has this feature on the bundle in item 1 - getClient(communityAWriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAWriterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1483,35 +1516,38 @@ public void testCanCreateBitstreamAdmin() throws Exception { final String feature = "canCreateBitstream"; // Verify the general admin has this feature on the bundle in item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bundle in item 1 - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bundle in item 1 - getClient(collectionXAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bundle in item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin doesn’t have this feature on the bundle in item 2 - getClient(communityAAdminToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAAdminToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle2.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1526,21 +1562,23 @@ public void testCanCreateBitstreamWriter() throws Exception { final String feature = "canCreateBitstream"; // Verify community A write doesn’t have this feature on the bundle in item 1 - getClient(communityAWriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAWriterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on the bundle in item 1 - getClient(collectionXWriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXWriterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on the bundle in item 1 - getClient(item1WriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1WriterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1561,7 +1599,7 @@ public void testCanCreateBitstreamWriter() throws Exception { context.restoreAuthSystemState(); String bundle1WriterToken = getAuthToken(bundle1Writer.getEmail(), password); // Verify the user doesn’t have this feature on the bundle in item 1 - getClient(bundle1WriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(bundle1WriterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1582,7 +1620,7 @@ public void testCanCreateBitstreamWriter() throws Exception { context.restoreAuthSystemState(); String bundle1AdderToken = getAuthToken(bundle1Adder.getEmail(), password); // Verify the user doesn’t have this feature on the bundle in item 1 - getClient(bundle1AdderToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(bundle1AdderToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1619,7 +1657,8 @@ public void testCanCreateBitstreamWriter() throws Exception { context.restoreAuthSystemState(); String bundle1WriterAdderToken = getAuthToken(bundle1WriterAdder.getEmail(), password); // Verify the user has this feature on the bundle in item 1 - getClient(bundle1WriterAdderToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(bundle1WriterAdderToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/bundles/" + bundle1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1639,21 +1678,23 @@ public void testCanCreateBundleWriter() throws Exception { final String feature = "canCreateBundle"; // Verify community A write doesn’t have this feature on item 1 - getClient(communityAWriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(communityAWriterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on item 1 - getClient(collectionXWriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(collectionXWriterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on item 1 - getClient(item1WriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1WriterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" @@ -1679,7 +1720,8 @@ public void testCanCreateBundleWriter() throws Exception { context.restoreAuthSystemState(); String item1AdderWriterToken = getAuthToken(item1AdderWriter.getEmail(), password); // Verify the user has this feature on item 1 - getClient(item1AdderWriterToken).perform(get("/api/authz/authorizations/search/object?embed=feature&uri=" + getClient(item1AdderWriterToken) + .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" + "http://localhost/api/core/items/" + item1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" From 3fd93df91e7db9364e9c331a8a43904004bfc0bf Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 20 Apr 2023 13:38:58 +0200 Subject: [PATCH 028/118] [DSC-963] Added missing applicationContext set on DefaultMethodSecurityExpressionHandler --- .../dspace/app/rest/security/MethodSecurityConfig.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/MethodSecurityConfig.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/MethodSecurityConfig.java index 29bfc13d83ce..5ee308c73ed8 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/MethodSecurityConfig.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/MethodSecurityConfig.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.security; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; @@ -22,10 +23,13 @@ public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { @Autowired private PermissionEvaluator dSpacePermissionEvaluator; + @Autowired + private ApplicationContext applicationContext; + @Override protected MethodSecurityExpressionHandler createExpressionHandler() { - DefaultMethodSecurityExpressionHandler expressionHandler = - new DefaultMethodSecurityExpressionHandler(); + DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); + expressionHandler.setApplicationContext(applicationContext); expressionHandler.setPermissionEvaluator(dSpacePermissionEvaluator); return expressionHandler; } From ced1c79d1ac4d0adfe5a28a36fb0089a730b8783 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 20 Apr 2023 13:56:08 +0200 Subject: [PATCH 029/118] [DSC-963] Removed duplicated @EnableGlobalMethodSecurity --- .../org/dspace/app/rest/security/WebSecurityConfiguration.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index fad60c20d250..fed978e7fad6 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -41,7 +41,6 @@ @EnableWebSecurity @Configuration @EnableConfigurationProperties(SecurityProperties.class) -@EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { public static final String ADMIN_GRANT = "ADMIN"; From b35b837a2a713b0620d43d0d54ea046b26191c30 Mon Sep 17 00:00:00 2001 From: Luca Giamminonni Date: Thu, 20 Apr 2023 14:23:30 +0200 Subject: [PATCH 030/118] [DSC-963] Fixed checkstyle --- .../org/dspace/app/rest/security/WebSecurityConfiguration.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java index fed978e7fad6..7aa22533fd9b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WebSecurityConfiguration.java @@ -20,7 +20,6 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; From efcf9dba20ac4c49516d8486a33e14f06db86080 Mon Sep 17 00:00:00 2001 From: MajoBerger Date: Fri, 9 Jun 2023 11:22:50 +0200 Subject: [PATCH 031/118] added failsafe while creating admin when db is not connected --- .../org/dspace/administer/CreateAdministrator.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java b/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java index 81250e9c8259..58b85493915a 100644 --- a/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java +++ b/dspace-api/src/main/java/org/dspace/administer/CreateAdministrator.java @@ -116,6 +116,17 @@ public static void main(String[] argv) protected CreateAdministrator() throws Exception { context = new Context(); + try { + context.getDBConfig(); + } catch (NullPointerException npr) { + // if database is null, there is no point in continuing. Prior to this exception and catch, + // NullPointerException was thrown, that wasn't very helpful. + throw new IllegalStateException("Problem connecting to database. This " + + "indicates issue with either network or version (or possibly some other). " + + "If you are running this in docker-compose, please make sure dspace-cli was " + + "built from the same sources as running dspace container AND that they are in " + + "the same project/network."); + } groupService = EPersonServiceFactory.getInstance().getGroupService(); ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); } From a4e580d3c5d33fbf5403dbd8ae9a51ee3d98bc09 Mon Sep 17 00:00:00 2001 From: Francesco Bacchelli Date: Tue, 22 Aug 2023 12:04:24 +0200 Subject: [PATCH 032/118] CST-11299 integration test fix --- .../org/dspace/app/rest/ExternalSourcesRestControllerIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java index ef38e00980a9..f22eb94aa012 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java @@ -54,7 +54,7 @@ public void findAllExternalSources() throws Exception { ExternalSourceMatcher.matchExternalSource( "openAIREFunding", "openAIREFunding", false) ))) - .andExpect(jsonPath("$.page.totalElements", Matchers.is(10))); + .andExpect(jsonPath("$.page.totalElements", Matchers.is(11))); } @Test From f821c3a628944623a12289d53a7462aae1655781 Mon Sep 17 00:00:00 2001 From: Agustina Martinez Date: Tue, 3 Oct 2023 10:53:54 +0200 Subject: [PATCH 033/118] Update InstallItemServiceImpl.java Remove setting dc.date.available --- .../org/dspace/content/InstallItemServiceImpl.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java index 32c5b92c605b..db65c40cba4d 100644 --- a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java @@ -150,7 +150,6 @@ public Item restoreItem(Context c, InProgressSubmission is, return finishItem(c, item, is); } - protected void populateMetadata(Context c, Item item) throws SQLException, AuthorizeException { // create accession date @@ -158,15 +157,6 @@ protected void populateMetadata(Context c, Item item) itemService.addMetadata(c, item, MetadataSchemaEnum.DC.getName(), "date", "accessioned", null, now.toString()); - // add date available if not under embargo, otherwise it will - // be set when the embargo is lifted. - // this will flush out fatal embargo metadata - // problems before we set inArchive. - if (embargoService.getEmbargoTermsAsDate(c, item) == null) { - itemService.addMetadata(c, item, MetadataSchemaEnum.DC.getName(), - "date", "available", null, now.toString()); - } - // If issue date is set as "today" (literal string), then set it to current date // In the below loop, we temporarily clear all issued dates and re-add, one-by-one, // replacing "today" with today's date. From 927d345ebe5ae263dfabb84988c9f1742bd2f725 Mon Sep 17 00:00:00 2001 From: Agustina Martinez Date: Thu, 5 Oct 2023 17:48:19 +0100 Subject: [PATCH 034/118] Fix integration test count number of fields --- .../java/org/dspace/app/rest/RelationshipRestRepositoryIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java index d8e53c770c70..0ff6fe04e9d8 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java @@ -1035,7 +1035,7 @@ public void addRelationshipsAndMetadataToValidatePlaceTest() throws Exception { list = itemService.getMetadata(publication1, "dc", "contributor", Item.ANY, Item.ANY); assertEquals(10, list.size()); //same size as authors list = itemService.getMetadata(publication1, "dc", Item.ANY, Item.ANY, Item.ANY); - assertEquals(16, list.size()); //also includes title, 4 date fields, uri + assertEquals(15, list.size()); //also includes title, 3 date fields, uri list = itemService.getMetadata(publication1, Item.ANY, Item.ANY, Item.ANY, Item.ANY); // also includes type, 3 relation.isAuthorOfPublication and 3 relation.isAuthorOfPublication.latestForDiscovery // values From 8f565590ea291f4c7b70eafde866b52499ad7b70 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 10 Nov 2023 16:04:23 +0100 Subject: [PATCH 035/118] [DURACOM-204][#9192] Makes forgot-password link removable --- .../org/dspace/app/util/AuthorizeUtil.java | 31 +++- .../impl/EPersonForgotPasswordFeature.java | 58 ++++++ .../RegistrationRestRepository.java | 3 + .../EPersonForgotPasswordFeatureIT.java | 167 ++++++++++++++++++ .../modules/authentication-password.cfg | 5 +- 5 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonForgotPasswordFeature.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java diff --git a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java index efd813d29b47..b498b693956a 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java +++ b/dspace-api/src/main/java/org/dspace/app/util/AuthorizeUtil.java @@ -628,12 +628,23 @@ public static boolean authorizeNewAccountRegistration(Context context, HttpServl // actually expected to be returning true. // For example the LDAP canSelfRegister will return true due to auto-register, while that // does not imply a new user can register explicitly - return AuthenticateServiceFactory.getInstance().getAuthenticationService() - .allowSetPassword(context, request, null); + return authorizePasswordChange(context, request); } return false; } + /** + * This method will return a boolean indicating whether the current user is allowed to reset the password + * or not + * + * @return A boolean indicating whether the current user can reset its password or not + * @throws SQLException If something goes wrong + */ + public static boolean authorizeForgotPassword() { + return DSpaceServicesFactory.getInstance().getConfigurationService() + .getBooleanProperty("user.forgot-password", true); + } + /** * This method will return a boolean indicating whether it's allowed to update the password for the EPerson * with the given email and canLogin property @@ -647,8 +658,7 @@ public static boolean authorizeUpdatePassword(Context context, String email) { if (eperson != null && eperson.canLogIn()) { HttpServletRequest request = new DSpace().getRequestService().getCurrentRequest() .getHttpServletRequest(); - return AuthenticateServiceFactory.getInstance().getAuthenticationService() - .allowSetPassword(context, request, null); + return authorizePasswordChange(context, request); } } catch (SQLException e) { log.error("Something went wrong trying to retrieve EPerson for email: " + email, e); @@ -656,6 +666,19 @@ public static boolean authorizeUpdatePassword(Context context, String email) { return false; } + /** + * Checks if the current configuration has at least one password based authentication method + * + * @param context Dspace Context + * @param request Current Request + * @return True if the password change is enabled + * @throws SQLException + */ + protected static boolean authorizePasswordChange(Context context, HttpServletRequest request) throws SQLException { + return AuthenticateServiceFactory.getInstance().getAuthenticationService() + .allowSetPassword(context, request, null); + } + /** * This method checks if the community Admin can manage accounts * diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonForgotPasswordFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonForgotPasswordFeature.java new file mode 100644 index 000000000000..c6e6b55526d6 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/EPersonForgotPasswordFeature.java @@ -0,0 +1,58 @@ +/** + * 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.authorization.impl; + +import java.sql.SQLException; + +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; +import org.dspace.app.rest.model.BaseObjectRest; +import org.dspace.app.rest.model.EPersonRest; +import org.dspace.app.rest.model.SiteRest; +import org.dspace.app.util.AuthorizeUtil; +import org.dspace.core.Context; +import org.springframework.stereotype.Component; + +/** + * Checks if the user provided is allowed to request a password reset. + * If none user specified, checks if the current context is allowed to set the password. + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +@Component +@AuthorizationFeatureDocumentation(name = EPersonForgotPasswordFeature.NAME, + description = "It can be used to check password reset for an eperson") +public class EPersonForgotPasswordFeature implements AuthorizationFeature { + + public static final String NAME = "epersonForgotPassword"; + + @Override + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { + boolean isEperson = object instanceof EPersonRest; + boolean isSite = object instanceof SiteRest; + if (!isEperson && !isSite) { + return false; + } + if (!AuthorizeUtil.authorizeForgotPassword()) { + return false; + } + if (isEperson) { + return AuthorizeUtil.authorizeUpdatePassword(context, ((EPersonRest) object).getEmail()); + } + return true; + } + + @Override + public String[] getSupportedTypes() { + return new String[] { + SiteRest.CATEGORY + "." + SiteRest.NAME, + EPersonRest.CATEGORY + "." + EPersonRest.NAME + }; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java index be28170d8a07..3d183fd3418c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/RegistrationRestRepository.java @@ -127,6 +127,9 @@ public RegistrationRest createAndReturn(Context context) { } if (eperson != null && accountType.equalsIgnoreCase(TYPE_FORGOT)) { try { + if (!AuthorizeUtil.authorizeForgotPassword()) { + throw new AccessDeniedException("Password reset is not allowed!"); + } if (!AuthorizeUtil.authorizeUpdatePassword(context, eperson.getEmail())) { throw new DSpaceBadRequestException("Password cannot be updated for the given EPerson with email: " + eperson.getEmail()); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java new file mode 100644 index 000000000000..79456c7a7a44 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java @@ -0,0 +1,167 @@ +/** + * 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.authorization; + +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.authorization.impl.EPersonForgotPasswordFeature; +import org.dspace.app.rest.authorization.impl.EPersonRegistrationFeature; +import org.dspace.app.rest.converter.EPersonConverter; +import org.dspace.app.rest.converter.SiteConverter; +import org.dspace.app.rest.model.EPersonRest; +import org.dspace.app.rest.model.SiteRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.builder.EPersonBuilder; +import org.dspace.content.Site; +import org.dspace.content.service.SiteService; +import org.dspace.eperson.EPerson; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + **/ +public class EPersonForgotPasswordFeatureIT extends AbstractControllerIntegrationTest { + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private SiteService siteService; + + @Autowired + private SiteConverter siteConverter; + + + @Autowired + private EPersonConverter personConverter; + + @Autowired + private Utils utils; + + private AuthorizationFeature epersonForgotPasswordFeature; + + public static final String[] SHIB_ONLY = {"org.dspace.authenticate.ShibAuthentication"}; + + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + epersonForgotPasswordFeature = authorizationFeatureService.find(EPersonForgotPasswordFeature.NAME); + } + + @Test + public void userForgotPasswordSuccessTest() throws Exception { + String property = configurationService.getProperty("user.forgot-password"); + + context.turnOffAuthorisationSystem(); + EPerson epersonPassLogin = EPersonBuilder.createEPerson(context) + .withNameInMetadata("Vincenzo", "Mecca") + .withCanLogin(true) + .withPassword("Strong-Password") + .withEmail("vincenzo.mecca@4science.it") + .build(); + context.restoreAuthSystemState(); + + try { + configurationService.setProperty("user.forgot-password", true); + EPersonRest personRest = personConverter.convert(epersonPassLogin, Projection.DEFAULT); + String personUri = utils.linkToSingleResource(personRest, "self").getHref(); + + getClient().perform(get("/api/authz/authorizations/search/object") + .param("uri", personUri) + .param("feature", epersonForgotPasswordFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", greaterThan(0))); + } finally { + configurationService.setProperty("user.forgot-password", property); + } + } + + @Test + public void userForgotPasswordFeatureUnauthorizedTest() throws Exception { + String property = configurationService.getProperty("user.forgot-password"); + + Site site = siteService.findSite(context); + SiteRest SiteRest = siteConverter.convert(site, Projection.DEFAULT); + String siteUri = utils.linkToSingleResource(SiteRest, "self").getHref(); + + try { + configurationService.setProperty("user.forgot-password", false); + + getClient().perform(get("/api/authz/authorizations/search/object") + .param("uri", siteUri) + .param("feature", epersonForgotPasswordFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + } finally { + configurationService.setProperty("user.forgot-password", property); + } + } + + + @Test + public void userForgotPasswordNoLoginTest() throws Exception { + + context.turnOffAuthorisationSystem(); + EPerson noLoginPerson = EPersonBuilder.createEPerson(context) + .withNameInMetadata("User", "NoLogin") + .withCanLogin(false) + .withPassword("Strong-Password") + .build(); + context.restoreAuthSystemState(); + + EPersonRest personRest = personConverter.convert(noLoginPerson, Projection.DEFAULT); + String personUri = utils.linkToSingleResource(personRest, "self").getHref(); + String property = configurationService.getProperty("user.forgot-password"); + try { + configurationService.setProperty("user.forgot-password", true); + getClient().perform(get("/api/authz/authorizations/search/object") + .param("uri", personUri) + .param("feature", epersonForgotPasswordFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } finally { + configurationService.setProperty("user.forgot-password", property); + } + } + + @Test + public void userForgotPasswordUnauthorizedNoPasswordAuthMethodTest() throws Exception { + //Enable Shibboleth and password login + String property = + configurationService.getProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod"); + + try { + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); + + EPersonRest personRest = personConverter.convert(eperson, Projection.DEFAULT); + String personUri = utils.linkToSingleResource(personRest, "self").getHref(); + + getClient().perform(get("/api/authz/authorizations/search/object") + .param("uri", personUri) + .param("feature", epersonForgotPasswordFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } finally { + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", property); + } + } +} diff --git a/dspace/config/modules/authentication-password.cfg b/dspace/config/modules/authentication-password.cfg index 9029e76e2621..81815e30e7d2 100644 --- a/dspace/config/modules/authentication-password.cfg +++ b/dspace/config/modules/authentication-password.cfg @@ -7,7 +7,10 @@ # # self-registration can be disabled completely by setting the user.registration property to false -# user.registration = false +#user.registration = false + +# forgot-password can be disabled completely by setting the user.forgot-password property to false +#user.forgot-password = false # Only emails ending in the following domains are allowed to self-register # Example - example.com domain : @example.com From 6f33da628b18d2fd371fdb140c36a337aa67f606 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 10 Nov 2023 16:40:11 +0100 Subject: [PATCH 036/118] [DURACOM-204][#9192] Removes unused import --- .../app/rest/authorization/EPersonForgotPasswordFeatureIT.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java index 79456c7a7a44..f7c0f3ba0ea4 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java @@ -14,7 +14,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.dspace.app.rest.authorization.impl.EPersonForgotPasswordFeature; -import org.dspace.app.rest.authorization.impl.EPersonRegistrationFeature; import org.dspace.app.rest.converter.EPersonConverter; import org.dspace.app.rest.converter.SiteConverter; import org.dspace.app.rest.model.EPersonRest; From c974e73e2c1bcaf7b16e28752f0b87c99a242a02 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Wed, 13 Dec 2023 15:42:47 +0100 Subject: [PATCH 037/118] [CST-12825] Ror integration --- .../external/datamodel/ImportRecord.java | 13 + .../RorParentOrgUnitMetadataContributor.java | 109 + .../external/ror/service/RorFieldMapping.java | 38 + .../RorImportMetadataSourceServiceImpl.java | 266 ++ .../spring-dspace-addon-import-services.xml | 6 + .../RorImportMetadataSourceServiceIT.java | 137 + .../app/rest/SitemapRestControllerIT.java | 14 +- .../org/dspace/app/rest/ror-record.json | 107 + .../org/dspace/app/rest/ror-records.json | 2383 +++++++++++++++++ dspace/config/dspace.cfg | 1 + dspace/config/modules/ror.cfg | 2 + .../registries/openaire-cerif-types.xml | 27 + .../registries/schema-organization-types.xml | 6 + .../config/spring/api/external-services.xml | 14 +- dspace/config/spring/api/ror-integration.xml | 111 + dspace/config/submission-forms.xml | 10 +- 16 files changed, 3236 insertions(+), 8 deletions(-) create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java create mode 100644 dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java create mode 100644 dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java create mode 100644 dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json create mode 100644 dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json create mode 100644 dspace/config/modules/ror.cfg create mode 100644 dspace/config/registries/openaire-cerif-types.xml create mode 100644 dspace/config/spring/api/ror-integration.xml diff --git a/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java b/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java index 3fc34dc51102..dcf1a62b28db 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java @@ -11,7 +11,9 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; +import org.dspace.content.MetadataFieldName; import org.dspace.importer.external.metadatamapping.MetadatumDTO; /** @@ -94,6 +96,17 @@ public Collection getValue(String schema, String element, String q return values; } + public Optional getSingleValue(String field) { + MetadataFieldName metadataFieldName = new MetadataFieldName(field); + return getSingleValue(metadataFieldName.schema, metadataFieldName.element, metadataFieldName.qualifier); + } + + public Optional getSingleValue(String schema, String element, String qualifier) { + return getValue(schema, element, qualifier).stream() + .map(MetadatumDTO::getValue) + .findFirst(); + } + /** * Add a value to the valueList * diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java new file mode 100644 index 000000000000..be1910d7a521 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java @@ -0,0 +1,109 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +public class RorParentOrgUnitMetadataContributor extends SimpleJsonPathMetadataContributor { + + private String typeField; + + private String parentType; + + private String labelField; + + /** + * Retrieve the metadata associated with the given object. + * The toString() of the resulting object will be used. + * + * @param fullJson A class to retrieve metadata from. + * @return a collection of import records. Only the identifier of the found records may be put in the record. + */ + @Override + public Collection contributeMetadata(String fullJson) { + + Collection metadata = new ArrayList<>(); + Collection metadataValue = new ArrayList<>(); + + JsonNode jsonNode = convertStringJsonToJsonNode(fullJson); + JsonNode array = jsonNode.at(getQuery()); + if (!array.isArray()) { + return metadata; + } + + Iterator nodes = array.iterator(); + while (nodes.hasNext()) { + JsonNode node = nodes.next(); + + if (!node.has(labelField)) { + continue; + } + + String type = node.has(typeField) ? node.get(typeField).asText() : null; + String label = node.get(labelField).asText(); + + if (parentType.equalsIgnoreCase(type)) { + metadataValue.add(label); + } + + } + + for (String value : metadataValue) { + MetadatumDTO metadatumDto = new MetadatumDTO(); + metadatumDto.setValue(value); + metadatumDto.setElement(getField().getElement()); + metadatumDto.setQualifier(getField().getQualifier()); + metadatumDto.setSchema(getField().getSchema()); + metadata.add(metadatumDto); + } + return metadata; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = null; + try { + body = mapper.readTree(json); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return body; + } + + public String getTypeField() { + return typeField; + } + + public void setTypeField(String typeField) { + this.typeField = typeField; + } + + public String getLabelField() { + return labelField; + } + + public void setLabelField(String labelField) { + this.labelField = labelField; + } + + public String getParentType() { + return parentType; + } + + public void setParentType(String parentType) { + this.parentType = parentType; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java new file mode 100644 index 000000000000..5248d793e292 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java @@ -0,0 +1,38 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.ror.service; + +import java.util.Map; +import javax.annotation.Resource; + +import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping; + + +/** + * An implementation of {@link AbstractMetadataFieldMapping} + * Responsible for defining the mapping of the Scopus metadatum fields on the DSpace metadatum fields + * + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + */ +public class RorFieldMapping extends AbstractMetadataFieldMapping { + + /** + * Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + * only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + * what metadatafield is generated. + * + * @param metadataFieldMap The map containing the link between retrieve metadata and metadata that will be set to + * the item. + */ + @Override + @Resource(name = "rorMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java new file mode 100644 index 000000000000..ebc7caefb26d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java @@ -0,0 +1,266 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.ror.service; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import javax.el.MethodNotFoundException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.datamodel.Query; +import org.dspace.importer.external.exception.MetadataSourceException; +import org.dspace.importer.external.liveimportclient.service.LiveImportClient; +import org.dspace.importer.external.service.AbstractImportMetadataSourceService; +import org.dspace.importer.external.service.components.QuerySource; +import org.springframework.beans.factory.annotation.Autowired; + +public class RorImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private final static Logger log = LogManager.getLogger(); + + private String url; + + private int timeout = 1000; + + @Autowired + private LiveImportClient liveImportClient; + + @Override + public String getImportSource() { + return "ror"; + } + + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + List records = retry(new SearchByIdCallable(id)); + return CollectionUtils.isEmpty(records) ? null : records.get(0); + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + return retry(new CountByQueryCallable(query)); + } + + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query)); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + return retry(new SearchByQueryCallable(query)); + } + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + List records = retry(new SearchByIdCallable(query)); + return CollectionUtils.isEmpty(records) ? null : records.get(0); + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for ROR"); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for ROR"); + } + + @Override + public void init() throws Exception { + } + + /** + * This class is a Callable implementation to get ADS entries based on query + * object. This Callable use as query value the string queryString passed to + * constructor. If the object will be construct through Query.class instance, a + * Query's map entry with key "query" will be used. Pagination is supported too, + * using the value of the Query's map with keys "start" and "count". + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ + private class SearchByQueryCallable implements Callable> { + + private Query query; + + private SearchByQueryCallable(String queryString) { + query = new Query(); + query.addParameter("query", queryString); + } + + private SearchByQueryCallable(Query query) { + this.query = query; + } + + @Override + public List call() throws Exception { + return search(query.getParameterAsClass("query", String.class)); + } + } + + /** + * This class is a Callable implementation to get an ADS entry using bibcode The + * bibcode to use can be passed through the constructor as a String or as + * Query's map entry, with the key "id". + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ + private class SearchByIdCallable implements Callable> { + private Query query; + + private SearchByIdCallable(Query query) { + this.query = query; + } + + private SearchByIdCallable(String id) { + this.query = new Query(); + query.addParameter("id", id); + } + + @Override + public List call() throws Exception { + return searchById(query.getParameterAsClass("id", String.class)); + } + } + + /** + * This class is a Callable implementation to count the number of entries for an + * ADS query. This Callable use as query value to ADS the string queryString + * passed to constructor. If the object will be construct through Query.class + * instance, the value of the Query's map with the key "query" will be used. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ + private class CountByQueryCallable implements Callable { + private Query query; + + private CountByQueryCallable(String queryString) { + query = new Query(); + query.addParameter("query", queryString); + } + + private CountByQueryCallable(Query query) { + this.query = query; + } + + @Override + public Integer call() throws Exception { + return count(query.getParameterAsClass("query", String.class)); + } + } + + public Integer count(String query) { + try { + Map> params = new HashMap>(); + + URIBuilder uriBuilder = new URIBuilder(this.url); + uriBuilder.addParameter("query", query); + + String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); + if (StringUtils.isEmpty(resp)) { + return 0; + } + JsonNode jsonNode = convertStringJsonToJsonNode(resp); + return jsonNode.at("/number_of_results").asInt(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + return 0; + } + + private List searchById(String id) { + + List adsResults = new ArrayList<>(); + + id = StringUtils.removeStart(id, "https://ror.org/"); + + try { + Map> params = new HashMap>(); + + URIBuilder uriBuilder = new URIBuilder(this.url + "/" + id); + + String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); + if (StringUtils.isEmpty(resp)) { + return adsResults; + } + + JsonNode jsonNode = convertStringJsonToJsonNode(resp); + adsResults.add(transformSourceRecords(jsonNode.toString())); + + } catch (URISyntaxException e) { + e.printStackTrace(); + } + return adsResults; + } + + private List search(String query) { + List adsResults = new ArrayList<>(); + try { + Map> params = new HashMap>(); + + URIBuilder uriBuilder = new URIBuilder(this.url); + uriBuilder.addParameter("query", query); + + String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); + if (StringUtils.isEmpty(resp)) { + return adsResults; + } + + JsonNode jsonNode = convertStringJsonToJsonNode(resp); + JsonNode docs = jsonNode.at("/items"); + if (docs.isArray()) { + Iterator nodes = docs.elements(); + while (nodes.hasNext()) { + JsonNode node = nodes.next(); + adsResults.add(transformSourceRecords(node.toString())); + } + } else { + adsResults.add(transformSourceRecords(docs.toString())); + } + } catch (URISyntaxException e) { + e.printStackTrace(); + } + return adsResults; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + try { + return new ObjectMapper().readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process json response.", e); + } + return null; + } + + public void setUrl(String url) { + this.url = url; + } + +} diff --git a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml index 6b0ef3e9b9e3..1b201c71efd2 100644 --- a/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml +++ b/dspace-api/src/main/resources/spring/spring-dspace-addon-import-services.xml @@ -150,6 +150,12 @@ + + + + + + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java new file mode 100644 index 000000000000..4f8e56f98054 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java @@ -0,0 +1,137 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +import static org.dspace.app.matcher.LambdaMatcher.matches; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Optional; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.liveimportclient.service.LiveImportClientImpl; +import org.dspace.importer.external.ror.service.RorImportMetadataSourceServiceImpl; +import org.hamcrest.Matcher; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +public class RorImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { + + @Autowired + private LiveImportClientImpl liveImportClient; + + @Autowired + private RorImportMetadataSourceServiceImpl rorServiceImpl; + + @Test + public void tesGetRecords() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClient.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream file = getClass().getResourceAsStream("ror-records.json")) { + + String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClient.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(jsonResponse, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Collection recordsImported = rorServiceImpl.getRecords("test query", 0, 2); + assertThat(recordsImported, hasSize(10)); + + ImportRecord record = recordsImported.iterator().next(); + + assertThat(record.getValueList(), hasSize(11)); + + assertThat(record.getSingleValue("dc.title"), is("The University of Texas")); + assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/02f6dcw23")); + assertThat(record.getSingleValue("oairecerif.acronym"), is("UTHSCSA")); + assertThat(record.getSingleValue("oairecerif.identifier.url"), is("http://www.uthscsa.edu/")); + assertThat(record.getSingleValue("dc.type"), is("Education")); + assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); + assertThat(record.getSingleValue("organization.foundingDate"), is("1959")); + assertThat(record.getValue("organization", "identifier", "crossrefid"), hasSize(2)); + assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0001 0629 5880")); + assertThat(record.getSingleValue("organization.parentOrganization"), is("The University of Texas System")); + + } finally { + liveImportClient.setHttpClient(originalHttpClient); + } + } + + @Test + public void tesCount() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClient.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream file = getClass().getResourceAsStream("ror-records.json")) { + + String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClient.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(jsonResponse, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + Integer count = rorServiceImpl.count("test"); + assertThat(count, equalTo(200)); + } finally { + liveImportClient.setHttpClient(originalHttpClient); + } + } + + @Test + public void tesGetRecord() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClient.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream file = getClass().getResourceAsStream("ror-record.json")) { + + String jsonResponse = IOUtils.toString(file, Charset.defaultCharset()); + + liveImportClient.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(jsonResponse, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + ImportRecord record = rorServiceImpl.getRecord("https://ror.org/01sps7q28"); + assertThat(record.getValueList(), hasSize(9)); + assertThat(record.getSingleValue("dc.title"), is("The University of Texas Health Science Center at Tyler")); + assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/01sps7q28")); + assertThat(record.getSingleValue("oairecerif.acronym"), is("UTHSCT")); + assertThat(record.getSingleValue("oairecerif.identifier.url"), + is("https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler")); + assertThat(record.getSingleValue("dc.type"), is("Healthcare")); + assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); + assertThat(record.getSingleValue("organization.foundingDate"), is("1947")); + assertThat(record.getSingleValue("organization.identifier.isni"), is("0000 0000 9704 5790")); + assertThat(record.getSingleValue("organization.parentOrganization"), is("The University of Texas System")); + + } finally { + liveImportClient.setHttpClient(originalHttpClient); + } + } + + private Matcher> is(String value) { + return matches(optionalValue -> optionalValue.isPresent() && optionalValue.get().equals(value)); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java index 175fb34e6cac..04d22718e846 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SitemapRestControllerIT.java @@ -216,7 +216,12 @@ public void testSitemap_sitemapIndexXml() throws Exception { //** THEN ** .andExpect(status().isOk()) //We expect the content type to match - .andExpect(content().contentType("application/xml;charset=UTF-8")) + .andExpect(res -> { + String actual = res.getResponse().getContentType(); + assertTrue("Content Type", + "text/xml;charset=UTF-8".equals(actual) || + "application/xml;charset=UTF-8".equals(actual)); + }) .andReturn(); String response = result.getResponse().getContentAsString(); @@ -232,7 +237,12 @@ public void testSitemap_sitemap0Xml() throws Exception { //** THEN ** .andExpect(status().isOk()) //We expect the content type to match - .andExpect(content().contentType("application/xml;charset=UTF-8")) + .andExpect(res -> { + String actual = res.getResponse().getContentType(); + assertTrue("Content Type", + "text/xml;charset=UTF-8".equals(actual) || + "application/xml;charset=UTF-8".equals(actual)); + }) .andReturn(); String response = result.getResponse().getContentAsString(); diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json new file mode 100644 index 000000000000..51924485b347 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-record.json @@ -0,0 +1,107 @@ +{ + "id": "https://ror.org/01sps7q28", + "name": "The University of Texas Health Science Center at Tyler", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1947, + "types": [ + "Healthcare" + ], + "relationships": [ + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 32.426014, + "lng": -95.212728, + "state": "Texas", + "state_code": "US-TX", + "city": "Tyler", + "geonames_city": { + "id": 4738214, + "city": "Tyler", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Smith County", + "id": 4729130, + "ascii_name": "Smith County", + "code": "US.TX.423" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler" + ], + "aliases": [ + "East Texas Tuberculosis Sanitarium", + "UT Health Northeast" + ], + "acronyms": [ + "UTHSCT" + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_Tyler", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9704 5790" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "3446655" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q7896437" + ] + }, + "GRID": { + "preferred": "grid.267310.1", + "all": "grid.267310.1" + } + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json new file mode 100644 index 000000000000..91ce8d33e084 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ror-records.json @@ -0,0 +1,2383 @@ +{ + "number_of_results": 200, + "time_taken": 12, + "items": [ + { + "id": "https://ror.org/02f6dcw23", + "name": "The University of Texas", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1959, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "Audie L. Murphy Memorial VA Hospital", + "type": "Related", + "id": "https://ror.org/035xhk118" + }, + { + "label": "San Antonio Military Medical Center", + "type": "Related", + "id": "https://ror.org/00m1mwc36" + }, + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 29.508129, + "lng": -98.574025, + "state": "Texas", + "state_code": "US-TX", + "city": "San Antonio", + "geonames_city": { + "id": 4726206, + "city": "San Antonio", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Bexar County", + "id": 4674023, + "ascii_name": "Bexar County", + "code": "US.TX.029" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uthscsa.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UTHSCSA" + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_San_Antonio", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 0629 5880" + ] + }, + "FundRef": { + "preferred": "100008635", + "all": [ + "100008635", + "100008636" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "1593427" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q4005868" + ] + }, + "GRID": { + "preferred": "grid.267309.9", + "all": "grid.267309.9" + } + } + }, + { + "id": "https://ror.org/01sps7q28", + "name": "The University of Texas Health Science Center at Tyler", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1947, + "types": [ + "Healthcare" + ], + "relationships": [ + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 32.426014, + "lng": -95.212728, + "state": "Texas", + "state_code": "US-TX", + "city": "Tyler", + "geonames_city": { + "id": 4738214, + "city": "Tyler", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Smith County", + "id": 4729130, + "ascii_name": "Smith County", + "code": "US.TX.423" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler" + ], + "aliases": [ + "East Texas Tuberculosis Sanitarium", + "UT Health Northeast" + ], + "acronyms": [ + "UTHSCT" + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Health_Science_Center_at_Tyler", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9704 5790" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "3446655" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q7896437" + ] + }, + "GRID": { + "preferred": "grid.267310.1", + "all": "grid.267310.1" + } + } + }, + { + "id": "https://ror.org/05byvp690", + "name": "The University of Texas Southwestern Medical Center", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1943, + "types": [ + "Healthcare" + ], + "relationships": [ + { + "label": "Children's Medical Center", + "type": "Related", + "id": "https://ror.org/02ndk3y82" + }, + { + "label": "Parkland Memorial Hospital", + "type": "Related", + "id": "https://ror.org/0208r0146" + }, + { + "label": "VA North Texas Health Care System", + "type": "Related", + "id": "https://ror.org/01nzxq896" + }, + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + }, + { + "label": "Institute for Exercise and Environmental Medicine", + "type": "Child", + "id": "https://ror.org/03gqc7y13" + }, + { + "label": "Texas Health Dallas", + "type": "Child", + "id": "https://ror.org/05k07p323" + } + ], + "addresses": [ + { + "lat": 32.812185, + "lng": -96.840174, + "state": "Texas", + "state_code": "US-TX", + "city": "Dallas", + "geonames_city": { + "id": 4684888, + "city": "Dallas", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Dallas County", + "id": 4684904, + "ascii_name": "Dallas County", + "code": "US.TX.113" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.utsouthwestern.edu/" + ], + "aliases": [ + "UT Southwestern" + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Texas_Southwestern_Medical_Center", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9482 7121" + ] + }, + "FundRef": { + "preferred": "100007914", + "all": [ + "100007914", + "100010487", + "100008260" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "617906" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2725999" + ] + }, + "GRID": { + "preferred": "grid.267313.2", + "all": "grid.267313.2" + } + } + }, + { + "id": "https://ror.org/019kgqr73", + "name": "The University of Texas at Arlington", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1895, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "VA North Texas Health Care System", + "type": "Related", + "id": "https://ror.org/01nzxq896" + }, + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 32.731, + "lng": -97.115, + "state": "Texas", + "state_code": "US-TX", + "city": "Arlington", + "geonames_city": { + "id": 4671240, + "city": "Arlington", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Tarrant County", + "id": 4735638, + "ascii_name": "Tarrant County", + "code": "US.TX.439" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uta.edu/uta/" + ], + "aliases": [ + "UT Arlington" + ], + "acronyms": [ + "UTA" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Texas_at_Arlington", + "labels": [ + { + "label": "Université du Texas à Arlington", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2181 9515" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100009497" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "906409" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q1230739" + ] + }, + "GRID": { + "preferred": "grid.267315.4", + "all": "grid.267315.4" + } + } + }, + { + "id": "https://ror.org/051smbs96", + "name": "The University of Texas of the Permian Basin", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1973, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "The University of Texas System", + "type": "Parent", + "id": "https://ror.org/01gek1696" + } + ], + "addresses": [ + { + "lat": 31.889444, + "lng": -102.329531, + "state": "Texas", + "state_code": "US-TX", + "city": "Odessa", + "geonames_city": { + "id": 5527554, + "city": "Odessa", + "geonames_admin1": { + "name": "Texas", + "id": 4736286, + "ascii_name": "Texas", + "code": "US.TX" + }, + "geonames_admin2": { + "name": "Ector County", + "id": 5520910, + "ascii_name": "Ector County", + "code": "US.TX.135" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.utpb.edu/" + ], + "aliases": [ + "UT Permian Basin" + ], + "acronyms": [ + "UTPB" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Texas_of_the_Permian_Basin", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9140 1491" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "1419441" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2495935" + ] + }, + "GRID": { + "preferred": "grid.267328.a", + "all": "grid.267328.a" + } + } + }, + { + "id": "https://ror.org/044vy1d05", + "name": "Tokushima University", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1949, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "Tokushima University Hospital", + "type": "Related", + "id": "https://ror.org/021ph5e41" + } + ], + "addresses": [ + { + "lat": 34.07, + "lng": 134.56, + "state": null, + "state_code": null, + "city": "Tokushima", + "geonames_city": { + "id": 1850158, + "city": "Tokushima", + "geonames_admin1": { + "name": "Tokushima", + "id": 1850157, + "ascii_name": "Tokushima", + "code": "JP.39" + }, + "geonames_admin2": { + "name": "Tokushima Shi", + "id": 1850156, + "ascii_name": "Tokushima Shi", + "code": "JP.39.1850156" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 1861060 + } + ], + "links": [ + "https://www.tokushima-u.ac.jp/" + ], + "aliases": [ + "Tokushima Daigaku", + "University of Tokushima" + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Tokushima", + "labels": [ + { + "label": "徳島大学", + "iso639": "ja" + } + ], + "country": { + "country_name": "Japan", + "country_code": "JP" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 1092 3579" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "501100005623" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "15696836" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q1150231" + ] + }, + "GRID": { + "preferred": "grid.267335.6", + "all": "grid.267335.6" + } + } + }, + { + "id": "https://ror.org/03np13864", + "name": "University of Trinidad and Tobago", + "email_address": null, + "ip_addresses": [ + + ], + "established": 2004, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 10.616667, + "lng": -61.216667, + "state": null, + "state_code": null, + "city": "Arima", + "geonames_city": { + "id": 3575051, + "city": "Arima", + "geonames_admin1": { + "name": "Borough of Arima", + "id": 3575052, + "ascii_name": "Borough of Arima", + "code": "TT.01" + }, + "geonames_admin2": { + "name": null, + "id": null, + "ascii_name": null, + "code": null + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 3573591 + } + ], + "links": [ + "https://utt.edu.tt/" + ], + "aliases": [ + + ], + "acronyms": [ + "UTT" + ], + "status": "active", + "wikipedia_url": "https://en.wikipedia.org/wiki/University_of_Trinidad_and_Tobago", + "labels": [ + { + "label": "Universidad de Trinidad y Tobago", + "iso639": "es" + } + ], + "country": { + "country_name": "Trinidad and Tobago", + "country_code": "TT" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9490 0886" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "8706288" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q648244" + ] + }, + "GRID": { + "preferred": "grid.267355.0", + "all": "grid.267355.0" + } + } + }, + { + "id": "https://ror.org/04wn28048", + "name": "University of Tulsa", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1894, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 36.152222, + "lng": -95.946389, + "state": "Oklahoma", + "state_code": "US-OK", + "city": "Tulsa", + "geonames_city": { + "id": 4553433, + "city": "Tulsa", + "geonames_admin1": { + "name": "Oklahoma", + "id": 4544379, + "ascii_name": "Oklahoma", + "code": "US.OK" + }, + "geonames_admin2": { + "name": "Tulsa County", + "id": 4553440, + "ascii_name": "Tulsa County", + "code": "US.OK.143" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://utulsa.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "TU" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Tulsa", + "labels": [ + { + "label": "Université de tulsa", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2160 264X" + ] + }, + "FundRef": { + "preferred": "100007147", + "all": [ + "100007147", + "100006455" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "32043" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q1848657" + ] + }, + "GRID": { + "preferred": "grid.267360.6", + "all": "grid.267360.6" + } + } + }, + { + "id": "https://ror.org/04scfb908", + "name": "Alfred Health", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1871, + "types": [ + "Healthcare" + ], + "relationships": [ + { + "label": "Caulfield Hospital", + "type": "Child", + "id": "https://ror.org/01fcxf261" + }, + { + "label": "Melbourne Sexual Health Centre", + "type": "Child", + "id": "https://ror.org/013fdz725" + }, + { + "label": "National Trauma Research Institute", + "type": "Child", + "id": "https://ror.org/048t93218" + }, + { + "label": "The Alfred Hospital", + "type": "Child", + "id": "https://ror.org/01wddqe20" + } + ], + "addresses": [ + { + "lat": -37.845542, + "lng": 144.981632, + "state": "Victoria", + "state_code": "AU-VIC", + "city": "Melbourne", + "geonames_city": { + "id": 2158177, + "city": "Melbourne", + "geonames_admin1": { + "name": "Victoria", + "id": 2145234, + "ascii_name": "Victoria", + "code": "AU.07" + }, + "geonames_admin2": { + "name": "Melbourne", + "id": 7839805, + "ascii_name": "Melbourne", + "code": "AU.07.24600" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 2077456 + } + ], + "links": [ + "http://www.alfred.org.au/" + ], + "aliases": [ + + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "", + "labels": [ + + ], + "country": { + "country_name": "Australia", + "country_code": "AU" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0004 0432 5259" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "501100002716" + ] + }, + "GRID": { + "preferred": "grid.267362.4", + "all": "grid.267362.4" + } + } + }, + { + "id": "https://ror.org/02c2f8975", + "name": "University of Ulsan", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1970, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "Ulsan University Hospital", + "type": "Related", + "id": "https://ror.org/03sab2a45" + } + ], + "addresses": [ + { + "lat": 35.542772, + "lng": 129.256725, + "state": null, + "state_code": null, + "city": "Ulsan", + "geonames_city": { + "id": 1833747, + "city": "Ulsan", + "geonames_admin1": { + "name": "Ulsan", + "id": 1833742, + "ascii_name": "Ulsan", + "code": "KR.21" + }, + "geonames_admin2": { + "name": null, + "id": null, + "ascii_name": null, + "code": null + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 1835841 + } + ], + "links": [ + "http://en.ulsan.ac.kr/contents/main/" + ], + "aliases": [ + + ], + "acronyms": [ + "UOU" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Ulsan", + "labels": [ + { + "label": "울산대학교", + "iso639": "ko" + } + ], + "country": { + "country_name": "South Korea", + "country_code": "KR" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0004 0533 4667" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "501100002568" + ] + }, + "OrgRef": { + "preferred": "10458246", + "all": [ + "10458246", + "15162872" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q491717" + ] + }, + "GRID": { + "preferred": "grid.267370.7", + "all": "grid.267370.7" + } + } + }, + { + "id": "https://ror.org/010acrp16", + "name": "University of West Alabama", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1835, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 32.59, + "lng": -88.186, + "state": "Alabama", + "state_code": "US-AL", + "city": "Livingston", + "geonames_city": { + "id": 4073383, + "city": "Livingston", + "geonames_admin1": { + "name": "Alabama", + "id": 4829764, + "ascii_name": "Alabama", + "code": "US.AL" + }, + "geonames_admin2": { + "name": "Sumter County", + "id": 4092386, + "ascii_name": "Sumter County", + "code": "US.AL.119" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uwa.edu/" + ], + "aliases": [ + "Livingston Female Academy" + ], + "acronyms": [ + "UWA" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Alabama", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9963 9197" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "2425212" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q637346" + ] + }, + "GRID": { + "preferred": "grid.267434.0", + "all": "grid.267434.0" + } + } + }, + { + "id": "https://ror.org/002w4zy91", + "name": "University of West Florida", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1963, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "State University System of Florida", + "type": "Parent", + "id": "https://ror.org/05sqd3t97" + } + ], + "addresses": [ + { + "lat": 30.549493, + "lng": -87.21812, + "state": "Florida", + "state_code": "US-FL", + "city": "Pensacola", + "geonames_city": { + "id": 4168228, + "city": "Pensacola", + "geonames_admin1": { + "name": "Florida", + "id": 4155751, + "ascii_name": "Florida", + "code": "US.FL" + }, + "geonames_admin2": { + "name": "Escambia County", + "id": 4154550, + "ascii_name": "Escambia County", + "code": "US.FL.033" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://uwf.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UWF" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Florida", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2112 2427" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100009842" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "750756" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q659255" + ] + }, + "GRID": { + "preferred": "grid.267436.2", + "all": "grid.267436.2" + } + } + }, + { + "id": "https://ror.org/01cqxk816", + "name": "University of West Georgia", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1906, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "University System of Georgia", + "type": "Parent", + "id": "https://ror.org/017wcm924" + } + ], + "addresses": [ + { + "lat": 33.573357, + "lng": -85.099593, + "state": "Georgia", + "state_code": "US-GA", + "city": "Carrollton", + "geonames_city": { + "id": 4186416, + "city": "Carrollton", + "geonames_admin1": { + "name": "Georgia", + "id": 4197000, + "ascii_name": "Georgia", + "code": "US.GA" + }, + "geonames_admin2": { + "name": "Carroll County", + "id": 4186396, + "ascii_name": "Carroll County", + "code": "US.GA.045" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.westga.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UWG" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_West_Georgia", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2223 6696" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100007922" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "595315" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2495945" + ] + }, + "GRID": { + "preferred": "grid.267437.3", + "all": "grid.267437.3" + } + } + }, + { + "id": "https://ror.org/03c8vvr84", + "name": "University of Western States", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1904, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 45.543351, + "lng": -122.523973, + "state": "Oregon", + "state_code": "US-OR", + "city": "Portland", + "geonames_city": { + "id": 5746545, + "city": "Portland", + "geonames_admin1": { + "name": "Oregon", + "id": 5744337, + "ascii_name": "Oregon", + "code": "US.OR" + }, + "geonames_admin2": { + "name": "Multnomah County", + "id": 5742126, + "ascii_name": "Multnomah County", + "code": "US.OR.051" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uws.edu/" + ], + "aliases": [ + "Western States Chiropractic College" + ], + "acronyms": [ + "UWS" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Western_States", + "labels": [ + + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0004 0455 9493" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "1655050" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q7896612" + ] + }, + "GRID": { + "preferred": "grid.267451.3", + "all": "grid.267451.3" + } + } + }, + { + "id": "https://ror.org/03fmjzx88", + "name": "University of Winchester", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1840, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 51.060338, + "lng": -1.325418, + "state": null, + "state_code": null, + "city": "Winchester", + "geonames_city": { + "id": 2633858, + "city": "Winchester", + "geonames_admin1": { + "name": "England", + "id": 6269131, + "ascii_name": "England", + "code": "GB.ENG" + }, + "geonames_admin2": { + "name": "Hampshire", + "id": 2647554, + "ascii_name": "Hampshire", + "code": "GB.ENG.F2" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": "SOUTH EAST (ENGLAND)", + "code": "UKJ" + }, + "nuts_level2": { + "name": "Hampshire and Isle of Wight", + "code": "UKJ3" + }, + "nuts_level3": { + "name": "Central Hampshire", + "code": "UKJ36" + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 2635167 + } + ], + "links": [ + "http://www.winchester.ac.uk/pages/home.aspx" + ], + "aliases": [ + + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Winchester", + "labels": [ + + ], + "country": { + "country_name": "United Kingdom", + "country_code": "GB" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0000 9422 2878" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100010057" + ] + }, + "HESA": { + "preferred": null, + "all": [ + "0021" + ] + }, + "UCAS": { + "preferred": null, + "all": [ + "W76" + ] + }, + "UKPRN": { + "preferred": null, + "all": [ + "10003614" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "3140939" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q3551690" + ] + }, + "GRID": { + "preferred": "grid.267454.6", + "all": "grid.267454.6" + } + } + }, + { + "id": "https://ror.org/01gw3d370", + "name": "University of Windsor", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1857, + "types": [ + "Education" + ], + "relationships": [ + + ], + "addresses": [ + { + "lat": 42.305196, + "lng": -83.067483, + "state": "Ontario", + "state_code": "CA-ON", + "city": "Windsor", + "geonames_city": { + "id": 6182962, + "city": "Windsor", + "geonames_admin1": { + "name": "Ontario", + "id": 6093943, + "ascii_name": "Ontario", + "code": "CA.08" + }, + "geonames_admin2": { + "name": null, + "id": null, + "ascii_name": null, + "code": null + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6251999 + } + ], + "links": [ + "http://www.uwindsor.ca/" + ], + "aliases": [ + "UWindsor", + "Assumption University of Windsor" + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Windsor", + "labels": [ + { + "label": "Université de windsor", + "iso639": "fr" + } + ], + "country": { + "country_name": "Canada", + "country_code": "CA" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0004 1936 9596" + ] + }, + "FundRef": { + "preferred": "100009154", + "all": [ + "100009154", + "501100000083" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "342733" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2065769" + ] + }, + "GRID": { + "preferred": "grid.267455.7", + "all": "grid.267455.7" + } + } + }, + { + "id": "https://ror.org/02gdzyx04", + "name": "University of Winnipeg", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1871, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "Winnipeg Institute for Theoretical Physics", + "type": "Child", + "id": "https://ror.org/010tw2j24" + } + ], + "addresses": [ + { + "lat": 49.890122, + "lng": -97.153367, + "state": "Manitoba", + "state_code": "CA-MB", + "city": "Winnipeg", + "geonames_city": { + "id": 6183235, + "city": "Winnipeg", + "geonames_admin1": { + "name": "Manitoba", + "id": 6065171, + "ascii_name": "Manitoba", + "code": "CA.03" + }, + "geonames_admin2": { + "name": null, + "id": null, + "ascii_name": null, + "code": null + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6251999 + } + ], + "links": [ + "http://www.uwinnipeg.ca/" + ], + "aliases": [ + + ], + "acronyms": [ + + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Winnipeg", + "labels": [ + { + "label": "Université de winnipeg", + "iso639": "fr" + } + ], + "country": { + "country_name": "Canada", + "country_code": "CA" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 1703 4731" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100009367" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "587404" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q472167" + ] + }, + "GRID": { + "preferred": "grid.267457.5", + "all": "grid.267457.5" + } + } + }, + { + "id": "https://ror.org/03mnm0t94", + "name": "University of Wisconsin–Eau Claire", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1916, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "University of Wisconsin System", + "type": "Parent", + "id": "https://ror.org/03ydkyb10" + } + ], + "addresses": [ + { + "lat": 44.79895, + "lng": -91.499346, + "state": "Wisconsin", + "state_code": "US-WI", + "city": "Eau Claire", + "geonames_city": { + "id": 5251436, + "city": "Eau Claire", + "geonames_admin1": { + "name": "Wisconsin", + "id": 5279468, + "ascii_name": "Wisconsin", + "code": "US.WI" + }, + "geonames_admin2": { + "name": "Eau Claire County", + "id": 5251439, + "ascii_name": "Eau Claire County", + "code": "US.WI.035" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uwec.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UWEC" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93Eau_Claire", + "labels": [ + { + "label": "Université du Wisconsin à Eau Claire", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2227 2494" + ] + }, + "FundRef": { + "preferred": null, + "all": [ + "100010315" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "496729" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q3551771" + ] + }, + "GRID": { + "preferred": "grid.267460.1", + "all": "grid.267460.1" + } + } + }, + { + "id": "https://ror.org/05hbexn54", + "name": "University of Wisconsin–Green Bay", + "email_address": null, + "ip_addresses": [ + + ], + "established": 1965, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "University of Wisconsin System", + "type": "Parent", + "id": "https://ror.org/03ydkyb10" + } + ], + "addresses": [ + { + "lat": 44.533203, + "lng": -87.921521, + "state": "Wisconsin", + "state_code": "US-WI", + "city": "Green Bay", + "geonames_city": { + "id": 5254962, + "city": "Green Bay", + "geonames_admin1": { + "name": "Wisconsin", + "id": 5279468, + "ascii_name": "Wisconsin", + "code": "US.WI" + }, + "geonames_admin2": { + "name": "Brown County", + "id": 5246898, + "ascii_name": "Brown County", + "code": "US.WI.009" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uwgb.edu/" + ], + "aliases": [ + + ], + "acronyms": [ + "UWGB" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93Green_Bay", + "labels": [ + { + "label": "Université du Wisconsin–Green Bay", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 0559 7692" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "1513886" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2378091" + ] + }, + "GRID": { + "preferred": "grid.267461.0", + "all": "grid.267461.0" + } + } + }, + { + "id": "https://ror.org/00x8ccz20", + "name": "University of Wisconsin–La Crosse", + "email_address": "", + "ip_addresses": [ + + ], + "established": 1909, + "types": [ + "Education" + ], + "relationships": [ + { + "label": "University of Wisconsin System", + "type": "Parent", + "id": "https://ror.org/03ydkyb10" + } + ], + "addresses": [ + { + "lat": 43.815576, + "lng": -91.233517, + "state": "Wisconsin", + "state_code": "US-WI", + "city": "La Crosse", + "geonames_city": { + "id": 5258957, + "city": "La Crosse", + "geonames_admin1": { + "name": "Wisconsin", + "id": 5279468, + "ascii_name": "Wisconsin", + "code": "US.WI" + }, + "geonames_admin2": { + "name": "La Crosse County", + "id": 5258961, + "ascii_name": "La Crosse County", + "code": "US.WI.063" + }, + "license": { + "attribution": "Data from geonames.org under a CC-BY 3.0 license", + "license": "http://creativecommons.org/licenses/by/3.0/" + }, + "nuts_level1": { + "name": null, + "code": null + }, + "nuts_level2": { + "name": null, + "code": null + }, + "nuts_level3": { + "name": null, + "code": null + } + }, + "postcode": null, + "primary": false, + "line": null, + "country_geonames_id": 6252001 + } + ], + "links": [ + "http://www.uwlax.edu/Home/Future-Students/" + ], + "aliases": [ + + ], + "acronyms": [ + "UW–L" + ], + "status": "active", + "wikipedia_url": "http://en.wikipedia.org/wiki/University_of_Wisconsin%E2%80%93La_Crosse", + "labels": [ + { + "label": "Université du Wisconsin–La Crosse", + "iso639": "fr" + } + ], + "country": { + "country_name": "United States", + "country_code": "US" + }, + "external_ids": { + "ISNI": { + "preferred": null, + "all": [ + "0000 0001 2169 5137" + ] + }, + "OrgRef": { + "preferred": null, + "all": [ + "2422287" + ] + }, + "Wikidata": { + "preferred": null, + "all": [ + "Q2688358" + ] + }, + "GRID": { + "preferred": "grid.267462.3", + "all": "grid.267462.3" + } + } + } + ], + "meta": { + "types": [ + { + "id": "company", + "title": "Company", + "count": 29790 + }, + { + "id": "education", + "title": "Education", + "count": 20325 + }, + { + "id": "nonprofit", + "title": "Nonprofit", + "count": 14187 + }, + { + "id": "healthcare", + "title": "Healthcare", + "count": 13107 + }, + { + "id": "facility", + "title": "Facility", + "count": 10080 + }, + { + "id": "other", + "title": "Other", + "count": 8369 + }, + { + "id": "government", + "title": "Government", + "count": 6511 + }, + { + "id": "archive", + "title": "Archive", + "count": 2967 + } + ], + "countries": [ + { + "id": "us", + "title": "United States", + "count": 31196 + }, + { + "id": "gb", + "title": "United Kingdom", + "count": 7410 + }, + { + "id": "de", + "title": "Germany", + "count": 5189 + }, + { + "id": "cn", + "title": "China", + "count": 4846 + }, + { + "id": "fr", + "title": "France", + "count": 4344 + }, + { + "id": "jp", + "title": "Japan", + "count": 3940 + }, + { + "id": "ca", + "title": "Canada", + "count": 3392 + }, + { + "id": "in", + "title": "India", + "count": 3075 + }, + { + "id": "cz", + "title": "Czech Republic", + "count": 2780 + }, + { + "id": "ru", + "title": "Russia", + "count": 2109 + } + ], + "statuses": [ + { + "id": "active", + "title": "active", + "count": 105336 + } + ] + } +} \ No newline at end of file diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d38ffd64339a..0d293ac09786 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1663,3 +1663,4 @@ include = ${module_dir}/usage-statistics.cfg include = ${module_dir}/versioning.cfg include = ${module_dir}/workflow.cfg include = ${module_dir}/external-providers.cfg +include = ${module_dir}/ror.cfg diff --git a/dspace/config/modules/ror.cfg b/dspace/config/modules/ror.cfg new file mode 100644 index 000000000000..4a7a6cfa4fac --- /dev/null +++ b/dspace/config/modules/ror.cfg @@ -0,0 +1,2 @@ +ror.orgunit-import.api-url = https://api.ror.org/organizations +ror.authority.prefix = will be referenced::ROR-ID:: \ No newline at end of file diff --git a/dspace/config/registries/openaire-cerif-types.xml b/dspace/config/registries/openaire-cerif-types.xml new file mode 100644 index 000000000000..07f6da4313ed --- /dev/null +++ b/dspace/config/registries/openaire-cerif-types.xml @@ -0,0 +1,27 @@ + + + + + + OpenAIRE Types + + + + oairecerif + https://www.openaire.eu/cerif-profile/1.1/ + + + + oairecerif + identifier + url + + + + + oairecerif + acronym + + + + \ No newline at end of file diff --git a/dspace/config/registries/schema-organization-types.xml b/dspace/config/registries/schema-organization-types.xml index 91ee203ae9b7..a9739796e68b 100644 --- a/dspace/config/registries/schema-organization-types.xml +++ b/dspace/config/registries/schema-organization-types.xml @@ -80,6 +80,12 @@ crossrefid Crossref identifier + + + organization + parentOrganization + + diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml index 6d7d50c39f1b..831a3872397c 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -93,7 +93,7 @@ - + @@ -199,6 +199,18 @@ + + + + + + + + + OrgUnit + + + diff --git a/dspace/config/spring/api/ror-integration.xml b/dspace/config/spring/api/ror-integration.xml new file mode 100644 index 000000000000..65de4997e00e --- /dev/null +++ b/dspace/config/spring/api/ror-integration.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml index 39a4778356c0..241acc6d3b6a 100644 --- a/dspace/config/submission-forms.xml +++ b/dspace/config/submission-forms.xml @@ -531,18 +531,18 @@ - isProjectOfOrgUnit + isOrgUnitOfProject orgunit false Enter the organization's name - project - funder - name + dc + contributor + other onebox - + ror From 91db1c07e3da864cb31653ee4b2849db058a9a1f Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Tue, 19 Dec 2023 17:38:48 +0100 Subject: [PATCH 038/118] [CST-12826] ROR integration for OAI-PMH --- .../oai/metadataFormats/oai_openaire.xsl | 7 +++++++ .../spring/api/virtual-metadata.xml.openaire4 | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 16c63c9c1a13..7e9ff074ca99 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -457,6 +457,13 @@ + + + + + + + diff --git a/dspace/config/spring/api/virtual-metadata.xml.openaire4 b/dspace/config/spring/api/virtual-metadata.xml.openaire4 index bbdae0e75927..9d0a662550f8 100644 --- a/dspace/config/spring/api/virtual-metadata.xml.openaire4 +++ b/dspace/config/spring/api/virtual-metadata.xml.openaire4 @@ -167,6 +167,7 @@ + @@ -201,15 +202,21 @@ --> - + - + + + + + + + ${basedir}/../../.. @@ -67,6 +68,31 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack + prepare-package + + unpack + + + + + org.dspace + dspace-server-webapp + ${dspace.version} + jar + **/static/**,**/*.properties + ${project.build.directory}/additions + + + + + + ${basedir}/../../.. From 6ea529325078e92733680907ae36638250c1c34f Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 28 Dec 2023 16:22:48 +0100 Subject: [PATCH 047/118] [DSC-963] Fixes testResources generation --- dspace-server-webapp/pom.xml | 49 +++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 05824e93fda4..5671a2c79b03 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -50,18 +50,43 @@ maven-resources-plugin - - - - ${basedir}/src/main/resources - - **/*.properties - **/static/** - - true - - - + + + testEnvironment + process-resources + + testResources + + + + + ${basedir}/src/test/resources + true + + + + + + webappFiltering + process-resources + + resources + + + + + ${basedir}/src/main/resources + + **/*.properties + **/static/** + **/spring/** + + true + + + + + org.apache.maven.plugins From 21b8c1b6a12cc7db056e0104e188bfa244e72b97 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Thu, 28 Dec 2023 17:09:57 +0100 Subject: [PATCH 048/118] [DSC-963] Fixes server unpacking --- dspace/modules/server/pom.xml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/dspace/modules/server/pom.xml b/dspace/modules/server/pom.xml index a0d1411b6974..c707028add5d 100644 --- a/dspace/modules/server/pom.xml +++ b/dspace/modules/server/pom.xml @@ -76,19 +76,14 @@ unpack prepare-package - unpack + unpack-dependencies - - - org.dspace - dspace-server-webapp - ${dspace.version} - jar - **/static/**,**/*.properties - ${project.build.directory}/additions - - + runtime + org.dspace + dspace-server-webapp + **/static/**,**/*.properties + ${project.build.directory}/additions From bb0693f3c607a58de86ed8e696030a78f9243c63 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Fri, 29 Dec 2023 09:31:20 +0100 Subject: [PATCH 049/118] [DSC-963] Fixes resource copy and filtering --- dspace-server-webapp/pom.xml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 5671a2c79b03..d5de9611d97c 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -61,7 +61,6 @@ ${basedir}/src/test/resources - true @@ -78,10 +77,15 @@ ${basedir}/src/main/resources **/*.properties + + true + + + ${basedir}/src/main/resources + **/static/** **/spring/** - true From e4da12ed2daa0933a46757d5a00161ee3a445c09 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Fri, 29 Dec 2023 14:57:09 +0100 Subject: [PATCH 050/118] Alteration to index-discovery script to only (re-)index specific type of IndexableObject Not compatible with `-b` option since this clears entire index first (& expect to rebuild it in its entirety) Compatible with `-f` to force reindex specific type of IndexableObject --- .../java/org/dspace/discovery/IndexClient.java | 16 ++++++++++++++-- .../org/dspace/discovery/IndexClientOptions.java | 11 +++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java index 661c48d91cfc..a01d43ce8e12 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java @@ -7,6 +7,8 @@ */ package org.dspace.discovery; +import static org.dspace.discovery.IndexClientOptions.TYPE_OPTION; + import java.io.IOException; import java.sql.SQLException; import java.util.Iterator; @@ -15,6 +17,7 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; @@ -51,6 +54,11 @@ public void internalRun() throws Exception { return; } + String type = null; + if (commandLine.hasOption(TYPE_OPTION)) { + type = commandLine.getOptionValue(TYPE_OPTION); + } + /** Acquire from dspace-services in future */ /** * new DSpace.getServiceManager().getServiceByName("org.dspace.discovery.SolrIndexer"); @@ -113,6 +121,10 @@ public void internalRun() throws Exception { } else if (indexClientOptions == IndexClientOptions.BUILD || indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) { handler.logInfo("(Re)building index from scratch."); + if (StringUtils.isNotBlank(type)) { + handler.logWarning(String.format("Type option, %s, not applicable for entire index rebuild option, b" + + ", type will be ignored", TYPE_OPTION)); + } indexer.deleteIndex(); indexer.createIndex(context); if (indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) { @@ -133,14 +145,14 @@ public void internalRun() throws Exception { } else if (indexClientOptions == IndexClientOptions.UPDATE || indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) { handler.logInfo("Updating Index"); - indexer.updateIndex(context, false); + indexer.updateIndex(context, false, type); if (indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) { checkRebuildSpellCheck(commandLine, indexer); } } else if (indexClientOptions == IndexClientOptions.FORCEUPDATE || indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) { handler.logInfo("Updating Index"); - indexer.updateIndex(context, true); + indexer.updateIndex(context, true, type); if (indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) { checkRebuildSpellCheck(commandLine, indexer); } diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexClientOptions.java b/dspace-api/src/main/java/org/dspace/discovery/IndexClientOptions.java index 74d9ba0c3a56..0de5b22d0655 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexClientOptions.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexClientOptions.java @@ -8,8 +8,13 @@ package org.dspace.discovery; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; +import org.dspace.discovery.indexobject.factory.IndexObjectFactoryFactory; /** * This Enum holds all the possible options and combinations for the Index discovery script @@ -29,6 +34,8 @@ public enum IndexClientOptions { FORCEUPDATEANDSPELLCHECK, HELP; + public static final String TYPE_OPTION = "t"; + /** * This method resolves the CommandLine parameters to figure out which action the index-discovery script should * perform @@ -71,11 +78,15 @@ protected static IndexClientOptions getIndexClientOption(CommandLine commandLine protected static Options constructOptions() { Options options = new Options(); + List indexableObjectTypes = IndexObjectFactoryFactory.getInstance().getIndexFactories().stream() + .map((indexFactory -> indexFactory.getType())).collect(Collectors.toList()); options .addOption("r", "remove", true, "remove an Item, Collection or Community from index based on its handle"); options.addOption("i", "index", true, "add or update an Item, Collection or Community based on its handle or uuid"); + options.addOption(TYPE_OPTION, "type", true, "reindex only specific type of " + + "(re)indexable objects; options: " + Arrays.toString(indexableObjectTypes.toArray())); options.addOption("c", "clean", false, "clean existing index removing any documents that no longer exist in the db"); options.addOption("d", "delete", false, From 5c72d2fa653b24a00fb8153bf834d15bdecb0faa Mon Sep 17 00:00:00 2001 From: Agustina Martinez Date: Tue, 12 Dec 2023 18:40:51 +0000 Subject: [PATCH 051/118] Support for RIOXX v3 OAI profile --- .../main/java/org/dspace/xoai/app/XOAI.java | 10 + .../xoai/filter/ItemsWithBitstreamFilter.java | 57 + .../java/org/dspace/xoai/util/ItemUtils.java | 78 +- .../xoai/tests/stylesheets/RioxxXslTest.java | 35 + .../src/test/resources/rioxx-test-invalid.xml | 89 ++ .../src/test/resources/rioxx-test-valid.xml | 92 ++ .../src/test/resources/xoai-rioxx-test.xml | 217 +++ .../oai/metadataFormats/oai_openaire.xsl | 27 +- .../crosswalks/oai/metadataFormats/rioxx.xsl | 1236 +++++++++++++++++ .../crosswalks/oai/transformers/rioxx.xsl | 361 +++++ dspace/config/crosswalks/oai/xoai.xml | 128 +- .../config/entities/rioxx3-relationships.xml | 91 ++ .../config/registries/dublin-core-types.xml | 1 + .../spring/api/virtual-metadata.xml.rioxx3 | 402 ++++++ dspace/solr/oai/conf/schema.xml | 4 +- 15 files changed, 2810 insertions(+), 18 deletions(-) create mode 100644 dspace-oai/src/main/java/org/dspace/xoai/filter/ItemsWithBitstreamFilter.java create mode 100644 dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/RioxxXslTest.java create mode 100644 dspace-oai/src/test/resources/rioxx-test-invalid.xml create mode 100644 dspace-oai/src/test/resources/rioxx-test-valid.xml create mode 100644 dspace-oai/src/test/resources/xoai-rioxx-test.xml create mode 100644 dspace/config/crosswalks/oai/metadataFormats/rioxx.xsl create mode 100644 dspace/config/crosswalks/oai/transformers/rioxx.xsl create mode 100644 dspace/config/entities/rioxx3-relationships.xml create mode 100644 dspace/config/spring/api/virtual-metadata.xml.rioxx3 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 4f842b8e944c..25cc1ee3655f 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 @@ -450,6 +450,16 @@ private SolrInputDocument index(Item item) doc.addField("item.communities", "com_" + com.getHandle().replace("/", "_")); } + boolean hasBitstream = false; + + for (Bundle b : item.getBundles("ORIGINAL")) { + if (b.getBitstreams().size() > 0) { + hasBitstream = true; + } + } + + doc.addField("item.hasbitstream", hasBitstream); + List allData = itemService.getMetadata(item, Item.ANY, Item.ANY, Item.ANY, Item.ANY); for (MetadataValue dc : allData) { MetadataField field = dc.getMetadataField(); diff --git a/dspace-oai/src/main/java/org/dspace/xoai/filter/ItemsWithBitstreamFilter.java b/dspace-oai/src/main/java/org/dspace/xoai/filter/ItemsWithBitstreamFilter.java new file mode 100644 index 000000000000..3599c5b9e168 --- /dev/null +++ b/dspace-oai/src/main/java/org/dspace/xoai/filter/ItemsWithBitstreamFilter.java @@ -0,0 +1,57 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.xoai.filter; + +import java.sql.SQLException; + +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; +import org.dspace.content.Bundle; +import org.dspace.content.Item; +import org.dspace.handle.factory.HandleServiceFactory; +import org.dspace.handle.service.HandleService; +import org.dspace.xoai.data.DSpaceItem; +import org.dspace.xoai.filter.results.SolrFilterResult; + + +/** + * Created by Philip Vissenaekens (philip at atmire dot com) + * Date: 21/04/15 + * Time: 15:18 + */ +public class ItemsWithBitstreamFilter extends DSpaceFilter { + + private static Logger log = LogManager.getLogger(ItemsWithBitstreamFilter.class); + + private static final HandleService handleService + = HandleServiceFactory.getInstance().getHandleService(); + + @Override + public SolrFilterResult buildSolrQuery() { + return new SolrFilterResult("item.hasbitstream:true"); + } + + @Override + public boolean isShown(DSpaceItem item) { + try { + String handle = DSpaceItem.parseHandle(item.getIdentifier()); + if (handle == null) { + return false; + } + Item dspaceItem = (Item) handleService.resolveToObject(context, handle); + for (Bundle b : dspaceItem.getBundles("ORIGINAL")) { + if (b.getBitstreams().size() > 0) { + return true; + } + } + } catch (SQLException e) { + log.error(e.getMessage(), e); + } + return false; + } +} diff --git a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java index 938cf0d64a5b..20dcabcb20c8 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/util/ItemUtils.java @@ -11,6 +11,8 @@ import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; import com.lyncode.xoai.dataprovider.xml.xoai.Element; @@ -21,6 +23,7 @@ import org.dspace.app.util.factory.UtilServiceFactory; import org.dspace.app.util.service.MetadataExposureService; import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Bitstream; @@ -114,23 +117,21 @@ private static Element createBundlesElement(Context context, Item item) throws S log.error("Null bitstream found, check item uuid: " + item.getID()); break; } + boolean primary = false; + // Check if current bitstream is in original bundle + 1 of the 2 following + // Bitstream = primary bitstream in bundle -> true + // No primary bitstream found in bundle-> only the first one gets flagged as "primary" + if (b.getName() != null && b.getName().equals("ORIGINAL") && (b.getPrimaryBitstream() != null + && b.getPrimaryBitstream().getID() == bit.getID() + || b.getPrimaryBitstream() == null && bit.getID() == bits.get(0).getID())) { + primary = true; + } + Element bitstream = create("bitstream"); bitstreams.getElement().add(bitstream); - String url = ""; - String bsName = bit.getName(); - String sid = String.valueOf(bit.getSequenceID()); + String baseUrl = configurationService.getProperty("oai.bitstream.baseUrl"); - String handle = null; - // get handle of parent Item of this bitstream, if there - // is one: - List bn = bit.getBundles(); - if (!bn.isEmpty()) { - List bi = bn.get(0).getItems(); - if (!bi.isEmpty()) { - handle = bi.get(0).getHandle(); - } - } - url = baseUrl + "/bitstreams/" + bit.getID().toString() + "/download"; + String url = baseUrl + "/bitstreams/" + bit.getID().toString() + "/download"; String cks = bit.getChecksum(); String cka = bit.getChecksumAlgorithm(); @@ -147,18 +148,65 @@ private static Element createBundlesElement(Context context, Item item) throws S if (description != null) { bitstream.getField().add(createValue("description", description)); } + // Add bitstream embargo information (READ policy present, for Anonymous group with a start date) + addResourcePolicyInformation(context, bit, bitstream); + bitstream.getField().add(createValue("format", bit.getFormat(context).getMIMEType())); bitstream.getField().add(createValue("size", "" + bit.getSizeBytes())); bitstream.getField().add(createValue("url", url)); bitstream.getField().add(createValue("checksum", cks)); bitstream.getField().add(createValue("checksumAlgorithm", cka)); bitstream.getField().add(createValue("sid", bit.getSequenceID() + "")); + // Add primary bitstream field to allow locating easily the primary bitstream information + bitstream.getField().add(createValue("primary", primary + "")); } } return bundles; } + /** + * This method will add metadata information about associated resource policies for a give bitstream. + * It will parse of relevant policies and add metadata information + * @param context + * @param bitstream the bitstream object + * @param bitstreamEl the bitstream metadata object to add resource policy information to + * @throws SQLException + */ + private static void addResourcePolicyInformation(Context context, Bitstream bitstream, Element bitstreamEl) + throws SQLException { + // Pre-filter access policies by DSO (bitstream) and Action (READ) + List policies = authorizeService.getPoliciesActionFilter(context, bitstream, Constants.READ); + + // Create resourcePolicies container + Element resourcePolicies = create("resourcePolicies"); + + for (ResourcePolicy policy : policies) { + String groupName = policy.getGroup() != null ? policy.getGroup().getName() : null; + String user = policy.getEPerson() != null ? policy.getEPerson().getName() : null; + String action = Constants.actionText[policy.getAction()]; + Date startDate = policy.getStartDate(); + Date endDate = policy.getEndDate(); + + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + + Element resourcePolicyEl = create("resourcePolicy"); + resourcePolicyEl.getField().add(createValue("group", groupName)); + resourcePolicyEl.getField().add(createValue("user", user)); + resourcePolicyEl.getField().add(createValue("action", action)); + if (startDate != null) { + resourcePolicyEl.getField().add(createValue("start-date", formatter.format(startDate))); + } + if (endDate != null) { + resourcePolicyEl.getField().add(createValue("end-date", formatter.format(endDate))); + } + // Add resourcePolicy to list of resourcePolicies + resourcePolicies.getElement().add(resourcePolicyEl); + } + // Add list of resource policies to the corresponding Bitstream XML Element + bitstreamEl.getElement().add(resourcePolicies); + } + private static Element createLicenseElement(Context context, Item item) throws SQLException, AuthorizeException, IOException { Element license = create("license"); @@ -178,7 +226,7 @@ private static Element createLicenseElement(Context context, Item item) license.getField().add(createValue("bin", Base64Utils.encode(out.toString()))); } else { log.info("Missing READ rights for license bitstream. Did not include license bitstream for item: " - + item.getID() + "."); + + item.getID() + "."); } } } diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/RioxxXslTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/RioxxXslTest.java new file mode 100644 index 000000000000..74dfaf2902c4 --- /dev/null +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/RioxxXslTest.java @@ -0,0 +1,35 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.xoai.tests.stylesheets; + +import static org.dspace.xoai.tests.support.XmlMatcherBuilder.xml; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; + +import org.dspace.xoai.tests.support.XmlMatcherBuilder; +import org.junit.Test; + +public class RioxxXslTest extends AbstractXSLTest { + @Test + public void rioxxCanTransformInput() throws Exception { + String result = apply("rioxx.xsl").to(resource("xoai-rioxx-test.xml")); + + assertThat(result, is(rioxx().withXPath("//dc:title", equalTo("The Intercorrelation Between " + + "Executive Function, Physics Problem Solving, Mathematical, and Matrix Reasoning Skills: " + + "Reflections from a Small-Scale Experiment")))); + } + + private XmlMatcherBuilder rioxx() { + return xml() + .withNamespace("rioxx", "http://www.rioxx.net/schema/v3.0/rioxx/") + .withNamespace("rioxxterms", "http://docs.rioxx.net/schema/v3.0/rioxxterms/") + .withNamespace("dcterms", "http://purl.org/dc/terms/") + .withNamespace("dc", "http://purl.org/dc/elements/1.1/"); + } +} diff --git a/dspace-oai/src/test/resources/rioxx-test-invalid.xml b/dspace-oai/src/test/resources/rioxx-test-invalid.xml new file mode 100644 index 000000000000..c8daf1a28d02 --- /dev/null +++ b/dspace-oai/src/test/resources/rioxx-test-invalid.xml @@ -0,0 +1,89 @@ + + + Data on Secchi disc depth (the depth at which a standard white disc lowered into the water just becomes invisible to a surface observer) show that water clarity in the North Sea declined during the 20th century, with likely consequences for marine primary production. However, the causes of this trend remain unknown. Here we analyse the hypothesis that changes in the North Sea's wave climate were largely responsible by causing an increase in the concentrations of suspended particulate matter (SPM) in the water column through the resuspension of seabed sediments. First, we analysed the broad-scale statistical relationships between SPM and bed shear stress due to waves and tides. We used hindcasts of wave and current data to construct a space–time dataset of bed shear stress between 1997 and 2017 across the northwest European Continental Shelf and compared the results with satellite-derived SPM concentrations. Bed shear stress was found to drive most of the inter-annual variation in SPM in the hydrographically mixed waters of the central and southern North Sea. We then used a long-term wave reanalysis to construct a time series of bed shear stress from 1900 to 2010. This shows that bed shear stress increased significantly across much of the shelf during this period, with increases of over 20 % in the southeastern North Sea. An increase in bed shear stress of this magnitude would have resulted in a large reduction in water clarity. Wave-driven processes are rarely included in projections of climate change impacts on marine ecosystems, but our analysis indicates that this should be reconsidered for shelf sea regions. + + en + + + European Geosciences Union + https://isni.org/isni/0000000110927289 + + + 1812-0792 + + Increasing turbidity in the North Sea during the 20th century due to changing wave climate + + 2019-10-02 + + + Wilson, Robert J. + https://orcid.org/0000-0002-0592-366X + + + + Heath, Michael R. + https://orcid.org/0000-0001-6602-3107 + https://viaf.org/viaf/15147423189944882613 + + + 2019-12-09 + + 2019-10-15 + + https://purl.org/coar/resource_type/c_2df8fbb1 + + + DP190101507 + + + + 61387 + + + + https://strathprints.strath.ac.uk/70117/7/Wilson_Heath_OS2019_Increasing_turbidity_in_the_North_Sea_during_the_20th_century.pdf + + + + + https://doi.org/10.1007/s11229-020-02724-x + + + + + https://doi.org/10.15129/5d28213e-8f9f-402a-b550-fc588518cb8b + + + + + https://doi.org/10.5281/zenodo.3478185 + + diff --git a/dspace-oai/src/test/resources/rioxx-test-valid.xml b/dspace-oai/src/test/resources/rioxx-test-valid.xml new file mode 100644 index 000000000000..74ffd43eb657 --- /dev/null +++ b/dspace-oai/src/test/resources/rioxx-test-valid.xml @@ -0,0 +1,92 @@ + + + + Data on Secchi disc depth (the depth at which a standard white disc lowered into the water just becomes invisible to a surface observer) show that water clarity in the North Sea declined during the 20th century, with likely consequences for marine primary production. However, the causes of this trend remain unknown. Here we analyse the hypothesis that changes in the North Sea's wave climate were largely responsible by causing an increase in the concentrations of suspended particulate matter (SPM) in the water column through the resuspension of seabed sediments. First, we analysed the broad-scale statistical relationships between SPM and bed shear stress due to waves and tides. We used hindcasts of wave and current data to construct a space–time dataset of bed shear stress between 1997 and 2017 across the northwest European Continental Shelf and compared the results with satellite-derived SPM concentrations. Bed shear stress was found to drive most of the inter-annual variation in SPM in the hydrographically mixed waters of the central and southern North Sea. We then used a long-term wave reanalysis to construct a time series of bed shear stress from 1900 to 2010. This shows that bed shear stress increased significantly across much of the shelf during this period, with increases of over 20 % in the southeastern North Sea. An increase in bed shear stress of this magnitude would have resulted in a large reduction in water clarity. Wave-driven processes are rarely included in projections of climate change impacts on marine ecosystems, but our analysis indicates that this should be reconsidered for shelf sea regions. + + en + + + European Geosciences Union + https://isni.org/isni/0000000110927289 + + + 1812-0792 + + Increasing turbidity in the North Sea during the 20th century due to changing wave climate + + 2019-10-02 + + + Wilson, Robert J. + https://orcid.org/0000-0002-0592-366X + + + + Heath, Michael R. + https://orcid.org/0000-0001-6602-3107 + https://viaf.org/viaf/15147423189944882613 + + + 2019-12-09 + + 2019-10-15 + + https://purl.org/coar/resource_type/c_2df8fbb1 + + + DP190101507 + + + + 61387 + + + https://strathprints.strath.ac.uk/70117/ + + + https://strathprints.strath.ac.uk/70117/7/Wilson_Heath_OS2019_Increasing_turbidity_in_the_North_Sea_during_the_20th_century.pdf + + + + + https://doi.org/10.1007/s11229-020-02724-x + + + + + https://doi.org/10.15129/5d28213e-8f9f-402a-b550-fc588518cb8b + + + + + https://doi.org/10.5281/zenodo.3478185 + + diff --git a/dspace-oai/src/test/resources/xoai-rioxx-test.xml b/dspace-oai/src/test/resources/xoai-rioxx-test.xml new file mode 100644 index 000000000000..33c2c3d1011a --- /dev/null +++ b/dspace-oai/src/test/resources/xoai-rioxx-test.xml @@ -0,0 +1,217 @@ + + + + + + + + Publication + + + + + + + + 2023-11-07 + + + + + + + + Tsigaridis, Konstantinos G. + virtual::44 + -1 + Wang, Rui + virtual::46 + -1 + Ellefson, Michelle R. + virtual::47 + -1 + + + + + + + 2023-11-07T11:34:10Z + + + + + 2023-11-07T11:34:10Z + + + + + 2022-11-30 + + + + + + + https://example.org/handle/1811/160 + + + + + + + eng + + + + + + The Intercorrelation Between Executive Function, Physics Problem Solving, Mathematical, and Matrix Reasoning Skills: Reflections from a Small-Scale Experiment + + + + + Article + + + + + + + a57363fa-f82e-4684-bd76-f7bc1e893603 + virtual::44 + -1 + e00b3d0d-65e2-4c30-825d-1a4839845790 + virtual::46 + -1 + bdd38a03-206d-4f9b-bafb-70e060ad176f + virtual::47 + -1 + + + + a57363fa-f82e-4684-bd76-f7bc1e893603 + virtual::44 + -1 + e00b3d0d-65e2-4c30-825d-1a4839845790 + virtual::46 + -1 + bdd38a03-206d-4f9b-bafb-70e060ad176f + virtual::47 + -1 + + + + + + 05a400b1-ff0b-4e40-80cd-a7d1b712ace2 + virtual::71 + -1 + + + + + 7524a0cf-3ea2-40c7-a265-d583425ed4d7 + virtual::71 + -1 + + + + 7524a0cf-3ea2-40c7-a265-d583425ed4d7 + virtual::71 + -1 + + + + + + + + + 0000-0003-0407-9767 + virtual::47 + -1 + + + + + + + + 2634-9876 + virtual::71 + -1 + + + + + + ORIGINAL + + + Tsigaridis et al., 2022.pdf + application/pdf + 1554917 + https://example.org/bitstreams/9121e795-0af3-4ff3-be2a-4b28418fb269/download + 42d8cd076931e43e02d0af70a36d704e + MD5 + 1 + true + + + Anonymous + Anonymous + READ + + + + + + + THUMBNAIL + + + cerj_volume_9_thumbnail.jpg + image/jpeg + 14513 + https://example.org/bitstreams/16245937-10bb-46db-9817-683a5ebd8d63/download + 8c39d691daa8e5f9d668668db7910cd6 + MD5 + 2 + false + + + Anonymous + Anonymous + READ + + + + + + + + 1811/160 + oai:example.org:1811/160 + 2023-12-13 13:07:56.51 + + open.access + + + + https://example.org + Diamond DSpace (dev) + support@example.org + + + diff --git a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl index 16c63c9c1a13..c96305db1639 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/oai_openaire.xsl @@ -883,7 +883,32 @@ - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/crosswalks/oai/metadataFormats/rioxx.xsl b/dspace/config/crosswalks/oai/metadataFormats/rioxx.xsl new file mode 100644 index 000000000000..db67ae91fe69 --- /dev/null +++ b/dspace/config/crosswalks/oai/metadataFormats/rioxx.xsl @@ -0,0 +1,1236 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://purl.org/coar/version/c_ab4af688f83e57aa + + + http://purl.org/coar/resource_type/c_1162 + + + http://purl.org/coar/version/c_b1a7d7d4d402bcce + + + http://purl.org/coar/version/c_e19f295774971610 + + + http://purl.org/coar/version/c_fa2ee174bc00049f + + + http://purl.org/coar/version/c_dc82b40f9837b551 + + + http://purl.org/coar/version/c_71e4c1898caa6e32 + + + http://purl.org/coar/version/c_be7fb7dd8ff6fe43 + + + + + + + + + + + + + + + http://purl.org/coar/resource_type/c_1162 + + + http://purl.org/coar/resource_type/c_0640 + + + http://purl.org/coar/resource_type/c_6501 + + + http://purl.org/coar/resource_type/c_6501 + + + http://purl.org/coar/resource_type/c_b239 + + + http://purl.org/coar/resource_type/c_7a1f + + + http://purl.org/coar/resource_type/c_86bc + + + http://purl.org/coar/resource_type/c_2f33 + + + http://purl.org/coar/resource_type/c_3248 + + + http://purl.org/coar/resource_type/c_ba08 + + + http://purl.org/coar/resource_type/c_7ad9 + + + http://purl.org/coar/resource_type/c_e9a0 + + + http://purl.org/coar/resource_type/c_f744 + + + http://purl.org/coar/resource_type/c_c94f + + + http://purl.org/coar/resource_type/c_5794 + + + http://purl.org/coar/resource_type/c_6670 + + + http://purl.org/coar/resource_type/c_3e5a + + + http://purl.org/coar/resource_type/c_beb9 + + + http://purl.org/coar/resource_type/c_ddb1 + + + http://purl.org/coar/resource_type/c_db06 + + + http://purl.org/coar/resource_type/c_c513 + + + http://purl.org/coar/resource_type/c_8544 + + + http://purl.org/coar/resource_type/c_0857 + + + http://purl.org/coar/resource_type/c_bdcc + + + http://purl.org/coar/resource_type/c_8a7e + + + http://purl.org/coar/resource_type/c_2659 + + + http://purl.org/coar/resource_type/c_545b + + + http://purl.org/coar/resource_type/c_15cd + + + http://purl.org/coar/resource_type/c_816b + + + http://purl.org/coar/resource_type/c_93fc + + + http://purl.org/coar/resource_type/c_ba1f + + + http://purl.org/coar/resource_type/c_baaf + + + http://purl.org/coar/resource_type/c_efa0 + + + http://purl.org/coar/resource_type/c_5ce6 + + + http://purl.org/coar/resource_type/c_ecc8 + + + http://purl.org/coar/resource_type/c_71bd + + + http://purl.org/coar/resource_type/c_393c + + + http://purl.org/coar/resource_type/c_8042 + + + http://purl.org/coar/resource_type/c_46ec + + + http://purl.org/coar/resource_type/c_12cc + + + http://purl.org/coar/resource_type/c_12cd + + + http://purl.org/coar/resource_type/c_12ce + + + http://purl.org/coar/resource_type/c_18cc + + + http://purl.org/coar/resource_type/c_18cd + + + http://purl.org/coar/resource_type/c_18cf + + + http://purl.org/coar/resource_type/c_18cp + + + http://purl.org/coar/resource_type/c_18co + + + http://purl.org/coar/resource_type/c_18cw + + + http://purl.org/coar/resource_type/c_18ww + + + http://purl.org/coar/resource_type/c_18wz + + + http://purl.org/coar/resource_type/c_18wq + + + http://purl.org/coar/resource_type/c_186u + + + http://purl.org/coar/resource_type/c_18op + + + http://purl.org/coar/resource_type/c_18hj + + + http://purl.org/coar/resource_type/c_18ws + + + http://purl.org/coar/resource_type/c_18gh + + + http://purl.org/coar/resource_type/c_dcae04bc + + + http://purl.org/coar/resource_type/c_2df8fbb1 + + + + http://purl.org/coar/resource_type/c_1843 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://purl.org/coar/access_right/c_abf2 + + + http://purl.org/coar/access_right/c_f1cf + + + http://purl.org/coar/access_right/c_16ec + + + http://purl.org/coar/access_right/c_14cb + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/crosswalks/oai/transformers/rioxx.xsl b/dspace/config/crosswalks/oai/transformers/rioxx.xsl new file mode 100644 index 000000000000..7fc597b483d5 --- /dev/null +++ b/dspace/config/crosswalks/oai/transformers/rioxx.xsl @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eng + + + eng + + + spa + + + deu + + + fra + + + ita + + + jpn + + + zho + + + por + + + tur + + + + + + + + + + + + + + + + + + annotation + + + journal + + + journal article + + + editorial + + + bachelor thesis + + + bibliography + + + book + + + book part + + + book review + + + website + + + interactive resource + + + conference proceedings + + + conference object + + + conference paper + + + conference poster + + + contribution to journal + + + data paper + + + dataset + + + doctoral thesis + + + image + + + lecture + + + letter + + + master thesis + + + moving image + + + periodical + + + letter to the editor + + + patent + + + preprint + + + report + + + report part + + + research proposal + + + review + + + software + + + still image + + + technical documentation + + + workflow + + + working paper + + + thesis + + + cartographic material + + + map + + + video + + + sound + + + musical composition + + + text + + + conference paper not in proceedings + + + conference poster not in proceedings + + + musical notation + + + internal report + + + memorandum + + + other type of report + + + policy report + + + project deliverable + + + report to funding agency + + + research report + + + technical report + + + review article + + + research article + + + other + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/crosswalks/oai/xoai.xml b/dspace/config/crosswalks/oai/xoai.xml index 9bcabdf0e955..9a4985d6674f 100644 --- a/dspace/config/crosswalks/oai/xoai.xml +++ b/dspace/config/crosswalks/oai/xoai.xml @@ -19,6 +19,7 @@ + This is the default context of the DSpace OAI-PMH data provider. @@ -66,7 +67,7 @@ - This contexts complies with Openaire Guidelines for Literature Repositories v3.0. + This context complies with OpenAIRE Guidelines for Literature Repositories v3.0. @@ -90,6 +91,23 @@ This contexts complies with Openaire Guidelines for Literature Repositories v4.0. + + + + + + + + + + + This contexts complies with RIOXX rules. + + @@ -185,17 +203,35 @@ http://namespace.openaire.eu/schema/oaire/ https://www.openaire.eu/schema/repo-lit/4.0/openaire.xsd + + + rioxx + metadataFormats/rioxx.xsl + http://www.rioxx.net/schema/v3.0/rioxx/ + http://www.rioxx.net/schema/v3.0/rioxx/ http://www.rioxx.net/schema/v3.0/rioxx/rioxx.xsd + + transformers/driver.xsl + Driver context transformer transformers/openaire.xsl + OpenAire context transformer transformers/openaire4.xsl + OpenAire v4 context transformer + + + transformers/rioxx.xsl + RIOXX context transformer @@ -359,6 +395,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.dspace.xoai.filter.DSpaceMetadataExistsFilter + + + dc.type + + + + + + org.dspace.xoai.filter.DSpaceMetadataExistsFilter + + + dc.language.iso + + + + + + + org.dspace.xoai.filter.ItemsWithBitstreamFilter + + org.dspace.xoai.filter.DSpaceMetadataExistsFilter @@ -529,5 +650,10 @@ OpenAIRE + + rioxx + RIOXX set + + diff --git a/dspace/config/entities/rioxx3-relationships.xml b/dspace/config/entities/rioxx3-relationships.xml new file mode 100644 index 000000000000..6ed577f60374 --- /dev/null +++ b/dspace/config/entities/rioxx3-relationships.xml @@ -0,0 +1,91 @@ + + + + + + + + Publication + Person + isAuthorOfPublication + isPublicationOfAuthor + + 0 + + + 0 + + + + + Publication + OrgUnit + isAuthorOfPublication + isPublicationOfAuthor + + 0 + + + 0 + + + + + Publication + Person + isContributorOfPublication + isPublicationOfContributor + + 0 + + + 0 + + + + + Publication + OrgUnit + isContributorOfPublication + isPublicationOfContributor + + 0 + + + 0 + + + + + Publication + Project + isProjectOfPublication + isPublicationOfProject + + 0 + + + 0 + + + + + Project + OrgUnit + isFundingAgencyOfProject + isProjectOfFundingAgency + + 0 + + + 0 + + + + diff --git a/dspace/config/registries/dublin-core-types.xml b/dspace/config/registries/dublin-core-types.xml index d0f340f89c3c..9a4aefb3fff1 100644 --- a/dspace/config/registries/dublin-core-types.xml +++ b/dspace/config/registries/dublin-core-types.xml @@ -265,6 +265,7 @@ dc identifier doi + The doi identifier minted by this repository. diff --git a/dspace/config/spring/api/virtual-metadata.xml.rioxx3 b/dspace/config/spring/api/virtual-metadata.xml.rioxx3 new file mode 100644 index 000000000000..7f5c0e8ceae8 --- /dev/null +++ b/dspace/config/spring/api/virtual-metadata.xml.rioxx3 @@ -0,0 +1,402 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + person.familyName + person.givenName + organization.legalName + + + + , + + + + + + + + person.givenName + + + + + + + person.familyName + + + + + + + person.affiliation.name + + + + + + + person.identifier + + + + + + + person.identifier.orcid + + + + + + + person.identifier.isni + + + + + + + organization.legalName + + + + + + + organization.identifier + + + + + + + + + + + + organization.legalName + + + + + + + + + + + + person.familyName + person.givenName + + + + , + + + + + + + + + + + organization.legalName + + + + + + + + + + + organization.legalName + + + + + + + + + + + + + + publicationvolume.volumeNumber + + + + + + + + + + + + + + + + + + + + + + creativeworkseries.issn + + + + + + + dc.title + + + + + + + + + + + + + + publicationissue.issueNumber + + + + + + + + + + + + + + dc.title + + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + + + + + + + oaire.fundingStream + + + + + + + dc.identifier + + + + + + + dc.identifier.uri + + + + + + + + + + + + + + + + + + + + + + + + + organization.legalName + + + + + + + organization.identifier + + + + diff --git a/dspace/solr/oai/conf/schema.xml b/dspace/solr/oai/conf/schema.xml index b2f61c2de9dd..aefa79834873 100644 --- a/dspace/solr/oai/conf/schema.xml +++ b/dspace/solr/oai/conf/schema.xml @@ -126,7 +126,9 @@ - + + + From d645939baf07d10d0f9f0b5e7b2722556400ff97 Mon Sep 17 00:00:00 2001 From: mohamed eskander Date: Wed, 17 Jan 2024 10:39:18 +0200 Subject: [PATCH 052/118] [DURACOM-143] Fix indexing errors & further improvements --- .../indexobject/IndexFactoryImpl.java | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java index 55c99b168e7a..4b012a072f33 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java @@ -2,7 +2,7 @@ * 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.discovery.indexobject; @@ -64,7 +64,14 @@ public SolrInputDocument buildDocument(Context context, T indexableObject) throw //Do any additional indexing, depends on the plugins for (SolrServiceIndexPlugin solrServiceIndexPlugin : ListUtils.emptyIfNull(solrServiceIndexPlugins)) { - solrServiceIndexPlugin.additionalIndex(context, indexableObject, doc); + try { + solrServiceIndexPlugin.additionalIndex(context, indexableObject, doc); + } catch (Exception e) { + log.error("An error occurred while indexing additional fields. " + + "Could not fully index item with UUID: {}. Plugin: {}", + indexableObject.getUniqueIndexID(), solrServiceIndexPlugin.getClass().getSimpleName()); + + } } return doc; @@ -82,7 +89,7 @@ public void writeDocument(Context context, T indexableObject, SolrInputDocument writeDocument(solrInputDocument, null); } catch (Exception e) { log.error("Error occurred while writing SOLR document for {} object {}", - indexableObject.getType(), indexableObject.getID(), e); + indexableObject.getType(), indexableObject.getID(), e); } } @@ -101,8 +108,8 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea if (streams != null && !streams.isEmpty()) { // limit full text indexing to first 100,000 characters unless configured otherwise final int charLimit = DSpaceServicesFactory.getInstance().getConfigurationService() - .getIntProperty("discovery.solr.fulltext.charLimit", - 100000); + .getIntProperty("discovery.solr.fulltext.charLimit", + 100000); // Use Tika's Text parser as the streams are always from the TEXT bundle (i.e. already extracted text) TextAndCSVParser tikaParser = new TextAndCSVParser(); @@ -113,6 +120,18 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea // Use Apache Tika to parse the full text stream(s) try (InputStream fullTextStreams = streams.getStream()) { tikaParser.parse(fullTextStreams, tikaHandler, tikaMetadata, tikaContext); + + // Write Tika metadata to "tika_meta_*" fields. + // This metadata is not very useful right now, + // but we'll keep it just in case it becomes more useful. + for (String name : tikaMetadata.names()) { + for (String value : tikaMetadata.getValues(name)) { + doc.addField("tika_meta_" + name, value); + } + } + + // Save (parsed) full text to "fulltext" field + doc.addField("fulltext", tikaHandler.toString()); } catch (SAXException saxe) { // Check if this SAXException is just a notice that this file was longer than the character limit. // Unfortunately there is not a unique, public exception type to catch here. This error is thrown @@ -121,30 +140,23 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea if (saxe.getMessage().contains("limit has been reached")) { // log that we only indexed up to that configured limit log.info("Full text is larger than the configured limit (discovery.solr.fulltext.charLimit)." - + " Only the first {} characters were indexed.", charLimit); + + " Only the first {} characters were indexed.", charLimit); } else { log.error("Tika parsing error. Could not index full text.", saxe); throw new IOException("Tika parsing error. Could not index full text.", saxe); } - } catch (TikaException ex) { + } catch (TikaException | IOException ex) { log.error("Tika parsing error. Could not index full text.", ex); throw new IOException("Tika parsing error. Could not index full text.", ex); + } finally { + // Add document to index + solr.add(doc); } - - // Write Tika metadata to "tika_meta_*" fields. - // This metadata is not very useful right now, but we'll keep it just in case it becomes more useful. - for (String name : tikaMetadata.names()) { - for (String value : tikaMetadata.getValues(name)) { - doc.addField("tika_meta_" + name, value); - } - } - - // Save (parsed) full text to "fulltext" field - doc.addField("fulltext", tikaHandler.toString()); + return; } - // Add document to index solr.add(doc); + } } From 324d2e3184dd35d2980e599f173adf18a7e31d64 Mon Sep 17 00:00:00 2001 From: mohamed eskander Date: Wed, 17 Jan 2024 10:56:08 +0200 Subject: [PATCH 053/118] [DURACOM-143] Fix license --- .../java/org/dspace/discovery/indexobject/IndexFactoryImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java index 4b012a072f33..f1ae137b9163 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java @@ -2,7 +2,7 @@ * 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.discovery.indexobject; From ec0ab92794cdd7d4af0185cad299f09bf5b5aca8 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Fri, 29 Dec 2023 14:57:09 +0100 Subject: [PATCH 054/118] Alteration to index-discovery script to only (re-)index specific type of IndexableObject Not compatible with `-b` option since this clears entire index first (& expect to rebuild it in its entirety) Compatible with `-f` to force reindex specific type of IndexableObject --- .../java/org/dspace/discovery/IndexClient.java | 16 ++++++++++++++-- .../org/dspace/discovery/IndexClientOptions.java | 11 +++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java index 661c48d91cfc..a01d43ce8e12 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java @@ -7,6 +7,8 @@ */ package org.dspace.discovery; +import static org.dspace.discovery.IndexClientOptions.TYPE_OPTION; + import java.io.IOException; import java.sql.SQLException; import java.util.Iterator; @@ -15,6 +17,7 @@ import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.DSpaceObject; @@ -51,6 +54,11 @@ public void internalRun() throws Exception { return; } + String type = null; + if (commandLine.hasOption(TYPE_OPTION)) { + type = commandLine.getOptionValue(TYPE_OPTION); + } + /** Acquire from dspace-services in future */ /** * new DSpace.getServiceManager().getServiceByName("org.dspace.discovery.SolrIndexer"); @@ -113,6 +121,10 @@ public void internalRun() throws Exception { } else if (indexClientOptions == IndexClientOptions.BUILD || indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) { handler.logInfo("(Re)building index from scratch."); + if (StringUtils.isNotBlank(type)) { + handler.logWarning(String.format("Type option, %s, not applicable for entire index rebuild option, b" + + ", type will be ignored", TYPE_OPTION)); + } indexer.deleteIndex(); indexer.createIndex(context); if (indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) { @@ -133,14 +145,14 @@ public void internalRun() throws Exception { } else if (indexClientOptions == IndexClientOptions.UPDATE || indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) { handler.logInfo("Updating Index"); - indexer.updateIndex(context, false); + indexer.updateIndex(context, false, type); if (indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) { checkRebuildSpellCheck(commandLine, indexer); } } else if (indexClientOptions == IndexClientOptions.FORCEUPDATE || indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) { handler.logInfo("Updating Index"); - indexer.updateIndex(context, true); + indexer.updateIndex(context, true, type); if (indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) { checkRebuildSpellCheck(commandLine, indexer); } diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexClientOptions.java b/dspace-api/src/main/java/org/dspace/discovery/IndexClientOptions.java index 74d9ba0c3a56..0de5b22d0655 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexClientOptions.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexClientOptions.java @@ -8,8 +8,13 @@ package org.dspace.discovery; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Options; +import org.dspace.discovery.indexobject.factory.IndexObjectFactoryFactory; /** * This Enum holds all the possible options and combinations for the Index discovery script @@ -29,6 +34,8 @@ public enum IndexClientOptions { FORCEUPDATEANDSPELLCHECK, HELP; + public static final String TYPE_OPTION = "t"; + /** * This method resolves the CommandLine parameters to figure out which action the index-discovery script should * perform @@ -71,11 +78,15 @@ protected static IndexClientOptions getIndexClientOption(CommandLine commandLine protected static Options constructOptions() { Options options = new Options(); + List indexableObjectTypes = IndexObjectFactoryFactory.getInstance().getIndexFactories().stream() + .map((indexFactory -> indexFactory.getType())).collect(Collectors.toList()); options .addOption("r", "remove", true, "remove an Item, Collection or Community from index based on its handle"); options.addOption("i", "index", true, "add or update an Item, Collection or Community based on its handle or uuid"); + options.addOption(TYPE_OPTION, "type", true, "reindex only specific type of " + + "(re)indexable objects; options: " + Arrays.toString(indexableObjectTypes.toArray())); options.addOption("c", "clean", false, "clean existing index removing any documents that no longer exist in the db"); options.addOption("d", "delete", false, From c0d68ee7d00e0521393b3313b16bf0c952c2d7e6 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Tue, 23 Jan 2024 12:19:13 +0100 Subject: [PATCH 055/118] [DSC-963] Addresses changes of the PR#8797 --- dspace-server-webapp/pom.xml | 13 +- .../GenericAuthorizationFeatureIT.java | 547 +++++------------- .../resources/application-test.properties | 1 + dspace/modules/server-boot/pom.xml | 2 +- dspace/modules/server/pom.xml | 2 +- 5 files changed, 171 insertions(+), 394 deletions(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index d5de9611d97c..1ea77347cd2b 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -76,10 +76,21 @@ ${basedir}/src/main/resources - **/*.properties + **/*application*.properties + **/*dspace*.properties true + + ${basedir}/src/main/resources + + **/*application*.properties + **/*dspace*.properties + + + **/*.properties + + ${basedir}/src/main/resources diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java index d59ef00018b1..3e990ff81097 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/GenericAuthorizationFeatureIT.java @@ -38,6 +38,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.ResultActions; /** * Test for the following authorization features: @@ -53,6 +54,8 @@ */ public class GenericAuthorizationFeatureIT extends AbstractControllerIntegrationTest { + private static final int SIZE = 100; + @Autowired ConfigurationService configurationService; @@ -209,215 +212,163 @@ private void testAdminsHavePermissionsAllDso(String feature) throws Exception { String siteId = ContentServiceFactory.getInstance().getSiteService().findSite(context).getID().toString(); // Verify the general admin has this feature on the site - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/sites/" + siteId)) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/sites/" + siteId) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin doesn’t have this feature on the site - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/sites/" + siteId)) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/sites/" + siteId) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on community A - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(adminToken,"http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on community A - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on community AA - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin doesn’t have this feature on community A - getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify community A admin doesn’t have this feature on community B - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityB.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/communities/" + communityB.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on collection X - getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on collection X - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on collection X - getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on collection X - getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X admin doesn’t have this feature on collection Y - getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionY.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/collections/" + collectionY.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on item 1 - getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on item 1 - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on item 1 - getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on item 1 - getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on item 2 - getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item2.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/items/" + item2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on the bundle in item 1 - getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bundle in item 1 - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bundle in item 1 - getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bundle in item 1 - getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on the bundle in item 2 - getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle2.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bundles/" + bundle2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on the bitstream in item 1 - getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bitstream in item 1 - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bitstream in item 1 - getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bitstream in item 1 - getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on the bitstream in item 2 - getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream2.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bitstreams/" + bitstream2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -430,41 +381,31 @@ private void testAdminsHavePermissionsItem(String feature) throws Exception { String item1AdminToken = getAuthToken(item1Admin.getEmail(), password); // Verify the general admin has this feature on item 1 - getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on item 1 - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on item 1 - getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on item 1 - getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin doesn’t have this feature on item 2 - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item2.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/items/" + item2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -479,73 +420,55 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify community A write has this feature on community A if the boolean parameter is true // (or doesn’t have access otherwise) if (hasDSOAccess) { - getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); } else { - getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); } // Verify community A write doesn’t have this feature on community AA - getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify community A write doesn’t have this feature on collection X - getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify community A write doesn’t have this feature on item 1 - getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify community A write doesn’t have this feature on the bundle in item 1 - getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify community A write doesn’t have this feature on the bitstream in item 1 - getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on community A - getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on community AA - getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -553,65 +476,49 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify collection X write has this feature on collection X if the boolean parameter is true // (or doesn’t have access otherwise) if (hasDSOAccess) { - getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); } else { - getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); } // Verify collection X write doesn’t have this feature on item 1 - getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on the bundle in item 1 - getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on the bitstream in item 1 - getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on community A - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on community AA - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on collection X - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -619,57 +526,43 @@ private void testWriteUsersHavePermissionsAllDso(String feature, boolean hasDSOA // Verify item 1 write has this feature on item 1 if the boolean parameter is true // (or doesn’t have access otherwise) if (hasDSOAccess) { - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); } else { - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); } // Verify item 1 write doesn’t have this feature on the bundle in item 1 - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on the bitstream in item 1 - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify community A write doesn’t have this feature on community B - getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityB.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/communities/" + communityB.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on collection Y - getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionY.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/collections/" + collectionY.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on item 2 - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item2.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/items/" + item2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -681,17 +574,13 @@ private void testWriteUsersHavePermissionsItem(String feature, boolean hasDSOAcc String item1WriterToken = getAuthToken(item1Writer.getEmail(), password); // Verify community A write doesn’t have this feature on item 1 - getClient(communityAWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on item 1 - getClient(collectionXWriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -699,25 +588,19 @@ private void testWriteUsersHavePermissionsItem(String feature, boolean hasDSOAcc // Verify item 1 write has this feature on item 1 if the boolean parameter is true // (or doesn’t have access otherwise) if (hasDSOAccess) { - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); } else { - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); } // Verify item 1 write doesn’t have this feature on item 2 - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item2.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/items/" + item2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -755,41 +638,31 @@ public void testCanMoveAdmin() throws Exception { final String feature = "canMove"; // Verify the general admin has this feature on item 1 - getClient(adminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on item 1 - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on item 1 - getClient(collectionXAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on item 1 - getClient(item1AdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify community A admin doesn’t have this feature on item 2 - getClient(communityAAdminToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item2.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/items/" + item2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -806,9 +679,7 @@ public void testCanMoveAdmin() throws Exception { context.restoreAuthSystemState(); // verify item 1 write has this feature on item 1 - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='canMove')]") .exists()); @@ -829,9 +700,7 @@ public void testCanMoveWriter() throws Exception { String item1WriterToken = getAuthToken(item1Writer.getEmail(), password); // verify item 1 write has this feature on item 1 - getClient(item1WriterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='canMove')]") .exists()); @@ -867,31 +736,25 @@ public void testCanDeleteAdmin() throws Exception { final String feature = "canDelete"; // Verify the general admin doesn’t have this feature on the site - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/sites/" + siteId)) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/sites/" + siteId) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on community A - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on community A - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on community AA - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -910,174 +773,139 @@ public void testCanDeleteAdmin() throws Exception { .build(); context.restoreAuthSystemState(); String communityAAAdminToken = getAuthToken(communityAAAdmin.getEmail(), password); - getClient(communityAAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(communityAAAdminToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X admin doesn’t have this feature on community A - getClient(collectionXAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify community A admin doesn’t have this feature on community B - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityB.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/communities/" + communityB.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on collection X - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on collection X - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin doesn’t have this feature on collection X - getClient(collectionXAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 admin doesn’t have this feature on collection X - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X admin doesn’t have this feature on collection Y - getClient(collectionXAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionY.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/collections/" + collectionY.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on item 1 - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAAdminToken,"http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on item 1 - getClient(collectionXAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXAdminToken,"http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 admin doesn’t have this feature on item 2 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item2.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/items/" + item2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on the bundle in item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bundle in item 1 - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bundle in item 1 - getClient(collectionXAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bundle in item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on the bundle in item 2 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle2.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bundles/" + bundle2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify the general admin has this feature on the bitstream in item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bitstream in item 1 - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bitstream in item 1 - getClient(collectionXAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bitstream in item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin doesn’t have this feature on the bitstream in item 2 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream2.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bitstreams/" + bitstream2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1104,9 +932,7 @@ public void testCanDeleteAdminParent() throws Exception { context.restoreAuthSystemState(); String communityAAAdminToken = getAuthToken(communityAAAdmin.getEmail(), password); //verify the community AA admin has this feature on community AA - getClient(communityAAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(communityAAAdminToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -1120,9 +946,7 @@ public void testCanDeleteAdminParent() throws Exception { .build(); context.restoreAuthSystemState(); // verify collection X admin has this feature on collection X - getClient(collectionXAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -1136,8 +960,7 @@ public void testCanDeleteAdminParent() throws Exception { .build(); context.restoreAuthSystemState(); // verify item 1 admin has this feature on item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -1167,16 +990,12 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String communityADeleterToken = getAuthToken(communityADeleter.getEmail(), password); // Verify the user has this feature on community A - getClient(communityADeleterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(communityADeleterToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on community AA - getClient(communityADeleterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(communityADeleterToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1197,23 +1016,17 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String communityARemoverToken = getAuthToken(communityARemover.getEmail(), password); // Verify the user has this feature on community AA - getClient(communityARemoverToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(communityARemoverToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on community A - getClient(communityARemoverToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityA.getID())) + getAuthorizationFeatures(communityARemoverToken, "http://localhost/api/core/communities/" + communityA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify this user doesn’t have this feature on collection X - getClient(communityARemoverToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(communityARemoverToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1233,23 +1046,17 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String communityAARemoverToken = getAuthToken(communityAARemover.getEmail(), password); // Verify the user has this feature on collection X - getClient(communityAARemoverToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(communityAARemoverToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on community AA - getClient(communityAARemoverToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/communities/" + communityAA.getID())) + getAuthorizationFeatures(communityAARemoverToken, "http://localhost/api/core/communities/" + communityAA.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify this user doesn’t have this feature on item 1 - getClient(communityAARemoverToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAARemoverToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1269,9 +1076,7 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String collectionXRemoverToken = getAuthToken(collectionXRemover.getEmail(), password); // Verify the user doesn’t have this feature on item 1 - getClient(collectionXRemoverToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXRemoverToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1291,8 +1096,7 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String item1DeleterToken = getAuthToken(item1Deleter.getEmail(), password); // Verify the user doesn’t have this feature on item 1 - getClient(item1DeleterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1DeleterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1317,23 +1121,17 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String collectionXRemoverItem1DeleterToken = getAuthToken(collectionXRemoverItem1Deleter.getEmail(), password); // Verify the user has this feature on item 1 - getClient(collectionXRemoverItem1DeleterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXRemoverItem1DeleterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on collection X - getClient(collectionXRemoverItem1DeleterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/collections/" + collectionX.getID())) + getAuthorizationFeatures(collectionXRemoverItem1DeleterToken, "http://localhost/api/core/collections/" + collectionX.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify this user doesn’t have this feature on the bundle in item 1 - getClient(collectionXRemoverItem1DeleterToken).perform( - get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(collectionXRemoverItem1DeleterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1353,20 +1151,17 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String item1RemoverToken = getAuthToken(item1Remover.getEmail(), password); // Verify the user has this feature on the bundle in item 1 - getClient(item1RemoverToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(item1RemoverToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify this user doesn’t have this feature on item 1 - getClient(item1RemoverToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1RemoverToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify this user doesn’t have this feature on the bitstream in item 1 - getClient(item1RemoverToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(item1RemoverToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1386,9 +1181,7 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String bundle1RemoverToken = getAuthToken(bundle1Remover.getEmail(), password); // Verify the user doesn’t have this feature on the bitstream in item 1 - getClient(bundle1RemoverToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(bundle1RemoverToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1414,9 +1207,7 @@ public void testCanDeleteMinimalPermissions() throws Exception { context.restoreAuthSystemState(); String bundle1item1RemoverToken = getAuthToken(bundle1item1Remover.getEmail(), password); // Verify the user has this feature on the bitstream in item 1 - getClient(bundle1item1RemoverToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bitstreams/" + bitstream1.getID())) + getAuthorizationFeatures(bundle1item1RemoverToken, "http://localhost/api/core/bitstreams/" + bitstream1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1431,39 +1222,31 @@ public void testCanReorderBitstreamsAdmin() throws Exception { final String feature = "canReorderBitstreams"; // Verify the general admin has this feature on the bundle in item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bundle in item 1 - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bundle in item 1 - getClient(collectionXAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bundle in item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin doesn’t have this feature on the bundle in item 2 - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle2.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/bundles/" + bundle2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1477,31 +1260,24 @@ public void testCanReorderBitstreamsWriter() throws Exception { final String feature = "canReorderBitstreams"; // Verify community A write doesn’t have this feature on the bundle in item 1 - getClient(communityAWriterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on the bundle in item 1 - getClient(collectionXWriterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on the bundle in item 1 - getClient(item1WriterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Create a new user, grant WRITE permissions on the bundle in item 1 to this user // Verify the user has this feature on the bundle in item 1 - getClient(communityAWriterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1516,39 +1292,31 @@ public void testCanCreateBitstreamAdmin() throws Exception { final String feature = "canCreateBitstream"; // Verify the general admin has this feature on the bundle in item 1 - getClient(adminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(adminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin has this feature on the bundle in item 1 - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify collection X admin has this feature on the bundle in item 1 - getClient(collectionXAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(collectionXAdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify item 1 admin has this feature on the bundle in item 1 - getClient(item1AdminToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(item1AdminToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); // Verify community A admin doesn’t have this feature on the bundle in item 2 - getClient(communityAAdminToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle2.getID())) + getAuthorizationFeatures(communityAAdminToken, "http://localhost/api/core/bundles/" + bundle2.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1562,24 +1330,19 @@ public void testCanCreateBitstreamWriter() throws Exception { final String feature = "canCreateBitstream"; // Verify community A write doesn’t have this feature on the bundle in item 1 - getClient(communityAWriterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on the bundle in item 1 - getClient(collectionXWriterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on the bundle in item 1 - getClient(item1WriterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1599,8 +1362,7 @@ public void testCanCreateBitstreamWriter() throws Exception { context.restoreAuthSystemState(); String bundle1WriterToken = getAuthToken(bundle1Writer.getEmail(), password); // Verify the user doesn’t have this feature on the bundle in item 1 - getClient(bundle1WriterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(bundle1WriterToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1620,8 +1382,7 @@ public void testCanCreateBitstreamWriter() throws Exception { context.restoreAuthSystemState(); String bundle1AdderToken = getAuthToken(bundle1Adder.getEmail(), password); // Verify the user doesn’t have this feature on the bundle in item 1 - getClient(bundle1AdderToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(bundle1AdderToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1657,9 +1418,7 @@ public void testCanCreateBitstreamWriter() throws Exception { context.restoreAuthSystemState(); String bundle1WriterAdderToken = getAuthToken(bundle1WriterAdder.getEmail(), password); // Verify the user has this feature on the bundle in item 1 - getClient(bundle1WriterAdderToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/bundles/" + bundle1.getID())) + getAuthorizationFeatures(bundle1WriterAdderToken, "http://localhost/api/core/bundles/" + bundle1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); @@ -1678,24 +1437,19 @@ public void testCanCreateBundleWriter() throws Exception { final String feature = "canCreateBundle"; // Verify community A write doesn’t have this feature on item 1 - getClient(communityAWriterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(communityAWriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify collection X write doesn’t have this feature on item 1 - getClient(collectionXWriterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(collectionXWriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); // Verify item 1 write doesn’t have this feature on item 1 - getClient(item1WriterToken).perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1WriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").doesNotExist()); @@ -1720,11 +1474,22 @@ public void testCanCreateBundleWriter() throws Exception { context.restoreAuthSystemState(); String item1AdderWriterToken = getAuthToken(item1AdderWriter.getEmail(), password); // Verify the user has this feature on item 1 - getClient(item1AdderWriterToken) - .perform(get("/api/authz/authorizations/search/object?size=1000&embed=feature&uri=" - + "http://localhost/api/core/items/" + item1.getID())) + getAuthorizationFeatures(item1AdderWriterToken, "http://localhost/api/core/items/" + item1.getID()) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.authorizations[?(@._embedded.feature.id=='" + feature + "')]").exists()); } + + private ResultActions getAuthorizationFeatures(String adminToken, String uri) throws Exception { + return getAuthorizationFeatures(adminToken, uri, SIZE); + } + + private ResultActions getAuthorizationFeatures(String adminToken, String uri, int size) throws Exception { + return getClient(adminToken) + .perform( + get( + "/api/authz/authorizations/search/object?size=" + size + "&embed=feature&uri=" + uri + ) + ); + } } \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/application-test.properties b/dspace-server-webapp/src/test/resources/application-test.properties index e92e1166e355..bd9e2ea4a17b 100644 --- a/dspace-server-webapp/src/test/resources/application-test.properties +++ b/dspace-server-webapp/src/test/resources/application-test.properties @@ -16,4 +16,5 @@ ## This file is found on classpath at src/test/resources/log4j2-test.xml logging.config = classpath:log4j2-test.xml +# Our integration tests expect application to be deployed at the root path (/) server.servlet.context-path=/ \ No newline at end of file diff --git a/dspace/modules/server-boot/pom.xml b/dspace/modules/server-boot/pom.xml index ce2a840a8e8c..82839b01cbfb 100644 --- a/dspace/modules/server-boot/pom.xml +++ b/dspace/modules/server-boot/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.dspace server-boot - DSpace Server Webapp:: Boot + DSpace Server Webapp:: Executable JAR - - - Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it - only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over - what metadatafield is generated. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it - only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over - what metadatafield is generated. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index b3659b4e98f9..8bc46a099e6b 100644 --- a/dspace/config/spring/api/scripts.xml +++ b/dspace/config/spring/api/scripts.xml @@ -97,9 +97,9 @@ - + - + diff --git a/dspace/config/spring/api/suggestions.xml b/dspace/config/spring/api/suggestions.xml index ebc174eca87f..77e0807fa2d4 100644 --- a/dspace/config/spring/api/suggestions.xml +++ b/dspace/config/spring/api/suggestions.xml @@ -21,7 +21,7 @@ - + @@ -37,7 +37,7 @@ + class="org.dspace.app.suggestion.openaire.AuthorNamesScorer"> dc.contributor.author @@ -50,9 +50,9 @@ + class="org.dspace.app.suggestion.openaire.DateScorer"> - + diff --git a/dspace/solr/qaevent/conf/schema.xml b/dspace/solr/qaevent/conf/schema.xml index 3212d88b0a16..4bcadcd5a169 100644 --- a/dspace/solr/qaevent/conf/schema.xml +++ b/dspace/solr/qaevent/conf/schema.xml @@ -53,13 +53,6 @@ - @@ -98,8 +91,6 @@ - @@ -117,7 +108,6 @@ - @@ -171,10 +161,6 @@ - diff --git a/dspace/solr/suggestion/conf/admin-extra.html b/dspace/solr/suggestion/conf/admin-extra.html deleted file mode 100644 index aa739da862cf..000000000000 --- a/dspace/solr/suggestion/conf/admin-extra.html +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/dspace/solr/suggestion/conf/elevate.xml b/dspace/solr/suggestion/conf/elevate.xml deleted file mode 100644 index 7630ebe20fb9..000000000000 --- a/dspace/solr/suggestion/conf/elevate.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/dspace/solr/suggestion/conf/schema.xml b/dspace/solr/suggestion/conf/schema.xml index ddb995465382..dc88df573683 100644 --- a/dspace/solr/suggestion/conf/schema.xml +++ b/dspace/solr/suggestion/conf/schema.xml @@ -17,196 +17,41 @@ --> - - + - - - + - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - @@ -224,19 +69,18 @@ - - - - + + @@ -247,8 +91,14 @@ ignoreCase="true" words="stopwords.txt" /> - - + + @@ -261,49 +111,33 @@ - + - - - - - - - - - - - - - - - - - - - - - - + words="stopwords.txt" /> + - @@ -312,134 +146,26 @@ ignoreCase="true" words="stopwords.txt" /> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -451,62 +177,37 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + + - + @@ -521,8 +222,8 @@ - - + + @@ -531,17 +232,9 @@ - - - - -suggestion_fullid - - - + + + suggestion_fullid + diff --git a/dspace/solr/suggestion/conf/scripts.conf b/dspace/solr/suggestion/conf/scripts.conf deleted file mode 100644 index f58b262ae0c8..000000000000 --- a/dspace/solr/suggestion/conf/scripts.conf +++ /dev/null @@ -1,24 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -user= -solr_hostname=localhost -solr_port=8983 -rsyncd_port=18983 -data_dir= -webapp_name=solr -master_host= -master_data_dir= -master_status_dir= diff --git a/dspace/solr/suggestion/conf/solrconfig.xml b/dspace/solr/suggestion/conf/solrconfig.xml index 1ffbffe57c13..ba2b42c981af 100644 --- a/dspace/solr/suggestion/conf/solrconfig.xml +++ b/dspace/solr/suggestion/conf/solrconfig.xml @@ -17,1927 +17,110 @@ --> - - - - 7.7.2 - - - - - - - - - - - - - - - ${solr.data.dir:} - - - - - - - - - - - - - - - - - - - - - - - - - 32 - 1000 - - - - - - - - - - - - ${solr.lock.type:native} - - - + 8.8.1 - - - - - - - - - - - - - - - - - - true - - - - - - - - - - - - - - - - ${solr.ulog.dir:} - - - - - 10000 - ${solr.autoCommit.maxTime:10000} - false - - - - - - ${solr.autoSoftCommit.maxTime:1000} - - - - - - - - - - - - - - - - - - - - 1024 - - - - - - - - - + + + + ${solr.data.dir:} + + + + + + + + + + 32 + 1000 + ${solr.lock.type:native} + + false + + + + + + + 10000 + ${solr.autoCommit.maxTime:10000} + true + + + + ${solr.autoSoftCommit.maxTime:-1} + + + + + + ${solr.max.booleanClauses:1024} + + + - - - - - - - - - - - - - false - - - - - - 20 - - - 200 - - - - - - - - - - - - static firstSearcher warming in solrconfig.xml - - - - - - false - - - 2 - - - - - - - - - + - The options below instruct Solr not to output any HTTP Caching - related headers - --> - - - - - - - - - - - - + + + + true + 20 + 200 + false + 2 + + + + + + + + + + + explicit 10 suggestion_id - - - - - - - - - - - explicit - json - true - suggestion_id - - - - - - - - true - json - true - - - - - - - - explicit - - - velocity - browse - layout - Solritas - - - edismax - - text^0.5 features^1.0 name^1.2 sku^1.5 suggestion_id^10.0 manu^1.1 cat^1.4 - title^10.0 description^5.0 keywords^5.0 author^2.0 resourcename^1.0 - - suggestion_id - 100% - *:* - 10 - *,score - - - text^0.5 features^1.0 name^1.2 sku^1.5 suggestion_id^10.0 manu^1.1 cat^1.4 - title^10.0 description^5.0 keywords^5.0 author^2.0 resourcename^1.0 - - text,features,name,sku,suggestion_id,manu,cat,title,description,keywords,author,resourcename - 3 - - - on - cat - manu_exact - content_type - author_s - ipod - GB - 1 - cat,inStock - after - price - 0 - 600 - 50 - popularity - 0 - 10 - 3 - manufacturedate_dt - NOW/YEAR-10YEARS - NOW - +1YEAR - before - after - - - on - content features title name - html - <b> - </b> - 0 - title - 0 - name - 3 - 200 - content - 750 + + - - on - false - 5 - 2 - 5 - true - true - 5 - 3 - - - - - spellcheck - - - - - - - - - - - - - + - application/json - - - - - - - application/csv - - - - - - - - true - ignored_ - - - true - links - ignored_ - - - - - - - - - - - - - - - - - - - - - - solrpingquery - - - all - - - - - - - - - explicit - true - - - - - - - - - - - - - - - - textSpell - - - default - name - ./spellchecker - - - - - - - - - - - - false - - false - - 1 - - - spellcheck - - - - - - - - true - - - tvComponent - - - - - - - - - text_general - - - - - - default - suggestion_id - solr.DirectSolrSpellChecker - - internal - - 0.5 - - 2 - - 1 - - 5 - - 4 - - 0.01 - - - - - - wordbreak - solr.WordBreakSolrSpellChecker - name - true - true - 10 - - - - - - - - - - - - - - - - suggestion_id - - default - wordbreak - on - true - 10 - 5 - 5 - true - true - 10 - 5 - - - spellcheck - - - - - - - - - - suggestion_id - true - - - tvComponent - - - - - - - - - default - - - org.carrot2.clustering.lingo.LingoClusteringAlgorithm - - - 20 - - - clustering/carrot2 - - - ENGLISH - - - stc - org.carrot2.clustering.stc.STCClusteringAlgorithm - - - - - - - true - default - true - - name - suggestion_id - - features - - true - - - - false - - edismax - - text^0.5 features^1.0 name^1.2 sku^1.5 suggestion_id^10.0 manu^1.1 cat^1.4 - - *:* - 10 - *,score - - - clustering - - - - - - - - - - true - false - - - terms - - - - - - - - string - elevate.xml - - - - - - explicit - suggestion_id - - - elevator - - - - - - - - - - - 100 + application/json - - - - - - - 70 - - 0.5 - - [-\w ,/\n\"']{20,200} - - - - - - - ]]> - ]]> - - - - - - - - - - - - - - - - - - - - - - - - ,, - ,, - ,, - ,, - ,]]> - ]]> - - - - - - 10 - .,!? - - - - - - - WORD - - - en - US - - - - - - - - - - - - - - - - - - - - - - - - text/plain; charset=UTF-8 - - - - - - - - - 5 - - - - - - - - - - - - - - - - - - *:* - + diff --git a/dspace/solr/suggestion/conf/spellings.txt b/dspace/solr/suggestion/conf/spellings.txt deleted file mode 100644 index d7ede6f5611d..000000000000 --- a/dspace/solr/suggestion/conf/spellings.txt +++ /dev/null @@ -1,2 +0,0 @@ -pizza -history \ No newline at end of file From dfdd2664e2b3f94ea863a08555f9fd0ad5332d40 Mon Sep 17 00:00:00 2001 From: frabacche Date: Thu, 25 Jan 2024 16:19:45 +0100 Subject: [PATCH 058/118] CST-5249_suggestion openaire integration xml fix --- .../openaire/PublicationLoaderRunnable.java | 62 ++--- .../spring/api/openaire-integration.xml | 226 ++++++++++++++++++ 2 files changed, 252 insertions(+), 36 deletions(-) create mode 100644 dspace/config/spring/api/openaire-integration.xml diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java index 0a3d56f0056b..575b53e55a60 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java @@ -8,15 +8,20 @@ package org.dspace.app.suggestion.openaire; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; -import java.util.UUID; import org.apache.commons.cli.ParseException; import org.dspace.content.Item; import org.dspace.core.Context; -import org.dspace.discovery.IndexableObject; +import org.dspace.discovery.DiscoverQuery; import org.dspace.discovery.SearchService; +import org.dspace.discovery.SearchServiceException; +import org.dspace.discovery.SearchUtils; +import org.dspace.discovery.utils.DiscoverQueryBuilder; +import org.dspace.discovery.utils.parameter.QueryBuilderSearchFilter; import org.dspace.scripts.DSpaceRunnable; +import org.dspace.sort.SortOption; import org.dspace.utils.DSpace; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -69,14 +74,9 @@ public void internalRun() throws Exception { context = new Context(); - List researchers = new ArrayList(); - if (profile != null) { - researchers = getResearcher(profile); - } else { - researchers = getResearchers(); - } - for (Item researcher : researchers) { - + Iterator researchers = getResearchers(profile); + while (researchers.hasNext()) { + Item researcher = researchers.next(); oairePublicationLoader.importAuthorRecords(context, researcher); } @@ -93,34 +93,24 @@ public void internalRun() throws Exception { * @return the researcher with specified UUID or all researchers */ @SuppressWarnings("rawtypes") - private List getResearcher(String profileUUID) { - final UUID uuid = profileUUID != null ? UUID.fromString(profileUUID) : null; + private Iterator getResearchers(String profileUUID) { SearchService searchService = new DSpace().getSingletonService(SearchService.class); - List objects = searchService.search(context, "search.resourceid:" + uuid.toString(), - "lastModified", false, 0, 1, "search.resourcetype:Item", "dspace.entity.type:Person"); - List items = new ArrayList(); - if (objects != null) { - for (IndexableObject o : objects) { - items.add((Item) o.getIndexedObject()); - } + DiscoverQueryBuilder queryBuilder = SearchUtils.getQueryBuilder(); + List filters = new ArrayList(); + if (profileUUID != null) { + QueryBuilderSearchFilter queryBuilderSearchFilter = + new QueryBuilderSearchFilter("search.resourceid", "equals", profileUUID.toString()); + filters.add(queryBuilderSearchFilter); } - LOGGER.info("Found " + items.size() + " researcher(s)"); - return items; - } - - @SuppressWarnings("rawtypes") - private List getResearchers() { - List objects = null; - SearchService searchService = new DSpace().getSingletonService(SearchService.class); - objects = searchService.search(context, "*:*", "lastModified", false, 0, - Integer.MAX_VALUE, "search.resourcetype:Item", "dspace.entity.type:Person"); - List items = new ArrayList(); - if (objects != null) { - for (IndexableObject o : objects) { - items.add((Item) o.getIndexedObject()); - } + try { + DiscoverQuery discoverQuery = queryBuilder.buildQuery(context, null, + SearchUtils.getDiscoveryConfigurationByName("person"), + "*:*", filters, + "Item", 10, Long.getLong("0"), null, SortOption.DESCENDING); + return searchService.iteratorSearch(context, null, discoverQuery); + } catch (SearchServiceException e) { + LOGGER.error("Unable to read researcher on solr", e); } - LOGGER.info("Found " + items.size() + " researcher(s)"); - return items; + return null; } } diff --git a/dspace/config/spring/api/openaire-integration.xml b/dspace/config/spring/api/openaire-integration.xml new file mode 100644 index 000000000000..fcf84474bd13 --- /dev/null +++ b/dspace/config/spring/api/openaire-integration.xml @@ -0,0 +1,226 @@ + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it + only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over + what metadatafield is generated. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 85b5e06fb0946a15f71abaa1d27ef5914f7b8df2 Mon Sep 17 00:00:00 2001 From: milanmajchrak Date: Sat, 27 Jan 2024 13:38:22 +0100 Subject: [PATCH 059/118] The cas.init() method is called in the VocabularyRestRepositoryIT#setup() method --- .../java/org/dspace/app/rest/VocabularyRestRepositoryIT.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java index 81d2dba67911..30890d7ef838 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VocabularyRestRepositoryIT.java @@ -92,6 +92,11 @@ public void setup() throws Exception { // the properties that we're altering above and this is only used within the tests DCInputAuthority.reset(); pluginService.clearNamedPluginClasses(); + + // The following line is needed to call init() method in the ChoiceAuthorityServiceImpl class, without it + // the `submissionConfigService` will be null what will cause a NPE in the clearCache() method + // https://github.com/DSpace/DSpace/issues/9292 + cas.getChoiceAuthoritiesNames(); cas.clearCache(); context.turnOffAuthorisationSystem(); From 79da22254433778b0f4483ef855c1aa911c3eeb6 Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Sat, 27 Jan 2024 21:40:38 +0100 Subject: [PATCH 060/118] CST-5249 clarify fingByTarget implementation --- .../java/org/dspace/app/suggestion/SuggestionServiceImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java index 4ac804ddaf84..66773fbc128d 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionServiceImpl.java @@ -76,6 +76,7 @@ public long countAllByTarget(Context context, UUID target) { public List findByTarget(Context context, UUID target, int pageSize, long offset) { List fullSourceTargets = new ArrayList(); for (String source : providersMap.keySet()) { + // all the suggestion target will be related to the same target (i.e. the same researcher - person item) SuggestionTarget sTarget = providersMap.get(source).findTarget(context, target); if (sTarget != null && sTarget.getTotal() > 0) { fullSourceTargets.add(sTarget); @@ -88,6 +89,8 @@ public int compare(SuggestionTarget arg0, SuggestionTarget arg1) { } } ); + // this list will be as large as the number of sources available in the repository so it is unlikely that + // real pagination will occur return fullSourceTargets.stream().skip(offset).limit(pageSize).collect(Collectors.toList()); } From 903c9800d50cdf5ea5bcc422840008c936ba688d Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Sat, 27 Jan 2024 21:41:07 +0100 Subject: [PATCH 061/118] CST-5249 improve implementation to use proper pagination across solr facets --- .../app/suggestion/SolrSuggestionStorageServiceImpl.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java index 8b492f115cc3..6281b6910701 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java @@ -38,6 +38,7 @@ import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.common.params.FacetParams; import org.dspace.content.Item; import org.dspace.content.dto.MetadataValueDTO; import org.dspace.content.service.ItemService; @@ -250,16 +251,13 @@ public List findAllTargets(Context context, String source, int solrQuery.setFacet(true); solrQuery.setFacetMinCount(1); solrQuery.addFacetField(TARGET_ID); - solrQuery.setFacetLimit((int) (pageSize + offset)); + solrQuery.setParam(FacetParams.FACET_OFFSET, String.valueOf(offset)); + solrQuery.setFacetLimit((int) (pageSize)); QueryResponse response = getSolr().query(solrQuery); FacetField facetField = response.getFacetField(TARGET_ID); List suggestionTargets = new ArrayList(); int idx = 0; for (Count c : facetField.getValues()) { - if (idx < offset) { - idx++; - continue; - } SuggestionTarget target = new SuggestionTarget(); target.setSource(source); target.setTotal((int) c.getCount()); From 15e3e3a164dc6c8061d4925ad02c4dd0ca6bc9c5 Mon Sep 17 00:00:00 2001 From: Andrea Bollini Date: Sat, 27 Jan 2024 21:57:39 +0100 Subject: [PATCH 062/118] CST-5249 add javadoc --- .../app/suggestion/SuggestionEvidence.java | 6 +- .../app/suggestion/SuggestionSource.java | 3 +- .../app/suggestion/SuggestionTarget.java | 5 +- .../app/suggestion/openaire/DateScorer.java | 58 ++++++++++++++----- 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java index daa257c8d652..d7f04929a19a 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionEvidence.java @@ -8,7 +8,9 @@ package org.dspace.app.suggestion; /** - * + * This DTO class is returned by an {@link org.dspace.app.suggestion.openaire.EvidenceScorer} to model the concept of + * an evidence / fact that has been used to evaluate the precision of a suggestion increasing or decreasing the score + * of the suggestion. * * @author Andrea Bollini (andrea.bollini at 4science.it) */ @@ -17,7 +19,7 @@ public class SuggestionEvidence { /** name of the evidence */ private String name; - /** score of the evidence */ + /** positive or negative value to influence the score of the suggestion */ private double score; /** additional notes */ diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java index 4a6f8dd7352f..6dcc3f7e1e4c 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionSource.java @@ -8,7 +8,8 @@ package org.dspace.app.suggestion; /** - * + * This DTO class is used to pass around the number of items interested by suggestion provided by a specific source + * (i.e. openaire) * * @author Andrea Bollini (andrea.bollini at 4science.it) */ diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java index 7666379f7a86..db82aa8081ba 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SuggestionTarget.java @@ -10,7 +10,8 @@ import org.dspace.content.Item; /** - * + * This DTO class is used to pass around the number of suggestions available from a specific source for a target + * repository item * * @author Andrea Bollini (andrea.bollini at 4science.it) */ @@ -29,7 +30,7 @@ public SuggestionTarget() { } /** - * Wrap a target person into a suggestion target. + * Wrap a target repository item (usually a person item) into a suggestion target. * * @param item must be not null */ diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java index 3b3457b217fc..ae7bb89f3228 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java @@ -8,12 +8,15 @@ package org.dspace.app.suggestion.openaire; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; +import java.util.List; import org.dspace.app.suggestion.SuggestionEvidence; import org.dspace.app.suggestion.SuggestionUtils; import org.dspace.content.Item; +import org.dspace.content.MetadataValue; import org.dspace.content.service.ItemService; import org.dspace.external.model.ExternalDataObject; import org.dspace.util.MultiFormatDateParser; @@ -28,23 +31,60 @@ */ public class DateScorer implements EvidenceScorer { + /** + * if available it should contains the metadata field key in the form (schema.element[.qualifier]) that contains + * the birth date of the researcher + */ private String birthDateMetadata; - //private String educationDateMetadata; + /** + * if available it should contains the metadata field key in the form (schema.element[.qualifier]) that contains + * the date of graduation of the researcher. If the metadata has multiple values the min will be used + */ + private String educationDateMetadata; + /** + * Force the scorer to reject any publication that was issued before the specified date + */ private String minDateMetadata; + /** + * Force the scorer to reject any publication that was issued after the specified date + */ private String maxDateMetadata; + /** + * The minimal age that is expected for a researcher to be a potential author of a scholarly contribution + * (i.e. the minimum delta from the publication date and the birth date) + */ private int birthDateDelta = 20; + + /** + * The maximum age that is expected for a researcher to be a potential author of a scholarly contribution + * (i.e. the maximum delta from the publication date and the birth date) + */ private int birthDateRange = 50; + /** + * The number of year from/before the graduation that is expected for a researcher to be a potential + * author of a scholarly contribution (i.e. the minimum delta from the publication date and the first + * graduation date) + */ private int educationDateDelta = -3; + + /** + * The maximum scientific longevity that is expected for a researcher from its graduation to be a potential + * author of a scholarly contribution (i.e. the maximum delta from the publication date and the first + * graduation date) + */ private int educationDateRange = 50; @Autowired private ItemService itemService; + /** + * the metadata used in the publication to track the publication date (i.e. dc.date.issued) + */ private String publicationDateMetadata; public void setItemService(ItemService itemService) { @@ -58,7 +98,7 @@ public void setBirthDateMetadata(String birthDate) { public String getBirthDateMetadata() { return birthDateMetadata; } - /* + public void setEducationDateMetadata(String educationDate) { this.educationDateMetadata = educationDate; } @@ -66,7 +106,6 @@ public void setEducationDateMetadata(String educationDate) { public String getEducationDateMetadata() { return educationDateMetadata; } - */ public void setBirthDateDelta(int birthDateDelta) { this.birthDateDelta = birthDateDelta; @@ -99,6 +138,7 @@ public void setPublicationDateMetadata(String publicationDateMetadata) { /** * Method which is responsible to evaluate ImportRecord based on the publication date. * ImportRecords which have a date outside the defined or calculated expected range will be discarded. + * {@link DateScorer#birthDateMetadata}, {@link DateScorer#educationDateMetadata} * * @param importRecord the ExternalDataObject to check * @param researcher DSpace item @@ -153,14 +193,8 @@ private Integer[] calculateRange(Item researcher) { } else { String birthDateStr = getSingleValue(researcher, birthDateMetadata); int birthDateYear = getYear(birthDateStr); - int educationDateYear = -1; - /* - getListMetadataValues(researcher, educationDateMetadata) - .stream() - .mapToInt(x -> getYear(x.getValue())) - .filter(d -> d > 0) - .min().orElse(-1); - */ + int educationDateYear = getListMetadataValues(researcher, educationDateMetadata).stream() + .mapToInt(x -> getYear(x.getValue())).filter(d -> d > 0).min().orElse(-1); if (educationDateYear > 0) { return new Integer[] { minYear > 0 ? minYear : educationDateYear + educationDateDelta, @@ -177,7 +211,6 @@ private Integer[] calculateRange(Item researcher) { } } - /* private List getListMetadataValues(Item researcher, String metadataKey) { if (metadataKey != null) { return itemService.getMetadataByMetadataString(researcher, metadataKey); @@ -185,7 +218,6 @@ private List getListMetadataValues(Item researcher, String metada return Collections.EMPTY_LIST; } } - */ private String getSingleValue(Item researcher, String metadataKey) { if (metadataKey != null) { From 0273c4899207c68beec52241c76e77ad3689e881 Mon Sep 17 00:00:00 2001 From: Marie Verdonck Date: Wed, 31 Jan 2024 11:24:51 +0100 Subject: [PATCH 063/118] PR#9243: index-discovery - validate type option --- .../src/main/java/org/dspace/discovery/IndexClient.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java index a01d43ce8e12..b70e9162f7a1 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java +++ b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java @@ -11,9 +11,12 @@ import java.io.IOException; import java.sql.SQLException; +import java.util.Arrays; import java.util.Iterator; +import java.util.List; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.ParseException; @@ -56,7 +59,13 @@ public void internalRun() throws Exception { String type = null; if (commandLine.hasOption(TYPE_OPTION)) { + List indexableObjectTypes = IndexObjectFactoryFactory.getInstance().getIndexFactories().stream() + .map((indexFactory -> indexFactory.getType())).collect(Collectors.toList()); type = commandLine.getOptionValue(TYPE_OPTION); + if (!indexableObjectTypes.contains(type)) { + handler.handleException(String.format("%s is not a valid indexable object type, options: %s", + type, Arrays.toString(indexableObjectTypes.toArray()))); + } } /** Acquire from dspace-services in future */ From a84d2440a77dd8156b06a6a3d4135fb045bd6f5a Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 2 Feb 2024 09:02:19 -0600 Subject: [PATCH 064/118] Fix "cannot override networks.dspacenet" by updating all compose files to reference 'dspacenet' network --- docker-compose-cli.yml | 12 ++++++------ docker-compose.yml | 2 +- dspace/src/main/docker-compose/README.md | 1 + .../main/docker-compose/docker-compose-angular.yml | 8 +++++--- .../src/main/docker-compose/docker-compose-iiif.yml | 8 +++++--- .../docker-compose/docker-compose-shibboleth.yml | 8 +++++--- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/docker-compose-cli.yml b/docker-compose-cli.yml index 7dbdde370378..b2c6df636b64 100644 --- a/docker-compose-cli.yml +++ b/docker-compose-cli.yml @@ -1,5 +1,10 @@ version: "3.7" - +networks: + # Default to using network named 'dspacenet' from docker-compose.yml. + # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet") + default: + name: ${COMPOSE_PROJECT_NAME}_dspacenet + external: true services: dspace-cli: image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}" @@ -26,13 +31,8 @@ services: - ./dspace/config:/dspace/config entrypoint: /dspace/bin/dspace command: help - networks: - - dspacenet tty: true stdin_open: true volumes: assetstore: - -networks: - dspacenet: diff --git a/docker-compose.yml b/docker-compose.yml index 14f47ebdb67b..99ee4982f715 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: depends_on: - dspacedb networks: - dspacenet: + - dspacenet ports: - published: 8080 target: 8080 diff --git a/dspace/src/main/docker-compose/README.md b/dspace/src/main/docker-compose/README.md index 8660a9796c7d..5e0dfed08768 100644 --- a/dspace/src/main/docker-compose/README.md +++ b/dspace/src/main/docker-compose/README.md @@ -78,6 +78,7 @@ docker-compose -p d7 up -d ``` docker-compose -p d7 -f docker-compose.yml -f dspace/src/main/docker-compose/docker-compose-angular.yml up -d ``` +NOTE: This starts the UI in development mode. It will take a few minutes to see the UI as the Angular code needs to be compiled. ## Run DSpace REST and DSpace Angular from local branches diff --git a/dspace/src/main/docker-compose/docker-compose-angular.yml b/dspace/src/main/docker-compose/docker-compose-angular.yml index 6690fb8bc526..d9a313e32e3e 100644 --- a/dspace/src/main/docker-compose/docker-compose-angular.yml +++ b/dspace/src/main/docker-compose/docker-compose-angular.yml @@ -8,7 +8,11 @@ version: '3.7' networks: - dspacenet: + # Default to using network named 'dspacenet' from docker-compose.yml. + # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet") + default: + name: ${COMPOSE_PROJECT_NAME}_dspacenet + external: true services: dspace-angular: container_name: dspace-angular @@ -24,8 +28,6 @@ services: DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: /server image: dspace/dspace-angular:${DSPACE_VER:-latest} - networks: - dspacenet: ports: - published: 4000 target: 4000 diff --git a/dspace/src/main/docker-compose/docker-compose-iiif.yml b/dspace/src/main/docker-compose/docker-compose-iiif.yml index 2ab58d9014f0..d795192f13c4 100644 --- a/dspace/src/main/docker-compose/docker-compose-iiif.yml +++ b/dspace/src/main/docker-compose/docker-compose-iiif.yml @@ -12,7 +12,11 @@ # version: '3.7' networks: - dspacenet: + # Default to using network named 'dspacenet' from docker-compose.yml. + # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet") + default: + name: ${COMPOSE_PROJECT_NAME}_dspacenet + external: true services: dspace-iiif: container_name: dspace-iiif @@ -21,8 +25,6 @@ services: # Using UCLA Library image as it seems to be most maintained at this time. There is no official image. # https://hub.docker.com/r/uclalibrary/cantaloupe image: uclalibrary/cantaloupe:5.0.4-0 - networks: - dspacenet: ports: - '8182:8182' # For a guide of environment variables that can be used, see diff --git a/dspace/src/main/docker-compose/docker-compose-shibboleth.yml b/dspace/src/main/docker-compose/docker-compose-shibboleth.yml index 58f1527d6ccb..33eadcb142d7 100644 --- a/dspace/src/main/docker-compose/docker-compose-shibboleth.yml +++ b/dspace/src/main/docker-compose/docker-compose-shibboleth.yml @@ -12,7 +12,11 @@ # version: '3.7' networks: - dspacenet: + # Default to using network named 'dspacenet' from docker-compose.yml. + # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet") + default: + name: ${COMPOSE_PROJECT_NAME}_dspacenet + external: true services: dspace-shibboleth: container_name: dspace-shibboleth @@ -22,8 +26,6 @@ services: build: # Must be relative to root, so that it can be built alongside [src]/docker-compose.yml context: ./dspace/src/main/docker/dspace-shibboleth - networks: - dspacenet: ports: - published: 80 target: 80 From 5f9565e267a18742e4192c29d17bcb6f9eaa1245 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 2 Feb 2024 09:13:16 -0600 Subject: [PATCH 065/118] Fix build issues with dockersolr by using "additional_contexts" to pass solr config path to Dockerfile. --- docker-compose.yml | 6 ++++-- dspace/src/main/docker/dspace-solr/Dockerfile | 12 +++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 99ee4982f715..980e88d04fd4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -89,8 +89,10 @@ services: container_name: dspacesolr image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" build: - context: . - dockerfile: ./dspace/src/main/docker/dspace-solr/Dockerfile + context: ./dspace/src/main/docker/dspace-solr/ + # Provide path to Solr configs necessary to build Docker image + additional_contexts: + solrconfigs: ./dspace/solr/ args: SOLR_VERSION: "${SOLR_VER:-8.11}" networks: diff --git a/dspace/src/main/docker/dspace-solr/Dockerfile b/dspace/src/main/docker/dspace-solr/Dockerfile index 5d02d0db811f..d079694ff55c 100644 --- a/dspace/src/main/docker/dspace-solr/Dockerfile +++ b/dspace/src/main/docker/dspace-solr/Dockerfile @@ -28,11 +28,13 @@ RUN mkdir -p $AUTHORITY_CONFIGSET_PATH && \ mkdir -p $STATISTICS_CONFIGSET_PATH && \ mkdir -p $QAEVENT_CONFIGSET_PATH -COPY dspace/solr/authority/conf/* $AUTHORITY_CONFIGSET_PATH/ -COPY dspace/solr/oai/conf/* $OAI_CONFIGSET_PATH/ -COPY dspace/solr/search/conf/* $SEARCH_CONFIGSET_PATH/ -COPY dspace/solr/statistics/conf/* $STATISTICS_CONFIGSET_PATH/ -COPY dspace/solr/qaevent/conf/* $QAEVENT_CONFIGSET_PATH/ +# NOTE: "solrconfigs" MUST be passed in by docker-compose via "additional_contexts" +# OR via "docker build --build-context solrconfigs=[path-to-dspace/solr]" +COPY --from=solrconfigs authority/conf/* $AUTHORITY_CONFIGSET_PATH/ +COPY --from=solrconfigs oai/conf/* $OAI_CONFIGSET_PATH/ +COPY --from=solrconfigs search/conf/* $SEARCH_CONFIGSET_PATH/ +COPY --from=solrconfigs statistics/conf/* $STATISTICS_CONFIGSET_PATH/ +COPY --from=solrconfigs qaevent/conf/* $QAEVENT_CONFIGSET_PATH/ RUN chown -R solr:solr /opt/solr/server/solr/configsets From 8fb4dd59549b004a9a7c7cbfedb75f4f5ca2f410 Mon Sep 17 00:00:00 2001 From: Tim Donohue Date: Fri, 2 Feb 2024 10:09:08 -0600 Subject: [PATCH 066/118] Fix 'dspacesolr' build issues in GitHub actions by adding 'solrconfig' as an additional_context --- .github/workflows/docker.yml | 2 ++ .github/workflows/reusable-docker-build.yml | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 338c7371f61a..9f1e407cff4b 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -102,6 +102,8 @@ jobs: build_id: dspace-solr image_name: dspace/dspace-solr dockerfile_path: ./dspace/src/main/docker/dspace-solr/Dockerfile + # Must pass solrconfigs to the Dockerfile so that it can find the required Solr config files + dockerfile_additional_contexts: 'solrconfigs=./dspace/solr/' secrets: DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }} diff --git a/.github/workflows/reusable-docker-build.yml b/.github/workflows/reusable-docker-build.yml index 46bdab3b6827..aa8327f4d11b 100644 --- a/.github/workflows/reusable-docker-build.yml +++ b/.github/workflows/reusable-docker-build.yml @@ -24,6 +24,12 @@ on: dockerfile_context: required: false type: string + default: '.' + # Optionally a list of "additional_contexts" to pass to Dockerfile. Defaults to empty + dockerfile_additional_contexts: + required: false + type: string + default: '' # If Docker image should have additional tag flavor details (e.g. a suffix), it may be passed in. tags_flavor: required: false @@ -123,7 +129,9 @@ jobs: id: docker_build uses: docker/build-push-action@v5 with: - context: ${{ inputs.dockerfile_context || '.' }} + build-contexts: | + ${{ inputs.dockerfile_additional_contexts }} + context: ${{ inputs.dockerfile_context }} file: ${{ inputs.dockerfile_path }} platforms: ${{ matrix.arch }} # For pull requests, we run the Docker build (to ensure no PR changes break the build), From 0d4c1ea63ab7cdd5ea79511228a5486a46c2a515 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:24:34 +0000 Subject: [PATCH 067/118] Update dependency com.h2database:h2 to v2.2.224 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bfde35bafa2e..8465c928bc61 100644 --- a/pom.xml +++ b/pom.xml @@ -1646,7 +1646,7 @@ com.h2database h2 - 2.2.220 + 2.2.224 test From 8d6f6e37b3beed98b9d20327cbb67ce683da9b3c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:38:10 +0000 Subject: [PATCH 068/118] Update dependency org.glassfish.jaxb:jaxb-runtime to v2.3.9 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8465c928bc61..34e9d133829b 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ 2.13.4.2 1.3.2 2.3.1 - 2.3.8 + 2.3.9 1.1.1 9.4.53.v20231009 From fe7800ab5a7d9b77e9783d09b000aff19be38c96 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:44:02 +0000 Subject: [PATCH 069/118] Update pdfbox-version to v2.0.30 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 34e9d133829b..c1d3b5308466 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 9.4.53.v20231009 2.20.0 - 2.0.29 + 2.0.30 1.19.0 1.7.36 2.5.0 From f67e0f65091b85d191d3694ea80339d42833ed9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:51:40 +0000 Subject: [PATCH 070/118] Update dependency commons-cli:commons-cli to v1.6.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c1d3b5308466..829e5b28c3ab 100644 --- a/pom.xml +++ b/pom.xml @@ -1409,7 +1409,7 @@ commons-cli commons-cli - 1.5.0 + 1.6.0 commons-codec From ab9279cbb88e8511f28fa01f7d2a005b61eca1d2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:52:16 +0000 Subject: [PATCH 071/118] Update dependency com.opencsv:opencsv to v5.9 --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 849dbd1356af..7029261619b9 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -784,7 +784,7 @@ com.opencsv opencsv - 5.7.1 + 5.9 From 70646a30de182a23b3f7ee675ff74a9a79c76b1f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 05:52:20 +0000 Subject: [PATCH 072/118] Update dependency commons-io:commons-io to v2.15.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 829e5b28c3ab..d8cb60079ec9 100644 --- a/pom.xml +++ b/pom.xml @@ -1447,7 +1447,7 @@ commons-io commons-io - 2.13.0 + 2.15.1 org.apache.commons From a6978137203c6c8112b9d31c095bf8c7823fe8db Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 06:24:36 +0000 Subject: [PATCH 073/118] Update log4j.version to v2.22.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d8cb60079ec9..65bb2c6373ce 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ 1.1.1 9.4.53.v20231009 - 2.20.0 + 2.22.1 2.0.30 1.19.0 1.7.36 From 07ec54832a7a8380a0d1c7b361e57938ed88360f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 06:33:06 +0000 Subject: [PATCH 074/118] Update dependency com.fasterxml:classmate to v1.6.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 65bb2c6373ce..dc49dbdc1ac1 100644 --- a/pom.xml +++ b/pom.xml @@ -1704,7 +1704,7 @@ com.fasterxml classmate - 1.5.1 + 1.6.0 com.fasterxml.jackson.core From 0958a98b1f4d339cfdca50ff65081ae0730b0d74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:13:14 +0000 Subject: [PATCH 075/118] Update dependency org.xmlunit:xmlunit-core to v2.9.1 --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 7029261619b9..33e78f7a9dd9 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -902,7 +902,7 @@ org.xmlunit xmlunit-core - 2.8.0 + 2.9.1 test From 68caa1dcf8e7c5687fc250ed62704832d93aa362 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 08:16:05 +0000 Subject: [PATCH 076/118] Update dependency com.maxmind.geoip2:geoip2 to v2.17.0 --- dspace-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 33e78f7a9dd9..8da32b50c1de 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -620,7 +620,7 @@ com.maxmind.geoip2 geoip2 - 2.11.0 + 2.17.0 org.apache.ant From 71ea2a752665bd6eaeb27575bd3fdf3ac05eacd4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 09:06:03 +0000 Subject: [PATCH 077/118] Update netty monorepo to v4.1.106.Final --- dspace-api/pom.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 8da32b50c1de..d6f72723e6d3 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -867,32 +867,32 @@ io.netty netty-buffer - 4.1.94.Final + 4.1.106.Final io.netty netty-transport - 4.1.94.Final + 4.1.106.Final io.netty netty-transport-native-unix-common - 4.1.94.Final + 4.1.106.Final io.netty netty-common - 4.1.94.Final + 4.1.106.Final io.netty netty-handler - 4.1.94.Final + 4.1.106.Final io.netty netty-codec - 4.1.94.Final + 4.1.106.Final org.apache.velocity From f42a981d1bfc588c5a42d918036110f2b92b1b8a Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 29 Nov 2023 08:33:39 +0300 Subject: [PATCH 078/118] pom.xml: update Spring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We need to keep Spring Framework, Spring Boot, and Spring Security versions updated together: - Spring Framework: 5.3.28→5.3.31 - Spring Boot: 2.7.13→2.7.18 - Spring Security: 5.7.9→5.7.11 --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index dc49dbdc1ac1..ae88ee0c3d27 100644 --- a/pom.xml +++ b/pom.xml @@ -19,9 +19,9 @@ 11 - 5.3.28 - 2.7.13 - 5.7.9 + 5.3.31 + 2.7.18 + 5.7.11 5.6.15.Final 6.2.5.Final 42.6.0 From fc6a9ca5cb2dec321c2ad67f722dcc6a2e894bc1 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 29 Nov 2023 10:01:27 +0300 Subject: [PATCH 079/118] pom.xml: update spotbugs and spotbugs-maven-plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update to latest versions: - spotbugs v4.1.2→v4.8.2 - spotbugs-maven-plugin v4.0.4→v4.8.2.0 These are not run in CI and seem to only run manually when asked, ie via maven: $ mvn spotbugs:spotbugs --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ae88ee0c3d27..83cde498336c 100644 --- a/pom.xml +++ b/pom.xml @@ -294,7 +294,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.0.4 + 4.8.2.0 Max Low @@ -304,7 +304,7 @@ com.github.spotbugs spotbugs - 4.1.2 + 4.8.2 From 4e071b2428d66d7350abc451598f75ba647e3394 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 07:07:13 +0000 Subject: [PATCH 080/118] Update dependency org.apache.ant:ant to v1.10.14 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 83cde498336c..dd61d0dc05cc 100644 --- a/pom.xml +++ b/pom.xml @@ -1294,7 +1294,7 @@ org.apache.ant ant - 1.10.13 + 1.10.14 org.apache.jena From 8809e98a18ba1980e99584b21ffb427ed70e028e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 07:24:40 +0000 Subject: [PATCH 081/118] Update json-path.version to v2.8.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dd61d0dc05cc..f96674c91e4e 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ - 2.6.0 + 2.8.0 7.9 From 710feb798d2ad2192f2941fd5beeede30b7d8498 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Wed, 29 Nov 2023 12:46:35 +0300 Subject: [PATCH 082/118] Update hamcrest to v2.2 Due to changes in hamcrest packaging we only need the main artifact now, but we add hamcrest-core (which is an empty pom) so it doesn't get pulled in by other deps. Last, the hamcrest docs recommend that we put hamcrest first so that we don't have dependency convergence issues from junit. See: https://hamcrest.org/JavaHamcrest/distributables --- dspace-api/pom.xml | 2 +- dspace-oai/pom.xml | 2 +- dspace-server-webapp/pom.xml | 2 +- dspace/modules/additions/pom.xml | 2 +- pom.xml | 16 ++++++++-------- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index d6f72723e6d3..f6b06f69edb7 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -528,7 +528,7 @@ org.hamcrest - hamcrest-all + hamcrest test diff --git a/dspace-oai/pom.xml b/dspace-oai/pom.xml index b900ebe88ded..db8b55c79b5d 100644 --- a/dspace-oai/pom.xml +++ b/dspace-oai/pom.xml @@ -156,7 +156,7 @@ org.hamcrest - hamcrest-all + hamcrest compile diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 1ea77347cd2b..7754c2876e1d 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -548,7 +548,7 @@ org.hamcrest - hamcrest-all + hamcrest test diff --git a/dspace/modules/additions/pom.xml b/dspace/modules/additions/pom.xml index 922e0f0fe51b..54d4dea25cde 100644 --- a/dspace/modules/additions/pom.xml +++ b/dspace/modules/additions/pom.xml @@ -273,7 +273,7 @@ org.hamcrest - hamcrest-core + hamcrest test diff --git a/pom.xml b/pom.xml index f96674c91e4e..c8ef69f5b21a 100644 --- a/pom.xml +++ b/pom.xml @@ -1619,21 +1619,21 @@ - junit - junit - 4.13.2 + org.hamcrest + hamcrest + 2.2 test org.hamcrest - hamcrest-all - 1.3 + hamcrest-core + 2.2 test - org.hamcrest - hamcrest-core - 1.3 + junit + junit + 4.13.2 test From ae12f1865fd18b5ba642bdd1852934c57dfbb49d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 07:37:42 +0000 Subject: [PATCH 083/118] Update dependency org.apache.maven.plugins:maven-checkstyle-plugin to v3.3.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c8ef69f5b21a..13b19b210ab6 100644 --- a/pom.xml +++ b/pom.xml @@ -257,7 +257,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.1.0 + 3.3.1 verify-style From 275757e6d46b9175b81748722114fcef9a62e1b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 09:53:22 +0000 Subject: [PATCH 084/118] Update dependency org.apache.maven.plugins:maven-assembly-plugin to v3.6.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 13b19b210ab6..e57094fd0df4 100644 --- a/pom.xml +++ b/pom.xml @@ -334,7 +334,7 @@ maven-assembly-plugin - 3.2.0 + 3.6.0 org.apache.maven.plugins From 2c1a45bc8840614690bacfbeb8fa4dfbe5b42d70 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 06:31:33 +0000 Subject: [PATCH 085/118] Update dependency org.webjars.bowergithub.twbs:bootstrap to v4.6.2 --- dspace-server-webapp/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 7754c2876e1d..441c6dd36b4a 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -406,7 +406,7 @@ org.webjars.bowergithub.twbs bootstrap - 4.6.1 + 4.6.2 From 6d0b5deb8e2553be8afc1f4892f5e74353b5fdc2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 06:32:41 +0000 Subject: [PATCH 086/118] Update dependency org.webjars.bowergithub.jquery:jquery-dist to v3.7.1 --- dspace-server-webapp/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 441c6dd36b4a..c31924f9a16f 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -360,7 +360,7 @@ org.webjars.bowergithub.jquery jquery-dist - 3.7.0 + 3.7.1 From 3f675d9cd5bddea7d31de39db44183b36941492c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 06:31:39 +0000 Subject: [PATCH 087/118] Update dependency org.apache.commons:commons-lang3 to v3.14.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e57094fd0df4..8567591a6ab9 100644 --- a/pom.xml +++ b/pom.xml @@ -1452,7 +1452,7 @@ org.apache.commons commons-lang3 - 3.12.0 + 3.14.0 From e76132b4d8c1984994bfb150e3a3abdcff6f69a8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 06:30:59 +0000 Subject: [PATCH 088/118] Update dependency commons-logging:commons-logging to v1.3.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8567591a6ab9..ea88db331d48 100644 --- a/pom.xml +++ b/pom.xml @@ -1459,7 +1459,7 @@ commons-logging commons-logging - 1.2 + 1.3.0 org.apache.commons From b583029a7d1e3b6a56f31ef2e086147ff005bab6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 09:42:09 +0000 Subject: [PATCH 089/118] Update dependency org.apache.commons:commons-configuration2 to v2.9.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ea88db331d48..a24befa3291e 100644 --- a/pom.xml +++ b/pom.xml @@ -1432,7 +1432,7 @@ org.apache.commons commons-configuration2 - 2.8.0 + 2.9.0 org.apache.commons From 6de85adeb7afa0d91ee0bf3273b8494d5b5d33c1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 09:42:44 +0000 Subject: [PATCH 090/118] Update dependency org.apache.commons:commons-collections4 to v4.4 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a24befa3291e..13db3920af7c 100644 --- a/pom.xml +++ b/pom.xml @@ -1419,7 +1419,7 @@ org.apache.commons commons-collections4 - 4.1 + 4.4 - 2.13.4 - 2.13.4.2 + 2.16.0 + 2.16.0 1.3.2 2.3.1 2.3.9 From bafb1b56570df3b4bfea045ccc913af0f0994cce Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Mon, 5 Feb 2024 17:41:40 +0300 Subject: [PATCH 092/118] pom.xml: update commons-dbcp2 and commons-pool2 Update commons-dbcp2 and commons-pool2 to latest stable versions. --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 5e8ff65fcfb3..383e3bd36af8 100644 --- a/pom.xml +++ b/pom.xml @@ -1437,7 +1437,7 @@ org.apache.commons commons-dbcp2 - 2.9.0 + 2.11.0 commons-fileupload @@ -1464,7 +1464,7 @@ org.apache.commons commons-pool2 - 2.11.1 + 2.12.0 org.apache.commons From 20fc8e4fb7c5deb81f66d926712da7fdbe25a687 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Mon, 5 Feb 2024 22:21:50 +0300 Subject: [PATCH 093/118] dspace-server-webapp/pom.xml: upgrade zjsonpatch Upgrade zjsonpatch from v0.4.6 to v0.4.16. --- dspace-server-webapp/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index c31924f9a16f..8e713bb6af8b 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -340,7 +340,7 @@ com.flipkart.zjsonpatch zjsonpatch - 0.4.14 + 0.4.16 From 5e9ad8123a63da4c68013b064e425506aff05b6f Mon Sep 17 00:00:00 2001 From: frabacche Date: Tue, 6 Feb 2024 14:50:51 +0100 Subject: [PATCH 094/118] CST-5249_suggestion move openaire base url from hardcoded to config --- .../service/OpenAireImportMetadataSourceServiceImpl.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java index 45737b8b22af..0e59c9eb0e42 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/openaire/service/OpenAireImportMetadataSourceServiceImpl.java @@ -28,6 +28,7 @@ import org.dspace.importer.external.metadatamapping.MetadatumDTO; import org.dspace.importer.external.service.AbstractImportMetadataSourceService; import org.dspace.importer.external.service.components.QuerySource; +import org.dspace.services.ConfigurationService; import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; @@ -36,6 +37,7 @@ import org.jdom2.input.SAXBuilder; import org.jdom2.xpath.XPathExpression; import org.jdom2.xpath.XPathFactory; +import org.springframework.beans.factory.annotation.Autowired; /** * Implements a data source for querying OpenAIRE @@ -45,6 +47,9 @@ public class OpenAireImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService implements QuerySource { + @Autowired(required = true) + protected ConfigurationService configurationService; + private String baseAddress; private WebTarget webTarget; @@ -182,7 +187,7 @@ public String getQueryParam() { public void init() throws Exception { Client client = ClientBuilder.newClient(); if (baseAddress == null) { - baseAddress = "http://api.openaire.eu/search/publications"; + baseAddress = configurationService.getProperty("openaire.base.url"); } if (queryParam == null) { queryParam = "title"; From 2f71ec229855eeb69e30cea9209a3abb9ecca654 Mon Sep 17 00:00:00 2001 From: frabacche Date: Tue, 6 Feb 2024 15:17:38 +0100 Subject: [PATCH 095/118] CST-5249_suggestion comment out openairecerif and openairefunding metadatas descriptions --- dspace/config/modules/openaire-client.cfg | 4 +++- dspace/config/spring/api/openaire-integration.xml | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/dspace/config/modules/openaire-client.cfg b/dspace/config/modules/openaire-client.cfg index b43ef781ee47..26d36fa816f5 100644 --- a/dspace/config/modules/openaire-client.cfg +++ b/dspace/config/modules/openaire-client.cfg @@ -32,4 +32,6 @@ openaire.token.clientId = CLIENT_ID_HERE openaire.token.clientSecret = CLIENT_SECRET_HERE # URL of Openaire Rest API -openaire.api.url = https://api.openaire.eu \ No newline at end of file +openaire.api.url = https://api.openaire.eu + +openaire.base.url = http://api.openaire.eu/search/publications \ No newline at end of file diff --git a/dspace/config/spring/api/openaire-integration.xml b/dspace/config/spring/api/openaire-integration.xml index fcf84474bd13..8e4becddc5cb 100644 --- a/dspace/config/spring/api/openaire-integration.xml +++ b/dspace/config/spring/api/openaire-integration.xml @@ -111,7 +111,7 @@ - + + - + From 69c81c95cecac3d519d66b8123a3d54d66783114 Mon Sep 17 00:00:00 2001 From: frabacche Date: Tue, 6 Feb 2024 15:27:58 +0100 Subject: [PATCH 096/118] CST-5249_suggestion improve import-openaire-suggestions bean description --- dspace/config/spring/api/scripts.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index 8bc46a099e6b..4a372d1c6d26 100644 --- a/dspace/config/spring/api/scripts.xml +++ b/dspace/config/spring/api/scripts.xml @@ -98,7 +98,7 @@ - + From c98f81e1114db5810848c38cd22a5251d5a96074 Mon Sep 17 00:00:00 2001 From: frabacche Date: Tue, 6 Feb 2024 15:47:56 +0100 Subject: [PATCH 097/118] CST-5249_suggestion fix docker.compose.yml type on suggestion config --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 74991f94a9fc..98f5af5eb3cc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -123,7 +123,7 @@ services: cp -r /opt/solr/server/solr/configsets/statistics/* statistics precreate-core qaevent /opt/solr/server/solr/configsets/qaevent cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent - precreate-core qaevent /opt/solr/server/solr/configsets/suggestion + precreate-core suggestion /opt/solr/server/solr/configsets/suggestion cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion exec solr -f volumes: From 56851b69a46c535a254f9b44484a4bc9ddbe595c Mon Sep 17 00:00:00 2001 From: frabacche Date: Tue, 6 Feb 2024 15:56:38 +0100 Subject: [PATCH 098/118] CST-5249_suggestion suggestion solr fix --- dspace/solr/suggestion/conf/solrconfig.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dspace/solr/suggestion/conf/solrconfig.xml b/dspace/solr/suggestion/conf/solrconfig.xml index ba2b42c981af..228b5f545e63 100644 --- a/dspace/solr/suggestion/conf/solrconfig.xml +++ b/dspace/solr/suggestion/conf/solrconfig.xml @@ -61,6 +61,11 @@ ${solr.autoSoftCommit.maxTime:-1} + + + + ${solr.ulog.dir:} + From db56de3887109404bb849413f5a02023ebb9bcae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 16:30:28 +0000 Subject: [PATCH 099/118] Bump com.jayway.jsonpath:json-path from 2.6.0 to 2.9.0 Bumps [com.jayway.jsonpath:json-path](https://github.com/jayway/JsonPath) from 2.6.0 to 2.9.0. - [Release notes](https://github.com/jayway/JsonPath/releases) - [Changelog](https://github.com/json-path/JsonPath/blob/master/changelog.md) - [Commits](https://github.com/jayway/JsonPath/compare/json-path-2.6.0...json-path-2.9.0) --- updated-dependencies: - dependency-name: com.jayway.jsonpath:json-path dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 383e3bd36af8..a202b9fd6732 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ - 2.8.0 + 2.9.0 7.9 From 40b7427d884002a0916ec4a3e687280e9426d081 Mon Sep 17 00:00:00 2001 From: Kim Shepherd Date: Wed, 7 Feb 2024 12:11:06 +1300 Subject: [PATCH 100/118] Improve default identifiers.cfg properties (Related to dspace-angular#2765) --- dspace/config/modules/identifiers.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dspace/config/modules/identifiers.cfg b/dspace/config/modules/identifiers.cfg index 63a9cda30f17..2660646af394 100644 --- a/dspace/config/modules/identifiers.cfg +++ b/dspace/config/modules/identifiers.cfg @@ -41,9 +41,7 @@ # Show Register DOI button in item status page? # Default: false -# This configuration property is exposed over rest. For dspace-angular to work, -# this property must always have a true or false value. Do not comment it out! -identifiers.item-status.register-doi = false +#identifiers.item-status.register-doi = true # Which identifier types to show in submission step? # Default: handle, doi (currently the only supported identifier 'types') From 5f522926341dab08fd4cad9afca88b6d4e66439d Mon Sep 17 00:00:00 2001 From: Agustina Martinez Date: Wed, 7 Feb 2024 19:39:16 +0000 Subject: [PATCH 101/118] Updated consistent description of Rioxx for context and transformer --- dspace/config/crosswalks/oai/xoai.xml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/dspace/config/crosswalks/oai/xoai.xml b/dspace/config/crosswalks/oai/xoai.xml index 9a4985d6674f..0f1cdf7d68cd 100644 --- a/dspace/config/crosswalks/oai/xoai.xml +++ b/dspace/config/crosswalks/oai/xoai.xml @@ -92,7 +92,7 @@ @@ -105,7 +105,7 @@ - This contexts complies with RIOXX rules. + This context complies with the Rioxx metadata profile version 3 @@ -203,9 +203,8 @@ http://namespace.openaire.eu/schema/oaire/ https://www.openaire.eu/schema/repo-lit/4.0/openaire.xsd - rioxx @@ -231,7 +230,7 @@ transformers/rioxx.xsl - RIOXX context transformer + Rioxx context transformer @@ -395,7 +394,7 @@ - @@ -507,7 +506,7 @@ - + org.dspace.xoai.filter.DSpaceMetadataExistsFilter @@ -652,7 +651,7 @@ rioxx - RIOXX set + Rioxx set From f7df6727c470619c501664ae476355a0b4c8ffc0 Mon Sep 17 00:00:00 2001 From: Agustina Martinez Date: Wed, 7 Feb 2024 22:25:57 +0000 Subject: [PATCH 102/118] Removed virtual-metadata.xml.rioxx3; virtual-metadata.xml.openaire4: fix populating funder info in fundingReference metadata --- .../config/entities/rioxx3-relationships.xml | 3 +- .../spring/api/virtual-metadata.xml.openaire4 | 6 +- .../spring/api/virtual-metadata.xml.rioxx3 | 402 ------------------ 3 files changed, 4 insertions(+), 407 deletions(-) delete mode 100644 dspace/config/spring/api/virtual-metadata.xml.rioxx3 diff --git a/dspace/config/entities/rioxx3-relationships.xml b/dspace/config/entities/rioxx3-relationships.xml index 6ed577f60374..558f766a5bb1 100644 --- a/dspace/config/entities/rioxx3-relationships.xml +++ b/dspace/config/entities/rioxx3-relationships.xml @@ -1,6 +1,7 @@ - + - - + - - + diff --git a/dspace/config/spring/api/virtual-metadata.xml.rioxx3 b/dspace/config/spring/api/virtual-metadata.xml.rioxx3 deleted file mode 100644 index 7f5c0e8ceae8..000000000000 --- a/dspace/config/spring/api/virtual-metadata.xml.rioxx3 +++ /dev/null @@ -1,402 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - person.familyName - person.givenName - organization.legalName - - - - , - - - - - - - - person.givenName - - - - - - - person.familyName - - - - - - - person.affiliation.name - - - - - - - person.identifier - - - - - - - person.identifier.orcid - - - - - - - person.identifier.isni - - - - - - - organization.legalName - - - - - - - organization.identifier - - - - - - - - - - - - organization.legalName - - - - - - - - - - - - person.familyName - person.givenName - - - - , - - - - - - - - - - - organization.legalName - - - - - - - - - - - organization.legalName - - - - - - - - - - - - - - publicationvolume.volumeNumber - - - - - - - - - - - - - - - - - - - - - - creativeworkseries.issn - - - - - - - dc.title - - - - - - - - - - - - - - publicationissue.issueNumber - - - - - - - - - - - - - - dc.title - - - - , - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - dc.title - - - - - - - oaire.fundingStream - - - - - - - dc.identifier - - - - - - - dc.identifier.uri - - - - - - - - - - - - - - - - - - - - - - - - - organization.legalName - - - - - - - organization.identifier - - - - From 07e3de9452fd715455eff3cbda4b9e0278b39327 Mon Sep 17 00:00:00 2001 From: Stefano Maffei Date: Fri, 9 Feb 2024 08:41:25 +0100 Subject: [PATCH 103/118] [CST-5249] fix issue with missing filter when running query for researchers --- .../app/suggestion/openaire/PublicationLoaderRunnable.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java index 575b53e55a60..a408c448e9f5 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java @@ -97,15 +97,14 @@ private Iterator getResearchers(String profileUUID) { SearchService searchService = new DSpace().getSingletonService(SearchService.class); DiscoverQueryBuilder queryBuilder = SearchUtils.getQueryBuilder(); List filters = new ArrayList(); + String query = "*:*"; if (profileUUID != null) { - QueryBuilderSearchFilter queryBuilderSearchFilter = - new QueryBuilderSearchFilter("search.resourceid", "equals", profileUUID.toString()); - filters.add(queryBuilderSearchFilter); + query = "search.resourceid:" + profileUUID.toString(); } try { DiscoverQuery discoverQuery = queryBuilder.buildQuery(context, null, SearchUtils.getDiscoveryConfigurationByName("person"), - "*:*", filters, + query, filters, "Item", 10, Long.getLong("0"), null, SortOption.DESCENDING); return searchService.iteratorSearch(context, null, discoverQuery); } catch (SearchServiceException e) { From e0c613ab00357ec99596f091765845c0d288bdbc Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Wed, 3 Jan 2024 12:59:48 +0100 Subject: [PATCH 104/118] Map item submission process at community level --- .../org/dspace/app/util/DCInputsReader.java | 10 +- .../app/util/SubmissionConfigReader.java | 84 ++++++++-- .../main/java/org/dspace/app/util/Util.java | 14 +- .../authority/ChoiceAuthorityServiceImpl.java | 4 +- .../ctask/general/RequiredMetadata.java | 9 +- .../service/SubmissionConfigService.java | 2 +- .../service/SubmissionConfigServiceImpl.java | 4 +- .../dspaceFolder/config/item-submission.xml | 10 ++ .../dspace/app/util/SubmissionConfigTest.java | 55 ++++++- .../converter/AInprogressItemConverter.java | 2 +- .../SubmissionDefinitionRestRepository.java | 2 +- .../WorkspaceItemRestRepository.java | 2 +- .../SubmissionDefinitionsControllerIT.java | 146 +++++++++++++++--- dspace/config/item-submission.dtd | 1 + dspace/config/item-submission.xml | 9 ++ 15 files changed, 289 insertions(+), 65 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java index 38692c73a6ce..8dc8239ca507 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInputsReader.java @@ -14,7 +14,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import javax.servlet.ServletException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; @@ -150,17 +149,16 @@ public List getPairs(String name) { * Returns the set of DC inputs used for a particular collection, or the * default set if no inputs defined for the collection * - * @param collectionHandle collection's unique Handle + * @param collection collection for which search the set of DC inputs * @return DC input set * @throws DCInputsReaderException if no default set defined - * @throws ServletException */ - public List getInputsByCollectionHandle(String collectionHandle) + public List getInputsByCollection(Collection collection) throws DCInputsReaderException { SubmissionConfig config; try { config = SubmissionServiceFactory.getInstance().getSubmissionConfigService() - .getSubmissionConfigByCollection(collectionHandle); + .getSubmissionConfigByCollection(collection); String formName = config.getSubmissionName(); if (formName == null) { throw new DCInputsReaderException("No form designated as default"); @@ -691,7 +689,7 @@ private String getValue(Node nd) { public String getInputFormNameByCollectionAndField(Collection collection, String field) throws DCInputsReaderException { - List inputSets = getInputsByCollectionHandle(collection.getHandle()); + List inputSets = getInputsByCollection(collection); for (DCInputSet inputSet : inputSets) { String[] tokenized = Utils.tokenize(field); String schema = tokenized[0]; diff --git a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java index 0f144fd69f46..b46a00793150 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java @@ -11,6 +11,7 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -21,6 +22,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.dspace.content.Collection; +import org.dspace.content.Community; import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.service.CollectionService; @@ -90,6 +92,13 @@ public class SubmissionConfigReader { */ private Map collectionToSubmissionConfig = null; + /** + * Hashmap which stores which submission process configuration is used by + * which community, computed from the item submission config file + * (specifically, the 'submission-map' tag) + */ + private Map communityToSubmissionConfig = null; + /** * Reference to the global submission step definitions defined in the * "step-definitions" section @@ -127,6 +136,7 @@ public SubmissionConfigReader() throws SubmissionConfigReaderException { public void reload() throws SubmissionConfigReaderException { collectionToSubmissionConfig = null; + communityToSubmissionConfig = null; stepDefns = null; submitDefns = null; buildInputs(configDir + SUBMIT_DEF_FILE_PREFIX + SUBMIT_DEF_FILE_SUFFIX); @@ -145,7 +155,8 @@ public void reload() throws SubmissionConfigReaderException { */ private void buildInputs(String fileName) throws SubmissionConfigReaderException { collectionToSubmissionConfig = new HashMap(); - submitDefns = new HashMap>>(); + communityToSubmissionConfig = new HashMap(); + submitDefns = new LinkedHashMap>>(); String uri = "file:" + new File(fileName).getAbsolutePath(); @@ -210,18 +221,39 @@ public int countSubmissionConfigs() { * Returns the Item Submission process config used for a particular * collection, or the default if none is defined for the collection * - * @param collectionHandle collection's unique Handle + * @param col collection for which search Submission process config * @return the SubmissionConfig representing the item submission config - * @throws SubmissionConfigReaderException if no default submission process configuration defined + * @throws IllegalStateException if no default submission process configuration defined */ - public SubmissionConfig getSubmissionConfigByCollection(String collectionHandle) { - // get the name of the submission process config for this collection - String submitName = collectionToSubmissionConfig - .get(collectionHandle); - if (submitName == null) { + public SubmissionConfig getSubmissionConfigByCollection(Collection col) { + + String submitName; + + if (col != null) { + + // get the name of the submission process config for this collection submitName = collectionToSubmissionConfig - .get(DEFAULT_COLLECTION); + .get(col.getHandle()); + if (submitName != null) { + return getSubmissionConfigByName(submitName); + } + + try { + List communities = col.getCommunities(); + for (Community com : communities) { + submitName = getSubmissionConfigByCommunity(com); + if (submitName != null) { + return getSubmissionConfigByName(submitName); + } + } + } catch (SQLException sqle) { + throw new IllegalStateException("Error occurred while getting item submission configured " + + "by community", sqle); + } } + + submitName = collectionToSubmissionConfig.get(DEFAULT_COLLECTION); + if (submitName == null) { throw new IllegalStateException( "No item submission process configuration designated as 'default' in 'submission-map' section of " + @@ -230,6 +262,30 @@ public SubmissionConfig getSubmissionConfigByCollection(String collectionHandle) return getSubmissionConfigByName(submitName); } + /** + * Recursive function to return the Item Submission process config + * used for a community or the closest community parent, or null + * if none is defined + * + * @param com community for which search Submission process config + * @return the SubmissionConfig representing the item submission config + */ + private String getSubmissionConfigByCommunity(Community com) { + String submitName = communityToSubmissionConfig + .get(com.getHandle()); + if (submitName != null) { + return submitName; + } + List communities = com.getParentCommunities(); + for (Community parentCom : communities) { + submitName = getSubmissionConfigByCommunity(parentCom); + if (submitName != null) { + return submitName; + } + } + return null; + } + /** * Returns the Item Submission process config * @@ -357,13 +413,14 @@ private void processMap(Node e) throws SAXException, SearchServiceException { Node nd = nl.item(i); if (nd.getNodeName().equals("name-map")) { String id = getAttribute(nd, "collection-handle"); + String communityId = getAttribute(nd, "community-handle"); String entityType = getAttribute(nd, "collection-entity-type"); String value = getAttribute(nd, "submission-name"); String content = getValue(nd); - if (id == null && entityType == null) { + if (id == null && communityId == null && entityType == null) { throw new SAXException( - "name-map element is missing collection-handle or collection-entity-type attribute " + - "in 'item-submission.xml'"); + "name-map element is missing collection-handle or community-handle or collection-entity-type " + + "attribute in 'item-submission.xml'"); } if (value == null) { throw new SAXException( @@ -375,7 +432,8 @@ private void processMap(Node e) throws SAXException, SearchServiceException { } if (id != null) { collectionToSubmissionConfig.put(id, value); - + } else if (communityId != null) { + communityToSubmissionConfig.put(communityId, value); } else { // get all collections for this entity-type List collections = collectionService.findAllCollectionsByEntityType( context, diff --git a/dspace-api/src/main/java/org/dspace/app/util/Util.java b/dspace-api/src/main/java/org/dspace/app/util/Util.java index f8ef3b1731f7..3bc828d6c496 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/Util.java +++ b/dspace-api/src/main/java/org/dspace/app/util/Util.java @@ -405,21 +405,13 @@ public static List getControlledVocabulariesDisplayValueLocalized( DCInput myInputs = null; boolean myInputsFound = false; String formFileName = I18nUtil.getInputFormsFileName(locale); - String col_handle = ""; Collection collection = item.getOwningCollection(); - if (collection == null) { - // set an empty handle so to get the default input set - col_handle = ""; - } else { - col_handle = collection.getHandle(); - } - // Read the input form file for the specific collection DCInputsReader inputsReader = new DCInputsReader(formFileName); - List inputSets = inputsReader.getInputsByCollectionHandle(col_handle); + List inputSets = inputsReader.getInputsByCollection(collection); // Replace the values of Metadatum[] with the correct ones in case // of @@ -500,8 +492,8 @@ public static List[] splitList(List idsList, int i) { public static List differenceInSubmissionFields(Collection fromCollection, Collection toCollection) throws DCInputsReaderException { DCInputsReader reader = new DCInputsReader(); - List from = reader.getInputsByCollectionHandle(fromCollection.getHandle()); - List to = reader.getInputsByCollectionHandle(toCollection.getHandle()); + List from = reader.getInputsByCollection(fromCollection); + List to = reader.getInputsByCollection(toCollection); Set fromFieldName = new HashSet<>(); Set toFieldName = new HashSet<>(); diff --git a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java index 34ba9e8c4550..f4d1f02710e1 100644 --- a/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/authority/ChoiceAuthorityServiceImpl.java @@ -242,7 +242,7 @@ public String getChoiceAuthorityName(String schema, String element, String quali // check if it is the requested collection Map controllerFormDef = controllerFormDefinitions.get(fieldKey); SubmissionConfig submissionConfig = submissionConfigService - .getSubmissionConfigByCollection(collection.getHandle()); + .getSubmissionConfigByCollection(collection); String submissionName = submissionConfig.getSubmissionName(); // check if the requested collection has a submission definition that use an authority for the metadata if (controllerFormDef.containsKey(submissionName)) { @@ -495,7 +495,7 @@ private ChoiceAuthority getAuthorityByFieldKeyCollection(String fieldKey, Collec try { configReaderService = SubmissionServiceFactory.getInstance().getSubmissionConfigService(); SubmissionConfig submissionName = configReaderService - .getSubmissionConfigByCollection(collection.getHandle()); + .getSubmissionConfigByCollection(collection); ma = controllerFormDefinitions.get(fieldKey).get(submissionName.getSubmissionName()); } catch (SubmissionConfigReaderException e) { // the system is in an illegal state as the submission definition is not valid diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/RequiredMetadata.java b/dspace-api/src/main/java/org/dspace/ctask/general/RequiredMetadata.java index 07bfed5fe572..2899e3f6bdd6 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/RequiredMetadata.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/RequiredMetadata.java @@ -17,6 +17,7 @@ import org.dspace.app.util.DCInputSet; import org.dspace.app.util.DCInputsReader; import org.dspace.app.util.DCInputsReaderException; +import org.dspace.content.Collection; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.MetadataValue; @@ -69,7 +70,7 @@ public int perform(DSpaceObject dso) throws IOException { handle = "in workflow"; } sb.append("Item: ").append(handle); - for (String req : getReqList(item.getOwningCollection().getHandle())) { + for (String req : getReqList(item.getOwningCollection())) { List vals = itemService.getMetadataByMetadataString(item, req); if (vals.size() == 0) { sb.append(" missing required field: ").append(req); @@ -91,14 +92,14 @@ public int perform(DSpaceObject dso) throws IOException { } } - protected List getReqList(String handle) throws DCInputsReaderException { - List reqList = reqMap.get(handle); + protected List getReqList(Collection collection) throws DCInputsReaderException { + List reqList = reqMap.get(collection.getHandle()); if (reqList == null) { reqList = reqMap.get("default"); } if (reqList == null) { reqList = new ArrayList(); - List inputSet = reader.getInputsByCollectionHandle(handle); + List inputSet = reader.getInputsByCollection(collection); for (DCInputSet inputs : inputSet) { for (DCInput[] row : inputs.getFields()) { for (DCInput input : row) { diff --git a/dspace-api/src/main/java/org/dspace/submit/service/SubmissionConfigService.java b/dspace-api/src/main/java/org/dspace/submit/service/SubmissionConfigService.java index c4b111a38f7e..36ba82f60d53 100644 --- a/dspace-api/src/main/java/org/dspace/submit/service/SubmissionConfigService.java +++ b/dspace-api/src/main/java/org/dspace/submit/service/SubmissionConfigService.java @@ -34,7 +34,7 @@ public interface SubmissionConfigService { public int countSubmissionConfigs(); - public SubmissionConfig getSubmissionConfigByCollection(String collectionHandle); + public SubmissionConfig getSubmissionConfigByCollection(Collection collection); public SubmissionConfig getSubmissionConfigByName(String submitName); diff --git a/dspace-api/src/main/java/org/dspace/submit/service/SubmissionConfigServiceImpl.java b/dspace-api/src/main/java/org/dspace/submit/service/SubmissionConfigServiceImpl.java index a72bcc2c3bf9..fe063954a1d3 100644 --- a/dspace-api/src/main/java/org/dspace/submit/service/SubmissionConfigServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/submit/service/SubmissionConfigServiceImpl.java @@ -57,8 +57,8 @@ public int countSubmissionConfigs() { } @Override - public SubmissionConfig getSubmissionConfigByCollection(String collectionHandle) { - return submissionConfigReader.getSubmissionConfigByCollection(collectionHandle); + public SubmissionConfig getSubmissionConfigByCollection(Collection collection) { + return submissionConfigReader.getSubmissionConfigByCollection(collection); } @Override diff --git a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index 452460501a54..bab5aa0f78ff 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -24,6 +24,8 @@ + + @@ -257,6 +259,14 @@ + + + + + + + + diff --git a/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigTest.java b/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigTest.java index cb1f828b93c4..b954c2f664d4 100644 --- a/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigTest.java +++ b/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigTest.java @@ -9,17 +9,22 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; +import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import org.dspace.AbstractUnitTest; +import org.dspace.content.Collection; +import org.dspace.content.Community; import org.dspace.submit.factory.SubmissionServiceFactory; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mock; /** * Tests for parsing and utilities on submission config forms / readers @@ -30,6 +35,21 @@ public class SubmissionConfigTest extends AbstractUnitTest { DCInputsReader inputReader; + @Mock + Community topcom; + + @Mock + Community subcom1; + + @Mock + Community subcom2; + + @Mock + private Collection col1; + + @Mock + private Collection col2; + @BeforeClass public static void setUpClass() { } @@ -56,6 +76,8 @@ public void testReadAndProcessTypeBindSubmissionConfig() String typeBindSubmissionName = "typebindtest"; String typeBindSubmissionStepName = "typebindtest"; + when(col1.getHandle()).thenReturn(typeBindHandle); + // Expected field lists from typebindtest form List allConfiguredFields = new ArrayList<>(); allConfiguredFields.add("dc.title"); @@ -67,7 +89,7 @@ public void testReadAndProcessTypeBindSubmissionConfig() // Get submission configuration SubmissionConfig submissionConfig = SubmissionServiceFactory.getInstance().getSubmissionConfigService() - .getSubmissionConfigByCollection(typeBindHandle); + .getSubmissionConfigByCollection(col1); // Submission name should match name defined in item-submission.xml assertEquals(typeBindSubmissionName, submissionConfig.getSubmissionName()); // Step 0 - our process only has one step. It should not be null and have the ID typebindtest @@ -87,4 +109,35 @@ public void testReadAndProcessTypeBindSubmissionConfig() assertEquals(unboundFields, allowedFieldsForArticle); assertEquals(unboundFields, allowedFieldsForNoType); } + + @Test + public void testSubmissionMapByCommunityHandleSubmissionConfig() + throws SubmissionConfigReaderException, DCInputsReaderException, SQLException { + + // Sep up a structure with one top community and two subcommunities + // with one collection + when(col1.getHandle()).thenReturn("123456789/not-mapped1"); + when(col1.getCommunities()).thenReturn(List.of(subcom1)); + + when(col2.getHandle()).thenReturn("123456789/not-mapped2"); + when(col2.getCommunities()).thenReturn(List.of(subcom2)); + + when(subcom1.getHandle()).thenReturn("123456789/subcommunity-test"); + + when(subcom2.getParentCommunities()).thenReturn(List.of(topcom)); + when(subcom2.getHandle()).thenReturn("123456789/not-mapped3"); + + when(topcom.getHandle()).thenReturn("123456789/topcommunity-test"); + + // for col1, it should return the item submission form defined for their parent subcom1 + SubmissionConfig submissionConfig1 = + new SubmissionConfigReader().getSubmissionConfigByCollection(col1); + assertEquals("subcommunitytest", submissionConfig1.getSubmissionName()); + + // for col2, it should return the item submission form defined for topcom + SubmissionConfig submissionConfig2 = + new SubmissionConfigReader().getSubmissionConfigByCollection(col2); + assertEquals("topcommunitytest", submissionConfig2.getSubmissionName()); + + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java index a5431d90004f..a80c8bd948b9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/AInprogressItemConverter.java @@ -82,7 +82,7 @@ protected void fillFromModel(T obj, R witem, Projection projection) { if (collection != null) { SubmissionDefinitionRest def = converter.toRest( - submissionConfigService.getSubmissionConfigByCollection(collection.getHandle()), projection); + submissionConfigService.getSubmissionConfigByCollection(collection), projection); witem.setSubmissionDefinition(def); for (SubmissionSectionRest sections : def.getPanels()) { SubmissionStepConfig stepConfig = submissionSectionConverter.toModel(sections); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionDefinitionRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionDefinitionRestRepository.java index d964994928eb..17eb90b7901e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionDefinitionRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionDefinitionRestRepository.java @@ -70,7 +70,7 @@ public SubmissionDefinitionRest findByCollection(@Parameter(value = "uuid", requ return null; } SubmissionDefinitionRest def = converter - .toRest(submissionConfigService.getSubmissionConfigByCollection(col.getHandle()), + .toRest(submissionConfigService.getSubmissionConfigByCollection(col), utils.obtainProjection()); return def; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java index b4d04e59e32f..5f4bb0dfe927 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkspaceItemRestRepository.java @@ -251,7 +251,7 @@ public Iterable upload(Context context, HttpServletRequest re } SubmissionConfig submissionConfig = - submissionConfigService.getSubmissionConfigByCollection(collection.getHandle()); + submissionConfigService.getSubmissionConfigByCollection(collection); List result = null; List records = new ArrayList<>(); try { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index babb1fac2326..70db05ef0919 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -91,6 +91,7 @@ public void findDefault() throws Exception { //The status has to be 200 OK .andExpect(status().isOk()) .andExpect(jsonPath("$", SubmissionDefinitionsMatcher.matchFullEmbeds())) + .andExpect(jsonPath("$", SubmissionDefinitionsMatcher.matchLinks())) //We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) @@ -257,10 +258,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=1"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=6"), Matchers.containsString("size=1")))) + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(7))) - .andExpect(jsonPath("$.page.totalPages", is(7))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) .andExpect(jsonPath("$.page.number", is(0))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -268,7 +269,7 @@ public void findAllPaginationTest() throws Exception { .param("page", "1")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("test-hidden"))) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("languagetestprocess"))) .andExpect(jsonPath("$._links.first.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=0"), Matchers.containsString("size=1")))) @@ -283,10 +284,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=1"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page="), Matchers.containsString("size=1")))) + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(7))) - .andExpect(jsonPath("$.page.totalPages", is(7))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) .andExpect(jsonPath("$.page.number", is(1))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -294,7 +295,7 @@ public void findAllPaginationTest() throws Exception { .param("page", "2")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("accessConditionNotDiscoverable"))) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("extractiontestprocess"))) .andExpect(jsonPath("$._links.first.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=0"), Matchers.containsString("size=1")))) @@ -309,10 +310,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=2"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=6"), Matchers.containsString("size=1")))) + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(7))) - .andExpect(jsonPath("$.page.totalPages", is(7))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) .andExpect(jsonPath("$.page.number", is(2))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -320,7 +321,7 @@ public void findAllPaginationTest() throws Exception { .param("page", "3")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("languagetestprocess"))) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("qualdroptest"))) .andExpect(jsonPath("$._links.first.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=0"), Matchers.containsString("size=1")))) @@ -335,10 +336,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=3"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=6"), Matchers.containsString("size=1")))) + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(7))) - .andExpect(jsonPath("$.page.totalPages", is(7))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) .andExpect(jsonPath("$.page.number", is(3))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -346,7 +347,7 @@ public void findAllPaginationTest() throws Exception { .param("page", "4")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("qualdroptest"))) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("typebindtest"))) .andExpect(jsonPath("$._links.first.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=0"), Matchers.containsString("size=1")))) @@ -361,10 +362,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=4"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=6"), Matchers.containsString("size=1")))) + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(7))) - .andExpect(jsonPath("$.page.totalPages", is(7))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) .andExpect(jsonPath("$.page.number", is(4))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -372,7 +373,7 @@ public void findAllPaginationTest() throws Exception { .param("page", "5")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("extractiontestprocess"))) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("accessConditionNotDiscoverable"))) .andExpect(jsonPath("$._links.first.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=0"), Matchers.containsString("size=1")))) @@ -386,12 +387,113 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=5"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.number", is(5))); + + getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") + .param("size", "1") + .param("page", "5")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("accessConditionNotDiscoverable"))) + .andExpect(jsonPath("$._links.first.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=4"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.next.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=6"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=5"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(7))) - .andExpect(jsonPath("$.page.totalPages", is(7))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) .andExpect(jsonPath("$.page.number", is(5))); + + getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") + .param("size", "1") + .param("page", "6")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("test-hidden"))) + .andExpect(jsonPath("$._links.first.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=5"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.next.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=7"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=6"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.number", is(6))); + + getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") + .param("size", "1") + .param("page", "7")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("topcommunitytest"))) + .andExpect(jsonPath("$._links.first.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=6"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.next.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=7"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.number", is(7))); + + getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") + .param("size", "1") + .param("page", "8")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("subcommunitytest"))) + .andExpect(jsonPath("$._links.first.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=7"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.totalElements", is(9))) + .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.number", is(8))); } } diff --git a/dspace/config/item-submission.dtd b/dspace/config/item-submission.dtd index dd1afa0dd02d..01da4df8088f 100644 --- a/dspace/config/item-submission.dtd +++ b/dspace/config/item-submission.dtd @@ -12,6 +12,7 @@ diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index e5175c3c4e21..eb952513f598 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -63,6 +63,15 @@ + + + From f098de7417fb25c3ef2f02366b5f2d92750147ff Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Wed, 7 Feb 2024 18:22:02 +0100 Subject: [PATCH 105/118] Skip traversing the com/coll hierarchy if no community mapped item submission forms are defined --- .../app/util/SubmissionConfigReader.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java index b46a00793150..1c91b40b9735 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java +++ b/dspace-api/src/main/java/org/dspace/app/util/SubmissionConfigReader.java @@ -238,17 +238,19 @@ public SubmissionConfig getSubmissionConfigByCollection(Collection col) { return getSubmissionConfigByName(submitName); } - try { - List communities = col.getCommunities(); - for (Community com : communities) { - submitName = getSubmissionConfigByCommunity(com); - if (submitName != null) { - return getSubmissionConfigByName(submitName); + if (!communityToSubmissionConfig.isEmpty()) { + try { + List communities = col.getCommunities(); + for (Community com : communities) { + submitName = getSubmissionConfigByCommunity(com); + if (submitName != null) { + return getSubmissionConfigByName(submitName); + } } + } catch (SQLException sqle) { + throw new IllegalStateException("Error occurred while getting item submission configured " + + "by community", sqle); } - } catch (SQLException sqle) { - throw new IllegalStateException("Error occurred while getting item submission configured " + - "by community", sqle); } } From 27dc6b249cd9368ebf73a149729e44187e95f054 Mon Sep 17 00:00:00 2001 From: frabacche Date: Fri, 9 Feb 2024 11:38:48 +0100 Subject: [PATCH 106/118] CST-5249_suggestion publication loader import batch - pagination for ExternalObjectData load --- .../openaire/PublicationLoader.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoader.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoader.java index c04ccfa849e0..7ad723af123c 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoader.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoader.java @@ -105,10 +105,22 @@ public List reduceAndTransform(Item researcher, List metadata = getImportRecords(researcher); - List records = reduceAndTransform(researcher, metadata); - for (Suggestion record : records) { - solrSuggestionStorageService.addSuggestion(record, false, false); + int offset = 0; + int limit = 10; + int loaded = limit; + List searchValues = searchMetadataValues(researcher); + while (loaded > 0) { + List metadata = getImportRecords(searchValues, researcher, offset, limit); + if (metadata.isEmpty()) { + loaded = 0; + continue; + } + offset += limit; + loaded = metadata.size(); + List records = reduceAndTransform(researcher, metadata); + for (Suggestion record : records) { + solrSuggestionStorageService.addSuggestion(record, false, false); + } } solrSuggestionStorageService.commit(); } @@ -154,14 +166,18 @@ public void setNames(List names) { * get from metadata key defined in class level variable names as author to query OpenAIRE. * * @see org.dspace.importer.external.openaire.service.OpenAireImportMetadataSourceServiceImpl + * @param searchValues query * @param researcher item to extract metadata from + * @param limit for pagination purpose + * @param offset for pagination purpose * @return list of ImportRecord */ - private List getImportRecords(Item researcher) { - List searchValues = searchMetadataValues(researcher); + private List getImportRecords(List searchValues, + Item researcher, int offset, int limit) { List matchingRecords = new ArrayList<>(); for (String searchValue : searchValues) { - matchingRecords.addAll(primaryProvider.searchExternalDataObjects(searchValue, 0, 9999)); + matchingRecords.addAll( + primaryProvider.searchExternalDataObjects(searchValue, offset, limit)); } List toReturn = removeDuplicates(matchingRecords); return toReturn; From 9e2f69fd1df98e7eaa3ec3769aca14bb810cdac3 Mon Sep 17 00:00:00 2001 From: frabacche Date: Fri, 9 Feb 2024 11:43:37 +0100 Subject: [PATCH 107/118] CST-5249_suggestion useless modification rollbacked on RestResourceController.java --- .../dspace/app/rest/RestResourceController.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java index 59782fd44b64..03bea35597ba 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java @@ -1118,6 +1118,13 @@ public ResponseEntity> delete(HttpServletRequest request, return deleteInternal(apiCategory, model, uuid); } + @RequestMapping(method = RequestMethod.DELETE, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG) + public ResponseEntity> delete(HttpServletRequest request, @PathVariable String apiCategory, + @PathVariable String model, @PathVariable String id) + throws HttpRequestMethodNotSupportedException { + return deleteInternal(apiCategory, model, id); + } + /** * Internal method to delete resource. * @@ -1135,13 +1142,6 @@ private ResponseEntity> deleteI return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); } - @RequestMapping(method = RequestMethod.DELETE, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG) - public ResponseEntity> delete(HttpServletRequest request, @PathVariable String apiCategory, - @PathVariable String model, @PathVariable String id) - throws HttpRequestMethodNotSupportedException { - return deleteInternal(apiCategory, model, id); - } - /** * Execute a PUT request for an entity with id of type UUID; * From 9d3dc68011f092ffa0bea6cc87268e3130f0bf4e Mon Sep 17 00:00:00 2001 From: frabacche Date: Fri, 9 Feb 2024 15:29:05 +0100 Subject: [PATCH 108/118] CST-5249_suggestion correct the metadata names used by the datescorer --- dspace/config/registries/schema-person-types.xml | 14 ++++++++++++++ dspace/config/spring/api/suggestions.xml | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/dspace/config/registries/schema-person-types.xml b/dspace/config/registries/schema-person-types.xml index 0a40060e5101..cbe9035c23dd 100644 --- a/dspace/config/registries/schema-person-types.xml +++ b/dspace/config/registries/schema-person-types.xml @@ -155,5 +155,19 @@ variant Full name variant + + + person + range + maxdate + Last year in which the person is likely to be a contributor + + + + person + range + mindate + First year in which the person is likely to be a contributor + \ No newline at end of file diff --git a/dspace/config/spring/api/suggestions.xml b/dspace/config/spring/api/suggestions.xml index 77e0807fa2d4..3618d820e8a1 100644 --- a/dspace/config/spring/api/suggestions.xml +++ b/dspace/config/spring/api/suggestions.xml @@ -52,8 +52,9 @@ - + + From 438f5223cb0c685a3d1d06267fcdc0482402cea0 Mon Sep 17 00:00:00 2001 From: Toni Prieto Date: Wed, 7 Feb 2024 18:29:59 +0100 Subject: [PATCH 109/118] Convert the testSubmissionMapByCommunityHandleSubmissionConfig test into an integration test and add a new check to ensure that collection-handle mapping has a higher priority --- .../dspaceFolder/config/item-submission.xml | 5 ++ .../dspace/app/util/SubmissionConfigIT.java | 73 ++++++++++++++++ .../dspace/app/util/SubmissionConfigTest.java | 45 ---------- .../SubmissionDefinitionsControllerIT.java | 86 ++++++++++++------- 4 files changed, 134 insertions(+), 75 deletions(-) create mode 100644 dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigIT.java diff --git a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index bab5aa0f78ff..921306ca2b56 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -26,6 +26,7 @@ + @@ -267,6 +268,10 @@ + + + + diff --git a/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigIT.java b/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigIT.java new file mode 100644 index 000000000000..0db4926d2283 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigIT.java @@ -0,0 +1,73 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.util; + +import static org.junit.Assert.assertEquals; + +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.submit.factory.SubmissionServiceFactory; +import org.dspace.submit.service.SubmissionConfigService; +import org.junit.Test; + +/** + * Integration Tests for parsing and utilities on submission config forms / readers + * + * @author Toni Prieto + */ +public class SubmissionConfigIT extends AbstractIntegrationTestWithDatabase { + + @Test + public void testSubmissionMapByCommunityHandleSubmissionConfig() + throws SubmissionConfigReaderException { + + context.turnOffAuthorisationSystem(); + // Sep up a structure with one top community and two subcommunities with collections + Community topcom = CommunityBuilder.createCommunity(context, "123456789/topcommunity-test") + .withName("Parent Community") + .build(); + Community subcom1 = CommunityBuilder.createSubCommunity(context, topcom, "123456789/subcommunity-test") + .withName("Subcommunity 1") + .build(); + Community subcom2 = CommunityBuilder.createSubCommunity(context, topcom, "123456789/not-mapped3") + .withName("Subcommunity 2") + .build(); + // col1 should use the form item submission form mapped for subcom1 + Collection col1 = CollectionBuilder.createCollection(context, subcom1, "123456789/not-mapped1") + .withName("Collection 1") + .build(); + // col2 should use the item submission form mapped for the top community + Collection col2 = CollectionBuilder.createCollection(context, subcom2, "123456789/not-mapped2") + .withName("Collection 2") + .build(); + // col3 should use the item submission form directly mapped for this collection + Collection col3 = CollectionBuilder.createCollection(context, subcom1, "123456789/collection-test") + .withName("Collection 3") + .build(); + context.restoreAuthSystemState(); + + SubmissionConfigService submissionConfigService = SubmissionServiceFactory.getInstance() + .getSubmissionConfigService(); + + // for col1, it should return the item submission form defined for their parent subcom1 + SubmissionConfig submissionConfig1 = submissionConfigService.getSubmissionConfigByCollection(col1); + assertEquals("subcommunitytest", submissionConfig1.getSubmissionName()); + + // for col2, it should return the item submission form defined for topcom + SubmissionConfig submissionConfig2 = submissionConfigService.getSubmissionConfigByCollection(col2); + assertEquals("topcommunitytest", submissionConfig2.getSubmissionName()); + + // for col3, it should return the item submission form defined directly for the collection + SubmissionConfig submissionConfig3 = submissionConfigService.getSubmissionConfigByCollection(col3); + assertEquals("collectiontest", submissionConfig3.getSubmissionName()); + + } +} diff --git a/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigTest.java b/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigTest.java index b954c2f664d4..4ac193109875 100644 --- a/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigTest.java +++ b/dspace-api/src/test/java/org/dspace/app/util/SubmissionConfigTest.java @@ -11,13 +11,11 @@ import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.when; -import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import org.dspace.AbstractUnitTest; import org.dspace.content.Collection; -import org.dspace.content.Community; import org.dspace.submit.factory.SubmissionServiceFactory; import org.junit.After; import org.junit.AfterClass; @@ -35,21 +33,9 @@ public class SubmissionConfigTest extends AbstractUnitTest { DCInputsReader inputReader; - @Mock - Community topcom; - - @Mock - Community subcom1; - - @Mock - Community subcom2; - @Mock private Collection col1; - @Mock - private Collection col2; - @BeforeClass public static void setUpClass() { } @@ -109,35 +95,4 @@ public void testReadAndProcessTypeBindSubmissionConfig() assertEquals(unboundFields, allowedFieldsForArticle); assertEquals(unboundFields, allowedFieldsForNoType); } - - @Test - public void testSubmissionMapByCommunityHandleSubmissionConfig() - throws SubmissionConfigReaderException, DCInputsReaderException, SQLException { - - // Sep up a structure with one top community and two subcommunities - // with one collection - when(col1.getHandle()).thenReturn("123456789/not-mapped1"); - when(col1.getCommunities()).thenReturn(List.of(subcom1)); - - when(col2.getHandle()).thenReturn("123456789/not-mapped2"); - when(col2.getCommunities()).thenReturn(List.of(subcom2)); - - when(subcom1.getHandle()).thenReturn("123456789/subcommunity-test"); - - when(subcom2.getParentCommunities()).thenReturn(List.of(topcom)); - when(subcom2.getHandle()).thenReturn("123456789/not-mapped3"); - - when(topcom.getHandle()).thenReturn("123456789/topcommunity-test"); - - // for col1, it should return the item submission form defined for their parent subcom1 - SubmissionConfig submissionConfig1 = - new SubmissionConfigReader().getSubmissionConfigByCollection(col1); - assertEquals("subcommunitytest", submissionConfig1.getSubmissionName()); - - // for col2, it should return the item submission form defined for topcom - SubmissionConfig submissionConfig2 = - new SubmissionConfigReader().getSubmissionConfigByCollection(col2); - assertEquals("topcommunitytest", submissionConfig2.getSubmissionName()); - - } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index 70db05ef0919..1346be3fa902 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -258,10 +258,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=1"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(0))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -284,10 +284,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=1"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(1))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -310,10 +310,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=2"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(2))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -336,10 +336,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=3"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(3))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -362,10 +362,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=4"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(4))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -388,10 +388,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=5"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(5))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -414,10 +414,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=5"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(5))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -440,10 +440,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=6"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(6))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -466,10 +466,10 @@ public void findAllPaginationTest() throws Exception { Matchers.containsString("page=7"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(7))); getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") @@ -484,16 +484,42 @@ public void findAllPaginationTest() throws Exception { .andExpect(jsonPath("$._links.prev.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=7"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.next.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.self.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), Matchers.containsString("page=8"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$._links.last.href", Matchers.allOf( Matchers.containsString("/api/config/submissiondefinitions?"), - Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) .andExpect(jsonPath("$.page.size", is(1))) - .andExpect(jsonPath("$.page.totalElements", is(9))) - .andExpect(jsonPath("$.page.totalPages", is(9))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) .andExpect(jsonPath("$.page.number", is(8))); + + getClient(tokenAdmin).perform(get("/api/config/submissiondefinitions") + .param("size", "1") + .param("page", "9")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.submissiondefinitions[0].id", is("collectiontest"))) + .andExpect(jsonPath("$._links.first.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=0"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.prev.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=8"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.self.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$._links.last.href", Matchers.allOf( + Matchers.containsString("/api/config/submissiondefinitions?"), + Matchers.containsString("page=9"), Matchers.containsString("size=1")))) + .andExpect(jsonPath("$.page.size", is(1))) + .andExpect(jsonPath("$.page.totalElements", is(10))) + .andExpect(jsonPath("$.page.totalPages", is(10))) + .andExpect(jsonPath("$.page.number", is(9))); } } From 8c3ec48438b88e1c94038f8f75c6435653e0a159 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 12 Feb 2024 13:23:34 +0100 Subject: [PATCH 110/118] [#9238][CST-12825] Addresses Changes Requests --- .../external/datamodel/ImportRecord.java | 14 ++++++ .../RorParentOrgUnitMetadataContributor.java | 29 ++++++++++-- .../external/ror/service/RorFieldMapping.java | 4 +- .../RorImportMetadataSourceServiceImpl.java | 44 ++++++++++++------- .../RorImportMetadataSourceServiceIT.java | 21 +++++++++ dspace/config/dspace.cfg | 1 - dspace/config/modules/external-providers.cfg | 5 +++ dspace/config/modules/ror.cfg | 2 - .../registries/schema-organization-types.xml | 2 +- 9 files changed, 96 insertions(+), 26 deletions(-) delete mode 100644 dspace/config/modules/ror.cfg diff --git a/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java b/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java index dcf1a62b28db..cbd4bc124507 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/datamodel/ImportRecord.java @@ -96,11 +96,25 @@ public Collection getValue(String schema, String element, String q return values; } + /** + * Returns an {@code Optional} representing the value + * of the metadata {@code field} found inside the {@code valueList}. + * @param field String of the MetadataField to search + * @return {@code Optional} non empty if found. + */ public Optional getSingleValue(String field) { MetadataFieldName metadataFieldName = new MetadataFieldName(field); return getSingleValue(metadataFieldName.schema, metadataFieldName.element, metadataFieldName.qualifier); } + /** + * Retrieves a single value for the given schema, element, and qualifier. + * + * @param schema the schema for the value + * @param element the element for the value + * @param qualifier the qualifier for the value + * @return an optional containing the single value, if present + */ public Optional getSingleValue(String schema, String element, String qualifier) { return getValue(schema, element, qualifier).stream() .map(MetadatumDTO::getValue) diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java index be1910d7a521..52c64225544a 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/RorParentOrgUnitMetadataContributor.java @@ -16,20 +16,41 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.dspace.importer.external.metadatamapping.MetadatumDTO; +/** + * A ROR JsonPath Metadata processor that should be configured inside the {@code ror-integration.xml} file. + * This allows the extraction of a given contributor with a specific mappings from the ROR JSON response. + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + */ public class RorParentOrgUnitMetadataContributor extends SimpleJsonPathMetadataContributor { + /** + * Determines which field of the JSON detains the {@code type} of this + * specific node (that needs to be mapped). + * + */ private String typeField; + /** + * Determines which is the type of the main parent node that needs to be mapped. + * It should match the value of the {@code typeField} of the JSON node. + * + */ private String parentType; + /** + * Determines which is the field of the JSON that contains the value + * that needs to be mapped into a {@code MetadatumDTO}. + */ private String labelField; /** - * Retrieve the metadata associated with the given object. - * The toString() of the resulting object will be used. + * Creates a {@code MetadatumDTO} for each correctly mapped JSON node + * of the ROR response. + * Partial / Unmatched parent-type metadatum will be ignored from this mapping. * - * @param fullJson A class to retrieve metadata from. - * @return a collection of import records. Only the identifier of the found records may be put in the record. + * @param fullJson ROR response + * @return a collection of read ROR metadata. */ @Override public Collection contributeMetadata(String fullJson) { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java index 5248d793e292..d7caeffdbaf2 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorFieldMapping.java @@ -15,9 +15,9 @@ /** * An implementation of {@link AbstractMetadataFieldMapping} - * Responsible for defining the mapping of the Scopus metadatum fields on the DSpace metadatum fields + * Responsible for defining the mapping of the ROR metadatum fields on the DSpace metadatum fields * - * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) */ public class RorFieldMapping extends AbstractMetadataFieldMapping { diff --git a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java index ebc7caefb26d..59063271f365 100644 --- a/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/importer/external/ror/service/RorImportMetadataSourceServiceImpl.java @@ -34,10 +34,16 @@ import org.dspace.importer.external.service.components.QuerySource; import org.springframework.beans.factory.annotation.Autowired; +/** + * Implements a {@code AbstractImportMetadataSourceService} for querying ROR services. + * + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) + */ public class RorImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService implements QuerySource { private final static Logger log = LogManager.getLogger(); + protected static final String ROR_IDENTIFIER_PREFIX = "https://ror.org/"; private String url; @@ -98,7 +104,7 @@ public void init() throws Exception { } /** - * This class is a Callable implementation to get ADS entries based on query + * This class is a Callable implementation to get ROR entries based on query * object. This Callable use as query value the string queryString passed to * constructor. If the object will be construct through Query.class instance, a * Query's map entry with key "query" will be used. Pagination is supported too, @@ -126,7 +132,7 @@ public List call() throws Exception { } /** - * This class is a Callable implementation to get an ADS entry using bibcode The + * This class is a Callable implementation to get an ROR entry using bibcode The * bibcode to use can be passed through the constructor as a String or as * Query's map entry, with the key "id". * @@ -151,12 +157,12 @@ public List call() throws Exception { } /** - * This class is a Callable implementation to count the number of entries for an - * ADS query. This Callable use as query value to ADS the string queryString - * passed to constructor. If the object will be construct through Query.class + * This class is a Callable implementation to count the number of entries for a + * ROR query. This Callable uses as query value to ROR the string queryString + * passed to constructor. If the object will be construct through {@code Query} * instance, the value of the Query's map with the key "query" will be used. * - * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + * @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com) */ private class CountByQueryCallable implements Callable { private Query query; @@ -176,6 +182,12 @@ public Integer call() throws Exception { } } + /** + * Counts the number of results for the given query. + * + * @param query the query string to count results for + * @return the number of results for the given query + */ public Integer count(String query) { try { Map> params = new HashMap>(); @@ -197,9 +209,9 @@ public Integer count(String query) { private List searchById(String id) { - List adsResults = new ArrayList<>(); + List importResults = new ArrayList<>(); - id = StringUtils.removeStart(id, "https://ror.org/"); + id = StringUtils.removeStart(id, ROR_IDENTIFIER_PREFIX); try { Map> params = new HashMap>(); @@ -208,20 +220,20 @@ private List searchById(String id) { String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); if (StringUtils.isEmpty(resp)) { - return adsResults; + return importResults; } JsonNode jsonNode = convertStringJsonToJsonNode(resp); - adsResults.add(transformSourceRecords(jsonNode.toString())); + importResults.add(transformSourceRecords(jsonNode.toString())); } catch (URISyntaxException e) { e.printStackTrace(); } - return adsResults; + return importResults; } private List search(String query) { - List adsResults = new ArrayList<>(); + List importResults = new ArrayList<>(); try { Map> params = new HashMap>(); @@ -230,7 +242,7 @@ private List search(String query) { String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params); if (StringUtils.isEmpty(resp)) { - return adsResults; + return importResults; } JsonNode jsonNode = convertStringJsonToJsonNode(resp); @@ -239,15 +251,15 @@ private List search(String query) { Iterator nodes = docs.elements(); while (nodes.hasNext()) { JsonNode node = nodes.next(); - adsResults.add(transformSourceRecords(node.toString())); + importResults.add(transformSourceRecords(node.toString())); } } else { - adsResults.add(transformSourceRecords(docs.toString())); + importResults.add(transformSourceRecords(docs.toString())); } } catch (URISyntaxException e) { e.printStackTrace(); } - return adsResults; + return importResults; } private JsonNode convertStringJsonToJsonNode(String json) { diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java index d290ef39e010..82665bdc56a9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java @@ -11,6 +11,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.when; import java.io.InputStream; @@ -137,6 +138,26 @@ public void tesGetRecord() throws Exception { } } + @Test + public void tesGetRecordsCount() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClient.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + try (InputStream rorResponse = getClass().getResourceAsStream("ror-records.json")) { + String rorJsonResponse = IOUtils.toString(rorResponse, Charset.defaultCharset()); + + liveImportClient.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(rorJsonResponse, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + int tot = rorServiceImpl.getRecordsCount("test query"); + assertEquals(200, tot); + } finally { + liveImportClient.setHttpClient(originalHttpClient); + } + } + private Matcher> is(String value) { return matches(optionalValue -> optionalValue.isPresent() && optionalValue.get().equals(value)); } diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 0d293ac09786..d38ffd64339a 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1663,4 +1663,3 @@ include = ${module_dir}/usage-statistics.cfg include = ${module_dir}/versioning.cfg include = ${module_dir}/workflow.cfg include = ${module_dir}/external-providers.cfg -include = ${module_dir}/ror.cfg diff --git a/dspace/config/modules/external-providers.cfg b/dspace/config/modules/external-providers.cfg index b7c0e120dbcd..f210a0aa5163 100644 --- a/dspace/config/modules/external-providers.cfg +++ b/dspace/config/modules/external-providers.cfg @@ -92,3 +92,8 @@ wos.url.search = https://wos-api.clarivate.com/api/wos/?databaseId=WOS&lang=en&u datacite.url = https://api.datacite.org/dois/ datacite.timeout = 180000 ################################################################# +#--------------------------- ROR -------------------------------# +#---------------------------------------------------------------# + +ror.orgunit-import.api-url = https://api.ror.org/organizations +################################################################# \ No newline at end of file diff --git a/dspace/config/modules/ror.cfg b/dspace/config/modules/ror.cfg deleted file mode 100644 index 4a7a6cfa4fac..000000000000 --- a/dspace/config/modules/ror.cfg +++ /dev/null @@ -1,2 +0,0 @@ -ror.orgunit-import.api-url = https://api.ror.org/organizations -ror.authority.prefix = will be referenced::ROR-ID:: \ No newline at end of file diff --git a/dspace/config/registries/schema-organization-types.xml b/dspace/config/registries/schema-organization-types.xml index a9739796e68b..d903bee297a4 100644 --- a/dspace/config/registries/schema-organization-types.xml +++ b/dspace/config/registries/schema-organization-types.xml @@ -84,7 +84,7 @@ organization parentOrganization - + The larger organization that this organization is a subOrganization of, if any. From c448e3201aa9cbf711626e4925b7da5d80344694 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 12 Feb 2024 13:28:36 +0100 Subject: [PATCH 111/118] [#9238][CST-12825] Removes oaire-cerif types --- .../RorImportMetadataSourceServiceIT.java | 5 ---- .../registries/openaire-cerif-types.xml | 27 ------------------- dspace/config/spring/api/ror-integration.xml | 18 ------------- 3 files changed, 50 deletions(-) delete mode 100644 dspace/config/registries/openaire-cerif-types.xml diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java index 82665bdc56a9..3b72605ca106 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java @@ -66,8 +66,6 @@ public void tesGetRecords() throws Exception { is("The University of Texas") ); assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/02f6dcw23")); - assertThat(record.getSingleValue("oairecerif.acronym"), is("UTHSCSA")); - assertThat(record.getSingleValue("oairecerif.identifier.url"), is("http://www.uthscsa.edu/")); assertThat(record.getSingleValue("dc.type"), is("Education")); assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); assertThat(record.getSingleValue("organization.foundingDate"), is("1959")); @@ -124,9 +122,6 @@ public void tesGetRecord() throws Exception { is("The University of Texas Health Science Center at Tyler") ); assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/01sps7q28")); - assertThat(record.getSingleValue("oairecerif.acronym"), is("UTHSCT")); - assertThat(record.getSingleValue("oairecerif.identifier.url"), - is("https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler")); assertThat(record.getSingleValue("dc.type"), is("Healthcare")); assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); assertThat(record.getSingleValue("organization.foundingDate"), is("1947")); diff --git a/dspace/config/registries/openaire-cerif-types.xml b/dspace/config/registries/openaire-cerif-types.xml deleted file mode 100644 index 07f6da4313ed..000000000000 --- a/dspace/config/registries/openaire-cerif-types.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - OpenAIRE Types - - - - oairecerif - https://www.openaire.eu/cerif-profile/1.1/ - - - - oairecerif - identifier - url - - - - - oairecerif - acronym - - - - \ No newline at end of file diff --git a/dspace/config/spring/api/ror-integration.xml b/dspace/config/spring/api/ror-integration.xml index 65de4997e00e..fd845e2d65ac 100644 --- a/dspace/config/spring/api/ror-integration.xml +++ b/dspace/config/spring/api/ror-integration.xml @@ -14,9 +14,7 @@ - - @@ -40,22 +38,6 @@ - - - - - - - - - - - - - - - - From b678e91eb74cc588d485eeb158c10e85ae735806 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 12 Feb 2024 14:23:25 +0100 Subject: [PATCH 112/118] [#9238][CST-12825] Fixes IT failure --- .../org/dspace/app/rest/RorImportMetadataSourceServiceIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java index 3b72605ca106..bc2d1cb08db7 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java @@ -59,7 +59,7 @@ public void tesGetRecords() throws Exception { ImportRecord record = recordsImported.iterator().next(); - assertThat(record.getValueList(), hasSize(11)); + assertThat(record.getValueList(), hasSize(9)); assertThat( record.getSingleValue("organization.legalName"), @@ -116,7 +116,7 @@ public void tesGetRecord() throws Exception { context.restoreAuthSystemState(); ImportRecord record = rorServiceImpl.getRecord("https://ror.org/01sps7q28"); - assertThat(record.getValueList(), hasSize(9)); + assertThat(record.getValueList(), hasSize(7)); assertThat( record.getSingleValue("organization.legalName"), is("The University of Texas Health Science Center at Tyler") From f926456c23c5043a9849bc2356f714c5c35cd77f Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Mon, 12 Feb 2024 16:38:11 +0100 Subject: [PATCH 113/118] [#9192][DURACOM-204] Addresses Change Requests --- .../rest/RegistrationRestRepositoryIT.java | 28 +++++++++ .../EPersonForgotPasswordFeatureIT.java | 63 ++++++------------- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java index 3ba84f344b4c..7bab342c18a6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RegistrationRestRepositoryIT.java @@ -286,6 +286,34 @@ public void forgotPasswordTest() throws Exception { } } + @Test + public void testUnauthorizedForgotPasswordTest() throws Exception { + configurationService.setProperty("user.registration", false); + configurationService.setProperty("user.forgot-password", false); + + List registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + try { + assertEquals(0, registrationDataList.size()); + + ObjectMapper mapper = new ObjectMapper(); + RegistrationRest registrationRest = new RegistrationRest(); + registrationRest.setEmail(eperson.getEmail()); + getClient().perform(post("/api/eperson/registrations") + .param(TYPE_QUERY_PARAM, TYPE_FORGOT) + .content(mapper.writeValueAsBytes(registrationRest)) + .contentType(contentType)) + .andExpect(status().isUnauthorized()); + registrationDataList = registrationDataDAO.findAll(context, RegistrationData.class); + assertEquals(0, registrationDataList.size()); + } finally { + Iterator iterator = registrationDataList.iterator(); + while (iterator.hasNext()) { + RegistrationData registrationData = iterator.next(); + registrationDataDAO.delete(context, registrationData); + } + } + } + @Test public void registrationFlowWithNoHeaderCaptchaTokenTest() throws Exception { String originVerification = configurationService.getProperty("registration.verification.enabled"); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java index f7c0f3ba0ea4..23f5aaabb655 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/EPersonForgotPasswordFeatureIT.java @@ -67,7 +67,6 @@ public void setUp() throws Exception { @Test public void userForgotPasswordSuccessTest() throws Exception { - String property = configurationService.getProperty("user.forgot-password"); context.turnOffAuthorisationSystem(); EPerson epersonPassLogin = EPersonBuilder.createEPerson(context) @@ -78,41 +77,31 @@ public void userForgotPasswordSuccessTest() throws Exception { .build(); context.restoreAuthSystemState(); - try { - configurationService.setProperty("user.forgot-password", true); - EPersonRest personRest = personConverter.convert(epersonPassLogin, Projection.DEFAULT); - String personUri = utils.linkToSingleResource(personRest, "self").getHref(); + configurationService.setProperty("user.forgot-password", true); + EPersonRest personRest = personConverter.convert(epersonPassLogin, Projection.DEFAULT); + String personUri = utils.linkToSingleResource(personRest, "self").getHref(); getClient().perform(get("/api/authz/authorizations/search/object") .param("uri", personUri) .param("feature", epersonForgotPasswordFeature.getName())) .andExpect(status().isOk()) .andExpect(jsonPath("$.page.totalElements", greaterThan(0))); - } finally { - configurationService.setProperty("user.forgot-password", property); - } } @Test public void userForgotPasswordFeatureUnauthorizedTest() throws Exception { - String property = configurationService.getProperty("user.forgot-password"); Site site = siteService.findSite(context); SiteRest SiteRest = siteConverter.convert(site, Projection.DEFAULT); String siteUri = utils.linkToSingleResource(SiteRest, "self").getHref(); - try { - configurationService.setProperty("user.forgot-password", false); - - getClient().perform(get("/api/authz/authorizations/search/object") - .param("uri", siteUri) - .param("feature", epersonForgotPasswordFeature.getName())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", is(0))); + configurationService.setProperty("user.forgot-password", false); - } finally { - configurationService.setProperty("user.forgot-password", property); - } + getClient().perform(get("/api/authz/authorizations/search/object") + .param("uri", siteUri) + .param("feature", epersonForgotPasswordFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); } @@ -129,38 +118,26 @@ public void userForgotPasswordNoLoginTest() throws Exception { EPersonRest personRest = personConverter.convert(noLoginPerson, Projection.DEFAULT); String personUri = utils.linkToSingleResource(personRest, "self").getHref(); - String property = configurationService.getProperty("user.forgot-password"); - try { - configurationService.setProperty("user.forgot-password", true); + configurationService.setProperty("user.forgot-password", true); getClient().perform(get("/api/authz/authorizations/search/object") .param("uri", personUri) .param("feature", epersonForgotPasswordFeature.getName())) .andExpect(status().isOk()) .andExpect(jsonPath("$.page.totalElements", is(0))); - } finally { - configurationService.setProperty("user.forgot-password", property); - } } @Test public void userForgotPasswordUnauthorizedNoPasswordAuthMethodTest() throws Exception { //Enable Shibboleth and password login - String property = - configurationService.getProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod"); - - try { - configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); - - EPersonRest personRest = personConverter.convert(eperson, Projection.DEFAULT); - String personUri = utils.linkToSingleResource(personRest, "self").getHref(); - - getClient().perform(get("/api/authz/authorizations/search/object") - .param("uri", personUri) - .param("feature", epersonForgotPasswordFeature.getName())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.page.totalElements", is(0))); - } finally { - configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", property); - } + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", SHIB_ONLY); + + EPersonRest personRest = personConverter.convert(eperson, Projection.DEFAULT); + String personUri = utils.linkToSingleResource(personRest, "self").getHref(); + + getClient().perform(get("/api/authz/authorizations/search/object") + .param("uri", personUri) + .param("feature", epersonForgotPasswordFeature.getName())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(0))); } } From c2f0c6f331a752e24d7be88ead61989e3b93f2b6 Mon Sep 17 00:00:00 2001 From: frabacche Date: Tue, 13 Feb 2024 11:10:54 +0100 Subject: [PATCH 114/118] CST-5249_suggestion datescorer remove date range + docker config fixes --- .../app/suggestion/openaire/DateScorer.java | 56 +++++-------------- .../config/registries/schema-person-types.xml | 14 ----- dspace/config/spring/api/suggestions.xml | 2 - dspace/src/main/docker/dspace-solr/Dockerfile | 7 ++- 4 files changed, 20 insertions(+), 59 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java index ae7bb89f3228..ec02e357e7b8 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java @@ -43,16 +43,6 @@ public class DateScorer implements EvidenceScorer { */ private String educationDateMetadata; - /** - * Force the scorer to reject any publication that was issued before the specified date - */ - private String minDateMetadata; - - /** - * Force the scorer to reject any publication that was issued after the specified date - */ - private String maxDateMetadata; - /** * The minimal age that is expected for a researcher to be a potential author of a scholarly contribution * (i.e. the minimum delta from the publication date and the birth date) @@ -123,14 +113,6 @@ public void setEducationDateRange(int educationDateRange) { this.educationDateRange = educationDateRange; } - public void setMaxDateMetadata(String maxDateMetadata) { - this.maxDateMetadata = maxDateMetadata; - } - - public void setMinDateMetadata(String minDateMetadata) { - this.minDateMetadata = minDateMetadata; - } - public void setPublicationDateMetadata(String publicationDateMetadata) { this.publicationDateMetadata = publicationDateMetadata; } @@ -184,30 +166,22 @@ public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject im * @return */ private Integer[] calculateRange(Item researcher) { - String minDateStr = getSingleValue(researcher, minDateMetadata); - int minYear = getYear(minDateStr); - String maxDateStr = getSingleValue(researcher, maxDateMetadata); - int maxYear = getYear(maxDateStr); - if (minYear > 0 && maxYear > 0) { - return new Integer[] { minYear, maxYear }; + String birthDateStr = getSingleValue(researcher, birthDateMetadata); + int birthDateYear = getYear(birthDateStr); + int educationDateYear = getListMetadataValues(researcher, educationDateMetadata).stream() + .mapToInt(x -> getYear(x.getValue())).filter(d -> d > 0).min().orElse(-1); + if (educationDateYear > 0) { + return new Integer[] { + educationDateYear + educationDateDelta, + educationDateYear + educationDateDelta + educationDateRange + }; + } else if (birthDateYear > 0) { + return new Integer[] { + birthDateYear + birthDateDelta, + birthDateYear + birthDateDelta + birthDateRange + }; } else { - String birthDateStr = getSingleValue(researcher, birthDateMetadata); - int birthDateYear = getYear(birthDateStr); - int educationDateYear = getListMetadataValues(researcher, educationDateMetadata).stream() - .mapToInt(x -> getYear(x.getValue())).filter(d -> d > 0).min().orElse(-1); - if (educationDateYear > 0) { - return new Integer[] { - minYear > 0 ? minYear : educationDateYear + educationDateDelta, - maxYear > 0 ? maxYear : educationDateYear + educationDateDelta + educationDateRange - }; - } else if (birthDateYear > 0) { - return new Integer[] { - minYear > 0 ? minYear : birthDateYear + birthDateDelta, - maxYear > 0 ? maxYear : birthDateYear + birthDateDelta + birthDateRange - }; - } else { - return null; - } + return null; } } diff --git a/dspace/config/registries/schema-person-types.xml b/dspace/config/registries/schema-person-types.xml index cbe9035c23dd..0a40060e5101 100644 --- a/dspace/config/registries/schema-person-types.xml +++ b/dspace/config/registries/schema-person-types.xml @@ -155,19 +155,5 @@ variant Full name variant - - - person - range - maxdate - Last year in which the person is likely to be a contributor - - - - person - range - mindate - First year in which the person is likely to be a contributor - \ No newline at end of file diff --git a/dspace/config/spring/api/suggestions.xml b/dspace/config/spring/api/suggestions.xml index 3618d820e8a1..85e7949809e5 100644 --- a/dspace/config/spring/api/suggestions.xml +++ b/dspace/config/spring/api/suggestions.xml @@ -53,8 +53,6 @@ class="org.dspace.app.suggestion.openaire.DateScorer"> - - diff --git a/dspace/src/main/docker/dspace-solr/Dockerfile b/dspace/src/main/docker/dspace-solr/Dockerfile index 5d02d0db811f..0c5d7986945f 100644 --- a/dspace/src/main/docker/dspace-solr/Dockerfile +++ b/dspace/src/main/docker/dspace-solr/Dockerfile @@ -18,7 +18,8 @@ ENV AUTHORITY_CONFIGSET_PATH=/opt/solr/server/solr/configsets/authority/conf \ OAI_CONFIGSET_PATH=/opt/solr/server/solr/configsets/oai/conf \ SEARCH_CONFIGSET_PATH=/opt/solr/server/solr/configsets/search/conf \ STATISTICS_CONFIGSET_PATH=/opt/solr/server/solr/configsets/statistics/conf \ - QAEVENT_CONFIGSET_PATH=/opt/solr/server/solr/configsets/qaevent/conf + QAEVENT_CONFIGSET_PATH=/opt/solr/server/solr/configsets/qaevent/conf \ + SUGGESTION_CONFIGSET_PATH=/opt/solr/server/solr/configsets/suggestion/conf USER root @@ -26,13 +27,15 @@ RUN mkdir -p $AUTHORITY_CONFIGSET_PATH && \ mkdir -p $OAI_CONFIGSET_PATH && \ mkdir -p $SEARCH_CONFIGSET_PATH && \ mkdir -p $STATISTICS_CONFIGSET_PATH && \ - mkdir -p $QAEVENT_CONFIGSET_PATH + mkdir -p $QAEVENT_CONFIGSET_PATH && \ + mkdir -p $SUGGESTION_CONFIGSET_PATH COPY dspace/solr/authority/conf/* $AUTHORITY_CONFIGSET_PATH/ COPY dspace/solr/oai/conf/* $OAI_CONFIGSET_PATH/ COPY dspace/solr/search/conf/* $SEARCH_CONFIGSET_PATH/ COPY dspace/solr/statistics/conf/* $STATISTICS_CONFIGSET_PATH/ COPY dspace/solr/qaevent/conf/* $QAEVENT_CONFIGSET_PATH/ +COPY dspace/solr/suggestion/conf/* $SUGGESTION_CONFIGSET_PATH/ RUN chown -R solr:solr /opt/solr/server/solr/configsets From 57a8b650d72928d35c15bf4c75b8140bcecc3024 Mon Sep 17 00:00:00 2001 From: Vincenzo Mecca Date: Tue, 13 Feb 2024 19:09:52 +0100 Subject: [PATCH 115/118] [#9237][CST-12826] Refactors ROR metadata mappings --- .../RorImportMetadataSourceServiceIT.java | 8 +++--- .../registries/openaire-cerif-types.xml | 27 ------------------- .../registries/schema-organization-types.xml | 14 +++++++++- dspace/config/spring/api/ror-integration.xml | 4 +-- 4 files changed, 19 insertions(+), 34 deletions(-) delete mode 100644 dspace/config/registries/openaire-cerif-types.xml diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java index d290ef39e010..e2f8cd9610c9 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RorImportMetadataSourceServiceIT.java @@ -65,8 +65,8 @@ public void tesGetRecords() throws Exception { is("The University of Texas") ); assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/02f6dcw23")); - assertThat(record.getSingleValue("oairecerif.acronym"), is("UTHSCSA")); - assertThat(record.getSingleValue("oairecerif.identifier.url"), is("http://www.uthscsa.edu/")); + assertThat(record.getSingleValue("organization.alternateName"), is("UTHSCSA")); + assertThat(record.getSingleValue("organization.url"), is("http://www.uthscsa.edu/")); assertThat(record.getSingleValue("dc.type"), is("Education")); assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); assertThat(record.getSingleValue("organization.foundingDate"), is("1959")); @@ -123,8 +123,8 @@ public void tesGetRecord() throws Exception { is("The University of Texas Health Science Center at Tyler") ); assertThat(record.getSingleValue("organization.identifier.ror"), is("https://ror.org/01sps7q28")); - assertThat(record.getSingleValue("oairecerif.acronym"), is("UTHSCT")); - assertThat(record.getSingleValue("oairecerif.identifier.url"), + assertThat(record.getSingleValue("organization.alternateName"), is("UTHSCT")); + assertThat(record.getSingleValue("organization.url"), is("https://www.utsystem.edu/institutions/university-texas-health-science-center-tyler")); assertThat(record.getSingleValue("dc.type"), is("Healthcare")); assertThat(record.getSingleValue("organization.address.addressCountry"), is("US")); diff --git a/dspace/config/registries/openaire-cerif-types.xml b/dspace/config/registries/openaire-cerif-types.xml deleted file mode 100644 index 07f6da4313ed..000000000000 --- a/dspace/config/registries/openaire-cerif-types.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - OpenAIRE Types - - - - oairecerif - https://www.openaire.eu/cerif-profile/1.1/ - - - - oairecerif - identifier - url - - - - - oairecerif - acronym - - - - \ No newline at end of file diff --git a/dspace/config/registries/schema-organization-types.xml b/dspace/config/registries/schema-organization-types.xml index a9739796e68b..438d2eeafdc3 100644 --- a/dspace/config/registries/schema-organization-types.xml +++ b/dspace/config/registries/schema-organization-types.xml @@ -86,6 +86,18 @@ parentOrganization - + + + + organization + alternateName + An alias for the organization. + + + + organization + url + Url of the organization. + diff --git a/dspace/config/spring/api/ror-integration.xml b/dspace/config/spring/api/ror-integration.xml index 65de4997e00e..ff554612052e 100644 --- a/dspace/config/spring/api/ror-integration.xml +++ b/dspace/config/spring/api/ror-integration.xml @@ -45,7 +45,7 @@ - + @@ -53,7 +53,7 @@ - + From 68b2770b45c539ae796204ff45fd8a40cd3418b6 Mon Sep 17 00:00:00 2001 From: frabacche Date: Wed, 14 Feb 2024 09:17:49 +0100 Subject: [PATCH 116/118] CST-5249_suggestion DateScorer log msg adjust --- .../java/org/dspace/app/suggestion/openaire/DateScorer.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java index ec02e357e7b8..94f81715fa63 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/DateScorer.java @@ -133,8 +133,7 @@ public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject im return new SuggestionEvidence(this.getClass().getSimpleName(), 0, "No assumption was possible about the publication year range. " - + "Please consider to set a min/max date in the profile, specify the birthday " - + "or education achievements"); + + "Please consider setting your birthday in your profile."); } else { String optDate = SuggestionUtils.getFirstEntryByMetadatum(importRecord, publicationDateMetadata); int year = getYear(optDate); From 608070962724bb0dc7cf65f84e6706a8ceaae755 Mon Sep 17 00:00:00 2001 From: Agustina Martinez Date: Thu, 15 Feb 2024 20:42:23 +0000 Subject: [PATCH 117/118] Fix integration test count number of fields --- .../java/org/dspace/app/rest/RelationshipRestRepositoryIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java index 0ff6fe04e9d8..ea164af7318a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java @@ -1039,7 +1039,7 @@ public void addRelationshipsAndMetadataToValidatePlaceTest() throws Exception { list = itemService.getMetadata(publication1, Item.ANY, Item.ANY, Item.ANY, Item.ANY); // also includes type, 3 relation.isAuthorOfPublication and 3 relation.isAuthorOfPublication.latestForDiscovery // values - assertEquals(23, list.size()); + assertEquals(22, list.size()); } finally { RelationshipBuilder.deleteRelationship(idRef1.get()); From 03fe58668114db529413d13d98af57c6e045e9df Mon Sep 17 00:00:00 2001 From: haoueclf Date: Fri, 16 Feb 2024 16:51:41 +0100 Subject: [PATCH 118/118] [DS-9345] Correct the package name of the IIIF search plugin --- dspace/config/modules/iiif.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace/config/modules/iiif.cfg b/dspace/config/modules/iiif.cfg index fc1e9bdf9f8d..9a2a3bb0d13a 100644 --- a/dspace/config/modules/iiif.cfg +++ b/dspace/config/modules/iiif.cfg @@ -15,7 +15,7 @@ iiif.image.server = http://localhost:8182/iiif/2/ # The search plugin used to support (experimental) IIIF Search. # This is the class used with https://dbmdz.github.io/solr-ocrhighlighting/ # It is currently the only supported option. -# iiif.search.plugin = org.dspace.app.rest.iiif.service.WordHighlightSolrSearch +# iiif.search.plugin = org.dspace.app.iiif.service.WordHighlightSolrSearch # Sets the viewing hint. Possible values: "paged" or "individuals". # Typically "paged" is preferred for multi-age documents. Use "individuals"