[CST-5306] Migrate Researcher Profile (REST)

This commit is contained in:
Luca Giamminonni
2022-03-30 12:48:54 +02:00
committed by eskander
parent 2e4489e4bb
commit 6a1cdd6e2d
33 changed files with 2969 additions and 2 deletions

View File

@@ -0,0 +1,39 @@
/**
* 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.exception;
/**
* This class provides an exception to be used when a conflict on a resource
* occurs.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class ResourceConflictException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final Object resource;
/**
* Create a ResourceConflictException with a message and the conflicting
* resource.
*
* @param message the error message
* @param resource the resource that caused the conflict
*/
public ResourceConflictException(String message, Object resource) {
super(message);
this.resource = resource;
}
public Object getResource() {
return resource;
}
}

View File

@@ -0,0 +1,85 @@
/**
* 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.profile;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.util.UUIDUtils;
import org.springframework.util.Assert;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
import static org.dspace.core.Constants.READ;
import static org.dspace.eperson.Group.ANONYMOUS;
/**
* Object representing a Researcher Profile.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class ResearcherProfile {
private final Item item;
private final MetadataValue dspaceObjectOwner;
/**
* Create a new ResearcherProfile object from the given item.
*
* @param item the profile item
* @throws IllegalArgumentException if the given item has not a dspace.object.owner
* metadata with a valid authority
*/
public ResearcherProfile(Item item) {
Assert.notNull(item, "A researcher profile requires an item");
this.item = item;
this.dspaceObjectOwner = getDspaceObjectOwnerMetadata(item);
}
public UUID getId() {
return UUIDUtils.fromString(dspaceObjectOwner.getAuthority());
}
public String getFullName() {
return dspaceObjectOwner.getValue();
}
public boolean isVisible() {
return item.getResourcePolicies().stream()
.filter(policy -> policy.getGroup() != null)
.anyMatch(policy -> READ == policy.getAction() && ANONYMOUS.equals(policy.getGroup().getName()));
}
public Item getItem() {
return item;
}
// public Optional<String> getOrcid() {
// return getMetadataValue(item, "person.identifier.orcid")
// .map(metadataValue -> metadataValue.getValue());
// }
private MetadataValue getDspaceObjectOwnerMetadata(Item item) {
return getMetadataValue(item, "dspace.object.owner")
.filter(metadata -> UUIDUtils.fromString(metadata.getAuthority()) != null)
.orElseThrow(() -> new IllegalArgumentException("A profile item must have a valid dspace.object.owner metadata"));
}
private Optional<MetadataValue> getMetadataValue(Item item, String metadataField) {
return getMetadataValues(item, metadataField).findFirst();
}
private Stream<MetadataValue> getMetadataValues(Item item, String metadataField) {
return item.getMetadata().stream()
.filter(metadata -> metadataField.equals(metadata.getMetadataField().toString('.')));
}
}

View File

