diff --git a/.gitignore b/.gitignore
index e8e3aab6ef..fc8dab2da9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,7 +19,7 @@ tags
overlays/
## Ignore project files created by NetBeans
-nbproject/private/
+nbproject/
build/
nbbuild/
dist/
@@ -41,4 +41,4 @@ nb-configuration.xml
.DS_Store
##Ignore JRebel project configuration
-rebel.xml
\ No newline at end of file
+rebel.xml
diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInput.java b/dspace-api/src/main/java/org/dspace/app/util/DCInput.java
index c3cbac115a..32fd5d634d 100644
--- a/dspace-api/src/main/java/org/dspace/app/util/DCInput.java
+++ b/dspace-api/src/main/java/org/dspace/app/util/DCInput.java
@@ -145,6 +145,7 @@ public class DCInput {
private String relationshipType = null;
private String searchConfiguration = null;
private String filter;
+ private List externalSources;
/**
* The scope of the input sets, this restricts hidden metadata fields from
@@ -226,6 +227,15 @@ public class DCInput {
relationshipType = fieldMap.get("relationship-type");
searchConfiguration = fieldMap.get("search-configuration");
filter = fieldMap.get("filter");
+ externalSources = new ArrayList<>();
+ String externalSourcesDef = fieldMap.get("externalsources");
+ if (StringUtils.isNotBlank(externalSourcesDef)) {
+ String[] sources = StringUtils.split(externalSourcesDef, ",");
+ for (String source: sources) {
+ externalSources.add(StringUtils.trim(source));
+ }
+ }
+
}
/**
@@ -522,6 +532,10 @@ public class DCInput {
return filter;
}
+ public List getExternalSources() {
+ return externalSources;
+ }
+
public boolean isQualdropValue() {
if ("qualdrop_value".equals(getInputType())) {
return true;
diff --git a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java
index 1fb2c4f4da..ac802e1d5d 100644
--- a/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/content/RelationshipServiceImpl.java
@@ -10,6 +10,7 @@ package org.dspace.content;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Comparator;
+import java.util.LinkedList;
import java.util.List;
import org.apache.commons.collections4.CollectionUtils;
@@ -19,12 +20,14 @@ import org.apache.logging.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.dao.RelationshipDAO;
+import org.dspace.content.service.EntityTypeService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.RelationshipService;
import org.dspace.content.service.RelationshipTypeService;
import org.dspace.content.virtual.VirtualMetadataPopulator;
import org.dspace.core.Constants;
import org.dspace.core.Context;
+import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
public class RelationshipServiceImpl implements RelationshipService {
@@ -43,6 +46,12 @@ public class RelationshipServiceImpl implements RelationshipService {
@Autowired(required = true)
protected RelationshipTypeService relationshipTypeService;
+ @Autowired
+ private ConfigurationService configurationService;
+
+ @Autowired
+ private EntityTypeService entityTypeService;
+
@Autowired
private RelationshipMetadataService relationshipMetadataService;
@Autowired
@@ -90,6 +99,7 @@ public class RelationshipServiceImpl implements RelationshipService {
Relationship relationshipToReturn = relationshipDAO.create(context, relationship);
updatePlaceInRelationship(context, relationshipToReturn);
update(context, relationshipToReturn);
+ updateItemsInRelationship(context, relationship);
return relationshipToReturn;
} else {
throw new AuthorizeException(
@@ -234,6 +244,10 @@ public class RelationshipServiceImpl implements RelationshipService {
Integer maxCardinality,
RelationshipType relationshipType,
boolean isLeft) throws SQLException {
+ if (maxCardinality == null) {
+ //no need to check the relationships
+ return true;
+ }
List rightRelationships = findByItemAndRelationshipType(context, itemToProcess, relationshipType,
isLeft);
if (maxCardinality != null && rightRelationships.size() >= maxCardinality) {
@@ -243,7 +257,8 @@ public class RelationshipServiceImpl implements RelationshipService {
}
private boolean verifyEntityTypes(Item itemToProcess, EntityType entityTypeToProcess) {
- List list = itemService.getMetadata(itemToProcess, "relationship", "type", null, Item.ANY);
+ List list = itemService.getMetadata(itemToProcess, "relationship", "type",
+ null, Item.ANY, false);
if (list.isEmpty()) {
return false;
}
@@ -337,6 +352,7 @@ public class RelationshipServiceImpl implements RelationshipService {
authorizeService.authorizeActionBoolean(context, relationship.getRightItem(), Constants.WRITE)) {
relationshipDAO.delete(context, relationship);
updatePlaceInRelationship(context, relationship);
+ updateItemsInRelationship(context, relationship);
} else {
throw new AuthorizeException(
"You do not have write rights on this relationship's items");
@@ -347,6 +363,128 @@ public class RelationshipServiceImpl implements RelationshipService {
}
}
+
+ /**
+ * Utility method to ensure discovery is updated for the 2 items
+ * This method is used when creating, modifying or deleting a relationship
+ * The virtual metadata of the 2 items may need to be updated, so they should be re-indexed
+ *
+ * @param context The relevant DSpace context
+ * @param relationship The relationship which has been created, updated or deleted
+ * @throws SQLException If something goes wrong
+ */
+ private void updateItemsInRelationship(Context context, Relationship relationship) throws SQLException {
+ // Since this call is performed after creating, updating or deleting the relationships, the permissions have
+ // already been verified. The following updateItem calls can however call the
+ // ItemService.update() functions which would fail if the user doesn't have permission on both items.
+ // Since we allow this edits to happen under these circumstances, we need to turn off the
+ // authorization system here so that this failure doesn't happen when the items need to be update
+ context.turnOffAuthorisationSystem();
+ try {
+ // Set a limit on the total amount of items to update at once during a relationship change
+ int max = configurationService.getIntProperty("relationship.update.relateditems.max", 20);
+ // Set a limit on the total depth of relationships to traverse during a relationship change
+ int maxDepth = configurationService.getIntProperty("relationship.update.relateditems.maxdepth", 5);
+ // This is the list containing all items which will have changes to their virtual metadata
+ List itemsToUpdate = new LinkedList<>();
+ itemsToUpdate.add(relationship.getLeftItem());
+ itemsToUpdate.add(relationship.getRightItem());
+
+ if (containsVirtualMetadata(relationship.getRelationshipType().getLeftwardType())) {
+ findModifiedDiscoveryItemsForCurrentItem(context, relationship.getLeftItem(),
+ itemsToUpdate, max, 0, maxDepth);
+ }
+ if (containsVirtualMetadata(relationship.getRelationshipType().getRightwardType())) {
+ findModifiedDiscoveryItemsForCurrentItem(context, relationship.getRightItem(),
+ itemsToUpdate, max, 0, maxDepth);
+ }
+
+ for (Item item : itemsToUpdate) {
+ updateItem(context, item);
+ }
+ } catch (AuthorizeException e) {
+ log.error("Authorization Exception while authorization has been disabled", e);
+ } finally {
+ context.restoreAuthSystemState();
+ }
+ }
+
+ /**
+ * Search for items whose metadata should be updated in discovery and adds them to itemsToUpdate
+ * It starts from the given item, excludes items already in itemsToUpdate (they're already handled),
+ * and can be limited in amount of items or depth to update
+ */
+ private void findModifiedDiscoveryItemsForCurrentItem(Context context, Item item, List itemsToUpdate,
+ int max, int currentDepth, int maxDepth)
+ throws SQLException {
+ if (itemsToUpdate.size() >= max) {
+ log.debug("skipping findModifiedDiscoveryItemsForCurrentItem for item "
+ + item.getID() + " due to " + itemsToUpdate.size() + " items to be updated");
+ return;
+ }
+ if (currentDepth == maxDepth) {
+ log.debug("skipping findModifiedDiscoveryItemsForCurrentItem for item "
+ + item.getID() + " due to " + currentDepth + " depth");
+ return;
+ }
+ String entityTypeStringFromMetadata = relationshipMetadataService.getEntityTypeStringFromMetadata(item);
+ EntityType actualEntityType = entityTypeService.findByEntityType(context, entityTypeStringFromMetadata);
+ // Get all types of relations for the current item
+ List relationshipTypes = relationshipTypeService.findByEntityType(context, actualEntityType);
+ for (RelationshipType relationshipType : relationshipTypes) {
+ //are we searching for items where the current item is on the left
+ boolean isLeft = relationshipType.getLeftType().equals(actualEntityType);
+
+ // Verify whether there's virtual metadata configured for this type of relation
+ // If it's not present, we don't need to update the virtual metadata in discovery
+ String typeToSearchInVirtualMetadata;
+ if (isLeft) {
+ typeToSearchInVirtualMetadata = relationshipType.getRightwardType();
+ } else {
+ typeToSearchInVirtualMetadata = relationshipType.getLeftwardType();
+ }
+ if (containsVirtualMetadata(typeToSearchInVirtualMetadata)) {
+ // we have a relationship type where the items attached to the current item will inherit
+ // virtual metadata from the current item
+ // retrieving the actual relationships so the related items can be updated
+ List list = findByItemAndRelationshipType(context, item, relationshipType, isLeft);
+ for (Relationship foundRelationship : list) {
+ Item nextItem;
+ if (isLeft) {
+ // current item on the left, next item is on the right
+ nextItem = foundRelationship.getRightItem();
+ } else {
+ nextItem = foundRelationship.getLeftItem();
+ }
+
+ // verify it hasn't been processed yet
+ if (!itemsToUpdate.contains(nextItem)) {
+ itemsToUpdate.add(nextItem);
+ // continue the process for the next item, it may also inherit item from the current item
+ findModifiedDiscoveryItemsForCurrentItem(context, nextItem,
+ itemsToUpdate, max, currentDepth + 1, maxDepth);
+ }
+ }
+ } else {
+ log.debug("skipping " + relationshipType.getID()
+ + " in findModifiedDiscoveryItemsForCurrentItem for item "
+ + item.getID() + " because no relevant virtual metadata was found");
+ }
+ }
+ }
+
+ /**
+ * Verifies whether there is virtual metadata generated for the given relationship
+ * If no such virtual metadata exists, there's no need to update the items in discovery
+ * @param typeToSearchInVirtualMetadata a leftWardType or rightWardType of a relationship type
+ * This can be e.g. isAuthorOfPublication
+ * @return true if there is virtual metadata for this relationship
+ */
+ private boolean containsVirtualMetadata(String typeToSearchInVirtualMetadata) {
+ return virtualMetadataPopulator.getMap().containsKey(typeToSearchInVirtualMetadata)
+ && virtualMetadataPopulator.getMap().get(typeToSearchInVirtualMetadata).size() > 0;
+ }
+
/**
* Converts virtual metadata from RelationshipMetadataValue objects to actual item metadata.
*
diff --git a/dspace-api/src/main/java/org/dspace/content/virtual/Related.java b/dspace-api/src/main/java/org/dspace/content/virtual/Related.java
index 24e98d0d1b..6367cf3a1a 100644
--- a/dspace-api/src/main/java/org/dspace/content/virtual/Related.java
+++ b/dspace-api/src/main/java/org/dspace/content/virtual/Related.java
@@ -18,9 +18,7 @@ import org.dspace.content.Item;
import org.dspace.content.Relationship;
import org.dspace.content.RelationshipType;
import org.dspace.content.service.EntityService;
-import org.dspace.content.service.EntityTypeService;
import org.dspace.content.service.RelationshipService;
-import org.dspace.content.service.RelationshipTypeService;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,15 +34,9 @@ import org.springframework.beans.factory.annotation.Autowired;
*/
public class Related implements VirtualMetadataConfiguration {
- @Autowired
- private RelationshipTypeService relationshipTypeService;
-
@Autowired
private RelationshipService relationshipService;
- @Autowired
- private EntityTypeService entityTypeService;
-
@Autowired
private EntityService entityService;
@@ -172,12 +164,12 @@ public class Related implements VirtualMetadataConfiguration {
}
for (Relationship relationship : relationships) {
- if (relationship.getRelationshipType().getLeftType() == entityType) {
+ if (relationship.getRelationshipType().getLeftType().equals(entityType)) {
if (place == null || relationship.getLeftPlace() == place) {
Item otherItem = relationship.getRightItem();
return virtualMetadataConfiguration.getValues(context, otherItem);
}
- } else if (relationship.getRelationshipType().getRightType() == entityType) {
+ } else if (relationship.getRelationshipType().getRightType().equals(entityType)) {
if (place == null || relationship.getRightPlace() == place) {
Item otherItem = relationship.getLeftItem();
return virtualMetadataConfiguration.getValues(context, otherItem);
diff --git a/dspace-api/src/main/java/org/dspace/discovery/IndexEventConsumer.java b/dspace-api/src/main/java/org/dspace/discovery/IndexEventConsumer.java
index 195c9cd6fc..620d29c45d 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/IndexEventConsumer.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/IndexEventConsumer.java
@@ -130,6 +130,18 @@ public class IndexEventConsumer implements Consumer {
}
} else {
log.debug("consume() adding event to update queue: " + event.toString());
+ if (event.getSubjectType() == Constants.ITEM) {
+ // if it is an item we cannot know about its previous state, so it could be a
+ // workspaceitem that has been deposited right now or an approved/reject
+ // workflowitem.
+ // As the workflow is not necessary enabled it can happen than a workspaceitem
+ // became directly an item without giving us the chance to retrieve a
+ // workflowitem... so we need to force the unindex of all the related data
+ // before to index it again to be sure to don't leave any zombie in solr
+ String detail =
+ Constants.typeText[event.getSubjectType()] + "-" + event.getSubjectID().toString();
+ uniqueIdsToDelete.add(detail);
+ }
objectsToUpdate.addAll(indexObjectServiceFactory.getIndexableObjects(ctx, subject));
}
break;
@@ -151,7 +163,7 @@ public class IndexEventConsumer implements Consumer {
if (event.getSubjectType() == -1 || event.getSubjectID() == null) {
log.warn("got null subject type and/or ID on DELETE event, skipping it.");
} else {
- String detail = event.getSubjectType() + "-" + event.getSubjectID().toString();
+ String detail = Constants.typeText[event.getSubjectType()] + "-" + event.getSubjectID().toString();
log.debug("consume() adding event to delete queue: " + event.toString());
uniqueIdsToDelete.add(detail);
}
@@ -175,6 +187,16 @@ public class IndexEventConsumer implements Consumer {
public void end(Context ctx) throws Exception {
try {
+ for (String uid : uniqueIdsToDelete) {
+ try {
+ indexer.unIndexContent(ctx, uid, false);
+ if (log.isDebugEnabled()) {
+ log.debug("UN-Indexed Item, handle=" + uid);
+ }
+ } catch (Exception e) {
+ log.error("Failed while UN-indexing object: " + uid, e);
+ }
+ }
// update the changed Items not deleted because they were on create list
for (IndexableObject iu : objectsToUpdate) {
/* we let all types through here and
@@ -183,7 +205,7 @@ public class IndexEventConsumer implements Consumer {
*/
iu.setIndexedObject(ctx.reloadEntity(iu.getIndexedObject()));
String uniqueIndexID = iu.getUniqueIndexID();
- if (uniqueIndexID != null && !uniqueIdsToDelete.contains(uniqueIndexID)) {
+ if (uniqueIndexID != null) {
try {
indexer.indexContent(ctx, iu, true, false);
log.debug("Indexed "
@@ -195,17 +217,6 @@ public class IndexEventConsumer implements Consumer {
}
}
}
-
- for (String uid : uniqueIdsToDelete) {
- try {
- indexer.unIndexContent(ctx, uid, false);
- if (log.isDebugEnabled()) {
- log.debug("UN-Indexed Item, handle=" + uid);
- }
- } catch (Exception e) {
- log.error("Failed while UN-indexing object: " + uid, e);
- }
- }
} finally {
if (!objectsToUpdate.isEmpty() || !uniqueIdsToDelete.isEmpty()) {
diff --git a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java
index 2c12f923ae..636e7ccd2a 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java
@@ -44,7 +44,7 @@ public class DiscoveryConfigurationService {
public DiscoveryConfiguration getDiscoveryConfiguration(IndexableObject dso) {
String name;
if (dso == null) {
- name = "site";
+ name = "default";
} else if (dso instanceof IndexableDSpaceObject) {
name = ((IndexableDSpaceObject) dso).getIndexedObject().getHandle();
} else {
diff --git a/dspace-api/src/main/java/org/dspace/discovery/indexobject/factory/IndexObjectFactoryFactory.java b/dspace-api/src/main/java/org/dspace/discovery/indexobject/factory/IndexObjectFactoryFactory.java
index cd8b481764..eb287ce937 100644
--- a/dspace-api/src/main/java/org/dspace/discovery/indexobject/factory/IndexObjectFactoryFactory.java
+++ b/dspace-api/src/main/java/org/dspace/discovery/indexobject/factory/IndexObjectFactoryFactory.java
@@ -68,7 +68,7 @@ public abstract class IndexObjectFactoryFactory {
*/
public IndexFactory getIndexFactoryByType(String indexableFactoryType) {
for (IndexFactory indexableObjectFactory : getIndexFactories()) {
- if (indexableObjectFactory.getType().equals(indexableFactoryType)) {
+ if (StringUtils.equalsIgnoreCase(indexableObjectFactory.getType(), indexableFactoryType)) {
return indexableObjectFactory;
}
}
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 edcf28a84c..909e97abbb 100644
--- a/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java
@@ -274,9 +274,7 @@ public class EPersonServiceImpl extends DSpaceObjectServiceImpl impleme
for (Group group: workFlowGroups) {
List ePeople = groupService.allMembers(context, group);
if (ePeople.size() == 1 && ePeople.contains(ePerson)) {
- throw new IllegalStateException(
- "Refused to delete user " + ePerson.getID() + " because it the only member of the workflow group"
- + group.getID() + ". Delete the tasks and group first if you want to remove this user.");
+ throw new EmptyWorkflowGroupException(ePerson.getID(), group.getID());
}
}
// check for presence of eperson in tables that
diff --git a/dspace-api/src/main/java/org/dspace/eperson/EmptyWorkflowGroupException.java b/dspace-api/src/main/java/org/dspace/eperson/EmptyWorkflowGroupException.java
new file mode 100644
index 0000000000..ba60ad9615
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/eperson/EmptyWorkflowGroupException.java
@@ -0,0 +1,44 @@
+/**
+ * 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.eperson;
+
+import java.util.UUID;
+
+/**
+ *
This exception class is used to distinguish the following condition:
+ * EPerson cannot be deleted because that would lead to one (or more)
+ * workflow groups being empty.
+ *
+ *
The message of this exception can be disclosed in the REST response to
+ * provide more granular feedback to the user.
+ *
+ * @author Bruno Roemers (bruno.roemers at atmire.com)
+ */
+public class EmptyWorkflowGroupException extends IllegalStateException {
+
+ public static final String msgFmt = "Refused to delete user %s because it is the only member of the " +
+ "workflow group %s. Delete the tasks and group first if you want to remove this user.";
+
+ private final UUID ePersonId;
+ private final UUID groupId;
+
+ public EmptyWorkflowGroupException(UUID ePersonId, UUID groupId) {
+ super(String.format(msgFmt, ePersonId, groupId));
+ this.ePersonId = ePersonId;
+ this.groupId = groupId;
+ }
+
+ public UUID getEPersonId() {
+ return ePersonId;
+ }
+
+ public UUID getGroupId() {
+ return groupId;
+ }
+
+}
diff --git a/dspace-api/src/main/resources/Messages.properties b/dspace-api/src/main/resources/Messages.properties
index 974c71083d..e7a03b983c 100644
--- a/dspace-api/src/main/resources/Messages.properties
+++ b/dspace-api/src/main/resources/Messages.properties
@@ -1935,3 +1935,9 @@ jsp.dspace-admin.batchimport.itemsimported = Items imported
jsp.dspace-admin.batchimport.downloadmapfile = Download mapfile
jsp.dspace-admin.batchimport.deleteitems = Delete uploaded items & remove import
jsp.dspace-admin.batchimport.resume = Resume upload
+
+# User exposed error messages
+org.dspace.app.rest.exception.RESTEmptyWorkflowGroupException.message = Refused to delete user {0} because it is the only member of the \
+ workflow group {1}. Delete the tasks and group first if you want to remove this user.
+org.dspace.app.rest.exception.EPersonNameNotProvidedException.message = The eperson.firstname and eperson.lastname values need to be filled in
+org.dspace.app.rest.exception.GroupNameNotProvidedException.message = Cannot create group, no group name is provided
diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg
index 5f32bd0919..92084d79ad 100644
--- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg
+++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg
@@ -84,6 +84,10 @@ loglevel.dspace = INFO
###########################################
# CUSTOM UNIT / INTEGRATION TEST SETTINGS #
###########################################
+# custom dispatcher to be used by dspace-api IT that doesn't need SOLR
+event.dispatcher.exclude-discovery.class = org.dspace.event.BasicDispatcher
+event.dispatcher.exclude-discovery.consumers = versioning, eperson
+
# Configure authority control for Unit Testing (in DSpaceControlledVocabularyTest)
# (This overrides default, commented out settings in dspace.cfg)
plugin.selfnamed.org.dspace.content.authority.ChoiceAuthority = \
diff --git a/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml b/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml
index 14e2affacb..f5d21d835a 100644
--- a/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml
+++ b/dspace-api/src/test/data/dspaceFolder/config/submission-forms.xml
@@ -63,6 +63,7 @@
authorname
+ orcid,my_staff_db
@@ -242,6 +243,7 @@ it, please enter the types and the actual numbers or codes.
creativework.publisher:somepublishernameSelect the journal related to this volume.
+
diff --git a/dspace-api/src/test/java/org/dspace/AbstractUnitTest.java b/dspace-api/src/test/java/org/dspace/AbstractUnitTest.java
index d91240d218..d58fbd3745 100644
--- a/dspace-api/src/test/java/org/dspace/AbstractUnitTest.java
+++ b/dspace-api/src/test/java/org/dspace/AbstractUnitTest.java
@@ -11,7 +11,6 @@ import static org.junit.Assert.fail;
import java.sql.SQLException;
-import org.apache.commons.lang3.ArrayUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.factory.AuthorizeServiceFactory;
@@ -21,8 +20,6 @@ import org.dspace.core.I18nUtil;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
-import org.dspace.services.ConfigurationService;
-import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.storage.rdbms.DatabaseUtils;
import org.junit.After;
import org.junit.Before;
@@ -123,8 +120,11 @@ public class AbstractUnitTest extends AbstractDSpaceTest {
context.restoreAuthSystemState();
// Ensure all tests run with Solr indexing disabled
- disableSolrIndexing();
-
+ // we turn this off because
+ // Solr is NOT used in the OLD dspace-api test framework. Instead, Solr/Discovery indexing is
+ // exercised in the new Integration Tests (which use an embedded Solr) and extends
+ // org.dspace.AbstractIntegrationTestWithDatabase
+ context.setDispatcher("exclude-discovery");
} catch (AuthorizeException ex) {
log.error("Error creating initial eperson or default groups", ex);
fail("Error creating initial eperson or default groups in AbstractUnitTest init()");
@@ -167,22 +167,4 @@ public class AbstractUnitTest extends AbstractDSpaceTest {
}
}
- /**
- * Utility method which ensures Solr indexing is DISABLED in all Tests. We turn this off because
- * Solr is NOT used in the dspace-api test framework. Instead, Solr/Discovery indexing is
- * exercised in the dspace-server Integration Tests (which use an embedded Solr).
- */
- protected static void disableSolrIndexing() {
- // Get our currently configured list of event consumers
- ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
- String[] consumers = configurationService.getArrayProperty("event.dispatcher.default.consumers");
-
- // Remove "discovery" from the configured consumers (if it exists).
- // This turns off Discovery/Solr indexing after any object changes.
- if (ArrayUtils.contains(consumers, "discovery")) {
- consumers = ArrayUtils.removeElement(consumers, "discovery");
- configurationService.setProperty("event.dispatcher.default.consumers", consumers);
- }
- }
-
}
diff --git a/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java
index eaee97e0fa..612ad82faa 100644
--- a/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java
+++ b/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java
@@ -22,6 +22,7 @@ import org.dspace.content.WorkspaceItem;
import org.dspace.content.service.WorkspaceItemService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
+import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
/**
* Builder to construct WorkspaceItem objects
@@ -110,6 +111,13 @@ public class WorkspaceItemBuilder extends AbstractBuilder();
@@ -212,8 +227,8 @@ public class RelationshipServiceImplTest {
when(metsList.get(0).getValue()).thenReturn("Entitylabel");
when(relationshipService
.findByItemAndRelationshipType(context, leftItem, testRel, true)).thenReturn(leftTypelist);
- when(itemService.getMetadata(leftItem, "relationship", "type", null, Item.ANY)).thenReturn(metsList);
- when(itemService.getMetadata(rightItem, "relationship", "type", null, Item.ANY)).thenReturn(metsList);
+ when(itemService.getMetadata(leftItem, "relationship", "type", null, Item.ANY, false)).thenReturn(metsList);
+ when(itemService.getMetadata(rightItem, "relationship", "type", null, Item.ANY, false)).thenReturn(metsList);
when(relationshipDAO.create(any(), any())).thenReturn(relationship);
// The reported Relationship should match our defined relationship
@@ -290,8 +305,8 @@ public class RelationshipServiceImplTest {
relationship = getRelationship(leftItem, rightItem, testRel, 0,0);
// Mock the state of objects utilized in update() to meet the success criteria of the invocation
- when(itemService.getMetadata(leftItem, "relationship", "type", null, Item.ANY)).thenReturn(metsList);
- when(itemService.getMetadata(rightItem, "relationship", "type", null, Item.ANY)).thenReturn(metsList);
+ when(itemService.getMetadata(leftItem, "relationship", "type", null, Item.ANY, false)).thenReturn(metsList);
+ when(itemService.getMetadata(rightItem, "relationship", "type", null, Item.ANY, false)).thenReturn(metsList);
when(authorizeService.authorizeActionBoolean(context, relationship.getLeftItem(),
Constants.WRITE)).thenReturn(true);
diff --git a/dspace-api/src/test/java/org/dspace/curate/CurationTest.java b/dspace-api/src/test/java/org/dspace/curate/CurationIT.java
similarity index 97%
rename from dspace-api/src/test/java/org/dspace/curate/CurationTest.java
rename to dspace-api/src/test/java/org/dspace/curate/CurationIT.java
index dadf131c38..6232793c74 100644
--- a/dspace-api/src/test/java/org/dspace/curate/CurationTest.java
+++ b/dspace-api/src/test/java/org/dspace/curate/CurationIT.java
@@ -20,7 +20,7 @@ import org.dspace.scripts.factory.ScriptServiceFactory;
import org.dspace.scripts.service.ScriptService;
import org.junit.Test;
-public class CurationTest extends AbstractIntegrationTestWithDatabase {
+public class CurationIT extends AbstractIntegrationTestWithDatabase {
@Test(expected = ParseException.class)
public void curationWithoutEPersonParameterTest() throws Exception {
diff --git a/dspace-api/src/test/java/org/dspace/curate/ITCurator.java b/dspace-api/src/test/java/org/dspace/curate/CuratorReportTest.java
similarity index 97%
rename from dspace-api/src/test/java/org/dspace/curate/ITCurator.java
rename to dspace-api/src/test/java/org/dspace/curate/CuratorReportTest.java
index b60589cd4d..489161bf8d 100644
--- a/dspace-api/src/test/java/org/dspace/curate/ITCurator.java
+++ b/dspace-api/src/test/java/org/dspace/curate/CuratorReportTest.java
@@ -35,11 +35,11 @@ import org.slf4j.LoggerFactory;
*
* @author mhwood
*/
-public class ITCurator
+public class CuratorReportTest
extends AbstractUnitTest {
- Logger LOG = LoggerFactory.getLogger(ITCurator.class);
+ Logger LOG = LoggerFactory.getLogger(CuratorReportTest.class);
- public ITCurator() {
+ public CuratorReportTest() {
}
@BeforeClass
diff --git a/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java
new file mode 100644
index 0000000000..05ff5ae41a
--- /dev/null
+++ b/dspace-api/src/test/java/org/dspace/discovery/DiscoveryIT.java
@@ -0,0 +1,331 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.discovery;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+
+import org.dspace.AbstractIntegrationTestWithDatabase;
+import org.dspace.authorize.AuthorizeException;
+import org.dspace.builder.ClaimedTaskBuilder;
+import org.dspace.builder.CollectionBuilder;
+import org.dspace.builder.CommunityBuilder;
+import org.dspace.builder.PoolTaskBuilder;
+import org.dspace.builder.WorkflowItemBuilder;
+import org.dspace.builder.WorkspaceItemBuilder;
+import org.dspace.content.Collection;
+import org.dspace.content.Community;
+import org.dspace.content.WorkspaceItem;
+import org.dspace.content.factory.ContentServiceFactory;
+import org.dspace.content.service.ItemService;
+import org.dspace.content.service.WorkspaceItemService;
+import org.dspace.discovery.indexobject.IndexableClaimedTask;
+import org.dspace.discovery.indexobject.IndexableItem;
+import org.dspace.discovery.indexobject.IndexablePoolTask;
+import org.dspace.discovery.indexobject.IndexableWorkflowItem;
+import org.dspace.discovery.indexobject.IndexableWorkspaceItem;
+import org.dspace.eperson.EPerson;
+import org.dspace.services.factory.DSpaceServicesFactory;
+import org.dspace.workflow.WorkflowException;
+import org.dspace.xmlworkflow.WorkflowConfigurationException;
+import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory;
+import org.dspace.xmlworkflow.service.WorkflowRequirementsService;
+import org.dspace.xmlworkflow.service.XmlWorkflowService;
+import org.dspace.xmlworkflow.state.Step;
+import org.dspace.xmlworkflow.state.Workflow;
+import org.dspace.xmlworkflow.state.actions.WorkflowActionConfig;
+import org.dspace.xmlworkflow.storedcomponents.ClaimedTask;
+import org.dspace.xmlworkflow.storedcomponents.PoolTask;
+import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem;
+import org.dspace.xmlworkflow.storedcomponents.service.ClaimedTaskService;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+/**
+ * This class will aim to test Discovery related use cases
+ */
+public class DiscoveryIT extends AbstractIntegrationTestWithDatabase {
+
+ protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService();
+ protected SearchService searchService = SearchUtils.getSearchService();
+
+ XmlWorkflowService workflowService = XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService();
+
+ WorkflowRequirementsService workflowRequirementsService = XmlWorkflowServiceFactory.getInstance().
+ getWorkflowRequirementsService();
+
+ ClaimedTaskService claimedTaskService = XmlWorkflowServiceFactory.getInstance().getClaimedTaskService();
+
+ ItemService itemService = ContentServiceFactory.getInstance().getItemService();
+
+ IndexingService indexer = DSpaceServicesFactory.getInstance().getServiceManager()
+ .getServiceByName(IndexingService.class.getName(),
+ IndexingService.class);
+
+
+ @Test
+ public void solrRecordsAfterDepositOrDeletionOfWorkspaceItemTest() throws Exception {
+ context.turnOffAuthorisationSystem();
+ Community community = CommunityBuilder.createCommunity(context)
+ .withName("Parent Community")
+ .build();
+ Collection col = CollectionBuilder.createCollection(context, community)
+ .withName("Collection without workflow")
+ .build();
+ Collection colWithWorkflow = CollectionBuilder.createCollection(context, community)
+ .withName("Collection WITH workflow")
+ .withWorkflowGroup(1, admin)
+ .build();
+ WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, col)
+ .withTitle("No workflow")
+ .withAbstract("headache")
+ .build();
+ WorkspaceItem anotherWorkspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, col)
+ .withTitle("Another WS Item in No workflow collection")
+ .withAbstract("headache")
+ .build();
+ WorkspaceItem workspaceItemInWfCollection = WorkspaceItemBuilder.createWorkspaceItem(context, colWithWorkflow)
+ .withTitle("WS Item in workflow collection")
+ .withAbstract("headache")
+ .build();
+ context.restoreAuthSystemState();
+
+ // we start with 3 ws items
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 3);
+ // simulate the deposit
+ deposit(workspaceItem);
+ // now we should have 1 archived item and 2 ws items, no wf items or tasks
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 0);
+ assertSearchQuery(IndexablePoolTask.TYPE, 0);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 0);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 2);
+ assertSearchQuery(IndexableItem.TYPE, 1);
+
+ // simulate the deposit of the ws item in the workflow collection
+ deposit(workspaceItemInWfCollection);
+ // now we should have 1 wf, 1 pool task, 1 ws item and 1 item
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 1);
+ assertSearchQuery(IndexablePoolTask.TYPE, 1);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 0);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 1);
+ assertSearchQuery(IndexableItem.TYPE, 1);
+
+ // simulate the delete of last workspace item
+ deleteSubmission(anotherWorkspaceItem);
+
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 1);
+ assertSearchQuery(IndexablePoolTask.TYPE, 1);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 0);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 0);
+ assertSearchQuery(IndexableItem.TYPE, 1);
+ }
+
+ @Test
+ public void solrRecordsAfterDealingWithWorkflowTest() throws Exception {
+ context.turnOffAuthorisationSystem();
+ Community community = CommunityBuilder.createCommunity(context)
+ .withName("Parent Community")
+ .build();
+ Collection collection = CollectionBuilder.createCollection(context, community)
+ .withWorkflowGroup(1, admin)
+ .build();
+ Workflow workflow = XmlWorkflowServiceFactory.getInstance().getWorkflowFactory().getWorkflow(collection);
+
+ ClaimedTask taskToApprove = ClaimedTaskBuilder.createClaimedTask(context, collection, admin)
+ .withTitle("Test workflow item to approve")
+ .withIssueDate("2019-03-06")
+ .withSubject("ExtraEntry")
+ .build();
+ ClaimedTask taskToReject = ClaimedTaskBuilder.createClaimedTask(context, collection, admin)
+ .withTitle("Test workflow item to reject")
+ .withIssueDate("2019-03-06")
+ .withSubject("ExtraEntry")
+ .build();
+ PoolTask taskToClaim = PoolTaskBuilder.createPoolTask(context, collection, admin)
+ .withTitle("Test pool task to claim")
+ .withIssueDate("2019-03-06")
+ .withSubject("ExtraEntry")
+ .build();
+ ClaimedTask taskToUnclaim = ClaimedTaskBuilder.createClaimedTask(context, collection, admin)
+ .withTitle("Test claimed task to unclaim")
+ .withIssueDate("2019-03-06")
+ .withSubject("ExtraEntry")
+ .build();
+ XmlWorkflowItem wfiToDelete = WorkflowItemBuilder.createWorkflowItem(context, collection)
+ .withTitle("Test workflow item to return")
+ .withIssueDate("2019-03-06")
+ .withSubject("ExtraEntry")
+ .build();
+
+ context.restoreAuthSystemState();
+ // we start with 5 workflow items, 3 claimed tasks, 2 pool task
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 5);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 3);
+ assertSearchQuery(IndexablePoolTask.TYPE, 2);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 0);
+ assertSearchQuery(IndexableItem.TYPE, 0);
+
+ // claim
+ claim(workflow, taskToClaim, admin);
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 5);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 4);
+ assertSearchQuery(IndexablePoolTask.TYPE, 1);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 0);
+ assertSearchQuery(IndexableItem.TYPE, 0);
+
+ // unclaim
+ returnClaimedTask(taskToUnclaim);
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 5);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 3);
+ assertSearchQuery(IndexablePoolTask.TYPE, 2);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 0);
+ assertSearchQuery(IndexableItem.TYPE, 0);
+
+ // reject
+ MockHttpServletRequest httpRejectRequest = new MockHttpServletRequest();
+ httpRejectRequest.setParameter("submit_reject", "submit_reject");
+ httpRejectRequest.setParameter("reason", "test");
+ executeWorkflowAction(httpRejectRequest, workflow, taskToReject);
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 4);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 2);
+ assertSearchQuery(IndexablePoolTask.TYPE, 2);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 1);
+ assertSearchQuery(IndexableItem.TYPE, 0);
+
+ // approve
+ MockHttpServletRequest httpApproveRequest = new MockHttpServletRequest();
+ httpApproveRequest.setParameter("submit_approve", "submit_approve");
+ executeWorkflowAction(httpApproveRequest, workflow, taskToApprove);
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 3);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 1);
+ assertSearchQuery(IndexablePoolTask.TYPE, 2);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 1);
+ assertSearchQuery(IndexableItem.TYPE, 1);
+
+ // abort pool task
+ // as we have already unclaimed this task it is a pool task now
+ abort(taskToUnclaim.getWorkflowItem());
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 2);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 1);
+ assertSearchQuery(IndexablePoolTask.TYPE, 1);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 2);
+ assertSearchQuery(IndexableItem.TYPE, 1);
+
+ // abort claimed task
+ // as we have already claimed this task it is a claimed task now
+ abort(taskToClaim.getWorkflowItem());
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 1);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 0);
+ assertSearchQuery(IndexablePoolTask.TYPE, 1);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 3);
+ assertSearchQuery(IndexableItem.TYPE, 1);
+
+ // delete pool task / workflow item
+ deleteWorkflowItem(wfiToDelete);
+ assertSearchQuery(IndexableWorkflowItem.TYPE, 0);
+ assertSearchQuery(IndexableClaimedTask.TYPE, 0);
+ assertSearchQuery(IndexablePoolTask.TYPE, 0);
+ assertSearchQuery(IndexableWorkspaceItem.TYPE, 3);
+ assertSearchQuery(IndexableItem.TYPE, 1);
+ }
+
+ private void assertSearchQuery(String resourceType, int size) throws SearchServiceException {
+ DiscoverQuery discoverQuery = new DiscoverQuery();
+ discoverQuery.setQuery("*:*");
+ discoverQuery.addFilterQueries("search.resourcetype:" + resourceType);
+ DiscoverResult discoverResult = searchService.search(context, discoverQuery);
+ List indexableObjects = discoverResult.getIndexableObjects();
+ assertEquals(size, indexableObjects.size());
+ assertEquals(size, discoverResult.getTotalSearchResults());
+ }
+
+
+ private void deposit(WorkspaceItem workspaceItem)
+ throws SQLException, AuthorizeException, IOException, WorkflowException, SearchServiceException {
+ context.turnOffAuthorisationSystem();
+ workspaceItem = context.reloadEntity(workspaceItem);
+ XmlWorkflowItem workflowItem = workflowService.startWithoutNotify(context, workspaceItem);
+ context.commit();
+ indexer.commit();
+ context.restoreAuthSystemState();
+ }
+
+ private void deleteSubmission(WorkspaceItem anotherWorkspaceItem)
+ throws SQLException, AuthorizeException, IOException, SearchServiceException {
+ context.turnOffAuthorisationSystem();
+ anotherWorkspaceItem = context.reloadEntity(anotherWorkspaceItem);
+ workspaceItemService.deleteAll(context, anotherWorkspaceItem);
+ context.commit();
+ indexer.commit();
+ context.restoreAuthSystemState();
+ }
+
+ private void deleteWorkflowItem(XmlWorkflowItem workflowItem)
+ throws SQLException, AuthorizeException, IOException, SearchServiceException {
+ context.turnOffAuthorisationSystem();
+ workflowItem = context.reloadEntity(workflowItem);
+ workflowService.deleteWorkflowByWorkflowItem(context, workflowItem, admin);
+ context.commit();
+ indexer.commit();
+ context.restoreAuthSystemState();
+ }
+
+ private void returnClaimedTask(ClaimedTask taskToUnclaim) throws SQLException, IOException,
+ WorkflowConfigurationException, AuthorizeException, SearchServiceException {
+ final EPerson previousUser = context.getCurrentUser();
+ taskToUnclaim = context.reloadEntity(taskToUnclaim);
+ context.setCurrentUser(taskToUnclaim.getOwner());
+ XmlWorkflowItem workflowItem = taskToUnclaim.getWorkflowItem();
+ workflowService.deleteClaimedTask(context, workflowItem, taskToUnclaim);
+ workflowRequirementsService.removeClaimedUser(context, workflowItem, taskToUnclaim.getOwner(),
+ taskToUnclaim.getStepID());
+ context.commit();
+ indexer.commit();
+ context.setCurrentUser(previousUser);
+ }
+
+ private void claim(Workflow workflow, PoolTask task, EPerson user)
+ throws Exception {
+ final EPerson previousUser = context.getCurrentUser();
+ task = context.reloadEntity(task);
+ context.setCurrentUser(user);
+ Step step = workflow.getStep(task.getStepID());
+ WorkflowActionConfig currentActionConfig = step.getActionConfig(task.getActionID());
+ workflowService.doState(context, user, null, task.getWorkflowItem().getID(), workflow, currentActionConfig);
+ context.commit();
+ indexer.commit();
+ context.setCurrentUser(previousUser);
+ }
+
+ private void executeWorkflowAction(HttpServletRequest httpServletRequest, Workflow workflow, ClaimedTask task)
+ throws Exception {
+ final EPerson previousUser = context.getCurrentUser();
+ task = context.reloadEntity(task);
+ context.setCurrentUser(task.getOwner());
+ workflowService.doState(context, task.getOwner(), httpServletRequest, task.getWorkflowItem().getID(), workflow,
+ workflow.getStep(task.getStepID()).getActionConfig(task.getActionID()));
+ context.commit();
+ indexer.commit();
+ context.setCurrentUser(previousUser);
+ }
+
+ private void abort(XmlWorkflowItem workflowItem)
+ throws SQLException, AuthorizeException, IOException, SearchServiceException {
+ final EPerson previousUser = context.getCurrentUser();
+ workflowItem = context.reloadEntity(workflowItem);
+ context.setCurrentUser(admin);
+ workflowService.abort(context, workflowItem, admin);
+ context.commit();
+ indexer.commit();
+ context.setCurrentUser(previousUser);
+ }
+}
diff --git a/dspace-api/src/test/java/org/dspace/eperson/GroupServiceImplIT.java b/dspace-api/src/test/java/org/dspace/eperson/GroupServiceImplTest.java
similarity index 99%
rename from dspace-api/src/test/java/org/dspace/eperson/GroupServiceImplIT.java
rename to dspace-api/src/test/java/org/dspace/eperson/GroupServiceImplTest.java
index 6d61483e29..2098438b20 100644
--- a/dspace-api/src/test/java/org/dspace/eperson/GroupServiceImplIT.java
+++ b/dspace-api/src/test/java/org/dspace/eperson/GroupServiceImplTest.java
@@ -21,9 +21,9 @@ import org.junit.Test;
*
* @author mwood
*/
-public class GroupServiceImplIT
+public class GroupServiceImplTest
extends AbstractUnitTest {
- public GroupServiceImplIT() {
+ public GroupServiceImplTest() {
super();
}
diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/processor/BitstreamEventProcessorTest.java b/dspace-api/src/test/java/org/dspace/statistics/export/processor/BitstreamEventProcessorIT.java
similarity index 97%
rename from dspace-api/src/test/java/org/dspace/statistics/export/processor/BitstreamEventProcessorTest.java
rename to dspace-api/src/test/java/org/dspace/statistics/export/processor/BitstreamEventProcessorIT.java
index 3b197b4955..e4cb59fe61 100644
--- a/dspace-api/src/test/java/org/dspace/statistics/export/processor/BitstreamEventProcessorTest.java
+++ b/dspace-api/src/test/java/org/dspace/statistics/export/processor/BitstreamEventProcessorIT.java
@@ -35,7 +35,7 @@ import org.junit.Test;
/**
* Test class for the BitstreamEventProcessor
*/
-public class BitstreamEventProcessorTest extends AbstractIntegrationTestWithDatabase {
+public class BitstreamEventProcessorIT extends AbstractIntegrationTestWithDatabase {
private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorTest.java b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java
similarity index 99%
rename from dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorTest.java
rename to dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java
index 8daa5d3843..0f250176f5 100644
--- a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorTest.java
+++ b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ExportEventProcessorIT.java
@@ -41,7 +41,7 @@ import org.mockito.Mock;
/**
* Test for the ExportEventProcessor class
*/
-public class ExportEventProcessorTest extends AbstractIntegrationTestWithDatabase {
+public class ExportEventProcessorIT extends AbstractIntegrationTestWithDatabase {
@Mock
private final HttpServletRequest request = mock(HttpServletRequest.class);
diff --git a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ItemEventProcessorTest.java b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ItemEventProcessorIT.java
similarity index 96%
rename from dspace-api/src/test/java/org/dspace/statistics/export/processor/ItemEventProcessorTest.java
rename to dspace-api/src/test/java/org/dspace/statistics/export/processor/ItemEventProcessorIT.java
index 3eced0fa2e..a2f5e4c5ab 100644
--- a/dspace-api/src/test/java/org/dspace/statistics/export/processor/ItemEventProcessorTest.java
+++ b/dspace-api/src/test/java/org/dspace/statistics/export/processor/ItemEventProcessorIT.java
@@ -29,7 +29,7 @@ import org.junit.Test;
/**
* Test class for the ItemEventProcessor
*/
-public class ItemEventProcessorTest extends AbstractIntegrationTestWithDatabase {
+public class ItemEventProcessorIT extends AbstractIntegrationTestWithDatabase {
private final ConfigurationService configurationService
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionFormConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionFormConverter.java
index 339f601dc4..4555d8b00a 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionFormConverter.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionFormConverter.java
@@ -11,6 +11,7 @@ import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
+import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.model.ScopeEnum;
import org.dspace.app.rest.model.SubmissionFormFieldRest;
@@ -174,6 +175,9 @@ public class SubmissionFormConverter implements DSpaceConverterExtend {@link UnprocessableEntityException} to provide a specific error message
+ * in the REST response. The error message is added to the response in
+ * {@link DSpaceApiExceptionControllerAdvice#handleCustomUnprocessableEntityException},
+ * hence it should not contain sensitive or security-compromising info.
+ *
+ * @author Bruno Roemers (bruno.roemers at atmire.com)
+ */
+public class EPersonNameNotProvidedException extends UnprocessableEntityException implements TranslatableException {
+
+ public static final String MESSAGE_KEY = "org.dspace.app.rest.exception.EPersonNameNotProvidedException.message";
+
+ public EPersonNameNotProvidedException() {
+ super(I18nUtil.getMessage(MESSAGE_KEY));
+ }
+
+ public EPersonNameNotProvidedException(Throwable cause) {
+ super(I18nUtil.getMessage(MESSAGE_KEY), cause);
+ }
+
+ public String getMessageKey() {
+ return MESSAGE_KEY;
+ }
+
+}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/GroupNameNotProvidedException.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/GroupNameNotProvidedException.java
new file mode 100644
index 0000000000..c3fd5d77cb
--- /dev/null
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/GroupNameNotProvidedException.java
@@ -0,0 +1,35 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.rest.exception;
+
+import org.dspace.core.I18nUtil;
+
+/**
+ *
Extend {@link UnprocessableEntityException} to provide a specific error message
+ * in the REST response. The error message is added to the response in
+ * {@link DSpaceApiExceptionControllerAdvice#handleCustomUnprocessableEntityException},
+ * hence it should not contain sensitive or security-compromising info.
+ *
+ * @author Bruno Roemers (bruno.roemers at atmire.com)
+ */
+public class GroupNameNotProvidedException extends UnprocessableEntityException implements TranslatableException {
+
+ public static final String MESSAGE_KEY = "org.dspace.app.rest.exception.GroupNameNotProvidedException.message";
+
+ public GroupNameNotProvidedException() {
+ super(I18nUtil.getMessage(MESSAGE_KEY));
+ }
+
+ public GroupNameNotProvidedException(Throwable cause) {
+ super(I18nUtil.getMessage(MESSAGE_KEY), cause);
+ }
+
+ public String getMessageKey() {
+ return MESSAGE_KEY;
+ }
+}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RESTEmptyWorkflowGroupException.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RESTEmptyWorkflowGroupException.java
new file mode 100644
index 0000000000..49774f30a7
--- /dev/null
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RESTEmptyWorkflowGroupException.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.app.rest.exception;
+
+import java.text.MessageFormat;
+
+import org.dspace.core.Context;
+import org.dspace.core.I18nUtil;
+import org.dspace.eperson.EmptyWorkflowGroupException;
+
+/**
+ *
Extend {@link UnprocessableEntityException} to provide a specific error message
+ * in the REST response. The error message is added to the response in
+ * {@link DSpaceApiExceptionControllerAdvice#handleCustomUnprocessableEntityException},
+ * hence it should not contain sensitive or security-compromising info.
+ *
+ *
Note there is a similarly named error in the DSpace API module.
+ *
+ * @author Bruno Roemers (bruno.roemers at atmire.com)
+ */
+public class RESTEmptyWorkflowGroupException extends UnprocessableEntityException implements TranslatableException {
+
+ /**
+ * @param formatStr string with placeholders, ideally obtained using {@link I18nUtil}
+ * @param cause {@link EmptyWorkflowGroupException}, from which EPerson id and group id are obtained
+ * @return message with EPerson id and group id substituted
+ */
+ private static String formatMessage(String formatStr, EmptyWorkflowGroupException cause) {
+ MessageFormat fmt = new MessageFormat(formatStr);
+ String[] values = {
+ cause.getEPersonId().toString(), // {0} in formatStr
+ cause.getGroupId().toString(), // {1} in formatStr
+ };
+ return fmt.format(values);
+ }
+
+ public static final String MESSAGE_KEY = "org.dspace.app.rest.exception.RESTEmptyWorkflowGroupException.message";
+
+ private final EmptyWorkflowGroupException cause;
+
+ public RESTEmptyWorkflowGroupException(EmptyWorkflowGroupException cause) {
+ super(formatMessage(
+ I18nUtil.getMessage(MESSAGE_KEY), cause
+ ), cause);
+ this.cause = cause;
+ }
+
+ public String getMessageKey() {
+ return MESSAGE_KEY;
+ }
+
+ public String getLocalizedMessage(Context context) {
+ return formatMessage(
+ I18nUtil.getMessage(MESSAGE_KEY, context), cause
+ );
+ }
+
+}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RepositoryNotFoundException.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RepositoryNotFoundException.java
index b72af6f9d3..9fee095103 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RepositoryNotFoundException.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/RepositoryNotFoundException.java
@@ -25,4 +25,8 @@ public class RepositoryNotFoundException extends RuntimeException {
this.model = model;
}
+ @Override
+ public String getMessage() {
+ return String.format("The repository type %s.%s was not found", apiCategory, model);
+ }
}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/TranslatableException.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/TranslatableException.java
new file mode 100644
index 0000000000..ff8d83bcbd
--- /dev/null
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/TranslatableException.java
@@ -0,0 +1,52 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+
+package org.dspace.app.rest.exception;
+
+import org.dspace.core.Context;
+import org.dspace.core.I18nUtil;
+
+/**
+ *
Implement TranslatableException to make Exceptions or RuntimeExceptions translatable.
+ *
+ *
In most cases, only {@link #getMessageKey()} should be implemented;
+ * {@link #getMessage()} and {@link #getLocalizedMessage()} are already provided by {@link Throwable}
+ * and the default implementation of {@link #getLocalizedMessage(Context)} is usually sufficient.
+ *
+ *
A locale-aware message can be obtained by calling {@link #getLocalizedMessage(Context)}.
+ *
+ * @author Bruno Roemers (bruno.roemers at atmire.com)
+ */
+public interface TranslatableException {
+
+ /**
+ * @return message key (used for lookup with {@link I18nUtil})
+ */
+ String getMessageKey();
+
+ /**
+ * Already implemented by {@link Throwable}.
+ * @return message for default locale
+ */
+ String getMessage();
+
+ /**
+ * Already implemented by {@link Throwable}.
+ * @return message for default locale
+ */
+ String getLocalizedMessage();
+
+ /**
+ * @param context current DSpace context (used to infer current locale)
+ * @return message for current locale (or default locale if current locale did not yield a result)
+ */
+ default String getLocalizedMessage(Context context) {
+ return I18nUtil.getMessage(getMessageKey(), context);
+ }
+
+}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/query/RestSearchOperator.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/query/RestSearchOperator.java
index 5dc845d02e..ae8713bc69 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/query/RestSearchOperator.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/query/RestSearchOperator.java
@@ -7,6 +7,9 @@
*/
package org.dspace.app.rest.model.query;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -95,4 +98,19 @@ public enum RestSearchOperator {
public String getDspaceOperator() {
return dspaceOperator;
}
+
+ /**
+ * Returns a list of dspace operators of this enum's values, plus "query" which is also allowed, but will be
+ * transformed in {@link org.dspace.app.rest.converter.query.SearchQueryConverter} to any of the others
+ *
+ * @return List of dspace operators of this enum's values, plus "query"
+ */
+ public static List getListOfAllowedSearchOperatorStrings() {
+ List allowedSearchOperatorStrings = new ArrayList<>();
+ for (RestSearchOperator restSearchOperator: Arrays.asList(RestSearchOperator.values())) {
+ allowedSearchOperatorStrings.add(restSearchOperator.getDspaceOperator());
+ }
+ allowedSearchOperatorStrings.add("query");
+ return allowedSearchOperatorStrings;
+ }
}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/submit/SelectableRelationship.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/submit/SelectableRelationship.java
index 97117321f1..c2d2767e9b 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/submit/SelectableRelationship.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/submit/SelectableRelationship.java
@@ -7,6 +7,8 @@
*/
package org.dspace.app.rest.model.submit;
+import java.util.List;
+
/**
* The SelectableRelationship REST Resource. It is not addressable directly, only
* used as inline object in the InputForm resource.
@@ -24,6 +26,7 @@ public class SelectableRelationship {
private String filter;
private String searchConfiguration;
private String nameVariants;
+ private List externalSources;
public void setRelationshipType(String relationshipType) {
this.relationshipType = relationshipType;
@@ -56,4 +59,12 @@ public class SelectableRelationship {
public String getNameVariants() {
return nameVariants;
}
+
+ public List getExternalSources() {
+ return externalSources;
+ }
+
+ public void setExternalSources(List externalSources) {
+ this.externalSources = externalSources;
+ }
}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/parameter/resolver/SearchFilterResolver.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/parameter/resolver/SearchFilterResolver.java
index 7607e7c19c..c11f8cbb1a 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/parameter/resolver/SearchFilterResolver.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/parameter/resolver/SearchFilterResolver.java
@@ -13,6 +13,8 @@ import java.util.LinkedList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
+import org.dspace.app.rest.exception.UnprocessableEntityException;
+import org.dspace.app.rest.model.query.RestSearchOperator;
import org.dspace.app.rest.parameter.SearchFilter;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -29,13 +31,15 @@ public class SearchFilterResolver implements HandlerMethodArgumentResolver {
public static final String SEARCH_FILTER_PREFIX = "f.";
public static final String FILTER_OPERATOR_SEPARATOR = ",";
+ public static final List ALLOWED_SEARCH_OPERATORS =
+ RestSearchOperator.getListOfAllowedSearchOperatorStrings();
+
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.getParameterType().equals(SearchFilter.class) || isSearchFilterList(parameter);
}
public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer,
- final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory)
- throws Exception {
+ final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory) {
List result = new LinkedList<>();
Iterator parameterNames = webRequest.getParameterNames();
@@ -48,7 +52,7 @@ public class SearchFilterResolver implements HandlerMethodArgumentResolver {
for (String value : webRequest.getParameterValues(parameterName)) {
String filterValue = StringUtils.substringBeforeLast(value, FILTER_OPERATOR_SEPARATOR);
String filterOperator = StringUtils.substringAfterLast(value, FILTER_OPERATOR_SEPARATOR);
-
+ this.checkIfValidOperator(filterOperator);
result.add(new SearchFilter(filterName, filterOperator, filterValue));
}
}
@@ -61,6 +65,19 @@ public class SearchFilterResolver implements HandlerMethodArgumentResolver {
}
}
+ private void checkIfValidOperator(String filterOperator) {
+ if (StringUtils.isNotBlank(filterOperator)) {
+ if (!ALLOWED_SEARCH_OPERATORS.contains(filterOperator.trim())) {
+ throw new UnprocessableEntityException(
+ "The operator can't be \"" + filterOperator + "\", must be the of one of: " +
+ String.join(", ", ALLOWED_SEARCH_OPERATORS));
+ }
+ } else {
+ throw new UnprocessableEntityException(
+ "The operator can't be empty, must be the one of: " + String.join(", ", ALLOWED_SEARCH_OPERATORS));
+ }
+ }
+
private boolean isSearchFilterList(final MethodParameter parameter) {
return parameter.getParameterType().equals(List.class)
&& parameter.getGenericParameterType() instanceof ParameterizedType
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java
index b6c82221aa..4ef929b03d 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/EPersonRestRepository.java
@@ -22,6 +22,8 @@ import org.dspace.app.rest.Parameter;
import org.dspace.app.rest.SearchRestMethod;
import org.dspace.app.rest.authorization.AuthorizationFeatureService;
import org.dspace.app.rest.exception.DSpaceBadRequestException;
+import org.dspace.app.rest.exception.EPersonNameNotProvidedException;
+import org.dspace.app.rest.exception.RESTEmptyWorkflowGroupException;
import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.EPersonRest;
import org.dspace.app.rest.model.MetadataRest;
@@ -34,6 +36,7 @@ import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.service.SiteService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
+import org.dspace.eperson.EmptyWorkflowGroupException;
import org.dspace.eperson.RegistrationData;
import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService;
@@ -199,8 +202,7 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository epersonLastName = metadataRest.getMap().get("eperson.lastname");
if (epersonFirstName == null || epersonLastName == null ||
epersonFirstName.isEmpty() || epersonLastName.isEmpty()) {
- throw new UnprocessableEntityException("The eperson.firstname and eperson.lastname values need to be " +
- "filled in");
+ throw new EPersonNameNotProvidedException();
}
}
String password = epersonRest.getPassword();
@@ -313,8 +315,10 @@ public class EPersonRestRepository extends DSpaceObjectRestRepository candidateElements.length) {
+ return false;
+ }
+ for (int elementN = 0; elementN < patternElements.length; elementN++) {
+ isPrefix &= candidateElements[elementN].equals(patternElements[elementN]);
+ }
+
+ return isPrefix;
+ }
}
diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java
index a1793c6776..870a272335 100644
--- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java
+++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/Utils.java
@@ -9,6 +9,7 @@ package org.dspace.app.rest.utils;
import static java.lang.Integer.parseInt;
import static java.util.stream.Collectors.toList;
+import static org.dspace.app.rest.utils.URLUtils.urlIsPrefixOf;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import java.beans.IntrospectionException;
@@ -24,6 +25,8 @@ import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -43,7 +46,8 @@ import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
-import org.apache.log4j.Logger;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.converter.ConverterService;
import org.dspace.app.rest.exception.PaginationException;
import org.dspace.app.rest.exception.RepositoryNotFoundException;
@@ -99,7 +103,7 @@ import org.springframework.web.multipart.MultipartFile;
@Component
public class Utils {
- private static final Logger log = Logger.getLogger(Utils.class);
+ private static final Logger log = LogManager.getLogger(Utils.class);
/**
* The default page size, if unspecified in the request.
@@ -220,9 +224,11 @@ public class Utils {
*
* @param apiCategory
* @param modelPlural
- * @return
+ * @return the requested repository.
+ * @throws RepositoryNotFoundException passed through.
*/
- public DSpaceRestRepository getResourceRepository(String apiCategory, String modelPlural) {
+ public DSpaceRestRepository getResourceRepository(String apiCategory, String modelPlural)
+ throws RepositoryNotFoundException {
String model = makeSingular(modelPlural);
return getResourceRepositoryByCategoryAndModel(apiCategory, model);
}
@@ -233,9 +239,11 @@ public class Utils {
*
* @param apiCategory
* @param modelSingular
- * @return
+ * @return the requested repository.
+ * @throws RepositoryNotFoundException if no such repository can be found.
*/
- public DSpaceRestRepository getResourceRepositoryByCategoryAndModel(String apiCategory, String modelSingular) {
+ public DSpaceRestRepository getResourceRepositoryByCategoryAndModel(String apiCategory, String modelSingular)
+ throws RepositoryNotFoundException {
try {
return applicationContext.getBean(apiCategory + "." + modelSingular, DSpaceRestRepository.class);
} catch (NoSuchBeanDefinitionException e) {
@@ -243,6 +251,10 @@ public class Utils {
}
}
+ /**
+ * Find the names of all {@link DSpaceRestRepository} implementations.
+ * @return the names of all repository types.
+ */
public String[] getRepositories() {
return applicationContext.getBeanNamesForType(DSpaceRestRepository.class);
}
@@ -304,8 +316,8 @@ public class Utils {
}
/**
- * Build the canonical representation of a metadata key in DSpace. I.e.
- * .[.]
+ * Build the canonical representation of a metadata key in DSpace. I.e.
+ * {@code .[.]}
*
* @param schema
* @param element
@@ -413,6 +425,7 @@ public class Utils {
* It will then look through all the DSpaceObjectServices to try and match this UUID to a DSpaceObject.
* If one is found, this DSpaceObject is added to the List of DSpaceObjects that we will return.
* @param context The relevant DSpace context
+ * @param list The interesting UUIDs.
* @return The resulting list of DSpaceObjects that we parsed out of the request
*/
public List constructDSpaceObjectList(Context context, List list) {
@@ -699,6 +712,7 @@ public class Utils {
/**
* Adds embeds (if the maximum embed level has not been exceeded yet) for all properties annotated with
* {@code @LinkRel} or whose return types are {@link RestAddressableModel} subclasses.
+ * @param resource the resource to be so augmented.
*/
public void embedMethodLevelRels(HALResource extends RestAddressableModel> resource) {
if (resource.getContent().getEmbedLevel() == EMBED_MAX_LEVELS) {
@@ -916,26 +930,45 @@ public class Utils {
*/
public BaseObjectRest getBaseObjectRestFromUri(Context context, String uri) throws SQLException {
String dspaceUrl = configurationService.getProperty("dspace.server.url");
- // first check if the uri could be valid
- if (!StringUtils.startsWith(uri, dspaceUrl)) {
- throw new IllegalArgumentException("the supplied uri is not valid: " + uri);
+
+ // Convert strings to URL objects.
+ // Do this early to check that inputs are well-formed.
+ URL dspaceUrlObject;
+ URL requestUrlObject;
+ try {
+ dspaceUrlObject = new URL(dspaceUrl);
+ requestUrlObject = new URL(uri);
+ } catch (MalformedURLException ex) {
+ throw new IllegalArgumentException(
+ String.format("Configuration '%s' or request '%s' is malformed", dspaceUrl, uri));
+ }
+
+ // Check whether the URI could be valid.
+ if (!urlIsPrefixOf(dspaceUrl, uri)) {
+ throw new IllegalArgumentException("the supplied uri is not ours: " + uri);
+ }
+
+ // Extract from the URI the category, model and id components.
+ // They start after the dspaceUrl/api/{apiCategory}/{apiModel}/{id}
+ int dspacePathLength = StringUtils.split(dspaceUrlObject.getPath(), '/').length;
+ String[] requestPath = StringUtils.split(requestUrlObject.getPath(), '/');
+ String[] uriParts = Arrays.copyOfRange(requestPath, dspacePathLength,
+ requestPath.length);
+ if ("api".equalsIgnoreCase(uriParts[0])) {
+ uriParts = Arrays.copyOfRange(uriParts, 1, uriParts.length);
}
- // extract from the uri the category, model and id components
- // they start after the dspaceUrl/api/{apiCategory}/{apiModel}/{id}
- String[] uriParts = uri.substring(dspaceUrl.length() + (dspaceUrl.endsWith("/") ? 0 : 1) + "api/".length())
- .split("/", 3);
if (uriParts.length != 3) {
- throw new IllegalArgumentException("the supplied uri is not valid: " + uri);
+ throw new IllegalArgumentException("the supplied uri lacks required path elements: " + uri);
}
DSpaceRestRepository repository;
try {
repository = getResourceRepository(uriParts[0], uriParts[1]);
if (!(repository instanceof ReloadableEntityObjectRepository)) {
- throw new IllegalArgumentException("the supplied uri is not valid: " + uri);
+ throw new IllegalArgumentException("the supplied uri is not for the right type of repository: " + uri);
}
} catch (RepositoryNotFoundException e) {
- throw new IllegalArgumentException("the supplied uri is not valid: " + uri, e);
+ throw new IllegalArgumentException("the repository for the URI '" + uri + "' was not found", e);
}
Serializable pk;
@@ -943,7 +976,7 @@ public class Utils {
// cast the string id in the uriParts to the real pk class
pk = castToPKClass((ReloadableEntityObjectRepository) repository, uriParts[2]);
} catch (Exception e) {
- throw new IllegalArgumentException("the supplied uri is not valid: " + uri, e);
+ throw new IllegalArgumentException("the supplied uri could not be cast to a Primary Key class: " + uri, e);
}
try {
// disable the security as we only need to retrieve the object to further process the authorization
diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java
index 002acbf482..7478d67a35 100644
--- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java
+++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryRestControllerIT.java
@@ -2370,7 +2370,7 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
}
@Test
- public void discoverSearchObjectsWithQueryOperatorContains() throws Exception {
+ public void discoverSearchObjectsWithQueryOperatorContains_query() throws Exception {
//We turn off the authorization system in order to create the structure as defined below
context.turnOffAuthorisationSystem();
@@ -2445,7 +2445,83 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
}
@Test
- public void discoverSearchObjectsWithQueryOperatorNotContains() throws Exception {
+ public void discoverSearchObjectsWithQueryOperatorContains() throws Exception {
+ //We turn off the authorization system in order to create the structure as defined below
+ context.turnOffAuthorisationSystem();
+
+ //** GIVEN **
+ //1. A community-collection structure with one parent community with sub-community and two collections.
+ parentCommunity = CommunityBuilder.createCommunity(context)
+ .withName("Parent Community")
+ .build();
+ Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
+ .withName("Sub Community")
+ .build();
+ Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
+ Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
+ //2. Three public items that are readable by Anonymous with different subjects
+ Item publicItem1 = ItemBuilder.createItem(context, col1)
+ .withTitle("Test")
+ .withIssueDate("2010-10-17")
+ .withAuthor("Smith, Donald")
+ .withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem2 = ItemBuilder.createItem(context, col2)
+ .withTitle("Test 2")
+ .withIssueDate("1990-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("Testing, Works")
+ .withSubject("TestingForMore").withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem3 = ItemBuilder.createItem(context, col2)
+ .withTitle("Public item 2")
+ .withIssueDate("2010-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("test,test")
+ .withAuthor("test2, test2").withAuthor("Maybe, Maybe")
+ .withSubject("AnotherTest").withSubject("TestingForMore")
+ .withSubject("ExtraEntry")
+ .build();
+
+ context.restoreAuthSystemState();
+
+ UUID scope = col2.getID();
+ //** WHEN **
+ //An anonymous user browses this endpoint to find the the objects in the system
+ //With the given search filter
+ getClient().perform(get("/api/discover/search/objects")
+ .param("f.title", "test,contains"))
+ //** THEN **
+ //The status has to be 200 OK
+ .andExpect(status().isOk())
+ //The type has to be 'discover'
+ .andExpect(jsonPath("$.type", is("discover")))
+ //The page object needs to look like this
+ .andExpect(jsonPath("$._embedded.searchResult.page", is(
+ PageMatcher.pageEntry(0, 20)
+ )))
+ //The search results have to contain the items that match the searchFilter
+ .andExpect(jsonPath("$._embedded.searchResult._embedded.objects", Matchers.containsInAnyOrder(
+ SearchResultMatcher.matchOnItemName("item", "items", "Test"),
+ SearchResultMatcher.matchOnItemName("item", "items", "Test 2")
+ )))
+ //These facets have to show up in the embedded.facets section as well with the given hasMore property
+ // because we don't exceed their default limit for a hasMore true (the default is 10)
+ .andExpect(jsonPath("$._embedded.facets", Matchers.containsInAnyOrder(
+ FacetEntryMatcher.authorFacet(false),
+ FacetEntryMatcher.entityTypeFacet(false),
+ FacetEntryMatcher.subjectFacet(false),
+ FacetEntryMatcher.dateIssuedFacet(false),
+ FacetEntryMatcher.hasContentInOriginalBundleFacet(false)
+ )))
+ //There always needs to be a self link available
+ .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")))
+ ;
+
+ }
+
+ @Test
+ public void discoverSearchObjectsWithQueryOperatorNotContains_query() throws Exception {
//We turn off the authorization system in order to create the structure as defined below
context.turnOffAuthorisationSystem();
@@ -2518,6 +2594,81 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
}
+ @Test
+ public void discoverSearchObjectsWithQueryOperatorNotContains() throws Exception {
+ //We turn off the authorization system in order to create the structure as defined below
+ context.turnOffAuthorisationSystem();
+
+ //** GIVEN **
+ //1. A community-collection structure with one parent community with sub-community and two collections.
+ parentCommunity = CommunityBuilder.createCommunity(context)
+ .withName("Parent Community")
+ .build();
+ Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
+ .withName("Sub Community")
+ .build();
+ Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
+ Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
+ //2. Three public items that are readable by Anonymous with different subjects
+ Item publicItem1 = ItemBuilder.createItem(context, col1)
+ .withTitle("Test")
+ .withIssueDate("2010-10-17")
+ .withAuthor("Smith, Donald")
+ .withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem2 = ItemBuilder.createItem(context, col2)
+ .withTitle("Test 2")
+ .withIssueDate("1990-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("Testing, Works")
+ .withSubject("TestingForMore").withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem3 = ItemBuilder.createItem(context, col2)
+ .withTitle("Public item 2")
+ .withIssueDate("2010-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("test,test")
+ .withAuthor("test2, test2").withAuthor("Maybe, Maybe")
+ .withSubject("AnotherTest").withSubject("TestingForMore")
+ .withSubject("ExtraEntry")
+ .build();
+
+ context.restoreAuthSystemState();
+
+ UUID scope = col2.getID();
+ //** WHEN **
+ //An anonymous user browses this endpoint to find the the objects in the system
+ //With the given search filter
+ getClient().perform(get("/api/discover/search/objects")
+ .param("f.title", "test,notcontains"))
+ //** THEN **
+ //The status has to be 200 OK
+ .andExpect(status().isOk())
+ //The type has to be 'discover'
+ .andExpect(jsonPath("$.type", is("discover")))
+ //The page object needs to look like this
+ .andExpect(jsonPath("$._embedded.searchResult.page", is(
+ PageMatcher.pageEntry(0, 20)
+ )))
+ //The search results have to contain the items that match the searchFilter
+ .andExpect(jsonPath("$._embedded.searchResult._embedded.objects", Matchers.hasItem(
+ SearchResultMatcher.matchOnItemName("item", "items", "Public item 2")
+ )))
+ //These facets have to show up in the embedded.facets section as well with the given hasMore property
+ // because we don't exceed their default limit for a hasMore true (the default is 10)
+ .andExpect(jsonPath("$._embedded.facets", Matchers.containsInAnyOrder(
+ FacetEntryMatcher.authorFacet(false),
+ FacetEntryMatcher.entityTypeFacet(false),
+ FacetEntryMatcher.subjectFacet(false),
+ FacetEntryMatcher.dateIssuedFacet(false),
+ FacetEntryMatcher.hasContentInOriginalBundleFacet(false)
+ )))
+ //There always needs to be a self link available
+ .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")))
+ ;
+
+ }
+
@Test
public void discoverSearchObjectsTestForMinMaxValues() throws Exception {
//We turn off the authorization system in order to create the structure as defined below
@@ -2673,7 +2824,7 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
}
@Test
- public void discoverSearchObjectsWithQueryOperatorEquals() throws Exception {
+ public void discoverSearchObjectsWithQueryOperatorEquals_query() throws Exception {
//We turn off the authorization system in order to create the structure as defined below
context.turnOffAuthorisationSystem();
@@ -2747,7 +2898,82 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
}
@Test
- public void discoverSearchObjectsWithQueryOperatorNotEquals() throws Exception {
+ public void discoverSearchObjectsWithQueryOperatorEquals() throws Exception {
+ //We turn off the authorization system in order to create the structure as defined below
+ context.turnOffAuthorisationSystem();
+
+ //** GIVEN **
+ //1. A community-collection structure with one parent community with sub-community and two collections.
+ parentCommunity = CommunityBuilder.createCommunity(context)
+ .withName("Parent Community")
+ .build();
+ Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
+ .withName("Sub Community")
+ .build();
+ Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
+ Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
+ //2. Three public items that are readable by Anonymous with different subjects
+ Item publicItem1 = ItemBuilder.createItem(context, col1)
+ .withTitle("Test")
+ .withIssueDate("2010-10-17")
+ .withAuthor("Smith, Donald")
+ .withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem2 = ItemBuilder.createItem(context, col2)
+ .withTitle("Test 2")
+ .withIssueDate("1990-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("Testing, Works")
+ .withSubject("TestingForMore").withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem3 = ItemBuilder.createItem(context, col2)
+ .withTitle("Public item 2")
+ .withIssueDate("2010-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("test,test")
+ .withAuthor("test2, test2").withAuthor("Maybe, Maybe")
+ .withSubject("AnotherTest").withSubject("TestingForMore")
+ .withSubject("ExtraEntry")
+ .build();
+
+ context.restoreAuthSystemState();
+
+ UUID scope = col2.getID();
+ //** WHEN **
+ //An anonymous user browses this endpoint to find the the objects in the system
+ //With the given search filter
+ getClient().perform(get("/api/discover/search/objects")
+ .param("f.title", "Test,equals"))
+ //** THEN **
+ //The status has to be 200 OK
+ .andExpect(status().isOk())
+ //The type has to be 'discover'
+ .andExpect(jsonPath("$.type", is("discover")))
+ //The page object needs to look like this
+ .andExpect(jsonPath("$._embedded.searchResult.page", is(
+ PageMatcher.pageEntry(0, 20)
+ )))
+ //The search results have to contain the items that match the searchFilter
+ .andExpect(jsonPath("$._embedded.searchResult._embedded.objects", Matchers.containsInAnyOrder(
+ SearchResultMatcher.matchOnItemName("item", "items", "Test")
+ )))
+ //These facets have to show up in the embedded.facets section as well with the given hasMore property
+ // because we don't exceed their default limit for a hasMore true (the default is 10)
+ .andExpect(jsonPath("$._embedded.facets", Matchers.containsInAnyOrder(
+ FacetEntryMatcher.authorFacet(false),
+ FacetEntryMatcher.entityTypeFacet(false),
+ FacetEntryMatcher.subjectFacet(false),
+ FacetEntryMatcher.dateIssuedFacet(false),
+ FacetEntryMatcher.hasContentInOriginalBundleFacet(false)
+ )))
+ //There always needs to be a self link available
+ .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")))
+ ;
+
+ }
+
+ @Test
+ public void discoverSearchObjectsWithQueryOperatorNotEquals_query() throws Exception {
//We turn off the authorization system in order to create the structure as defined below
context.turnOffAuthorisationSystem();
@@ -2822,7 +3048,83 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
}
@Test
- public void discoverSearchObjectsWithQueryOperatorNotAuthority() throws Exception {
+ public void discoverSearchObjectsWithQueryOperatorNotEquals() throws Exception {
+ //We turn off the authorization system in order to create the structure as defined below
+ context.turnOffAuthorisationSystem();
+
+ //** GIVEN **
+ //1. A community-collection structure with one parent community with sub-community and two collections.
+ parentCommunity = CommunityBuilder.createCommunity(context)
+ .withName("Parent Community")
+ .build();
+ Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
+ .withName("Sub Community")
+ .build();
+ Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
+ Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
+ //2. Three public items that are readable by Anonymous with different subjects
+ Item publicItem1 = ItemBuilder.createItem(context, col1)
+ .withTitle("Test")
+ .withIssueDate("2010-10-17")
+ .withAuthor("Smith, Donald")
+ .withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem2 = ItemBuilder.createItem(context, col2)
+ .withTitle("Test 2")
+ .withIssueDate("1990-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("Testing, Works")
+ .withSubject("TestingForMore").withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem3 = ItemBuilder.createItem(context, col2)
+ .withTitle("Public item 2")
+ .withIssueDate("2010-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("test,test")
+ .withAuthor("test2, test2").withAuthor("Maybe, Maybe")
+ .withSubject("AnotherTest").withSubject("TestingForMore")
+ .withSubject("ExtraEntry")
+ .build();
+
+ context.restoreAuthSystemState();
+
+ UUID scope = col2.getID();
+ //** WHEN **
+ //An anonymous user browses this endpoint to find the the objects in the system
+ //With the given search filter
+ getClient().perform(get("/api/discover/search/objects")
+ .param("f.title", "Test,notequals"))
+ //** THEN **
+ //The status has to be 200 OK
+ .andExpect(status().isOk())
+ //The type has to be 'discover'
+ .andExpect(jsonPath("$.type", is("discover")))
+ //The page object needs to look like this
+ .andExpect(jsonPath("$._embedded.searchResult.page", is(
+ PageMatcher.pageEntry(0, 20)
+ )))
+ //The search results have to contain the items that match the searchFilter
+ .andExpect(jsonPath("$._embedded.searchResult._embedded.objects", Matchers.hasItems(
+ SearchResultMatcher.matchOnItemName("item", "items", "Test 2"),
+ SearchResultMatcher.matchOnItemName("item", "items", "Public item 2")
+ )))
+ //These facets have to show up in the embedded.facets section as well with the given hasMore property
+ // because we don't exceed their default limit for a hasMore true (the default is 10)
+ .andExpect(jsonPath("$._embedded.facets", Matchers.containsInAnyOrder(
+ FacetEntryMatcher.authorFacet(false),
+ FacetEntryMatcher.entityTypeFacet(false),
+ FacetEntryMatcher.subjectFacet(false),
+ FacetEntryMatcher.dateIssuedFacet(false),
+ FacetEntryMatcher.hasContentInOriginalBundleFacet(false)
+ )))
+ //There always needs to be a self link available
+ .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")))
+ ;
+
+ }
+
+ @Test
+ public void discoverSearchObjectsWithQueryOperatorNotAuthority_query() throws Exception {
//We turn off the authorization system in order to create the structure as defined below
context.turnOffAuthorisationSystem();
@@ -2895,6 +3197,108 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
}
+ @Test
+ public void discoverSearchObjectsWithQueryOperatorNotAuthority() throws Exception {
+ //We turn off the authorization system in order to create the structure as defined below
+ context.turnOffAuthorisationSystem();
+
+ //** GIVEN **
+ //1. A community-collection structure with one parent community with sub-community and two collections.
+ parentCommunity = CommunityBuilder.createCommunity(context)
+ .withName("Parent Community")
+ .build();
+ Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity)
+ .withName("Sub Community")
+ .build();
+ Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build();
+ Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2").build();
+ //2. Three public items that are readable by Anonymous with different subjects
+ Item publicItem1 = ItemBuilder.createItem(context, col1)
+ .withTitle("Test")
+ .withIssueDate("2010-10-17")
+ .withAuthor("Smith, Donald")
+ .withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem2 = ItemBuilder.createItem(context, col2)
+ .withTitle("Test 2")
+ .withIssueDate("1990-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("Testing, Works")
+ .withSubject("TestingForMore").withSubject("ExtraEntry")
+ .build();
+
+ Item publicItem3 = ItemBuilder.createItem(context, col2)
+ .withTitle("Public item 2")
+ .withIssueDate("2010-02-13")
+ .withAuthor("Smith, Maria").withAuthor("Doe, Jane").withAuthor("test,test")
+ .withAuthor("test2, test2").withAuthor("Maybe, Maybe")
+ .withSubject("AnotherTest").withSubject("TestingForMore")
+ .withSubject("ExtraEntry")
+ .build();
+
+ context.restoreAuthSystemState();
+
+ UUID scope = col2.getID();
+ //** WHEN **
+ //An anonymous user browses this endpoint to find the the objects in the system
+ //With the given search filter
+ getClient().perform(get("/api/discover/search/objects")
+ .param("f.title", "test,notauthority"))
+ //** THEN **
+ //The status has to be 200 OK
+ .andExpect(status().isOk())
+ //The type has to be 'discover'
+ .andExpect(jsonPath("$.type", is("discover")))
+ //The page object needs to look like this
+ .andExpect(jsonPath("$._embedded.searchResult.page", is(
+ PageMatcher.pageEntry(0, 20)
+ )))
+ //The search results have to contain the items that match the searchFilter
+ .andExpect(jsonPath("$._embedded.searchResult._embedded.objects", Matchers.hasItem(
+ SearchResultMatcher.matchOnItemName("item", "items", "Public item 2")
+ )))
+ //These facets have to show up in the embedded.facets section as well with the given hasMore property
+ // because we don't exceed their default limit for a hasMore true (the default is 10)
+ .andExpect(jsonPath("$._embedded.facets", Matchers.containsInAnyOrder(
+ FacetEntryMatcher.authorFacet(false),
+ FacetEntryMatcher.entityTypeFacet(false),
+ FacetEntryMatcher.subjectFacet(false),
+ FacetEntryMatcher.dateIssuedFacet(false),
+ FacetEntryMatcher.hasContentInOriginalBundleFacet(false)
+ )))
+ //There always needs to be a self link available
+ .andExpect(jsonPath("$._links.self.href", containsString("/api/discover/search/objects")))
+ ;
+
+ }
+
+ @Test
+ public void discoverSearchObjectsWithMissingQueryOperator() throws Exception {
+ //** WHEN **
+ // An anonymous user browses this endpoint to find the the objects in the system
+ // With the given search filter where there is the filter operator missing in the value (must be of form
+ // <:filter-value>,<:filter-operator>)
+ getClient().perform(get("/api/discover/search/objects")
+ .param("f.title", "test"))
+ //** THEN **
+ //Will result in 422 status because of missing filter operator
+ .andExpect(status().isUnprocessableEntity());
+ }
+
+ @Test
+ public void discoverSearchObjectsWithNotValidQueryOperator() throws Exception {
+ //** WHEN **
+ // An anonymous user browses this endpoint to find the the objects in the system
+ // With the given search filter where there is a non-valid filter operator given (must be of form
+ // <:filter-value>,<:filter-operator> where the filter operator is one of: “contains”, “notcontains”, "equals"
+ // “notequals”, “authority”, “notauthority”, "query”); see enum RestSearchOperator
+ getClient().perform(get("/api/discover/search/objects")
+ .param("f.title", "test,operator"))
+ //** THEN **
+ //Will result in 422 status because of non-valid filter operator
+ .andExpect(status().isUnprocessableEntity());
+ }
+
@Test
public void discoverSearchObjectsTestWithDateIssuedQueryTest() throws Exception {
//We turn off the authorization system in order to create the structure as defined below
@@ -4478,7 +4882,7 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.perform(get("/api/discover/search/objects")
.param("configuration", "administrativeView")
.param("query", "Test")
- .param("f.withdrawn", "true")
+ .param("f.withdrawn", "true,contains")
)
.andExpect(status().isOk())
@@ -4497,7 +4901,7 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.perform(get("/api/discover/search/objects")
.param("configuration", "administrativeView")
.param("query", "Test")
- .param("f.withdrawn", "false")
+ .param("f.withdrawn", "false,contains")
)
.andExpect(status().isOk())
@@ -4517,7 +4921,7 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.perform(get("/api/discover/search/objects")
.param("configuration", "administrativeView")
.param("query", "Test")
- .param("f.discoverable", "true")
+ .param("f.discoverable", "true,contains")
)
.andExpect(status().isOk())
@@ -4537,7 +4941,7 @@ public class DiscoveryRestControllerIT extends AbstractControllerIntegrationTest
.perform(get("/api/discover/search/objects")
.param("configuration", "administrativeView")
.param("query", "Test")
- .param("f.discoverable", "false")
+ .param("f.discoverable", "false,contains")
)
.andExpect(status().isOk())
diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java
index d7fff36dee..084c08b231 100644
--- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java
+++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/EPersonRestRepositoryIT.java
@@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -31,15 +32,19 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
+import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import javax.ws.rs.core.MediaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
+import org.dspace.app.rest.exception.EPersonNameNotProvidedException;
+import org.dspace.app.rest.exception.RESTEmptyWorkflowGroupException;
import org.dspace.app.rest.jackson.IgnoreJacksonWriteOnlyAccess;
import org.dspace.app.rest.matcher.EPersonMatcher;
import org.dspace.app.rest.matcher.GroupMatcher;
@@ -61,6 +66,7 @@ import org.dspace.builder.GroupBuilder;
import org.dspace.builder.WorkflowItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Community;
+import org.dspace.core.I18nUtil;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.PasswordHash;
@@ -69,6 +75,7 @@ import org.dspace.eperson.service.AccountService;
import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.services.ConfigurationService;
+import org.dspace.workflow.WorkflowService;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -85,6 +92,9 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
@Autowired
private EPersonService ePersonService;
+ @Autowired
+ private WorkflowService workflowService;
+
@Autowired
private RegistrationDataDAO registrationDataDAO;
@Autowired
@@ -832,13 +842,62 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
// 422 error when trying to DELETE the eperson=submitter
getClient(token).perform(delete("/api/eperson/epersons/" + ePerson.getID()))
- .andExpect(status().is(422));
+ .andExpect(status().isUnprocessableEntity());
// Verify the eperson is still here
getClient(token).perform(get("/api/eperson/epersons/" + ePerson.getID()))
.andExpect(status().isOk());
}
+ @Test
+ public void deleteLastPersonInWorkflowGroup() throws Exception {
+ // set up workflow group with ePerson as only member
+ context.turnOffAuthorisationSystem();
+ EPerson ePerson = EPersonBuilder
+ .createEPerson(context)
+ .withEmail("eperson@example.com")
+ .withNameInMetadata("Sample", "EPerson")
+ .build();
+ Community community = CommunityBuilder
+ .createCommunity(context)
+ .build();
+ Collection collection = CollectionBuilder
+ .createCollection(context, community)
+ .withWorkflowGroup(1, ePerson)
+ .build();
+ Group workflowGroup = collection.getWorkflowStep1(context);
+ context.restoreAuthSystemState();
+
+ // enable Polish locale
+ configurationService.setProperty("webui.supported.locales", "en, pl");
+
+ // generate expectations
+ String key = RESTEmptyWorkflowGroupException.MESSAGE_KEY;
+ String[] values = {
+ ePerson.getID().toString(),
+ workflowGroup.getID().toString(),
+ };
+ MessageFormat defaultFmt = new MessageFormat(I18nUtil.getMessage(key));
+ MessageFormat plFmt = new MessageFormat(I18nUtil.getMessage(key, new Locale("pl")));
+
+ // make request using Polish locale
+ getClient(getAuthToken(admin.getEmail(), password))
+ .perform(
+ delete("/api/eperson/epersons/" + ePerson.getID())
+ .header("Accept-Language", "pl") // request Polish response
+ )
+ .andExpect(status().isUnprocessableEntity())
+ .andExpect(status().reason(is(plFmt.format(values))))
+ .andExpect(status().reason(startsWith("[PL]"))); // verify it did not fall back to default locale
+
+ // make request using default locale
+ getClient(getAuthToken(admin.getEmail(), password))
+ .perform(delete("/api/eperson/epersons/" + ePerson.getID()))
+ .andExpect(status().isUnprocessableEntity())
+ .andExpect(status().reason(is(defaultFmt.format(values))))
+ .andExpect(status().reason(not(startsWith("[PL]"))));
+ }
+
@Test
public void patchByForbiddenUser() throws Exception {
@@ -2528,12 +2587,33 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess());
+ // enable Polish locale
+ configurationService.setProperty("webui.supported.locales", "en, pl");
+
try {
+ // make request using Polish locale
getClient().perform(post("/api/eperson/epersons")
- .param("token", newRegisterToken)
- .content(mapper.writeValueAsBytes(ePersonRest))
- .contentType(MediaType.APPLICATION_JSON))
- .andExpect(status().isUnprocessableEntity());
+ .header("Accept-Language", "pl") // request Polish response
+ .param("token", newRegisterToken)
+ .content(mapper.writeValueAsBytes(ePersonRest))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isUnprocessableEntity())
+ .andExpect(status().reason(is(
+ // find message in dspace-server-webapp/src/test/resources/Messages_pl.properties
+ I18nUtil.getMessage(EPersonNameNotProvidedException.MESSAGE_KEY, new Locale("pl"))
+ )))
+ .andExpect(status().reason(startsWith("[PL]"))); // verify default locale was NOT used
+
+ // make request using default locale
+ getClient().perform(post("/api/eperson/epersons")
+ .param("token", newRegisterToken)
+ .content(mapper.writeValueAsBytes(ePersonRest))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isUnprocessableEntity())
+ .andExpect(status().reason(is(
+ I18nUtil.getMessage(EPersonNameNotProvidedException.MESSAGE_KEY)
+ )))
+ .andExpect(status().reason(not(startsWith("[PL]"))));
EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail);
assertNull(createdEPerson);
@@ -2574,12 +2654,34 @@ public class EPersonRestRepositoryIT extends AbstractControllerIntegrationTest {
mapper.setAnnotationIntrospector(new IgnoreJacksonWriteOnlyAccess());
+ // enable Polish locale
+ configurationService.setProperty("webui.supported.locales", "en, pl");
+
try {
+ // make request using Polish locale
getClient().perform(post("/api/eperson/epersons")
- .param("token", newRegisterToken)
- .content(mapper.writeValueAsBytes(ePersonRest))
- .contentType(MediaType.APPLICATION_JSON))
- .andExpect(status().isUnprocessableEntity());
+ .header("Accept-Language", "pl") // request Polish response
+ .param("token", newRegisterToken)
+ .content(mapper.writeValueAsBytes(ePersonRest))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isUnprocessableEntity())
+ .andExpect(status().reason(is(
+ // find message in dspace-server-webapp/src/test/resources/Messages_pl.properties
+ I18nUtil.getMessage(EPersonNameNotProvidedException.MESSAGE_KEY, new Locale("pl"))
+ )))
+ .andExpect(status().reason(startsWith("[PL]"))); // verify default locale was NOT used
+
+ // make request using default locale
+ getClient().perform(post("/api/eperson/epersons")
+ .param("token", newRegisterToken)
+ .content(mapper.writeValueAsBytes(ePersonRest))
+ .contentType(MediaType.APPLICATION_JSON))
+ .andExpect(status().isUnprocessableEntity())
+ .andExpect(status().reason(is(
+ // find message in dspace-server-webapp/src/test/resources/Messages_pl.properties
+ I18nUtil.getMessage(EPersonNameNotProvidedException.MESSAGE_KEY)
+ )))
+ .andExpect(status().reason(not(startsWith("[PL]"))));
EPerson createdEPerson = ePersonService.findByEmail(context, newRegisterEmail);
assertNull(createdEPerson);
diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/GroupRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/GroupRestRepositoryIT.java
index 1291e0b5a4..8d438adb68 100644
--- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/GroupRestRepositoryIT.java
+++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/GroupRestRepositoryIT.java
@@ -9,8 +9,11 @@ package org.dspace.app.rest;
import static com.jayway.jsonpath.JsonPath.read;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
+import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -27,11 +30,13 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
+import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import javax.ws.rs.core.MediaType;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.dspace.app.rest.exception.GroupNameNotProvidedException;
import org.dspace.app.rest.matcher.EPersonMatcher;
import org.dspace.app.rest.matcher.GroupMatcher;
import org.dspace.app.rest.matcher.HalMatcher;
@@ -54,6 +59,7 @@ import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.CommunityService;
import org.dspace.core.Constants;
+import org.dspace.core.I18nUtil;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.factory.EPersonServiceFactory;
@@ -194,10 +200,49 @@ public class GroupRestRepositoryIT extends AbstractControllerIntegrationTest {
String authToken = getAuthToken(admin.getEmail(), password);
getClient(authToken)
- .perform(post("/api/eperson/groups")
- .content(mapper.writeValueAsBytes(groupRest))
- .contentType(contentType))
- .andExpect(status().isUnprocessableEntity());
+ .perform(
+ post("/api/eperson/groups").content("").contentType(contentType)
+ )
+ .andExpect(status().isUnprocessableEntity())
+ .andExpect(status().reason(containsString("Unprocessable")));
+ }
+
+ @Test
+ public void createWithoutNameTest() throws Exception {
+ ObjectMapper mapper = new ObjectMapper();
+ GroupRest groupRest = new GroupRest(); // no name set
+
+ String authToken = getAuthToken(admin.getEmail(), password);
+
+ // enable Polish locale
+ configurationService.setProperty("webui.supported.locales", "en, pl");
+
+ // make request using Polish locale
+ getClient(authToken)
+ .perform(
+ post("/api/eperson/groups")
+ .header("Accept-Language", "pl") // request Polish response
+ .content(mapper.writeValueAsBytes(groupRest))
+ .contentType(contentType)
+ )
+ .andExpect(status().isUnprocessableEntity())
+ .andExpect(status().reason(is(
+ I18nUtil.getMessage(GroupNameNotProvidedException.MESSAGE_KEY, new Locale("pl"))
+ )))
+ .andExpect(status().reason(startsWith("[PL]"))); // verify it did not fall back to default locale
+
+ // make request using default locale
+ getClient(authToken)
+ .perform(
+ post("/api/eperson/groups")
+ .content(mapper.writeValueAsBytes(groupRest))
+ .contentType(contentType)
+ )
+ .andExpect(status().isUnprocessableEntity())
+ .andExpect(status().reason(is(
+ I18nUtil.getMessage(GroupNameNotProvidedException.MESSAGE_KEY)
+ )))
+ .andExpect(status().reason(not(startsWith("[PL]"))));
}
@Test
diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java
index db1582ff52..d8f4188a66 100644
--- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java
+++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/RelationshipRestRepositoryIT.java
@@ -9,6 +9,8 @@ package org.dspace.app.rest;
import static com.jayway.jsonpath.JsonPath.read;
import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
@@ -18,6 +20,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -33,6 +36,8 @@ import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonObject;
import org.apache.commons.lang3.StringUtils;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.response.QueryResponse;
import org.dspace.app.rest.matcher.PageMatcher;
import org.dspace.app.rest.matcher.RelationshipMatcher;
import org.dspace.app.rest.model.RelationshipRest;
@@ -41,13 +46,16 @@ import org.dspace.authorize.service.AuthorizeService;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
+import org.dspace.builder.EntityTypeBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.builder.MetadataFieldBuilder;
import org.dspace.builder.RelationshipBuilder;
+import org.dspace.builder.RelationshipTypeBuilder;
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.MetadataSchema;
import org.dspace.content.MetadataValue;
import org.dspace.content.Relationship;
@@ -59,6 +67,7 @@ import org.dspace.content.service.MetadataSchemaService;
import org.dspace.content.service.RelationshipTypeService;
import org.dspace.core.Constants;
import org.dspace.core.I18nUtil;
+import org.dspace.discovery.MockSolrSearchCore;
import org.dspace.eperson.EPerson;
import org.junit.Before;
import org.junit.Test;
@@ -86,6 +95,8 @@ public class RelationshipRestRepositoryIT extends AbstractEntityIntegrationTest
@Autowired
private MetadataSchemaService metadataSchemaService;
+ @Autowired
+ MockSolrSearchCore mockSolrSearchCore;
private Community parentCommunity;
private Community child1;
@@ -2673,4 +2684,149 @@ public class RelationshipRestRepositoryIT extends AbstractEntityIntegrationTest
RelationshipBuilder.deleteRelationship(idRef.get());
}
}
+
+ @Test
+ public void testVirtualMdInRESTAndSolrDoc() throws Exception {
+ context.turnOffAuthorisationSystem();
+ // Create entity types if needed
+ EntityType journalEntityType = entityTypeService.findByEntityType(context, "Journal");
+ if (journalEntityType == null) {
+ journalEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "Journal").build();
+ }
+ EntityType journalVolumeEntityType = entityTypeService.findByEntityType(context, "JournalVolume");
+ if (journalVolumeEntityType == null) {
+ journalVolumeEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "JournalVolume").build();
+ }
+ EntityType journalIssueEntityType = entityTypeService.findByEntityType(context, "JournalIssue");
+ if (journalIssueEntityType == null) {
+ journalIssueEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "JournalIssue").build();
+ }
+ EntityType publicationEntityType = entityTypeService.findByEntityType(context, "Publication");
+ if (publicationEntityType == null) {
+ publicationEntityType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build();
+ }
+
+ // Create relationship types if needed
+ RelationshipType isPublicationOfJournalIssue = relationshipTypeService
+ .findbyTypesAndTypeName(context, journalIssueEntityType, publicationEntityType,
+ "isPublicationOfJournalIssue", "isJournalIssueOfPublication");
+ if (isPublicationOfJournalIssue == null) {
+ isPublicationOfJournalIssue = RelationshipTypeBuilder.createRelationshipTypeBuilder(context,
+ journalIssueEntityType, publicationEntityType, "isPublicationOfJournalIssue",
+ "isJournalIssueOfPublication", null, null, null, null).build();
+ }
+ RelationshipType isIssueOfJournalVolume = relationshipTypeService
+ .findbyTypesAndTypeName(context, journalVolumeEntityType, journalIssueEntityType,
+ "isIssueOfJournalVolume", "isJournalVolumeOfIssue");
+ if (isIssueOfJournalVolume == null) {
+ isIssueOfJournalVolume = RelationshipTypeBuilder.createRelationshipTypeBuilder(context,
+ journalVolumeEntityType, journalIssueEntityType, "isIssueOfJournalVolume",
+ "isJournalVolumeOfIssue", null, null, null, null).build();
+ } else {
+ // Otherwise error in destroy methods when removing Journal Issue-Journal Volume relationship
+ // since the rightMinCardinality constraint would be violated upon deletion
+ isIssueOfJournalVolume.setRightMinCardinality(0);
+ }
+ RelationshipType isVolumeOfJournal = relationshipTypeService
+ .findbyTypesAndTypeName(context, journalEntityType, journalVolumeEntityType,
+ "isVolumeOfJournal", "isJournalOfVolume");
+ if (isVolumeOfJournal == null) {
+ isVolumeOfJournal = RelationshipTypeBuilder.createRelationshipTypeBuilder(context,
+ journalEntityType, journalVolumeEntityType, "isVolumeOfJournal", "isJournalOfVolume",
+ null, null, null, null).build();
+ } else {
+ // Otherwise error in destroy methods when removing Journal Volume - Journal relationship
+ // since the rightMinCardinality constraint would be violated upon deletion
+ isVolumeOfJournal.setRightMinCardinality(0);
+ }
+
+ // Create virtual metadata fields if needed
+ MetadataSchema journalSchema = metadataSchemaService.find(context, "journal");
+ if (journalSchema == null) {
+ journalSchema = metadataSchemaService.create(context, "journal", "journal");
+ }
+ String journalTitleVirtualMdField = "journal.title";
+ MetadataField journalTitleField = metadataFieldService.findByString(context, journalTitleVirtualMdField, '.');
+ if (journalTitleField == null) {
+ metadataFieldService.create(context, journalSchema, "title", null, "Journal Title");
+ }
+
+ String journalTitle = "Journal Title Test";
+
+ // Create entity items
+ Item journal =
+ ItemBuilder.createItem(context, col1).withRelationshipType("Journal").withTitle(journalTitle).build();
+ Item journalVolume =
+ ItemBuilder.createItem(context, col1).withRelationshipType("JournalVolume").withTitle("JournalVolume")
+ .build();
+ Item journalIssue =
+ ItemBuilder.createItem(context, col1).withRelationshipType("JournalIssue").withTitle("JournalIssue")
+ .build();
+ Item publication =
+ ItemBuilder.createItem(context, col1).withRelationshipType("Publication").withTitle("Publication").build();
+
+ // Link Publication-Journal Issue
+ RelationshipBuilder.createRelationshipBuilder(context, journalIssue, publication, isPublicationOfJournalIssue)
+ .build();
+ // Link Journal Issue-Journal Volume
+ RelationshipBuilder.createRelationshipBuilder(context, journalVolume, journalIssue, isIssueOfJournalVolume)
+ .build();
+ mockSolrSearchCore.getSolr().commit(false, false);
+
+ // Verify Publication item via REST does not contain virtual md journal.title
+ getClient().perform(get("/api/core/items/" + publication.getID()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.metadata." + journalTitleVirtualMdField).doesNotExist());
+
+ // Verify Publication item via Solr does not contain virtual md journal.title
+ SolrQuery solrQuery = new SolrQuery();
+ solrQuery.setQuery("search.resourceid:" + publication.getID());
+ QueryResponse queryResponse = mockSolrSearchCore.getSolr().query(solrQuery);
+ assertThat(queryResponse.getResults().size(), equalTo(1));
+ assertNull(queryResponse.getResults().get(0).getFieldValues(journalTitleVirtualMdField));
+
+ // Link Journal Volume - Journal
+ RelationshipBuilder.createRelationshipBuilder(context, journal, journalVolume, isVolumeOfJournal).build();
+ mockSolrSearchCore.getSolr().commit(false, false);
+
+ // Verify Publication item via REST does contain virtual md journal.title
+ getClient().perform(get("/api/core/items/" + publication.getID()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.metadata", allOf(
+ matchMetadata(journalTitleVirtualMdField, journalTitle))));
+
+ // Verify Publication item via Solr contains virtual md journal.title
+ queryResponse = mockSolrSearchCore.getSolr().query(solrQuery);
+ assertThat(queryResponse.getResults().size(), equalTo(1));
+ assertEquals(journalTitle,
+ ((List) queryResponse.getResults().get(0).getFieldValues(journalTitleVirtualMdField)).get(0));
+
+ // Verify Journal Volume item via REST also contains virtual md journal.title
+ getClient().perform(get("/api/core/items/" + journalVolume.getID()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.metadata", allOf(
+ matchMetadata(journalTitleVirtualMdField, journalTitle))));
+
+ // Verify Journal Volume item via Solr also contains virtual md journal.title
+ solrQuery.setQuery("search.resourceid:" + journalVolume.getID());
+ queryResponse = mockSolrSearchCore.getSolr().query(solrQuery);
+ assertThat(queryResponse.getResults().size(), equalTo(1));
+ assertEquals(journalTitle,
+ ((List) queryResponse.getResults().get(0).getFieldValues(journalTitleVirtualMdField)).get(0));
+
+ // Verify Journal Issue item via REST also contains virtual md journal.title
+ getClient().perform(get("/api/core/items/" + journalIssue.getID()))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.metadata", allOf(
+ matchMetadata(journalTitleVirtualMdField, journalTitle))));
+
+ // Verify Journal Issue item via Solr also contains virtual md journal.title
+ solrQuery.setQuery("search.resourceid:" + journalIssue.getID());
+ queryResponse = mockSolrSearchCore.getSolr().query(solrQuery);
+ assertThat(queryResponse.getResults().size(), equalTo(1));
+ assertEquals(journalTitle,
+ ((List) queryResponse.getResults().get(0).getFieldValues(journalTitleVirtualMdField)).get(0));
+
+ context.restoreAuthSystemState();
+ }
}
diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java
index 66938e5991..f89ab3869d 100644
--- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java
+++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionFormsControllerIT.java
@@ -11,6 +11,7 @@ import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
@@ -591,6 +592,47 @@ public class SubmissionFormsControllerIT extends AbstractControllerIntegrationTe
resetLocalesConfiguration();
}
+ @Test
+ public void multipleExternalSourcesTest() throws Exception {
+ String token = getAuthToken(admin.getEmail(), password);
+
+ getClient(token).perform(get("/api/config/submissionforms/traditionalpageone"))
+ //The status has to be 200 OK
+ .andExpect(status().isOk())
+ //We expect the content type to be "application/hal+json;charset=UTF-8"
+ .andExpect(content().contentType(contentType))
+ //Check that the JSON root matches the expected "traditionalpageone" input forms
+ .andExpect(jsonPath("$.id", is("traditionalpageone")))
+ .andExpect(jsonPath("$.name", is("traditionalpageone")))
+ .andExpect(jsonPath("$.type", is("submissionform")))
+ .andExpect(jsonPath("$._links.self.href", Matchers
+ .startsWith(REST_SERVER_URL + "config/submissionforms/traditionalpageone")))
+ // check the external sources of the first field in the first row
+ .andExpect(jsonPath("$.rows[0].fields[0].selectableRelationship.externalSources",
+ contains(is("orcid"), is("my_staff_db"))))
+ ;
+ }
+
+ @Test
+ public void noExternalSourcesTest() throws Exception {
+ String token = getAuthToken(admin.getEmail(), password);
+
+ getClient(token).perform(get("/api/config/submissionforms/journalVolumeStep"))
+ //The status has to be 200 OK
+ .andExpect(status().isOk())
+ //We expect the content type to be "application/hal+json;charset=UTF-8"
+ .andExpect(content().contentType(contentType))
+ //Check that the JSON root matches the expected "journalVolumeStep" input forms
+ .andExpect(jsonPath("$.id", is("journalVolumeStep")))
+ .andExpect(jsonPath("$.name", is("journalVolumeStep")))
+ .andExpect(jsonPath("$.type", is("submissionform")))
+ .andExpect(jsonPath("$._links.self.href", Matchers
+ .startsWith(REST_SERVER_URL + "config/submissionforms/journalVolumeStep")))
+ // check the external sources of the first field in the first row
+ .andExpect(jsonPath("$.rows[0].fields[0].selectableRelationship.externalSources", nullValue()))
+ ;
+ }
+
private void resetLocalesConfiguration() throws DCInputsReaderException {
configurationService.setProperty("default.locale","en");
configurationService.setProperty("webui.supported.locales",null);
diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/URLUtilsTest.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/URLUtilsTest.java
new file mode 100644
index 0000000000..a2c1822d73
--- /dev/null
+++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/utils/URLUtilsTest.java
@@ -0,0 +1,74 @@
+/**
+ * The contents of this file are subject to the license and copyright
+ * detailed in the LICENSE and NOTICE files at the root of the source
+ * tree and available online at
+ *
+ * http://www.dspace.org/license/
+ */
+package org.dspace.app.rest.utils;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * @author mwood
+ */
+public class URLUtilsTest {
+
+ public URLUtilsTest() {
+ }
+
+ /**
+ * Test of decode method, of class URLUtils.
+ */
+ @Ignore
+ @Test
+ public void testDecode() {
+ }
+
+ /**
+ * Test of encode method, of class URLUtils.
+ */
+ @Ignore
+ @Test
+ public void testEncode() {
+ }
+
+ /**
+ * Test of urlIsPrefixOf method, of class URLUtils.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ @SuppressWarnings("UnusedAssignment")
+ public void testUrlIsPrefixOf() {
+ boolean isPrefix;
+
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com/path", "http://example.com/path");
+ assertTrue("Should match if all is equal", isPrefix);
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com:80/test", "http://example.com:80/test/1");
+ assertTrue("Should match if pattern path is longer", isPrefix);
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com:80/test", "http://example.com/test");
+ assertTrue("Should match if missing port matches default", isPrefix);
+
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com/path", "https://example.com/path");
+ assertFalse("Should not match if protocols don't match", isPrefix);
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com/", "http://oops.example.com/");
+ assertFalse("Should not match if hosts don't match", isPrefix);
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com:80/", "http://example.com:8080/");
+ assertFalse("Should not match if ports don't match", isPrefix);
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com/path1/a", "http://example.com/path2/a");
+ assertFalse("Should not match if paths don't match", isPrefix);
+
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com/path", "http://example.com/path/");
+ assertTrue("Should match with, without trailing slash", isPrefix);
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com/path1", "http://example.com/path2");
+ assertFalse("Should not match if paths don't match", isPrefix);
+ isPrefix = URLUtils.urlIsPrefixOf("http://example.com/path", "http://example.com/path2/sub");
+ assertFalse("Should not match if interior path elements don't match", isPrefix);
+
+ // Check if a malformed URL raises an exception
+ isPrefix = URLUtils.urlIsPrefixOf(null, "http://example.com/");
+ }
+}
diff --git a/dspace-server-webapp/src/test/resources/Messages_pl.properties b/dspace-server-webapp/src/test/resources/Messages_pl.properties
new file mode 100644
index 0000000000..9f2db2e0da
--- /dev/null
+++ b/dspace-server-webapp/src/test/resources/Messages_pl.properties
@@ -0,0 +1,13 @@
+#
+# 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/
+#
+
+# User exposed error messages
+org.dspace.app.rest.exception.RESTEmptyWorkflowGroupException.message = [PL] Refused to delete user {0} because it is the only member of the \
+ workflow group {1}. Delete the tasks and group first if you want to remove this user.
+org.dspace.app.rest.exception.EPersonNameNotProvidedException.message = [PL] The eperson.firstname and eperson.lastname values need to be filled in
+org.dspace.app.rest.exception.GroupNameNotProvidedException.message = [PL] Cannot create group, no group name is provided
diff --git a/dspace/config/modules/relationship.cfg b/dspace/config/modules/relationship.cfg
new file mode 100644
index 0000000000..8272bfee1f
--- /dev/null
+++ b/dspace/config/modules/relationship.cfg
@@ -0,0 +1,16 @@
+#---------------------------------------------------------------#
+#-----------------RELATIONSHIP CONFIGURATIONS-------------------#
+#---------------------------------------------------------------#
+# Configuration properties used by the RelationshipService #
+#---------------------------------------------------------------#
+
+# The maximum number of items to be updated when adjusting a relationship.
+# This includes the relationship’s left and right item.
+# If the max is below 2, the relationship’s left and right item will still be processed. Defaults to 20
+# relationship.update.relateditems.max = 20
+
+# The maximum depth of relationships to traverse.
+# A value of 5 means that a maximum of 5 levels (relationships) deep will be scanned for updates on both the left side
+# and the right side. Indirectly related items requiring more than 5 items will be skipped. Defaults to 5
+# relationship.update.relateditems.maxdepth = 5
+
diff --git a/dspace/config/submission-forms.dtd b/dspace/config/submission-forms.dtd
index 6f277ce208..d8acf3a8b0 100644
--- a/dspace/config/submission-forms.dtd
+++ b/dspace/config/submission-forms.dtd
@@ -7,19 +7,19 @@
-
+
-
+
-
+
@@ -40,7 +40,7 @@
-
+
@@ -49,16 +49,17 @@
-
+
-
+
-
+
+
diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml
index 9729fb74c5..266ee3c8ba 100644
--- a/dspace/config/submission-forms.xml
+++ b/dspace/config/submission-forms.xml
@@ -31,7 +31,7 @@
oneboxEnter the name of the file.
- You must enter a main title for this item.
+ You must enter a name for this file
@@ -54,14 +54,19 @@
persontrue
- Add an author
+ Enter the author's name (Family name, Given names).dccontributorauthor
- name
+ onebox
- At least one author (plain text or relationship) is required
+ orcid
+
+
+
@@ -74,7 +79,6 @@
oneboxEnter the main title of the item.You must enter a main title for this item.
-
@@ -99,8 +103,7 @@
datePlease give the date of previous publication or public distribution.
- You can leave out the day and/or month if they aren't
- applicable.
+ You can leave out the day and/or month if they aren't applicable.
You must enter at least the year.
@@ -241,6 +244,15 @@
-
+