Merge pull request #2590 from atmire/feature-external-sources-entities-post

Feature: Creating an archived item from an external source
This commit is contained in:
Tim Donohue
2019-12-05 14:52:42 -06:00
committed by GitHub
7 changed files with 527 additions and 6 deletions

View File

@@ -7,9 +7,14 @@
*/
package org.dspace.external.service;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.external.provider.ExternalDataProvider;
@@ -59,4 +64,17 @@ public interface ExternalDataService {
* @return The total amount of results that can be returned for this query in the given source
*/
public int getNumberOfResults(String source, String query);
/**
* This method will create an Item in the given Collection based on the given ExternalDataObject.
* Note that this Item will be Archived
* @param context The relevant DSpace context
* @param externalDataObject The relevant ExternalDataObject to be used
* @param collection The Collection in which the item will be present
* @return The created Item
* @throws AuthorizeException If something goes wrong
* @throws SQLException If something goes wrong
*/
Item createItemFromExternalDataObject(Context context, ExternalDataObject externalDataObject, Collection collection)
throws AuthorizeException, SQLException;
}

View File

@@ -7,9 +7,22 @@
*/
package org.dspace.external.service.impl;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import org.apache.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.dto.MetadataValueDTO;
import org.dspace.content.service.InstallItemService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.WorkspaceItemService;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.external.provider.ExternalDataProvider;
import org.dspace.external.service.ExternalDataService;
@@ -20,9 +33,23 @@ import org.springframework.beans.factory.annotation.Autowired;
*/
public class ExternalDataServiceImpl implements ExternalDataService {
private static final Logger log = Logger.getLogger(ExternalDataServiceImpl.class);
@Autowired
private List<ExternalDataProvider> externalDataProviders;
@Autowired
private ItemService itemService;
@Autowired
private WorkspaceItemService workspaceItemService;
@Autowired
private InstallItemService installItemService;
@Autowired
private AuthorizeService authorizeService;
@Override
public Optional<ExternalDataObject> getExternalDataObject(String source, String id) {
ExternalDataProvider provider = getExternalDataProvider(source);
@@ -65,4 +92,27 @@ public class ExternalDataServiceImpl implements ExternalDataService {
}
return provider.getNumberOfResults(query);
}
@Override
public Item createItemFromExternalDataObject(Context context, ExternalDataObject externalDataObject,
Collection collection)
throws AuthorizeException, SQLException {
if (!authorizeService.isAdmin(context)) {
throw new AuthorizeException("You have to be an admin to create an Item from an ExternalDataObject");
}
WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, true);
Item item = workspaceItem.getItem();
for (MetadataValueDTO metadataValueDTO : externalDataObject.getMetadata()) {
itemService.addMetadata(context, item, metadataValueDTO.getSchema(), metadataValueDTO.getElement(),
metadataValueDTO.getQualifier(), metadataValueDTO.getLanguage(),
metadataValueDTO.getValue(), metadataValueDTO.getAuthority(),
metadataValueDTO.getConfidence());
}
log.info(LogManager.getHeader(context, "create_item_from_externalDataObject", "Created item" +
"with id: " + item.getID() + " from source: " + externalDataObject.getSource() + " with identifier: " +
externalDataObject.getId()));
return installItemService.installItem(context, workspaceItem);
}
}

View File

