E reloadEntity(E entity) throws SQLException;
/**
- * Remove a DSpace object from the session cache when batch processing a
- * large number of objects.
+ * Remove all entities from the session cache.
*
- * Objects removed from cache are not saved in any way. Therefore, if you
- * have modified an object, you should be sure to {@link commit()} changes
+ *
Entities removed from cache are not saved in any way. Therefore, if you
+ * have modified any entities, you should be sure to {@link #commit()} changes
* before calling this method.
*
- * @param Type of {@link entity}
- * @param entity The DSpace object to decache.
- * @throws java.sql.SQLException passed through.
+ * @throws SQLException passed through.
+ */
+ public void uncacheEntities() throws SQLException;
+
+ /**
+ * Remove an entity from the session cache.
+ *
+ * Entities removed from cache are not saved in any way. Therefore, if you
+ * have modified the entity, you should be sure to {@link #commit()} changes
+ * before calling this method.
+ *
+ * @param Type of entity.
+ * @param entity The entity to decache.
+ * @throws SQLException passed through.
*/
public void uncacheEntity(E entity) throws SQLException;
diff --git a/dspace-api/src/main/java/org/dspace/core/Email.java b/dspace-api/src/main/java/org/dspace/core/Email.java
index f6df740a53ef..c157d16d4967 100644
--- a/dspace-api/src/main/java/org/dspace/core/Email.java
+++ b/dspace-api/src/main/java/org/dspace/core/Email.java
@@ -395,7 +395,7 @@ public void send() throws MessagingException, IOException {
for (String headerName : templateHeaders) {
String headerValue = (String) vctx.get(headerName);
if ("subject".equalsIgnoreCase(headerName)) {
- if (null != headerValue) {
+ if ((subject == null || subject.isEmpty()) && null != headerValue) {
subject = headerValue;
}
} else if ("charset".equalsIgnoreCase(headerName)) {
diff --git a/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java b/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java
index b371af80eede..bd00b844ba9a 100644
--- a/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java
+++ b/dspace-api/src/main/java/org/dspace/core/HibernateDBConnection.java
@@ -243,6 +243,11 @@ private void configureDatabaseMode() throws SQLException {
}
}
+ @Override
+ public void uncacheEntities() throws SQLException {
+ getSession().clear();
+ }
+
/**
* Evict an entity from the hibernate cache.
*
diff --git a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java
index 0fc48b908b82..76f6a196fc87 100644
--- a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java
+++ b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java
@@ -96,14 +96,14 @@ private static Locale makeLocale(String localeSpec) {
*/
public static Locale getEPersonLocale(EPerson ep) {
if (ep == null) {
- log.error("No EPerson specified, returning default locale");
+ log.info("No EPerson specified, returning default locale");
return I18nUtil.getDefaultLocale();
}
String lang = ep.getLanguage();
if (StringUtils.isBlank(lang)) {
- log.error("No language specified for EPerson " + ep.getID());
+ log.info("No language specified for EPerson " + ep.getID() + ", returning default locale");
return I18nUtil.getDefaultLocale();
}
@@ -257,7 +257,7 @@ public static String getMessage(String key, Locale locale) {
String message = messages.getString(key.trim());
return message;
} catch (MissingResourceException e) {
- log.error("'" + key + "' translation undefined in locale '"
+ log.warn("'" + key + "' translation undefined in locale '"
+ locale.toString() + "'");
return key;
}
diff --git a/dspace-api/src/main/java/org/dspace/core/UUIDIterator.java b/dspace-api/src/main/java/org/dspace/core/UUIDIterator.java
new file mode 100644
index 000000000000..28c8d7354169
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/core/UUIDIterator.java
@@ -0,0 +1,63 @@
+/**
+ * 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.core;
+
+import java.sql.SQLException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.UUID;
+
+import com.google.common.collect.AbstractIterator;
+import org.dspace.content.DSpaceObject;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Iterator implementation which allows to iterate over items and commit while
+ * iterating. Using an iterator over previous retrieved UUIDs the iterator doesn't
+ * get invalidated after a commit that would instead close the database ResultSet
+ *
+ * @author Andrea Bollini (andrea.bollini at 4science.com)
+ * @param class type
+ */
+public class UUIDIterator extends AbstractIterator {
+ private Class clazz;
+
+ private Iterator iterator;
+
+ @Autowired
+ private AbstractHibernateDSODAO dao;
+
+ private Context ctx;
+
+ public UUIDIterator(Context ctx, List uuids, Class clazz, AbstractHibernateDSODAO dao)
+ throws SQLException {
+ this.ctx = ctx;
+ this.clazz = clazz;
+ this.dao = dao;
+ this.iterator = uuids.iterator();
+ }
+
+ @Override
+ protected T computeNext() {
+ try {
+ if (iterator.hasNext()) {
+ T item = dao.findByID(ctx, clazz, iterator.next());
+ if (item != null) {
+ return item;
+ } else {
+ return computeNext();
+ }
+ } else {
+ return endOfData();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java
index fbc6eebdb5b8..a302159ea9a4 100644
--- a/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java
+++ b/dspace-api/src/main/java/org/dspace/ctask/general/BasicLinkChecker.java
@@ -19,6 +19,8 @@
import org.dspace.content.MetadataValue;
import org.dspace.curate.AbstractCurationTask;
import org.dspace.curate.Curator;
+import org.dspace.services.ConfigurationService;
+import org.dspace.services.factory.DSpaceServicesFactory;
/**
* A basic link checker that is designed to be extended. By default this link checker
@@ -42,6 +44,9 @@ public class BasicLinkChecker extends AbstractCurationTask {
// The log4j logger for this class
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(BasicLinkChecker.class);
+ protected static final ConfigurationService configurationService
+ = DSpaceServicesFactory.getInstance().getConfigurationService();
+
/**
* Perform the link checking.
@@ -110,7 +115,8 @@ protected List getURLs(Item item) {
*/
protected boolean checkURL(String url, StringBuilder results) {
// Link check the URL
- int httpStatus = getResponseStatus(url);
+ int redirects = 0;
+ int httpStatus = getResponseStatus(url, redirects);
if ((httpStatus >= 200) && (httpStatus < 300)) {
results.append(" - " + url + " = " + httpStatus + " - OK\n");
@@ -128,14 +134,24 @@ protected boolean checkURL(String url, StringBuilder results) {
* @param url The url to open
* @return The HTTP response code (e.g. 200 / 301 / 404 / 500)
*/
- protected int getResponseStatus(String url) {
+ protected int getResponseStatus(String url, int redirects) {
try {
URL theURL = new URL(url);
HttpURLConnection connection = (HttpURLConnection) theURL.openConnection();
- int code = connection.getResponseCode();
- connection.disconnect();
+ connection.setInstanceFollowRedirects(true);
+ int statusCode = connection.getResponseCode();
+ int maxRedirect = configurationService.getIntProperty("curate.checklinks.max-redirect", 0);
+ if ((statusCode == HttpURLConnection.HTTP_MOVED_TEMP || statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
+ statusCode == HttpURLConnection.HTTP_SEE_OTHER)) {
+ connection.disconnect();
+ String newUrl = connection.getHeaderField("Location");
+ if (newUrl != null && (maxRedirect >= redirects || maxRedirect == -1)) {
+ redirects++;
+ return getResponseStatus(newUrl, redirects);
+ }
- return code;
+ }
+ return statusCode;
} catch (IOException ioe) {
// Must be a bad URL
diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java b/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java
index d1fa70565dfd..6b84efe31963 100644
--- a/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java
+++ b/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java
@@ -15,11 +15,10 @@
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
-import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
-import org.dspace.authorize.AuthorizeException;
+import org.apache.commons.collections4.ListUtils;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.DSpaceObject;
@@ -99,7 +98,12 @@ public int perform(DSpaceObject dso) throws IOException {
}
try {
- Bundle bundle = itemService.getBundles(item, "ORIGINAL").get(0);
+ List bundles = itemService.getBundles(item, "ORIGINAL");
+ if (ListUtils.emptyIfNull(bundles).isEmpty()) {
+ setResult("No ORIGINAL bundle found for item: " + getItemHandle(item));
+ return Curator.CURATE_SKIP;
+ }
+ Bundle bundle = bundles.get(0);
results = new ArrayList();
for (Bitstream bitstream : bundle.getBitstreams()) {
InputStream inputstream = bitstreamService.retrieve(Curator.curationContext(), bitstream);
@@ -121,10 +125,11 @@ public int perform(DSpaceObject dso) throws IOException {
}
}
- } catch (AuthorizeException authE) {
- throw new IOException(authE.getMessage(), authE);
- } catch (SQLException sqlE) {
- throw new IOException(sqlE.getMessage(), sqlE);
+ } catch (Exception e) {
+ // Any exception which may occur during the performance of the task should be caught here
+ // And end the process gracefully
+ log.error("Error scanning item: " + getItemHandle(item), e);
+ status = Curator.CURATE_ERROR;
} finally {
closeSession();
}
diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java b/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java
index 9639461426ef..0734d60946bc 100644
--- a/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java
+++ b/dspace-api/src/main/java/org/dspace/ctask/general/CreateMissingIdentifiers.java
@@ -10,6 +10,7 @@
import java.io.IOException;
import java.sql.SQLException;
+import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -25,7 +26,6 @@
import org.dspace.identifier.VersionedHandleIdentifierProviderWithCanonicalHandles;
import org.dspace.identifier.factory.IdentifierServiceFactory;
import org.dspace.identifier.service.IdentifierService;
-import org.dspace.services.factory.DSpaceServicesFactory;
/**
* Ensure that an object has all of the identifiers that it should, minting them
@@ -45,20 +45,6 @@ public int perform(DSpaceObject dso)
return Curator.CURATE_SKIP;
}
- // XXX Temporary escape when an incompatible provider is configured.
- // XXX Remove this when the provider is fixed.
- boolean compatible = DSpaceServicesFactory
- .getInstance()
- .getServiceManager()
- .getServiceByName(
- VersionedHandleIdentifierProviderWithCanonicalHandles.class.getCanonicalName(),
- IdentifierProvider.class) == null;
- if (!compatible) {
- setResult("This task is not compatible with VersionedHandleIdentifierProviderWithCanonicalHandles");
- return Curator.CURATE_ERROR;
- }
- // XXX End of escape
-
String typeText = Constants.typeText[dso.getType()];
// Get a Context
@@ -75,6 +61,18 @@ public int perform(DSpaceObject dso)
.getInstance()
.getIdentifierService();
+ // XXX Temporary escape when an incompatible provider is configured.
+ // XXX Remove this when the provider is fixed.
+ List providerList = identifierService.getProviders();
+ boolean compatible =
+ providerList.stream().noneMatch(p -> p instanceof VersionedHandleIdentifierProviderWithCanonicalHandles);
+
+ if (!compatible) {
+ setResult("This task is not compatible with VersionedHandleIdentifierProviderWithCanonicalHandles");
+ return Curator.CURATE_ERROR;
+ }
+ // XXX End of escape
+
// Register any missing identifiers.
try {
identifierService.register(context, dso);
diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/RequiredMetadata.java b/dspace-api/src/main/java/org/dspace/ctask/general/RequiredMetadata.java
index 07bfed5fe572..2899e3f6bdd6 100644
--- a/dspace-api/src/main/java/org/dspace/ctask/general/RequiredMetadata.java
+++ b/dspace-api/src/main/java/org/dspace/ctask/general/RequiredMetadata.java
@@ -17,6 +17,7 @@
import org.dspace.app.util.DCInputSet;
import org.dspace.app.util.DCInputsReader;
import org.dspace.app.util.DCInputsReaderException;
+import org.dspace.content.Collection;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
@@ -69,7 +70,7 @@ public int perform(DSpaceObject dso) throws IOException {
handle = "in workflow";
}
sb.append("Item: ").append(handle);
- for (String req : getReqList(item.getOwningCollection().getHandle())) {
+ for (String req : getReqList(item.getOwningCollection())) {
List vals = itemService.getMetadataByMetadataString(item, req);
if (vals.size() == 0) {
sb.append(" missing required field: ").append(req);
@@ -91,14 +92,14 @@ public int perform(DSpaceObject dso) throws IOException {
}
}
- protected List getReqList(String handle) throws DCInputsReaderException {
- List reqList = reqMap.get(handle);
+ protected List getReqList(Collection collection) throws DCInputsReaderException {
+ List reqList = reqMap.get(collection.getHandle());
if (reqList == null) {
reqList = reqMap.get("default");
}
if (reqList == null) {
reqList = new ArrayList();
- List inputSet = reader.getInputsByCollectionHandle(handle);
+ List inputSet = reader.getInputsByCollection(collection);
for (DCInputSet inputs : inputSet) {
for (DCInput[] row : inputs.getFields()) {
for (DCInput input : row) {
diff --git a/dspace-api/src/main/java/org/dspace/curate/Curation.java b/dspace-api/src/main/java/org/dspace/curate/Curation.java
index 4d70286e79e0..625692a866b3 100644
--- a/dspace-api/src/main/java/org/dspace/curate/Curation.java
+++ b/dspace-api/src/main/java/org/dspace/curate/Curation.java
@@ -165,7 +165,7 @@ private long runQueue(TaskQueue queue, Curator curator) throws SQLException, Aut
* End of curation script; logs script time if -v verbose is set
*
* @param timeRun Time script was started
- * @throws SQLException If DSpace contextx can't complete
+ * @throws SQLException If DSpace context can't complete
*/
private void endScript(long timeRun) throws SQLException {
context.complete();
@@ -185,7 +185,7 @@ private Curator initCurator() throws FileNotFoundException {
Curator curator = new Curator(handler);
OutputStream reporterStream;
if (null == this.reporter) {
- reporterStream = new NullOutputStream();
+ reporterStream = NullOutputStream.NULL_OUTPUT_STREAM;
} else if ("-".equals(this.reporter)) {
reporterStream = System.out;
} else {
@@ -300,9 +300,17 @@ private void initGeneralLineOptionsAndCheckIfValid() {
// scope
if (this.commandLine.getOptionValue('s') != null) {
this.scope = this.commandLine.getOptionValue('s');
- if (this.scope != null && Curator.TxScope.valueOf(this.scope.toUpperCase()) == null) {
- this.handler.logError("Bad transaction scope '" + this.scope + "': only 'object', 'curation' or " +
- "'open' recognized");
+ boolean knownScope;
+ try {
+ Curator.TxScope.valueOf(this.scope.toUpperCase());
+ knownScope = true;
+ } catch (IllegalArgumentException | NullPointerException e) {
+ knownScope = false;
+ }
+ if (!knownScope) {
+ this.handler.logError("Bad transaction scope '"
+ + this.scope
+ + "': only 'object', 'curation' or 'open' recognized");
throw new IllegalArgumentException(
"Bad transaction scope '" + this.scope + "': only 'object', 'curation' or " +
"'open' recognized");
diff --git a/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java b/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java
index 27a162d543c2..ec32ff92f9a2 100644
--- a/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/curate/XmlWorkflowCuratorServiceImpl.java
@@ -140,14 +140,14 @@ public boolean curate(Curator curator, Context c, XmlWorkflowItem wfi)
item.setOwningCollection(wfi.getCollection());
for (Task task : step.tasks) {
curator.addTask(task.name);
+ }
- // Check whether the task is configured to be queued rather than automatically run
- if (StringUtils.isNotEmpty(step.queue)) {
- // queue attribute has been set in the FlowStep configuration: add task to configured queue
- curator.queue(c, item.getID().toString(), step.queue);
- } else {
- // Task is configured to be run automatically
- curator.curate(c, item);
+ if (StringUtils.isNotEmpty(step.queue)) { // Step's tasks are to be queued.
+ curator.queue(c, item.getID().toString(), step.queue);
+ } else { // Step's tasks are to be run now.
+ curator.curate(c, item);
+
+ for (Task task : step.tasks) {
int status = curator.getStatus(task.name);
String result = curator.getResult(task.name);
String action = "none";
@@ -184,14 +184,14 @@ public boolean curate(Curator curator, Context c, XmlWorkflowItem wfi)
}
}
curator.clear();
- }
- // Record any reporting done by the tasks.
- if (reporter.length() > 0) {
- LOG.info("Curation tasks over item {} for step {} report:%n{}",
- () -> wfi.getItem().getID(),
- () -> step.step,
- () -> reporter.toString());
+ // Record any reporting done by the tasks.
+ if (reporter.length() > 0) {
+ LOG.info("Curation tasks over item {} for step {} report:\n{}",
+ () -> wfi.getItem().getID(),
+ () -> step.step,
+ () -> reporter.toString());
+ }
}
}
return true;
diff --git a/dspace-api/src/main/java/org/dspace/curate/package-info.java b/dspace-api/src/main/java/org/dspace/curate/package-info.java
index 492642f60c57..1168bbd283d2 100644
--- a/dspace-api/src/main/java/org/dspace/curate/package-info.java
+++ b/dspace-api/src/main/java/org/dspace/curate/package-info.java
@@ -20,6 +20,8 @@
*
*
* Curation requests may be run immediately or queued for batch processing.
+ * See {@link TaskQueue} and its relatives, {@link Curation} and its relatives
+ * for more on queued curation.
*
*
Tasks may also be attached to a workflow step, so that a set of tasks is
* applied to each uninstalled Item which passes through that step. See
@@ -27,5 +29,15 @@
*
*
A task may return to the Curator a status code, a final status message,
* and an optional report character stream.
+ *
+ *
The {@link Reporter} classes absorb strings of text and preserve it in
+ * various ways. A Reporter is a simple {@link Appendable} and makes no
+ * assumptions about e.g. whether a string represents a complete line. If you
+ * want your report formatted, insert appropriate newlines and other whitespace
+ * as needed. Your tasks can emit marked-up text if you wish, but the stock
+ * Reporter implementations make no attempt to render it.
+ *
+ *
Tasks may be annotated to inform the Curator of special properties. See
+ * {@link Distributive}, {@link Mutative}, {@link Suspendable} etc.
*/
package org.dspace.curate;
diff --git a/dspace-api/src/main/java/org/dspace/discovery/DiscoverResult.java b/dspace-api/src/main/java/org/dspace/discovery/DiscoverResult.java
index 00236d2bfe32..a56804e3e7ea 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/DiscoverResult.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/DiscoverResult.java
@@ -32,6 +32,9 @@ public class DiscoverResult {
private List indexableObjects;
private Map> facetResults;
+ // Total count of facet entries calculated for a metadata browsing query
+ private long totalEntries;
+
/**
* A map that contains all the documents sougth after, the key is a string representation of the Indexable Object
*/
@@ -64,6 +67,14 @@ public void setTotalSearchResults(long totalSearchResults) {
this.totalSearchResults = totalSearchResults;
}
+ public long getTotalEntries() {
+ return totalEntries;
+ }
+
+ public void setTotalEntries(long totalEntries) {
+ this.totalEntries = totalEntries;
+ }
+
public int getStart() {
return start;
}
diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java
index 661c48d91cfc..867359a949c6 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/IndexClient.java
@@ -21,6 +21,7 @@
import org.dspace.content.Item;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
+import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.discovery.indexobject.IndexableCollection;
import org.dspace.discovery.indexobject.IndexableCommunity;
@@ -92,7 +93,7 @@ public void internalRun() throws Exception {
.getHandleService().resolveToObject(context, param);
if (dso != null) {
final IndexFactory indexableObjectService = IndexObjectFactoryFactory.getInstance().
- getIndexFactoryByType(String.valueOf(dso.getType()));
+ getIndexFactoryByType(Constants.typeText[dso.getType()]);
indexableObject = indexableObjectService.findIndexableObject(context, dso.getID().toString());
}
}
diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java
index 7aece5acf313..6142dd0dba4b 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceFileInfoPlugin.java
@@ -52,21 +52,23 @@ public void additionalIndex(Context context, IndexableObject indexableObject, So
List bitstreams = bundle.getBitstreams();
if (bitstreams != null) {
for (Bitstream bitstream : bitstreams) {
- document.addField(SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName());
- // Add _keyword and _filter fields which are necessary to support filtering and faceting
- // for the file names
- document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_keyword", bitstream.getName());
- document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_filter", bitstream.getName());
+ if (bitstream != null) {
+ document.addField(SOLR_FIELD_NAME_FOR_FILENAMES, bitstream.getName());
+ // Add _keyword and _filter fields which are necessary to
+ // support filtering and faceting for the file names
+ document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_keyword", bitstream.getName());
+ document.addField(SOLR_FIELD_NAME_FOR_FILENAMES + "_filter", bitstream.getName());
- String description = bitstream.getDescription();
- if ((description != null) && !description.isEmpty()) {
- document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description);
- // Add _keyword and _filter fields which are necessary to support filtering and
- // faceting for the descriptions
- document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_keyword",
- description);
- document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter",
- description);
+ String description = bitstream.getDescription();
+ if ((description != null) && !description.isEmpty()) {
+ document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS, description);
+ // Add _keyword and _filter fields which are necessary to support filtering and
+ // faceting for the descriptions
+ document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_keyword",
+ description);
+ document.addField(SOLR_FIELD_NAME_FOR_DESCRIPTIONS + "_filter",
+ description);
+ }
}
}
}
diff --git a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java
index da9fff17e3bb..58106be3a8ab 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/SolrServiceImpl.java
@@ -42,6 +42,8 @@
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.json.BucketBasedJsonFacet;
+import org.apache.solr.client.solrj.response.json.NestableJsonFacet;
import org.apache.solr.client.solrj.util.ClientUtils;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
@@ -1087,6 +1089,8 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query)
}
//Resolve our facet field values
resolveFacetFields(context, query, result, skipLoadingResponse, solrQueryResponse);
+ //Add total entries count for metadata browsing
+ resolveEntriesCount(result, solrQueryResponse);
}
// If any stale entries are found in the current page of results,
// we remove those stale entries and rerun the same query again.
@@ -1112,7 +1116,37 @@ protected DiscoverResult retrieveResult(Context context, DiscoverQuery query)
return result;
}
+ /**
+ * Stores the total count of entries for metadata index browsing. The count is calculated by the
+ * json.facet parameter with the following value:
+ *
+ *
+ * {
+ * "entries_count": {
+ * "type": "terms",
+ * "field": "facetNameField_filter",
+ * "limit": 0,
+ * "prefix": "prefix_value",
+ * "numBuckets": true
+ * }
+ * }
+ *
+ *
+ * This value is returned in the facets field of the Solr response.
+ *
+ * @param result DiscoverResult object where the total entries count will be stored
+ * @param solrQueryResponse QueryResponse object containing the solr response
+ */
+ private void resolveEntriesCount(DiscoverResult result, QueryResponse solrQueryResponse) {
+ NestableJsonFacet response = solrQueryResponse.getJsonFacetingResponse();
+ if (response != null) {
+ BucketBasedJsonFacet facet = response.getBucketBasedFacets("entries_count");
+ if (facet != null) {
+ result.setTotalEntries(facet.getNumBucketsCount());
+ }
+ }
+ }
private void resolveFacetFields(Context context, DiscoverQuery query, DiscoverResult result,
boolean skipLoadingResponse, QueryResponse solrQueryResponse) throws SQLException {
@@ -1443,8 +1477,6 @@ protected String transformFacetField(DiscoverFacetField facetFieldConfig, String
} else {
return field + "_acid";
}
- } else if (facetFieldConfig.getType().equals(DiscoveryConfigurationParameters.TYPE_STANDARD)) {
- return field;
} else {
return field;
}
diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java
index 55c99b168e7a..c9a865ec85b2 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/IndexFactoryImpl.java
@@ -64,7 +64,14 @@ public SolrInputDocument buildDocument(Context context, T indexableObject) throw
//Do any additional indexing, depends on the plugins
for (SolrServiceIndexPlugin solrServiceIndexPlugin : ListUtils.emptyIfNull(solrServiceIndexPlugins)) {
- solrServiceIndexPlugin.additionalIndex(context, indexableObject, doc);
+ try {
+ solrServiceIndexPlugin.additionalIndex(context, indexableObject, doc);
+ } catch (Exception e) {
+ log.error("An error occurred while indexing additional fields. " +
+ "Could not fully index item with UUID: {}. Plugin: {}",
+ indexableObject.getUniqueIndexID(), solrServiceIndexPlugin.getClass().getSimpleName());
+
+ }
}
return doc;
@@ -82,7 +89,7 @@ public void writeDocument(Context context, T indexableObject, SolrInputDocument
writeDocument(solrInputDocument, null);
} catch (Exception e) {
log.error("Error occurred while writing SOLR document for {} object {}",
- indexableObject.getType(), indexableObject.getID(), e);
+ indexableObject.getType(), indexableObject.getID(), e);
}
}
@@ -101,8 +108,8 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea
if (streams != null && !streams.isEmpty()) {
// limit full text indexing to first 100,000 characters unless configured otherwise
final int charLimit = DSpaceServicesFactory.getInstance().getConfigurationService()
- .getIntProperty("discovery.solr.fulltext.charLimit",
- 100000);
+ .getIntProperty("discovery.solr.fulltext.charLimit",
+ 100000);
// Use Tika's Text parser as the streams are always from the TEXT bundle (i.e. already extracted text)
TextAndCSVParser tikaParser = new TextAndCSVParser();
@@ -111,8 +118,10 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea
ParseContext tikaContext = new ParseContext();
// Use Apache Tika to parse the full text stream(s)
+ boolean extractionSucceeded = false;
try (InputStream fullTextStreams = streams.getStream()) {
tikaParser.parse(fullTextStreams, tikaHandler, tikaMetadata, tikaContext);
+ extractionSucceeded = true;
} 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 +130,32 @@ protected void writeDocument(SolrInputDocument doc, FullTextContentStreams strea
if (saxe.getMessage().contains("limit has been reached")) {
// log that we only indexed up to that configured limit
log.info("Full text is larger than the configured limit (discovery.solr.fulltext.charLimit)."
- + " Only the first {} characters were indexed.", charLimit);
+ + " Only the first {} characters were indexed.", charLimit);
+ extractionSucceeded = true;
} 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);
}
-
- // 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);
+ if (extractionSucceeded) {
+ // 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());
}
-
- // Save (parsed) full text to "fulltext" field
- doc.addField("fulltext", tikaHandler.toString());
}
-
// Add document to index
solr.add(doc);
+
}
}
diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java
index 7cdb8b93d80e..a7a755749604 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/ItemIndexFactoryImpl.java
@@ -67,8 +67,6 @@
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.util.MultiFormatDateParser;
import org.dspace.util.SolrUtils;
-import org.dspace.versioning.Version;
-import org.dspace.versioning.VersionHistory;
import org.dspace.versioning.service.VersionHistoryService;
import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
import org.dspace.xmlworkflow.storedcomponents.service.XmlWorkflowItemService;
@@ -151,12 +149,14 @@ public SolrInputDocument buildDocument(Context context, IndexableItem indexableI
doc.addField("withdrawn", item.isWithdrawn());
doc.addField("discoverable", item.isDiscoverable());
doc.addField("lastModified", SolrUtils.getDateFormatter().format(item.getLastModified()));
- doc.addField("latestVersion", isLatestVersion(context, item));
+ doc.addField("latestVersion", itemService.isLatestVersion(context, item));
EPerson submitter = item.getSubmitter();
- if (submitter != null) {
- addFacetIndex(doc, "submitter", submitter.getID().toString(),
- submitter.getFullName());
+ if (submitter != null && !(DSpaceServicesFactory.getInstance().getConfigurationService().getBooleanProperty(
+ "discovery.index.item.submitter.enabled", false))) {
+ doc.addField("submitter_authority", submitter.getID().toString());
+ } else if (submitter != null) {
+ addFacetIndex(doc, "submitter", submitter.getID().toString(), submitter.getFullName());
}
// Add the item metadata
@@ -175,43 +175,6 @@ public SolrInputDocument buildDocument(Context context, IndexableItem indexableI
return doc;
}
- /**
- * Check whether the given item is the latest version.
- * If the latest item cannot be determined, because either the version history or the latest version is not present,
- * assume the item is latest.
- * @param context the DSpace context.
- * @param item the item that should be checked.
- * @return true if the item is the latest version, false otherwise.
- */
- protected boolean isLatestVersion(Context context, Item item) throws SQLException {
- VersionHistory history = versionHistoryService.findByItem(context, item);
- if (history == null) {
- // not all items have a version history
- // if an item does not have a version history, it is by definition the latest version
- return true;
- }
-
- // start with the very latest version of the given item (may still be in workspace)
- Version latestVersion = versionHistoryService.getLatestVersion(context, history);
-
- // find the latest version of the given item that is archived
- while (latestVersion != null && !latestVersion.getItem().isArchived()) {
- latestVersion = versionHistoryService.getPrevious(context, history, latestVersion);
- }
-
- // could not find an archived version of the given item
- if (latestVersion == null) {
- // this scenario should never happen, but let's err on the side of showing too many items vs. to little
- // (see discovery.xml, a lot of discovery configs filter out all items that are not the latest version)
- return true;
- }
-
- // sanity check
- assert latestVersion.getItem().isArchived();
-
- return item.equals(latestVersion.getItem());
- }
-
@Override
public SolrInputDocument buildNewDocument(Context context, IndexableItem indexableItem)
throws SQLException, IOException {
@@ -704,7 +667,7 @@ public List getIndexableObjects(Context context, Item item) throws SQLException
return List.copyOf(workflowItemIndexFactory.getIndexableObjects(context, xmlWorkflowItem));
}
- if (!isLatestVersion(context, item)) {
+ if (!itemService.isLatestVersion(context, item)) {
// the given item is an older version of another item
return List.of(new IndexableItem(item));
}
diff --git a/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java b/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java
index 92a973dff883..b816e222539a 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/utils/DiscoverQueryBuilder.java
@@ -302,7 +302,7 @@ private void configureSorting(String sortProperty, String sortDirection, Discove
if (StringUtils.isNotBlank(sortBy) && !isConfigured(sortBy, searchSortConfiguration)) {
throw new SearchServiceException(
- "The field: " + sortBy + "is not configured for the configuration!");
+ "The field: " + sortBy + " is not configured for the configuration!");
}
diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java
index 66fe6562ea25..453d5d0726be 100644
--- a/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java
@@ -142,10 +142,15 @@ public EPerson getSystemEPerson(Context c)
@Override
public EPerson findByIdOrLegacyId(Context context, String id) throws SQLException {
- if (StringUtils.isNumeric(id)) {
- return findByLegacyId(context, Integer.parseInt(id));
- } else {
- return find(context, UUID.fromString(id));
+ try {
+ if (StringUtils.isNumeric(id)) {
+ return findByLegacyId(context, Integer.parseInt(id));
+ } else {
+ return find(context, UUID.fromString(id));
+ }
+ } catch (IllegalArgumentException e) {
+ // Not a valid legacy ID or valid UUID
+ return null;
}
}
diff --git a/dspace-api/src/main/java/org/dspace/eperson/Group2GroupCache.java b/dspace-api/src/main/java/org/dspace/eperson/Group2GroupCache.java
index 09bdf34d4cad..58781cd402fa 100644
--- a/dspace-api/src/main/java/org/dspace/eperson/Group2GroupCache.java
+++ b/dspace-api/src/main/java/org/dspace/eperson/Group2GroupCache.java
@@ -14,6 +14,7 @@
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
import org.hibernate.proxy.HibernateProxyHelper;
@@ -23,7 +24,7 @@
* @author kevinvandevelde at atmire.com
*/
@Entity
-@Table(name = "group2groupcache")
+@Table(name = "group2groupcache", uniqueConstraints = { @UniqueConstraint(columnNames = {"parent_id", "child_id"}) })
public class Group2GroupCache implements Serializable {
@Id
diff --git a/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java
index b8d8c75d0f2e..4cec4c9c0d93 100644
--- a/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/eperson/GroupServiceImpl.java
@@ -20,6 +20,7 @@
import java.util.UUID;
import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.SetUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
@@ -147,7 +148,7 @@ public void addMember(Context context, Group group, EPerson e) {
public void addMember(Context context, Group groupParent, Group groupChild) throws SQLException {
// don't add if it's already a member
// and don't add itself
- if (groupParent.contains(groupChild) || groupParent.getID() == groupChild.getID()) {
+ if (groupParent.contains(groupChild) || groupParent.getID().equals(groupChild.getID())) {
return;
}
@@ -178,7 +179,7 @@ public void removeMember(Context context, Group group, EPerson ePerson) throws S
Role role = stepByName.getRole();
for (CollectionRole collectionRole : collectionRoles) {
if (StringUtils.equals(collectionRole.getRoleId(), role.getId())
- && claimedTask.getWorkflowItem().getCollection() == collectionRole.getCollection()) {
+ && claimedTask.getWorkflowItem().getCollection().equals(collectionRole.getCollection())) {
// Count number of EPersons who are *direct* members of this group
int totalDirectEPersons = ePersonService.countByGroups(context, Set.of(group));
// Count number of Groups which have this groupParent as a direct parent
@@ -673,15 +674,14 @@ protected boolean isEPersonInGroup(Context context, Group group, EPerson ePerson
/**
- * Regenerate the group cache AKA the group2groupcache table in the database -
- * meant to be called when a group is added or removed from another group
+ * Returns a set with pairs of parent and child group UUIDs, representing the new cache table rows.
*
- * @param context The relevant DSpace Context.
- * @param flushQueries flushQueries Flush all pending queries
+ * @param context The relevant DSpace Context.
+ * @param flushQueries flushQueries Flush all pending queries
+ * @return Pairs of parent and child group UUID of the new cache.
* @throws SQLException An exception that provides information on a database access error or other errors.
*/
- protected void rethinkGroupCache(Context context, boolean flushQueries) throws SQLException {
-
+ private Set> computeNewCache(Context context, boolean flushQueries) throws SQLException {
Map> parents = new HashMap<>();
List> group2groupResults = groupDAO.getGroup2GroupResults(context, flushQueries);
@@ -689,19 +689,8 @@ protected void rethinkGroupCache(Context context, boolean flushQueries) throws S
UUID parent = group2groupResult.getLeft();
UUID child = group2groupResult.getRight();
- // if parent doesn't have an entry, create one
- if (!parents.containsKey(parent)) {
- Set children = new HashSet<>();
-
- // add child id to the list
- children.add(child);
- parents.put(parent, children);
- } else {
- // parent has an entry, now add the child to the parent's record
- // of children
- Set children = parents.get(parent);
- children.add(child);
- }
+ parents.putIfAbsent(parent, new HashSet<>());
+ parents.get(parent).add(child);
}
// now parents is a hash of all of the IDs of groups that are parents
@@ -714,27 +703,42 @@ protected void rethinkGroupCache(Context context, boolean flushQueries) throws S
parent.getValue().addAll(myChildren);
}
- // empty out group2groupcache table
- group2GroupCacheDAO.deleteAll(context);
-
- // write out new one
+ // write out new cache IN MEMORY ONLY and returns it
+ Set> newCache = new HashSet<>();
for (Map.Entry> parent : parents.entrySet()) {
UUID key = parent.getKey();
-
for (UUID child : parent.getValue()) {
+ newCache.add(Pair.of(key, child));
+ }
+ }
+ return newCache;
+ }
+
+
+ /**
+ * Regenerate the group cache AKA the group2groupcache table in the database -
+ * meant to be called when a group is added or removed from another group
+ *
+ * @param context The relevant DSpace Context.
+ * @param flushQueries flushQueries Flush all pending queries
+ * @throws SQLException An exception that provides information on a database access error or other errors.
+ */
+ protected void rethinkGroupCache(Context context, boolean flushQueries) throws SQLException {
+ // current cache in the database
+ Set> oldCache = group2GroupCacheDAO.getCache(context);
- Group parentGroup = find(context, key);
- Group childGroup = find(context, child);
+ // correct cache, computed from the Group table
+ Set> newCache = computeNewCache(context, flushQueries);
+ SetUtils.SetView> toDelete = SetUtils.difference(oldCache, newCache);
+ SetUtils.SetView> toCreate = SetUtils.difference(newCache, oldCache);
- if (parentGroup != null && childGroup != null && group2GroupCacheDAO
- .find(context, parentGroup, childGroup) == null) {
- Group2GroupCache group2GroupCache = group2GroupCacheDAO.create(context, new Group2GroupCache());
- group2GroupCache.setParent(parentGroup);
- group2GroupCache.setChild(childGroup);
- group2GroupCacheDAO.save(context, group2GroupCache);
- }
- }
+ for (Pair pair : toDelete ) {
+ group2GroupCacheDAO.deleteFromCache(context, pair.getLeft(), pair.getRight());
+ }
+
+ for (Pair pair : toCreate ) {
+ group2GroupCacheDAO.addToCache(context, pair.getLeft(), pair.getRight());
}
}
@@ -872,10 +876,15 @@ protected Set getChildren(Map> parents, UUID parent) {
@Override
public Group findByIdOrLegacyId(Context context, String id) throws SQLException {
- if (org.apache.commons.lang3.StringUtils.isNumeric(id)) {
- return findByLegacyId(context, Integer.parseInt(id));
- } else {
- return find(context, UUIDUtils.fromString(id));
+ try {
+ if (StringUtils.isNumeric(id)) {
+ return findByLegacyId(context, Integer.parseInt(id));
+ } else {
+ return find(context, UUID.fromString(id));
+ }
+ } catch (IllegalArgumentException e) {
+ // Not a valid legacy ID or valid UUID
+ return null;
}
}
diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/Group2GroupCacheDAO.java b/dspace-api/src/main/java/org/dspace/eperson/dao/Group2GroupCacheDAO.java
index 7db569a59e2b..d41d52c7e618 100644
--- a/dspace-api/src/main/java/org/dspace/eperson/dao/Group2GroupCacheDAO.java
+++ b/dspace-api/src/main/java/org/dspace/eperson/dao/Group2GroupCacheDAO.java
@@ -9,7 +9,10 @@
import java.sql.SQLException;
import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import org.apache.commons.lang3.tuple.Pair;
import org.dspace.core.Context;
import org.dspace.core.GenericDAO;
import org.dspace.eperson.Group;
@@ -25,13 +28,74 @@
*/
public interface Group2GroupCacheDAO extends GenericDAO