@@ -0,0 +1,312 @@
/**
* 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.profile;
import static org.dspace.content.authority.Choices.CF_ACCEPTED;
import static org.dspace.core.Constants.READ;
import static org.dspace.eperson.Group.ANONYMOUS;
import java.io.IOException;
import java.net.URI;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import javax.annotation.PostConstruct;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.dspace.app.exception.ResourceConflictException;
import org.dspace.app.profile.service.AfterResearcherProfileCreationAction;
import org.dspace.app.profile.service.ResearcherProfileService;
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.MetadataValue;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.service.CollectionService;
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.discovery.DiscoverQuery;
import org.dspace.discovery.DiscoverResult;
import org.dspace.discovery.IndexableObject;
import org.dspace.discovery.SearchService;
import org.dspace.discovery.SearchServiceException;
import org.dspace.discovery.indexobject.IndexableCollection;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group;
import org.dspace.eperson.service.GroupService;
import org.dspace.services.ConfigurationService;
import org.dspace.util.UUIDUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
/**
* Implementation of {@link ResearcherProfileService}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class ResearcherProfileServiceImpl implements ResearcherProfileService {
private static Logger log = LoggerFactory.getLogger(ResearcherProfileServiceImpl.class);
@Autowired
private ItemService itemService;
@Autowired
private WorkspaceItemService workspaceItemService;
@Autowired
private InstallItemService installItemService;
@Autowired
private ConfigurationService configurationService;
@Autowired
private CollectionService collectionService;
@Autowired
private SearchService searchService;
@Autowired
private GroupService groupService;
@Autowired
private AuthorizeService authorizeService;
@Autowired(required = false)
private List<AfterResearcherProfileCreationAction> afterCreationActions;
@PostConstruct
public void postConstruct() {
if (afterCreationActions == null) {
afterCreationActions = Collections.emptyList();
}
}
@Override
public ResearcherProfile findById(Context context, UUID id) throws SQLException, AuthorizeException {
Assert.notNull(id, "An id must be provided to find a researcher profile");
Item profileItem = findResearcherProfileItemById(context, id);
if (profileItem == null) {
return null;
}
return new ResearcherProfile(profileItem);
}
@Override
public ResearcherProfile createAndReturn(Context context, EPerson ePerson)
throws AuthorizeException, SQLException, SearchServiceException {
Item profileItem = findResearcherProfileItemById(context, ePerson.getID());
if (profileItem != null) {
ResearcherProfile profile = new ResearcherProfile(profileItem);
throw new ResourceConflictException("A profile is already linked to the provided User", profile);
}
Collection collection = findProfileCollection(context);
if (collection == null) {
throw new IllegalStateException("No collection found for researcher profiles");
}
context.turnOffAuthorisationSystem();
Item item = createProfileItem(context, ePerson, collection);
context.restoreAuthSystemState();
ResearcherProfile researcherProfile = new ResearcherProfile(item);
for (AfterResearcherProfileCreationAction afterCreationAction : afterCreationActions) {
afterCreationAction.perform(context, researcherProfile, ePerson);
}
return researcherProfile;
}
@Override
public void deleteById(Context context, UUID id) throws SQLException, AuthorizeException {
Assert.notNull(id, "An id must be provided to find a researcher profile");
Item profileItem = findResearcherProfileItemById(context, id);
if (profileItem == null) {
return;
}
if (isHardDeleteEnabled()) {
deleteItem(context, profileItem);
} else {
removeDspaceObjectOwnerMetadata(context, profileItem);
}
}
@Override
public void changeVisibility(Context context, ResearcherProfile profile, boolean visible)
throws AuthorizeException, SQLException {
if (profile.isVisible() == visible) {
return;
}
Item item = profile.getItem();
Group anonymous = groupService.findByName(context, ANONYMOUS);
if (visible) {
authorizeService.addPolicy(context, item, READ, anonymous);
} else {
authorizeService.removeGroupPolicies(context, item, anonymous);
}
}
@Override
public ResearcherProfile claim(final Context context, final EPerson ePerson, final URI uri)
throws SQLException, AuthorizeException, SearchServiceException {
Item profileItem = findResearcherProfileItemById(context, ePerson.getID());
if (profileItem != null) {
ResearcherProfile profile = new ResearcherProfile(profileItem);
throw new ResourceConflictException("A profile is already linked to the provided User", profile);
}
Collection collection = findProfileCollection(context);
if (collection == null) {
throw new IllegalStateException("No collection found for researcher profiles");
}
final String path = uri.getPath();
final UUID uuid = UUIDUtils.fromString(path.substring(path.lastIndexOf("/") + 1 ));
Item item = itemService.find(context, uuid);
if (Objects.isNull(item) || !item.isArchived() || item.isWithdrawn() || notClaimableEntityType(item)) {
throw new IllegalArgumentException("Provided uri does not represent a valid Item to be claimed");
}
final String existingOwner = itemService.getMetadataFirstValue(item, "dspace", "object",
"owner", null);
if (StringUtils.isNotBlank(existingOwner)) {
throw new IllegalArgumentException("Item with provided uri has already an owner");
}
context.turnOffAuthorisationSystem();
itemService.addMetadata(context, item, "dspace", "object", "owner", null, ePerson.getName(),
ePerson.getID().toString(), CF_ACCEPTED);
context.restoreAuthSystemState();
return new ResearcherProfile(item);
}
private boolean notClaimableEntityType(final Item item) {
final String entityType = itemService.getEntityType(item);
return Arrays.stream(configurationService.getArrayProperty("claimable.entityType"))
.noneMatch(entityType::equals);
}
private Item findResearcherProfileItemById(Context context, UUID id) throws SQLException, AuthorizeException {
String profileType = getProfileType();
Iterator<Item> items = itemService.findByAuthorityValue(context, "dspace", "object", "owner", id.toString());
while (items.hasNext()) {
Item item = items.next();
if (hasEntityTypeMetadataEqualsTo(item, profileType)) {
return item;
}
}
return null;
}
@SuppressWarnings("rawtypes")
private Collection findProfileCollection(Context context) throws SQLException, SearchServiceException {
UUID uuid = UUIDUtils.fromString(configurationService.getProperty("researcher-profile.collection.uuid"));
if (uuid != null) {
return collectionService.find(context, uuid);
}
String profileType = getProfileType();
DiscoverQuery discoverQuery = new DiscoverQuery();
discoverQuery.setDSpaceObjectFilter(IndexableCollection.TYPE);
discoverQuery.addFilterQueries("dspace.entity.type:" + profileType);
DiscoverResult discoverResult = searchService.search(context, discoverQuery);
List<IndexableObject> indexableObjects = discoverResult.getIndexableObjects();
if (CollectionUtils.isEmpty(indexableObjects)) {
return null;
}
if (indexableObjects.size() > 1) {
log.warn("Multiple " + profileType + " type collections were found during profile creation");
return null;
}
return (Collection) indexableObjects.get(0).getIndexedObject();
}
private Item createProfileItem(Context context, EPerson ePerson, Collection collection)
throws AuthorizeException, SQLException {
String id = ePerson.getID().toString();
WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, true);
Item item = workspaceItem.getItem();
itemService.addMetadata(context, item, "dc", "title", null, null, ePerson.getFullName());
itemService.addMetadata(context, item, "dspace", "object", "owner", null, ePerson.getFullName(), id, CF_ACCEPTED);
item = installItemService.installItem(context, workspaceItem);
Group anonymous = groupService.findByName(context, ANONYMOUS);
authorizeService.removeGroupPolicies(context, item, anonymous);
authorizeService.addPolicy(context, item, READ, ePerson);
return item;
}
private boolean hasEntityTypeMetadataEqualsTo(Item item, String entityType) {
return item.getMetadata().stream().anyMatch(metadataValue -> {
return "dspace.entity.type".equals(metadataValue.getMetadataField().toString('.')) &&
entityType.equals(metadataValue.getValue());
});
}
private boolean isHardDeleteEnabled() {
return configurationService.getBooleanProperty("researcher-profile.hard-delete.enabled");
}
private void removeDspaceObjectOwnerMetadata(Context context, Item profileItem) throws SQLException {
List<MetadataValue> metadata = itemService.getMetadata(profileItem, "dspace", "object", "owner", Item.ANY);
itemService.removeMetadataValues(context, profileItem, metadata);
}
private void deleteItem(Context context, Item profileItem) throws SQLException, AuthorizeException {
try {
context.turnOffAuthorisationSystem();
itemService.delete(context, profileItem);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
context.restoreAuthSystemState();
}
}
private String getProfileType() {
return configurationService.getProperty("researcher-profile.type", "Person");
}
}

View File

@@ -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.profile.service;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import java.sql.SQLException;
/**
* Interface to mark classes that allow to perform additional logic on created
* researcher profile.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface AfterResearcherProfileCreationAction {
/**
* Perform some actions on the given researcher profile and returns the updated
* profile.
*
* @param context the DSpace context
* @param researcherProfile the created researcher profile
* @param owner the EPerson that is owner of the given profile
* @throws SQLException if a SQL error occurs
*/
void perform(Context context, ResearcherProfile researcherProfile, EPerson owner) throws SQLException;
}

View File

@@ -0,0 +1,86 @@
/**
* 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.profile.service;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.dspace.discovery.SearchServiceException;
import org.dspace.eperson.EPerson;
import java.net.URI;
import java.sql.SQLException;
import java.util.UUID;
/**
* Service interface class for the {@link ResearcherProfile} object. The
* implementation of this class is responsible for all business logic calls for
* the {@link ResearcherProfile} object.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface ResearcherProfileService {
/**
* Find the ResearcherProfile by UUID.
*
* @param context the relevant DSpace Context.
* @param id the ResearcherProfile id
* @return the found ResearcherProfile
* @throws SQLException
* @throws AuthorizeException
*/
public ResearcherProfile findById(Context context, UUID id) throws SQLException, AuthorizeException;
/**
* Create a new researcher profile for the given ePerson.
*
* @param context the relevant DSpace Context.
* @param ePerson the ePerson
* @return the created profile
* @throws SQLException
* @throws AuthorizeException
* @throws SearchServiceException
*/
public ResearcherProfile createAndReturn(Context context, EPerson ePerson)
throws AuthorizeException, SQLException, SearchServiceException;
/**
* Removes the association between the researcher profile and eperson related to
* the input uuid.
*
* @param context the relevant DSpace Context.
* @param id the researcher profile id
* @throws AuthorizeException
* @throws SQLException
*/
public void deleteById(Context context, UUID id) throws SQLException, AuthorizeException;
/**
* Changes the visibility of the given profile using the given new visible value
*
* @param context the relevant DSpace Context.
* @param profile the researcher profile to update
* @param visible the visible value to set
* @throws SQLException
* @throws AuthorizeException
*/
public void changeVisibility(Context context, ResearcherProfile profile, boolean visible)
throws AuthorizeException, SQLException;
/**
* Claims and links an eperson to an existing DSpaceObject
* @param context the relevant DSpace Context.
* @param ePerson the ePerson
* @param uri uri of existing DSpaceObject to be linked to the eperson
* @return
*/
ResearcherProfile claim(Context context, EPerson ePerson, URI uri)
throws SQLException, AuthorizeException, SearchServiceException;
}

