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 @@ author name + orcid,my_staff_db @@ -242,6 +243,7 @@ it, please enter the types and the actual numbers or codes. creativework.publisher:somepublishername Select 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 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 @@ onebox Enter 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 @@ person true - Add an author + Enter the author's name (Family name, Given names). dc contributor author - name + onebox - At least one author (plain text or relationship) is required + orcid + + + @@ -74,7 +79,6 @@ onebox Enter the main title of the item. You must enter a main title for this item. - @@ -99,8 +103,7 @@ date Please 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 @@
+ + + isPublicationOfAuthor + publication + + import a publicaton + pubmed + + person @@ -494,6 +506,7 @@ creativework.publisher:somepublishername Select the journal related to this volume. + sherpaJournal @@ -625,6 +638,7 @@ author name + orcid At least one author (plain text or relationship) is required @@ -852,7 +866,7 @@ - +