[CST-18015] Added base OpenAlex Publication integration.

[CST-18015] Added base OpenAlex Publication integration.
This commit is contained in:
Adamo
2025-02-06 00:25:19 +01:00
parent 74c38481aa
commit fd47e47f61
11 changed files with 736 additions and 3 deletions

View File

@@ -0,0 +1,62 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.importer.external.metadatamapping.contributor;
import java.util.ArrayList;
import java.util.Collection;
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.Logger;
/**
* @author adamo.fapohunda at 4science.com
**/
public abstract class AbstractJsonPathMetadataProcessor implements JsonPathMetadataProcessor {
@Override
public Collection<String> processMetadata(String json) {
Collection<String> values = new ArrayList<>();
JsonNode jsonNode = convertStringJsonToJsonNode(json);
JsonNode node = jsonNode.at(getPath());
if (node.isArray()) {
for (JsonNode value : node) {
String nodeValue = getStringValue(value);
if (StringUtils.isNotBlank(nodeValue)) {
values.add(nodeValue);
}
}
} else if (!node.isNull() && StringUtils.isNotBlank(node.toString())) {
String nodeValue = getStringValue(node);
if (StringUtils.isNotBlank(nodeValue)) {
values.add(nodeValue);
}
}
return values;
}
protected abstract String getStringValue(JsonNode node);
protected abstract Logger getLogger();
protected abstract String getPath();
private JsonNode convertStringJsonToJsonNode(String json) {
ObjectMapper mapper = new ObjectMapper();
JsonNode body = null;
try {
body = mapper.readTree(json);
} catch (JsonProcessingException e) {
getLogger().error("Unable to process json response.", e);
}
return body;
}
}

View File

@@ -0,0 +1,55 @@
/**
* 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.SortedMap;
import java.util.TreeMap;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* @author adamo.fapohunda at 4science.com
**/
public class InvertedIndexProcessor extends AbstractJsonPathMetadataProcessor {
private static final Logger log = LogManager.getLogger(InvertedIndexProcessor.class);
private String path;
@Override
protected String getStringValue(JsonNode node) {
if (node == null || node.isEmpty()) {
return "";
}
SortedMap<Integer, String> positionMap = new TreeMap<>();
node.at(path).fields().forEachRemaining(entry -> entry.getValue()
.forEach(position ->
positionMap
.put(position.asInt(), entry.getKey())));
return String.join(" ", positionMap.values());
}
@Override
protected Logger getLogger() {
return log;
}
@Override
protected String getPath() {
return "";
}
public void setPath(String path) {
this.path = path;
}
}

View File

@@ -18,6 +18,6 @@ import java.util.Collection;
*/
public interface JsonPathMetadataProcessor {
public Collection<String> processMetadata(String json);
Collection<String> processMetadata(String json);
}

View File

@@ -0,0 +1,57 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.importer.external.openalex.metadatamapping;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.importer.external.metadatamapping.contributor.AbstractJsonPathMetadataProcessor;
/**
* @author adamo.fapohunda at 4science.com
**/
public class OpenAlexDateMetadataProcessor extends AbstractJsonPathMetadataProcessor {
private static final Logger log = LogManager.getLogger(OpenAlexDateMetadataProcessor.class);
private String path;
@Override
protected String getStringValue(JsonNode node) {
if (node == null || !node.isTextual()) {
throw new IllegalArgumentException("Input must be a non-null JsonNode containing a text value");
}
try {
String dateStr = node.asText();
LocalDate date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_DATE);
return date.toString();
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid ISO 8601 date format: " + e.getMessage(), e);
}
}
@Override
protected Logger getLogger() {
return log;
}
@Override
protected String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}

View File