View File

@@ -959,4 +959,8 @@ public class AuthorizeServiceImpl implements AuthorizeService {
return query + " AND "; return query + " AND ";
} }
} }
@Override
public boolean isPartOfTheGroup(Context c, String egroup) throws SQLException {
return false;
}
} }

View File

@@ -592,4 +592,5 @@ public interface AuthorizeService {
*/ */
long countAdminAuthorizedCollection(Context context, String query) long countAdminAuthorizedCollection(Context context, String query)
throws SearchServiceException, SQLException; throws SearchServiceException, SQLException;
public boolean isPartOfTheGroup(Context c, String egroup) throws SQLException;
} }

View File

@@ -1131,6 +1131,50 @@ prevent the generation of resource policy entry values with null dspace_object a
return !(hasCustomPolicy && isAnonimousGroup && datesAreNull); return !(hasCustomPolicy && isAnonimousGroup && datesAreNull);
} }
/**
* Returns an iterator of Items possessing the passed metadata field, or only
* those matching the passed value, if value is not Item.ANY
*
* @param context DSpace context object
* @param schema metadata field schema
* @param element metadata field element
* @param qualifier metadata field qualifier
* @param value field value or Item.ANY to match any value
* @return an iterator over the items matching that authority value
* @throws SQLException if database error
* An exception that provides information on a database access error or other errors.
* @throws AuthorizeException if authorization error
* Exception indicating the current user of the context does not have permission
* to perform a particular action.
*/
@Override
public Iterator<Item> findArchivedByMetadataField(Context context,
String schema, String element, String qualifier, String value)
throws SQLException, AuthorizeException {
MetadataSchema mds = metadataSchemaService.find(context, schema);
if (mds == null) {
throw new IllegalArgumentException("No such metadata schema: " + schema);
}
MetadataField mdf = metadataFieldService.findByElement(context, mds, element, qualifier);
if (mdf == null) {
throw new IllegalArgumentException(
"No such metadata field: schema=" + schema + ", element=" + element + ", qualifier=" + qualifier);
}
if (Item.ANY.equals(value)) {
return itemDAO.findByMetadataField(context, mdf, null, true);
} else {
return itemDAO.findByMetadataField(context, mdf, value, true);
}
}
@Override
public Iterator<Item> findArchivedByMetadataField(Context context, String metadataField, String value)
throws SQLException, AuthorizeException {
String[] mdValueByField = getMDValueByField(metadataField);
return findArchivedByMetadataField(context, mdValueByField[0], mdValueByField[1], mdValueByField[2], value);
}
/** /**
* Returns an iterator of Items possessing the passed metadata field, or only * Returns an iterator of Items possessing the passed metadata field, or only
* those matching the passed value, if value is not Item.ANY * those matching the passed value, if value is not Item.ANY
@@ -1535,5 +1579,9 @@ prevent the generation of resource policy entry values with null dspace_object a
.stream().findFirst().orElse(null); .stream().findFirst().orElse(null);
} }
@Override
public String getEntityType(Item item) {
return getMetadataFirstValue(item, new MetadataFieldName("dspace.entity.type"), Item.ANY);
}
} }

View File

@@ -0,0 +1,93 @@
/**
* 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.content.authority;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.apache.log4j.Logger;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService;
import org.dspace.util.UUIDUtils;
/**
*
* @author Mykhaylo Boychuk (4science.it)
*/
public class EPersonAuthority implements ChoiceAuthority {
private static final Logger log = Logger.getLogger(EPersonAuthority.class);
/**
* the name assigned to the specific instance by the PluginService, @see
* {@link NameAwarePlugin}
**/
private String authorityName;
private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService();
@Override
public Choices getBestMatch(String text, String locale) {
return getMatches(text, 0, 2, locale);
}
@Override
public Choices getMatches(String text, int start, int limit, String locale) {
Context context = null;
if (limit <= 0) {
limit = 20;
}
context = new Context();
List<EPerson> ePersons = null;
try {
ePersons = ePersonService.search(context, text, start, limit);
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e.getMessage(), e);
}
List<Choice> choiceList = new ArrayList<Choice>();
for (EPerson eperson : ePersons) {
choiceList.add(new Choice(eperson.getID().toString(), eperson.getFullName(), eperson.getFullName()));
}
Choice[] results = new Choice[choiceList.size()];
results = choiceList.toArray(results);
return new Choices(results, start, ePersons.size(), Choices.CF_AMBIGUOUS, ePersons.size() > (start + limit), 0);
}
@Override
public String getLabel(String key, String locale) {
UUID uuid = UUIDUtils.fromString(key);
if (uuid == null) {
return null;
}
Context context = new Context();
try {
EPerson ePerson = ePersonService.find(context, uuid);
return ePerson != null ? ePerson.getFullName() : null;
} catch (SQLException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
public String getPluginInstanceName() {
return authorityName;
}
@Override
public void setPluginInstanceName(String name) {
this.authorityName = name;
}
}

View File

@@ -579,6 +579,36 @@ public interface ItemService
*/ */
public boolean canCreateNewVersion(Context context, Item item) throws SQLException; public boolean canCreateNewVersion(Context context, Item item) throws SQLException;
/**
* Returns an iterator of in archive items possessing the passed metadata field, or only
* those matching the passed value, if value is not Item.ANY
*
* @param context DSpace context object
* @param schema metadata field schema
* @param element metadata field element
* @param qualifier metadata field qualifier
* @param value field value or Item.ANY to match any value
* @return an iterator over the items matching that authority value
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public Iterator<Item> findArchivedByMetadataField(Context context, String schema, String element,
String qualifier, String value) throws SQLException, AuthorizeException;
/**
* Returns an iterator of in archive items possessing the passed metadata field, or only
* those matching the passed value, if value is not Item.ANY
*
* @param context DSpace context object
* @param metadataField metadata
* @param value field value or Item.ANY to match any value
* @return an iterator over the items matching that authority value
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public Iterator<Item> findArchivedByMetadataField(Context context, String metadataField, String value)
throws SQLException, AuthorizeException;
/** /**
* Returns an iterator of Items possessing the passed metadata field, or only * Returns an iterator of Items possessing the passed metadata field, or only
* those matching the passed value, if value is not Item.ANY * those matching the passed value, if value is not Item.ANY
@@ -618,7 +648,7 @@ public interface ItemService
*/ */
public Iterator<Item> findByAuthorityValue(Context context, public Iterator<Item> findByAuthorityValue(Context context,
String schema, String element, String qualifier, String value) String schema, String element, String qualifier, String value)
throws SQLException, AuthorizeException, IOException; throws SQLException, AuthorizeException;
public Iterator<Item> findByMetadataFieldAuthority(Context context, String mdString, String authority) public Iterator<Item> findByMetadataFieldAuthority(Context context, String mdString, String authority)
@@ -782,5 +812,12 @@ public interface ItemService
*/ */
public List<MetadataValue> getMetadata(Item item, String schema, String element, String qualifier, public List<MetadataValue> getMetadata(Item item, String schema, String element, String qualifier,
String lang, boolean enableVirtualMetadata); String lang, boolean enableVirtualMetadata);
/**
* Returns the item's entity type, if any.
*
* @param item the item
* @return the entity type as string, if any
*/
public String getEntityType(Item item);
} }

View File

@@ -20,7 +20,7 @@
# For example, including "dspace.dir" in this local.cfg will override the # For example, including "dspace.dir" in this local.cfg will override the
# default value of "dspace.dir" in the dspace.cfg file. # default value of "dspace.dir" in the dspace.cfg file.
# #
researcher-profile.type = Person
########################## ##########################
# SERVER CONFIGURATION # # SERVER CONFIGURATION #
########################## ##########################

View File

@@ -0,0 +1,55 @@
/**
* 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.matcher;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import java.util.function.Predicate;
/**
* Matcher based on an {@link Predicate}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
* @param <T> the type of the instance to match
*/
public class LambdaMatcher<T> extends BaseMatcher<T> {
private final Predicate<T> matcher;
private final String description;
public static <T> LambdaMatcher<T> matches(Predicate<T> matcher) {
return new LambdaMatcher<T>(matcher, "Matches the given predicate");
}
public static <T> LambdaMatcher<T> matches(Predicate<T> matcher, String description) {
return new LambdaMatcher<T>(matcher, description);
}
public static <T> Matcher<Iterable<? super T>> has(Predicate<T> matcher) {
return Matchers.hasItem(matches(matcher));
}
private LambdaMatcher(Predicate<T> matcher, String description) {
this.matcher = matcher;
this.description = description;
}
@Override
@SuppressWarnings("unchecked")
public boolean matches(Object argument) {
return matcher.test((T) argument);
}
@Override
public void describeTo(Description description) {
description.appendText(this.description);
}
}

View File