@@ -30,6 +30,7 @@ import org.dspace.app.rest.model.BundleRest;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.model.patch.Patch;
import org.dspace.app.rest.projection.Projection;
import org.dspace.app.rest.repository.handler.service.UriListHandlerService;
import org.dspace.app.rest.repository.patch.ItemPatch;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bundle;
@@ -93,6 +94,10 @@ public class ItemRestRepository extends DSpaceObjectRestRepository<Item, ItemRes
@Autowired
RelationshipTypeService relationshipTypeService;
@Autowired
private UriListHandlerService uriListHandlerService;
public ItemRestRepository(ItemService dsoService, ItemPatch dsoPatch) {
super(dsoService, dsoPatch);
}
@@ -137,7 +142,7 @@ public class ItemRestRepository extends DSpaceObjectRestRepository<Item, ItemRes
@Override
protected void updateDSpaceObject(Item item, ItemRest itemRest)
throws AuthorizeException, SQLException {
throws AuthorizeException, SQLException {
super.updateDSpaceObject(item, itemRest);
Context context = obtainContext();
@@ -175,11 +180,11 @@ public class ItemRestRepository extends DSpaceObjectRestRepository<Item, ItemRes
}
if (itemService.isInProgressSubmission(context, item)) {
throw new UnprocessableEntityException("The item cannot be deleted. "
+ "It's part of a in-progress submission.");
+ "It's part of a in-progress submission.");
}
if (item.getTemplateItemOf() != null) {
throw new UnprocessableEntityException("The item cannot be deleted. "
+ "It's a template for a collection");
+ "It's a template for a collection");
}
} catch (SQLException e) {
throw new RuntimeException(e.getMessage(), e);
@@ -293,7 +298,7 @@ public class ItemRestRepository extends DSpaceObjectRestRepository<Item, ItemRes
Collection collection = collectionService.find(context, owningCollectionUuid);
if (collection == null) {
throw new DSpaceBadRequestException("The given owningCollection parameter is invalid: "
+ owningCollectionUuid);
+ owningCollectionUuid);
}
WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, false);
Item item = workspaceItem.getItem();
@@ -312,7 +317,7 @@ public class ItemRestRepository extends DSpaceObjectRestRepository<Item, ItemRes
@PreAuthorize("hasPermission(#uuid, 'ITEM', 'WRITE')")
protected ItemRest put(Context context, HttpServletRequest request, String apiCategory, String model, UUID uuid,
JsonNode jsonNode)
throws RepositoryMethodNotImplementedException, SQLException, AuthorizeException {
throws RepositoryMethodNotImplementedException, SQLException, AuthorizeException {
HttpServletRequest req = getRequestService().getCurrentRequest().getHttpServletRequest();
ObjectMapper mapper = new ObjectMapper();
ItemRest itemRest = null;
@@ -359,7 +364,6 @@ public class ItemRestRepository extends DSpaceObjectRestRepository<Item, ItemRes
context.commit();
return bundle;
}
/**
@@ -400,4 +404,13 @@ public class ItemRestRepository extends DSpaceObjectRestRepository<Item, ItemRes
collectionService.removeTemplateItem(context, collection);
collectionService.update(context, collection);
}
@Override
protected ItemRest createAndReturn(Context context, List<String> stringList)
throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException {
HttpServletRequest req = getRequestService().getCurrentRequest().getHttpServletRequest();
Item item = uriListHandlerService.handle(context, req, stringList, Item.class);
return converter.toRest(item, Projection.DEFAULT);
}
}

View File

@@ -0,0 +1,142 @@
/**
* 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.repository.handler;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.model.ExternalSourceRest;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.service.CollectionService;
import org.dspace.core.Context;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.external.service.ExternalDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* This class will handle ExternalSourceEntryUriList and it'll create Item objects based on them
*/
@Component
public class ExternalSourceEntryUriListHandler implements UriListHandler<Item> {
private List<RequestMethod> allowedRequestMethods = new LinkedList<>(Arrays.asList(RequestMethod.POST));
@Autowired
private AuthorizeService authorizeService;
@Autowired
private ExternalDataService externalDataService;
@Autowired
private CollectionService collectionService;
private static final Logger log = org.apache.logging.log4j.LogManager
.getLogger(ExternalSourceEntryUriListHandler.class);
@Override
public boolean supports(List<String> uriList, String method) {
if (!allowedRequestMethods.contains(RequestMethod.valueOf(method))) {
return false;
}
for (String string : uriList) {
if (!(StringUtils.contains(string, ExternalSourceRest.CATEGORY + "/" + ExternalSourceRest.PLURAL_NAME) &&
StringUtils.contains(string, "entryValues"))) {
return false;
}
}
return true;
}
@Override
public boolean validate(Context context, HttpServletRequest request, List<String> uriList,
Class clazz) throws AuthorizeException {
if (uriList.size() > 1) {
return false;
}
if (clazz != Item.class) {
return false;
}
String owningCollectionString = request.getParameter("owningCollection");
if (StringUtils.isBlank(owningCollectionString)) {
return false;
}
try {
if (!authorizeService.isAdmin(context)) {
throw new AuthorizeException("Only admins are allowed to create items using external data");
}
Collection collection = collectionService.find(context, UUID.fromString(owningCollectionString));
if (collection == null) {
return false;
}
} catch (SQLException e) {
log.error("Search for owningCollection with UUID:" + owningCollectionString + " resulted in an error",
e);
return false;
}
return true;
}
@Override
public Item handle(Context context, HttpServletRequest request, List<String> uriList)
throws SQLException, AuthorizeException {
ExternalDataObject dataObject = getExternalDataObjectFromUriList(uriList);
String owningCollectionUuid = request.getParameter("owningCollection");
Collection collection = null;
Item item = null;
try {
collection = collectionService.find(context, UUID.fromString(owningCollectionUuid));
item = externalDataService.createItemFromExternalDataObject(context, dataObject, collection);
} catch (AuthorizeException | SQLException e) {
log.error("An error occured when trying to create item in collection with uuid: " + owningCollectionUuid,
e);
throw e;
}
return item;
}
/**
* This method will take the first (and only, verified in the validate method) uriList string from the list
* and it'll perform regex logic to get the AuthorityName and ID parameters from it to then retrieve
* an ExternalDataObject from the service
* @param uriList The list of UriList strings to be parsed
* @return The appropriate ExternalDataObject
*/
private ExternalDataObject getExternalDataObjectFromUriList(List<String> uriList) {
String inputString = uriList.get(0);
Pattern pattern = Pattern.compile("api\\/integration\\/externalsources\\/(.*)\\/entryValues\\/(.*)");
Matcher matcher = pattern.matcher(inputString);
matcher.find();
String externalSourceIdentifer = matcher.group(1);
String id = matcher.group(2);
Optional<ExternalDataObject> externalDataObject = externalDataService
.getExternalDataObject(externalSourceIdentifer, id);
return externalDataObject.orElseThrow(() -> new ResourceNotFoundException(
"Couldn't find an ExternalSource for source: " + externalSourceIdentifer + " and ID: " + id));
}
}

View File

@@ -0,0 +1,53 @@
/**
* 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.repository.handler;
import java.sql.SQLException;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
/**
* This is an interface to be implemented by classes that want to handle a UriList call
* @param <T> the class of the object that's returned by the handle method
*/
public interface UriListHandler<T> {
/**
* This method will take the UriList and method as input and verify whether the implementing UriListHandler
* can handle this input or not
* @param uriList The list of UriList Strings to be checked if they're supported
* @param method The request method to be checked if it's supported
* @return A boolean indicating whether the implementing UriListHandler can handle this input
*/
boolean supports(List<String> uriList, String method);
/**
* This method will take all the required input and validate them to see if there are any issues before
* calling the handle method
* @param context The relevant DSpace context
* @param request The current request
* @param uriList The list of UriList Strings
* @param clazz The class to be returned by the handle method
* @return A boolean indicating whether all this input is valid for the implementing UriListHandler
*/
boolean validate(Context context, HttpServletRequest request, List<String> uriList, Class clazz)
throws AuthorizeException;
/**
* This method will perform the actual handle logic
* @param context The relevant DSpace context
* @param request The current request
* @param uriList The list of UriList Strings
* @return The object of class T that was handled
*/
T handle(Context context, HttpServletRequest request, List<String> uriList) throws SQLException, AuthorizeException;
}

View File

@@ -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.repository.handler.service;
import java.sql.SQLException;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.dspace.app.rest.exception.DSpaceBadRequestException;
import org.dspace.app.rest.repository.handler.UriListHandler;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* This class is a wrapper Service class for the {@link UriListHandler} objects. It will find the right one and try to
* execute it for the given arguments
*/
@Component
public class UriListHandlerService {
@Autowired
private List<UriListHandler> uriListHandlers;
/**
* This method will take the UriList, the request, relevant DSpace context and the class of the object to be handled
* It'll then loop over all the UriListHandlers defined within the codebase and first check if it supports the given
* method and the urilist. If the handler supports this, it'll then validate the input and only if it's valid, it'll
* execute the handle method and perform the logic
*
* @param context The relevant DSpace context
* @param request The current active Request
* @param uriList The list of Strings representing the UriList to be handled
* @param clazz The class to be hadled
* @param <T> The class to be returned, same as the class parameter above
* @return The object that was handled through this method
*/
public <T> T handle(Context context, HttpServletRequest request, List<String> uriList, Class<T> clazz)
throws SQLException, AuthorizeException {
// Loop all the uriListHandlers
for (UriListHandler uriListHandler : uriListHandlers) {
// Does the class support the given uri list and the request method
if (uriListHandler.supports(uriList, request.getMethod())) {
// Can the class handle the given uri list and can the given class, params and authorization be handled
if (uriListHandler.validate(context, request, uriList, clazz)) {
// If all these things succeed, call handle
return (T) uriListHandler.handle(context, request, uriList);
} else {
throw new DSpaceBadRequestException("The input given to the UriListHandler was invalid");
}
}
}
throw new DSpaceBadRequestException("No UriListHandler was found that supports the inputs given");
}
}

View File

@@ -1906,5 +1906,187 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
.andExpect(status().isForbidden());
}
@Test
public void createItemFromExternalSources() 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();
context.restoreAuthSystemState();
ObjectMapper mapper = new ObjectMapper();
String token = getAuthToken(admin.getEmail(), password);
MvcResult mvcResult = getClient(token).perform(post("/api/core/items?owningCollection="
+ col1.getID().toString())
.contentType(org.springframework.http.MediaType.parseMediaType(
org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE))
.content("https://localhost:8080/server/api/integration/externalsources/" +
"mock/entryValues/one")).andExpect(status().isCreated()).andReturn();
String content = mvcResult.getResponse().getContentAsString();
Map<String,Object> map = mapper.readValue(content, Map.class);
String itemUuidString = String.valueOf(map.get("uuid"));
String itemHandleString = String.valueOf(map.get("handle"));
getClient(token).perform(get("/api/core/items/" + itemUuidString))
.andExpect(status().isOk())
.andExpect(jsonPath("$", Matchers.allOf(
hasJsonPath("$.id", is(itemUuidString)),
hasJsonPath("$.uuid", is(itemUuidString)),
hasJsonPath("$.handle", is(itemHandleString)),
hasJsonPath("$.type", is("item")),
hasJsonPath("$.metadata", Matchers.allOf(
MetadataMatcher.matchMetadata("dc.contributor.author", "Donald, Smith")
)))));
}
@Test
public void createItemFromExternalSourcesNoOwningCollectionUuidBadRequest() throws Exception {
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(post("/api/core/items")
.contentType(org.springframework.http.MediaType.parseMediaType(
org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE))
.content("https://localhost:8080/server/api/integration/externalsources/" +
"mock/entryValues/one")).andExpect(status().isBadRequest()).andReturn();
}
@Test
public void createItemFromExternalSourcesRandomOwningCollectionUuidBadRequest() throws Exception {
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(post("/api/core/items?owningCollection=" + UUID.randomUUID())
.contentType(org.springframework.http.MediaType.parseMediaType(
org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE))
.content("https://localhost:8080/server/api/integration/externalsources/" +
"mock/entryValues/one")).andExpect(status().isBadRequest()).andReturn();
}
@Test
public void createItemFromExternalSourcesWrongUriList() 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();
context.restoreAuthSystemState();
ObjectMapper mapper = new ObjectMapper();
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(post("/api/core/items?owningCollection=" + col1.getID().toString())
.contentType(org.springframework.http.MediaType.parseMediaType(
org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE))
.content("https://localhost:8080/server/mock/mock/mock/" +
"mock/entryValues/one")).andExpect(status().isBadRequest());
}
@Test
public void createItemFromExternalSourcesWrongSourceBadRequest() 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();
context.restoreAuthSystemState();
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(post("/api/core/items?owningCollection=" + col1.getID().toString())
.contentType(org.springframework.http.MediaType.parseMediaType(
org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE))
.content("https://localhost:8080/server/api/integration/externalsources/" +
"mockWrongSource/entryValues/one")).andExpect(status().isBadRequest());
}
@Test
public void createItemFromExternalSourcesWrongIdResourceNotFound() 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();
context.restoreAuthSystemState();
String token = getAuthToken(admin.getEmail(), password);
getClient(token).perform(post("/api/core/items?owningCollection=" + col1.getID().toString())
.contentType(org.springframework.http.MediaType.parseMediaType(
org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE))
.content("https://localhost:8080/server/api/integration/externalsources/" +
"mock/entryValues/azeazezaezeaz")).andExpect(status().is(404));
}
@Test
public void createItemFromExternalSourcesForbidden() 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();
context.restoreAuthSystemState();
String token = getAuthToken(eperson.getEmail(), password);
getClient(token).perform(post("/api/core/items?owningCollection=" + col1.getID().toString())
.contentType(org.springframework.http.MediaType.parseMediaType(
org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE))
.content("https://localhost:8080/server/api/integration/externalsources/" +
"mock/entryValues/one")).andExpect(status().isForbidden());
}
@Test
public void createItemFromExternalSourcesUnauthorized() 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();
context.restoreAuthSystemState();
getClient().perform(post("/api/core/items?owningCollection=" + col1.getID().toString())
.contentType(org.springframework.http.MediaType.parseMediaType(
org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE))
.content("https://localhost:8080/server/api/integration/externalsources/" +
"mock/entryValues/one")).andExpect(status().isUnauthorized());
}
}