@@ -0,0 +1,61 @@
/**
* 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.openalex.metadatamapping;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.importer.external.metadatamapping.contributor.AbstractJsonPathMetadataProcessor;
/**
* @author adamo.fapohunda at 4science.com
**/
public class OpenAlexIdMetadataProcessor extends AbstractJsonPathMetadataProcessor {
private static final Logger log = LogManager.getLogger(OpenAlexIdMetadataProcessor.class);
private String path;
private String toBeReplaced;
private String replacement;
@Override
protected String getStringValue(JsonNode node) {
if (node == null || !node.isTextual()) {
throw new IllegalArgumentException("Input must be a non-null JsonNode containing a text value");
}
String idStr = node.asText();
if (toBeReplaced == null || toBeReplaced.isEmpty() || replacement == null) {
return idStr;
}
return idStr.replaceAll(toBeReplaced, replacement);
}
@Override
protected Logger getLogger() {
return log;
}
@Override
protected String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public void setToBeReplaced(String toBeReplaced) {
this.toBeReplaced = toBeReplaced;
}
public void setReplacement(String replacement) {
this.replacement = replacement;
}
}

View File

@@ -0,0 +1,26 @@
/**
* 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.openalex.metadatamapping;
import java.util.Map;
import jakarta.annotation.Resource;
import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping;
/**
* @author adamo.fapohunda at 4science.com
**/
public class OpenAlexPublicationFieldMapping extends AbstractMetadataFieldMapping {
@Override
@Resource(name = "openalexPublicationsMetadataFieldMap")
public void setMetadataFieldMap(Map metadataFieldMap) {
super.setMetadataFieldMap(metadataFieldMap);
}
}

View File

@@ -0,0 +1,16 @@
/**
* 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.openalex.service;
import org.dspace.importer.external.service.components.QuerySource;
/**
* @author adamo.fapohunda at 4science.com
**/
public interface OpenAlexImportMetadataSourceService extends QuerySource {
}

View File

