diff --git a/dspace-api/src/main/java/org/dspace/core/Context.java b/dspace-api/src/main/java/org/dspace/core/Context.java index 962a67adb8..0567b9897b 100644 --- a/dspace-api/src/main/java/org/dspace/core/Context.java +++ b/dspace-api/src/main/java/org/dspace/core/Context.java @@ -10,6 +10,7 @@ package org.dspace.core; import java.sql.SQLException; import java.util.ArrayList; import java.util.Deque; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -91,12 +92,12 @@ public class Context implements AutoCloseable { /** * Group IDs of special groups user is a member of */ - private List specialGroups; + private Set specialGroups; /** * Temporary store for the specialGroups when the current user is temporary switched */ - private List specialGroupsPreviousState; + private Set specialGroupsPreviousState; /** * The currently used authentication method @@ -183,7 +184,7 @@ public class Context implements AutoCloseable { extraLogInfo = ""; ignoreAuth = false; - specialGroups = new ArrayList<>(); + specialGroups = new HashSet<>(); authStateChangeHistory = new ConcurrentLinkedDeque<>(); authStateClassCallHistory = new ConcurrentLinkedDeque<>(); @@ -703,7 +704,7 @@ public class Context implements AutoCloseable { currentUserPreviousState = currentUser; specialGroupsPreviousState = specialGroups; - specialGroups = new ArrayList<>(); + specialGroups = new HashSet<>(); currentUser = newUser; } diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.java new file mode 100644 index 0000000000..abf84f52d0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefAuthorMetadataProcessor.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.importer.external.crossref; + +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.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.importer.external.metadatamapping.contributor.JsonPathMetadataProcessor; + +/** + * This class is used for CrossRef's Live-Import to extract + * attributes such as "given" and "family" from the array of authors/editors + * and return them concatenated. + * Beans are configured in the crossref-integration.xml file. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public class CrossRefAuthorMetadataProcessor implements JsonPathMetadataProcessor { + + private final static Logger log = LogManager.getLogger(); + + private String pathToArray; + + @Override + public Collection processMetadata(String json) { + JsonNode rootNode = convertStringJsonToJsonNode(json); + Iterator authors = rootNode.at(pathToArray).iterator(); + Collection values = new ArrayList<>(); + while (authors.hasNext()) { + JsonNode author = authors.next(); + String givenName = author.at("/given").textValue(); + String familyName = author.at("/family").textValue(); + if (StringUtils.isNoneBlank(givenName) && StringUtils.isNoneBlank(familyName)) { + values.add(givenName + " " + familyName); + } + } + return values; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = null; + try { + body = mapper.readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process json response.", e); + } + return body; + } + + public void setPathToArray(String pathToArray) { + this.pathToArray = pathToArray; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefFieldMapping.java new file mode 100644 index 0000000000..5e879b4d26 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefFieldMapping.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.importer.external.crossref; + +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 CrossRef metadatum fields on the DSpace metadatum fields + * + * @author Pasquale Cavallo (pasquale.cavallo at 4science dot it) + */ +@SuppressWarnings("rawtypes") +public class CrossRefFieldMapping 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 + @SuppressWarnings("unchecked") + @Resource(name = "crossrefMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.java new file mode 100644 index 0000000000..61bee8eb5b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/crossref/CrossRefImportMetadataSourceServiceImpl.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.crossref; + +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +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.collections4.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.DoiCheck; +import org.dspace.importer.external.service.components.QuerySource; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implements a data source for querying CrossRef + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class CrossRefImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private final static Logger log = LogManager.getLogger(); + + private String url; + + @Autowired + private LiveImportClient liveImportClient; + + @Override + public String getImportSource() { + return "crossref"; + } + + @Override + public void init() throws Exception {} + + @Override + public ImportRecord getRecord(String recordId) throws MetadataSourceException { + String id = getID(recordId); + List records = StringUtils.isNotBlank(id) ? retry(new SearchByIdCallable(id)) + : retry(new SearchByIdCallable(recordId)); + return CollectionUtils.isEmpty(records) ? null : records.get(0); + } + + @Override + public int getRecordsCount(String query) throws MetadataSourceException { + String id = getID(query); + return StringUtils.isNotBlank(id) ? retry(new DoiCheckCallable(id)) : retry(new CountByQueryCallable(query)); + } + + @Override + public int getRecordsCount(Query query) throws MetadataSourceException { + String id = getID(query.toString()); + return StringUtils.isNotBlank(id) ? retry(new DoiCheckCallable(id)) : retry(new CountByQueryCallable(query)); + } + + @Override + public Collection getRecords(String query, int start, int count) throws MetadataSourceException { + String id = getID(query.toString()); + return StringUtils.isNotBlank(id) ? retry(new SearchByIdCallable(id)) + : retry(new SearchByQueryCallable(query, count, start)); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + String id = getID(query.toString()); + if (StringUtils.isNotBlank(id)) { + return retry(new SearchByIdCallable(id)); + } + return retry(new SearchByQueryCallable(query)); + } + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + String id = getID(query.toString()); + List records = StringUtils.isNotBlank(id) ? retry(new SearchByIdCallable(id)) + : retry(new SearchByIdCallable(query)); + return CollectionUtils.isEmpty(records) ? null : records.get(0); + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + String id = getID(query.toString()); + return StringUtils.isNotBlank(id) ? retry(new SearchByIdCallable(id)) + : retry(new FindMatchingRecordCallable(query)); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for CrossRef"); + } + + public String getID(String id) { + return DoiCheck.isDoi(id) ? "filter=doi:" + id : StringUtils.EMPTY; + } + + /** + * This class is a Callable implementation to get CrossRef 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, Integer maxResult, Integer start) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("count", maxResult); + query.addParameter("start", start); + } + + private SearchByQueryCallable(Query query) { + this.query = query; + } + + @Override + public List call() throws Exception { + List results = new ArrayList<>(); + Integer count = query.getParameterAsClass("count", Integer.class); + Integer start = query.getParameterAsClass("start", Integer.class); + + URIBuilder uriBuilder = new URIBuilder(url); + uriBuilder.addParameter("query", query.getParameterAsClass("query", String.class)); + if (Objects.nonNull(count)) { + uriBuilder.addParameter("rows", count.toString()); + } + if (Objects.nonNull(start)) { + uriBuilder.addParameter("offset", start.toString()); + } + + String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + JsonNode jsonNode = convertStringJsonToJsonNode(response); + Iterator nodes = jsonNode.at("/message/items").iterator(); + while (nodes.hasNext()) { + JsonNode node = nodes.next(); + results.add(transformSourceRecords(node.toString())); + } + return results; + } + + } + + /** + * This class is a Callable implementation to get an CrossRef entry using DOI + * The DOI 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 { + List results = new ArrayList<>(); + String ID = URLDecoder.decode(query.getParameterAsClass("id", String.class), "UTF-8"); + URIBuilder uriBuilder = new URIBuilder(url + "/" + ID); + String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + JsonNode jsonNode = convertStringJsonToJsonNode(responseString); + JsonNode messageNode = jsonNode.at("/message"); + results.add(transformSourceRecords(messageNode.toString())); + return results; + } + } + + /** + * This class is a Callable implementation to search CrossRef entries using author and title. + * There are two field in the Query map to pass, with keys "title" and "author" + * (at least one must be used). + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ + private class FindMatchingRecordCallable implements Callable> { + + private Query query; + + private FindMatchingRecordCallable(Query q) { + query = q; + } + + @Override + public List call() throws Exception { + String queryValue = query.getParameterAsClass("query", String.class); + Integer count = query.getParameterAsClass("count", Integer.class); + Integer start = query.getParameterAsClass("start", Integer.class); + String author = query.getParameterAsClass("author", String.class); + String title = query.getParameterAsClass("title", String.class); + String bibliographics = query.getParameterAsClass("bibliographics", String.class); + List results = new ArrayList<>(); + URIBuilder uriBuilder = new URIBuilder(url); + if (Objects.nonNull(queryValue)) { + uriBuilder.addParameter("query", queryValue); + } + if (Objects.nonNull(count)) { + uriBuilder.addParameter("rows", count.toString()); + } + if (Objects.nonNull(start)) { + uriBuilder.addParameter("offset", start.toString()); + } + if (Objects.nonNull(author)) { + uriBuilder.addParameter("query.author", author); + } + if (Objects.nonNull(title )) { + uriBuilder.addParameter("query.container-title", title); + } + if (Objects.nonNull(bibliographics)) { + uriBuilder.addParameter("query.bibliographic", bibliographics); + } + + String resp = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + JsonNode jsonNode = convertStringJsonToJsonNode(resp); + Iterator nodes = jsonNode.at("/message/items").iterator(); + while (nodes.hasNext()) { + JsonNode node = nodes.next(); + results.add(transformSourceRecords(node.toString())); + } + return results; + } + + } + + /** + * This class is a Callable implementation to count the number of entries for an CrossRef query. + * This Callable use as query value to CrossRef 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 { + URIBuilder uriBuilder = new URIBuilder(url); + uriBuilder.addParameter("query", query.getParameterAsClass("query", String.class)); + String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + JsonNode jsonNode = convertStringJsonToJsonNode(responseString); + return jsonNode.at("/message/total-results").asInt(); + } + } + + /** + * This class is a Callable implementation to check if exist an CrossRef entry using DOI. + * The DOI to use can be passed through the constructor as a String or as Query's map entry, with the key "id". + * return 1 if CrossRef entry exists otherwise 0 + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ + private class DoiCheckCallable implements Callable { + + private final Query query; + + private DoiCheckCallable(final String id) { + final Query query = new Query(); + query.addParameter("id", id); + this.query = query; + } + + private DoiCheckCallable(final Query query) { + this.query = query; + } + + @Override + public Integer call() throws Exception { + URIBuilder uriBuilder = new URIBuilder(url + "/" + query.getParameterAsClass("id", String.class)); + String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + JsonNode jsonNode = convertStringJsonToJsonNode(responseString); + return StringUtils.equals(jsonNode.at("/status").toString(), "ok") ? 1 : 0; + } + } + + 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; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClient.java b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClient.java new file mode 100644 index 0000000000..b443b8e345 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClient.java @@ -0,0 +1,21 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.liveimportclient.service; + +import java.util.Map; + +/** + * Interface for classes that allow to contact LiveImport clients. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public interface LiveImportClient { + + public String executeHttpGetRequest(int timeout, String URL, Map requestParams); + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.java new file mode 100644 index 0000000000..ef3cab723b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/liveimportclient/service/LiveImportClientImpl.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.importer.external.liveimportclient.service; + +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.config.RequestConfig.Builder; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link LiveImportClient}. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science dot com) + */ +public class LiveImportClientImpl implements LiveImportClient { + + private final static Logger log = LogManager.getLogger(); + + private CloseableHttpClient httpClient; + + @Autowired + private ConfigurationService configurationService; + + @Override + public String executeHttpGetRequest(int timeout, String URL, Map requestParams) { + HttpGet method = null; + try (CloseableHttpClient httpClient = Optional.ofNullable(this.httpClient) + .orElseGet(HttpClients::createDefault)) { + + Builder requestConfigBuilder = RequestConfig.custom(); + requestConfigBuilder.setConnectionRequestTimeout(timeout); + RequestConfig defaultRequestConfig = requestConfigBuilder.build(); + + method = new HttpGet(getSearchUrl(URL, requestParams)); + method.setConfig(defaultRequestConfig); + + configureProxy(method, defaultRequestConfig); + + HttpResponse httpResponse = httpClient.execute(method); + if (isNotSuccessfull(httpResponse)) { + throw new RuntimeException("The request failed with: " + getStatusCode(httpResponse) + " code"); + } + InputStream inputStream = httpResponse.getEntity().getContent(); + return IOUtils.toString(inputStream, Charset.defaultCharset()); + } catch (Exception e1) { + log.error(e1.getMessage(), e1); + } finally { + if (Objects.nonNull(method)) { + method.releaseConnection(); + } + } + return StringUtils.EMPTY; + } + + private void configureProxy(HttpGet method, RequestConfig defaultRequestConfig) { + String proxyHost = configurationService.getProperty("http.proxy.host"); + String proxyPort = configurationService.getProperty("http.proxy.port"); + if (StringUtils.isNotBlank(proxyHost) && StringUtils.isNotBlank(proxyPort)) { + RequestConfig requestConfig = RequestConfig.copy(defaultRequestConfig) + .setProxy(new HttpHost(proxyHost, Integer.parseInt(proxyPort), "http")) + .build(); + method.setConfig(requestConfig); + } + } + + private String getSearchUrl(String URL, Map requestParams) throws URISyntaxException { + URIBuilder uriBuilder = new URIBuilder(URL); + for (String param : requestParams.keySet()) { + uriBuilder.setParameter(param, requestParams.get(param)); + } + return uriBuilder.toString(); + } + + private boolean isNotSuccessfull(HttpResponse response) { + int statusCode = getStatusCode(response); + return statusCode < 200 || statusCode > 299; + } + + private int getStatusCode(HttpResponse response) { + return response.getStatusLine().getStatusCode(); + } + + public CloseableHttpClient getHttpClient() { + return httpClient; + } + + public void setHttpClient(CloseableHttpClient httpClient) { + this.httpClient = httpClient; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ArrayElementAttributeProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ArrayElementAttributeProcessor.java new file mode 100644 index 0000000000..b938a290c2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/ArrayElementAttributeProcessor.java @@ -0,0 +1,82 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.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.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This Processor allows to extract attribute values of an array. + * For exaple to extract all values of secondAttribute, + * "array":[ + * { + * "firstAttribute":"first value", + * "secondAttribute":"second value" + * }, + * { + * "firstAttribute":"first value", + * "secondAttribute":"second value" + * } + * ] + * + * it's possible configure a bean with + * pathToArray=/array and elementAttribute=/secondAttribute + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public class ArrayElementAttributeProcessor implements JsonPathMetadataProcessor { + + private final static Logger log = LogManager.getLogger(); + + private String pathToArray; + + private String elementAttribute; + + @Override + public Collection processMetadata(String json) { + JsonNode rootNode = convertStringJsonToJsonNode(json); + Iterator array = rootNode.at(pathToArray).iterator(); + Collection values = new ArrayList<>(); + while (array.hasNext()) { + JsonNode element = array.next(); + String value = element.at(elementAttribute).textValue(); + if (StringUtils.isNoneBlank(value)) { + values.add(value); + } + } + return values; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = null; + try { + body = mapper.readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process json response.", e); + } + return body; + } + + public void setPathToArray(String pathToArray) { + this.pathToArray = pathToArray; + } + + public void setElementAttribute(String elementAttribute) { + this.elementAttribute = elementAttribute; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/JsonPathMetadataProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/JsonPathMetadataProcessor.java new file mode 100644 index 0000000000..2de0c6a0bb --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/JsonPathMetadataProcessor.java @@ -0,0 +1,23 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.metadatamapping.contributor; + +import java.util.Collection; + +/** + * Service interface class for processing json object. + * The implementation of this class is responsible for all business logic calls + * for extracting of values from json object. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public interface JsonPathMetadataProcessor { + + public Collection processMetadata(String json); + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/MatrixElementProcessor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/MatrixElementProcessor.java new file mode 100644 index 0000000000..c8e93971f4 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/MatrixElementProcessor.java @@ -0,0 +1,87 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.metadatamapping.contributor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This Processor allows to extract all values of a matrix. + * Only need to configure the path to the matrix in "pathToMatrix" + * For exaple to extract all values + * "matrix": [ + * [ + * "first", + * "second" + * ], + * [ + * "third" + * ], + * [ + * "fourth", + * "fifth" + * ] + * ], + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public class MatrixElementProcessor implements JsonPathMetadataProcessor { + + private final static Logger log = LogManager.getLogger(); + + private String pathToMatrix; + + @Override + public Collection processMetadata(String json) { + JsonNode rootNode = convertStringJsonToJsonNode(json); + Iterator array = rootNode.at(pathToMatrix).elements(); + Collection values = new ArrayList<>(); + while (array.hasNext()) { + JsonNode element = array.next(); + if (element.isArray()) { + Iterator nodes = element.iterator(); + while (nodes.hasNext()) { + String nodeValue = nodes.next().textValue(); + if (StringUtils.isNotBlank(nodeValue)) { + values.add(nodeValue); + } + } + } else { + String nodeValue = element.textValue(); + if (StringUtils.isNotBlank(nodeValue)) { + values.add(nodeValue); + } + } + } + return values; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = null; + try { + body = mapper.readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process json response.", e); + } + return body; + } + + public void setPathToMatrix(String pathToMatrix) { + this.pathToMatrix = pathToMatrix; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java new file mode 100644 index 0000000000..f739980220 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/metadatamapping/contributor/SimpleJsonPathMetadataContributor.java @@ -0,0 +1,181 @@ +/** + * 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 java.util.Objects; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.importer.external.metadatamapping.MetadataFieldConfig; +import org.dspace.importer.external.metadatamapping.MetadataFieldMapping; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +/** + * A simple JsonPath Metadata processor + * that allow extract value from json object + * by configuring the path in the query variable via the bean. + * moreover this can also perform more compact extractions + * by configuring specific json processor in "metadataProcessor" + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public class SimpleJsonPathMetadataContributor implements MetadataContributor { + + private final static Logger log = LogManager.getLogger(); + + private String query; + + private MetadataFieldConfig field; + + protected JsonPathMetadataProcessor metadataProcessor; + + /** + * Initialize SimpleJsonPathMetadataContributor with a query, prefixToNamespaceMapping and MetadataFieldConfig + * + * @param query The JSonPath query + * @param field the matadata field to map the result of the Json path query + * MetadataFieldConfig + */ + public SimpleJsonPathMetadataContributor(String query, MetadataFieldConfig field) { + this.query = query; + this.field = field; + } + + + /** + * Unused by this implementation + */ + @Override + public void setMetadataFieldMapping(MetadataFieldMapping> rt) { + + } + + /** + * Empty constructor for SimpleJsonPathMetadataContributor + */ + public SimpleJsonPathMetadataContributor() { + + } + + /** + * Return the MetadataFieldConfig used while retrieving MetadatumDTO + * + * @return MetadataFieldConfig + */ + public MetadataFieldConfig getField() { + return field; + } + + /** + * Setting the MetadataFieldConfig + * + * @param field MetadataFieldConfig used while retrieving MetadatumDTO + */ + public void setField(MetadataFieldConfig field) { + this.field = field; + } + + /** + * Return query used to create the JSonPath + * + * @return the query this instance is based on + */ + public String getQuery() { + return query; + } + + /** + * Return query used to create the JSonPath + * + */ + public void setQuery(String query) { + this.query = query; + } + + /** + * Used to process data got by jsonpath expression, like arrays to stringify, change date format or else + * If it is null, toString will be used. + * + * @param metadataProcessor + */ + public void setMetadataProcessor(JsonPathMetadataProcessor metadataProcessor) { + this.metadataProcessor = metadataProcessor; + } + + /** + * Retrieve the metadata associated with the given object. + * The toString() of the resulting object will be used. + * + * @param t 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<>(); + if (Objects.nonNull(metadataProcessor)) { + metadataValue = metadataProcessor.processMetadata(fullJson); + } else { + JsonNode jsonNode = convertStringJsonToJsonNode(fullJson); + JsonNode node = jsonNode.at(query); + if (node.isArray()) { + Iterator nodes = node.iterator(); + while (nodes.hasNext()) { + String nodeValue = getStringValue(nodes.next()); + if (StringUtils.isNotBlank(nodeValue)) { + metadataValue.add(nodeValue); + } + } + } else if (!node.isNull() && StringUtils.isNotBlank(node.toString())) { + String nodeValue = getStringValue(node); + if (StringUtils.isNotBlank(nodeValue)) { + metadataValue.add(nodeValue); + } + } + } + for (String value : metadataValue) { + MetadatumDTO metadatumDto = new MetadatumDTO(); + metadatumDto.setValue(value); + metadatumDto.setElement(field.getElement()); + metadatumDto.setQualifier(field.getQualifier()); + metadatumDto.setSchema(field.getSchema()); + metadata.add(metadatumDto); + } + return metadata; + } + + private String getStringValue(JsonNode node) { + if (node.isTextual()) { + return node.textValue(); + } + if (node.isNumber()) { + return node.numberValue().toString(); + } + log.error("It wasn't possible to convert the value of the following JsonNode:" + node.asText()); + return StringUtils.EMPTY; + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = null; + try { + body = mapper.readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process json response.", e); + } + return body; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java b/dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java new file mode 100644 index 0000000000..3b15a421b8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/service/DoiCheck.java @@ -0,0 +1,47 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.importer.external.service; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility class that provides methods to check if a given string is a DOI and exists on CrossRef services + * + * @author Corrado Lombardi (corrado.lombardi at 4science.it) + */ +public class DoiCheck { + + private static final List DOI_PREFIXES = Arrays.asList("http://dx.doi.org/", "https://dx.doi.org/"); + + private static final Pattern PATTERN = Pattern.compile("10.\\d{4,9}/[-._;()/:A-Z0-9]+" + + "|10.1002/[^\\s]+" + + "|10.\\d{4}/\\d+-\\d+X?(\\d+)" + + "\\d+<[\\d\\w]+:[\\d\\w]*>\\d+.\\d+.\\w+;\\d" + + "|10.1021/\\w\\w\\d++" + + "|10.1207/[\\w\\d]+\\&\\d+_\\d+", + Pattern.CASE_INSENSITIVE); + + private DoiCheck() {} + + public static boolean isDoi(final String value) { + Matcher m = PATTERN.matcher(purgeDoiValue(value)); + return m.matches(); + } + + public static String purgeDoiValue(final String query) { + String value = query.replaceAll(",", ""); + for (final String prefix : DOI_PREFIXES) { + value = value.replaceAll(prefix, ""); + } + return value.trim(); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java new file mode 100644 index 0000000000..63cb9b126f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/vufind/VuFindImportMetadataSourceServiceImpl.java @@ -0,0 +1,336 @@ +/** + * 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.vufind; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +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.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; + +/** + * Implements a data source for querying VuFind + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class VuFindImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService + implements QuerySource { + + private final static Logger log = LogManager.getLogger(); + + private String url; + private String urlSearch; + + private String fields; + + @Autowired + private LiveImportClient liveImportClient; + + public VuFindImportMetadataSourceServiceImpl(String fields) { + this.fields = fields; + } + + @Override + public String getImportSource() { + return "VuFind"; + } + + @Override + public ImportRecord getRecord(String id) throws MetadataSourceException { + String records = retry(new GetByVuFindIdCallable(id, fields)); + List importRecords = extractMetadataFromRecordList(records); + return importRecords != null && !importRecords.isEmpty() ? importRecords.get(0) : null; + } + + @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 { + String records = retry(new SearchByQueryCallable(query, count, start, fields)); + return extractMetadataFromRecordList(records); + } + + @Override + public Collection getRecords(Query query) throws MetadataSourceException { + String records = retry(new SearchByQueryCallable(query, fields)); + return extractMetadataFromRecordList(records); + } + + @Override + public ImportRecord getRecord(Query query) throws MetadataSourceException { + String records = retry(new SearchByQueryCallable(query, fields)); + List importRecords = extractMetadataFromRecordList(records); + return importRecords != null && !importRecords.isEmpty() ? importRecords.get(0) : null; + } + + @Override + public Collection findMatchingRecords(Query query) throws MetadataSourceException { + String records = retry(new FindMatchingRecordsCallable(query)); + return extractMetadataFromRecordList(records); + } + + @Override + public Collection findMatchingRecords(Item item) throws MetadataSourceException { + throw new MethodNotFoundException("This method is not implemented for VuFind"); + } + + @Override + public void init() throws Exception {} + + /** + * This class is a Callable implementation to count the number of entries for an VuFind query. + * This Callable use as query value to CrossRef 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; + + public CountByQueryCallable(String queryString) { + query = new Query(); + query.addParameter("query", queryString); + } + + public CountByQueryCallable(Query query) { + this.query = query; + } + + @Override + public Integer call() throws Exception { + Integer start = 0; + Integer count = 1; + int page = start / count + 1; + URIBuilder uriBuilder = new URIBuilder(urlSearch); + uriBuilder.addParameter("type", "AllField"); + uriBuilder.addParameter("page", String.valueOf(page)); + uriBuilder.addParameter("limit", count.toString()); + uriBuilder.addParameter("prettyPrint", String.valueOf(true)); + uriBuilder.addParameter("lookfor", query.getParameterAsClass("query", String.class)); + String responseString = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + JsonNode node = convertStringJsonToJsonNode(responseString); + JsonNode resultCountNode = node.get("resultCount"); + return resultCountNode.intValue(); + } + } + + /** + * This class is a Callable implementation to get an VuFind entry using VuFind id + * The id 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 GetByVuFindIdCallable implements Callable { + + private String id; + + private String fields; + + public GetByVuFindIdCallable(String id, String fields) { + this.id = id; + if (fields != null && fields.length() > 0) { + this.fields = fields; + } else { + this.fields = null; + } + } + + @Override + public String call() throws Exception { + URIBuilder uriBuilder = new URIBuilder(url); + uriBuilder.addParameter("id", id); + uriBuilder.addParameter("prettyPrint", "false"); + if (StringUtils.isNotBlank(fields)) { + for (String field : fields.split(",")) { + uriBuilder.addParameter("field[]", field); + } + } + String response = liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), + new HashMap()); + return response; + } + } + + /** + * This class is a Callable implementation to get VuFind 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 String fields; + + public SearchByQueryCallable(String queryString, Integer maxResult, Integer start, String fields) { + query = new Query(); + query.addParameter("query", queryString); + query.addParameter("count", maxResult); + query.addParameter("start", start); + if (StringUtils.isNotBlank(fields)) { + this.fields = fields; + } else { + this.fields = null; + } + } + + public SearchByQueryCallable(Query query, String fields) { + this.query = query; + if (StringUtils.isNotBlank(fields)) { + this.fields = fields; + } else { + this.fields = null; + } + } + + @Override + public String call() throws Exception { + Integer start = query.getParameterAsClass("start", Integer.class); + Integer count = query.getParameterAsClass("count", Integer.class); + int page = count != 0 ? start / count : 0; + URIBuilder uriBuilder = new URIBuilder(urlSearch); + uriBuilder.addParameter("type", "AllField"); + //page looks 1 based (start = 0, count = 20 -> page = 0) + uriBuilder.addParameter("page", String.valueOf(page + 1)); + uriBuilder.addParameter("limit", count.toString()); + uriBuilder.addParameter("prettyPrint", String.valueOf(true)); + uriBuilder.addParameter("lookfor", query.getParameterAsClass("query", String.class)); + if (StringUtils.isNotBlank(fields)) { + for (String field : fields.split(",")) { + uriBuilder.addParameter("field[]", field); + } + } + return liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), new HashMap()); + } + + } + + /** + * This class is a Callable implementation to search VuFind entries using author and title. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ + public class FindMatchingRecordsCallable implements Callable { + + private Query query; + + private String fields; + + public FindMatchingRecordsCallable(Query query) { + this.query = query; + } + + @Override + public String call() throws Exception { + String author = query.getParameterAsClass("author", String.class); + String title = query.getParameterAsClass("title", String.class); + Integer start = query.getParameterAsClass("start", Integer.class); + Integer count = query.getParameterAsClass("count", Integer.class); + int page = count != 0 ? start / count : 0; + URIBuilder uriBuilder = new URIBuilder(url); + uriBuilder.addParameter("type", "AllField"); + //pagination is 1 based (first page: start = 0, count = 20 -> page = 0 -> +1 = 1) + uriBuilder.addParameter("page", String.valueOf(page ++)); + uriBuilder.addParameter("limit", count.toString()); + uriBuilder.addParameter("prettyPrint", "true"); + if (fields != null && !fields.isEmpty()) { + for (String field : fields.split(",")) { + uriBuilder.addParameter("field[]", field); + } + } + String filter = StringUtils.EMPTY; + if (StringUtils.isNotBlank(author)) { + filter = "author:" + author; + } + if (StringUtils.isNotBlank(title)) { + if (StringUtils.isNotBlank(filter)) { + filter = filter + " AND title:" + title; + } else { + filter = "title:" + title; + } + } + uriBuilder.addParameter("lookfor", filter); + return liveImportClient.executeHttpGetRequest(1000, uriBuilder.toString(), new HashMap()); + } + + } + + private JsonNode convertStringJsonToJsonNode(String json) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode body = null; + try { + body = mapper.readTree(json); + } catch (JsonProcessingException e) { + log.error("Unable to process json response.", e); + } + return body; + } + + private List extractMetadataFromRecordList(String records) { + List recordsResult = new ArrayList<>(); + JsonNode jsonNode = convertStringJsonToJsonNode(records); + JsonNode node = jsonNode.get("records"); + if (Objects.nonNull(node) && node.isArray()) { + Iterator nodes = node.iterator(); + while (nodes.hasNext()) { + recordsResult.add(transformSourceRecords(nodes.next().toString())); + } + } + return recordsResult; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUrlSearch() { + return urlSearch; + } + + public void setUrlSearch(String urlSearch) { + this.urlSearch = urlSearch; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/importer/external/vufind/metadatamapping/VuFindFieldMapping.java b/dspace-api/src/main/java/org/dspace/importer/external/vufind/metadatamapping/VuFindFieldMapping.java new file mode 100644 index 0000000000..b14927a14c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/importer/external/vufind/metadatamapping/VuFindFieldMapping.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.importer.external.vufind.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 VuFind metadatum fields on the DSpace metadatum fields + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +@SuppressWarnings("rawtypes") +public class VuFindFieldMapping 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 + @SuppressWarnings("unchecked") + @Resource(name = "vufindMetadataFieldMap") + public void setMetadataFieldMap(Map metadataFieldMap) { + super.setMetadataFieldMap(metadataFieldMap); + } + +} 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 5e69ee9c42..787f64b68c 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,6 +115,20 @@ + + + + + + + + + + + + + + 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 0ad04f57fb..4a39f67b81 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 @@ -6,6 +6,8 @@ + + diff --git a/dspace-api/src/test/java/org/dspace/core/ContextTest.java b/dspace-api/src/test/java/org/dspace/core/ContextTest.java index 811582c569..c6cd849d21 100644 --- a/dspace-api/src/test/java/org/dspace/core/ContextTest.java +++ b/dspace-api/src/test/java/org/dspace/core/ContextTest.java @@ -8,6 +8,7 @@ package org.dspace.core; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; @@ -511,9 +512,8 @@ public class ContextTest extends AbstractUnitTest { // Now get our special groups List specialGroups = instance.getSpecialGroups(); - assertThat("testGetSpecialGroup 0", specialGroups.size(), equalTo(2)); - assertThat("testGetSpecialGroup 1", specialGroups.get(0), equalTo(group)); - assertThat("testGetSpecialGroup 1", specialGroups.get(1), equalTo(adminGroup)); + assertThat("testGetSpecialGroup size", specialGroups.size(), equalTo(2)); + assertThat("testGetSpecialGroup content", specialGroups, hasItems(group, adminGroup)); // Cleanup our context & group groupService.delete(instance, group); diff --git a/dspace-api/src/test/java/org/dspace/util/DoiCheckTest.java b/dspace-api/src/test/java/org/dspace/util/DoiCheckTest.java new file mode 100644 index 0000000000..17e21779d4 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/util/DoiCheckTest.java @@ -0,0 +1,65 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.util; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.text.ParseException; +import java.util.Arrays; +import java.util.List; + +import org.apache.commons.lang.StringUtils; +import org.dspace.importer.external.service.DoiCheck; +import org.junit.Test; + +/** + * Test class for the DoiCheck + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public class DoiCheckTest { + + @Test + public void checkDOIsTest() throws ParseException { + for (String doi : DOIsToTest()) { + assertTrue("The: " + doi + " is a doi!", DoiCheck.isDoi(doi)); + } + } + + @Test + public void checkWrongDOIsTest() throws ParseException { + for (String key : wrongDOIsToTest()) { + assertFalse("This : " + key + " isn't a doi!", DoiCheck.isDoi(key)); + } + } + + private List DOIsToTest() { + return Arrays.asList( + "10.1430/8105", + "10.1038/nphys1170", + "10.1002/0470841559.ch1", + "10.1594/PANGAEA.726855", + "10.1594/GFZ.GEOFON.gfz2009kciu", + "10.3866/PKU.WHXB201112303", + "10.11467/isss2003.7.1_11", + "10.3972/water973.0145.db" + ); + } + + private List wrongDOIsToTest() { + return Arrays.asList( + StringUtils.EMPTY, + "123456789", + "nphys1170/10.1038", + "10.", "10", + "10.1038/" + ); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java index 7d9cb470f9..313fe2de60 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java @@ -7,8 +7,13 @@ */ package org.dspace.app.rest; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; + import java.sql.SQLException; import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -19,9 +24,11 @@ import org.dspace.app.rest.model.AuthenticationStatusRest; import org.dspace.app.rest.model.AuthenticationTokenRest; import org.dspace.app.rest.model.AuthnRest; import org.dspace.app.rest.model.EPersonRest; +import org.dspace.app.rest.model.GroupRest; import org.dspace.app.rest.model.hateoas.AuthenticationStatusResource; import org.dspace.app.rest.model.hateoas.AuthenticationTokenResource; import org.dspace.app.rest.model.hateoas.AuthnResource; +import org.dspace.app.rest.model.hateoas.EmbeddedPage; import org.dspace.app.rest.model.wrapper.AuthenticationToken; import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.security.RestAuthenticationService; @@ -34,6 +41,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PagedResourcesAssembler; +import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -109,6 +120,8 @@ public class AuthenticationRestController implements InitializingBean { if (context.getCurrentUser() != null) { ePersonRest = converter.toRest(context.getCurrentUser(), projection); } + List groupList = context.getSpecialGroups().stream() + .map(g -> (GroupRest) converter.toRest(g, projection)).collect(Collectors.toList()); AuthenticationStatusRest authenticationStatusRest = new AuthenticationStatusRest(ePersonRest); // When not authenticated add WWW-Authenticate so client can retrieve all available authentication methods @@ -120,11 +133,41 @@ public class AuthenticationRestController implements InitializingBean { } authenticationStatusRest.setAuthenticationMethod(context.getAuthenticationMethod()); authenticationStatusRest.setProjection(projection); + authenticationStatusRest.setSpecialGroups(groupList); + AuthenticationStatusResource authenticationStatusResource = converter.toResource(authenticationStatusRest); return authenticationStatusResource; } + /** + * Check the current user's authentication status (i.e. whether they are authenticated or not) and, + * if authenticated, retrieves the current context's special groups. + * @param page + * @param assembler + * @param request + * @param response + * @return + * @throws SQLException + */ + @RequestMapping(value = "/status/specialGroups", method = RequestMethod.GET) + public EntityModel retrieveSpecialGroups(Pageable page, PagedResourcesAssembler assembler, + HttpServletRequest request, HttpServletResponse response) + throws SQLException { + Context context = ContextUtil.obtainContext(request); + Projection projection = utils.obtainProjection(); + + List groupList = context.getSpecialGroups().stream() + .map(g -> (GroupRest) converter.toRest(g, projection)).collect(Collectors.toList()); + Page groupPage = (Page) utils.getPage(groupList, page); + Link link = linkTo( + methodOn(AuthenticationRestController.class).retrieveSpecialGroups(page, assembler, request, response)) + .withSelfRel(); + + return EntityModel.of(new EmbeddedPage(link.getHref(), + groupPage.map(converter::toResource), null, "specialGroups")); + } + /** * Check whether the login has succeeded or not. The actual login is performed by one of the enabled login filters * (e.g. {@link org.dspace.app.rest.security.StatelessLoginFilter}). diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationStatusRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationStatusRest.java index 81a59bbd69..784c06e059 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationStatusRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/AuthenticationStatusRest.java @@ -7,9 +7,12 @@ */ package org.dspace.app.rest.model; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonIgnore; import org.dspace.app.rest.RestResourceController; + /** * Find out your authentication status. */ @@ -18,7 +21,11 @@ public class AuthenticationStatusRest extends BaseObjectRest { private boolean authenticated; private String authenticationMethod; + private EPersonRest ePersonRest; + private List specialGroups; + public static final String NAME = "status"; + public static final String SPECIALGROUPS = "specialGroups"; public static final String CATEGORY = RestAddressableModel.AUTHENTICATION; @Override @@ -41,9 +48,6 @@ public class AuthenticationStatusRest extends BaseObjectRest { return RestResourceController.class; } - - private EPersonRest ePersonRest; - public AuthenticationStatusRest() { setOkay(true); setAuthenticated(false); @@ -90,4 +94,14 @@ public class AuthenticationStatusRest extends BaseObjectRest { public void setAuthenticationMethod(final String authenticationMethod) { this.authenticationMethod = authenticationMethod; } + + public void setSpecialGroups(List groupList) { + this.specialGroups = groupList; + } + + @LinkRest(name = "specialGroups") + @JsonIgnore + public List getSpecialGroups() { + return specialGroups; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GroupRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GroupRestPermissionEvaluatorPlugin.java index 6f168efc91..d2675399d8 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GroupRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GroupRestPermissionEvaluatorPlugin.java @@ -68,6 +68,11 @@ public class GroupRestPermissionEvaluatorPlugin extends RestObjectPermissionEval Group group = groupService.find(context, dsoId); + // if the group is one of the special groups of the context it is readable + if (context.getSpecialGroups().contains(group)) { + return true; + } + // anonymous user if (ePerson == null) { return false; diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java new file mode 100644 index 0000000000..af845581f3 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AbstractLiveImportIntegrationTest.java @@ -0,0 +1,100 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.codec.binary.StringUtils; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.tools.ant.filters.StringInputStream; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; + +/** + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class AbstractLiveImportIntegrationTest extends AbstractControllerIntegrationTest { + + protected void matchRecords(ArrayList recordsImported, ArrayList records2match) { + assertEquals(records2match.size(), recordsImported.size()); + for (int i = 0; i < recordsImported.size(); i++) { + ImportRecord firstImported = recordsImported.get(i); + ImportRecord first2match = records2match.get(i); + checkMetadataValue(firstImported.getValueList(), first2match.getValueList()); + } + } + + private void checkMetadataValue(List list, List list2) { + assertEquals(list.size(), list2.size()); + for (int i = 0; i < list.size(); i++) { + assertTrue(sameMetadatum(list.get(i), list2.get(i))); + } + } + + private boolean sameMetadatum(MetadatumDTO metadatum, MetadatumDTO metadatum2) { + if (StringUtils.equals(metadatum.getSchema(), metadatum2.getSchema()) && + StringUtils.equals(metadatum.getElement(), metadatum2.getElement()) && + StringUtils.equals(metadatum.getQualifier(), metadatum2.getQualifier()) && + StringUtils.equals(metadatum.getValue(), metadatum2.getValue())) { + return true; + } + return false; + } + + protected MetadatumDTO createMetadatumDTO(String schema, String element, String qualifier, String value) { + MetadatumDTO metadatumDTO = new MetadatumDTO(); + metadatumDTO.setSchema(schema); + metadatumDTO.setElement(element); + metadatumDTO.setQualifier(qualifier); + metadatumDTO.setValue(value); + return metadatumDTO; + } + + protected CloseableHttpResponse mockResponse(String xmlExample, int statusCode, String reason) + throws UnsupportedEncodingException { + BasicHttpEntity basicHttpEntity = new BasicHttpEntity(); + basicHttpEntity.setChunked(true); + basicHttpEntity.setContent(new StringInputStream(xmlExample)); + + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + when(response.getStatusLine()).thenReturn(statusLine(statusCode, reason)); + when(response.getEntity()).thenReturn(basicHttpEntity); + return response; + } + + protected StatusLine statusLine(int statusCode, String reason) { + return new StatusLine() { + @Override + public ProtocolVersion getProtocolVersion() { + return new ProtocolVersion("http", 1, 1); + } + + @Override + public int getStatusCode() { + return statusCode; + } + + @Override + public String getReasonPhrase() { + return reason; + } + }; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java index df8986b4f2..8c5f333505 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/AuthenticationRestControllerIT.java @@ -53,6 +53,7 @@ import org.dspace.app.rest.converter.EPersonConverter; import org.dspace.app.rest.matcher.AuthenticationStatusMatcher; import org.dspace.app.rest.matcher.AuthorizationMatcher; import org.dspace.app.rest.matcher.EPersonMatcher; +import org.dspace.app.rest.matcher.GroupMatcher; import org.dspace.app.rest.matcher.HalMatcher; import org.dspace.app.rest.model.AuthnRest; import org.dspace.app.rest.model.EPersonRest; @@ -120,6 +121,10 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio "org.dspace.authenticate.IPAuthentication", "org.dspace.authenticate.ShibAuthentication" }; + public static final String[] PASS_AND_IP = { + "org.dspace.authenticate.PasswordAuthentication", + "org.dspace.authenticate.IPAuthentication" + }; // see proxies.trusted.ipranges in local.cfg public static final String TRUSTED_IP = "7.7.7.7"; @@ -174,6 +179,101 @@ public class AuthenticationRestControllerIT extends AbstractControllerIntegratio .andExpect(status().isNoContent()); } + /** + * This test verifies: + * - that a logged in via password user finds the expected specialGroupPwd in _embedded.specialGroups; + * - that a logged in via password and specific IP user finds the expected specialGroupPwd and specialGroupIP + * in _embedded.specialGroups; + * - that a not logged in user with a specific IP finds the expected specialGroupIP in _embedded.specialGroups; + * @throws Exception + */ + @Test + public void testStatusGetSpecialGroups() throws Exception { + context.turnOffAuthorisationSystem(); + + Group specialGroupPwd = GroupBuilder.createGroup(context) + .withName("specialGroupPwd") + .build(); + Group specialGroupIP = GroupBuilder.createGroup(context) + .withName("specialGroupIP") + .build(); + + configurationService.setProperty("plugin.sequence.org.dspace.authenticate.AuthenticationMethod", PASS_AND_IP); + configurationService.setProperty("authentication-password.login.specialgroup","specialGroupPwd"); + configurationService.setProperty("authentication-ip.specialGroupIP", "123.123.123.123"); + context.restoreAuthSystemState(); + + String token = getAuthToken(eperson.getEmail(), password); + + getClient(token).perform(get("/api/authn/status").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", AuthenticationStatusMatcher.matchFullEmbeds())) + .andExpect(jsonPath("$", AuthenticationStatusMatcher.matchLinks())) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.authenticationMethod", is("password"))) + .andExpect(jsonPath("$.type", is("status"))) + .andExpect(jsonPath("$._links.specialGroups.href", startsWith(REST_SERVER_URL))) + .andExpect(jsonPath("$._embedded.specialGroups._embedded.specialGroups", + Matchers.containsInAnyOrder( + GroupMatcher.matchGroupWithName("specialGroupPwd")))); + + // try the special groups link endpoint in the same scenario than above + getClient(token).perform(get("/api/authn/status/specialGroups").param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.specialGroups", + Matchers.containsInAnyOrder( + GroupMatcher.matchGroupWithName("specialGroupPwd")))); + + getClient(token).perform(get("/api/authn/status").param("projection", "full") + .with(ip("123.123.123.123"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", AuthenticationStatusMatcher.matchFullEmbeds())) + .andExpect(jsonPath("$", AuthenticationStatusMatcher.matchLinks())) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(true))) + .andExpect(jsonPath("$.authenticationMethod", is("password"))) + .andExpect(jsonPath("$.type", is("status"))) + .andExpect(jsonPath("$._links.specialGroups.href", startsWith(REST_SERVER_URL))) + .andExpect(jsonPath("$._embedded.specialGroups._embedded.specialGroups", + Matchers.containsInAnyOrder( + GroupMatcher.matchGroupWithName("specialGroupPwd"), + GroupMatcher.matchGroupWithName("specialGroupIP")))); + + // try the special groups link endpoint in the same scenario than above + getClient(token).perform(get("/api/authn/status/specialGroups").param("projection", "full") + .with(ip("123.123.123.123"))) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.specialGroups", + Matchers.containsInAnyOrder( + GroupMatcher.matchGroupWithName("specialGroupPwd"), + GroupMatcher.matchGroupWithName("specialGroupIP")))); + + getClient().perform(get("/api/authn/status").param("projection", "full").with(ip("123.123.123.123"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", AuthenticationStatusMatcher.matchFullEmbeds())) + // fails due to bug https://github.com/DSpace/DSpace/issues/8274 + //.andExpect(jsonPath("$", AuthenticationStatusMatcher.matchLinks())) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.okay", is(true))) + .andExpect(jsonPath("$.authenticated", is(false))) + .andExpect(jsonPath("$._embedded.specialGroups._embedded.specialGroups", + Matchers.containsInAnyOrder(GroupMatcher.matchGroupWithName("specialGroupIP")))); + + // try the special groups link endpoint in the same scenario than above + getClient().perform(get("/api/authn/status/specialGroups").param("projection", "full") + .with(ip("123.123.123.123"))) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.specialGroups", + Matchers.containsInAnyOrder( + GroupMatcher.matchGroupWithName("specialGroupIP")))); + } + @Test @Ignore // Ignored until an endpoint is added to return all groups. Anonymous is not considered a direct group. diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java new file mode 100644 index 0000000000..9a0d39225c --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/CrossRefImportMetadataSourceServiceIT.java @@ -0,0 +1,199 @@ +/** + * 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.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import javax.el.MethodNotFoundException; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Item; +import org.dspace.importer.external.crossref.CrossRefImportMetadataSourceServiceImpl; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.liveimportclient.service.LiveImportClientImpl; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link CrossRefImportMetadataSourceServiceImpl} + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class CrossRefImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { + + @Autowired + private LiveImportClientImpl liveImportClientImpl; + + @Autowired + private CrossRefImportMetadataSourceServiceImpl crossRefServiceImpl; + + @Test + public void crossRefImportMetadataGetRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + try (InputStream crossRefResp = getClass().getResourceAsStream("crossRef-test.json")) { + + String crossRefRespXmlResp = IOUtils.toString(crossRefResp, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(crossRefRespXmlResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + ArrayList collection2match = getRecords(); + Collection recordsImported = crossRefServiceImpl.getRecords("test query", 0, 2); + assertEquals(2, recordsImported.size()); + matchRecords(new ArrayList(recordsImported), collection2match); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test + public void crossRefImportMetadataGetRecordsCountTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + try (InputStream crossRefResp = getClass().getResourceAsStream("crossRef-test.json")) { + String crossRefRespXmlResp = IOUtils.toString(crossRefResp, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(crossRefRespXmlResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + int tot = crossRefServiceImpl.getRecordsCount("test query"); + assertEquals(10, tot); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test + public void crossRefImportMetadataGetRecordByIdTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream crossRefResp = getClass().getResourceAsStream("crossRef-by-id.json")) { + + String crossRefRespXmlResp = IOUtils.toString(crossRefResp, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(crossRefRespXmlResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + ArrayList collection2match = getRecords(); + collection2match.remove(1); + ImportRecord recordImported = crossRefServiceImpl.getRecord("10.26693/jmbs01.02.184"); + assertNotNull(recordImported); + Collection recordsImported = Arrays.asList(recordImported); + matchRecords(new ArrayList(recordsImported), collection2match); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test(expected = MethodNotFoundException.class) + public void crossRefImportMetadataFindMatchingRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + org.dspace.content.Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + Item testItem = ItemBuilder.createItem(context, col1) + .withTitle("test item") + .withIssueDate("2021") + .build(); + + context.restoreAuthSystemState(); + crossRefServiceImpl.findMatchingRecords(testItem); + } + + private ArrayList getRecords() { + ArrayList records = new ArrayList<>(); + //define first record + List metadatums = new ArrayList(); + MetadatumDTO title = createMetadatumDTO("dc", "title", null, + "State of Awareness of Freshers’ Groups Chortkiv State" + + " Medical College of Prevention of Iodine Deficiency Diseases"); + MetadatumDTO author = createMetadatumDTO("dc", "contributor", "author", "L.V. Senyuk"); + MetadatumDTO type = createMetadatumDTO("dc", "type", null, "journal-article"); + MetadatumDTO date = createMetadatumDTO("dc", "date", "issued", "2016"); + MetadatumDTO ispartof = createMetadatumDTO("dc", "relation", "ispartof", + "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu"); + MetadatumDTO doi = createMetadatumDTO("dc", "identifier", "doi", "10.26693/jmbs01.02.184"); + MetadatumDTO issn = createMetadatumDTO("dc", "identifier", "issn", "2415-3060"); + MetadatumDTO volume = createMetadatumDTO("oaire", "citation", "volume", "1"); + MetadatumDTO issue = createMetadatumDTO("oaire", "citation", "issue", "2"); + + metadatums.add(title); + metadatums.add(author); + metadatums.add(date); + metadatums.add(type); + metadatums.add(ispartof); + metadatums.add(doi); + metadatums.add(issn); + metadatums.add(volume); + metadatums.add(issue); + + ImportRecord firstrRecord = new ImportRecord(metadatums); + + //define second record + List metadatums2 = new ArrayList(); + MetadatumDTO title2 = createMetadatumDTO("dc", "title", null, + "Ischemic Heart Disease and Role of Nurse of Cardiology Department"); + MetadatumDTO author2 = createMetadatumDTO("dc", "contributor", "author", "K. І. Kozak"); + MetadatumDTO type2 = createMetadatumDTO("dc", "type", null, "journal-article"); + MetadatumDTO date2 = createMetadatumDTO("dc", "date", "issued", "2016"); + MetadatumDTO ispartof2 = createMetadatumDTO("dc", "relation", "ispartof", + "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu"); + MetadatumDTO doi2 = createMetadatumDTO("dc", "identifier", "doi", "10.26693/jmbs01.02.105"); + MetadatumDTO issn2 = createMetadatumDTO("dc", "identifier", "issn", "2415-3060"); + MetadatumDTO volume2 = createMetadatumDTO("oaire", "citation", "volume", "1"); + MetadatumDTO issue2 = createMetadatumDTO("oaire", "citation", "issue", "2"); + + metadatums2.add(title2); + metadatums2.add(author2); + metadatums2.add(date2); + metadatums2.add(type2); + metadatums2.add(ispartof2); + metadatums2.add(doi2); + metadatums2.add(issn2); + metadatums2.add(volume2); + metadatums2.add(issue2); + + ImportRecord secondRecord = new ImportRecord(metadatums2); + records.add(firstrRecord); + records.add(secondRecord); + return records; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/VuFindImportMetadataSourceServiceIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VuFindImportMetadataSourceServiceIT.java new file mode 100644 index 0000000000..c3063ca234 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/VuFindImportMetadataSourceServiceIT.java @@ -0,0 +1,199 @@ +/** + * 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.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import javax.el.MethodNotFoundException; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.impl.client.CloseableHttpClient; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Item; +import org.dspace.importer.external.datamodel.ImportRecord; +import org.dspace.importer.external.liveimportclient.service.LiveImportClientImpl; +import org.dspace.importer.external.metadatamapping.MetadatumDTO; +import org.dspace.importer.external.vufind.VuFindImportMetadataSourceServiceImpl; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link VuFindImportMetadataSourceServiceImpl} + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class VuFindImportMetadataSourceServiceIT extends AbstractLiveImportIntegrationTest { + + @Autowired + private LiveImportClientImpl liveImportClientImpl; + + @Autowired + private VuFindImportMetadataSourceServiceImpl vuFindService; + + @Test + public void vuFindImportMetadataGetRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream vuFindRespIS = getClass().getResourceAsStream("vuFind-generic.json")) { + + String vuFindResp = IOUtils.toString(vuFindRespIS, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(vuFindResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + ArrayList collection2match = getRecords(); + Collection recordsImported = vuFindService.getRecords("test query", 0, 2); + assertEquals(2, recordsImported.size()); + matchRecords(new ArrayList<>(recordsImported), collection2match); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test + public void vuFindImportMetadataGetRecordsCountTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream vuFindRespIS = getClass().getResourceAsStream("vuFind-generic.json")) { + String vuFindResp = IOUtils.toString(vuFindRespIS, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(vuFindResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + int tot = vuFindService.getRecordsCount("test query"); + assertEquals(1994, tot); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test + public void vuFindImportMetadataGetRecordByIdTest() throws Exception { + context.turnOffAuthorisationSystem(); + CloseableHttpClient originalHttpClient = liveImportClientImpl.getHttpClient(); + CloseableHttpClient httpClient = Mockito.mock(CloseableHttpClient.class); + + try (InputStream vuFindByIdResp = getClass().getResourceAsStream("vuFind-by-id.json")) { + + String vuFindResp = IOUtils.toString(vuFindByIdResp, Charset.defaultCharset()); + + liveImportClientImpl.setHttpClient(httpClient); + CloseableHttpResponse response = mockResponse(vuFindResp, 200, "OK"); + when(httpClient.execute(ArgumentMatchers.any())).thenReturn(response); + + context.restoreAuthSystemState(); + ArrayList collection2match = getRecords(); + collection2match.remove(1); + ImportRecord recordImported = vuFindService.getRecord("653510"); + assertNotNull(recordImported); + Collection recordsImported = Arrays.asList(recordImported); + matchRecords(new ArrayList<>(recordsImported), collection2match); + } finally { + liveImportClientImpl.setHttpClient(originalHttpClient); + } + } + + @Test(expected = MethodNotFoundException.class) + public void vuFindImportMetadataFindMatchingRecordsTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + org.dspace.content.Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + Item testItem = ItemBuilder.createItem(context, col1) + .withTitle("test item") + .withIssueDate("2021") + .build(); + context.restoreAuthSystemState(); + vuFindService.findMatchingRecords(testItem); + } + + private ArrayList getRecords() { + ArrayList records = new ArrayList<>(); + //define first record + List metadatums = new ArrayList(); + MetadatumDTO identifierOther = createMetadatumDTO("dc", "identifier", "other", "653510"); + MetadatumDTO language = createMetadatumDTO("dc", "language", "iso", "Italian"); + MetadatumDTO title = createMetadatumDTO("dc", "title", null, + "La pianta marmorea di Roma antica: Forma urbis Romae /"); + MetadatumDTO subject = createMetadatumDTO("dc", "subject", null, "Rome (Italy)"); + MetadatumDTO subject2 = createMetadatumDTO("dc", "subject", null, "Maps"); + MetadatumDTO subject3 = createMetadatumDTO("dc", "subject", null, "Early works to 1800."); + MetadatumDTO subject4 = createMetadatumDTO("dc", "subject", null, "Rome (Italy)"); + MetadatumDTO subject5 = createMetadatumDTO("dc", "subject", null, "Antiquities"); + MetadatumDTO subject6 = createMetadatumDTO("dc", "subject", null, "Maps."); + MetadatumDTO identifier = createMetadatumDTO("dc", "identifier", null, + "http://hdl.handle.net/20.500.12390/231"); + metadatums.add(identifierOther); + metadatums.add(language); + metadatums.add(title); + metadatums.add(identifier); + metadatums.add(subject); + metadatums.add(subject2); + metadatums.add(subject3); + metadatums.add(subject4); + metadatums.add(subject5); + metadatums.add(subject6); + + ImportRecord firstrRecord = new ImportRecord(metadatums); + + //define second record + List metadatums2 = new ArrayList(); + MetadatumDTO identifierOther2 = createMetadatumDTO("dc", "identifier", "other", "1665326"); + MetadatumDTO language2 = createMetadatumDTO("dc", "language", "iso", "English"); + MetadatumDTO title2 = createMetadatumDTO("dc", "title", null, + "Expert frames : scientific and policy practices of Roma classification /"); + MetadatumDTO subject7 = createMetadatumDTO("dc", "subject", null, "Public opinion"); + MetadatumDTO subject8 = createMetadatumDTO("dc", "subject", null, "Europe."); + MetadatumDTO subject9 = createMetadatumDTO("dc", "subject", null, "Stereotypes (Social psychology)"); + MetadatumDTO subject10 = createMetadatumDTO("dc", "subject", null, "Romanies"); + MetadatumDTO subject11 = createMetadatumDTO("dc", "subject", null, "Public opinion."); + MetadatumDTO identifier2 = createMetadatumDTO("dc", "identifier", null, + "http://ezproxy.villanova.edu/login?URL=http://www.jstor.org/stable/10.7829/j.ctt1ggjj08"); + metadatums2.add(identifierOther2); + metadatums2.add(language2); + metadatums2.add(title2); + metadatums2.add(identifier2); + metadatums2.add(subject7); + metadatums2.add(subject8); + metadatums2.add(subject9); + metadatums2.add(subject10); + metadatums2.add(subject11); + + ImportRecord secondRecord = new ImportRecord(metadatums2); + records.add(firstrRecord); + records.add(secondRecord); + return records; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/AuthenticationStatusMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/AuthenticationStatusMatcher.java index 66d4ca19d4..1e986c2181 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/AuthenticationStatusMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/AuthenticationStatusMatcher.java @@ -27,7 +27,8 @@ public class AuthenticationStatusMatcher { */ public static Matcher matchFullEmbeds() { return matchEmbeds( - "eperson" + "eperson", + "specialGroups" ); } @@ -36,7 +37,9 @@ public class AuthenticationStatusMatcher { */ public static Matcher matchLinks() { return allOf( + //FIXME https://github.com/DSpace/DSpace/issues/8274 hasJsonPath("$._links.eperson.href", containsString("api/eperson/epersons")), - hasJsonPath("$._links.self.href", containsString("api/authn/status"))); + hasJsonPath("$._links.self.href", containsString("api/authn/status")), + hasJsonPath("$._links.specialGroups.href", containsString("api/authn/status/specialGroups"))); } } diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/crossRef-by-id.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/crossRef-by-id.json new file mode 100644 index 0000000000..d889319ca7 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/crossRef-by-id.json @@ -0,0 +1,169 @@ +{ + "status":"ok", + "message-type":"work", + "message-version":"1.0.0", + "message":{ + "indexed":{ + "date-parts":[ + [ + 2022, + 4, + 5 + ] + ], + "date-time":"2022-04-05T22:05:30Z", + "timestamp":1649196330913 + }, + "reference-count":0, + "publisher":"Petro Mohyla Black Sea National University", + "issue":"2", + "content-domain":{ + "domain":[ + + ], + "crossmark-restriction":false + }, + "short-container-title":[ + "Ukr. \u017e. med. b\u00ecol. sportu" + ], + "published-print":{ + "date-parts":[ + [ + 2016, + 5, + 19 + ] + ] + }, + "DOI":"10.26693\/jmbs01.02.184", + "type":"journal-article", + "created":{ + "date-parts":[ + [ + 2017, + 9, + 7 + ] + ], + "date-time":"2017-09-07T13:30:46Z", + "timestamp":1504791046000 + }, + "page":"184-187", + "source":"Crossref", + "is-referenced-by-count":0, + "title":[ + "State of Awareness of Freshers\u2019 Groups Chortkiv State Medical College of Prevention of Iodine Deficiency Diseases" + ], + "prefix":"10.26693", + "volume":"1", + "author":[ + { + "given":"L.V.", + "family":"Senyuk", + "sequence":"first", + "affiliation":[ + + ] + }, + { + "name":"Chortkiv State Medical College 7, Gogola St., Chortkiv, Ternopil region 48500, Ukraine", + "sequence":"first", + "affiliation":[ + + ] + } + ], + "member":"11225", + "published-online":{ + "date-parts":[ + [ + 2016, + 5, + 19 + ] + ] + }, + "container-title":[ + "Ukra\u00efns\u02b9kij \u017eurnal medicini, b\u00ecolog\u00ec\u00ef ta sportu" + ], + "original-title":[ + "\u0421\u0422\u0410\u041d \u041e\u0411\u0406\u0417\u041d\u0410\u041d\u041e\u0421\u0422\u0406 \u0421\u0422\u0423\u0414\u0415\u041d\u0422\u0406\u0412 \u041d\u041e\u0412\u041e\u041d\u0410\u0411\u0420\u0410\u041d\u0418\u0425 \u0413\u0420\u0423\u041f \u0427\u041e\u0420\u0422\u041a\u0406\u0412\u0421\u042c\u041a\u041e\u0413\u041e \u0414\u0415\u0420\u0416\u0410\u0412\u041d\u041e\u0413\u041e \u041c\u0415\u0414\u0418\u0427\u041d\u041e\u0413\u041e \u041a\u041e\u041b\u0415\u0414\u0416\u0423 \u0417 \u041f\u0418\u0422\u0410\u041d\u042c \u041f\u0420\u041e\u0424\u0406\u041b\u0410\u041a\u0422\u0418\u041a\u0418 \u0419\u041e\u0414\u041e\u0414\u0415\u0424\u0406\u0426\u0418\u0422\u041d\u0418\u0425 \u0417\u0410\u0425\u0412\u041e\u0420\u042e\u0412\u0410\u041d\u042c" + ], + "deposited":{ + "date-parts":[ + [ + 2017, + 9, + 8 + ] + ], + "date-time":"2017-09-08T10:14:53Z", + "timestamp":1504865693000 + }, + "score":1, + "resource":{ + "primary":{ + "URL":"http:\/\/en.jmbs.com.ua\/archive\/1\/2\/184" + } + }, + "subtitle":[ + + ], + "short-title":[ + + ], + "issued":{ + "date-parts":[ + [ + 2016, + 5, + 19 + ] + ] + }, + "references-count":0, + "journal-issue":{ + "issue":"2", + "published-online":{ + "date-parts":[ + [ + 2016, + 5, + 19 + ] + ] + }, + "published-print":{ + "date-parts":[ + [ + 2016, + 5, + 19 + ] + ] + } + }, + "URL":"http:\/\/dx.doi.org\/10.26693\/jmbs01.02.184", + "relation":{ + + }, + "ISSN":[ + "2415-3060" + ], + "issn-type":[ + { + "value":"2415-3060", + "type":"print" + } + ], + "published":{ + "date-parts":[ + [ + 2016, + 5, + 19 + ] + ] + } + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/crossRef-test.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/crossRef-test.json new file mode 100644 index 0000000000..69a9433868 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/crossRef-test.json @@ -0,0 +1,309 @@ +{ + "status": "ok", + "message-type": "work-list", + "message-version": "1.0.0", + "message": { + "facets": {}, + "total-results": 10, + "items": [ + { + "indexed": { + "date-parts": [ + [ + 2021, + 12, + 22 + ] + ], + "date-time": "2021-12-22T10:58:16Z", + "timestamp": 1640170696598 + }, + "reference-count": 0, + "publisher": "Petro Mohyla Black Sea National University", + "issue": "2", + "content-domain": { + "domain": [], + "crossmark-restriction": false + }, + "short-container-title": [ + "Ukr. ž. med. bìol. sportu" + ], + "published-print": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "DOI": "10.26693/jmbs01.02.184", + "type": "journal-article", + "created": { + "date-parts": [ + [ + 2017, + 9, + 7 + ] + ], + "date-time": "2017-09-07T13:30:46Z", + "timestamp": 1504791046000 + }, + "page": "184-187", + "source": "Crossref", + "is-referenced-by-count": 0, + "title": [ + "State of Awareness of Freshers’ Groups Chortkiv State Medical College of Prevention of Iodine Deficiency Diseases" + ], + "prefix": "10.26693", + "volume": "1", + "author": [ + { + "given": "L.V.", + "family": "Senyuk", + "sequence": "first", + "affiliation": [] + }, + { + "name": "Chortkiv State Medical College 7, Gogola St., Chortkiv, Ternopil region 48500, Ukraine", + "sequence": "first", + "affiliation": [] + } + ], + "member": "11225", + "published-online": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "container-title": [ + "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu" + ], + "original-title": [ + "СТАН ОБІЗНАНОСТІ СТУДЕНТІВ НОВОНАБРАНИХ ГРУП ЧОРТКІВСЬКОГО ДЕРЖАВНОГО МЕДИЧНОГО КОЛЕДЖУ З ПИТАНЬ ПРОФІЛАКТИКИ ЙОДОДЕФІЦИТНИХ ЗАХВОРЮВАНЬ" + ], + "deposited": { + "date-parts": [ + [ + 2017, + 9, + 8 + ] + ], + "date-time": "2017-09-08T10:14:53Z", + "timestamp": 1504865693000 + }, + "score": 22.728952, + "issued": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "references-count": 0, + "journal-issue": { + "issue": "2", + "published-online": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "published-print": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + } + }, + "URL": "http://dx.doi.org/10.26693/jmbs01.02.184", + "ISSN": [ + "2415-3060" + ], + "issn-type": [ + { + "value": "2415-3060", + "type": "print" + } + ], + "published": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + } + }, + { + "indexed": { + "date-parts": [ + [ + 2022, + 3, + 29 + ] + ], + "date-time": "2022-03-29T13:04:48Z", + "timestamp": 1648559088439 + }, + "reference-count": 0, + "publisher": "Petro Mohyla Black Sea National University", + "issue": "2", + "content-domain": { + "domain": [], + "crossmark-restriction": false + }, + "short-container-title": [ + "Ukr. ž. med. bìol. sportu" + ], + "published-print": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "DOI": "10.26693/jmbs01.02.105", + "type": "journal-article", + "created": { + "date-parts": [ + [ + 2017, + 9, + 1 + ] + ], + "date-time": "2017-09-01T10:04:04Z", + "timestamp": 1504260244000 + }, + "page": "105-108", + "source": "Crossref", + "is-referenced-by-count": 0, + "title": [ + "Ischemic Heart Disease and Role of Nurse of Cardiology Department" + ], + "prefix": "10.26693", + "volume": "1", + "author": [ + { + "given": "K. І.", + "family": "Kozak", + "sequence": "first", + "affiliation": [] + }, + { + "name": "Chortkiv State Medical College 7, Gogola St., Chortkiv, Ternopil region 48500, Ukraine", + "sequence": "first", + "affiliation": [] + } + ], + "member": "11225", + "published-online": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "container-title": [ + "Ukraïnsʹkij žurnal medicini, bìologìï ta sportu" + ], + "original-title": [ + "ІШЕМІЧНА ХВОРОБА СЕРЦЯ ТА РОЛЬ МЕДИЧНОЇ СЕСТРИ КАРДІОЛОГІЧНОГО ВІДДІЛЕННЯ" + ], + "deposited": { + "date-parts": [ + [ + 2017, + 9, + 2 + ] + ], + "date-time": "2017-09-02T12:36:15Z", + "timestamp": 1504355775000 + }, + "score": 18.263277, + "resource": { + "primary": { + "URL": "http://en.jmbs.com.ua/archive/1/2/105" + } + }, + "issued": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "references-count": 0, + "journal-issue": { + "issue": "2", + "published-online": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + }, + "published-print": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + } + }, + "URL": "http://dx.doi.org/10.26693/jmbs01.02.105", + "ISSN": [ + "2415-3060" + ], + "issn-type": [ + { + "value": "2415-3060", + "type": "print" + } + ], + "published": { + "date-parts": [ + [ + 2016, + 5, + 19 + ] + ] + } + } + ], + "items-per-page": 2, + "query": { + "start-index": 0, + "search-terms": "chortkiv" + } + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/vuFind-by-id.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/vuFind-by-id.json new file mode 100644 index 0000000000..c992ea5f57 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/vuFind-by-id.json @@ -0,0 +1,44 @@ +{ + "resultCount": 1, + "records": [ + { + "authors": { + "primary": [], + "secondary": { + "Carettoni, Gianfilippo.": [] + }, + "corporate": [] + }, + "formats": [ + "Map", + "Map", + "Book" + ], + "id": "653510", + "languages": [ + "Italian" + ], + "series": [], + "subjects": [ + [ + "Rome (Italy)", + "Maps", + "Early works to 1800." + ], + [ + "Rome (Italy)", + "Antiquities", + "Maps." + ] + ], + "title": "La pianta marmorea di Roma antica: Forma urbis Romae /", + "urls": [ + { + "url": "http://hdl.handle.net/20.500.12390/231", + "desc": "http://hdl.handle.net/20.500.12390/231" + } + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/vuFind-generic.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/vuFind-generic.json new file mode 100644 index 0000000000..889ec89016 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/vuFind-generic.json @@ -0,0 +1,82 @@ +{ + "resultCount": 1994, + "records": [ + { + "authors": { + "primary": [], + "secondary": { + "Carettoni, Gianfilippo.": [] + }, + "corporate": [] + }, + "formats": [ + "Map", + "Map", + "Book" + ], + "id": "653510", + "languages": [ + "Italian" + ], + "series": [], + "subjects": [ + [ + "Rome (Italy)", + "Maps", + "Early works to 1800." + ], + [ + "Rome (Italy)", + "Antiquities", + "Maps." + ] + ], + "title": "La pianta marmorea di Roma antica: Forma urbis Romae /", + "urls": [ + { + "url": "http://hdl.handle.net/20.500.12390/231", + "desc": "http://hdl.handle.net/20.500.12390/231" + } + ] + }, + { + "authors": { + "primary": { + "Surdu, Mihai.": [] + }, + "secondary": [], + "corporate": [] + }, + "formats": [ + "Online", + "Book" + ], + "id": "1665326", + "languages": [ + "English" + ], + "series": [], + "subjects": [ + [ + "Public opinion", + "Europe." + ], + [ + "Stereotypes (Social psychology)" + ], + [ + "Romanies", + "Public opinion." + ] + ], + "title": "Expert frames : scientific and policy practices of Roma classification /", + "urls": [ + { + "url": "http://ezproxy.villanova.edu/login?URL=http://www.jstor.org/stable/10.7829/j.ctt1ggjj08", + "desc": "http://ezproxy.villanova.edu/login?URL=http://www.jstor.org/stable/10.7829/j.ctt1ggjj08" + } + ] + } + ], + "status": "OK" +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/test-config.properties b/dspace-server-webapp/src/test/resources/test-config.properties index 3af96b20fc..37db1c9f9c 100644 --- a/dspace-server-webapp/src/test/resources/test-config.properties +++ b/dspace-server-webapp/src/test/resources/test-config.properties @@ -13,4 +13,4 @@ test.folder.assetstore = ./target/testing/dspace/assetstore test.bitstream = ./target/testing/dspace/assetstore/ConstitutionofIreland.pdf #Path for a test Taskfile for the curate script -test.curateTaskFile = ./target/testing/dspace/assetstore/curate.txt +test.curateTaskFile = ./target/testing/dspace/assetstore/curate.txt \ No newline at end of file diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 86b1775ce0..e62c4af0fd 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1575,4 +1575,5 @@ include = ${module_dir}/translator.cfg include = ${module_dir}/usage-statistics.cfg include = ${module_dir}/versioning.cfg include = ${module_dir}/workflow.cfg -include = ${module_dir}/authority.cfg \ No newline at end of file +include = ${module_dir}/authority.cfg +include = ${module_dir}/external-providers.cfg diff --git a/dspace/config/modules/external-providers.cfg b/dspace/config/modules/external-providers.cfg new file mode 100644 index 0000000000..dbefd93ff1 --- /dev/null +++ b/dspace/config/modules/external-providers.cfg @@ -0,0 +1,22 @@ +#---------------------------------------------------------------# +#------------- EXTERNAL PROVIDER CONFIGURATIONS ----------------# +#---------------------------------------------------------------# +# Configuration properties used solely by external providers # +# as Scopus, Pubmed, CiNii and ect. # +#---------------------------------------------------------------# + + +################################################################# +#---------------------- CrossRef ---------------------------# +#---------------------------------------------------------------# + +crossref.url = https://api.crossref.org/works + +################################################################# +#---------------------- VuFind -----------------------------# +#---------------------------------------------------------------# + +vufind.url = https://vufind.org/advanced_demo/api/v1/record +vufind.url.search = https://vufind.org/advanced_demo/api/v1/search + +################################################################# \ No newline at end of file diff --git a/dspace/config/spring/api/crossref-integration.xml b/dspace/config/spring/api/crossref-integration.xml new file mode 100644 index 0000000000..e01b613833 --- /dev/null +++ b/dspace/config/spring/api/crossref-integration.xml @@ -0,0 +1,141 @@ + + + + + + + + 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/external-services.xml b/dspace/config/spring/api/external-services.xml index d1d5c568b4..c8d1d9a4bc 100644 --- a/dspace/config/spring/api/external-services.xml +++ b/dspace/config/spring/api/external-services.xml @@ -5,6 +5,8 @@ + + @@ -92,5 +94,28 @@ - + + + + + + + Publication + none + + + + + + + + + + Publication + none + + + + + \ No newline at end of file diff --git a/dspace/config/spring/api/vufind-integration.xml b/dspace/config/spring/api/vufind-integration.xml new file mode 100644 index 0000000000..bc6c5def84 --- /dev/null +++ b/dspace/config/spring/api/vufind-integration.xml @@ -0,0 +1,165 @@ + + + + + + + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +