Merge branch 'main' of https://github.com/DSpace/DSpace into DURACOM-225

This commit is contained in:
Andrea Bollini
2024-02-17 18:53:03 +01:00
251 changed files with 13543 additions and 14167 deletions

View File

@@ -528,7 +528,7 @@
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<artifactId>hamcrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
@@ -620,7 +620,7 @@
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>2.11.0</version>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
@@ -784,7 +784,7 @@
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.7.1</version>
<version>5.9</version>
</dependency>
<!-- Email templating -->
@@ -867,32 +867,32 @@
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-buffer</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-unix-common</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-common</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-handler</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec</artifactId>
<version>4.1.94.Final</version>
<version>4.1.106.Final</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
@@ -902,7 +902,7 @@
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.8.0</version>
<version>2.9.1</version>
<scope>test</scope>
</dependency>
<dependency>

View File

@@ -116,6 +116,17 @@ public final class CreateAdministrator {
protected CreateAdministrator()
throws Exception {
context = new Context();
try {
context.getDBConfig();
} catch (NullPointerException npr) {
// if database is null, there is no point in continuing. Prior to this exception and catch,
// NullPointerException was thrown, that wasn't very helpful.
throw new IllegalStateException("Problem connecting to database. This " +
"indicates issue with either network or version (or possibly some other). " +
"If you are running this in docker-compose, please make sure dspace-cli was " +
"built from the same sources as running dspace container AND that they are in " +
"the same project/network.");
}
groupService = EPersonServiceFactory.getInstance().getGroupService();
ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
}

View File

@@ -0,0 +1,140 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import org.apache.logging.log4j.Logger;
import org.apache.solr.client.solrj.SolrServerException;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.external.model.ExternalDataObject;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Suggestion provider that read the suggestion from the local suggestion solr
* core
*
* @author Andrea Bollini (andrea.bollini at 4science dot it)
*
*/
public abstract class SolrSuggestionProvider implements SuggestionProvider {
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SolrSuggestionProvider.class);
@Autowired
protected ItemService itemService;
@Autowired
protected SolrSuggestionStorageService solrSuggestionStorageService;
private String sourceName;
public String getSourceName() {
return sourceName;
}
public void setSourceName(String sourceName) {
this.sourceName = sourceName;
}
public void setItemService(ItemService itemService) {
this.itemService = itemService;
}
@Override
public long countAllTargets(Context context) {
try {
return this.solrSuggestionStorageService.countAllTargets(context, sourceName);
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public long countUnprocessedSuggestionByTarget(Context context, UUID target) {
try {
return this.solrSuggestionStorageService.countUnprocessedSuggestionByTarget(context, sourceName, target);
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public List<Suggestion> findAllUnprocessedSuggestions(Context context, UUID target, int pageSize, long offset,
boolean ascending) {
try {
return this.solrSuggestionStorageService.findAllUnprocessedSuggestions(context, sourceName,
target, pageSize, offset, ascending);
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public List<SuggestionTarget> findAllTargets(Context context, int pageSize, long offset) {
try {
return this.solrSuggestionStorageService.findAllTargets(context, sourceName, pageSize, offset);
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Suggestion findUnprocessedSuggestion(Context context, UUID target, String id) {
try {
return this.solrSuggestionStorageService.findUnprocessedSuggestion(context, sourceName, target, id);
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public SuggestionTarget findTarget(Context context, UUID target) {
try {
return this.solrSuggestionStorageService.findTarget(context, sourceName, target);
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void rejectSuggestion(Context context, UUID target, String idPart) {
Suggestion suggestion = findUnprocessedSuggestion(context, target, idPart);
try {
solrSuggestionStorageService.flagSuggestionAsProcessed(suggestion);
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void flagRelatedSuggestionsAsProcessed(Context context, ExternalDataObject externalDataObject) {
if (!isExternalDataObjectPotentiallySuggested(context, externalDataObject)) {
return;
}
try {
solrSuggestionStorageService.flagAllSuggestionAsProcessed(sourceName, externalDataObject.getId());
} catch (SolrServerException | IOException e) {
log.error(e.getMessage(), e);
}
}
/**
* check if the externalDataObject may have suggestion
* @param context
* @param externalDataObject
* @return true if the externalDataObject could be suggested by this provider
* (i.e. it comes from a DataProvider used by this suggestor)
*/
protected abstract boolean isExternalDataObjectPotentiallySuggested(Context context,
ExternalDataObject externalDataObject);
}

View File

@@ -0,0 +1,191 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import org.apache.solr.client.solrj.SolrServerException;
import org.dspace.core.Context;
/**
* Service to deal with the local suggestion solr core used by the
* SolrSuggestionProvider(s)
*
* @author Andrea Bollini (andrea.bollini at 4science dot it)
* @author Luca Giamminonni (luca.giamminonni at 4science dot it)
*
*/
public interface SolrSuggestionStorageService {
public static final String SOURCE = "source";
/** This is the URI Part of the suggestion source:target:id */
public static final String SUGGESTION_FULLID = "suggestion_fullid";
public static final String SUGGESTION_ID = "suggestion_id";
public static final String TARGET_ID = "target_id";
public static final String TITLE = "title";
public static final String DATE = "date";
public static final String DISPLAY = "display";
public static final String CONTRIBUTORS = "contributors";
public static final String ABSTRACT = "abstract";
public static final String CATEGORY = "category";
public static final String EXTERNAL_URI = "external-uri";
public static final String PROCESSED = "processed";
public static final String SCORE = "trust";
public static final String EVIDENCES = "evidences";
/**
* Add a new suggestion to SOLR
*
* @param suggestion
* @param force true if the suggestion must be reindexed
* @param commit
* @throws IOException
* @throws SolrServerException
*/
public void addSuggestion(Suggestion suggestion, boolean force, boolean commit)
throws SolrServerException, IOException;
/**
* Return true if the suggestion is already in SOLR and flagged as processed
*
* @param suggestion
* @return true if the suggestion is already in SOLR and flagged as processed
* @throws IOException
* @throws SolrServerException
*/
public boolean exist(Suggestion suggestion) throws SolrServerException, IOException;
/**
* Delete a suggestion from SOLR if any
*
* @param suggestion
* @throws IOException
* @throws SolrServerException
*/
public void deleteSuggestion(Suggestion suggestion) throws SolrServerException, IOException;
/**
* Flag a suggestion as processed in SOLR if any
*
* @param suggestion
* @throws IOException
* @throws SolrServerException
*/
public void flagSuggestionAsProcessed(Suggestion suggestion) throws SolrServerException, IOException;
/**
* Delete all the suggestions from SOLR if any related to a specific target
*
* @param target
* @throws IOException
* @throws SolrServerException
*/
public void deleteTarget(SuggestionTarget target) throws SolrServerException, IOException;
/**
* Performs an explicit commit, causing pending documents to be committed for
* indexing.
*
* @throws SolrServerException
* @throws IOException
*/
void commit() throws SolrServerException, IOException;
/**
* Flag all the suggestion related to the given source and id as processed.
*
* @param source the source name
* @param idPart the id's last part
* @throws SolrServerException
* @throws IOException
*/
void flagAllSuggestionAsProcessed(String source, String idPart) throws SolrServerException, IOException;
/**
* Count all the targets related to the given source.
*
* @param source the source name
* @return the target's count
* @throws IOException
* @throws SolrServerException
*/
long countAllTargets(Context context, String source) throws SolrServerException, IOException;
/**
* Count all the unprocessed suggestions related to the given source and target.
*
* @param context the DSpace Context
* @param source the source name
* @param target the target id
* @return the suggestion count
* @throws SolrServerException
* @throws IOException
*/
long countUnprocessedSuggestionByTarget(Context context, String source, UUID target)
throws SolrServerException, IOException;
/**
* Find all the unprocessed suggestions related to the given source and target.
*
* @param context the DSpace Context
* @param source the source name
* @param target the target id
* @param pageSize the page size
* @param offset the page offset
* @param ascending true to retrieve the suggestions ordered by score
* ascending
* @return the found suggestions
* @throws SolrServerException
* @throws IOException
*/
List<Suggestion> findAllUnprocessedSuggestions(Context context, String source, UUID target,
int pageSize, long offset, boolean ascending) throws SolrServerException, IOException;
/**
*
* Find all the suggestion targets related to the given source.
*
* @param context the DSpace Context
* @param source the source name
* @param pageSize the page size
* @param offset the page offset
* @return the found suggestion targets
* @throws SolrServerException
* @throws IOException
*/
List<SuggestionTarget> findAllTargets(Context context, String source, int pageSize, long offset)
throws SolrServerException, IOException;
/**
* Find an unprocessed suggestion by the given source, target id and suggestion
* id.
*
* @param context the DSpace Context
* @param source the source name
* @param target the target id
* @param id the suggestion id
* @return the suggestion, if any
* @throws SolrServerException
* @throws IOException
*/
Suggestion findUnprocessedSuggestion(Context context, String source, UUID target, String id)
throws SolrServerException, IOException;
/**
* Find a suggestion target by the given source and target.
*
* @param context the DSpace Context
* @param source the source name
* @param target the target id
* @return the suggestion target, if any
* @throws SolrServerException
* @throws IOException
*/
SuggestionTarget findTarget(Context context, String source, UUID target) throws SolrServerException, IOException;
}

View File

@@ -0,0 +1,360 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.SortClause;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.FacetField.Count;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.FacetParams;
import org.dspace.content.Item;
import org.dspace.content.dto.MetadataValueDTO;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.util.UUIDUtils;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Service to deal with the local suggestion solr core used by the
* SolrSuggestionProvider(s)
*
* @author Andrea Bollini (andrea.bollini at 4science dot it)
*
*/
public class SolrSuggestionStorageServiceImpl implements SolrSuggestionStorageService {
private static final Logger log = LogManager.getLogger(SolrSuggestionStorageServiceImpl.class);
protected SolrClient solrSuggestionClient;
@Autowired
private ItemService itemService;
/**
* Get solr client which use suggestion core
*
* @return solr client
*/
protected SolrClient getSolr() {
if (solrSuggestionClient == null) {
String solrService = DSpaceServicesFactory.getInstance().getConfigurationService()
.getProperty("suggestion.solr.server", "http://localhost:8983/solr/suggestion");
solrSuggestionClient = new HttpSolrClient.Builder(solrService).build();
}
return solrSuggestionClient;
}
@Override
public void addSuggestion(Suggestion suggestion, boolean force, boolean commit)
throws SolrServerException, IOException {
if (force || !exist(suggestion)) {
ObjectMapper jsonMapper = new JsonMapper();
jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
SolrInputDocument document = new SolrInputDocument();
document.addField(SOURCE, suggestion.getSource());
// suggestion id is written as concatenation of
// source + ":" + targetID + ":" + idPart (of externalDataObj)
String suggestionFullID = suggestion.getID();
document.addField(SUGGESTION_FULLID, suggestionFullID);
document.addField(SUGGESTION_ID, suggestionFullID.split(":", 3)[2]);
document.addField(TARGET_ID, suggestion.getTarget().getID().toString());
document.addField(DISPLAY, suggestion.getDisplay());
document.addField(TITLE, getFirstValue(suggestion, "dc", "title", null));
document.addField(DATE, getFirstValue(suggestion, "dc", "date", "issued"));
document.addField(CONTRIBUTORS, getAllValues(suggestion, "dc", "contributor", "author"));
document.addField(ABSTRACT, getFirstValue(suggestion, "dc", "description", "abstract"));
document.addField(CATEGORY, getAllValues(suggestion, "dc", "source", null));
document.addField(EXTERNAL_URI, suggestion.getExternalSourceUri());
document.addField(SCORE, suggestion.getScore());
document.addField(PROCESSED, false);
document.addField(EVIDENCES, jsonMapper.writeValueAsString(suggestion.getEvidences()));
getSolr().add(document);
if (commit) {
getSolr().commit();
}
}
}
@Override
public void commit() throws SolrServerException, IOException {
getSolr().commit();
}
private List<String> getAllValues(Suggestion suggestion, String schema, String element, String qualifier) {
return suggestion.getMetadata().stream()
.filter(st -> StringUtils.isNotBlank(st.getValue()) && StringUtils.equals(st.getSchema(), schema)
&& StringUtils.equals(st.getElement(), element)
&& StringUtils.equals(st.getQualifier(), qualifier))
.map(st -> st.getValue()).collect(Collectors.toList());
}
private String getFirstValue(Suggestion suggestion, String schema, String element, String qualifier) {
return suggestion.getMetadata().stream()
.filter(st -> StringUtils.isNotBlank(st.getValue())
&& StringUtils.equals(st.getSchema(), schema)
&& StringUtils.equals(st.getElement(), element)
&& StringUtils.equals(st.getQualifier(), qualifier))
.map(st -> st.getValue()).findFirst().orElse(null);
}
@Override
public boolean exist(Suggestion suggestion) throws SolrServerException, IOException {
SolrQuery query = new SolrQuery(
SUGGESTION_FULLID + ":\"" + suggestion.getID() + "\" AND " + PROCESSED + ":true");
return getSolr().query(query).getResults().getNumFound() == 1;
}
@Override
public void deleteSuggestion(Suggestion suggestion) throws SolrServerException, IOException {
getSolr().deleteById(suggestion.getID());
getSolr().commit();
}
@Override
public void flagSuggestionAsProcessed(Suggestion suggestion) throws SolrServerException, IOException {
SolrInputDocument sdoc = new SolrInputDocument();
sdoc.addField(SUGGESTION_FULLID, suggestion.getID());
Map<String, Object> fieldModifier = new HashMap<>(1);
fieldModifier.put("set", true);
sdoc.addField(PROCESSED, fieldModifier); // add the map as the field value
getSolr().add(sdoc);
getSolr().commit();
}
@Override
public void flagAllSuggestionAsProcessed(String source, String idPart) throws SolrServerException, IOException {
SolrQuery query = new SolrQuery(SOURCE + ":" + source + " AND " + SUGGESTION_ID + ":\"" + idPart + "\"");
query.setRows(Integer.MAX_VALUE);
query.setFields(SUGGESTION_FULLID);
SolrDocumentList results = getSolr().query(query).getResults();
if (results.getNumFound() > 0) {
for (SolrDocument rDoc : results) {
SolrInputDocument sdoc = new SolrInputDocument();
sdoc.addField(SUGGESTION_FULLID, rDoc.getFieldValue(SUGGESTION_FULLID));
Map<String, Object> fieldModifier = new HashMap<>(1);
fieldModifier.put("set", true);
sdoc.addField(PROCESSED, fieldModifier); // add the map as the field value
getSolr().add(sdoc);
}
}
getSolr().commit();
}
@Override
public void deleteTarget(SuggestionTarget target) throws SolrServerException, IOException {
getSolr().deleteByQuery(
SOURCE + ":" + target.getSource() + " AND " + TARGET_ID + ":" + target.getTarget().getID().toString());
getSolr().commit();
}
@Override
public long countAllTargets(Context context, String source) throws SolrServerException, IOException {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(0);
solrQuery.setQuery(SOURCE + ":" + source);
solrQuery.addFilterQuery(PROCESSED + ":false");
solrQuery.setFacet(true);
solrQuery.setFacetMinCount(1);
solrQuery.addFacetField(TARGET_ID);
solrQuery.setFacetLimit(Integer.MAX_VALUE);
QueryResponse response = getSolr().query(solrQuery);
return response.getFacetField(TARGET_ID).getValueCount();
}
@Override
public long countUnprocessedSuggestionByTarget(Context context, String source, UUID target)
throws SolrServerException, IOException {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(0);
solrQuery.setQuery("*:*");
solrQuery.addFilterQuery(
SOURCE + ":" + source,
TARGET_ID + ":" + target.toString(),
PROCESSED + ":false");
QueryResponse response = getSolr().query(solrQuery);
return response.getResults().getNumFound();
}
@Override
public List<Suggestion> findAllUnprocessedSuggestions(Context context, String source, UUID target,
int pageSize, long offset, boolean ascending) throws SolrServerException, IOException {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(pageSize);
solrQuery.setStart((int) offset);
solrQuery.setQuery("*:*");
solrQuery.addFilterQuery(
SOURCE + ":" + source,
TARGET_ID + ":" + target.toString(),
PROCESSED + ":false");
if (ascending) {
solrQuery.addSort(SortClause.asc("trust"));
} else {
solrQuery.addSort(SortClause.desc("trust"));
}
solrQuery.addSort(SortClause.desc("date"));
solrQuery.addSort(SortClause.asc("title"));
QueryResponse response = getSolr().query(solrQuery);
List<Suggestion> suggestions = new ArrayList<Suggestion>();
for (SolrDocument solrDoc : response.getResults()) {
suggestions.add(convertSolrDoc(context, solrDoc, source));
}
return suggestions;
}
@Override
public List<SuggestionTarget> findAllTargets(Context context, String source, int pageSize, long offset)
throws SolrServerException, IOException {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(0);
solrQuery.setQuery(SOURCE + ":" + source);
solrQuery.addFilterQuery(PROCESSED + ":false");
solrQuery.setFacet(true);
solrQuery.setFacetMinCount(1);
solrQuery.addFacetField(TARGET_ID);
solrQuery.setParam(FacetParams.FACET_OFFSET, String.valueOf(offset));
solrQuery.setFacetLimit((int) (pageSize));
QueryResponse response = getSolr().query(solrQuery);
FacetField facetField = response.getFacetField(TARGET_ID);
List<SuggestionTarget> suggestionTargets = new ArrayList<SuggestionTarget>();
int idx = 0;
for (Count c : facetField.getValues()) {
SuggestionTarget target = new SuggestionTarget();
target.setSource(source);
target.setTotal((int) c.getCount());
target.setTarget(findItem(context, c.getName()));
suggestionTargets.add(target);
idx++;
}
return suggestionTargets;
}
@Override
public Suggestion findUnprocessedSuggestion(Context context, String source, UUID target, String id)
throws SolrServerException, IOException {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(1);
solrQuery.setQuery("*:*");
solrQuery.addFilterQuery(
SOURCE + ":" + source,
TARGET_ID + ":" + target.toString(),
SUGGESTION_ID + ":\"" + id + "\"",
PROCESSED + ":false");
SolrDocumentList results = getSolr().query(solrQuery).getResults();
return isEmpty(results) ? null : convertSolrDoc(context, results.get(0), source);
}
@Override
public SuggestionTarget findTarget(Context context, String source, UUID target)
throws SolrServerException, IOException {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(0);
solrQuery.setQuery(SOURCE + ":" + source);
solrQuery.addFilterQuery(
TARGET_ID + ":" + target.toString(),
PROCESSED + ":false");
QueryResponse response = getSolr().query(solrQuery);
SuggestionTarget sTarget = new SuggestionTarget();
sTarget.setSource(source);
sTarget.setTotal((int) response.getResults().getNumFound());
Item itemTarget = findItem(context, target);
if (itemTarget != null) {
sTarget.setTarget(itemTarget);
} else {
return null;
}
return sTarget;
}
private Suggestion convertSolrDoc(Context context, SolrDocument solrDoc, String sourceName) {
Item target = findItem(context, (String) solrDoc.getFieldValue(TARGET_ID));
Suggestion suggestion = new Suggestion(sourceName, target, (String) solrDoc.getFieldValue(SUGGESTION_ID));
suggestion.setDisplay((String) solrDoc.getFieldValue(DISPLAY));
suggestion.getMetadata()
.add(new MetadataValueDTO("dc", "title", null, null, (String) solrDoc.getFieldValue(TITLE)));
suggestion.getMetadata()
.add(new MetadataValueDTO("dc", "date", "issued", null, (String) solrDoc.getFieldValue(DATE)));
suggestion.getMetadata().add(
new MetadataValueDTO("dc", "description", "abstract", null, (String) solrDoc.getFieldValue(ABSTRACT)));
suggestion.setExternalSourceUri((String) solrDoc.getFieldValue(EXTERNAL_URI));
if (solrDoc.containsKey(CATEGORY)) {
for (Object o : solrDoc.getFieldValues(CATEGORY)) {
suggestion.getMetadata().add(
new MetadataValueDTO("dc", "source", null, null, (String) o));
}
}
if (solrDoc.containsKey(CONTRIBUTORS)) {
for (Object o : solrDoc.getFieldValues(CONTRIBUTORS)) {
suggestion.getMetadata().add(
new MetadataValueDTO("dc", "contributor", "author", null, (String) o));
}
}
String evidencesJson = (String) solrDoc.getFieldValue(EVIDENCES);
ObjectMapper jsonMapper = new JsonMapper();
jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
List<SuggestionEvidence> evidences = new LinkedList<SuggestionEvidence>();
try {
evidences = jsonMapper.readValue(evidencesJson, new TypeReference<List<SuggestionEvidence>>() {});
} catch (JsonProcessingException e) {
log.error(e);
}
suggestion.getEvidences().addAll(evidences);
return suggestion;
}
private Item findItem(Context context, UUID itemId) {
try {
return itemService.find(context, itemId);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private Item findItem(Context context, String itemId) {
return findItem(context, UUIDUtils.fromString(itemId));
}
}

View File

@@ -0,0 +1,99 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import java.util.LinkedList;
import java.util.List;
import org.dspace.content.Item;
import org.dspace.content.dto.MetadataValueDTO;
/**
* This entity contains metadatas that should be added to the targeted Item
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class Suggestion {
/** id of the suggestion */
private String id;
/** the dc.title of the item */
private String display;
/** the external source name the suggestion comes from */
private String source;
/** external uri of the item */
private String externalSourceUri;
/** item targeted by this suggestion */
private Item target;
private List<SuggestionEvidence> evidences = new LinkedList<SuggestionEvidence>();
private List<MetadataValueDTO> metadata = new LinkedList<MetadataValueDTO>();
/** suggestion creation
* @param source name of the external source
* @param target the targeted item in repository
* @param idPart external item id, used mainly for suggestion @see #id creation
* */
public Suggestion(String source, Item target, String idPart) {
this.source = source;
this.target = target;
this.id = source + ":" + target.getID().toString() + ":" + idPart;
}
public String getDisplay() {
return display;
}
public void setDisplay(String display) {
this.display = display;
}
public String getSource() {
return source;
}
public String getExternalSourceUri() {
return externalSourceUri;
}
public void setExternalSourceUri(String externalSourceUri) {
this.externalSourceUri = externalSourceUri;
}
public List<SuggestionEvidence> getEvidences() {
return evidences;
}
public List<MetadataValueDTO> getMetadata() {
return metadata;
}
public Item getTarget() {
return target;
}
public String getID() {
return id;
}
public Double getScore() {
if (evidences != null && evidences.size() > 0) {
double score = 0;
for (SuggestionEvidence evidence : evidences) {
score += evidence.getScore();
}
return score;
}
return null;
}
}

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.app.suggestion;
/**
* This DTO class is returned by an {@link org.dspace.app.suggestion.openaire.EvidenceScorer} to model the concept of
* an evidence / fact that has been used to evaluate the precision of a suggestion increasing or decreasing the score
* of the suggestion.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class SuggestionEvidence {
/** name of the evidence */
private String name;
/** positive or negative value to influence the score of the suggestion */
private double score;
/** additional notes */
private String notes;
public SuggestionEvidence() {
}
public SuggestionEvidence(String name, double score, String notes) {
this.name = name;
this.score = score;
this.notes = notes;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
}

View File

@@ -0,0 +1,54 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import java.util.List;
import java.util.UUID;
import org.dspace.core.Context;
import org.dspace.external.model.ExternalDataObject;
/**
*
* Interface for suggestion management like finding and counting.
* @see org.dspace.app.suggestion.SuggestionTarget
* @author Francesco Bacchelli (francesco.bacchelli at 4science.com)
*
*/
public interface SuggestionProvider {
/** find all suggestion targets
* @see org.dspace.app.suggestion.SuggestionTarget
* */
public List<SuggestionTarget> findAllTargets(Context context, int pageSize, long offset);
/** count all suggestion targets */
public long countAllTargets(Context context);
/** find a suggestion target by UUID */
public SuggestionTarget findTarget(Context context, UUID target);
/** find unprocessed suggestions (paged) by target UUID
* @see org.dspace.app.suggestion.Suggestion
* */
public List<Suggestion> findAllUnprocessedSuggestions(Context context, UUID target, int pageSize, long offset,
boolean ascending);
/** find unprocessed suggestions by target UUID */
public long countUnprocessedSuggestionByTarget(Context context, UUID target);
/** find an unprocessed suggestion by target UUID and suggestion id */
public Suggestion findUnprocessedSuggestion(Context context, UUID target, String id);
/** reject a specific suggestion by target @param target and by suggestion id @param idPart */
public void rejectSuggestion(Context context, UUID target, String idPart);
/** flag a suggestion as processed */
public void flagRelatedSuggestionsAsProcessed(Context context, ExternalDataObject externalDataObject);
}

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.app.suggestion;
import java.util.List;
import java.util.UUID;
import org.dspace.core.Context;
/**
* Service that handles {@link Suggestion}.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public interface SuggestionService {
/** find a {@link SuggestionTarget } by source name and suggestion id */
public SuggestionTarget find(Context context, String source, UUID id);
/** count all suggetion targets by suggestion source */
public long countAll(Context context, String source);
/** find all suggestion targets by source (paged) */
public List<SuggestionTarget> findAllTargets(Context context, String source, int pageSize, long offset);
/** count all (unprocessed) suggestions by the given target uuid */
public long countAllByTarget(Context context, UUID target);
/** find suggestion target by targeted item (paged) */
public List<SuggestionTarget> findByTarget(Context context, UUID target, int pageSize, long offset);
/** find suggestion source by source name */
public SuggestionSource findSource(Context context, String source);
/** count all suggestion sources */
public long countSources(Context context);
/** find all suggestion sources (paged) */
public List<SuggestionSource> findAllSources(Context context, int pageSize, long offset);
/** find unprocessed suggestion by id */
public Suggestion findUnprocessedSuggestion(Context context, String id);
/** reject a specific suggestion by its id */
public void rejectSuggestion(Context context, String id);
/** find all suggestions by targeted item and external source */
public List<Suggestion> findByTargetAndSource(Context context, UUID target, String source, int pageSize,
long offset, boolean ascending);
/** count all suggestions by targeted item id and source name */
public long countAllByTargetAndSource(Context context, String source, UUID target);
/** returns all suggestion providers */
public List<SuggestionProvider> getSuggestionProviders();
}

View File

@@ -0,0 +1,194 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import org.apache.logging.log4j.Logger;
import org.dspace.core.Context;
import org.springframework.stereotype.Service;
@Service
public class SuggestionServiceImpl implements SuggestionService {
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SuggestionServiceImpl.class);
@Resource(name = "suggestionProviders")
private Map<String, SuggestionProvider> providersMap;
@Override
public List<SuggestionProvider> getSuggestionProviders() {
if (providersMap != null) {
return providersMap.values().stream().collect(Collectors.toList());
}
return null;
}
@Override
public SuggestionTarget find(Context context, String source, UUID id) {
if (providersMap.containsKey(source)) {
return providersMap.get(source).findTarget(context, id);
} else {
return null;
}
}
@Override
public long countAll(Context context, String source) {
if (providersMap.containsKey(source)) {
return providersMap.get(source).countAllTargets(context);
} else {
return 0;
}
}
@Override
public List<SuggestionTarget> findAllTargets(Context context, String source, int pageSize, long offset) {
if (providersMap.containsKey(source)) {
return providersMap.get(source).findAllTargets(context, pageSize, offset);
} else {
return null;
}
}
@Override
public long countAllByTarget(Context context, UUID target) {
int count = 0;
for (String provider : providersMap.keySet()) {
if (providersMap.get(provider).countUnprocessedSuggestionByTarget(context, target) > 0) {
count++;
}
}
return count;
}
@Override
public List<SuggestionTarget> findByTarget(Context context, UUID target, int pageSize, long offset) {
List<SuggestionTarget> fullSourceTargets = new ArrayList<SuggestionTarget>();
for (String source : providersMap.keySet()) {
// all the suggestion target will be related to the same target (i.e. the same researcher - person item)
SuggestionTarget sTarget = providersMap.get(source).findTarget(context, target);
if (sTarget != null && sTarget.getTotal() > 0) {
fullSourceTargets.add(sTarget);
}
}
fullSourceTargets.sort(new Comparator<SuggestionTarget>() {
@Override
public int compare(SuggestionTarget arg0, SuggestionTarget arg1) {
return -(arg0.getTotal() - arg1.getTotal());
}
}
);
// this list will be as large as the number of sources available in the repository so it is unlikely that
// real pagination will occur
return fullSourceTargets.stream().skip(offset).limit(pageSize).collect(Collectors.toList());
}
@Override
public long countSources(Context context) {
return providersMap.size();
}
@Override
public SuggestionSource findSource(Context context, String source) {
if (providersMap.containsKey(source)) {
SuggestionSource ssource = new SuggestionSource(source);
ssource.setTotal((int) providersMap.get(source).countAllTargets(context));
return ssource;
} else {
return null;
}
}
@Override
public List<SuggestionSource> findAllSources(Context context, int pageSize, long offset) {
List<SuggestionSource> fullSources = getSources(context).stream().skip(offset).limit(pageSize)
.collect(Collectors.toList());
return fullSources;
}
private List<SuggestionSource> getSources(Context context) {
List<SuggestionSource> results = new ArrayList<SuggestionSource>();
for (String source : providersMap.keySet()) {
SuggestionSource ssource = new SuggestionSource(source);
ssource.setTotal((int) providersMap.get(source).countAllTargets(context));
results.add(ssource);
}
return results;
}
@Override
public long countAllByTargetAndSource(Context context, String source, UUID target) {
if (providersMap.containsKey(source)) {
return providersMap.get(source).countUnprocessedSuggestionByTarget(context, target);
}
return 0;
}
@Override
public List<Suggestion> findByTargetAndSource(Context context, UUID target, String source, int pageSize,
long offset, boolean ascending) {
if (providersMap.containsKey(source)) {
return providersMap.get(source).findAllUnprocessedSuggestions(context, target, pageSize, offset, ascending);
}
return null;
}
@Override
public Suggestion findUnprocessedSuggestion(Context context, String id) {
String source = null;
UUID target = null;
String idPart = null;
String[] split;
try {
split = id.split(":", 3);
source = split[0];
target = UUID.fromString(split[1]);
idPart = split[2];
} catch (Exception e) {
log.warn("findSuggestion got an invalid id " + id + ", return null");
return null;
}
if (split.length != 3) {
return null;
}
if (providersMap.containsKey(source)) {
return providersMap.get(source).findUnprocessedSuggestion(context, target, idPart);
}
return null;
}
@Override
public void rejectSuggestion(Context context, String id) {
String source = null;
UUID target = null;
String idPart = null;
String[] split;
try {
split = id.split(":", 3);
source = split[0];
target = UUID.fromString(split[1]);
idPart = split[2];
} catch (Exception e) {
log.warn("rejectSuggestion got an invalid id " + id + ", doing nothing");
return;
}
if (split.length != 3) {
return;
}
if (providersMap.containsKey(source)) {
providersMap.get(source).rejectSuggestion(context, target, idPart);
}
}
}

View File

@@ -0,0 +1,49 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
/**
* This DTO class is used to pass around the number of items interested by suggestion provided by a specific source
* (i.e. openaire)
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class SuggestionSource {
/** source name of the suggestion */
private String name;
/** number of targeted items */
private int total;
public SuggestionSource() {
}
/**
* Summarize the available suggestions from a source.
*
* @param name the name must be not null
*/
public SuggestionSource(String name) {
super();
this.name = name;
}
public String getID() {
return name;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
}

View File

@@ -0,0 +1,75 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import org.dspace.content.Item;
/**
* This DTO class is used to pass around the number of suggestions available from a specific source for a target
* repository item
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class SuggestionTarget {
/** the item targeted */
private Item target;
/** source name of the suggestion */
private String source;
/** total count of suggestions for same target and source */
private int total;
public SuggestionTarget() {
}
/**
* Wrap a target repository item (usually a person item) into a suggestion target.
*
* @param item must be not null
*/
public SuggestionTarget(Item item) {
super();
this.target = item;
}
/**
* The suggestion target uses the concatenation of the source and target uuid separated by colon as id
*
* @return the source:uuid of the wrapped item
*/
public String getID() {
return source + ":" + (target != null ? target.getID() : "");
}
public Item getTarget() {
return target;
}
public void setTarget(Item target) {
this.target = target;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
}

View File

@@ -0,0 +1,111 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.dspace.external.model.ExternalDataObject;
/**
* This utility class provides convenient methods to deal with the
* {@link ExternalDataObject} for the purpose of the Suggestion framework
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class SuggestionUtils {
private SuggestionUtils() {
}
/**
* This method receive an ExternalDataObject and a metadatum key.
* It return only the values of the Metadata associated with the key.
*
* @param record the ExternalDataObject to extract metadata from
* @param schema schema of the searching record
* @param element element of the searching record
* @param qualifier qualifier of the searching record
* @return value of the first matching record
*/
public static List<String> getAllEntriesByMetadatum(ExternalDataObject record, String schema, String element,
String qualifier) {
return record.getMetadata().stream()
.filter(x ->
StringUtils.equals(x.getSchema(), schema)
&& StringUtils.equals(x.getElement(), element)
&& StringUtils.equals(x.getQualifier(), qualifier))
.map(x -> x.getValue()).collect(Collectors.toList());
}
/**
* This method receive an ExternalDataObject and a metadatum key.
* It return only the values of the Metadata associated with the key.
*
* @param record the ExternalDataObject to extract metadata from
* @param metadataFieldKey the metadata field key (i.e. dc.title or dc.contributor.author),
* the jolly char is not supported
* @return value of the first matching record
*/
public static List<String> getAllEntriesByMetadatum(ExternalDataObject record, String metadataFieldKey) {
if (metadataFieldKey == null) {
return Collections.EMPTY_LIST;
}
String[] fields = metadataFieldKey.split("\\.");
String schema = fields[0];
String element = fields[1];
String qualifier = null;
if (fields.length == 3) {
qualifier = fields[2];
}
return getAllEntriesByMetadatum(record, schema, element, qualifier);
}
/**
* This method receive and ExternalDataObject and a metadatum key.
* It return only the value of the first Metadatum from the list associated with the key.
*
* @param record the ExternalDataObject to extract metadata from
* @param schema schema of the searching record
* @param element element of the searching record
* @param qualifier qualifier of the searching record
* @return value of the first matching record
*/
public static String getFirstEntryByMetadatum(ExternalDataObject record, String schema, String element,
String qualifier) {
return record.getMetadata().stream()
.filter(x ->
StringUtils.equals(x.getSchema(), schema)
&& StringUtils.equals(x.getElement(), element)
&& StringUtils.equals(x.getQualifier(), qualifier))
.map(x -> x.getValue()).findFirst().orElse(null);
}
/**
* This method receive and ExternalDataObject and a metadatum key.
* It return only the value of the first Metadatum from the list associated with the key.
*
* @param record the ExternalDataObject to extract metadata from
* @param metadataFieldKey the metadata field key (i.e. dc.title or dc.contributor.author),
* the jolly char is not supported
* @return value of the first matching record
*/
public static String getFirstEntryByMetadatum(ExternalDataObject record, String metadataFieldKey) {
if (metadataFieldKey == null) {
return null;
}
String[] fields = metadataFieldKey.split("\\.");
String schema = fields[0];
String element = fields[1];
String qualifier = null;
if (fields.length == 3) {
qualifier = fields[2];
}
return getFirstEntryByMetadatum(record, schema, element, qualifier);
}
}

View File

@@ -0,0 +1,151 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.openaire;
import static org.dspace.app.suggestion.SuggestionUtils.getAllEntriesByMetadatum;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import com.ibm.icu.text.CharsetDetector;
import com.ibm.icu.text.CharsetMatch;
import com.ibm.icu.text.Normalizer;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.suggestion.SuggestionEvidence;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.external.model.ExternalDataObject;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@see org.dspace.app.suggestion.oaire.EvidenceScorer} which evaluate ImportRecords
* based on Author's name.
*
* @author Andrea Bollini (andrea.bollini at 4science dot it)
* @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
*
*/
public class AuthorNamesScorer implements EvidenceScorer {
private List<String> contributorMetadata;
private List<String> names;
@Autowired
private ItemService itemService;
/**
* returns the metadata key of the Item which to base the filter on
* @return metadata key
*/
public List<String> getContributorMetadata() {
return contributorMetadata;
}
/**
* set the metadata key of the Item which to base the filter on
*/
public void setContributorMetadata(List<String> contributorMetadata) {
this.contributorMetadata = contributorMetadata;
}
/**
* return the metadata key of ImportRecord which to base the filter on
* @return
*/
public List<String> getNames() {
return names;
}
/**
* set the metadata key of ImportRecord which to base the filter on
*/
public void setNames(List<String> names) {
this.names = names;
}
/**
* Method which is responsible to evaluate ImportRecord based on authors name.
* This method extract the researcher name from Item using contributorMetadata fields
* and try to match them with values extract from ImportRecord using metadata keys defined
* in names.
* ImportRecords which don't match will be discarded.
*
* @param importRecord the import record to check
* @param researcher DSpace item
* @return the generated evidence or null if the record must be discarded
*/
@Override
public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecord) {
List<String[]> names = searchMetadataValues(researcher);
int maxNameLenght = names.stream().mapToInt(n -> n[0].length()).max().orElse(1);
List<String> metadataAuthors = new ArrayList<>();
for (String contributorMetadatum : contributorMetadata) {
metadataAuthors.addAll(getAllEntriesByMetadatum(importRecord, contributorMetadatum));
}
List<String> normalizedMetadataAuthors = metadataAuthors.stream().map(x -> normalize(x))
.collect(Collectors.toList());
int idx = 0;
for (String nMetadataAuthor : normalizedMetadataAuthors) {
Optional<String[]> found = names.stream()
.filter(a -> StringUtils.equalsIgnoreCase(a[0], nMetadataAuthor)).findFirst();
if (found.isPresent()) {
return new SuggestionEvidence(this.getClass().getSimpleName(),
100 * ((double) nMetadataAuthor.length() / (double) maxNameLenght),
"The author " + metadataAuthors.get(idx) + " at position " + (idx + 1)
+ " in the authors list matches the name " + found.get()[1]
+ " in the researcher profile");
}
idx++;
}
return null;
}
/**
* Return list of Item metadata values starting from metadata keys defined in class level variable names.
*
* @param researcher DSpace item
* @return list of metadata values
*/
private List<String[]> searchMetadataValues(Item researcher) {
List<String[]> authors = new ArrayList<String[]>();
for (String name : names) {
List<MetadataValue> values = itemService.getMetadataByMetadataString(researcher, name);
if (values != null) {
for (MetadataValue v : values) {
authors.add(new String[] {normalize(v.getValue()), v.getValue()});
}
}
}
return authors;
}
/**
* cleans up undesired characters
* @param value the string to clean up
* @return cleaned up string
* */
private String normalize(String value) {
String norm = Normalizer.normalize(value, Normalizer.NFD);
CharsetDetector cd = new CharsetDetector();
cd.setText(value.getBytes());
CharsetMatch detect = cd.detect();
if (detect != null && detect.getLanguage() != null) {
norm = norm.replaceAll("[^\\p{L}]", " ").toLowerCase(new Locale(detect.getLanguage()));
} else {
norm = norm.replaceAll("[^\\p{L}]", " ").toLowerCase();
}
return Arrays.asList(norm.split("\\s+")).stream().sorted().collect(Collectors.joining());
}
}

View File

@@ -0,0 +1,214 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.openaire;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import org.dspace.app.suggestion.SuggestionEvidence;
import org.dspace.app.suggestion.SuggestionUtils;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.util.MultiFormatDateParser;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@see org.dspace.app.suggestion.oaire.EvidenceScorer} which evaluate ImportRecords
* based on the distance from a date extracted from the ResearcherProfile (birthday / graduation date)
*
* @author Andrea Bollini (andrea.bollini at 4science dot it)
*
*/
public class DateScorer implements EvidenceScorer {
/**
* if available it should contains the metadata field key in the form (schema.element[.qualifier]) that contains
* the birth date of the researcher
*/
private String birthDateMetadata;
/**
* if available it should contains the metadata field key in the form (schema.element[.qualifier]) that contains
* the date of graduation of the researcher. If the metadata has multiple values the min will be used
*/
private String educationDateMetadata;
/**
* The minimal age that is expected for a researcher to be a potential author of a scholarly contribution
* (i.e. the minimum delta from the publication date and the birth date)
*/
private int birthDateDelta = 20;
/**
* The maximum age that is expected for a researcher to be a potential author of a scholarly contribution
* (i.e. the maximum delta from the publication date and the birth date)
*/
private int birthDateRange = 50;
/**
* The number of year from/before the graduation that is expected for a researcher to be a potential
* author of a scholarly contribution (i.e. the minimum delta from the publication date and the first
* graduation date)
*/
private int educationDateDelta = -3;
/**
* The maximum scientific longevity that is expected for a researcher from its graduation to be a potential
* author of a scholarly contribution (i.e. the maximum delta from the publication date and the first
* graduation date)
*/
private int educationDateRange = 50;
@Autowired
private ItemService itemService;
/**
* the metadata used in the publication to track the publication date (i.e. dc.date.issued)
*/
private String publicationDateMetadata;
public void setItemService(ItemService itemService) {
this.itemService = itemService;
}
public void setBirthDateMetadata(String birthDate) {
this.birthDateMetadata = birthDate;
}
public String getBirthDateMetadata() {
return birthDateMetadata;
}
public void setEducationDateMetadata(String educationDate) {
this.educationDateMetadata = educationDate;
}
public String getEducationDateMetadata() {
return educationDateMetadata;
}
public void setBirthDateDelta(int birthDateDelta) {
this.birthDateDelta = birthDateDelta;
}
public void setBirthDateRange(int birthDateRange) {
this.birthDateRange = birthDateRange;
}
public void setEducationDateDelta(int educationDateDelta) {
this.educationDateDelta = educationDateDelta;
}
public void setEducationDateRange(int educationDateRange) {
this.educationDateRange = educationDateRange;
}
public void setPublicationDateMetadata(String publicationDateMetadata) {
this.publicationDateMetadata = publicationDateMetadata;
}
/**
* Method which is responsible to evaluate ImportRecord based on the publication date.
* ImportRecords which have a date outside the defined or calculated expected range will be discarded.
* {@link DateScorer#birthDateMetadata}, {@link DateScorer#educationDateMetadata}
*
* @param importRecord the ExternalDataObject to check
* @param researcher DSpace item
* @return the generated evidence or null if the record must be discarded
*/
@Override
public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecord) {
Integer[] range = calculateRange(researcher);
if (range == null) {
return new SuggestionEvidence(this.getClass().getSimpleName(),
0,
"No assumption was possible about the publication year range. "
+ "Please consider setting your birthday in your profile.");
} else {
String optDate = SuggestionUtils.getFirstEntryByMetadatum(importRecord, publicationDateMetadata);
int year = getYear(optDate);
if (year > 0) {
if ((range[0] == null || year >= range[0]) &&
(range[1] == null || year <= range[1])) {
return new SuggestionEvidence(this.getClass().getSimpleName(),
10,
"The publication date is within the expected range [" + range[0] + ", "
+ range[1] + "]");
} else {
// outside the range, discard the suggestion
return null;
}
} else {
return new SuggestionEvidence(this.getClass().getSimpleName(),
0,
"No assumption was possible as the publication date is " + (optDate != null
? "unprocessable [" + optDate + "]"
: "unknown"));
}
}
}
/**
* returns min and max year interval in between it's probably that the researcher
* actually contributed to the suggested item
* @param researcher
* @return
*/
private Integer[] calculateRange(Item researcher) {
String birthDateStr = getSingleValue(researcher, birthDateMetadata);
int birthDateYear = getYear(birthDateStr);
int educationDateYear = getListMetadataValues(researcher, educationDateMetadata).stream()
.mapToInt(x -> getYear(x.getValue())).filter(d -> d > 0).min().orElse(-1);
if (educationDateYear > 0) {
return new Integer[] {
educationDateYear + educationDateDelta,
educationDateYear + educationDateDelta + educationDateRange
};
} else if (birthDateYear > 0) {
return new Integer[] {
birthDateYear + birthDateDelta,
birthDateYear + birthDateDelta + birthDateRange
};
} else {
return null;
}
}
private List<MetadataValue> getListMetadataValues(Item researcher, String metadataKey) {
if (metadataKey != null) {
return itemService.getMetadataByMetadataString(researcher, metadataKey);
} else {
return Collections.EMPTY_LIST;
}
}
private String getSingleValue(Item researcher, String metadataKey) {
if (metadataKey != null) {
return itemService.getMetadata(researcher, metadataKey);
}
return null;
}
private int getYear(String birthDateStr) {
int birthDateYear = -1;
if (birthDateStr != null) {
Date birthDate = MultiFormatDateParser.parse(birthDateStr);
if (birthDate != null) {
Calendar calendar = new GregorianCalendar();
calendar.setTime(birthDate);
birthDateYear = calendar.get(Calendar.YEAR);
}
}
return birthDateYear;
}
}

View File

@@ -0,0 +1,37 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.openaire;
import org.dspace.app.suggestion.SuggestionEvidence;
import org.dspace.content.Item;
import org.dspace.external.model.ExternalDataObject;
/**
* Interface used in {@see org.dspace.app.suggestion.oaire.PublicationApproverServiceImpl}
* to construct filtering pipeline.
*
* For each EvidenceScorer, the service call computeEvidence method.
*
* @author Andrea Bollini (andrea.bollini at 4science dot it)
* @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
*
*/
public interface EvidenceScorer {
/**
* Method to compute the suggestion evidence of an ImportRecord, a null evidence
* would lead the record to be discarded.
*
* @param researcher DSpace item
* @param importRecord the record to evaluate
* @return the generated suggestion evidence or null if the record should be
* discarded
*/
public SuggestionEvidence computeEvidence(Item researcher, ExternalDataObject importRecord);
}

View File

@@ -0,0 +1,256 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.openaire;
import static org.dspace.app.suggestion.SuggestionUtils.getAllEntriesByMetadatum;
import static org.dspace.app.suggestion.SuggestionUtils.getFirstEntryByMetadatum;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.solr.client.solrj.SolrServerException;
import org.dspace.app.suggestion.SolrSuggestionProvider;
import org.dspace.app.suggestion.Suggestion;
import org.dspace.app.suggestion.SuggestionEvidence;
import org.dspace.content.Item;
import org.dspace.content.dto.MetadataValueDTO;
import org.dspace.core.Context;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.external.provider.ExternalDataProvider;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Class responsible to load and manage ImportRecords from OpenAIRE
*
* @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
*
*/
public class PublicationLoader extends SolrSuggestionProvider {
private List<String> names;
private ExternalDataProvider primaryProvider;
private List<ExternalDataProvider> otherProviders;
@Autowired
private ConfigurationService configurationService;
private List<EvidenceScorer> pipeline;
public void setPrimaryProvider(ExternalDataProvider primaryProvider) {
this.primaryProvider = primaryProvider;
}
public void setOtherProviders(List<ExternalDataProvider> otherProviders) {
this.otherProviders = otherProviders;
}
/**
* Set the pipeline of Approver
* @param pipeline list Approver
*/
public void setPipeline(List<EvidenceScorer> pipeline) {
this.pipeline = pipeline;
}
/**
* This method filter a list of ImportRecords using a pipeline of AuthorNamesApprover
* and return a filtered list of ImportRecords.
*
* @see org.dspace.app.suggestion.openaire.AuthorNamesScorer
* @param researcher the researcher Item
* @param importRecords List of import record
* @return a list of filtered import records
*/
public List<Suggestion> reduceAndTransform(Item researcher, List<ExternalDataObject> importRecords) {
List<Suggestion> results = new ArrayList<>();
for (ExternalDataObject r : importRecords) {
boolean skip = false;
List<SuggestionEvidence> evidences = new ArrayList<SuggestionEvidence>();
for (EvidenceScorer authorNameApprover : pipeline) {
SuggestionEvidence evidence = authorNameApprover.computeEvidence(researcher, r);
if (evidence != null) {
evidences.add(evidence);
} else {
skip = true;
break;
}
}
if (!skip) {
Suggestion suggestion = translateImportRecordToSuggestion(researcher, r);
suggestion.getEvidences().addAll(evidences);
results.add(suggestion);
}
}
return results;
}
/**
* Save a List of ImportRecord into Solr.
* ImportRecord will be translate into a SolrDocument by the method translateImportRecordToSolrDocument.
*
* @param context the DSpace Context
* @param researcher a DSpace Item
* @throws SolrServerException
* @throws IOException
*/
public void importAuthorRecords(Context context, Item researcher)
throws SolrServerException, IOException {
int offset = 0;
int limit = 10;
int loaded = limit;
List<String> searchValues = searchMetadataValues(researcher);
while (loaded > 0) {
List<ExternalDataObject> metadata = getImportRecords(searchValues, researcher, offset, limit);
if (metadata.isEmpty()) {
loaded = 0;
continue;
}
offset += limit;
loaded = metadata.size();
List<Suggestion> records = reduceAndTransform(researcher, metadata);
for (Suggestion record : records) {
solrSuggestionStorageService.addSuggestion(record, false, false);
}
}
solrSuggestionStorageService.commit();
}
/**
* Translate an ImportRecord into a Suggestion
* @param item DSpace item
* @param record ImportRecord
* @return Suggestion
*/
private Suggestion translateImportRecordToSuggestion(Item item, ExternalDataObject record) {
String openAireId = record.getId();
Suggestion suggestion = new Suggestion(getSourceName(), item, openAireId);
suggestion.setDisplay(getFirstEntryByMetadatum(record, "dc", "title", null));
suggestion.getMetadata().add(
new MetadataValueDTO("dc", "title", null, null, getFirstEntryByMetadatum(record, "dc", "title", null)));
suggestion.getMetadata().add(new MetadataValueDTO("dc", "date", "issued", null,
getFirstEntryByMetadatum(record, "dc", "date", "issued")));
suggestion.getMetadata().add(new MetadataValueDTO("dc", "description", "abstract", null,
getFirstEntryByMetadatum(record, "dc", "description", "abstract")));
suggestion.setExternalSourceUri(configurationService.getProperty("dspace.server.url")
+ "/api/integration/externalsources/" + primaryProvider.getSourceIdentifier() + "/entryValues/"
+ openAireId);
for (String o : getAllEntriesByMetadatum(record, "dc", "source", null)) {
suggestion.getMetadata().add(new MetadataValueDTO("dc", "source", null, null, o));
}
for (String o : getAllEntriesByMetadatum(record, "dc", "contributor", "author")) {
suggestion.getMetadata().add(new MetadataValueDTO("dc", "contributor", "author", null, o));
}
return suggestion;
}
public List<String> getNames() {
return names;
}
public void setNames(List<String> names) {
this.names = names;
}
/**
* Load metadata from OpenAIRE using the import service. The service use the value
* get from metadata key defined in class level variable names as author to query OpenAIRE.
*
* @see org.dspace.importer.external.openaire.service.OpenAireImportMetadataSourceServiceImpl
* @param searchValues query
* @param researcher item to extract metadata from
* @param limit for pagination purpose
* @param offset for pagination purpose
* @return list of ImportRecord
*/
private List<ExternalDataObject> getImportRecords(List<String> searchValues,
Item researcher, int offset, int limit) {
List<ExternalDataObject> matchingRecords = new ArrayList<>();
for (String searchValue : searchValues) {
matchingRecords.addAll(
primaryProvider.searchExternalDataObjects(searchValue, offset, limit));
}
List<ExternalDataObject> toReturn = removeDuplicates(matchingRecords);
return toReturn;
}
/**
* This method remove duplicates from importRecords list.
* An element is a duplicate if in the list exist another element
* with the same value of the metadatum 'dc.identifier.other'
*
* @param importRecords list of ImportRecord
* @return list of ImportRecords without duplicates
*/
private List<ExternalDataObject> removeDuplicates(List<ExternalDataObject> importRecords) {
List<ExternalDataObject> filteredRecords = new ArrayList<>();
for (ExternalDataObject currentRecord : importRecords) {
if (!isDuplicate(currentRecord, filteredRecords)) {
filteredRecords.add(currentRecord);
}
}
return filteredRecords;
}
/**
* Check if the ImportRecord is already present in the list.
* The comparison is made on the value of metadatum with key 'dc.identifier.other'
*
* @param dto An importRecord instance
* @param importRecords a list of importRecord
* @return true if dto is already present in importRecords, false otherwise
*/
private boolean isDuplicate(ExternalDataObject dto, List<ExternalDataObject> importRecords) {
String currentItemId = dto.getId();
if (currentItemId == null) {
return true;
}
for (ExternalDataObject importRecord : importRecords) {
if (currentItemId.equals(importRecord.getId())) {
return true;
}
}
return false;
}
/**
* Return list of Item metadata values starting from metadata keys defined in class level variable names.
*
* @param researcher DSpace item
* @return list of metadata values
*/
private List<String> searchMetadataValues(Item researcher) {
List<String> authors = new ArrayList<String>();
for (String name : names) {
String value = itemService.getMetadata(researcher, name);
if (value != null) {
authors.add(value);
}
}
return authors;
}
@Override
protected boolean isExternalDataObjectPotentiallySuggested(Context context, ExternalDataObject externalDataObject) {
if (StringUtils.equals(externalDataObject.getSource(), primaryProvider.getSourceIdentifier())) {
return true;
} else if (otherProviders != null) {
return otherProviders.stream()
.anyMatch(x -> StringUtils.equals(externalDataObject.getSource(), x.getSourceIdentifier()));
} else {
return false;
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.openaire;
import org.apache.commons.cli.Options;
/**
* Extension of {@link PublicationLoaderScriptConfiguration} for CLI.
*
* @author Alessandro Martelli (alessandro.martelli at 4science.it)
*/
public class PublicationLoaderCliScriptConfiguration<T extends PublicationLoaderRunnable>
extends PublicationLoaderScriptConfiguration<T> {
@Override
public Options getOptions() {
Options options = super.getOptions();
options.addOption("h", "help", false, "help");
options.getOption("h").setType(boolean.class);
super.options = options;
return options;
}
}

View File

@@ -0,0 +1,115 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.openaire;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.cli.ParseException;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.discovery.DiscoverQuery;
import org.dspace.discovery.SearchService;
import org.dspace.discovery.SearchServiceException;
import org.dspace.discovery.SearchUtils;
import org.dspace.discovery.utils.DiscoverQueryBuilder;
import org.dspace.discovery.utils.parameter.QueryBuilderSearchFilter;
import org.dspace.scripts.DSpaceRunnable;
import org.dspace.sort.SortOption;
import org.dspace.utils.DSpace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Runner responsible to import metadata about authors from OpenAIRE to Solr.
* This runner works in two ways:
* If -s parameter with a valid UUID is received, then the specific researcher
* with this UUID will be used.
* Invocation without any parameter results in massive import, processing all
* authors registered in DSpace.
*
* @author Alessandro Martelli (alessandro.martelli at 4science.it)
*/
public class PublicationLoaderRunnable
extends DSpaceRunnable<PublicationLoaderScriptConfiguration<PublicationLoaderRunnable>> {
private static final Logger LOGGER = LoggerFactory.getLogger(PublicationLoaderRunnable.class);
private PublicationLoader oairePublicationLoader = null;
protected Context context;
protected String profile;
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public PublicationLoaderScriptConfiguration<PublicationLoaderRunnable> getScriptConfiguration() {
PublicationLoaderScriptConfiguration configuration = new DSpace().getServiceManager()
.getServiceByName("import-openaire-suggestions", PublicationLoaderScriptConfiguration.class);
return configuration;
}
@Override
public void setup() throws ParseException {
oairePublicationLoader = new DSpace().getServiceManager().getServiceByName(
"OpenairePublicationLoader", PublicationLoader.class);
profile = commandLine.getOptionValue("s");
if (profile == null) {
LOGGER.info("No argument for -s, process all profile");
} else {
LOGGER.info("Process eperson item with UUID " + profile);
}
}
@Override
public void internalRun() throws Exception {
context = new Context();
Iterator<Item> researchers = getResearchers(profile);
while (researchers.hasNext()) {
Item researcher = researchers.next();
oairePublicationLoader.importAuthorRecords(context, researcher);
}
}
/**
* Get the Item(s) which map a researcher from Solr. If the uuid is specified,
* the researcher with this UUID will be chosen. If the uuid doesn't match any
* researcher, the method returns an empty array list. If uuid is null, all
* research will be return.
*
* @param profileUUID uuid of the researcher. If null, all researcher will be
* returned.
* @return the researcher with specified UUID or all researchers
*/
@SuppressWarnings("rawtypes")
private Iterator<Item> getResearchers(String profileUUID) {
SearchService searchService = new DSpace().getSingletonService(SearchService.class);
DiscoverQueryBuilder queryBuilder = SearchUtils.getQueryBuilder();
List<QueryBuilderSearchFilter> filters = new ArrayList<QueryBuilderSearchFilter>();
String query = "*:*";
if (profileUUID != null) {
query = "search.resourceid:" + profileUUID.toString();
}
try {
DiscoverQuery discoverQuery = queryBuilder.buildQuery(context, null,
SearchUtils.getDiscoveryConfigurationByName("person"),
query, filters,
"Item", 10, Long.getLong("0"), null, SortOption.DESCENDING);
return searchService.iteratorSearch(context, null, discoverQuery);
} catch (SearchServiceException e) {
LOGGER.error("Unable to read researcher on solr", e);
}
return null;
}
}

View File

@@ -0,0 +1,36 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.openaire;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.ParseException;
import org.dspace.utils.DSpace;
public class PublicationLoaderRunnableCli extends PublicationLoaderRunnable {
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public PublicationLoaderCliScriptConfiguration getScriptConfiguration() {
PublicationLoaderCliScriptConfiguration configuration = new DSpace().getServiceManager()
.getServiceByName("import-openaire-suggestions", PublicationLoaderCliScriptConfiguration.class);
return configuration;
}
@Override
public void setup() throws ParseException {
super.setup();
// in case of CLI we show the help prompt
if (commandLine.hasOption('h')) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("Import Researchers Suggestions", getScriptConfiguration().getOptions());
System.exit(0);
}
}
}

View File

@@ -0,0 +1,56 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion.openaire;
import org.apache.commons.cli.Options;
import org.dspace.scripts.configuration.ScriptConfiguration;
public class PublicationLoaderScriptConfiguration<T extends PublicationLoaderRunnable>
extends ScriptConfiguration<T> {
private Class<T> dspaceRunnableClass;
@Override
public Class<T> getDspaceRunnableClass() {
return dspaceRunnableClass;
}
/**
* Generic setter for the dspaceRunnableClass
* @param dspaceRunnableClass The dspaceRunnableClass to be set on this PublicationLoaderScriptConfiguration
*/
@Override
public void setDspaceRunnableClass(Class<T> dspaceRunnableClass) {
this.dspaceRunnableClass = dspaceRunnableClass;
}
/*
@Override
public boolean isAllowedToExecute(Context context) {
try {
return authorizeService.isAdmin(context);
} catch (SQLException e) {
throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e);
}
}
*/
@Override
public Options getOptions() {
if (options == null) {
Options options = new Options();
options.addOption("s", "single-researcher", true, "Single researcher UUID");
options.getOption("s").setType(String.class);
super.options = options;
}
return options;
}
}

View File

@@ -628,12 +628,23 @@ public class AuthorizeUtil {
// actually expected to be returning true.
// For example the LDAP canSelfRegister will return true due to auto-register, while that
// does not imply a new user can register explicitly
return AuthenticateServiceFactory.getInstance().getAuthenticationService()
.allowSetPassword(context, request, null);
return authorizePasswordChange(context, request);
}
return false;
}
/**
* This method will return a boolean indicating whether the current user is allowed to reset the password
* or not
*
* @return A boolean indicating whether the current user can reset its password or not
* @throws SQLException If something goes wrong
*/
public static boolean authorizeForgotPassword() {
return DSpaceServicesFactory.getInstance().getConfigurationService()
.getBooleanProperty("user.forgot-password", true);
}
/**
* This method will return a boolean indicating whether it's allowed to update the password for the EPerson
* with the given email and canLogin property
@@ -647,8 +658,7 @@ public class AuthorizeUtil {
if (eperson != null && eperson.canLogIn()) {
HttpServletRequest request = new DSpace().getRequestService().getCurrentRequest()
.getHttpServletRequest();
return AuthenticateServiceFactory.getInstance().getAuthenticationService()
.allowSetPassword(context, request, null);
return authorizePasswordChange(context, request);
}
} catch (SQLException e) {
log.error("Something went wrong trying to retrieve EPerson for email: " + email, e);
@@ -656,6 +666,19 @@ public class AuthorizeUtil {
return false;
}
/**
* Checks if the current configuration has at least one password based authentication method
*
* @param context Dspace Context
* @param request Current Request
* @return True if the password change is enabled
* @throws SQLException
*/
protected static boolean authorizePasswordChange(Context context, HttpServletRequest request) throws SQLException {
return AuthenticateServiceFactory.getInstance().getAuthenticationService()
.allowSetPassword(context, request, null);
}
/**
* This method checks if the community Admin can manage accounts
*

View File

@@ -14,7 +14,6 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
@@ -150,17 +149,16 @@ public class DCInputsReader {
* Returns the set of DC inputs used for a particular collection, or the
* default set if no inputs defined for the collection
*
* @param collectionHandle collection's unique Handle
* @param collection collection for which search the set of DC inputs
* @return DC input set
* @throws DCInputsReaderException if no default set defined
* @throws ServletException
*/
public List<DCInputSet> getInputsByCollectionHandle(String collectionHandle)
public List<DCInputSet> getInputsByCollection(Collection collection)
throws DCInputsReaderException {
SubmissionConfig config;
try {
config = SubmissionServiceFactory.getInstance().getSubmissionConfigService()
.getSubmissionConfigByCollection(collectionHandle);
.getSubmissionConfigByCollection(collection);
String formName = config.getSubmissionName();
if (formName == null) {
throw new DCInputsReaderException("No form designated as default");
@@ -691,7 +689,7 @@ public class DCInputsReader {
public String getInputFormNameByCollectionAndField(Collection collection, String field)
throws DCInputsReaderException {
List<DCInputSet> inputSets = getInputsByCollectionHandle(collection.getHandle());
List<DCInputSet> inputSets = getInputsByCollection(collection);
for (DCInputSet inputSet : inputSets) {
String[] tokenized = Utils.tokenize(field);
String schema = tokenized[0];

View File

@@ -11,6 +11,7 @@ import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -21,6 +22,7 @@ import javax.xml.parsers.FactoryConfigurationError;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DSpaceObject;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.CollectionService;
@@ -90,6 +92,13 @@ public class SubmissionConfigReader {
*/
private Map<String, String> collectionToSubmissionConfig = null;
/**
* Hashmap which stores which submission process configuration is used by
* which community, computed from the item submission config file
* (specifically, the 'submission-map' tag)
*/
private Map<String, String> communityToSubmissionConfig = null;
/**
* Reference to the global submission step definitions defined in the
* "step-definitions" section
@@ -127,6 +136,7 @@ public class SubmissionConfigReader {
public void reload() throws SubmissionConfigReaderException {
collectionToSubmissionConfig = null;
communityToSubmissionConfig = null;
stepDefns = null;
submitDefns = null;
buildInputs(configDir + SUBMIT_DEF_FILE_PREFIX + SUBMIT_DEF_FILE_SUFFIX);
@@ -145,7 +155,8 @@ public class SubmissionConfigReader {
*/
private void buildInputs(String fileName) throws SubmissionConfigReaderException {
collectionToSubmissionConfig = new HashMap<String, String>();
submitDefns = new HashMap<String, List<Map<String, String>>>();
communityToSubmissionConfig = new HashMap<String, String>();
submitDefns = new LinkedHashMap<String, List<Map<String, String>>>();
String uri = "file:" + new File(fileName).getAbsolutePath();
@@ -210,18 +221,41 @@ public class SubmissionConfigReader {
* Returns the Item Submission process config used for a particular
* collection, or the default if none is defined for the collection
*
* @param collectionHandle collection's unique Handle
* @param col collection for which search Submission process config
* @return the SubmissionConfig representing the item submission config
* @throws SubmissionConfigReaderException if no default submission process configuration defined
* @throws IllegalStateException if no default submission process configuration defined
*/
public SubmissionConfig getSubmissionConfigByCollection(String collectionHandle) {
// get the name of the submission process config for this collection
String submitName = collectionToSubmissionConfig
.get(collectionHandle);
if (submitName == null) {
public SubmissionConfig getSubmissionConfigByCollection(Collection col) {
String submitName;
if (col != null) {
// get the name of the submission process config for this collection
submitName = collectionToSubmissionConfig
.get(DEFAULT_COLLECTION);
.get(col.getHandle());
if (submitName != null) {
return getSubmissionConfigByName(submitName);
}
if (!communityToSubmissionConfig.isEmpty()) {
try {
List<Community> communities = col.getCommunities();
for (Community com : communities) {
submitName = getSubmissionConfigByCommunity(com);
if (submitName != null) {
return getSubmissionConfigByName(submitName);
}
}
} catch (SQLException sqle) {
throw new IllegalStateException("Error occurred while getting item submission configured " +
"by community", sqle);
}
}
}
submitName = collectionToSubmissionConfig.get(DEFAULT_COLLECTION);
if (submitName == null) {
throw new IllegalStateException(
"No item submission process configuration designated as 'default' in 'submission-map' section of " +
@@ -230,6 +264,30 @@ public class SubmissionConfigReader {
return getSubmissionConfigByName(submitName);
}
/**
* Recursive function to return the Item Submission process config
* used for a community or the closest community parent, or null
* if none is defined
*
* @param com community for which search Submission process config
* @return the SubmissionConfig representing the item submission config
*/
private String getSubmissionConfigByCommunity(Community com) {
String submitName = communityToSubmissionConfig
.get(com.getHandle());
if (submitName != null) {
return submitName;
}
List<Community> communities = com.getParentCommunities();
for (Community parentCom : communities) {
submitName = getSubmissionConfigByCommunity(parentCom);
if (submitName != null) {
return submitName;
}
}
return null;
}
/**
* Returns the Item Submission process config
*
@@ -357,13 +415,14 @@ public class SubmissionConfigReader {
Node nd = nl.item(i);
if (nd.getNodeName().equals("name-map")) {
String id = getAttribute(nd, "collection-handle");
String communityId = getAttribute(nd, "community-handle");
String entityType = getAttribute(nd, "collection-entity-type");
String value = getAttribute(nd, "submission-name");
String content = getValue(nd);
if (id == null && entityType == null) {
if (id == null && communityId == null && entityType == null) {
throw new SAXException(
"name-map element is missing collection-handle or collection-entity-type attribute " +
"in 'item-submission.xml'");
"name-map element is missing collection-handle or community-handle or collection-entity-type " +
"attribute in 'item-submission.xml'");
}
if (value == null) {
throw new SAXException(
@@ -375,7 +434,8 @@ public class SubmissionConfigReader {
}
if (id != null) {
collectionToSubmissionConfig.put(id, value);
} else if (communityId != null) {
communityToSubmissionConfig.put(communityId, value);
} else {
// get all collections for this entity-type
List<Collection> collections = collectionService.findAllCollectionsByEntityType( context,

View File

@@ -405,21 +405,13 @@ public class Util {
DCInput myInputs = null;
boolean myInputsFound = false;
String formFileName = I18nUtil.getInputFormsFileName(locale);
String col_handle = "";
Collection collection = item.getOwningCollection();
if (collection == null) {
// set an empty handle so to get the default input set
col_handle = "";
} else {
col_handle = collection.getHandle();
}
// Read the input form file for the specific collection
DCInputsReader inputsReader = new DCInputsReader(formFileName);
List<DCInputSet> inputSets = inputsReader.getInputsByCollectionHandle(col_handle);
List<DCInputSet> inputSets = inputsReader.getInputsByCollection(collection);
// Replace the values of Metadatum[] with the correct ones in case
// of
@@ -500,8 +492,8 @@ public class Util {
public static List<String> differenceInSubmissionFields(Collection fromCollection, Collection toCollection)
throws DCInputsReaderException {
DCInputsReader reader = new DCInputsReader();
List<DCInputSet> from = reader.getInputsByCollectionHandle(fromCollection.getHandle());
List<DCInputSet> to = reader.getInputsByCollectionHandle(toCollection.getHandle());
List<DCInputSet> from = reader.getInputsByCollection(fromCollection);
List<DCInputSet> to = reader.getInputsByCollection(toCollection);
Set<String> fromFieldName = new HashSet<>();
Set<String> toFieldName = new HashSet<>();

View File

@@ -150,7 +150,6 @@ public class InstallItemServiceImpl implements InstallItemService {
return finishItem(c, item, is);
}
protected void populateMetadata(Context c, Item item)
throws SQLException, AuthorizeException {
// create accession date
@@ -158,15 +157,6 @@ public class InstallItemServiceImpl implements InstallItemService {
itemService.addMetadata(c, item, MetadataSchemaEnum.DC.getName(),
"date", "accessioned", null, now.toString());
// add date available if not under embargo, otherwise it will
// be set when the embargo is lifted.
// this will flush out fatal embargo metadata
// problems before we set inArchive.
if (embargoService.getEmbargoTermsAsDate(c, item) == null) {
itemService.addMetadata(c, item, MetadataSchemaEnum.DC.getName(),
"date", "available", null, now.toString());
}
// If issue date is set as "today" (literal string), then set it to current date
// In the below loop, we temporarily clear all issued dates and re-add, one-by-one,
// replacing "today" with today's date.

View File

@@ -1435,16 +1435,6 @@ prevent the generation of resource policy entry values with null dspace_object a
}
}
@Override
public Iterator<Item> findByMetadataQuery(Context context, List<List<MetadataField>> listFieldList,
List<String> query_op, List<String> query_val, List<UUID> collectionUuids,
String regexClause, int offset, int limit)
throws SQLException, AuthorizeException, IOException {
return itemDAO
.findByMetadataQuery(context, listFieldList, query_op, query_val, collectionUuids, regexClause, offset,
limit);
}
@Override
public DSpaceObject getAdminObject(Context context, Item item, int action) throws SQLException {
DSpaceObject adminObject = null;

View File

@@ -242,7 +242,7 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService
// check if it is the requested collection
Map<String, ChoiceAuthority> controllerFormDef = controllerFormDefinitions.get(fieldKey);
SubmissionConfig submissionConfig = submissionConfigService
.getSubmissionConfigByCollection(collection.getHandle());
.getSubmissionConfigByCollection(collection);
String submissionName = submissionConfig.getSubmissionName();
// check if the requested collection has a submission definition that use an authority for the metadata
if (controllerFormDef.containsKey(submissionName)) {
@@ -495,7 +495,7 @@ public final class ChoiceAuthorityServiceImpl implements ChoiceAuthorityService
try {
configReaderService = SubmissionServiceFactory.getInstance().getSubmissionConfigService();
SubmissionConfig submissionName = configReaderService
.getSubmissionConfigByCollection(collection.getHandle());
.getSubmissionConfigByCollection(collection);
ma = controllerFormDefinitions.get(fieldKey).get(submissionName.getSubmissionName());
} catch (SubmissionConfigReaderException e) {
// the system is in an illegal state as the submission definition is not valid

View File

@@ -156,7 +156,8 @@ public class DCInputAuthority extends SelfNamedPlugin implements ChoiceAuthority
int found = 0;
List<Choice> v = new ArrayList<Choice>();
for (int i = 0; i < valuesLocale.length; ++i) {
if (query == null || StringUtils.containsIgnoreCase(valuesLocale[i], query)) {
// In a DCInputAuthority context, a user will want to query the labels, not the values
if (query == null || StringUtils.containsIgnoreCase(labelsLocale[i], query)) {
if (found >= start && v.size() < limit) {
v.add(new Choice(null, valuesLocale[i], labelsLocale[i]));
if (valuesLocale[i].equalsIgnoreCase(query)) {

View File

@@ -11,7 +11,6 @@ import java.sql.SQLException;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import org.dspace.content.Collection;
import org.dspace.content.Community;
@@ -80,10 +79,6 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
public Iterator<Item> findByMetadataField(Context context, MetadataField metadataField, String value,
boolean inArchive) throws SQLException;
public Iterator<Item> findByMetadataQuery(Context context, List<List<MetadataField>> listFieldList,
List<String> query_op, List<String> query_val, List<UUID> collectionUuids,
String regexClause, int offset, int limit) throws SQLException;
public Iterator<Item> findByAuthorityValue(Context context, MetadataField metadataField, String authority,
boolean inArchive) throws SQLException;

View File

@@ -12,7 +12,6 @@ import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.criteria.CriteriaBuilder;
@@ -24,21 +23,11 @@ import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.Item_;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataValue;
import org.dspace.content.dao.ItemDAO;
import org.dspace.core.AbstractHibernateDSODAO;
import org.dspace.core.Context;
import org.dspace.core.UUIDIterator;
import org.dspace.eperson.EPerson;
import org.hibernate.Criteria;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Property;
import org.hibernate.criterion.Restrictions;
import org.hibernate.criterion.Subqueries;
import org.hibernate.type.StandardBasicTypes;
/**
* Hibernate implementation of the Database Access Object interface class for the Item object.
@@ -193,120 +182,6 @@ public class ItemDAOImpl extends AbstractHibernateDSODAO<Item> implements ItemDA
return new UUIDIterator<Item>(context, uuids, Item.class, this);
}
enum OP {
equals {
public Criterion buildPredicate(String val, String regexClause) {
return Property.forName("mv.value").eq(val);
}
},
not_equals {
public Criterion buildPredicate(String val, String regexClause) {
return OP.equals.buildPredicate(val, regexClause);
}
},
like {
public Criterion buildPredicate(String val, String regexClause) {
return Property.forName("mv.value").like(val);
}
},
not_like {
public Criterion buildPredicate(String val, String regexClause) {
return OP.like.buildPredicate(val, regexClause);
}
},
contains {
public Criterion buildPredicate(String val, String regexClause) {
return Property.forName("mv.value").like("%" + val + "%");
}
},
doesnt_contain {
public Criterion buildPredicate(String val, String regexClause) {
return OP.contains.buildPredicate(val, regexClause);
}
},
exists {
public Criterion buildPredicate(String val, String regexClause) {
return Property.forName("mv.value").isNotNull();
}
},
doesnt_exist {
public Criterion buildPredicate(String val, String regexClause) {
return OP.exists.buildPredicate(val, regexClause);
}
},
matches {
public Criterion buildPredicate(String val, String regexClause) {
return Restrictions.sqlRestriction(regexClause, val, StandardBasicTypes.STRING);
}
},
doesnt_match {
public Criterion buildPredicate(String val, String regexClause) {
return OP.matches.buildPredicate(val, regexClause);
}
};
public abstract Criterion buildPredicate(String val, String regexClause);
}
@Override
@Deprecated
public Iterator<Item> findByMetadataQuery(Context context, List<List<MetadataField>> listFieldList,
List<String> query_op, List<String> query_val, List<UUID> collectionUuids,
String regexClause, int offset, int limit) throws SQLException {
Criteria criteria = getHibernateSession(context).createCriteria(Item.class, "item");
criteria.setFirstResult(offset);
criteria.setMaxResults(limit);
if (!collectionUuids.isEmpty()) {
DetachedCriteria dcollCriteria = DetachedCriteria.forClass(Collection.class, "coll");
dcollCriteria.setProjection(Projections.property("coll.id"));
dcollCriteria.add(Restrictions.eqProperty("coll.id", "item.owningCollection"));
dcollCriteria.add(Restrictions.in("coll.id", collectionUuids));
criteria.add(Subqueries.exists(dcollCriteria));
}
int index = Math.min(listFieldList.size(), Math.min(query_op.size(), query_val.size()));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < index; i++) {
OP op = OP.valueOf(query_op.get(i));
if (op == null) {
log.warn("Skipping Invalid Operator: " + query_op.get(i));
continue;
}
if (op == OP.matches || op == OP.doesnt_match) {
if (regexClause.isEmpty()) {
log.warn("Skipping Unsupported Regex Operator: " + query_op.get(i));
continue;
}
}
DetachedCriteria subcriteria = DetachedCriteria.forClass(MetadataValue.class, "mv");
subcriteria.add(Property.forName("mv.dSpaceObject").eqProperty("item.id"));
subcriteria.setProjection(Projections.property("mv.dSpaceObject"));
if (!listFieldList.get(i).isEmpty()) {
subcriteria.add(Restrictions.in("metadataField", listFieldList.get(i)));
}
subcriteria.add(op.buildPredicate(query_val.get(i), regexClause));
if (op == OP.exists || op == OP.equals || op == OP.like || op == OP.contains || op == OP.matches) {
criteria.add(Subqueries.exists(subcriteria));
} else {
criteria.add(Subqueries.notExists(subcriteria));
}
}
criteria.addOrder(Order.asc("item.id"));
log.debug(String.format("Running custom query with %d filters", index));
return ((List<Item>) criteria.list()).iterator();
}
@Override
public Iterator<Item> findByAuthorityValue(Context context, MetadataField metadataField, String authority,
boolean inArchive) throws SQLException {

View File

@@ -23,7 +23,6 @@ import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.EntityType;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataValue;
import org.dspace.content.Thumbnail;
import org.dspace.content.WorkspaceItem;
@@ -749,11 +748,6 @@ public interface ItemService
String schema, String element, String qualifier, String value)
throws SQLException, AuthorizeException, IOException;
public Iterator<Item> findByMetadataQuery(Context context, List<List<MetadataField>> listFieldList,
List<String> query_op, List<String> query_val, List<UUID> collectionUuids,
String regexClause, int offset, int limit)
throws SQLException, AuthorizeException, IOException;
/**
* Find all the items in the archive with a given authority key value
* in the indicated metadata field.

View File

@@ -17,6 +17,7 @@ import org.dspace.app.util.DCInput;
import org.dspace.app.util.DCInputSet;
import org.dspace.app.util.DCInputsReader;
import org.dspace.app.util.DCInputsReaderException;
import org.dspace.content.Collection;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
@@ -69,7 +70,7 @@ public class RequiredMetadata extends AbstractCurationTask {
handle = "in workflow";
}
sb.append("Item: ").append(handle);
for (String req : getReqList(item.getOwningCollection().getHandle())) {
for (String req : getReqList(item.getOwningCollection())) {
List<MetadataValue> vals = itemService.getMetadataByMetadataString(item, req);
if (vals.size() == 0) {
sb.append(" missing required field: ").append(req);
@@ -91,14 +92,14 @@ public class RequiredMetadata extends AbstractCurationTask {
}
}
protected List<String> getReqList(String handle) throws DCInputsReaderException {
List<String> reqList = reqMap.get(handle);
protected List<String> getReqList(Collection collection) throws DCInputsReaderException {
List<String> reqList = reqMap.get(collection.getHandle());
if (reqList == null) {
reqList = reqMap.get("default");
}
if (reqList == null) {
reqList = new ArrayList<String>();
List<DCInputSet> inputSet = reader.getInputsByCollectionHandle(handle);
List<DCInputSet> inputSet = reader.getInputsByCollection(collection);
for (DCInputSet inputs : inputSet) {
for (DCInput[] row : inputs.getFields()) {
for (DCInput input : row) {

View File

@@ -7,14 +7,20 @@
*/
package org.dspace.discovery;
import static org.dspace.discovery.IndexClientOptions.TYPE_OPTION;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DSpaceObject;
@@ -51,6 +57,17 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
return;
}
String type = null;
if (commandLine.hasOption(TYPE_OPTION)) {
List<String> indexableObjectTypes = IndexObjectFactoryFactory.getInstance().getIndexFactories().stream()
.map((indexFactory -> indexFactory.getType())).collect(Collectors.toList());
type = commandLine.getOptionValue(TYPE_OPTION);
if (!indexableObjectTypes.contains(type)) {
handler.handleException(String.format("%s is not a valid indexable object type, options: %s",
type, Arrays.toString(indexableObjectTypes.toArray())));
}
}
/** Acquire from dspace-services in future */
/**
* new DSpace.getServiceManager().getServiceByName("org.dspace.discovery.SolrIndexer");
@@ -113,6 +130,10 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
} else if (indexClientOptions == IndexClientOptions.BUILD ||
indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) {
handler.logInfo("(Re)building index from scratch.");
if (StringUtils.isNotBlank(type)) {
handler.logWarning(String.format("Type option, %s, not applicable for entire index rebuild option, b" +
", type will be ignored", TYPE_OPTION));
}
indexer.deleteIndex();
indexer.createIndex(context);
if (indexClientOptions == IndexClientOptions.BUILDANDSPELLCHECK) {
@@ -133,14 +154,14 @@ public class IndexClient extends DSpaceRunnable<IndexDiscoveryScriptConfiguratio
} else if (indexClientOptions == IndexClientOptions.UPDATE ||
indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) {
handler.logInfo("Updating Index");
indexer.updateIndex(context, false);
indexer.updateIndex(context, false, type);
if (indexClientOptions == IndexClientOptions.UPDATEANDSPELLCHECK) {
checkRebuildSpellCheck(commandLine, indexer);
}
} else if (indexClientOptions == IndexClientOptions.FORCEUPDATE ||
indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) {
handler.logInfo("Updating Index");
indexer.updateIndex(context, true);
indexer.updateIndex(context, true, type);
if (indexClientOptions == IndexClientOptions.FORCEUPDATEANDSPELLCHECK) {
checkRebuildSpellCheck(commandLine, indexer);
}

View File

@@ -8,8 +8,13 @@
package org.dspace.discovery;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Options;
import org.dspace.discovery.indexobject.factory.IndexObjectFactoryFactory;
/**
* This Enum holds all the possible options and combinations for the Index discovery script
@@ -29,6 +34,8 @@ public enum IndexClientOptions {
FORCEUPDATEANDSPELLCHECK,
HELP;
public static final String TYPE_OPTION = "t";
/**
* This method resolves the CommandLine parameters to figure out which action the index-discovery script should
* perform
@@ -71,11 +78,15 @@ public enum IndexClientOptions {
protected static Options constructOptions() {
Options options = new Options();
List<String> indexableObjectTypes = IndexObjectFactoryFactory.getInstance().getIndexFactories().stream()
.map((indexFactory -> indexFactory.getType())).collect(Collectors.toList());
options
.addOption("r", "remove", true, "remove an Item, Collection or Community from index based on its handle");
options.addOption("i", "index", true,
"add or update an Item, Collection or Community based on its handle or uuid");
options.addOption(TYPE_OPTION, "type", true, "reindex only specific type of " +
"(re)indexable objects; options: " + Arrays.toString(indexableObjectTypes.toArray()));
options.addOption("c", "clean", false,
"clean existing index removing any documents that no longer exist in the db");
options.addOption("d", "delete", false,

View File

@@ -64,7 +64,14 @@ public abstract class IndexFactoryImpl<T extends IndexableObject, S> implements
//Do any additional indexing, depends on the plugins
for (SolrServiceIndexPlugin solrServiceIndexPlugin : ListUtils.emptyIfNull(solrServiceIndexPlugins)) {
solrServiceIndexPlugin.additionalIndex(context, indexableObject, doc);
try {
solrServiceIndexPlugin.additionalIndex(context, indexableObject, doc);
} catch (Exception e) {
log.error("An error occurred while indexing additional fields. " +
"Could not fully index item with UUID: {}. Plugin: {}",
indexableObject.getUniqueIndexID(), solrServiceIndexPlugin.getClass().getSimpleName());
}
}
return doc;
@@ -82,7 +89,7 @@ public abstract class IndexFactoryImpl<T extends IndexableObject, S> implements
writeDocument(solrInputDocument, null);
} catch (Exception e) {
log.error("Error occurred while writing SOLR document for {} object {}",
indexableObject.getType(), indexableObject.getID(), e);
indexableObject.getType(), indexableObject.getID(), e);
}
}
@@ -101,8 +108,8 @@ public abstract class IndexFactoryImpl<T extends IndexableObject, S> implements
if (streams != null && !streams.isEmpty()) {
// limit full text indexing to first 100,000 characters unless configured otherwise
final int charLimit = DSpaceServicesFactory.getInstance().getConfigurationService()
.getIntProperty("discovery.solr.fulltext.charLimit",
100000);
.getIntProperty("discovery.solr.fulltext.charLimit",
100000);
// Use Tika's Text parser as the streams are always from the TEXT bundle (i.e. already extracted text)
TextAndCSVParser tikaParser = new TextAndCSVParser();
@@ -113,6 +120,18 @@ public abstract class IndexFactoryImpl<T extends IndexableObject, S> implements
// Use Apache Tika to parse the full text stream(s)
try (InputStream fullTextStreams = streams.getStream()) {
tikaParser.parse(fullTextStreams, tikaHandler, tikaMetadata, tikaContext);
// Write Tika metadata to "tika_meta_*" fields.
// This metadata is not very useful right now,
// but we'll keep it just in case it becomes more useful.
for (String name : tikaMetadata.names()) {
for (String value : tikaMetadata.getValues(name)) {
doc.addField("tika_meta_" + name, value);
}
}
// Save (parsed) full text to "fulltext" field
doc.addField("fulltext", tikaHandler.toString());
} catch (SAXException saxe) {
// Check if this SAXException is just a notice that this file was longer than the character limit.
// Unfortunately there is not a unique, public exception type to catch here. This error is thrown
@@ -121,30 +140,23 @@ public abstract class IndexFactoryImpl<T extends IndexableObject, S> implements
if (saxe.getMessage().contains("limit has been reached")) {
// log that we only indexed up to that configured limit
log.info("Full text is larger than the configured limit (discovery.solr.fulltext.charLimit)."
+ " Only the first {} characters were indexed.", charLimit);
+ " Only the first {} characters were indexed.", charLimit);
} else {
log.error("Tika parsing error. Could not index full text.", saxe);
throw new IOException("Tika parsing error. Could not index full text.", saxe);
}
} catch (TikaException ex) {
} catch (TikaException | IOException ex) {
log.error("Tika parsing error. Could not index full text.", ex);
throw new IOException("Tika parsing error. Could not index full text.", ex);
} finally {
// Add document to index
solr.add(doc);
}
// Write Tika metadata to "tika_meta_*" fields.
// This metadata is not very useful right now, but we'll keep it just in case it becomes more useful.
for (String name : tikaMetadata.names()) {
for (String value : tikaMetadata.getValues(name)) {
doc.addField("tika_meta_" + name, value);
}
}
// Save (parsed) full text to "fulltext" field
doc.addField("fulltext", tikaHandler.toString());
return;
}
// Add document to index
solr.add(doc);
}
}

View File

@@ -13,6 +13,8 @@ import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.logging.log4j.Logger;
import org.dspace.app.suggestion.SuggestionProvider;
import org.dspace.app.suggestion.SuggestionService;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Collection;
import org.dspace.content.Item;
@@ -44,6 +46,9 @@ public class ExternalDataServiceImpl implements ExternalDataService {
@Autowired
private WorkspaceItemService workspaceItemService;
@Autowired
private SuggestionService suggestionService;
@Override
public Optional<ExternalDataObject> getExternalDataObject(String source, String id) {
ExternalDataProvider provider = getExternalDataProvider(source);
@@ -105,6 +110,16 @@ public class ExternalDataServiceImpl implements ExternalDataService {
log.info(LogHelper.getHeader(context, "create_item_from_externalDataObject", "Created item" +
"with id: " + item.getID() + " from source: " + externalDataObject.getSource() + " with identifier: " +
externalDataObject.getId()));
try {
List<SuggestionProvider> providers = suggestionService.getSuggestionProviders();
if (providers != null) {
for (SuggestionProvider p : providers) {
p.flagRelatedSuggestionsAsProcessed(context, externalDataObject);
}
}
} catch (Exception e) {
log.error("Got problems with the solr suggestion storage service: " + e.getMessage(), e);
}
return workspaceItem;
}

View File

@@ -11,7 +11,9 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.dspace.content.MetadataFieldName;
import org.dspace.importer.external.metadatamapping.MetadatumDTO;
/**
@@ -94,6 +96,31 @@ public class ImportRecord {
return values;
}
/**
* Returns an {@code Optional<String>} representing the value
* of the metadata {@code field} found inside the {@code valueList}.
* @param field String of the MetadataField to search
* @return {@code Optional<String>} non empty if found.
*/
public Optional<String> getSingleValue(String field) {
MetadataFieldName metadataFieldName = new MetadataFieldName(field);
return getSingleValue(metadataFieldName.schema, metadataFieldName.element, metadataFieldName.qualifier);
}
/**
* Retrieves a single value for the given schema, element, and qualifier.
*
* @param schema the schema for the value
* @param element the element for the value
* @param qualifier the qualifier for the value
* @return an optional containing the single value, if present
*/
public Optional<String> getSingleValue(String schema, String element, String qualifier) {
return getValue(schema, element, qualifier).stream()
.map(MetadatumDTO::getValue)
.findFirst();
}
/**
* Add a value to the valueList
*

View File

@@ -0,0 +1,130 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.importer.external.metadatamapping.contributor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dspace.importer.external.metadatamapping.MetadatumDTO;
/**
* A ROR JsonPath Metadata processor that should be configured inside the {@code ror-integration.xml} file.
* This allows the extraction of a given contributor with a specific mappings from the ROR JSON response.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
*/
public class RorParentOrgUnitMetadataContributor extends SimpleJsonPathMetadataContributor {
/**
* Determines which field of the JSON detains the {@code type} of this
* specific node (that needs to be mapped).
*
*/
private String typeField;
/**
* Determines which is the type of the main parent node that needs to be mapped.
* It should match the value of the {@code typeField} of the JSON node.
*
*/
private String parentType;
/**
* Determines which is the field of the JSON that contains the value
* that needs to be mapped into a {@code MetadatumDTO}.
*/
private String labelField;
/**
* Creates a {@code MetadatumDTO} for each correctly mapped JSON node
* of the ROR response.
* Partial / Unmatched parent-type metadatum will be ignored from this mapping.
*
* @param fullJson ROR response
* @return a collection of read ROR metadata.
*/
@Override
public Collection<MetadatumDTO> contributeMetadata(String fullJson) {
Collection<MetadatumDTO> metadata = new ArrayList<>();
Collection<String> metadataValue = new ArrayList<>();
JsonNode jsonNode = convertStringJsonToJsonNode(fullJson);
JsonNode array = jsonNode.at(getQuery());
if (!array.isArray()) {
return metadata;
}
Iterator<JsonNode> nodes = array.iterator();
while (nodes.hasNext()) {
JsonNode node = nodes.next();
if (!node.has(labelField)) {
continue;
}
String type = node.has(typeField) ? node.get(typeField).asText() : null;
String label = node.get(labelField).asText();
if (parentType.equalsIgnoreCase(type)) {
metadataValue.add(label);
}
}
for (String value : metadataValue) {
MetadatumDTO metadatumDto = new MetadatumDTO();
metadatumDto.setValue(value);
metadatumDto.setElement(getField().getElement());
metadatumDto.setQualifier(getField().getQualifier());
metadatumDto.setSchema(getField().getSchema());
metadata.add(metadatumDto);
}
return metadata;
}
private JsonNode convertStringJsonToJsonNode(String json) {
ObjectMapper mapper = new ObjectMapper();
JsonNode body = null;
try {
body = mapper.readTree(json);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
return body;
}
public String getTypeField() {
return typeField;
}
public void setTypeField(String typeField) {
this.typeField = typeField;
}
public String getLabelField() {
return labelField;
}
public void setLabelField(String labelField) {
this.labelField = labelField;
}
public String getParentType() {
return parentType;
}
public void setParentType(String parentType) {
this.parentType = parentType;
}
}

View File

@@ -87,5 +87,4 @@ public class SimpleXpathDateFormatMetadataContributor extends SimpleXpathMetadat
dcValue.setSchema(field.getSchema());
return dcValue;
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.importer.external.openaire.metadatamapping;
import java.util.Map;
import javax.annotation.Resource;
import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping;
/**
* An implementation of {@link AbstractMetadataFieldMapping} responsible for
* defining the mapping of the OpenAIRE metadatum fields on the DSpace metadatum
* fields
*
* @author Mykhaylo Boychuk (4science.it)
*/
public class OpenAIREPublicationFieldMapping extends AbstractMetadataFieldMapping {
@Override
@Resource(name = "openairePublicationsMetadataFieldMap")
public void setMetadataFieldMap(Map metadataFieldMap) {
super.setMetadataFieldMap(metadataFieldMap);
}
}

View File

@@ -0,0 +1,353 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.importer.external.openaire.service;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import javax.el.MethodNotFoundException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import org.dspace.content.Item;
import org.dspace.importer.external.datamodel.ImportRecord;
import org.dspace.importer.external.datamodel.Query;
import org.dspace.importer.external.exception.MetadataSourceException;
import org.dspace.importer.external.metadatamapping.MetadatumDTO;
import org.dspace.importer.external.service.AbstractImportMetadataSourceService;
import org.dspace.importer.external.service.components.QuerySource;
import org.dspace.services.ConfigurationService;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.Namespace;
import org.jdom2.filter.Filters;
import org.jdom2.input.SAXBuilder;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implements a data source for querying OpenAIRE
*
* @author Pasquale Cavallo (pasquale.cavallo at 4science dot it)
*/
public class OpenAireImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService<Element>
implements QuerySource {
@Autowired(required = true)
protected ConfigurationService configurationService;
private String baseAddress;
private WebTarget webTarget;
private String queryParam;
@Override
public String getImportSource() {
return "openaire";
}
/**
* The string that identifies this import implementation. Preferable a URI
*
* @return the identifying uri
*/
@Override
public ImportRecord getRecord(String id) throws MetadataSourceException {
return retry(new SearchByIdCallable(id));
}
/**
* The string that identifies this import implementation. Preferable a URI
*
* @return the identifying uri
*/
@Override
public ImportRecord getRecord(Query query) throws MetadataSourceException {
return retry(new SearchByIdCallable(query));
}
/**
* Find the number of records matching a query;
*
* @param query a query string to base the search on.
* @return the sum of the matching records over this import source
* @throws MetadataSourceException if the underlying methods throw any exception.
*/
@Override
public int getRecordsCount(String query) throws MetadataSourceException {
return retry(new CountByQueryCallable(query));
}
/**
* Find the number of records matching a query;
*
* @param query a query object to base the search on.
* @return the sum of the matching records over this import source
* @throws MetadataSourceException if the underlying methods throw any exception.
*/
@Override
public int getRecordsCount(Query query) throws MetadataSourceException {
return retry(new CountByQueryCallable(query));
}
/**
* Find the number of records matching a string query. Supports pagination
*
* @param query a query string to base the search on.
* @param start offset to start at
* @param count number of records to retrieve.
* @return a set of records. Fully transformed.
* @throws MetadataSourceException if the underlying methods throw any exception.
*/
@Override
public Collection<ImportRecord> getRecords(String query, int start, int count) throws MetadataSourceException {
return retry(new SearchByQueryCallable(query, start, count));
}
/**
* Find records based on a object query.
*
* @param query a query object to base the search on.
* @return a set of records. Fully transformed.
* @throws MetadataSourceException if the underlying methods throw any exception.
*/
@Override
public Collection<ImportRecord> getRecords(Query query) throws MetadataSourceException {
return retry(new SearchByQueryCallable(query));
}
@Override
public Collection<ImportRecord> findMatchingRecords(Query query) throws MetadataSourceException {
throw new MethodNotFoundException("This method is not implemented for OpenAIRE");
}
@Override
public Collection<ImportRecord> findMatchingRecords(Item item) throws MetadataSourceException {
throw new MethodNotFoundException("This method is not implemented for OpenAIRE");
}
/**
* Set the baseAddress to this object
*
* @param baseAddress The String object that represents the baseAddress of this object
*/
public void setBaseAddress(String baseAddress) {
this.baseAddress = baseAddress;
}
/**
* Return the baseAddress set to this object
*
* @return The String object that represents the baseAddress of this object
*/
public String getBaseAddress() {
return baseAddress;
}
/**
* Set the name of the query param, this correspond to the index used (title, author)
*
* @param queryParam on which index make the query
*/
public void setQueryParam(String queryParam) {
this.queryParam = queryParam;
}
/**
* Get the name of the query param for the rest call
*
* @return the name of the query param, i.e. the index (title, author) to use
*/
public String getQueryParam() {
return queryParam;
}
/**
* Initialize the class
*
* @throws Exception on generic exception
*/
@Override
public void init() throws Exception {
Client client = ClientBuilder.newClient();
if (baseAddress == null) {
baseAddress = configurationService.getProperty("openaire.base.url");
}
if (queryParam == null) {
queryParam = "title";
}
webTarget = client.target(baseAddress);
}
public class SearchByIdCallable implements Callable<ImportRecord> {
String id = null;
public SearchByIdCallable(String id) {
this.id = id;
}
public SearchByIdCallable(Query query) {
this.id = query.getParameterAsClass("id", String.class);
}
@Override
public ImportRecord call() throws Exception {
List<ImportRecord> results = new ArrayList<ImportRecord>();
WebTarget localTarget = webTarget.queryParam("openairePublicationID", id);
Invocation.Builder invocationBuilder = localTarget.request();
Response response = invocationBuilder.get();
if (response.getStatus() == 200) {
String responseString = response.readEntity(String.class);
List<Element> omElements = splitToRecords(responseString);
if (omElements != null) {
for (Element record : omElements) {
results.add(filterMultipleTitles(transformSourceRecords(record)));
}
}
return results != null ? results.get(0) : null;
} else {
return null;
}
}
}
public class CountByQueryCallable implements Callable<Integer> {
String q;
public CountByQueryCallable(String query) {
q = query;
}
public CountByQueryCallable(Query query) {
q = query.getParameterAsClass("query", String.class);
}
@Override
public Integer call() throws Exception {
WebTarget localTarget = webTarget.queryParam(queryParam, q);
Invocation.Builder invocationBuilder = localTarget.request();
Response response = invocationBuilder.get();
if (response.getStatus() == 200) {
String responseString = response.readEntity(String.class);
SAXBuilder saxBuilder = new SAXBuilder();
Document document = saxBuilder.build(new StringReader(responseString));
Element root = document.getRootElement();
XPathExpression<Element> xpath = XPathFactory.instance().compile("/header/total",
Filters.element(), null);
Element totalItem = (Element) xpath.evaluateFirst(root);
return totalItem != null ? Integer.parseInt(totalItem.getText()) : null;
} else {
return 0;
}
}
}
public class SearchByQueryCallable implements Callable<List<ImportRecord>> {
String q;
int page;
int count;
public SearchByQueryCallable(String query, int start, int count) {
this.q = query;
this.page = start / count;
this.count = count;
}
public SearchByQueryCallable(Query query) {
this.q = query.getParameterAsClass("query", String.class);
this.page = query.getParameterAsClass("start", Integer.class) /
query.getParameterAsClass("count", Integer.class);
this.count = query.getParameterAsClass("count", Integer.class);
}
@Override
public List<ImportRecord> call() throws Exception {
WebTarget localTarget = webTarget.queryParam(queryParam, q);
localTarget = localTarget.queryParam("page", page + 1);
localTarget = localTarget.queryParam("size", count);
List<ImportRecord> results = new ArrayList<ImportRecord>();
Invocation.Builder invocationBuilder = localTarget.request();
Response response = invocationBuilder.get();
if (response.getStatus() == 200) {
String responseString = response.readEntity(String.class);
List<Element> omElements = splitToRecords(responseString);
if (omElements != null) {
for (Element record : omElements) {
results.add(filterMultipleTitles(transformSourceRecords(record)));
}
}
}
return results;
}
}
/**
* This method remove multiple titles occurrences
*
* @param transformSourceRecords
* @return ImportRecord with one or zero title
*/
private ImportRecord filterMultipleTitles(ImportRecord transformSourceRecords) {
List<MetadatumDTO> metadata = (List<MetadatumDTO>)transformSourceRecords.getValueList();
ArrayList<MetadatumDTO> nextSourceRecord = new ArrayList<>();
boolean found = false;
for (MetadatumDTO dto : metadata) {
if ("dc".equals(dto.getSchema()) && "title".equals(dto.getElement()) && dto.getQualifier() == null) {
if (!found) {
nextSourceRecord.add(dto);
found = true;
}
} else {
nextSourceRecord.add(dto);
}
}
return new ImportRecord(nextSourceRecord);
}
private List<Element> splitToRecords(String recordsSrc) {
try {
SAXBuilder saxBuilder = new SAXBuilder();
Document document = saxBuilder.build(new StringReader(recordsSrc));
Element root = document.getRootElement();
List namespaces = Arrays.asList(
Namespace.getNamespace("dri", "http://www.driver-repository.eu/namespace/dri"),
Namespace.getNamespace("oaf", "http://namespace.openaire.eu/oaf"),
Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance"));
XPathExpression<Element> xpath = XPathFactory.instance().compile("//results/result",
Filters.element(), null, namespaces);
List<Element> recordsList = xpath.evaluate(root);
return recordsList;
} catch (JDOMException | IOException e) {
return null;
}
}
}

View File

@@ -0,0 +1,38 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.importer.external.ror.service;
import java.util.Map;
import javax.annotation.Resource;
import org.dspace.importer.external.metadatamapping.AbstractMetadataFieldMapping;
/**
* An implementation of {@link AbstractMetadataFieldMapping}
* Responsible for defining the mapping of the ROR metadatum fields on the DSpace metadatum fields
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
*/
public class RorFieldMapping extends AbstractMetadataFieldMapping {
/**
* Defines which metadatum is mapped on which metadatum. Note that while the key must be unique it
* only matters here for postprocessing of the value. The mapped MetadatumContributor has full control over
* what metadatafield is generated.
*
* @param metadataFieldMap The map containing the link between retrieve metadata and metadata that will be set to
* the item.
*/
@Override
@Resource(name = "rorMetadataFieldMap")
public void setMetadataFieldMap(Map metadataFieldMap) {
super.setMetadataFieldMap(metadataFieldMap);
}
}

View File

@@ -0,0 +1,278 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.importer.external.ror.service;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.el.MethodNotFoundException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Item;
import org.dspace.importer.external.datamodel.ImportRecord;
import org.dspace.importer.external.datamodel.Query;
import org.dspace.importer.external.exception.MetadataSourceException;
import org.dspace.importer.external.liveimportclient.service.LiveImportClient;
import org.dspace.importer.external.service.AbstractImportMetadataSourceService;
import org.dspace.importer.external.service.components.QuerySource;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implements a {@code AbstractImportMetadataSourceService} for querying ROR services.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
*/
public class RorImportMetadataSourceServiceImpl extends AbstractImportMetadataSourceService<String>
implements QuerySource {
private final static Logger log = LogManager.getLogger();
protected static final String ROR_IDENTIFIER_PREFIX = "https://ror.org/";
private String url;
private int timeout = 1000;
@Autowired
private LiveImportClient liveImportClient;
@Override
public String getImportSource() {
return "ror";
}
@Override
public ImportRecord getRecord(String id) throws MetadataSourceException {
List<ImportRecord> records = retry(new SearchByIdCallable(id));
return CollectionUtils.isEmpty(records) ? null : records.get(0);
}
@Override
public int getRecordsCount(String query) throws MetadataSourceException {
return retry(new CountByQueryCallable(query));
}
@Override
public int getRecordsCount(Query query) throws MetadataSourceException {
return retry(new CountByQueryCallable(query));
}
@Override
public Collection<ImportRecord> getRecords(String query, int start, int count) throws MetadataSourceException {
return retry(new SearchByQueryCallable(query));
}
@Override
public Collection<ImportRecord> getRecords(Query query) throws MetadataSourceException {
return retry(new SearchByQueryCallable(query));
}
@Override
public ImportRecord getRecord(Query query) throws MetadataSourceException {
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 ROR");
}
@Override
public Collection<ImportRecord> findMatchingRecords(Item item) throws MetadataSourceException {
throw new MethodNotFoundException("This method is not implemented for ROR");
}
@Override
public void init() throws Exception {
}
/**
* This class is a Callable implementation to get ROR entries based on query
* object. This Callable use as query value the string queryString passed to
* constructor. If the object will be construct through Query.class instance, a
* Query's map entry with key "query" will be used. Pagination is supported too,
* 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<List<ImportRecord>> {
private Query query;
private SearchByQueryCallable(String queryString) {
query = new Query();
query.addParameter("query", queryString);
}
private SearchByQueryCallable(Query query) {
this.query = query;
}
@Override
public List<ImportRecord> call() throws Exception {
return search(query.getParameterAsClass("query", String.class));
}
}
/**
* This class is a Callable implementation to get an ROR entry using bibcode The
* bibcode to use can be passed through the constructor as a String or as
* Query's map entry, with the key "id".
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com)
*/
private class SearchByIdCallable implements Callable<List<ImportRecord>> {
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<ImportRecord> call() throws Exception {
return searchById(query.getParameterAsClass("id", String.class));
}
}
/**
* This class is a Callable implementation to count the number of entries for a
* ROR query. This Callable uses as query value to ROR the string queryString
* passed to constructor. If the object will be construct through {@code Query}
* instance, the value of the Query's map with the key "query" will be used.
*
* @author Vincenzo Mecca (vins01-4science - vincenzo.mecca at 4science.com)
*/
private class CountByQueryCallable implements Callable<Integer> {
private Query query;
private CountByQueryCallable(String queryString) {
query = new Query();
query.addParameter("query", queryString);
}
private CountByQueryCallable(Query query) {
this.query = query;
}
@Override
public Integer call() throws Exception {
return count(query.getParameterAsClass("query", String.class));
}
}
/**
* Counts the number of results for the given query.
*
* @param query the query string to count results for
* @return the number of results for the given query
*/
public Integer count(String query) {
try {
Map<String, Map<String, String>> params = new HashMap<String, Map<String, String>>();
URIBuilder uriBuilder = new URIBuilder(this.url);
uriBuilder.addParameter("query", query);
String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params);
if (StringUtils.isEmpty(resp)) {
return 0;
}
JsonNode jsonNode = convertStringJsonToJsonNode(resp);
return jsonNode.at("/number_of_results").asInt();
} catch (URISyntaxException e) {
e.printStackTrace();
}
return 0;
}
private List<ImportRecord> searchById(String id) {
List<ImportRecord> importResults = new ArrayList<>();
id = StringUtils.removeStart(id, ROR_IDENTIFIER_PREFIX);
try {
Map<String, Map<String, String>> params = new HashMap<String, Map<String, String>>();
URIBuilder uriBuilder = new URIBuilder(this.url + "/" + id);
String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params);
if (StringUtils.isEmpty(resp)) {
return importResults;
}
JsonNode jsonNode = convertStringJsonToJsonNode(resp);
importResults.add(transformSourceRecords(jsonNode.toString()));
} catch (URISyntaxException e) {
e.printStackTrace();
}
return importResults;
}
private List<ImportRecord> search(String query) {
List<ImportRecord> importResults = new ArrayList<>();
try {
Map<String, Map<String, String>> params = new HashMap<String, Map<String, String>>();
URIBuilder uriBuilder = new URIBuilder(this.url);
uriBuilder.addParameter("query", query);
String resp = liveImportClient.executeHttpGetRequest(timeout, uriBuilder.toString(), params);
if (StringUtils.isEmpty(resp)) {
return importResults;
}
JsonNode jsonNode = convertStringJsonToJsonNode(resp);
JsonNode docs = jsonNode.at("/items");
if (docs.isArray()) {
Iterator<JsonNode> nodes = docs.elements();
while (nodes.hasNext()) {
JsonNode node = nodes.next();
importResults.add(transformSourceRecords(node.toString()));
}
} else {
importResults.add(transformSourceRecords(docs.toString()));
}
} catch (URISyntaxException e) {
e.printStackTrace();
}
return importResults;
}
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;
}
}

View File

@@ -34,7 +34,7 @@ public interface SubmissionConfigService {
public int countSubmissionConfigs();
public SubmissionConfig getSubmissionConfigByCollection(String collectionHandle);
public SubmissionConfig getSubmissionConfigByCollection(Collection collection);
public SubmissionConfig getSubmissionConfigByName(String submitName);

View File

@@ -57,8 +57,8 @@ public class SubmissionConfigServiceImpl implements SubmissionConfigService, Ini
}
@Override
public SubmissionConfig getSubmissionConfigByCollection(String collectionHandle) {
return submissionConfigReader.getSubmissionConfigByCollection(collectionHandle);
public SubmissionConfig getSubmissionConfigByCollection(Collection collection) {
return submissionConfigReader.getSubmissionConfigByCollection(collection);
}
@Override

View File

@@ -123,6 +123,20 @@
</property>
</bean>
<bean id="openaireImportServiceByAuthor"
class="org.dspace.importer.external.openaire.service.OpenAireImportMetadataSourceServiceImpl" scope="singleton">
<property name="metadataFieldMapping" ref="openairePublicationMetadataFieldMapping"/>
<property name="queryParam" value="author"/>
</bean>
<bean id="openaireImportServiceByTitle"
class="org.dspace.importer.external.openaire.service.OpenAireImportMetadataSourceServiceImpl" scope="singleton">
<property name="metadataFieldMapping" ref="openairePublicationMetadataFieldMapping"/>
<property name="queryParam" value="title"/>
</bean>
<bean id="openairePublicationMetadataFieldMapping"
class="org.dspace.importer.external.openaire.metadatamapping.OpenAIREPublicationFieldMapping">
</bean>
<bean id="CrossRefImportService" class="org.dspace.importer.external.crossref.CrossRefImportMetadataSourceServiceImpl" scope="singleton">
<property name="metadataFieldMapping" ref="CrossRefMetadataFieldMapping"/>
<property name="url" value="${crossref.url}"/>
@@ -150,6 +164,12 @@
<property name="viewMode" value="${scopus.search-api.viewMode}"/>
</bean>
<bean id="scopusMetadataFieldMapping" class="org.dspace.importer.external.scopus.service.ScopusFieldMapping"/>
<bean id="rorImportService" class="org.dspace.importer.external.ror.service.RorImportMetadataSourceServiceImpl">
<property name="metadataFieldMapping" ref="rorMetadataFieldMapping"/>
<property name="url" value="${ror.orgunit-import.api-url}"/>
</bean>
<bean id="rorMetadataFieldMapping" class="org.dspace.importer.external.ror.service.RorFieldMapping"/>
<bean id="vufindImportService" class="org.dspace.importer.external.vufind.VuFindImportMetadataSourceServiceImpl" scope="singleton">
<!-- Set to empty to use the default set of fields -->

View File

@@ -0,0 +1,23 @@
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
default-autowire-candidates="*Service,*DAO,javax.sql.DataSource">
<context:annotation-config/> <!-- allows us to use spring annotations in beans -->
<bean class="org.dspace.app.suggestion.SuggestionServiceImpl" id="org.dspace.app.suggestion.SuggestionService" />
</beans>

View File

@@ -24,6 +24,9 @@
<name-map collection-handle="123456789/typebind-test" submission-name="typebindtest"/>
<name-map collection-handle="123456789/accessCondition-not-discoverable" submission-name="accessConditionNotDiscoverable"/>
<name-map collection-handle="123456789/test-hidden" submission-name="test-hidden"/>
<name-map community-handle="123456789/topcommunity-test" submission-name="topcommunitytest"/>
<name-map community-handle="123456789/subcommunity-test" submission-name="subcommunitytest"/>
<name-map collection-handle="123456789/collection-test" submission-name="collectiontest"/>
</submission-map>
@@ -257,6 +260,18 @@
<step id="test-always-hidden"/>
</submission-process>
<submission-process name="topcommunitytest">
<step id="collection"/>
</submission-process>
<submission-process name="subcommunitytest">
<step id="collection"/>
</submission-process>
<submission-process name="collectiontest">
<step id="collection"/>
</submission-process>
</submission-definitions>
</item-submission>

View File

@@ -90,6 +90,8 @@
</list>
</property>
</bean>
<bean class="org.dspace.app.suggestion.MockSuggestionExternalDataSource" />
<bean id="dataciteLiveImportDataProvider" class="org.dspace.external.provider.impl.LiveImportDataProvider">
<property name="metadataSource" ref="DataCiteImportService"/>

View File

@@ -47,12 +47,14 @@
<bean id="org.dspace.statistics.SolrStatisticsCore"
class="org.dspace.statistics.MockSolrStatisticsCore"
autowire-candidate="true"/>
<!-- qa events -->
<bean class="org.dspace.qaevent.MockQAEventService"
id="org.dspace.qaevent.service.QAEventService" />
<bean class="org.dspace.statistics.GeoIpService" autowire-candidate="true"/>
<!-- suggestion service for solr providers -->
<bean id="org.dspace.app.suggestion.SolrSuggestionStorageService" class="org.dspace.app.suggestion.MockSolrSuggestionStorageService" />
</beans>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<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-3.0.xsd">
<context:annotation-config /> <!-- allows us to use spring annotations in beans -->
<!-- This is defined in the solr-services.xml
<bean id="org.dspace.app.nbevent.service.NBEventService"
class="org.dspace.app.nbevent.service.impl.NBEventServiceImpl" /> -->
<util:map id="suggestionProviders" map-class="java.util.HashMap"
key-type="java.lang.String" value-type="org.dspace.app.suggestion.SuggestionProvider">
<entry key="scopus">
<bean class="org.dspace.app.suggestion.MockSolrSuggestionProvider">
<property name="sourceName" value="scopus"></property>
</bean>
</entry>
<entry key="reciter">
<bean class="org.dspace.app.suggestion.MockSolrSuggestionProvider">
<property name="sourceName" value="reciter"></property>
</bean>
</entry>
</util:map>
</beans>

View File

@@ -0,0 +1,20 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import org.apache.commons.lang3.StringUtils;
import org.dspace.core.Context;
import org.dspace.external.model.ExternalDataObject;
public class MockSolrSuggestionProvider extends SolrSuggestionProvider {
@Override
protected boolean isExternalDataObjectPotentiallySuggested(Context context, ExternalDataObject externalDataObject) {
return StringUtils.equals(MockSuggestionExternalDataSource.NAME, externalDataObject.getSource());
}
}

View File

@@ -0,0 +1,38 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import org.dspace.solr.MockSolrServer;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
/**
* Mock SOLR service for the suggestion Core.
*/
@Service
public class MockSolrSuggestionStorageService extends SolrSuggestionStorageServiceImpl
implements InitializingBean, DisposableBean {
private MockSolrServer mockSolrServer;
@Override
public void afterPropertiesSet() throws Exception {
mockSolrServer = new MockSolrServer("suggestion");
solrSuggestionClient = mockSolrServer.getSolrServer();
}
/** Clear all records from the search core. */
public void reset() {
mockSolrServer.reset();
}
@Override
public void destroy() throws Exception {
mockSolrServer.destroy();
}
}

View File

@@ -0,0 +1,67 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import java.util.List;
import java.util.Optional;
import org.apache.commons.codec.binary.StringUtils;
import org.dspace.core.Context;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.external.provider.AbstractExternalDataProvider;
import org.dspace.services.RequestService;
import org.dspace.services.model.Request;
import org.dspace.utils.DSpace;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MockSuggestionExternalDataSource extends AbstractExternalDataProvider {
public static final String NAME = "suggestion";
@Autowired
private SuggestionService suggestionService;
@Override
public String getSourceIdentifier() {
return NAME;
}
@Override
public Optional<ExternalDataObject> getExternalDataObject(String id) {
RequestService requestService = new DSpace().getRequestService();
Request currentRequest = requestService.getCurrentRequest();
Context context = (Context) currentRequest.getAttribute("dspace.context");
Suggestion suggestion = suggestionService.findUnprocessedSuggestion(context, id);
if (suggestion != null) {
ExternalDataObject extDataObj = new ExternalDataObject(NAME);
extDataObj.setDisplayValue(suggestion.getDisplay());
extDataObj.setId(suggestion.getExternalSourceUri()
.substring(suggestion.getExternalSourceUri().lastIndexOf("/") + 1));
extDataObj.setMetadata(suggestion.getMetadata());
return Optional.of(extDataObj);
}
return null;
}
@Override
public List<ExternalDataObject> searchExternalDataObjects(String query, int start, int limit) {
return null;
}
@Override
public boolean supports(String source) {
return StringUtils.equals(NAME, source);
}
@Override
public int getNumberOfResults(String query) {
return 0;
}
}

View File

@@ -0,0 +1,218 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.suggestion;
import static java.util.Optional.of;
import static org.dspace.app.suggestion.SuggestionUtils.getFirstEntryByMetadatum;
import static org.dspace.orcid.model.OrcidProfileSectionType.EXTERNAL_IDS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.net.URL;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import org.apache.commons.collections.CollectionUtils;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.dto.MetadataValueDTO;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
import org.dspace.external.factory.ExternalServiceFactory;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.external.provider.ExternalDataProvider;
import org.dspace.external.provider.impl.OrcidPublicationDataProvider;
import org.dspace.external.service.ExternalDataService;
import org.dspace.kernel.ServiceManager;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.client.OrcidConfiguration;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.dspace.orcid.service.OrcidProfileSectionFactoryService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.utils.DSpace;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.orcid.jaxb.model.v3.release.record.Work;
import org.orcid.jaxb.model.v3.release.record.WorkBulk;
import org.orcid.jaxb.model.v3.release.record.summary.Works;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Tests for suggestion utilities @see SuggestionUtils
* @author Francesco Bacchelli (francesco.bacchelli at 4science.it)
*/
public class SuggestionUtilsIT extends AbstractIntegrationTestWithDatabase {
private static ConfigurationService cfg;
private static final String ORCID = "0000-1111-2222-3333";
private static final String ACCESS_TOKEN = "32c83ccb-c6d5-4981-b6ea-6a34a36de8ab";
private static final String BASE_XML_DIR_PATH = "org/dspace/app/orcid-works/";
private OrcidPublicationDataProvider dataProvider;
private SolrSuggestionProvider solrSuggestionProvider;
private OrcidProfileSectionFactoryService profileSectionFactoryService;
private ItemService itemService;
private Collection collection;
private ExternalDataProvider primaryProvider;
private Collection persons;
private OrcidConfiguration orcidConfiguration;
private OrcidClient orcidClientMock;
private OrcidClient orcidClient;
private String originalClientId;
@Autowired
private SuggestionService suggestionService;
@Before
public void setup() throws Exception {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent Community")
.build();
persons = CollectionBuilder.createCollection(context, parentCommunity)
.withEntityType("Person")
.withName("Profiles")
.build();
profileSectionFactoryService = OrcidServiceFactory.getInstance().getOrcidProfileSectionFactoryService();
itemService = ContentServiceFactory.getInstance().getItemService();
context.restoreAuthSystemState();
cfg = DSpaceServicesFactory.getInstance().getConfigurationService();
ServiceManager serviceManager = DSpaceServicesFactory.getInstance().getServiceManager();
HashMap<String,SuggestionProvider> providers = serviceManager.getServiceByName("suggestionProviders",
HashMap.class);
solrSuggestionProvider = (SolrSuggestionProvider) providers.get("scopus");
dataProvider = new DSpace().getServiceManager()
.getServiceByName("orcidPublicationDataProvider", OrcidPublicationDataProvider.class);
ExternalDataService externalDataService = ExternalServiceFactory.getInstance().getExternalDataService();
primaryProvider = externalDataService.getExternalDataProvider("openaireFunding");
orcidConfiguration = new DSpace().getServiceManager()
.getServiceByName("org.dspace.orcid.client.OrcidConfiguration", OrcidConfiguration.class);
orcidClientMock = mock(OrcidClient.class);
orcidClient = dataProvider.getOrcidClient();
dataProvider.setReadPublicAccessToken(null);
dataProvider.setOrcidClient(orcidClientMock);
originalClientId = orcidConfiguration.getClientId();
orcidConfiguration.setClientId("DSPACE-CLIENT-ID");
orcidConfiguration.setClientSecret("DSPACE-CLIENT-SECRET");
when(orcidClientMock.getReadPublicAccessToken()).thenReturn(buildTokenResponse(ACCESS_TOKEN));
when(orcidClientMock.getWorks(any(), eq(ORCID))).thenReturn(unmarshall("works.xml", Works.class));
when(orcidClientMock.getWorks(eq(ORCID))).thenReturn(unmarshall("works.xml", Works.class));
when(orcidClientMock.getObject(any(), eq(ORCID), any(), eq(Work.class)))
.then((invocation) -> of(unmarshall("work-" + invocation.getArgument(2) + ".xml", Work.class)));
when(orcidClientMock.getObject(eq(ORCID), any(), eq(Work.class)))
.then((invocation) -> of(unmarshall("work-" + invocation.getArgument(1) + ".xml", Work.class)));
when(orcidClientMock.getWorkBulk(any(), eq(ORCID), any()))
.then((invocation) -> unmarshallWorkBulk(invocation.getArgument(2)));
when(orcidClientMock.getWorkBulk(eq(ORCID), any()))
.then((invocation) -> unmarshallWorkBulk(invocation.getArgument(1)));
}
@After
public void after() {
dataProvider.setOrcidClient(orcidClient);
orcidConfiguration.setClientId(originalClientId);
}
@Test
public void testGetAllEntriesByMetadatum() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, persons)
.withTitle("Test profile")
.withScopusAuthorIdentifier("SCOPUS-123456")
.withResearcherIdentifier("R-ID-01")
.build();
context.restoreAuthSystemState();
List<MetadataValue> values = List.of(getMetadata(item, "person.identifier.scopus-author-id", 0));
Object firstOrcidObject = profileSectionFactoryService.createOrcidObject(context, values, EXTERNAL_IDS);
Optional<ExternalDataObject> optional = dataProvider.getExternalDataObject(ORCID + "::277902");
ExternalDataObject externalDataObject = optional.get();
String openAireId = externalDataObject.getId();
Suggestion suggestion = new Suggestion(solrSuggestionProvider.getSourceName(), item, openAireId);
suggestion.getMetadata().add(
new MetadataValueDTO("dc", "title", null, null, "dcTitle"));
suggestion.setDisplay(getFirstEntryByMetadatum(externalDataObject, "dc", "title", null));
suggestion.getMetadata().add(new MetadataValueDTO("dc", "date", "issued", null, new Date().toString()));
suggestion.getMetadata().add(new MetadataValueDTO("dc", "description", "abstract", null, "description"));
suggestion.setExternalSourceUri(cfg.getProperty("dspace.server.url")
+ "/api/integration/externalsources/" + primaryProvider.getSourceIdentifier() + "/entryValues/"
+ openAireId);
List<String> result = SuggestionUtils.getAllEntriesByMetadatum(externalDataObject, "dc", "title", null);
assertTrue(result != null && !result.isEmpty());
assertTrue(CollectionUtils.isEqualCollection(
SuggestionUtils.getAllEntriesByMetadatum(externalDataObject, "dc.title"),
result));
String firstResult = SuggestionUtils.getFirstEntryByMetadatum(externalDataObject, "dc", "title", null);
assertTrue("Another cautionary tale.".equalsIgnoreCase(firstResult));
firstResult = SuggestionUtils.getFirstEntryByMetadatum(externalDataObject, "dc.title");
assertTrue("Another cautionary tale.".equalsIgnoreCase(firstResult));
}
private MetadataValue getMetadata(Item item, String metadataField, int place) {
List<MetadataValue> values = itemService.getMetadataByMetadataString(item, metadataField);
assertThat(values.size(), greaterThan(place));
return values.get(place);
}
private OrcidTokenResponseDTO buildTokenResponse(String accessToken) {
OrcidTokenResponseDTO response = new OrcidTokenResponseDTO();
response.setAccessToken(accessToken);
return response;
}
private WorkBulk unmarshallWorkBulk(List<String> putCodes) throws Exception {
return unmarshall("workBulk-" + String.join("-", putCodes) + ".xml", WorkBulk.class);
}
@SuppressWarnings("unchecked")
private <T> T unmarshall(String fileName, Class<T> clazz) throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
URL resource = getClass().getClassLoader().getResource(BASE_XML_DIR_PATH + fileName);
if (resource == null) {
throw new IllegalStateException("No resource found named " + BASE_XML_DIR_PATH + fileName);
}
return (T) unmarshaller.unmarshal(new File(resource.getFile()));
}
}

View File

@@ -0,0 +1,73 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.util;
import static org.junit.Assert.assertEquals;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.submit.factory.SubmissionServiceFactory;
import org.dspace.submit.service.SubmissionConfigService;
import org.junit.Test;
/**
* Integration Tests for parsing and utilities on submission config forms / readers
*
* @author Toni Prieto
*/
public class SubmissionConfigIT extends AbstractIntegrationTestWithDatabase {
@Test
public void testSubmissionMapByCommunityHandleSubmissionConfig()
throws SubmissionConfigReaderException {
context.turnOffAuthorisationSystem();
// Sep up a structure with one top community and two subcommunities with collections
Community topcom = CommunityBuilder.createCommunity(context, "123456789/topcommunity-test")
.withName("Parent Community")
.build();
Community subcom1 = CommunityBuilder.createSubCommunity(context, topcom, "123456789/subcommunity-test")
.withName("Subcommunity 1")
.build();
Community subcom2 = CommunityBuilder.createSubCommunity(context, topcom, "123456789/not-mapped3")
.withName("Subcommunity 2")
.build();
// col1 should use the form item submission form mapped for subcom1
Collection col1 = CollectionBuilder.createCollection(context, subcom1, "123456789/not-mapped1")
.withName("Collection 1")
.build();
// col2 should use the item submission form mapped for the top community
Collection col2 = CollectionBuilder.createCollection(context, subcom2, "123456789/not-mapped2")
.withName("Collection 2")
.build();
// col3 should use the item submission form directly mapped for this collection
Collection col3 = CollectionBuilder.createCollection(context, subcom1, "123456789/collection-test")
.withName("Collection 3")
.build();
context.restoreAuthSystemState();
SubmissionConfigService submissionConfigService = SubmissionServiceFactory.getInstance()
.getSubmissionConfigService();
// for col1, it should return the item submission form defined for their parent subcom1
SubmissionConfig submissionConfig1 = submissionConfigService.getSubmissionConfigByCollection(col1);
assertEquals("subcommunitytest", submissionConfig1.getSubmissionName());
// for col2, it should return the item submission form defined for topcom
SubmissionConfig submissionConfig2 = submissionConfigService.getSubmissionConfigByCollection(col2);
assertEquals("topcommunitytest", submissionConfig2.getSubmissionName());
// for col3, it should return the item submission form defined directly for the collection
SubmissionConfig submissionConfig3 = submissionConfigService.getSubmissionConfigByCollection(col3);
assertEquals("collectiontest", submissionConfig3.getSubmissionName());
}
}

View File

@@ -9,17 +9,20 @@ package org.dspace.app.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.List;
import org.dspace.AbstractUnitTest;
import org.dspace.content.Collection;
import org.dspace.submit.factory.SubmissionServiceFactory;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mock;
/**
* Tests for parsing and utilities on submission config forms / readers
@@ -30,6 +33,9 @@ public class SubmissionConfigTest extends AbstractUnitTest {
DCInputsReader inputReader;
@Mock
private Collection col1;
@BeforeClass
public static void setUpClass() {
}
@@ -56,6 +62,8 @@ public class SubmissionConfigTest extends AbstractUnitTest {
String typeBindSubmissionName = "typebindtest";
String typeBindSubmissionStepName = "typebindtest";
when(col1.getHandle()).thenReturn(typeBindHandle);
// Expected field lists from typebindtest form
List<String> allConfiguredFields = new ArrayList<>();
allConfiguredFields.add("dc.title");
@@ -67,7 +75,7 @@ public class SubmissionConfigTest extends AbstractUnitTest {
// Get submission configuration
SubmissionConfig submissionConfig =
SubmissionServiceFactory.getInstance().getSubmissionConfigService()
.getSubmissionConfigByCollection(typeBindHandle);
.getSubmissionConfigByCollection(col1);
// Submission name should match name defined in item-submission.xml
assertEquals(typeBindSubmissionName, submissionConfig.getSubmissionName());
// Step 0 - our process only has one step. It should not be null and have the ID typebindtest

View File

@@ -16,6 +16,7 @@ import org.apache.logging.log4j.Logger;
import org.dspace.alerts.service.SystemWideAlertService;
import org.dspace.app.requestitem.factory.RequestItemServiceFactory;
import org.dspace.app.requestitem.service.RequestItemService;
import org.dspace.app.suggestion.SolrSuggestionStorageService;
import org.dspace.app.util.SubmissionConfigReaderException;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.factory.AuthorizeServiceFactory;
@@ -116,6 +117,7 @@ public abstract class AbstractBuilder<T, S> {
static SubscribeService subscribeService;
static SupervisionOrderService supervisionOrderService;
static QAEventService qaEventService;
static SolrSuggestionStorageService solrSuggestionService;
protected Context context;
@@ -185,6 +187,7 @@ public abstract class AbstractBuilder<T, S> {
subscribeService = ContentServiceFactory.getInstance().getSubscribeService();
supervisionOrderService = SupervisionOrderServiceFactory.getInstance().getSupervisionOrderService();
qaEventService = new DSpace().getSingletonService(QAEventService.class);
solrSuggestionService = new DSpace().getSingletonService(SolrSuggestionStorageService.class);
}

View File

@@ -186,6 +186,10 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
return addMetadataValue(item, "iiif", "image", "height", String.valueOf(i));
}
public ItemBuilder withDSpaceObjectOwner(String name, String authority) {
return addMetadataValue(item, "dspace", "object", "owner", null, name, authority, 600);
}
public ItemBuilder withMetadata(final String schema, final String element, final String qualifier,
final String value) {
return addMetadataValue(item, schema, element, qualifier, value);

View File

@@ -68,8 +68,8 @@ public class ProcessBuilder extends AbstractBuilder<Process, ProcessService> {
public ProcessBuilder withStartAndEndTime(String startTime, String endTime) throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MM/yyyy");
process.setStartTime(simpleDateFormat.parse(startTime));
process.setFinishedTime(simpleDateFormat.parse(endTime));
process.setStartTime(startTime == null ? null : simpleDateFormat.parse(startTime));
process.setFinishedTime(endTime == null ? null : simpleDateFormat.parse(endTime));
return this;
}

View File

@@ -0,0 +1,161 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.builder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.solr.client.solrj.SolrServerException;
import org.dspace.app.suggestion.MockSuggestionExternalDataSource;
import org.dspace.app.suggestion.SolrSuggestionStorageService;
import org.dspace.app.suggestion.Suggestion;
import org.dspace.app.suggestion.SuggestionEvidence;
import org.dspace.app.suggestion.SuggestionTarget;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.dto.MetadataValueDTO;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
/**
* Builder to construct Item objects
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
public class SuggestionTargetBuilder extends AbstractBuilder<SuggestionTarget, SolrSuggestionStorageService> {
public final static String EVIDENCE_MOCK_NAME = "MockEvidence";
public final static String EVIDENCE_MOCK_NOTE = "Generated for testing purpose...";
private Item item;
private SuggestionTarget target;
private List<Suggestion> suggestions;
private String source;
private int total;
protected SuggestionTargetBuilder(Context context) {
super(context);
}
public static SuggestionTargetBuilder createTarget(final Context context, final Collection col, final String name) {
return createTarget(context, col, name, null);
}
public static SuggestionTargetBuilder createTarget(final Context context, final Collection col, final String name,
final EPerson eperson) {
SuggestionTargetBuilder builder = new SuggestionTargetBuilder(context);
return builder.create(context, col, name, eperson);
}
public static SuggestionTargetBuilder createTarget(final Context context, final Item item) {
SuggestionTargetBuilder builder = new SuggestionTargetBuilder(context);
return builder.create(context, item);
}
private SuggestionTargetBuilder create(final Context context, final Collection col, final String name) {
return create(context, col, name, null);
}
private SuggestionTargetBuilder create(final Context context, final Collection col, final String name,
final EPerson eperson) {
this.context = context;
try {
ItemBuilder itemBuilder = ItemBuilder.createItem(context, col).withTitle(name);
if (eperson != null) {
itemBuilder = itemBuilder.withDSpaceObjectOwner(eperson.getFullName(), eperson.getID().toString());
}
item = itemBuilder.build();
context.dispatchEvents();
indexingService.commit();
} catch (Exception e) {
return handleException(e);
}
return this;
}
private SuggestionTargetBuilder create(final Context context, final Item item) {
this.context = context;
this.item = item;
return this;
}
public SuggestionTargetBuilder withSuggestionCount(final String source, final int total) {
this.source = source;
this.total = total;
return this;
}
@Override
public SuggestionTarget build() {
target = new SuggestionTarget(item);
target.setTotal(total);
target.setSource(source);
suggestions = generateAllSuggestion();
try {
for (Suggestion s : suggestions) {
solrSuggestionService.addSuggestion(s, false, false);
}
solrSuggestionService.commit();
} catch (SolrServerException | IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
return target;
}
@Override
public void cleanup() throws Exception {
solrSuggestionService.deleteTarget(target);
}
@Override
protected SolrSuggestionStorageService getService() {
return solrSuggestionService;
}
@Override
public void delete(Context c, SuggestionTarget dso) throws Exception {
solrSuggestionService.deleteTarget(dso);
}
private List<Suggestion> generateAllSuggestion() {
List<Suggestion> allSuggestions = new ArrayList<Suggestion>();
for (int idx = 0; idx < target.getTotal(); idx++) {
String idPartStr = String.valueOf(idx + 1);
Suggestion sug = new Suggestion(source, item, idPartStr);
sug.setDisplay("Suggestion " + source + " " + idPartStr);
MetadataValueDTO mTitle = new MetadataValueDTO();
mTitle.setSchema("dc");
mTitle.setElement("title");
mTitle.setValue("Title Suggestion " + idPartStr);
MetadataValueDTO mSource1 = new MetadataValueDTO();
mSource1.setSchema("dc");
mSource1.setElement("source");
mSource1.setValue("Source 1");
MetadataValueDTO mSource2 = new MetadataValueDTO();
mSource2.setSchema("dc");
mSource2.setElement("source");
mSource2.setValue("Source 2");
sug.getMetadata().add(mTitle);
sug.getMetadata().add(mSource1);
sug.getMetadata().add(mSource2);
sug.setExternalSourceUri(
"http://localhost/api/integration/externalsources/" + MockSuggestionExternalDataSource.NAME
+ "/entryValues/" + idPartStr);
sug.getEvidences().add(new SuggestionEvidence(EVIDENCE_MOCK_NAME,
idx % 2 == 0 ? 100 - idx : (double) idx / 2, EVIDENCE_MOCK_NOTE));
allSuggestions.add(sug);
}
return allSuggestions;
}
}

View File

@@ -77,6 +77,7 @@
</common:source>
<work:title>
<common:title>Another cautionary tale.</common:title>
<common:title>Second title</common:title>
</work:title>
<work:type>journal-article</work:type>
<common:publication-date>