@@ -0,0 +1,287 @@
/**
* 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.openalex.service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.el.MethodNotFoundException;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
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.liveimportclient.service.LiveImportClientImpl;
import org.dspace.importer.external.service.AbstractImportMetadataSourceService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author adamo.fapohunda at 4science.com
**/
public class OpenAlexImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService<String>
implements OpenAlexImportMetadataSourceService {
private final static Logger log = LogManager.getLogger();
private final int timeout = 1000;
private String url;
@Autowired
private LiveImportClient liveImportClient;
@Override
public String getImportSource() {
return "openalex";
}
@Override
public ImportRecord getRecord(String id) throws MetadataSourceException {
if (id == null) {
throw new MetadataSourceException("ID cannot be null");
}
List<ImportRecord> records = retry(new SearchByIdCallable(id));
return CollectionUtils.isEmpty(records) ? null : records.get(0);
}
@Override
public int getRecordsCount(String query) throws MetadataSourceException {
if (query == null) {
throw new MetadataSourceException("Query cannot be null");
}
return retry(new CountByQueryCallable(query));
}
@Override
public int getRecordsCount(Query query) throws MetadataSourceException {
if (query == null) {
throw new MetadataSourceException("Query cannot be null");
}
return retry(new CountByQueryCallable(query));
}
@Override
public Collection<ImportRecord> getRecords(String query, int start, int count) throws MetadataSourceException {
if (query == null) {
throw new MetadataSourceException("Query cannot be null");
}
return retry(new SearchByQueryCallable(query, start, count));
}
@Override
public Collection<ImportRecord> getRecords(Query query) throws MetadataSourceException {
throw new MethodNotFoundException("This method is not implemented for OpenAlex");
}
@Override
public ImportRecord getRecord(Query query) throws MetadataSourceException {
if (query == null) {
throw new MetadataSourceException("Query cannot be null");
}
List<ImportRecord> records = retry(new SearchByIdCallable(query));
return CollectionUtils.isEmpty(records) ? null : records.get(0);
}
@Override
public Collection<ImportRecord> findMatchingRecords(Query query) throws MetadataSourceException {
throw new MethodNotFoundException("This method is not implemented for OpenAlex");
}
@Override
public Collection<ImportRecord> findMatchingRecords(Item item) throws MetadataSourceException {
throw new MethodNotFoundException("This method is not implemented for OpenAlex");
}
@Override
public void init() throws Exception {
if (liveImportClient == null) {
throw new IllegalStateException("LiveImportClient not properly initialized");
}
if (StringUtils.isBlank(url)) {
throw new IllegalStateException("URL not properly configured");
}
}
public Integer count(String query) throws MetadataSourceException {
if (query == null) {
throw new MetadataSourceException("Query cannot be null");
}
Map<String, Map<String, String>> params = new HashMap<>();
Map<String, String> uriParams = new HashMap<>();
params.put(LiveImportClientImpl.URI_PARAMETERS, uriParams);
try {
uriParams.put("search", query);
String resp = liveImportClient.executeHttpGetRequest(timeout, this.url, params);
if (StringUtils.isEmpty(resp)) {
log.error("Got an empty response from LiveImportClient for query: {}", query);
return 0;
}
JsonNode jsonNode = convertStringJsonToJsonNode(resp);
if (jsonNode != null && jsonNode.hasNonNull("meta")
&& jsonNode.at("/meta/count").isNumber()) {
return jsonNode.at("/meta/count").asInt();
}
} catch (Exception e) {
log.error("Error executing count query", e);
}
return 0;
}
private List<ImportRecord> searchById(String id) {
List<ImportRecord> results = new ArrayList<>();
try {
String resp = liveImportClient.executeHttpGetRequest(timeout, this.url + "/" + id, new HashMap<>());
if (StringUtils.isEmpty(resp)) {
return results;
}
JsonNode jsonNode = convertStringJsonToJsonNode(resp);
if (jsonNode != null) {
ImportRecord record = transformSourceRecords(jsonNode.toString());
if (record != null) {
results.add(record);
}
}
} catch (Exception e) {
log.error("Error searching by ID: {}", id, e);
}
return results;
}
private List<ImportRecord> search(String query, Integer page, Integer pageSize) {
List<ImportRecord> results = new ArrayList<>();
Map<String, Map<String, String>> params = new HashMap<>();
Map<String, String> uriParams = new HashMap<>();
params.put(LiveImportClientImpl.URI_PARAMETERS, uriParams);
try {
uriParams.put("search", query);
if (page != null) {
uriParams.put("page", String.valueOf(page + 1));
}
if (pageSize != null) {
uriParams.put("per_page", String.valueOf(pageSize));
}
String resp = liveImportClient.executeHttpGetRequest(timeout, this.url, params);
if (StringUtils.isEmpty(resp)) {
return results;
}
JsonNode jsonNode = convertStringJsonToJsonNode(resp);
if (jsonNode != null) {
JsonNode docs = jsonNode.at("/results");
if (docs != null && docs.isArray()) {
for (JsonNode node : docs) {
if (node != null) {
ImportRecord record = transformSourceRecords(node.toString());
if (record != null) {
results.add(record);
}
}
}
}
}
} catch (Exception e) {
log.error("Error executing search query", e);
}
return results;
}
private JsonNode convertStringJsonToJsonNode(String json) {
if (StringUtils.isEmpty(json)) {
return null;
}
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 = StringUtils.trimToNull(url);
}
private class SearchByQueryCallable implements Callable<List<ImportRecord>> {
private final Query query;
private SearchByQueryCallable(String queryString, int start, int count) {
query = new Query();
query.addParameter("query", queryString);
query.addParameter("page", start / count);
query.addParameter("count", count);
}
@Override
public List<ImportRecord> call() throws Exception {
String queryString = query.getParameterAsClass("query", String.class);
if (queryString == null) {
throw new MetadataSourceException("Query cannot be null");
}
return search(queryString,
query.getParameterAsClass("page", Integer.class),
query.getParameterAsClass("count", Integer.class));
}
}
private class SearchByIdCallable implements Callable<List<ImportRecord>> {
private final Query query;
private SearchByIdCallable(String id) {
this.query = new Query();
query.addParameter("id", id);
}
private SearchByIdCallable(Query query) {
this.query = query;
}
@Override
public List<ImportRecord> call() throws Exception {
String id = query.getParameterAsClass("id", String.class);
if (id == null) {
throw new MetadataSourceException("Id cannot be null");
}
return searchById(id);
}
}
private class CountByQueryCallable implements Callable<Integer> {
private final 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 {
String queryString = query.getParameterAsClass("query", String.class);
if (queryString == null) {
throw new MetadataSourceException("Query cannot be null");
}
return count(queryString);
}
}
}