@@ -0,0 +1,96 @@
/**
* 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.matcher;
import org.dspace.content.MetadataValue;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import java.util.Objects;
/**
* Implementation of {@link org.hamcrest.Matcher} to match a MetadataValue by
* all its attributes.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class MetadataValueMatcher extends TypeSafeMatcher<MetadataValue> {
private String field;
private String value;
private String language;
private String authority;
private Integer place;
private Integer confidence;
private MetadataValueMatcher(String field, String value, String language, String authority, Integer place,
Integer confidence) {
this.field = field;
this.value = value;
this.language = language;
this.authority = authority;
this.place = place;
this.confidence = confidence;
}
@Override
public void describeTo(Description description) {
description.appendText("MetadataValue with the following attributes [field=" + field + ", value="
+ value + ", language=" + language + ", authority=" + authority + ", place=" + place + ", confidence="
+ confidence + "]");
}
@Override
protected void describeMismatchSafely(MetadataValue item, Description mismatchDescription) {
mismatchDescription.appendText("was ")
.appendValue("MetadataValue [metadataField=").appendValue(item.getMetadataField().toString('.'))
.appendValue(", value=").appendValue(item.getValue()).appendValue(", language=").appendValue(language)
.appendValue(", place=").appendValue(item.getPlace()).appendValue(", authority=")
.appendValue(item.getAuthority()).appendValue(", confidence=").appendValue(item.getConfidence() + "]");
}
@Override
protected boolean matchesSafely(MetadataValue metadataValue) {
return Objects.equals(metadataValue.getValue(), value) &&
Objects.equals(metadataValue.getMetadataField().toString('.'), field) &&
Objects.equals(metadataValue.getLanguage(), language) &&
Objects.equals(metadataValue.getAuthority(), authority) &&
Objects.equals(metadataValue.getPlace(), place) &&
Objects.equals(metadataValue.getConfidence(), confidence);
}
public static MetadataValueMatcher with(String field, String value, String language,
String authority, Integer place, Integer confidence) {
return new MetadataValueMatcher(field, value, language, authority, place, confidence);
}
public static MetadataValueMatcher with(String field, String value) {
return with(field, value, null, null, 0, -1);
}
public static MetadataValueMatcher with(String field, String value, String authority, int place, int confidence) {
return with(field, value, null, authority, place, confidence);
}
public static MetadataValueMatcher with(String field, String value, String authority, int confidence) {
return with(field, value, null, authority, 0, confidence);
}
public static MetadataValueMatcher with(String field, String value, int place) {
return with(field, value, null, null, place, -1);
}
}

View File

@@ -22,6 +22,9 @@ import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import static org.dspace.content.MetadataSchemaEnum.DC;
import static org.dspace.content.authority.Choices.CF_ACCEPTED;
/** /**
* Builder to construct Item objects * Builder to construct Item objects
* *
@@ -73,6 +76,11 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
public ItemBuilder withAuthor(final String authorName) { public ItemBuilder withAuthor(final String authorName) {
return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author", authorName); return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author", authorName);
} }
public ItemBuilder withAuthor(final String authorName, final String authority) {
return addMetadataValue(item, DC.getName(), "contributor", "author", null, authorName, authority, 600);
}
public ItemBuilder withAuthor(final String authorName, final String authority, final int confidence) { public ItemBuilder withAuthor(final String authorName, final String authority, final int confidence) {
return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author", return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author",
null, authorName, authority, confidence); null, authorName, authority, confidence);
@@ -144,6 +152,13 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
return addMetadataValue(item, schema, element, qualifier, value); return addMetadataValue(item, schema, element, qualifier, value);
} }
public ItemBuilder withDspaceObjectOwner(String value, String authority) {
return addMetadataValue(item, "dspace", "object", "owner", null, value, authority, CF_ACCEPTED);
}
public ItemBuilder withDspaceObjectOwner(EPerson ePerson) {
return withDspaceObjectOwner(ePerson.getFullName(), ePerson.getID().toString());
}
public ItemBuilder makeUnDiscoverable() { public ItemBuilder makeUnDiscoverable() {
item.setDiscoverable(false); item.setDiscoverable(false);
return this; return this;
@@ -181,6 +196,9 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
return setAdminPermission(item, ePerson, null); return setAdminPermission(item, ePerson, null);
} }
public ItemBuilder withPersonEmail(String email) {
return addMetadataValue(item, "person", "email", null, email);
}
@Override @Override
public Item build() { public Item build() {

View File

@@ -0,0 +1,94 @@
/**
* 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.converter;
//import static org.dspace.app.orcid.model.OrcidEntityType.FUNDING;
//import static org.dspace.app.orcid.model.OrcidEntityType.PUBLICATION;
import java.util.List;
import java.util.stream.Collectors;
//import org.dspace.app.orcid.service.OrcidSynchronizationService;
//import org.dspace.app.profile.OrcidEntitySyncPreference;
//import org.dspace.app.profile.OrcidProfileSyncPreference;
//import org.dspace.app.profile.OrcidSynchronizationMode;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.app.rest.model.ResearcherProfileRest;
//import org.dspace.app.rest.model.ResearcherProfileRest.OrcidSynchronizationRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.content.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* This converter is responsible for transforming an model that represent a
* ResearcherProfile to the REST representation of an ResearcherProfile.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Component
public class ResearcherProfileConverter implements DSpaceConverter<ResearcherProfile, ResearcherProfileRest> {
// @Autowired
// private OrcidSynchronizationService orcidSynchronizationService;
@Override
public ResearcherProfileRest convert(ResearcherProfile profile, Projection projection) {
ResearcherProfileRest researcherProfileRest = new ResearcherProfileRest();
researcherProfileRest.setVisible(profile.isVisible());
researcherProfileRest.setId(profile.getId());
researcherProfileRest.setProjection(projection);
Item item = profile.getItem();
// if (orcidSynchronizationService.isLinkedToOrcid(item)) {
// profile.getOrcid().ifPresent(researcherProfileRest::setOrcid);
//
// OrcidSynchronizationRest orcidSynchronization = new OrcidSynchronizationRest();
// orcidSynchronization.setMode(getMode(item));
// orcidSynchronization.setProfilePreferences(getProfilePreferences(item));
// orcidSynchronization.setFundingsPreference(getFundingsPreference(item));
// orcidSynchronization.setPublicationsPreference(getPublicationsPreference(item));
// researcherProfileRest.setOrcidSynchronization(orcidSynchronization);
// }
return researcherProfileRest;
}
// private String getPublicationsPreference(Item item) {
// return orcidSynchronizationService.getEntityPreference(item, PUBLICATION)
// .map(OrcidEntitySyncPreference::name)
// .orElse(OrcidEntitySyncPreference.DISABLED.name());
// }
//
// private String getFundingsPreference(Item item) {
// return orcidSynchronizationService.getEntityPreference(item, FUNDING)
// .map(OrcidEntitySyncPreference::name)
// .orElse(OrcidEntitySyncPreference.DISABLED.name());
// }
//
// private List<String> getProfilePreferences(Item item) {
// return orcidSynchronizationService.getProfilePreferences(item).stream()
// .map(OrcidProfileSyncPreference::name)
// .collect(Collectors.toList());
// }
//
// private String getMode(Item item) {
// return orcidSynchronizationService.getSynchronizationMode(item)
// .map(OrcidSynchronizationMode::name)
// .orElse(OrcidSynchronizationMode.MANUAL.name());
// }
@Override
public Class<ResearcherProfile> getModelClass() {
return ResearcherProfile.class;
}
}

View File

@@ -20,11 +20,16 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.app.exception.ResourceConflictException;
import org.dspace.app.rest.converter.ConverterService;
import org.dspace.app.rest.model.RestModel;
import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.app.rest.utils.Utils;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.springframework.beans.TypeMismatchException; import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.repository.support.QueryMethodParameterConversionException; import org.springframework.data.repository.support.QueryMethodParameterConversionException;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -67,6 +72,12 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
@Inject @Inject
private ConfigurationService configurationService; private ConfigurationService configurationService;
@Autowired
private ConverterService converterService;
@Autowired
private Utils utils;
@ExceptionHandler({AuthorizeException.class, RESTAuthorizationException.class, AccessDeniedException.class}) @ExceptionHandler({AuthorizeException.class, RESTAuthorizationException.class, AccessDeniedException.class})
protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex) protected void handleAuthorizeException(HttpServletRequest request, HttpServletResponse response, Exception ex)
throws IOException { throws IOException {
@@ -166,6 +177,12 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
HttpStatus.BAD_REQUEST.value()); HttpStatus.BAD_REQUEST.value());
} }
@ExceptionHandler(ResourceConflictException.class)
protected ResponseEntity<? extends RestModel> resourceConflictException(ResourceConflictException ex) {
RestModel resource = converterService.toRest(ex.getResource(), utils.obtainProjection());
return new ResponseEntity<RestModel>(resource, HttpStatus.CONFLICT);
}
@ExceptionHandler(MissingParameterException.class) @ExceptionHandler(MissingParameterException.class)
protected void MissingParameterException(HttpServletRequest request, HttpServletResponse response, Exception ex) protected void MissingParameterException(HttpServletRequest request, HttpServletResponse response, Exception ex)
throws IOException { throws IOException {

View File

@@ -0,0 +1,27 @@
/**
* 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.login;
import org.dspace.core.Context;
/**
* Interface for classes that need to perform some operations after the user
* login.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface PostLoggedInAction {
/**
* Perform some operations after the user login.
*
* @param context the DSpace context
*/
public void loggedIn(Context context);
}

View File