View File

@@ -244,4 +244,13 @@
<constructor-arg value="dc.identifier.other"/>
</bean>
<bean id="openalexImportServiceByTitle" class="org.dspace.importer.external.openalex.service.OpenAlexImportMetadataSourceServiceImpl">
<property name="metadataFieldMapping" ref="openalexPublicationMetadataFieldMapping"/>
<property name="url" value="${openalex.url.works}"/>
</bean>
<bean id="openalexPublicationMetadataFieldMapping"
class="org.dspace.importer.external.openalex.metadatamapping.OpenAlexPublicationFieldMapping">
</bean>
</beans>

View File

@@ -1,4 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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/
-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
default-lazy-init="true">
@@ -282,4 +291,17 @@
</list>
</property>
</bean>
<bean id="openalexLiveImportDataProviderByTitle" class="org.dspace.external.provider.impl.LiveImportDataProvider">
<property name="metadataSource" ref="openalexImportServiceByTitle"/>
<property name="sourceIdentifier" value="openalex"/>
<property name="recordIdMetadata" value="dc.identifier.other"/>
<property name="supportedEntityTypes">
<list>
<value>Publication</value>
<value>none</value>
</list>
</property>
</bean>
</beans>

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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/
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
default-autowire-candidates="*Service,*DAO,javax.sql.DataSource">
<context:annotation-config/>
<!-- allows us to use spring annotations in beans -->
<util:map id="openalexPublicationsMetadataFieldMap" key-type="org.dspace.importer.external.metadatamapping.MetadataFieldConfig"
value-type="org.dspace.importer.external.metadatamapping.contributor.MetadataContributor">
<description>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.
</description>
<entry key-ref="openalex.title" value-ref="openalexTitleContrib"/>
<entry key-ref="openalex.abstract" value-ref="openalexAbstractContrib"/>
<entry key-ref="openalex.publication.date" value-ref="openalexDateContrib"/>
<entry key-ref="openalex.doi" value-ref="openalexDoiContrib"/>
<entry key-ref="openalex.pmid" value-ref="openalexPmidContrib"/>
<entry key-ref="openalex.openalexId" value-ref="openalexIdContrib"/>
<!-- <entry key-ref="openalex.mag" value-ref="openalexMagContrib"/>-->
<entry key-ref="openalex.author" value-ref="openalexAuthorContrib"/>
<entry key-ref="openalex.language" value-ref="openalexLanguageContrib"/>
</util:map>
<bean id="openalexTitleContrib" class="org.dspace.importer.external.metadatamapping.contributor.SimpleJsonPathMetadataContributor">
<property name="field" ref="openalex.title"/>
<property name="query" value="/title"/>
</bean>
<bean id="openalex.title" class="org.dspace.importer.external.metadatamapping.MetadataFieldConfig">
<constructor-arg value="dc.title"/>
</bean>
<bean id="openalexAbstractContrib" class="org.dspace.importer.external.metadatamapping.contributor.SimpleJsonPathMetadataContributor">
<property name="field" ref="openalex.abstract"/>
<property name="metadataProcessor" ref="invertedIndexMetadataProcessor"/>
</bean>
<bean id="openalex.abstract" class="org.dspace.importer.external.metadatamapping.MetadataFieldConfig">
<constructor-arg value="dc.description.abstract"/>
</bean>
<bean name="invertedIndexMetadataProcessor" class="org.dspace.importer.external.metadatamapping.contributor.InvertedIndexProcessor">
<property name="path" value="/abstract_inverted_index"/>
</bean>
<bean id="openalexDateContrib" class="org.dspace.importer.external.metadatamapping.contributor.SimpleJsonPathMetadataContributor">
<property name="field" ref="openalex.publication.date"/>
<property name="metadataProcessor" ref="openalexDateMetadataProcessor"/>
</bean>
<bean name="openalexDateMetadataProcessor" class="org.dspace.importer.external.openalex.metadatamapping.OpenAlexDateMetadataProcessor">
<property name="path" value="/publication_date"/>
</bean>
<bean id="openalex.publication.date" class="org.dspace.importer.external.metadatamapping.MetadataFieldConfig">
<constructor-arg value="dc.date.issued"/>
</bean>
<bean id="openalexDoiContrib" class="org.dspace.importer.external.metadatamapping.contributor.SimpleJsonPathMetadataContributor">
<property name="field" ref="openalex.doi"/>
<property name="metadataProcessor" ref="openalexDoiMetadataProcessor"/>
</bean>
<bean name="openalexDoiMetadataProcessor" class="org.dspace.importer.external.openalex.metadatamapping.OpenAlexIdMetadataProcessor">
<property name="path" value="/ids/doi"/>
<property name="toBeReplaced" value="https://doi.org/"/>
<property name="replacement" value=""/>
</bean>
<bean id="openalex.doi" class="org.dspace.importer.external.metadatamapping.MetadataFieldConfig">
<constructor-arg value="dc.identifier.doi"/>
</bean>
<bean id="openalexPmidContrib" class="org.dspace.importer.external.metadatamapping.contributor.SimpleJsonPathMetadataContributor">
<property name="field" ref="openalex.pmid"/>
<property name="metadataProcessor" ref="openalexPmidMetadataProcessor"/>
</bean>
<bean name="openalexPmidMetadataProcessor" class="org.dspace.importer.external.openalex.metadatamapping.OpenAlexIdMetadataProcessor">
<property name="path" value="/ids/pmid"/>
<property name="toBeReplaced" value="https://pubmed.ncbi.nlm.nih.gov/"/>
<property name="replacement" value=""/>
</bean>
<bean id="openalex.pmid" class="org.dspace.importer.external.metadatamapping.MetadataFieldConfig">
<constructor-arg value="dc.identifier.pmid"/>
</bean>
<bean id="openalexIdContrib" class="org.dspace.importer.external.metadatamapping.contributor.SimpleJsonPathMetadataContributor">
<property name="field" ref="openalex.openalexId"/>
<property name="metadataProcessor" ref="openalexOpenAlexIdMetadataProcessor"/>
</bean>
<bean name="openalexOpenAlexIdMetadataProcessor" class="org.dspace.importer.external.openalex.metadatamapping.OpenAlexIdMetadataProcessor">
<property name="path" value="/ids/openalex"/>
<property name="toBeReplaced" value="https://openalex.org/"/>
<property name="replacement" value=""/>
</bean>
<bean id="openalex.openalexId" class="org.dspace.importer.external.metadatamapping.MetadataFieldConfig">
<constructor-arg value="dc.identifier.other"/>
</bean>
<bean id="openalexMagContrib" class="org.dspace.importer.external.metadatamapping.contributor.SimpleJsonPathMetadataContributor">
<property name="field" ref="openalex.mag"/>
<property name="query" value="/ids/mag"/>
</bean>
<bean id="openalex.mag" class="org.dspace.importer.external.metadatamapping.MetadataFieldConfig">
<constructor-arg value="dc.identifier.mag"/>
</bean>
<bean id="openalexAuthorContrib" class="org.dspace.importer.external.metadatamapping.contributor.SimpleJsonPathMetadataContributor">
<property name="field" ref="openalex.author"/>
<property name="metadataProcessor" ref="openalexAuthorProcessor"/>
</bean>
<bean name="openalexAuthorProcessor" class="org.dspace.importer.external.metadatamapping.contributor.ArrayElementAttributeProcessor">
<property name="pathToArray" value="/authorships"/>
<property name="elementAttribute" value="/raw_author_name"/>
</bean>
<bean id="openalex.author" class="org.dspace.importer.external.metadatamapping.MetadataFieldConfig">
<constructor-arg value="dc.contributor.author"/>
</bean>
<bean id="openalexLanguageContrib" class="org.dspace.importer.external.metadatamapping.contributor.SimpleJsonPathMetadataContributor">
<property name="field" ref="openalex.language"/>
<property name="query" value="/language"/>
</bean>
<bean id="openalex.language" class="org.dspace.importer.external.metadatamapping.MetadataFieldConfig">
<constructor-arg value="dc.language.iso"/>
</bean>
</beans>