@@ -0,0 +1,46 @@
/**
* 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.login.impl;
import org.dspace.app.rest.login.PostLoggedInAction;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.services.EventService;
import org.dspace.usage.UsageEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* Implementation of {@link PostLoggedInAction} that fire an LOGIN event.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class LoginEventFireAction implements PostLoggedInAction {
@Autowired
private EventService eventService;
@Override
public void loggedIn(Context context) {
HttpServletRequest request = getCurrentRequest();
EPerson currentUser = context.getCurrentUser();
eventService.fireEvent(new UsageEvent(UsageEvent.Action.LOGIN, request, context, currentUser));
}
private HttpServletRequest getCurrentRequest() {
return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
}
}

View File

@@ -0,0 +1,129 @@
/**
* 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.login.impl;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.app.rest.login.PostLoggedInAction;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.content.MetadataFieldName;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.apache.commons.collections4.IteratorUtils.toList;
import static org.dspace.content.authority.Choices.CF_ACCEPTED;
/**
* Implementation of {@link PostLoggedInAction} that perform an automatic claim
* between the logged eperson and possible profiles without eperson present in
* the system. This pairing between eperson and profile is done starting from
* the configured metadata of the logged in user.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class ResearcherProfileAutomaticClaim implements PostLoggedInAction {
private final static Logger LOGGER = LoggerFactory.getLogger(ResearcherProfileAutomaticClaim.class);
@Autowired
private ResearcherProfileService researcherProfileService;
@Autowired
private ItemService itemService;
@Autowired
private EPersonService ePersonService;
private final String ePersonField;
private final String profileFiled;
public ResearcherProfileAutomaticClaim(String ePersonField, String profileField) {
Assert.notNull(ePersonField, "An eperson field is required to perform automatic claim");
Assert.notNull(profileField, "An profile field is required to perform automatic claim");
this.ePersonField = ePersonField;
this.profileFiled = profileField;
}
@Override
public void loggedIn(Context context) {
EPerson currentUser = context.getCurrentUser();
if (currentUser == null) {
return;
}
try {
claimProfile(context, currentUser);
} catch (SQLException | AuthorizeException e) {
LOGGER.error("An error occurs during the profile claim by email", e);
}
}
private void claimProfile(Context context, EPerson currentUser) throws SQLException, AuthorizeException {
UUID id = currentUser.getID();
String fullName = currentUser.getFullName();
if (currentUserHasAlreadyResearcherProfile(context)) {
return;
}
Item item = findClaimableItem(context, currentUser);
if (item != null) {
itemService.addMetadata(context, item, "dspace", "object", "owner", null, fullName, id.toString(), CF_ACCEPTED);
}
}
private boolean currentUserHasAlreadyResearcherProfile(Context context) throws SQLException, AuthorizeException {
return researcherProfileService.findById(context, context.getCurrentUser().getID()) != null;
}
private Item findClaimableItem(Context context, EPerson currentUser)
throws SQLException, AuthorizeException {
String value = getValueToSearchFor(context, currentUser);
if (StringUtils.isEmpty(value)) {
return null;
}
List<Item> items = toList(itemService.findArchivedByMetadataField(context, profileFiled, value)).stream()
.filter(this::hasNotCrisOwner)
.collect(Collectors.toList());
return items.size() == 1 ? items.get(0) : null;
}
private String getValueToSearchFor(Context context, EPerson currentUser) {
if ("email".equals(ePersonField)) {
return currentUser.getEmail();
}
return ePersonService.getMetadataFirstValue(currentUser, new MetadataFieldName(ePersonField), Item.ANY);
}
private boolean hasNotCrisOwner(Item item) {
return CollectionUtils.isEmpty(itemService.getMetadata(item, "dspace", "object", "owner", Item.ANY));
}
}

View File

@@ -0,0 +1,134 @@
/**
* 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.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import org.dspace.app.rest.RestResourceController;
import java.util.List;
import java.util.UUID;
/**
* The Researcher Profile REST resource.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@LinksRest(links = {
@LinkRest(name = ResearcherProfileRest.ITEM, method = "getItem"),
@LinkRest(name = ResearcherProfileRest.EPERSON, method = "getEPerson")
})
public class ResearcherProfileRest extends BaseObjectRest<UUID> {
private static final long serialVersionUID = 1L;
// changed from RestModel.CRIS to RestModel.EPERSON
public static final String CATEGORY = RestModel.EPERSON;
public static final String NAME = "profile";
public static final String ITEM = "item";
public static final String EPERSON = "eperson";
private boolean visible;
// @JsonInclude(Include.NON_NULL)
// private String orcid;
// @JsonInclude(Include.NON_NULL)
// private OrcidSynchronizationRest orcidSynchronization;
public boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
// public OrcidSynchronizationRest getOrcidSynchronization() {
// return orcidSynchronization;
// }
// public void setOrcidSynchronization(OrcidSynchronizationRest orcidSynchronization) {
// this.orcidSynchronization = orcidSynchronization;
// }
// public String getOrcid() {
// return orcid;
// }
//
// public void setOrcid(String orcid) {
// this.orcid = orcid;
// }
@Override
public String getType() {
return NAME;
}
@Override
public String getCategory() {
return CATEGORY;
}
@Override
public Class<?> getController() {
return RestResourceController.class;
}
/**
* Inner class to model ORCID synchronization preferences and mode.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
// public static class OrcidSynchronizationRest {
//
// private String mode;
//
// private String publicationsPreference;
//
// private String fundingsPreference;
//
// private List<String> profilePreferences;
//
// public String getMode() {
// return mode;
// }
//
// public void setMode(String mode) {
// this.mode = mode;
// }
//
// public List<String> getProfilePreferences() {
// return profilePreferences;
// }
//
// public void setProfilePreferences(List<String> profilePreferences) {
// this.profilePreferences = profilePreferences;
// }
//
// public String getPublicationsPreference() {
// return publicationsPreference;
// }
//
// public void setPublicationsPreference(String publicationsPreference) {
// this.publicationsPreference = publicationsPreference;
// }
//
// public String getFundingsPreference() {
// return fundingsPreference;
// }
//
// public void setFundingsPreference(String fundingsPreference) {
// this.fundingsPreference = fundingsPreference;
// }
//
// }
}

View File

@@ -0,0 +1,29 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.hateoas;
import org.dspace.app.rest.model.ResearcherProfileRest;
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
import org.dspace.app.rest.utils.Utils;
/**
* This class serves as a wrapper class to wrap the SearchConfigurationRest into
* a HAL resource.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@RelNameDSpaceResource(ResearcherProfileRest.NAME)
public class ResearcherProfileResource extends DSpaceResource<ResearcherProfileRest> {
public ResearcherProfileResource(ResearcherProfileRest data, Utils utils) {
super(data, utils);
}
}

View File

@@ -0,0 +1,78 @@
/**
* 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;
import java.sql.SQLException;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.app.rest.model.EPersonRest;
import org.dspace.app.rest.model.ResearcherProfileRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
* Link repository for "ePerson" subresource of an individual researcher
* profile.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME + "." + ResearcherProfileRest.EPERSON)
public class ResearcherProfileEPersonLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository {
@Autowired
private EPersonService ePersonService;
@Autowired
private ResearcherProfileService researcherProfileService;
/**
* Returns the ePerson related to the Research profile with the given UUID.
*
* @param request the http servlet request
* @param id the profile UUID
* @param pageable the optional pageable
* @param projection the projection object
* @return the ePerson rest representation
*/
@PreAuthorize("hasPermission(#id, 'PROFILE', 'READ')")
public EPersonRest getEPerson(@Nullable HttpServletRequest request, UUID id,
@Nullable Pageable pageable, Projection projection) {
try {
Context context = obtainContext();
ResearcherProfile profile = researcherProfileService.findById(context, id);
if (profile == null) {
throw new ResourceNotFoundException("No such profile with UUID: " + id);
}
EPerson ePerson = ePersonService.find(context, id);
if (ePerson == null) {
throw new ResourceNotFoundException("No such eperson related to a profile with EPerson UUID: " + id);
}
return converter.toRest(ePerson, projection);
} catch (SQLException | AuthorizeException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,67 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.repository;
import java.sql.SQLException;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.model.ResearcherProfileRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
/**
* Link repository for "item" subresource of an individual researcher profile.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME + "." + ResearcherProfileRest.ITEM)
public class ResearcherProfileItemLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository {
@Autowired
private ResearcherProfileService researcherProfileService;
/**
* Returns the item related to the Research profile with the given UUID.
*
* @param request the http servlet request
* @param id the profile UUID
* @param pageable the optional pageable
* @param projection the projection object
* @return the item rest representation
*/
@PreAuthorize("hasPermission(#id, 'PROFILE', 'READ')")
public ItemRest getItem(@Nullable HttpServletRequest request, UUID id,
@Nullable Pageable pageable, Projection projection) {
try {
Context context = obtainContext();
ResearcherProfile profile = researcherProfileService.findById(context, id);
if (profile == null) {
throw new ResourceNotFoundException("No such item related to a profile with EPerson UUID: " + id);
}
return converter.toRest(profile.getItem(), projection);
} catch (SQLException | AuthorizeException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,189 @@
/**
* 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;
import org.apache.commons.collections.CollectionUtils;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.app.rest.exception.DSpaceBadRequestException;
import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException;
import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.ResearcherProfileRest;
import org.dspace.app.rest.model.patch.Patch;
import org.dspace.app.rest.repository.patch.ResourcePatch;
import org.dspace.app.rest.security.DSpacePermissionEvaluator;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.dspace.discovery.SearchServiceException;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.util.UUIDUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Conditional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
/**
* This is the repository responsible of exposing researcher profiles.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Component(ResearcherProfileRest.CATEGORY + "." + ResearcherProfileRest.NAME)
@ConditionalOnProperty(
value="researcher-profile.type"
)
public class ResearcherProfileRestRepository extends DSpaceRestRepository<ResearcherProfileRest, UUID> {
public static final String NO_VISIBILITY_CHANGE_MSG = "Refused to perform the Researcher Profile patch based "
+ "on a token without changing the visibility";
@Autowired
private ResearcherProfileService researcherProfileService;
@Autowired
private DSpacePermissionEvaluator permissionEvaluator;
@Autowired
private EPersonService ePersonService;
@Autowired
private ResourcePatch<ResearcherProfile> resourcePatch;
@Override
@PreAuthorize("hasPermission(#id, 'PROFILE', 'READ')")
public ResearcherProfileRest findOne(Context context, UUID id) {
try {
ResearcherProfile profile = researcherProfileService.findById(context, id);
if (profile == null) {
return null;
}
return converter.toRest(profile, utils.obtainProjection());
} catch (SQLException | AuthorizeException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
@PreAuthorize("isAuthenticated()")
protected ResearcherProfileRest createAndReturn(Context context) throws AuthorizeException, SQLException {
UUID id = getEPersonIdFromRequest(context);
if (isNotAuthorized(id, "WRITE")) {
throw new AuthorizeException("User unauthorized to create a new profile for user " + id);
}
EPerson ePerson = ePersonService.find(context, id);
if (ePerson == null) {
throw new UnprocessableEntityException("No EPerson exists with id: " + id);
}
try {
ResearcherProfile newProfile = researcherProfileService.createAndReturn(context, ePerson);
return converter.toRest(newProfile, utils.obtainProjection());
} catch (SearchServiceException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
protected ResearcherProfileRest createAndReturn(final Context context, final List<String> list)
throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException {
if (CollectionUtils.isEmpty(list) || list.size() > 1) {
throw new IllegalArgumentException("Uri list must contain exactly one element");
}
UUID id = getEPersonIdFromRequest(context);
if (isNotAuthorized(id, "WRITE")) {
throw new AuthorizeException("User unauthorized to create a new profile for user " + id);
}
EPerson ePerson = ePersonService.find(context, id);
if (ePerson == null) {
throw new UnprocessableEntityException("No EPerson exists with id: " + id);
}
try {
ResearcherProfile newProfile = researcherProfileService
.claim(context, ePerson, URI.create(list.get(0)));
return converter.toRest(newProfile, utils.obtainProjection());
} catch (SearchServiceException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
public Page<ResearcherProfileRest> findAll(Context context, Pageable pageable) {
throw new RepositoryMethodNotImplementedException("No implementation found; Method not allowed!", "");
}
@Override
@PreAuthorize("hasPermission(#id, 'PROFILE', 'WRITE')")
protected void delete(Context context, UUID id) {
try {
researcherProfileService.deleteById(context, id);
} catch (SQLException | AuthorizeException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
@Override
@PreAuthorize("hasPermission(#id, 'PROFILE', #patch)")
protected void patch(Context context, HttpServletRequest request, String apiCategory, String model,
UUID id, Patch patch) throws SQLException, AuthorizeException {
ResearcherProfile profile = researcherProfileService.findById(context, id);
if (profile == null) {
throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + id + " not found");
}
resourcePatch.patch(context, profile, patch.getOperations());
}
@Override
public Class<ResearcherProfileRest> getDomainClass() {
return ResearcherProfileRest.class;
}
private UUID getEPersonIdFromRequest(Context context) {
HttpServletRequest request = getRequestService().getCurrentRequest().getHttpServletRequest();
String ePersonId = request.getParameter("eperson");
if (ePersonId == null) {
return context.getCurrentUser().getID();
}
UUID uuid = UUIDUtils.fromString(ePersonId);
if (uuid == null) {
throw new DSpaceBadRequestException("The provided eperson parameter is not a valid uuid");
}
return uuid;
}
private boolean isNotAuthorized(UUID id, String permission) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return !permissionEvaluator.hasPermission(authentication, id, "PROFILE", permission);
}
}

View File

@@ -0,0 +1,67 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.repository.patch.operation;
import java.sql.SQLException;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.app.rest.exception.RESTAuthorizationException;
import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.patch.Operation;
import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Implementation for ResearcherProfile visibility patches.
*
* Example:
* <code> curl -X PATCH http://${dspace.server.url}/api/cris/profiles/<:id-eperson> -H "
* Content-Type: application/json" -d '[{ "op": "replace", "path": "
* /visible", "value": true]'
* </code>
*/
@Component
public class ResearcherProfileVisibleReplaceOperation extends PatchOperation<ResearcherProfile> {
@Autowired
private ResearcherProfileService researcherProfileService;
/**
* Path in json body of patch that uses this operation.
*/
public static final String OPERATION_VISIBLE_CHANGE = "/visible";
@Override
public ResearcherProfile perform(Context context, ResearcherProfile profile, Operation operation)
throws SQLException {
Object value = operation.getValue();
if (value == null | !(value instanceof Boolean)) {
throw new UnprocessableEntityException("The /visible value must be a boolean (true|false)");
}
try {
researcherProfileService.changeVisibility(context, profile, (boolean) value);
} catch (AuthorizeException e) {
throw new RESTAuthorizationException("Unauthorized user for profile visibility change");
}
return profile;
}
@Override
public boolean supports(Object objectToMatch, Operation operation) {
return (objectToMatch instanceof ResearcherProfile
&& operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE)
&& operation.getPath().trim().equalsIgnoreCase(OPERATION_VISIBLE_CHANGE));
}
}

View File

@@ -11,12 +11,15 @@ import static org.dspace.app.rest.security.WebSecurityConfiguration.ADMIN_GRANT;
import static org.dspace.app.rest.security.WebSecurityConfiguration.AUTHENTICATED_GRANT; import static org.dspace.app.rest.security.WebSecurityConfiguration.AUTHENTICATED_GRANT;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.login.PostLoggedInAction;
import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.app.util.AuthorizeUtil; import org.dspace.app.util.AuthorizeUtil;
import org.dspace.authenticate.AuthenticationMethod; import org.dspace.authenticate.AuthenticationMethod;
@@ -62,6 +65,16 @@ public class EPersonRestAuthenticationProvider implements AuthenticationProvider
@Autowired @Autowired
private HttpServletRequest request; private HttpServletRequest request;
@Autowired(required = false)
private List<PostLoggedInAction> postLoggedInActions;
@PostConstruct
public void postConstruct() {
if (postLoggedInActions == null) {
postLoggedInActions = Collections.emptyList();
}
}
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Context context = ContextUtil.obtainContext(request); Context context = ContextUtil.obtainContext(request);
@@ -122,6 +135,11 @@ public class EPersonRestAuthenticationProvider implements AuthenticationProvider
.getHeader(newContext, "login", "type=explicit")); .getHeader(newContext, "login", "type=explicit"));
output = createAuthentication(newContext); output = createAuthentication(newContext);
for (PostLoggedInAction action : postLoggedInActions) {
action.loggedIn(newContext);
}
} else { } else {
log.info(LogHelper.getHeader(newContext, "failed_login", "email=" log.info(LogHelper.getHeader(newContext, "failed_login", "email="
+ name + ", result=" + name + ", result="

View File

@@ -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.security;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.rest.model.ResearcherProfileRest;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.services.RequestService;
import org.dspace.services.model.Request;
import org.dspace.util.UUIDUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.UUID;
import static org.dspace.app.rest.security.DSpaceRestPermission.*;
/**
*
* An authenticated user is allowed to view, update or delete his or her own
* data. This {@link RestPermissionEvaluatorPlugin} implements that requirement.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Component
public class ResearcherProfileRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin {
@Autowired
private RequestService requestService;
@Override
public boolean hasDSpacePermission(Authentication authentication, Serializable targetId, String targetType,
DSpaceRestPermission restPermission) {
if (!READ.equals(restPermission) && !WRITE.equals(restPermission) && !DELETE.equals(restPermission)) {
return false;
}
if (!StringUtils.equalsIgnoreCase(targetType, ResearcherProfileRest.NAME)) {
return false;
}
UUID id = UUIDUtils.fromString(targetId.toString());
if (id == null) {
return false;
}
Request request = requestService.getCurrentRequest();
Context context = ContextUtil.obtainContext((HttpServletRequest) request.getServletRequest());
EPerson currentUser = context.getCurrentUser();
if (currentUser == null) {
return false;
}
if (id.equals(currentUser.getID())) {
return true;
}
return false;
}
}

View File

@@ -15,6 +15,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.core.StringEndsWith; import org.hamcrest.core.StringEndsWith;
/** /**
@@ -67,6 +68,22 @@ public class MetadataMatcher {
return hasJsonPath("$.['" + key + "'][" + position + "].value", is(value)); return hasJsonPath("$.['" + key + "'][" + position + "].value", is(value));
} }
/**
* Gets a matcher to ensure a given value is present at a specific position in
* the list of values for a given key.
*
* @param key the metadata key.
* @param value the value that must be present.
* @param authority the authority that must be present.
* @param position the position it must be present at.
* @return the matcher.
*/
public static Matcher<? super Object> matchMetadata(String key, String value, String authority, int position) {
Matcher<Object> hasValue = hasJsonPath("$.['" + key + "'][" + position + "].value", is(value));
Matcher<Object> hasAuthority = hasJsonPath("$.['" + key + "'][" + position + "].authority", is(authority));
return Matchers.allOf(hasValue, hasAuthority);
}
/** /**
* Gets a matcher to ensure a given key is not present. * Gets a matcher to ensure a given key is not present.
* *

View File

@@ -0,0 +1,15 @@
#---------------------------------------------------------------#
#----------------- AUTHORITY CONFIGURATIONS --------------------#
#---------------------------------------------------------------#
# These configs are used by the authority framework #
#---------------------------------------------------------------#
##### Authority Control Settings #####
plugin.named.org.dspace.content.authority.ChoiceAuthority = \
org.dspace.content.authority.EPersonAuthority = EPersonAuthority
choices.plugin.dspace.object.owner = EPersonAuthority
choices.presentation.dspace.object.owner = suggest
authority.controlled.dspace.object.owner = true

View File

@@ -43,4 +43,12 @@
<qualifier>enabled</qualifier> <qualifier>enabled</qualifier>
<scope_note>Stores a boolean text value (true or false) to indicate if the iiif feature is enabled or not for the dspace object. If absent the value is derived from the parent dspace object</scope_note> <scope_note>Stores a boolean text value (true or false) to indicate if the iiif feature is enabled or not for the dspace object. If absent the value is derived from the parent dspace object</scope_note>
</dc-type> </dc-type>
<dc-type>
<schema>dspace</schema>
<element>object</element>
<qualifier>owner</qualifier>
<scope_note>Stores the reference to the eperson that own the item </scope_note>
</dc-type>
</dspace-dc-types> </dspace-dc-types>

View File

@@ -63,6 +63,8 @@
<bean class="org.dspace.content.authority.ChoiceAuthorityServiceImpl"/> <bean class="org.dspace.content.authority.ChoiceAuthorityServiceImpl"/>
<bean class="org.dspace.content.authority.MetadataAuthorityServiceImpl" lazy-init="true"/> <bean class="org.dspace.content.authority.MetadataAuthorityServiceImpl" lazy-init="true"/>
<bean class="org.dspace.app.profile.ResearcherProfileServiceImpl"/>
<bean class='org.dspace.service.impl.HttpConnectionPoolService' <bean class='org.dspace.service.impl.HttpConnectionPoolService'
id='solrHttpConnectionPoolService' id='solrHttpConnectionPoolService'
scope='singleton' scope='singleton'

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<bean id="researcherProfileClaimByEmail" class="org.dspace.app.rest.login.impl.ResearcherProfileAutomaticClaim">
<constructor-arg name="ePersonField" value="email" />
<constructor-arg name="profileField" value="person.email" />
</bean>
<bean id="loginEventFireAction" class="org.dspace.app.rest.login.impl.LoginEventFireAction"/>
</beans>