diff --git a/dspace-api/src/main/java/org/dspace/alerts/AllowSessionsEnum.java b/dspace-api/src/main/java/org/dspace/alerts/AllowSessionsEnum.java new file mode 100644 index 0000000000..a200cab878 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/alerts/AllowSessionsEnum.java @@ -0,0 +1,54 @@ +/** + * 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.alerts; + +/** + * Enum representing the options for allowing sessions: + * ALLOW_ALL_SESSIONS - Will allow all users to log in and continue their sessions + * ALLOW_CURRENT_SESSIONS_ONLY - Will prevent non admin users from logging in, however logged-in users + * will remain logged in + * ALLOW_ADMIN_SESSIONS_ONLY - Only admin users can log in, non admin sessions will be interrupted + * + * NOTE: This functionality can be stored in the database, but no support is present right now to interrupt and prevent + * sessions. + */ +public enum AllowSessionsEnum { + ALLOW_ALL_SESSIONS("all"), + ALLOW_CURRENT_SESSIONS_ONLY("current"), + ALLOW_ADMIN_SESSIONS_ONLY("admin"); + + private String allowSessionsType; + + AllowSessionsEnum(String allowSessionsType) { + this.allowSessionsType = allowSessionsType; + } + + public String getValue() { + return allowSessionsType; + } + + public static AllowSessionsEnum fromString(String alertAllowSessionType) { + if (alertAllowSessionType == null) { + return AllowSessionsEnum.ALLOW_ALL_SESSIONS; + } + + switch (alertAllowSessionType) { + case "all": + return AllowSessionsEnum.ALLOW_ALL_SESSIONS; + case "current": + return AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY; + case "admin" : + return AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY; + default: + throw new IllegalArgumentException("No corresponding enum value for provided string: " + + alertAllowSessionType); + } + } + + +} diff --git a/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java new file mode 100644 index 0000000000..f56cbdcce9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlert.java @@ -0,0 +1,179 @@ +/** + * 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.alerts; + +import java.util.Date; +import javax.persistence.Cacheable; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.dspace.core.ReloadableEntity; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * Database object representing system-wide alerts + */ +@Entity +@Cacheable +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, include = "non-lazy") +@Table(name = "systemwidealert") +public class SystemWideAlert implements ReloadableEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "alert_id_seq") + @SequenceGenerator(name = "alert_id_seq", sequenceName = "alert_id_seq", allocationSize = 1) + @Column(name = "alert_id", unique = true, nullable = false) + private Integer alertId; + + @Column(name = "message", nullable = false) + private String message; + + @Column(name = "allow_sessions") + private String allowSessions; + + @Column(name = "countdown_to") + @Temporal(TemporalType.TIMESTAMP) + private Date countdownTo; + + @Column(name = "active") + private boolean active; + + protected SystemWideAlert() { + } + + /** + * This method returns the ID that the system-wide alert holds within the database + * + * @return The ID that the system-wide alert holds within the database + */ + @Override + public Integer getID() { + return alertId; + } + + /** + * Set the ID for the system-wide alert + * + * @param alertID The ID to set + */ + public void setID(final Integer alertID) { + this.alertId = alertID; + } + + /** + * Retrieve the message of the system-wide alert + * + * @return the message of the system-wide alert + */ + public String getMessage() { + return message; + } + + /** + * Set the message of the system-wide alert + * + * @param message The message to set + */ + public void setMessage(final String message) { + this.message = message; + } + + /** + * Retrieve what kind of sessions are allowed while the system-wide alert is active + * + * @return what kind of sessions are allowed while the system-wide alert is active + */ + public AllowSessionsEnum getAllowSessions() { + return AllowSessionsEnum.fromString(allowSessions); + } + + /** + * Set what kind of sessions are allowed while the system-wide alert is active + * + * @param allowSessions Integer representing what kind of sessions are allowed + */ + public void setAllowSessions(AllowSessionsEnum allowSessions) { + this.allowSessions = allowSessions.getValue(); + } + + /** + * Retrieve the date to which will be count down when the system-wide alert is active + * + * @return the date to which will be count down when the system-wide alert is active + */ + public Date getCountdownTo() { + return countdownTo; + } + + /** + * Set the date to which will be count down when the system-wide alert is active + * + * @param countdownTo The date to which will be count down + */ + public void setCountdownTo(final Date countdownTo) { + this.countdownTo = countdownTo; + } + + /** + * Retrieve whether the system-wide alert is active + * + * @return whether the system-wide alert is active + */ + public boolean isActive() { + return active; + } + + /** + * Set whether the system-wide alert is active + * + * @param active Whether the system-wide alert is active + */ + public void setActive(final boolean active) { + this.active = active; + } + + /** + * Return true if other is the same SystemWideAlert + * as this object, false otherwise + * + * @param other object to compare to + * @return true if object passed in represents the same + * system-wide alert as this object + */ + @Override + public boolean equals(Object other) { + return (other instanceof SystemWideAlert && + new EqualsBuilder().append(this.getID(), ((SystemWideAlert) other).getID()) + .append(this.getMessage(), ((SystemWideAlert) other).getMessage()) + .append(this.getAllowSessions(), ((SystemWideAlert) other).getAllowSessions()) + .append(this.getCountdownTo(), ((SystemWideAlert) other).getCountdownTo()) + .append(this.isActive(), ((SystemWideAlert) other).isActive()) + .isEquals()); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(this.getID()) + .append(this.getMessage()) + .append(this.getAllowSessions()) + .append(this.getCountdownTo()) + .append(this.isActive()) + .toHashCode(); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlertServiceImpl.java b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlertServiceImpl.java new file mode 100644 index 0000000000..9ddf6c97d1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/alerts/SystemWideAlertServiceImpl.java @@ -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.alerts; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.dspace.alerts.dao.SystemWideAlertDAO; +import org.dspace.alerts.service.SystemWideAlertService; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.dspace.core.LogHelper; +import org.dspace.eperson.EPerson; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * The implementation for the {@link SystemWideAlertService} class + */ +public class SystemWideAlertServiceImpl implements SystemWideAlertService { + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SystemWideAlertService.class); + + + @Autowired + private SystemWideAlertDAO systemWideAlertDAO; + + @Autowired + private AuthorizeService authorizeService; + + @Override + public SystemWideAlert create(final Context context, final String message, + final AllowSessionsEnum allowSessionsType, + final Date countdownTo, final boolean active) throws SQLException, + AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators can create a system-wide alert"); + } + SystemWideAlert systemWideAlert = new SystemWideAlert(); + systemWideAlert.setMessage(message); + systemWideAlert.setAllowSessions(allowSessionsType); + systemWideAlert.setCountdownTo(countdownTo); + systemWideAlert.setActive(active); + + SystemWideAlert createdAlert = systemWideAlertDAO.create(context, systemWideAlert); + log.info(LogHelper.getHeader(context, "system_wide_alert_create", + "System Wide Alert has been created with message: '" + message + "' and ID " + + createdAlert.getID() + " and allowSessionsType " + allowSessionsType + + " and active set to " + active)); + + + return createdAlert; + } + + @Override + public SystemWideAlert find(final Context context, final int alertId) throws SQLException { + return systemWideAlertDAO.findByID(context, SystemWideAlert.class, alertId); + } + + @Override + public List findAll(final Context context) throws SQLException { + return systemWideAlertDAO.findAll(context, SystemWideAlert.class); + } + + @Override + public List findAll(final Context context, final int limit, final int offset) throws SQLException { + return systemWideAlertDAO.findAll(context, limit, offset); + } + + @Override + public List findAllActive(final Context context, final int limit, final int offset) + throws SQLException { + return systemWideAlertDAO.findAllActive(context, limit, offset); + } + + @Override + public void delete(final Context context, final SystemWideAlert systemWideAlert) + throws SQLException, IOException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators can create a system-wide alert"); + } + systemWideAlertDAO.delete(context, systemWideAlert); + log.info(LogHelper.getHeader(context, "system_wide_alert_create", + "System Wide Alert with ID " + systemWideAlert.getID() + " has been deleted")); + + } + + @Override + public void update(final Context context, final SystemWideAlert systemWideAlert) + throws SQLException, AuthorizeException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException( + "Only administrators can create a system-wide alert"); + } + systemWideAlertDAO.save(context, systemWideAlert); + + } + + @Override + public boolean canNonAdminUserLogin(Context context) throws SQLException { + List active = findAllActive(context, 1, 0); + if (active == null || active.isEmpty()) { + return true; + } + return active.get(0).getAllowSessions() == AllowSessionsEnum.ALLOW_ALL_SESSIONS; + } + + @Override + public boolean canUserMaintainSession(Context context, EPerson ePerson) throws SQLException { + if (authorizeService.isAdmin(context, ePerson)) { + return true; + } + List active = findAllActive(context, 1, 0); + if (active == null || active.isEmpty()) { + return true; + } + return active.get(0).getAllowSessions() != AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY; + } +} diff --git a/dspace-api/src/main/java/org/dspace/alerts/dao/SystemWideAlertDAO.java b/dspace-api/src/main/java/org/dspace/alerts/dao/SystemWideAlertDAO.java new file mode 100644 index 0000000000..b26b647583 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/alerts/dao/SystemWideAlertDAO.java @@ -0,0 +1,45 @@ +/** + * 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.alerts.dao; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.alerts.SystemWideAlert; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +/** + * This is the Data Access Object for the {@link SystemWideAlert} object + */ +public interface SystemWideAlertDAO extends GenericDAO { + + /** + * Returns a list of all SystemWideAlert objects in the database + * + * @param context The relevant DSpace context + * @param limit The limit for the amount of SystemWideAlerts returned + * @param offset The offset for the Processes to be returned + * @return The list of all SystemWideAlert objects in the Database + * @throws SQLException If something goes wrong + */ + List findAll(Context context, int limit, int offset) throws SQLException; + + /** + * Returns a list of all active SystemWideAlert objects in the database + * + * @param context The relevant DSpace context + * @param limit The limit for the amount of SystemWideAlerts returned + * @param offset The offset for the Processes to be returned + * @return The list of all SystemWideAlert objects in the Database + * @throws SQLException If something goes wrong + */ + List findAllActive(Context context, int limit, int offset) throws SQLException; + + +} diff --git a/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java b/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java new file mode 100644 index 0000000000..13a0e0af23 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/alerts/dao/impl/SystemWideAlertDAOImpl.java @@ -0,0 +1,48 @@ +/** + * 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.alerts.dao.impl; + +import java.sql.SQLException; +import java.util.List; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Root; + +import org.dspace.alerts.SystemWideAlert; +import org.dspace.alerts.SystemWideAlert_; +import org.dspace.alerts.dao.SystemWideAlertDAO; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +/** + * Implementation class for the {@link SystemWideAlertDAO} + */ +public class SystemWideAlertDAOImpl extends AbstractHibernateDAO implements SystemWideAlertDAO { + + public List findAll(final Context context, final int limit, final int offset) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, SystemWideAlert.class); + Root alertRoot = criteriaQuery.from(SystemWideAlert.class); + criteriaQuery.select(alertRoot); + + return list(context, criteriaQuery, false, SystemWideAlert.class, limit, offset); + } + + public List findAllActive(final Context context, final int limit, final int offset) + throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, SystemWideAlert.class); + Root alertRoot = criteriaQuery.from(SystemWideAlert.class); + criteriaQuery.select(alertRoot); + criteriaQuery.where(criteriaBuilder.equal(alertRoot.get(SystemWideAlert_.active), true)); + + return list(context, criteriaQuery, false, SystemWideAlert.class, limit, offset); + } + + +} diff --git a/dspace-api/src/main/java/org/dspace/alerts/service/SystemWideAlertService.java b/dspace-api/src/main/java/org/dspace/alerts/service/SystemWideAlertService.java new file mode 100644 index 0000000000..cf23130884 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/alerts/service/SystemWideAlertService.java @@ -0,0 +1,118 @@ +/** + * 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.alerts.service; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +import org.dspace.alerts.AllowSessionsEnum; +import org.dspace.alerts.SystemWideAlert; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; + +/** + * An interface for the SystemWideAlertService with methods regarding the SystemWideAlert workload + */ +public interface SystemWideAlertService { + + /** + * This method will create a SystemWideAlert object in the database + * + * @param context The relevant DSpace context + * @param message The message of the system-wide alert + * @param allowSessionsType Which sessions need to be allowed for the system-wide alert + * @param countdownTo The date to which to count down to when the system-wide alert is active + * @param active Whether the system-wide alert os active + * @return The created SystemWideAlert object + * @throws SQLException If something goes wrong + */ + SystemWideAlert create(Context context, String message, AllowSessionsEnum allowSessionsType, + Date countdownTo, boolean active + ) throws SQLException, AuthorizeException; + + /** + * This method will retrieve a SystemWideAlert object from the Database with the given ID + * + * @param context The relevant DSpace context + * @param alertId The alert id on which we'll search for in the database + * @return The system-wide alert that holds the given alert id + * @throws SQLException If something goes wrong + */ + SystemWideAlert find(Context context, int alertId) throws SQLException; + + /** + * Returns a list of all SystemWideAlert objects in the database + * + * @param context The relevant DSpace context + * @return The list of all SystemWideAlert objects in the Database + * @throws SQLException If something goes wrong + */ + List findAll(Context context) throws SQLException; + + /** + * Returns a list of all SystemWideAlert objects in the database + * + * @param context The relevant DSpace context + * @param limit The limit for the amount of system-wide alerts returned + * @param offset The offset for the system-wide alerts to be returned + * @return The list of all SystemWideAlert objects in the Database + * @throws SQLException If something goes wrong + */ + List findAll(Context context, int limit, int offset) throws SQLException; + + + /** + * Returns a list of all active SystemWideAlert objects in the database + * + * @param context The relevant DSpace context + * @return The list of all active SystemWideAlert objects in the database + * @throws SQLException If something goes wrong + */ + List findAllActive(Context context, int limit, int offset) throws SQLException; + + /** + * This method will delete the given SystemWideAlert object from the database + * + * @param context The relevant DSpace context + * @param systemWideAlert The SystemWideAlert object to be deleted + * @throws SQLException If something goes wrong + */ + void delete(Context context, SystemWideAlert systemWideAlert) + throws SQLException, IOException, AuthorizeException; + + + /** + * This method will be used to update the given SystemWideAlert object in the database + * + * @param context The relevant DSpace context + * @param systemWideAlert The SystemWideAlert object to be updated + * @throws SQLException If something goes wrong + */ + void update(Context context, SystemWideAlert systemWideAlert) throws SQLException, AuthorizeException; + + + /** + * Verifies if the user connected to the current context can retain its session + * + * @param context The relevant DSpace context + * @return if the user connected to the current context can retain its session + */ + boolean canUserMaintainSession(Context context, EPerson ePerson) throws SQLException; + + + /** + * Verifies if a non admin user can log in + * + * @param context The relevant DSpace context + * @return if a non admin user can log in + */ + boolean canNonAdminUserLogin(Context context) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java index 8488b4eaf1..ddfd38694f 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -736,7 +736,7 @@ public class CollectionServiceImpl extends DSpaceObjectServiceImpl i collection.getID(), collection.getHandle(), getIdentifiers(context, collection))); // remove subscriptions - hmm, should this be in Subscription.java? - subscribeService.deleteByCollection(context, collection); + subscribeService.deleteByDspaceObject(context, collection); // Remove Template Item removeTemplateItem(context, collection); diff --git a/dspace-api/src/main/java/org/dspace/content/CommunityServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CommunityServiceImpl.java index d0c414eba2..923745f761 100644 --- a/dspace-api/src/main/java/org/dspace/content/CommunityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CommunityServiceImpl.java @@ -36,6 +36,7 @@ import org.dspace.core.I18nUtil; import org.dspace.core.LogHelper; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; +import org.dspace.eperson.service.SubscribeService; import org.dspace.event.Event; import org.dspace.identifier.IdentifierException; import org.dspace.identifier.service.IdentifierService; @@ -73,7 +74,8 @@ public class CommunityServiceImpl extends DSpaceObjectServiceImpl imp protected SiteService siteService; @Autowired(required = true) protected IdentifierService identifierService; - + @Autowired(required = true) + protected SubscribeService subscribeService; protected CommunityServiceImpl() { super(); @@ -217,12 +219,12 @@ public class CommunityServiceImpl extends DSpaceObjectServiceImpl imp @Override public Bitstream setLogo(Context context, Community community, InputStream is) - throws AuthorizeException, IOException, SQLException { + throws AuthorizeException, IOException, SQLException { // Check authorisation // authorized to remove the logo when DELETE rights // authorized when canEdit if (!((is == null) && authorizeService.authorizeActionBoolean( - context, community, Constants.DELETE))) { + context, community, Constants.DELETE))) { canEdit(context, community); } @@ -242,7 +244,7 @@ public class CommunityServiceImpl extends DSpaceObjectServiceImpl imp // now create policy for logo bitstream // to match our READ policy List policies = authorizeService - .getPoliciesActionFilter(context, community, Constants.READ); + .getPoliciesActionFilter(context, community, Constants.READ); authorizeService.addPolicies(context, policies, newLogo); log.info(LogHelper.getHeader(context, "set_logo", @@ -549,6 +551,8 @@ public class CommunityServiceImpl extends DSpaceObjectServiceImpl imp context.addEvent(new Event(Event.DELETE, Constants.COMMUNITY, community.getID(), community.getHandle(), getIdentifiers(context, community))); + subscribeService.deleteByDspaceObject(context, community); + // Remove collections Iterator collections = community.getCollections().iterator(); diff --git a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java index 11cd4c107c..403d2f6fb6 100644 --- a/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/InstallItemServiceImpl.java @@ -10,9 +10,14 @@ package org.dspace.content; import java.io.IOException; import java.sql.SQLException; import java.util.List; +import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.logic.Filter; +import org.dspace.content.logic.FilterUtils; import org.dspace.content.service.CollectionService; import org.dspace.content.service.InstallItemService; import org.dspace.content.service.ItemService; @@ -20,6 +25,7 @@ import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.embargo.service.EmbargoService; import org.dspace.event.Event; +import org.dspace.identifier.Identifier; import org.dspace.identifier.IdentifierException; import org.dspace.identifier.service.IdentifierService; import org.springframework.beans.factory.annotation.Autowired; @@ -42,9 +48,11 @@ public class InstallItemServiceImpl implements InstallItemService { protected IdentifierService identifierService; @Autowired(required = true) protected ItemService itemService; + @Autowired(required = false) + + Logger log = LogManager.getLogger(InstallItemServiceImpl.class); protected InstallItemServiceImpl() { - } @Override @@ -59,10 +67,14 @@ public class InstallItemServiceImpl implements InstallItemService { AuthorizeException { Item item = is.getItem(); Collection collection = is.getCollection(); + // Get map of filters to use for identifier types. + Map, Filter> filters = FilterUtils.getIdentifierFilters(false); try { if (suppliedHandle == null) { - identifierService.register(c, item); + // Register with the filters we've set up + identifierService.register(c, item, filters); } else { + // This will register the handle but a pending DOI won't be compatible and so won't be registered identifierService.register(c, item, suppliedHandle); } } catch (IdentifierException e) { diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index 2c0ca9b1bb..de5826fa01 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -61,6 +61,7 @@ import org.dspace.discovery.indexobject.IndexableItem; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.GroupService; +import org.dspace.eperson.service.SubscribeService; import org.dspace.event.Event; import org.dspace.harvest.HarvestedItem; import org.dspace.harvest.service.HarvestedItemService; @@ -163,6 +164,9 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It @Autowired(required = true) private RequestItemService requestItemService; + @Autowired(required = true) + protected SubscribeService subscribeService; + protected ItemServiceImpl() { super(); } @@ -769,7 +773,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It log.info(LogHelper.getHeader(context, "delete_item", "item_id=" + item.getID())); - + //remove subscription related with it + subscribeService.deleteByDspaceObject(context, item); // Remove relationships for (Relationship relationship : relationshipService.findByItem(context, item, -1, -1, false, false)) { relationshipService.forceDelete(context, relationship, false, false); diff --git a/dspace-api/src/main/java/org/dspace/content/WorkspaceItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/WorkspaceItemServiceImpl.java index f40bb5256f..b847fd6352 100644 --- a/dspace-api/src/main/java/org/dspace/content/WorkspaceItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/WorkspaceItemServiceImpl.java @@ -24,6 +24,8 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.dao.WorkspaceItemDAO; +import org.dspace.content.logic.Filter; +import org.dspace.content.logic.FilterUtils; import org.dspace.content.service.CollectionService; import org.dspace.content.service.ItemService; import org.dspace.content.service.WorkspaceItemService; @@ -32,6 +34,13 @@ import org.dspace.core.Context; import org.dspace.core.LogHelper; import org.dspace.eperson.EPerson; import org.dspace.event.Event; +import org.dspace.identifier.DOI; +import org.dspace.identifier.DOIIdentifierProvider; +import org.dspace.identifier.Identifier; +import org.dspace.identifier.IdentifierException; +import org.dspace.identifier.factory.IdentifierServiceFactory; +import org.dspace.identifier.service.DOIService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.workflow.WorkflowItem; import org.dspace.workflow.WorkflowService; import org.springframework.beans.factory.annotation.Autowired; @@ -58,6 +67,8 @@ public class WorkspaceItemServiceImpl implements WorkspaceItemService { protected ItemService itemService; @Autowired(required = true) protected WorkflowService workflowService; + @Autowired(required = true) + protected DOIService doiService; protected WorkspaceItemServiceImpl() { @@ -160,6 +171,26 @@ public class WorkspaceItemServiceImpl implements WorkspaceItemService { } itemService.update(context, item); + + // If configured, register identifiers (eg handle, DOI) now. This is typically used with the Show Identifiers + // submission step which previews minted handles and DOIs during the submission process. Default: false + if (DSpaceServicesFactory.getInstance().getConfigurationService() + .getBooleanProperty("identifiers.submission.register", false)) { + try { + // Get map of filters to use for identifier types, while the item is in progress + Map, Filter> filters = FilterUtils.getIdentifierFilters(true); + IdentifierServiceFactory.getInstance().getIdentifierService().register(context, item, filters); + // Look for a DOI and move it to PENDING + DOI doi = doiService.findDOIByDSpaceObject(context, item); + if (doi != null) { + doi.setStatus(DOIIdentifierProvider.PENDING); + doiService.update(context, doi); + } + } catch (IdentifierException e) { + log.error("Could not register identifier(s) for item {}: {}", item.getID(), e.getMessage()); + } + } + workspaceItem.setItem(item); log.info(LogHelper.getHeader(context, "create_workspace_item", diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/SubscriptionDsoMetadataForEmailCompose.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/SubscriptionDsoMetadataForEmailCompose.java new file mode 100644 index 0000000000..05fda2b974 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/SubscriptionDsoMetadataForEmailCompose.java @@ -0,0 +1,80 @@ +/** + * 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.crosswalk; + +import static org.dspace.content.Item.ANY; + +import java.io.OutputStream; +import java.io.PrintStream; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.handle.factory.HandleServiceFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Creates a String to be sent as email body for subscriptions + * + * @author Alba Aliu + */ +public class SubscriptionDsoMetadataForEmailCompose implements StreamDisseminationCrosswalk { + + private List metadata = new ArrayList<>(); + + @Autowired + private ItemService itemService; + + @Override + public boolean canDisseminate(Context context, DSpaceObject dso) { + return Objects.nonNull(dso) && dso.getType() == Constants.ITEM; + } + + @Override + public void disseminate(Context context, DSpaceObject dso, OutputStream out) throws SQLException { + if (dso.getType() == Constants.ITEM) { + Item item = (Item) dso; + PrintStream printStream = new PrintStream(out); + for (String actualMetadata : metadata) { + String[] splitted = actualMetadata.split("\\."); + String qualifier = null; + if (splitted.length == 1) { + qualifier = splitted[2]; + } + var metadataValue = itemService.getMetadataFirstValue(item, splitted[0], splitted[1], qualifier, ANY); + printStream.print(metadataValue + " "); + } + String itemURL = HandleServiceFactory.getInstance() + .getHandleService() + .resolveToURL(context, item.getHandle()); + printStream.print(itemURL); + printStream.print("\n"); + printStream.close(); + } + } + + @Override + public String getMIMEType() { + return "text/plain"; + } + + public List getMetadata() { + return metadata; + } + + public void setMetadata(List metadata) { + this.metadata = metadata; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactory.java b/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactory.java index 4010e14861..a56dda41c6 100644 --- a/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactory.java +++ b/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactory.java @@ -33,6 +33,7 @@ import org.dspace.content.service.RelationshipTypeService; import org.dspace.content.service.SiteService; import org.dspace.content.service.SupervisedItemService; import org.dspace.content.service.WorkspaceItemService; +import org.dspace.eperson.service.SubscribeService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.workflow.factory.WorkflowServiceFactory; @@ -75,6 +76,8 @@ public abstract class ContentServiceFactory { public abstract SiteService getSiteService(); + public abstract SubscribeService getSubscribeService(); + /** * Return the implementation of the RelationshipTypeService interface * @@ -114,11 +117,7 @@ public abstract class ContentServiceFactory { } public DSpaceObjectService getDSpaceObjectService(T dso) { - // No need to worry when supressing, as long as our "getDSpaceObjectManager" method is properly implemented - // no casting issues should occur - @SuppressWarnings("unchecked") - DSpaceObjectService manager = getDSpaceObjectService(dso.getType()); - return manager; + return getDSpaceObjectService(dso.getType()); } @SuppressWarnings("unchecked") diff --git a/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactoryImpl.java index 6f123ae1ba..05eabcccdc 100644 --- a/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactoryImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/factory/ContentServiceFactoryImpl.java @@ -30,6 +30,7 @@ import org.dspace.content.service.RelationshipTypeService; import org.dspace.content.service.SiteService; import org.dspace.content.service.SupervisedItemService; import org.dspace.content.service.WorkspaceItemService; +import org.dspace.eperson.service.SubscribeService; import org.springframework.beans.factory.annotation.Autowired; /** @@ -71,7 +72,8 @@ public class ContentServiceFactoryImpl extends ContentServiceFactory { private SupervisedItemService supervisedItemService; @Autowired(required = true) private SiteService siteService; - + @Autowired(required = true) + private SubscribeService subscribeService; @Autowired(required = true) private RelationshipService relationshipService; @Autowired(required = true) @@ -158,6 +160,11 @@ public class ContentServiceFactoryImpl extends ContentServiceFactory { return siteService; } + @Override + public SubscribeService getSubscribeService() { + return subscribeService ; + } + @Override public RelationshipTypeService getRelationshipTypeService() { return relationshipTypeService; diff --git a/dspace-api/src/main/java/org/dspace/content/logic/DefaultFilter.java b/dspace-api/src/main/java/org/dspace/content/logic/DefaultFilter.java index 490c3949ea..1ac3930952 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/DefaultFilter.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/DefaultFilter.java @@ -18,10 +18,10 @@ import org.dspace.core.Context; * statement as a property (unlike an operator) and takes no parameters (unlike a condition) * * @author Kim Shepherd - * @version $Revision$ */ public class DefaultFilter implements Filter { private LogicalStatement statement; + private String name; private final static Logger log = LogManager.getLogger(); /** @@ -44,4 +44,15 @@ public class DefaultFilter implements Filter { public boolean getResult(Context context, Item item) throws LogicalStatementException { return this.statement.getResult(context, item); } + + @Override + public void setBeanName(String name) { + log.debug("Initialize bean " + name); + this.name = name; + } + + @Override + public String getName() { + return name; + } } diff --git a/dspace-api/src/main/java/org/dspace/content/logic/Filter.java b/dspace-api/src/main/java/org/dspace/content/logic/Filter.java index 84e9d6bc08..f789860e77 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/Filter.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/Filter.java @@ -9,6 +9,7 @@ package org.dspace.content.logic; import org.dspace.content.Item; import org.dspace.core.Context; +import org.springframework.beans.factory.BeanNameAware; /** * The interface for Filter currently doesn't add anything to LogicalStatement but inherits from it @@ -22,7 +23,7 @@ import org.dspace.core.Context; * @author Kim Shepherd * @see org.dspace.content.logic.DefaultFilter */ -public interface Filter extends LogicalStatement { +public interface Filter extends LogicalStatement, BeanNameAware { /** * Get the result of logical evaluation for an item * @param context DSpace context @@ -32,4 +33,11 @@ public interface Filter extends LogicalStatement { */ @Override boolean getResult(Context context, Item item) throws LogicalStatementException; + + /** + * Get the name of a filter. This can be used by filters which make use of BeanNameAware + * to return the bean name. + * @return the id/name of this spring bean + */ + String getName(); } diff --git a/dspace-api/src/main/java/org/dspace/content/logic/FilterUtils.java b/dspace-api/src/main/java/org/dspace/content/logic/FilterUtils.java new file mode 100644 index 0000000000..a878d69e6e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/logic/FilterUtils.java @@ -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.content.logic; + +import java.util.HashMap; +import java.util.Map; + +import org.dspace.identifier.DOI; +import org.dspace.identifier.Handle; +import org.dspace.identifier.Identifier; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * General utility methods for logical item filtering + * + * @author Kim Shepherd + */ +public class FilterUtils { + + @Autowired(required = true) + ConfigurationService configurationService; + + /** + * Get a Filter by configuration property name + * For example, if a module has implemented "my-feature.filter" configuration property + * this method will return a filter with the ID specified by the configuration property + * @param property DSpace configuration property name (Apache Commons config) + * @return Filter object, with a bean ID configured for this property key, or null + */ + public static Filter getFilterFromConfiguration(String property) { + String filterName = DSpaceServicesFactory.getInstance().getConfigurationService().getProperty(property); + if (filterName != null) { + return DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName(filterName, Filter.class); + } + return null; + } + + /** + * Get a Filter by configuration property name + * For example, if a module has implemented "my-feature.filter" configuration property + * this method will return a filter with the ID specified by the configuration property + * @param property DSpace configuration property name (Apache Commons config) + * @return Filter object, with a bean ID configured for this property key, or default filter + */ + public static Filter getFilterFromConfiguration(String property, Filter defaultFilter) { + Filter filter = getFilterFromConfiguration(property); + if (filter != null) { + return filter; + } + return defaultFilter; + } + + /** + * Get a map of identifier types and filters to use when creating workspace or archived items + * This is used by services installing new archived or workspace items to filter by identifier type + * as some filters should apply to DOI creation but not Handle creation, and so on. + * The in progress or archived status will be used to load the appropriate filter from configuration + *

+ * @param inProgress + * @return + */ + public static Map, Filter> getIdentifierFilters(boolean inProgress) { + String configurationSuffix = "install"; + if (inProgress) { + configurationSuffix = "workspace"; + } + Map, Filter> filters = new HashMap<>(); + // Put DOI 'can we create DOI on install / workspace?' filter + Filter filter = FilterUtils.getFilterFromConfiguration("identifiers.submission.filter." + configurationSuffix); + // A null filter should be handled safely by the identifier provier (default, or "always true") + filters.put(DOI.class, filter); + // This won't have an affect until handle providers implement filtering, but is an example of + // how the filters can be used for other types + filters.put(Handle.class, DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName( + "always_true_filter", TrueFilter.class)); + return filters; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/logic/LogicalStatement.java b/dspace-api/src/main/java/org/dspace/content/logic/LogicalStatement.java index 5fc3e76cd5..0119f48b51 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/LogicalStatement.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/LogicalStatement.java @@ -17,7 +17,6 @@ import org.dspace.core.Context; * used as sub-statements in other Filters and Operators. * * @author Kim Shepherd - * @version $Revision$ */ public interface LogicalStatement { /** diff --git a/dspace-api/src/main/java/org/dspace/content/logic/LogicalStatementException.java b/dspace-api/src/main/java/org/dspace/content/logic/LogicalStatementException.java index 758a0a7124..4e3b3e3b7d 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/LogicalStatementException.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/LogicalStatementException.java @@ -12,7 +12,6 @@ package org.dspace.content.logic; * defined as spring beans. * * @author Kim Shepherd - * @version $Revision$ */ public class LogicalStatementException extends RuntimeException { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/TestLogicRunner.java b/dspace-api/src/main/java/org/dspace/content/logic/TestLogicRunner.java index b78de7f190..bf218eaa8a 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/TestLogicRunner.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/TestLogicRunner.java @@ -33,7 +33,6 @@ import org.dspace.services.factory.DSpaceServicesFactory; * A command-line runner used for testing a logical filter against an item, or all items * * @author Kim Shepherd - * @version $Revision$ */ public class TestLogicRunner { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/TrueFilter.java b/dspace-api/src/main/java/org/dspace/content/logic/TrueFilter.java new file mode 100644 index 0000000000..b15ab4eaaa --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/logic/TrueFilter.java @@ -0,0 +1,41 @@ +/** + * 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.logic; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Item; +import org.dspace.core.Context; + +/** + * Extremely simple filter that always returns true! + * Useful to pass to methods that expect a filter, in order to effectively say "all items". + * This could be configured in Spring XML but it is more stable and reliable to have it hard-coded here + * so that any broken configuration doesn't silently break parts of DSpace that expect it to work. + * + * @author Kim Shepherd + */ +public class TrueFilter implements Filter { + private String name; + private final static Logger log = LogManager.getLogger(); + + public boolean getResult(Context context, Item item) throws LogicalStatementException { + return true; + } + + @Override + public void setBeanName(String name) { + log.debug("Initialize bean " + name); + this.name = name; + } + + @Override + public String getName() { + return name; + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/AbstractCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/AbstractCondition.java index 0202243265..ce5b274a8d 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/AbstractCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/AbstractCondition.java @@ -23,7 +23,6 @@ import org.springframework.beans.factory.annotation.Autowired; * Abstract class for conditions, to implement the basic getter and setter parameters * * @author Kim Shepherd - * @version $Revision$ */ public abstract class AbstractCondition implements Condition { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/BitstreamCountCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/BitstreamCountCondition.java index 635f0997d3..36e506122e 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/BitstreamCountCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/BitstreamCountCondition.java @@ -18,7 +18,6 @@ import org.dspace.core.Context; * A condition to evaluate an item based on how many bitstreams it has in a particular bundle * * @author Kim Shepherd - * @version $Revision$ */ public class BitstreamCountCondition extends AbstractCondition { /** diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/Condition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/Condition.java index c86509899f..7647dce4a4 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/Condition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/Condition.java @@ -22,7 +22,6 @@ import org.dspace.core.Context; * operator is not a condition but also a logical statement. * * @author Kim Shepherd - * @version $Revision$ */ public interface Condition extends LogicalStatement { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/InCollectionCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/InCollectionCondition.java index 0aaa1bff1d..df94f183d1 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/InCollectionCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/InCollectionCondition.java @@ -23,7 +23,6 @@ import org.dspace.core.Context; * if the item belongs to any of them. * * @author Kim Shepherd - * @version $Revision$ */ public class InCollectionCondition extends AbstractCondition { private static Logger log = LogManager.getLogger(InCollectionCondition.class); diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/InCommunityCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/InCommunityCondition.java index 9f588f9c3b..6a72011e73 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/InCommunityCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/InCommunityCondition.java @@ -24,7 +24,6 @@ import org.dspace.core.Context; * if the item belongs to any of them. * * @author Kim Shepherd - * @version $Revision$ */ public class InCommunityCondition extends AbstractCondition { private final static Logger log = LogManager.getLogger(); diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/IsArchivedCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/IsArchivedCondition.java new file mode 100644 index 0000000000..4f50d2b6f6 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/IsArchivedCondition.java @@ -0,0 +1,37 @@ +/** + * 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.logic.condition; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Item; +import org.dspace.content.logic.LogicalStatementException; +import org.dspace.core.Context; + +/** + * A condition that returns true if the item is archived + * + * @author Kim Shepherd + */ +public class IsArchivedCondition extends AbstractCondition { + private final static Logger log = LogManager.getLogger(); + + /** + * Return true if item is archived + * Return false if not + * @param context DSpace context + * @param item Item to evaluate + * @return boolean result of evaluation + * @throws LogicalStatementException + */ + @Override + public boolean getResult(Context context, Item item) throws LogicalStatementException { + log.debug("Result of isArchived is " + item.isArchived()); + return item.isArchived(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/IsWithdrawnCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/IsWithdrawnCondition.java index 6424e6f35f..850b69bda0 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/IsWithdrawnCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/IsWithdrawnCondition.java @@ -17,7 +17,6 @@ import org.dspace.core.Context; * A condition that returns true if the item is withdrawn * * @author Kim Shepherd - * @version $Revision$ */ public class IsWithdrawnCondition extends AbstractCondition { private final static Logger log = LogManager.getLogger(); diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValueMatchCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValueMatchCondition.java index 4e30c75a2a..e87c479de6 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValueMatchCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValueMatchCondition.java @@ -23,7 +23,6 @@ import org.dspace.core.Context; * in a given metadata field * * @author Kim Shepherd - * @version $Revision$ */ public class MetadataValueMatchCondition extends AbstractCondition { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValuesMatchCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValuesMatchCondition.java index 74ccfa4ca8..c6ca9dfb9f 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValuesMatchCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/MetadataValuesMatchCondition.java @@ -23,7 +23,6 @@ import org.dspace.core.Context; * in a given metadata field * * @author Kim Shepherd - * @version $Revision$ */ public class MetadataValuesMatchCondition extends AbstractCondition { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java b/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java index 65f9925222..20138beb47 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/condition/ReadableByGroupCondition.java @@ -25,7 +25,6 @@ import org.dspace.core.Context; * can perform the action on a given item * * @author Kim Shepherd - * @version $Revision$ */ public class ReadableByGroupCondition extends AbstractCondition { private final static Logger log = LogManager.getLogger(); diff --git a/dspace-api/src/main/java/org/dspace/content/logic/operator/AbstractOperator.java b/dspace-api/src/main/java/org/dspace/content/logic/operator/AbstractOperator.java index 99ece622f7..3882414def 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/operator/AbstractOperator.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/operator/AbstractOperator.java @@ -22,7 +22,6 @@ import org.dspace.core.Context; * as a logical result * * @author Kim Shepherd - * @version $Revision$ */ public abstract class AbstractOperator implements LogicalStatement { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/operator/And.java b/dspace-api/src/main/java/org/dspace/content/logic/operator/And.java index 26606f2099..79bc5c381e 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/operator/And.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/operator/And.java @@ -19,7 +19,6 @@ import org.dspace.core.Context; * true if all sub-statements return true * * @author Kim Shepherd - * @version $Revision$ */ public class And extends AbstractOperator { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/operator/Nand.java b/dspace-api/src/main/java/org/dspace/content/logic/operator/Nand.java index 1021ec6722..2a4b6823b6 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/operator/Nand.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/operator/Nand.java @@ -18,7 +18,6 @@ import org.dspace.core.Context; * An operator that implements NAND by negating an AND operation * * @author Kim Shepherd - * @version $Revision$ */ public class Nand extends AbstractOperator { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/operator/Not.java b/dspace-api/src/main/java/org/dspace/content/logic/operator/Not.java index 35c7bb22a7..277acdfd01 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/operator/Not.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/operator/Not.java @@ -19,7 +19,6 @@ import org.dspace.core.Context; * Not can have one sub-statement only, while and, or, nor, ... can have multiple sub-statements. * * @author Kim Shepherd - * @version $Revision$ */ public class Not implements LogicalStatement { diff --git a/dspace-api/src/main/java/org/dspace/content/logic/operator/Or.java b/dspace-api/src/main/java/org/dspace/content/logic/operator/Or.java index 5110ac31ba..e5697f8cc3 100644 --- a/dspace-api/src/main/java/org/dspace/content/logic/operator/Or.java +++ b/dspace-api/src/main/java/org/dspace/content/logic/operator/Or.java @@ -19,7 +19,6 @@ import org.dspace.core.Context; * true if one or more sub-statements return true * * @author Kim Shepherd - * @version $Revision$ */ public class Or extends AbstractOperator { diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/RegisterDOI.java b/dspace-api/src/main/java/org/dspace/ctask/general/RegisterDOI.java index 4e777d70a8..0765d7b000 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/RegisterDOI.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/RegisterDOI.java @@ -13,11 +13,15 @@ import java.sql.SQLException; import org.apache.logging.log4j.Logger; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; +import org.dspace.content.logic.Filter; +import org.dspace.content.logic.FilterUtils; +import org.dspace.content.logic.TrueFilter; import org.dspace.curate.AbstractCurationTask; import org.dspace.curate.Curator; import org.dspace.identifier.DOIIdentifierProvider; import org.dspace.identifier.IdentifierException; import org.dspace.identifier.doi.DOIIdentifierNotApplicableException; +import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.utils.DSpace; /** @@ -39,6 +43,7 @@ public class RegisterDOI extends AbstractCurationTask { private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(RegisterDOI.class); // DOI provider private DOIIdentifierProvider provider; + private Filter trueFilter; /** * Initialise the curation task and read configuration, instantiate the DOI provider @@ -46,14 +51,14 @@ public class RegisterDOI extends AbstractCurationTask { @Override public void init(Curator curator, String taskId) throws IOException { super.init(curator, taskId); - // Get 'skip filter' behaviour from configuration, with a default value of 'true' - skipFilter = configurationService.getBooleanProperty(PLUGIN_PREFIX + ".skip-filter", true); // Get distribution behaviour from configuration, with a default value of 'false' distributed = configurationService.getBooleanProperty(PLUGIN_PREFIX + ".distributed", false); log.debug("PLUGIN_PREFIX = " + PLUGIN_PREFIX + ", skipFilter = " + skipFilter + ", distributed = " + distributed); // Instantiate DOI provider singleton provider = new DSpace().getSingletonService(DOIIdentifierProvider.class); + trueFilter = DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName( + "always_true_filter", TrueFilter.class); } /** @@ -118,8 +123,9 @@ public class RegisterDOI extends AbstractCurationTask { String doi = null; // Attempt DOI registration and report successes and failures try { - log.debug("Registering DOI with skipFilter = " + skipFilter); - doi = provider.register(Curator.curationContext(), item, skipFilter); + Filter filter = FilterUtils.getFilterFromConfiguration("identifiers.submission.filter.curation", + trueFilter); + doi = provider.register(Curator.curationContext(), item, filter); if (doi != null) { String message = "New DOI minted in database for item " + item.getHandle() + ": " + doi + ". This DOI will be registered online with the DOI provider when the queue is next run"; diff --git a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java index 636e7ccd2a..c02c83ece6 100644 --- a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java +++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryConfigurationService.java @@ -139,4 +139,23 @@ public class DiscoveryConfigurationService { } } } + + /** + * Retrieves a list of all DiscoveryConfiguration objects where key starts with prefixConfigurationName + * + * @param prefixConfigurationName string as prefix key + */ + public List getDiscoveryConfigurationWithPrefixName(final String prefixConfigurationName) { + List discoveryConfigurationList = new ArrayList<>(); + if (StringUtils.isNotBlank(prefixConfigurationName)) { + for (String key : map.keySet()) { + if (key.equals(prefixConfigurationName) || key.startsWith(prefixConfigurationName)) { + DiscoveryConfiguration config = map.get(key); + discoveryConfigurationList.add(config); + } + } + } + return discoveryConfigurationList; + } + } diff --git a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryRelatedItemConfiguration.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryRelatedItemConfiguration.java new file mode 100644 index 0000000000..6c24a6bac6 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoveryRelatedItemConfiguration.java @@ -0,0 +1,16 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.discovery.configuration; + +/** + * This class extends {@link DiscoveryConfiguration} and add method for set parameters + * to filter query list + * + * @author Danilo Di Nuzzo (danilo.dinuzzo at 4science.it) + */ +public class DiscoveryRelatedItemConfiguration extends DiscoveryConfiguration {} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortFunctionConfiguration.java b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortFunctionConfiguration.java new file mode 100644 index 0000000000..7fb020cd56 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/discovery/configuration/DiscoverySortFunctionConfiguration.java @@ -0,0 +1,66 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.discovery.configuration; + +import java.io.Serializable; +import java.text.MessageFormat; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * + * Extension of {@link DiscoverySortFieldConfiguration} used to configure sorting + * taking advantage of solr function feature. + * + * Order is evaluated by mean of function parameter value and passed in arguments as input. + * + * @author Corrado Lombardi (corrado.lombardi at 4science.it) + * + */ +public class DiscoverySortFunctionConfiguration extends DiscoverySortFieldConfiguration { + + public static final String SORT_FUNCTION = "sort_function"; + private String function; + private List arguments; + private String id; + + public void setFunction(final String function) { + this.function = function; + } + + public void setArguments(final List arguments) { + this.arguments = arguments; + } + + @Override + public String getType() { + return SORT_FUNCTION; + } + + @Override + public String getMetadataField() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + /** + * Returns the function to be used by solr to sort result + * @param functionArgs variable arguments to be inserted in function + * @return + */ + public String getFunction(final Serializable... functionArgs) { + final String args = String.join(",", Optional.ofNullable(arguments).orElse(Collections.emptyList())); + final String result = function + "(" + args + ")"; + return MessageFormat.format(result, functionArgs); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/FrequencyType.java b/dspace-api/src/main/java/org/dspace/eperson/FrequencyType.java new file mode 100644 index 0000000000..72822fb871 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/FrequencyType.java @@ -0,0 +1,81 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.eperson; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Calendar; + +import org.apache.commons.codec.binary.StringUtils; + +/** + * This enum holds all the possible frequency types + * that can be used in "subscription-send" script + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public enum FrequencyType { + DAY("D"), + WEEK("W"), + MONTH("M"); + + private String shortName; + + private FrequencyType(String shortName) { + this.shortName = shortName; + } + + public static String findLastFrequency(String frequency) { + String startDate = ""; + String endDate = ""; + Calendar cal = Calendar.getInstance(); + // Full ISO 8601 is e.g. + SimpleDateFormat fullIsoStart = new SimpleDateFormat("yyyy-MM-dd'T'00:00:00'Z'"); + SimpleDateFormat fullIsoEnd = new SimpleDateFormat("yyyy-MM-dd'T'23:59:59'Z'"); + switch (frequency) { + case "D": + cal.add(Calendar.DAY_OF_MONTH, -1); + endDate = fullIsoEnd.format(cal.getTime()); + startDate = fullIsoStart.format(cal.getTime()); + break; + case "M": + int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH); + cal.add(Calendar.DAY_OF_MONTH, -dayOfMonth); + endDate = fullIsoEnd.format(cal.getTime()); + cal.add(Calendar.MONTH, -1); + cal.add(Calendar.DAY_OF_MONTH, 1); + startDate = fullIsoStart.format(cal.getTime()); + break; + case "W": + cal.add(Calendar.DAY_OF_WEEK, -1); + int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK) - 1; + cal.add(Calendar.DAY_OF_WEEK, -dayOfWeek); + endDate = fullIsoEnd.format(cal.getTime()); + cal.add(Calendar.DAY_OF_WEEK, -6); + startDate = fullIsoStart.format(cal.getTime()); + break; + default: + return null; + } + return "[" + startDate + " TO " + endDate + "]"; + } + + public static boolean isSupportedFrequencyType(String value) { + for (FrequencyType ft : Arrays.asList(FrequencyType.values())) { + if (StringUtils.equals(ft.getShortName(), value)) { + return true; + } + } + return false; + } + + public String getShortName() { + return shortName; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/SubscribeCLITool.java b/dspace-api/src/main/java/org/dspace/eperson/SubscribeCLITool.java deleted file mode 100644 index 9e5ecaa4fb..0000000000 --- a/dspace-api/src/main/java/org/dspace/eperson/SubscribeCLITool.java +++ /dev/null @@ -1,432 +0,0 @@ -/** - * The contents of this file are subject to the license and copyright - * detailed in the LICENSE and NOTICE files at the root of the source - * tree and available online at - * - * http://www.dspace.org/license/ - */ -package org.dspace.eperson; - -import java.io.IOException; -import java.sql.SQLException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.ResourceBundle; -import java.util.TimeZone; -import javax.mail.MessagingException; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.Logger; -import org.dspace.content.Collection; -import org.dspace.content.DCDate; -import org.dspace.content.Item; -import org.dspace.content.MetadataSchemaEnum; -import org.dspace.content.MetadataValue; -import org.dspace.content.factory.ContentServiceFactory; -import org.dspace.content.service.ItemService; -import org.dspace.core.Context; -import org.dspace.core.Email; -import org.dspace.core.I18nUtil; -import org.dspace.core.LogHelper; -import org.dspace.eperson.factory.EPersonServiceFactory; -import org.dspace.eperson.service.SubscribeService; -import org.dspace.handle.factory.HandleServiceFactory; -import org.dspace.handle.service.HandleService; -import org.dspace.search.Harvest; -import org.dspace.search.HarvestedItemInfo; -import org.dspace.services.ConfigurationService; -import org.dspace.services.factory.DSpaceServicesFactory; - -/** - * CLI tool used for sending new item e-mail alerts to users - * - * @author Robert Tansley - * @version $Revision$ - */ -public class SubscribeCLITool { - - private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SubscribeCLITool.class); - - private static final HandleService handleService - = HandleServiceFactory.getInstance().getHandleService(); - private static final ItemService itemService - = ContentServiceFactory.getInstance().getItemService(); - private static final SubscribeService subscribeService - = EPersonServiceFactory.getInstance().getSubscribeService(); - private static final ConfigurationService configurationService - = DSpaceServicesFactory.getInstance().getConfigurationService(); - - /** - * Default constructor - */ - private SubscribeCLITool() { } - - /** - * Process subscriptions. This must be invoked only once a day. Messages are - * only sent out when a collection has actually received new items, so that - * people's mailboxes are not clogged with many "no new items" mails. - *

- * Yesterday's newly available items are included. If this is run at for - * example midday, any items that have been made available during the - * current day will not be included, but will be included in the next day's - * run. - *

- * For example, if today's date is 2002-10-10 (in UTC) items made available - * during 2002-10-09 (UTC) will be included. - * - * @param context The relevant DSpace Context. - * @param test If true, do a "dry run", i.e. don't actually send email, just log the attempt - * @throws SQLException An exception that provides information on a database access error or other errors. - * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations. - */ - public static void processDaily(Context context, boolean test) throws SQLException, - IOException { - // Grab the subscriptions - - List subscriptions = subscribeService.findAll(context); - - EPerson currentEPerson = null; - List collections = null; // List of Collections - - // Go through the list collating subscriptions for each e-person - for (Subscription subscription : subscriptions) { - // Does this row relate to the same e-person as the last? - if ((currentEPerson == null) - || (!subscription.getePerson().getID().equals(currentEPerson - .getID()))) { - // New e-person. Send mail for previous e-person - if (currentEPerson != null) { - - try { - sendEmail(context, currentEPerson, collections, test); - } catch (MessagingException me) { - log.error("Failed to send subscription to eperson_id=" - + currentEPerson.getID()); - log.error(me); - } - } - - currentEPerson = subscription.getePerson(); - collections = new ArrayList<>(); - } - - collections.add(subscription.getCollection()); - } - - // Process the last person - if (currentEPerson != null) { - try { - sendEmail(context, currentEPerson, collections, test); - } catch (MessagingException me) { - log.error("Failed to send subscription to eperson_id=" - + currentEPerson.getID()); - log.error(me); - } - } - } - - /** - * Sends an email to the given e-person with details of new items in the - * given collections, items that appeared yesterday. No e-mail is sent if - * there aren't any new items in any of the collections. - * - * @param context DSpace context object - * @param eperson eperson to send to - * @param collections List of collection IDs (Integers) - * @param test If true, do a "dry run", i.e. don't actually send email, just log the attempt - * @throws IOException A general class of exceptions produced by failed or interrupted I/O operations. - * @throws MessagingException A general class of exceptions for sending email. - * @throws SQLException An exception that provides information on a database access error or other errors. - */ - public static void sendEmail(Context context, EPerson eperson, - List collections, boolean test) throws IOException, MessagingException, - SQLException { - // Get a resource bundle according to the eperson language preferences - Locale supportedLocale = I18nUtil.getEPersonLocale(eperson); - ResourceBundle labels = ResourceBundle.getBundle("Messages", supportedLocale); - - // Get the start and end dates for yesterday - - // The date should reflect the timezone as well. Otherwise we stand to lose that information - // in truncation and roll to an earlier date than intended. - Calendar cal = Calendar.getInstance(TimeZone.getDefault()); - cal.setTime(new Date()); - - // What we actually want to pass to Harvest is "Midnight of yesterday in my current timezone" - // Truncation will actually pass in "Midnight of yesterday in UTC", which will be, - // at least in CDT, "7pm, the day before yesterday, in my current timezone". - cal.add(Calendar.HOUR, -24); - cal.set(Calendar.HOUR_OF_DAY, 0); - cal.set(Calendar.MINUTE, 0); - cal.set(Calendar.SECOND, 0); - Date midnightYesterday = cal.getTime(); - - - // FIXME: text of email should be more configurable from an - // i18n viewpoint - StringBuilder emailText = new StringBuilder(); - boolean isFirst = true; - - for (int i = 0; i < collections.size(); i++) { - Collection c = collections.get(i); - - try { - boolean includeAll = configurationService - .getBooleanProperty("harvest.includerestricted.subscription", true); - - // we harvest all the changed item from yesterday until now - List itemInfos = Harvest - .harvest(context, c, new DCDate(midnightYesterday).toString(), null, 0, // Limit - // and - // offset - // zero, - // get - // everything - 0, true, // Need item objects - false, // But not containers - false, // Or withdrawals - includeAll); - - if (configurationService.getBooleanProperty("eperson.subscription.onlynew", false)) { - // get only the items archived yesterday - itemInfos = filterOutModified(itemInfos); - } else { - // strip out the item archived today or - // not archived yesterday and modified today - itemInfos = filterOutToday(itemInfos); - } - - // Only add to buffer if there are new items - if (itemInfos.size() > 0) { - if (!isFirst) { - emailText - .append("\n---------------------------------------\n"); - } else { - isFirst = false; - } - - emailText.append(labels.getString("org.dspace.eperson.Subscribe.new-items")).append(" ").append( - c.getName()).append(": ").append( - itemInfos.size()).append("\n\n"); - - for (int j = 0; j < itemInfos.size(); j++) { - HarvestedItemInfo hii = (HarvestedItemInfo) itemInfos - .get(j); - - String title = hii.item.getName(); - emailText.append(" ").append(labels.getString("org.dspace.eperson.Subscribe.title")) - .append(" "); - - if (StringUtils.isNotBlank(title)) { - emailText.append(title); - } else { - emailText.append(labels.getString("org.dspace.eperson.Subscribe.untitled")); - } - - List authors = itemService - .getMetadata(hii.item, MetadataSchemaEnum.DC.getName(), "contributor", Item.ANY, Item.ANY); - - if (authors.size() > 0) { - emailText.append("\n ").append(labels.getString("org.dspace.eperson.Subscribe.authors")) - .append(" ").append( - authors.get(0).getValue()); - - for (int k = 1; k < authors.size(); k++) { - emailText.append("\n ").append( - authors.get(k).getValue()); - } - } - - emailText.append("\n ").append(labels.getString("org.dspace.eperson.Subscribe.id")) - .append(" ").append( - handleService.getCanonicalForm(hii.handle)).append( - "\n\n"); - } - } - } catch (ParseException pe) { - // This should never get thrown as the Dates are auto-generated - } - } - - // Send an e-mail if there were any new items - if (emailText.length() > 0) { - - if (test) { - log.info(LogHelper.getHeader(context, "subscription:", "eperson=" + eperson.getEmail())); - log.info(LogHelper.getHeader(context, "subscription:", "text=" + emailText.toString())); - - } else { - - Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "subscription")); - email.addRecipient(eperson.getEmail()); - email.addArgument(emailText.toString()); - email.send(); - - log.info(LogHelper.getHeader(context, "sent_subscription", "eperson_id=" + eperson.getID())); - - } - - - } - } - - /** - * Method for invoking subscriptions via the command line - * - * @param argv the command line arguments given - */ - public static void main(String[] argv) { - String usage = "org.dspace.eperson.Subscribe [-t] or nothing to send out subscriptions."; - - Options options = new Options(); - HelpFormatter formatter = new HelpFormatter(); - CommandLine line = null; - - { - Option opt = new Option("t", "test", false, "Run test session"); - opt.setRequired(false); - options.addOption(opt); - } - - { - Option opt = new Option("h", "help", false, "Print this help message"); - opt.setRequired(false); - options.addOption(opt); - } - - try { - line = new DefaultParser().parse(options, argv); - } catch (org.apache.commons.cli.ParseException e) { - // automatically generate the help statement - formatter.printHelp(usage, e.getMessage(), options, ""); - System.exit(1); - } - - if (line.hasOption("h")) { - // automatically generate the help statement - formatter.printHelp(usage, options); - System.exit(1); - } - - boolean test = line.hasOption("t"); - - Context context = null; - - try { - context = new Context(Context.Mode.READ_ONLY); - processDaily(context, test); - context.complete(); - } catch (IOException | SQLException e) { - log.fatal(e); - } finally { - if (context != null && context.isValid()) { - // Nothing is actually written - context.abort(); - } - } - } - - private static List filterOutToday(List completeList) { - log.debug("Filtering out all today item to leave new items list size=" - + completeList.size()); - List filteredList = new ArrayList<>(); - - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); - String today = sdf.format(new Date()); - // Get the start and end dates for yesterday - Date thisTimeYesterday = new Date(System.currentTimeMillis() - - (24 * 60 * 60 * 1000)); - String yesterday = sdf.format(thisTimeYesterday); - - for (HarvestedItemInfo infoObject : completeList) { - Date lastUpdate = infoObject.item.getLastModified(); - String lastUpdateStr = sdf.format(lastUpdate); - - // has the item modified today? - if (lastUpdateStr.equals(today)) { - List dateAccArr = itemService.getMetadata(infoObject.item, "dc", - "date", "accessioned", Item.ANY); - // we need only the item archived yesterday - if (dateAccArr != null && dateAccArr.size() > 0) { - for (MetadataValue date : dateAccArr) { - if (date != null && date.getValue() != null) { - // if it hasn't been archived today - if (date.getValue().startsWith(yesterday)) { - filteredList.add(infoObject); - log.debug("adding : " + dateAccArr.get(0).getValue() - + " : " + today + " : " - + infoObject.handle); - break; - } else { - log.debug("ignoring : " + dateAccArr.get(0).getValue() - + " : " + today + " : " - + infoObject.handle); - } - } - } - } else { - log.debug("no date accessioned, adding : " - + infoObject.handle); - filteredList.add(infoObject); - } - } else { - // the item has been modified yesterday... - filteredList.add(infoObject); - } - } - - return filteredList; - } - - private static List filterOutModified(List completeList) { - log.debug("Filtering out all modified to leave new items list size=" + completeList.size()); - List filteredList = new ArrayList<>(); - - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); - // Get the start and end dates for yesterday - Date thisTimeYesterday = new Date(System.currentTimeMillis() - - (24 * 60 * 60 * 1000)); - String yesterday = sdf.format(thisTimeYesterday); - - for (HarvestedItemInfo infoObject : completeList) { - List dateAccArr = itemService - .getMetadata(infoObject.item, "dc", "date", "accessioned", Item.ANY); - - if (dateAccArr != null && dateAccArr.size() > 0) { - for (MetadataValue date : dateAccArr) { - if (date != null && date.getValue() != null) { - // if it has been archived yesterday - if (date.getValue().startsWith(yesterday)) { - filteredList.add(infoObject); - log.debug("adding : " + dateAccArr.get(0) - .getValue() + " : " + yesterday + " : " + infoObject - .handle); - break; - } else { - log.debug("ignoring : " + dateAccArr.get(0) - .getValue() + " : " + yesterday + " : " + infoObject - .handle); - } - } - } - - - } else { - log.debug("no date accessioned, adding : " + infoObject.handle); - filteredList.add(infoObject); - } - } - - return filteredList; - } -} diff --git a/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java index 81c367f0ea..2e4d94f443 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/SubscribeServiceImpl.java @@ -9,11 +9,16 @@ package org.dspace.eperson; import java.sql.SQLException; import java.util.List; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; import org.dspace.content.service.CollectionService; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -29,106 +34,177 @@ import org.springframework.beans.factory.annotation.Autowired; * @version $Revision$ */ public class SubscribeServiceImpl implements SubscribeService { - /** - * log4j logger - */ - private Logger log = org.apache.logging.log4j.LogManager.getLogger(SubscribeServiceImpl.class); + + private Logger log = LogManager.getLogger(SubscribeServiceImpl.class); @Autowired(required = true) - protected SubscriptionDAO subscriptionDAO; - + private SubscriptionDAO subscriptionDAO; @Autowired(required = true) - protected AuthorizeService authorizeService; + private AuthorizeService authorizeService; @Autowired(required = true) - protected CollectionService collectionService; - - protected SubscribeServiceImpl() { - - } + private CollectionService collectionService; @Override - public List findAll(Context context) throws SQLException { - return subscriptionDAO.findAllOrderedByEPerson(context); - } - - @Override - public void subscribe(Context context, EPerson eperson, - Collection collection) throws SQLException, AuthorizeException { - // Check authorisation. Must be administrator, or the eperson. - if (authorizeService.isAdmin(context) - || ((context.getCurrentUser() != null) && (context - .getCurrentUser().getID().equals(eperson.getID())))) { - if (!isSubscribed(context, eperson, collection)) { - Subscription subscription = subscriptionDAO.create(context, new Subscription()); - subscription.setCollection(collection); - subscription.setePerson(eperson); - } + public List findAll(Context context, String resourceType, Integer limit, Integer offset) + throws Exception { + if (StringUtils.isBlank(resourceType)) { + return subscriptionDAO.findAllOrderedByDSO(context, limit, offset); } else { - throw new AuthorizeException( - "Only admin or e-person themselves can subscribe"); + if (resourceType.equals(Collection.class.getSimpleName()) || + resourceType.equals(Community.class.getSimpleName())) { + return subscriptionDAO.findAllOrderedByIDAndResourceType(context, resourceType, limit, offset); + } else { + log.error("Resource type must be Collection or Community"); + throw new Exception("Resource type must be Collection or Community"); + } } } @Override - public void unsubscribe(Context context, EPerson eperson, - Collection collection) throws SQLException, AuthorizeException { + public Subscription subscribe(Context context, EPerson eperson, + DSpaceObject dSpaceObject, + List subscriptionParameterList, + String type) throws SQLException, AuthorizeException { // Check authorisation. Must be administrator, or the eperson. if (authorizeService.isAdmin(context) - || ((context.getCurrentUser() != null) && (context - .getCurrentUser().getID().equals(eperson.getID())))) { - if (collection == null) { + || ((context.getCurrentUser() != null) && (context + .getCurrentUser().getID().equals(eperson.getID())))) { + Subscription newSubscription = subscriptionDAO.create(context, new Subscription()); + subscriptionParameterList.forEach(subscriptionParameter -> + newSubscription.addParameter(subscriptionParameter)); + newSubscription.setEPerson(eperson); + newSubscription.setDSpaceObject(dSpaceObject); + newSubscription.setSubscriptionType(type); + return newSubscription; + } else { + throw new AuthorizeException("Only admin or e-person themselves can subscribe"); + } + } + + @Override + public void unsubscribe(Context context, EPerson eperson, DSpaceObject dSpaceObject) + throws SQLException, AuthorizeException { + // Check authorisation. Must be administrator, or the eperson. + if (authorizeService.isAdmin(context) + || ((context.getCurrentUser() != null) && (context + .getCurrentUser().getID().equals(eperson.getID())))) { + if (dSpaceObject == null) { // Unsubscribe from all subscriptionDAO.deleteByEPerson(context, eperson); } else { - subscriptionDAO.deleteByCollectionAndEPerson(context, collection, eperson); + subscriptionDAO.deleteByDSOAndEPerson(context, dSpaceObject, eperson); log.info(LogHelper.getHeader(context, "unsubscribe", "eperson_id=" + eperson.getID() + ",collection_id=" - + collection.getID())); + + dSpaceObject.getID())); } } else { - throw new AuthorizeException( - "Only admin or e-person themselves can unsubscribe"); + throw new AuthorizeException("Only admin or e-person themselves can unsubscribe"); } } @Override - public List getSubscriptions(Context context, EPerson eperson) - throws SQLException { - return subscriptionDAO.findByEPerson(context, eperson); + public List findSubscriptionsByEPerson(Context context, EPerson eperson, Integer limit,Integer offset) + throws SQLException { + return subscriptionDAO.findByEPerson(context, eperson, limit, offset); } @Override - public List getAvailableSubscriptions(Context context) - throws SQLException { - return getAvailableSubscriptions(context, null); + public List findSubscriptionsByEPersonAndDso(Context context, EPerson eperson, + DSpaceObject dSpaceObject, + Integer limit, Integer offset) throws SQLException { + return subscriptionDAO.findByEPersonAndDso(context, eperson, dSpaceObject, limit, offset); } @Override - public List getAvailableSubscriptions(Context context, EPerson eperson) - throws SQLException { - List collections; - if (eperson != null) { + public List findAvailableSubscriptions(Context context) throws SQLException { + return findAvailableSubscriptions(context, null); + } + + @Override + public List findAvailableSubscriptions(Context context, EPerson eperson) throws SQLException { + if (Objects.nonNull(eperson)) { context.setCurrentUser(eperson); } - collections = collectionService.findAuthorized(context, null, Constants.ADD); - - return collections; + return collectionService.findAuthorized(context, null, Constants.ADD); } @Override - public boolean isSubscribed(Context context, EPerson eperson, - Collection collection) throws SQLException { - return subscriptionDAO.findByCollectionAndEPerson(context, eperson, collection) != null; + public boolean isSubscribed(Context context, EPerson eperson, DSpaceObject dSpaceObject) throws SQLException { + return subscriptionDAO.findByEPersonAndDso(context, eperson, dSpaceObject, -1, -1) != null; } @Override - public void deleteByCollection(Context context, Collection collection) throws SQLException { - subscriptionDAO.deleteByCollection(context, collection); + public void deleteByDspaceObject(Context context, DSpaceObject dSpaceObject) throws SQLException { + subscriptionDAO.deleteByDspaceObject(context, dSpaceObject); } @Override public void deleteByEPerson(Context context, EPerson ePerson) throws SQLException { subscriptionDAO.deleteByEPerson(context, ePerson); } + + @Override + public Subscription findById(Context context, int id) throws SQLException { + return subscriptionDAO.findByID(context, Subscription.class, id); + } + + @Override + public Subscription updateSubscription(Context context, Integer id, String subscriptionType, + List subscriptionParameterList) + throws SQLException { + Subscription subscriptionDB = subscriptionDAO.findByID(context, Subscription.class, id); + subscriptionDB.removeParameterList(); + subscriptionDB.setSubscriptionType(subscriptionType); + subscriptionParameterList.forEach(x -> subscriptionDB.addParameter(x)); + subscriptionDAO.save(context, subscriptionDB); + return subscriptionDB; + } + + @Override + public Subscription addSubscriptionParameter(Context context, Integer id, SubscriptionParameter subscriptionParam) + throws SQLException { + Subscription subscriptionDB = subscriptionDAO.findByID(context, Subscription.class, id); + subscriptionDB.addParameter(subscriptionParam); + subscriptionDAO.save(context, subscriptionDB); + return subscriptionDB; + } + + @Override + public Subscription removeSubscriptionParameter(Context context,Integer id, SubscriptionParameter subscriptionParam) + throws SQLException { + Subscription subscriptionDB = subscriptionDAO.findByID(context, Subscription.class, id); + subscriptionDB.removeParameter(subscriptionParam); + subscriptionDAO.save(context, subscriptionDB); + return subscriptionDB; + } + + @Override + public void deleteSubscription(Context context, Subscription subscription) throws SQLException { + subscriptionDAO.delete(context, subscription); + } + + @Override + public List findAllSubscriptionsBySubscriptionTypeAndFrequency(Context context, + String subscriptionType, String frequencyValue) throws SQLException { + return subscriptionDAO.findAllSubscriptionsBySubscriptionTypeAndFrequency(context, subscriptionType, + frequencyValue); + } + + @Override + public Long countAll(Context context) throws SQLException { + return subscriptionDAO.countAll(context); + } + + @Override + public Long countSubscriptionsByEPerson(Context context, EPerson ePerson) throws SQLException { + return subscriptionDAO.countAllByEPerson(context, ePerson); + } + + @Override + public Long countByEPersonAndDSO(Context context, EPerson ePerson, DSpaceObject dSpaceObject) + throws SQLException { + return subscriptionDAO.countAllByEPersonAndDso(context, ePerson, dSpaceObject); + } + } diff --git a/dspace-api/src/main/java/org/dspace/eperson/Subscription.java b/dspace-api/src/main/java/org/dspace/eperson/Subscription.java index 1719888ca8..5db63740f4 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/Subscription.java +++ b/dspace-api/src/main/java/org/dspace/eperson/Subscription.java @@ -7,6 +7,9 @@ */ package org.dspace.eperson; +import java.util.ArrayList; +import java.util.List; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; @@ -15,10 +18,11 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import javax.persistence.SequenceGenerator; import javax.persistence.Table; -import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject; import org.dspace.core.Context; import org.dspace.core.ReloadableEntity; @@ -37,40 +41,78 @@ public class Subscription implements ReloadableEntity { @SequenceGenerator(name = "subscription_seq", sequenceName = "subscription_seq", allocationSize = 1) private Integer id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "collection_id") - private Collection collection; + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "dspace_object_id") + private DSpaceObject dSpaceObject; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "eperson_id") private EPerson ePerson; /** - * Protected constructor, create object using: - * {@link org.dspace.eperson.service.SubscribeService#subscribe(Context, EPerson, Collection)} + * Represent subscription type, for example, "content" or "statistics". + * + * NOTE: Currently, in DSpace we use only one "content" */ - protected Subscription() { + @Column(name = "type") + private String subscriptionType; - } + @OneToMany(fetch = FetchType.LAZY, mappedBy = "subscription", cascade = CascadeType.ALL, orphanRemoval = true) + private List subscriptionParameterList = new ArrayList<>(); + + /** + * Protected constructor, create object using: + * {@link org.dspace.eperson.service.SubscribeService#subscribe(Context, EPerson, DSpaceObject, List, String)} + */ + protected Subscription() {} @Override public Integer getID() { return id; } - public Collection getCollection() { - return collection; + public DSpaceObject getDSpaceObject() { + return this.dSpaceObject; } - void setCollection(Collection collection) { - this.collection = collection; + void setDSpaceObject(DSpaceObject dSpaceObject) { + this.dSpaceObject = dSpaceObject; } - public EPerson getePerson() { + public EPerson getEPerson() { return ePerson; } - void setePerson(EPerson ePerson) { + public void setEPerson(EPerson ePerson) { this.ePerson = ePerson; } -} + + public String getSubscriptionType() { + return subscriptionType; + } + + public void setSubscriptionType(String subscriptionType) { + this.subscriptionType = subscriptionType; + } + + public List getSubscriptionParameterList() { + return subscriptionParameterList; + } + + public void setSubscriptionParameterList(List subscriptionList) { + this.subscriptionParameterList = subscriptionList; + } + + public void addParameter(SubscriptionParameter subscriptionParameter) { + subscriptionParameterList.add(subscriptionParameter); + subscriptionParameter.setSubscription(this); + } + + public void removeParameterList() { + subscriptionParameterList.clear(); + } + + public void removeParameter(SubscriptionParameter subscriptionParameter) { + subscriptionParameterList.remove(subscriptionParameter); + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/eperson/SubscriptionParameter.java b/dspace-api/src/main/java/org/dspace/eperson/SubscriptionParameter.java new file mode 100644 index 0000000000..7526535d7f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/SubscriptionParameter.java @@ -0,0 +1,98 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.eperson; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.dspace.core.ReloadableEntity; + +/** + * Database entity representation of the subscription_parameter table + * SubscriptionParameter represents a frequency with which an user wants to be notified. + * + * @author Alba Aliu at atis.al + */ +@Entity +@Table(name = "subscription_parameter") +public class SubscriptionParameter implements ReloadableEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "subscription_parameter_seq") + @SequenceGenerator(name = "subscription_parameter_seq", sequenceName = "subscription_parameter_seq", + allocationSize = 1) + @Column(name = "subscription_parameter_id", unique = true) + private Integer id; + + @ManyToOne + @JoinColumn(name = "subscription_id", nullable = false) + private Subscription subscription; + + /* + * Currently, we have only one use case for this attribute: "frequency" + */ + @Column + private String name; + + /* + * Currently, we use this attribute only with following values: "D", "W", "M". + * Where D stand for Day, W stand for Week and M stand for Month + */ + @Column + private String value; + + public SubscriptionParameter() {} + + public SubscriptionParameter(Integer id, Subscription subscription, String name, String value) { + this.id = id; + this.subscription = subscription; + this.name = name; + this.value = value; + } + + public Subscription getSubscription() { + return subscription; + } + + public void setSubscription(Subscription subscription) { + this.subscription = subscription; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public Integer getID() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/SubscriptionDAO.java b/dspace-api/src/main/java/org/dspace/eperson/dao/SubscriptionDAO.java index e9f2d57059..4d762c1775 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/SubscriptionDAO.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/SubscriptionDAO.java @@ -10,7 +10,7 @@ package org.dspace.eperson.dao; import java.sql.SQLException; import java.util.List; -import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject; import org.dspace.core.Context; import org.dspace.core.GenericDAO; import org.dspace.eperson.EPerson; @@ -26,17 +26,125 @@ import org.dspace.eperson.Subscription; */ public interface SubscriptionDAO extends GenericDAO { - public void deleteByCollection(Context context, Collection collection) throws SQLException; + /** + * Delete all subscription of provided dSpaceObject + * + * @param context DSpace context object + * @param dSpaceObject DSpace resource + * @throws SQLException If database error + */ + public void deleteByDspaceObject(Context context, DSpaceObject dSpaceObject) throws SQLException; - public List findByEPerson(Context context, EPerson eperson) throws SQLException; + /** + * Return a paginated list of all subscriptions of the eperson + * + * @param context DSpace context object + * @param eperson ePerson whose subscriptions want to find + * @param limit Paging limit + * @param offset The position of the first result to return + * @return + * @throws SQLException If database error + */ + public List findByEPerson(Context context, EPerson eperson, Integer limit, Integer offset) + throws SQLException; - public Subscription findByCollectionAndEPerson(Context context, EPerson eperson, Collection collection) - throws SQLException; + /** + * Return a paginated list of subscriptions related to a DSpaceObject belong to an ePerson + * + * @param context DSpace context object + * @param eperson ePerson whose subscriptions want to find + * @param dSpaceObject DSpaceObject of whom subscriptions want to find + * @param limit Paging limit + * @param offset The position of the first result to return + * @return + * @throws SQLException If database error + */ + public List findByEPersonAndDso(Context context, EPerson eperson, DSpaceObject dSpaceObject, + Integer limit, Integer offset) throws SQLException; + /** + * Delete all subscription of provided ePerson + * + * @param context DSpace context object + * @param eperson ePerson whose subscriptions want to delete + * @throws SQLException If database error + */ public void deleteByEPerson(Context context, EPerson eperson) throws SQLException; - public void deleteByCollectionAndEPerson(Context context, Collection collection, EPerson eperson) - throws SQLException; + /** + * Delete all subscriptions related to a DSpaceObject belong to an ePerson + * + * @param context DSpace context object + * @param dSpaceObject DSpaceObject of whom subscriptions want to delete + * @param eperson ePerson whose subscriptions want to delete + * @throws SQLException If database error + */ + public void deleteByDSOAndEPerson(Context context, DSpaceObject dSpaceObject, EPerson eperson) throws SQLException; + + /** + * Return a paginated list of all subscriptions ordered by ID and resourceType + * + * @param context DSpace context object + * @param resourceType Could be Collection or Community + * @param limit Paging limit + * @param offset The position of the first result to return + * @return + * @throws SQLException If database error + */ + public List findAllOrderedByIDAndResourceType(Context context, String resourceType, + Integer limit, Integer offset) throws SQLException; + + /** + * Return a paginated list of subscriptions ordered by DSpaceObject + * + * @param context DSpace context object + * @param limit Paging limit + * @param offset The position of the first result to return + * @return + * @throws SQLException If database error + */ + public List findAllOrderedByDSO(Context context, Integer limit, Integer offset) throws SQLException; + + /** + * Return a list of all subscriptions by subscriptionType and frequency + * + * @param context DSpace context object + * @param subscriptionType Could be "content" or "statistics". NOTE: in DSpace we have only "content" + * @param frequencyValue Could be "D" stand for Day, "W" stand for Week, and "M" stand for Month + * @return + * @throws SQLException If database error + */ + public List findAllSubscriptionsBySubscriptionTypeAndFrequency(Context context, + String subscriptionType, String frequencyValue) throws SQLException; + + /** + * Count all subscriptions + * + * @param context DSpace context object + * @return Total of all subscriptions + * @throws SQLException If database error + */ + public Long countAll(Context context) throws SQLException; + + /** + * Count all subscriptions belong to an ePerson + * + * @param context DSpace context object + * @param ePerson ePerson whose subscriptions want count + * @return Total of all subscriptions belong to an ePerson + * @throws SQLException If database error + */ + public Long countAllByEPerson(Context context, EPerson ePerson) throws SQLException; + + /** + * Count all subscriptions related to a DSpaceObject belong to an ePerson + * + * @param context DSpace context object + * @param ePerson ePerson whose subscriptions want count + * @param dSpaceObject DSpaceObject of whom subscriptions want count + * @return + * @throws SQLException If database error + */ + public Long countAllByEPersonAndDso(Context context, EPerson ePerson,DSpaceObject dSpaceObject) throws SQLException; - public List findAllOrderedByEPerson(Context context) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/SubscriptionParameterDAO.java b/dspace-api/src/main/java/org/dspace/eperson/dao/SubscriptionParameterDAO.java new file mode 100644 index 0000000000..ea9c7b0bbd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/SubscriptionParameterDAO.java @@ -0,0 +1,22 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.eperson.dao; +import org.dspace.core.GenericDAO; +import org.dspace.eperson.SubscriptionParameter; + + +/** + * Database Access Object interface class for the SubscriptionParamter object. + * The implementation of this class is responsible for all database calls for the SubscriptionParameter object and is + * autowired by spring + * This class should only be accessed from a single service and should never be exposed outside of the API + * + * @author Alba Aliu at atis.al + */ +public interface SubscriptionParameterDAO extends GenericDAO { +} diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/SubscriptionDAOImpl.java b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/SubscriptionDAOImpl.java index 6f2cb4b4fb..6c36211f31 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/SubscriptionDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/SubscriptionDAOImpl.java @@ -9,17 +9,21 @@ package org.dspace.eperson.dao.impl; import java.sql.SQLException; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; import javax.persistence.criteria.Root; -import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject; import org.dspace.core.AbstractHibernateDAO; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Subscription; +import org.dspace.eperson.SubscriptionParameter; +import org.dspace.eperson.SubscriptionParameter_; import org.dspace.eperson.Subscription_; import org.dspace.eperson.dao.SubscriptionDAO; @@ -31,42 +35,50 @@ import org.dspace.eperson.dao.SubscriptionDAO; * @author kevinvandevelde at atmire.com */ public class SubscriptionDAOImpl extends AbstractHibernateDAO implements SubscriptionDAO { + protected SubscriptionDAOImpl() { super(); } @Override - public List findByEPerson(Context context, EPerson eperson) throws SQLException { + public List findByEPerson(Context context, EPerson eperson, Integer limit, Integer offset) + throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); javax.persistence.criteria.CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Subscription.class); Root subscriptionRoot = criteriaQuery.from(Subscription.class); criteriaQuery.select(subscriptionRoot); criteriaQuery.where(criteriaBuilder.equal(subscriptionRoot.get(Subscription_.ePerson), eperson)); - return list(context, criteriaQuery, false, Subscription.class, -1, -1); - + List orderList = new LinkedList<>(); + orderList.add(criteriaBuilder.asc(subscriptionRoot.get(Subscription_.dSpaceObject))); + criteriaQuery.orderBy(orderList); + return list(context, criteriaQuery, false, Subscription.class, limit, offset); } @Override - public Subscription findByCollectionAndEPerson(Context context, EPerson eperson, Collection collection) - throws SQLException { + public List findByEPersonAndDso(Context context, EPerson eperson, + DSpaceObject dSpaceObject, + Integer limit, Integer offset) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); - javax.persistence.criteria.CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Subscription.class); + javax.persistence.criteria.CriteriaQuery criteriaQuery = + getCriteriaQuery(criteriaBuilder, Subscription.class); Root subscriptionRoot = criteriaQuery.from(Subscription.class); criteriaQuery.select(subscriptionRoot); - criteriaQuery - .where(criteriaBuilder.and(criteriaBuilder.equal(subscriptionRoot.get(Subscription_.ePerson), eperson), - criteriaBuilder.equal(subscriptionRoot.get(Subscription_.collection), collection) - ) - ); - return singleResult(context, criteriaQuery); + criteriaQuery.where(criteriaBuilder.and(criteriaBuilder.equal( + subscriptionRoot.get(Subscription_.ePerson), eperson), + criteriaBuilder.equal(subscriptionRoot.get(Subscription_.dSpaceObject), dSpaceObject) + )); + List orderList = new LinkedList<>(); + orderList.add(criteriaBuilder.asc(subscriptionRoot.get(Subscription_.dSpaceObject))); + criteriaQuery.orderBy(orderList); + return list(context, criteriaQuery, false, Subscription.class, limit, offset); } @Override - public void deleteByCollection(Context context, Collection collection) throws SQLException { - String hqlQuery = "delete from Subscription where collection=:collection"; + public void deleteByDspaceObject(Context context, DSpaceObject dSpaceObject) throws SQLException { + String hqlQuery = "delete from Subscription where dSpaceObject=:dSpaceObject"; Query query = createQuery(context, hqlQuery); - query.setParameter("collection", collection); + query.setParameter("dSpaceObject", dSpaceObject); query.executeUpdate(); } @@ -79,28 +91,98 @@ public class SubscriptionDAOImpl extends AbstractHibernateDAO impl } @Override - public void deleteByCollectionAndEPerson(Context context, Collection collection, EPerson eperson) - throws SQLException { - String hqlQuery = "delete from Subscription where collection=:collection AND ePerson=:ePerson"; + public void deleteByDSOAndEPerson(Context context, DSpaceObject dSpaceObject, EPerson eperson) + throws SQLException { + String hqlQuery = "delete from Subscription where dSpaceObject=:dSpaceObject AND ePerson=:ePerson"; Query query = createQuery(context, hqlQuery); - query.setParameter("collection", collection); + query.setParameter("dSpaceObject", dSpaceObject); query.setParameter("ePerson", eperson); query.executeUpdate(); } @Override - public List findAllOrderedByEPerson(Context context) throws SQLException { - + public List findAllOrderedByIDAndResourceType(Context context, String resourceType, + Integer limit, Integer offset) throws SQLException { + String hqlQuery = "select s from Subscription s join %s dso " + + "ON dso.id = s.dSpaceObject ORDER BY subscription_id"; + if (resourceType != null) { + hqlQuery = String.format(hqlQuery, resourceType); + } + Query query = createQuery(context, hqlQuery); + if (limit != -1) { + query.setMaxResults(limit); + } + if (offset != -1) { + query.setFirstResult(offset); + } + query.setHint("org.hibernate.cacheable", false); + return query.getResultList(); + } + @Override + public List findAllOrderedByDSO(Context context, Integer limit, Integer offset) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Subscription.class); Root subscriptionRoot = criteriaQuery.from(Subscription.class); criteriaQuery.select(subscriptionRoot); + List orderList = new LinkedList<>(); + orderList.add(criteriaBuilder.asc(subscriptionRoot.get(Subscription_.dSpaceObject))); + criteriaQuery.orderBy(orderList); + return list(context, criteriaQuery, false, Subscription.class, limit, offset); + } + @Override + public List findAllSubscriptionsBySubscriptionTypeAndFrequency(Context context, + String subscriptionType, String frequencyValue) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Subscription.class); + Root subscriptionRoot = criteriaQuery.from(Subscription.class); + criteriaQuery.select(subscriptionRoot); + Join childJoin = subscriptionRoot.join("subscriptionParameterList"); + criteriaQuery.where( + criteriaBuilder.and( + criteriaBuilder.equal(subscriptionRoot.get(Subscription_.SUBSCRIPTION_TYPE), subscriptionType), + criteriaBuilder.equal(childJoin.get(SubscriptionParameter_.name), "frequency"), + criteriaBuilder.equal(childJoin.get(SubscriptionParameter_.value), frequencyValue) + )); List orderList = new ArrayList<>(1); orderList.add(criteriaBuilder.asc(subscriptionRoot.get(Subscription_.ePerson))); + orderList.add(criteriaBuilder.asc(subscriptionRoot.get(Subscription_.id))); criteriaQuery.orderBy(orderList); - - return list(context, criteriaQuery, false, Subscription.class, -1, -1); + return list(context, criteriaQuery, false, Subscription.class, 10000, -1); } + + @Override + public Long countAll(Context context) throws SQLException { + CriteriaBuilder qb = getCriteriaBuilder(context); + CriteriaQuery cq = qb.createQuery(Long.class); + cq.select(qb.count(cq.from(Subscription.class))); + Query query = this.getHibernateSession(context).createQuery(cq); + return (Long) query.getSingleResult(); + } + + @Override + public Long countAllByEPerson(Context context, EPerson ePerson) throws SQLException { + CriteriaBuilder qb = getCriteriaBuilder(context); + CriteriaQuery cq = qb.createQuery(Long.class); + Root subscriptionRoot = cq.from(Subscription.class); + cq.select(qb.count(subscriptionRoot)); + cq.where(qb.equal(subscriptionRoot.get(Subscription_.ePerson), ePerson)); + Query query = this.getHibernateSession(context).createQuery(cq); + return (Long) query.getSingleResult(); + } + + @Override + public Long countAllByEPersonAndDso(Context context, + EPerson ePerson, DSpaceObject dSpaceObject) throws SQLException { + CriteriaBuilder qb = getCriteriaBuilder(context); + CriteriaQuery cq = qb.createQuery(Long.class); + Root subscriptionRoot = cq.from(Subscription.class); + cq.select(qb.count(subscriptionRoot)); + cq.where(qb.and(qb.equal(subscriptionRoot.get(Subscription_.ePerson) + , ePerson), qb.equal(subscriptionRoot.get(Subscription_.dSpaceObject), dSpaceObject))); + Query query = this.getHibernateSession(context).createQuery(cq); + return (Long) query.getSingleResult(); + } + } \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/eperson/dao/impl/SubscriptionParameterDAOImpl.java b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/SubscriptionParameterDAOImpl.java new file mode 100644 index 0000000000..37af787ed3 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/eperson/dao/impl/SubscriptionParameterDAOImpl.java @@ -0,0 +1,28 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.eperson.dao.impl; + +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.eperson.SubscriptionParameter; +import org.dspace.eperson.dao.SubscriptionParameterDAO; + +/** + * Hibernate implementation of the Database Access Object interface class for the SubscriptionParameter object. + * This class is responsible for all database calls for the SubscriptionParameter object and is autowired by spring + * This class should never be accessed directly. + * + * @author Alba Aliu at atis.al + */ +public class SubscriptionParameterDAOImpl extends AbstractHibernateDAO + implements SubscriptionParameterDAO { + + protected SubscriptionParameterDAOImpl() { + super(); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/eperson/service/SubscribeService.java b/dspace-api/src/main/java/org/dspace/eperson/service/SubscribeService.java index 347c69bf5b..e70f40e0ed 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/service/SubscribeService.java +++ b/dspace-api/src/main/java/org/dspace/eperson/service/SubscribeService.java @@ -12,9 +12,11 @@ import java.util.List; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Subscription; +import org.dspace.eperson.SubscriptionParameter; /** * Service interface class for the Subscription object. @@ -31,49 +33,74 @@ public interface SubscribeService { * new item appears in the collection. * * @param context DSpace context + * @param limit Number of subscriptions to return + * @param offset Offset number * @return list of Subscription objects * @throws SQLException An exception that provides information on a database access error or other errors. */ - public List findAll(Context context) throws SQLException; + public List findAll(Context context, String resourceType, Integer limit, Integer offset) + throws Exception; /** - * Subscribe an e-person to a collection. An e-mail will be sent every day a - * new item appears in the collection. - * - * @param context DSpace context - * @param eperson EPerson to subscribe - * @param collection Collection to subscribe to - * @throws SQLException An exception that provides information on a database access error or other errors. - * @throws AuthorizeException Exception indicating the current user of the context does not have permission - * to perform a particular action. + * Subscribe an EPerson to a dSpaceObject (Collection or Community). An e-mail will be sent every day a + * new item appears in the Collection or Community. + * + * @param context DSpace context object + * @param eperson EPerson to subscribe + * @param dSpaceObject DSpaceObject to subscribe + * @param subscriptionParameters list of @SubscriptionParameter + * @param subscriptionType Currently supported only "content" + * @return + * @throws SQLException An exception that provides information on a database access error or other errors. + * @throws AuthorizeException Exception indicating the current user of the context does not have permission + * to perform a particular action. */ - public void subscribe(Context context, EPerson eperson, - Collection collection) throws SQLException, AuthorizeException; + public Subscription subscribe(Context context, EPerson eperson, DSpaceObject dSpaceObject, + List subscriptionParameters, + String subscriptionType) throws SQLException, AuthorizeException; /** * Unsubscribe an e-person to a collection. Passing in null * for the collection unsubscribes the e-person from all collections they * are subscribed to. * - * @param context DSpace context - * @param eperson EPerson to unsubscribe - * @param collection Collection to unsubscribe from + * @param context DSpace context + * @param eperson EPerson to unsubscribe + * @param dSpaceObject DSpaceObject to unsubscribe from * @throws SQLException An exception that provides information on a database access error or other errors. * @throws AuthorizeException Exception indicating the current user of the context does not have permission * to perform a particular action. */ - public void unsubscribe(Context context, EPerson eperson, - Collection collection) throws SQLException, AuthorizeException; + public void unsubscribe(Context context, EPerson eperson, DSpaceObject dSpaceObject) + throws SQLException, AuthorizeException; /** * Find out which collections an e-person is subscribed to * * @param context DSpace context * @param eperson EPerson + * @param limit Number of subscriptions to return + * @param offset Offset number * @return array of collections e-person is subscribed to * @throws SQLException An exception that provides information on a database access error or other errors. */ - public List getSubscriptions(Context context, EPerson eperson) throws SQLException; + public List findSubscriptionsByEPerson(Context context, EPerson eperson, Integer limit,Integer offset) + throws SQLException; + + /** + * Find out which collections an e-person is subscribed to and related with dso + * + * @param context DSpace context + * @param eperson EPerson + * @param dSpaceObject DSpaceObject + * @param limit Number of subscriptions to return + * @param offset Offset number + * @return array of collections e-person is subscribed to and related with dso + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public List findSubscriptionsByEPersonAndDso(Context context, EPerson eperson, + DSpaceObject dSpaceObject, + Integer limit, Integer offset) throws SQLException; /** * Find out which collections the currently logged in e-person can subscribe to @@ -82,8 +109,7 @@ public interface SubscribeService { * @return array of collections the currently logged in e-person can subscribe to * @throws SQLException An exception that provides information on a database access error or other errors. */ - public List getAvailableSubscriptions(Context context) - throws SQLException; + public List findAvailableSubscriptions(Context context) throws SQLException; /** * Find out which collections an e-person can subscribe to @@ -93,29 +119,27 @@ public interface SubscribeService { * @return array of collections e-person can subscribe to * @throws SQLException An exception that provides information on a database access error or other errors. */ - public List getAvailableSubscriptions(Context context, EPerson eperson) - throws SQLException; + public List findAvailableSubscriptions(Context context, EPerson eperson) throws SQLException; /** * Is that e-person subscribed to that collection? * - * @param context DSpace context - * @param eperson find out if this e-person is subscribed - * @param collection find out if subscribed to this collection + * @param context DSpace context + * @param eperson find out if this e-person is subscribed + * @param dSpaceObject find out if subscribed to this dSpaceObject * @return true if they are subscribed * @throws SQLException An exception that provides information on a database access error or other errors. */ - public boolean isSubscribed(Context context, EPerson eperson, - Collection collection) throws SQLException; + public boolean isSubscribed(Context context, EPerson eperson, DSpaceObject dSpaceObject) throws SQLException; /** * Delete subscription by collection. * - * @param context DSpace context - * @param collection find out if subscribed to this collection + * @param context DSpace context + * @param dSpaceObject find out if subscribed to this dSpaceObject * @throws SQLException An exception that provides information on a database access error or other errors. */ - public void deleteByCollection(Context context, Collection collection) throws SQLException; + public void deleteByDspaceObject(Context context, DSpaceObject dSpaceObject) throws SQLException; /** * Delete subscription by eperson (subscriber). @@ -125,4 +149,92 @@ public interface SubscribeService { * @throws SQLException An exception that provides information on a database access error or other errors. */ public void deleteByEPerson(Context context, EPerson ePerson) throws SQLException; -} + + /** + * Finds a subscription by id + * + * @param context DSpace context + * @param id the id of subscription to be searched + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public Subscription findById(Context context, int id) throws SQLException; + + /** + * Updates a subscription by id + * + * @param context DSpace context + * @param id Integer id + * @param subscriptionParameterList List subscriptionParameterList + * @param subscriptionType type + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public Subscription updateSubscription(Context context, Integer id, String subscriptionType, + List subscriptionParameterList) throws SQLException; + + /** + * Adds a parameter to a subscription + * + * @param context DSpace context + * @param id Integer id + * @param subscriptionParameter SubscriptionParameter subscriptionParameter + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public Subscription addSubscriptionParameter(Context context,Integer id, + SubscriptionParameter subscriptionParameter) throws SQLException; + + /** + * Deletes a parameter from subscription + * + * @param context DSpace context + * @param id Integer id + * @param subscriptionParam SubscriptionParameter subscriptionParameter + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public Subscription removeSubscriptionParameter(Context context, Integer id, + SubscriptionParameter subscriptionParam) throws SQLException; + + /** + * Deletes a subscription + * + * @param context DSpace context + * @param subscription The subscription to delete + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public void deleteSubscription(Context context, Subscription subscription) throws SQLException; + + /** + * Finds all subscriptions by subscriptionType and frequency + * + * @param context DSpace context + * @param subscriptionType Could be "content" or "statistics". NOTE: in DSpace we have only "content" + * @param frequencyValue Could be "D" stand for Day, "W" stand for Week, and "M" stand for Month + * @throws SQLException An exception that provides information on a database access error or other errors. + */ + public List findAllSubscriptionsBySubscriptionTypeAndFrequency(Context context, + String subscriptionType, String frequencyValue) throws SQLException; + + /** + * Counts all subscriptions + * + * @param context DSpace context + */ + public Long countAll(Context context) throws SQLException; + + /** + * Counts all subscriptions by ePerson + * + * @param context DSpace context + * @param ePerson EPerson ePerson + */ + public Long countSubscriptionsByEPerson(Context context, EPerson ePerson) throws SQLException; + + /** + * Counts all subscriptions by ePerson and DSO + * + * @param context DSpace context + * @param ePerson EPerson ePerson + * @param dSpaceObject DSpaceObject dSpaceObject + */ + public Long countByEPersonAndDSO(Context context, EPerson ePerson, DSpaceObject dSpaceObject) throws SQLException; + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java index 66e7b94a4b..b70eda960d 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java @@ -21,6 +21,7 @@ import org.dspace.content.MetadataValue; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.logic.Filter; import org.dspace.content.logic.LogicalStatementException; +import org.dspace.content.logic.TrueFilter; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -28,6 +29,7 @@ import org.dspace.identifier.doi.DOIConnector; import org.dspace.identifier.doi.DOIIdentifierException; import org.dspace.identifier.doi.DOIIdentifierNotApplicableException; import org.dspace.identifier.service.DOIService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -44,6 +46,7 @@ import org.springframework.beans.factory.annotation.Autowired; *

Any identifier a method of this class returns is a string in the following format: doi:10.123/456.

* * @author Pascal-Nicolas Becker + * @author Kim Shepherd */ public class DOIIdentifierProvider extends FilteredIdentifierProvider { private static final Logger log = LoggerFactory.getLogger(DOIIdentifierProvider.class); @@ -71,16 +74,44 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { public static final String MD_SCHEMA = "dc"; public static final String DOI_ELEMENT = "identifier"; public static final String DOI_QUALIFIER = "uri"; - + // The DOI is queued for registered with the service provider public static final Integer TO_BE_REGISTERED = 1; + // The DOI is queued for reservation with the service provider public static final Integer TO_BE_RESERVED = 2; + // The DOI has been registered online public static final Integer IS_REGISTERED = 3; + // The DOI has been reserved online public static final Integer IS_RESERVED = 4; + // The DOI is reserved and requires an updated metadata record to be sent to the service provider public static final Integer UPDATE_RESERVED = 5; + // The DOI is registered and requires an updated metadata record to be sent to the service provider public static final Integer UPDATE_REGISTERED = 6; + // The DOI metadata record should be updated before performing online registration public static final Integer UPDATE_BEFORE_REGISTRATION = 7; + // The DOI will be deleted locally and marked as deleted in the DOI service provider public static final Integer TO_BE_DELETED = 8; + // The DOI has been deleted and is no longer associated with an item public static final Integer DELETED = 9; + // The DOI is created in the database and is waiting for either successful filter check on item install or + // manual intervention by an administrator to proceed to reservation or registration + public static final Integer PENDING = 10; + // The DOI is created in the database, but no more context is known + public static final Integer MINTED = 11; + + public static final String[] statusText = { + "UNKNOWN", // 0 + "TO_BE_REGISTERED", // 1 + "TO_BE_RESERVED", // 2 + "IS_REGISTERED", // 3 + "IS_RESERVED", // 4 + "UPDATE_RESERVED", // 5 + "UPDATE_REGISTERED", // 6 + "UPDATE_BEFORE_REGISTRATION", // 7 + "TO_BE_DELETED", // 8 + "DELETED", // 9 + "PENDING", // 10 + "MINTED", // 11 + }; @Autowired(required = true) protected DOIService doiService; @@ -89,8 +120,6 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { @Autowired(required = true) protected ItemService itemService; - protected Filter filterService; - /** * Empty / default constructor for Spring */ @@ -153,16 +182,6 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { this.connector = connector; } - /** - * Set the Filter to use when testing items to see if a DOI should be registered - * Spring will use this setter to set the filter from the configured property in identifier-services.xml - * @param filterService - an object implementing the org.dspace.content.logic.Filter interface - */ - @Override - public void setFilterService(Filter filterService) { - this.filterService = filterService; - } - /** * This identifier provider supports identifiers of type * {@link org.dspace.identifier.DOI}. @@ -206,7 +225,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { @Override public String register(Context context, DSpaceObject dso) throws IdentifierException { - return register(context, dso, false); + return register(context, dso, this.filter); } /** @@ -219,29 +238,29 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { @Override public void register(Context context, DSpaceObject dso, String identifier) throws IdentifierException { - register(context, dso, identifier, false); + register(context, dso, identifier, this.filter); } /** * Register a new DOI for a given DSpaceObject * @param context - DSpace context * @param dso - DSpaceObject identified by the new DOI - * @param skipFilter - boolean indicating whether to skip any filtering of items before performing registration + * @param filter - Logical item filter to determine whether this identifier should be registered * @throws IdentifierException */ @Override - public String register(Context context, DSpaceObject dso, boolean skipFilter) + public String register(Context context, DSpaceObject dso, Filter filter) throws IdentifierException { if (!(dso instanceof Item)) { // DOI are currently assigned only to Item return null; } - String doi = mint(context, dso, skipFilter); + String doi = mint(context, dso, filter); // register tries to reserve doi if it's not already. // So we don't have to reserve it here. - register(context, dso, doi, skipFilter); + register(context, dso, doi, filter); return doi; } @@ -250,11 +269,11 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { * @param context - DSpace context * @param dso - DSpaceObject identified by the new DOI * @param identifier - String containing the DOI to register - * @param skipFilter - boolean indicating whether to skip any filtering of items before performing registration + * @param filter - Logical item filter to determine whether this identifier should be registered * @throws IdentifierException */ @Override - public void register(Context context, DSpaceObject dso, String identifier, boolean skipFilter) + public void register(Context context, DSpaceObject dso, String identifier, Filter filter) throws IdentifierException { if (!(dso instanceof Item)) { // DOI are currently assigned only to Item @@ -265,7 +284,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { // search DOI in our db try { - doiRow = loadOrCreateDOI(context, dso, doi, skipFilter); + doiRow = loadOrCreateDOI(context, dso, doi, filter); } catch (SQLException ex) { log.error("Error in databse connection: " + ex.getMessage()); throw new RuntimeException("Error in database conncetion.", ex); @@ -277,7 +296,6 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { + "is marked as DELETED.", DOIIdentifierException.DOI_IS_DELETED); } - // Check status of DOI if (IS_REGISTERED.equals(doiRow.getStatus())) { return; } @@ -290,6 +308,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { log.warn("SQLException while changing status of DOI {} to be registered.", doi); throw new RuntimeException(sqle); } + } /** @@ -309,7 +328,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { @Override public void reserve(Context context, DSpaceObject dso, String identifier) throws IdentifierException, IllegalArgumentException { - reserve(context, dso, identifier, false); + reserve(context, dso, identifier, this.filter); } /** @@ -317,20 +336,18 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { * @param context - DSpace context * @param dso - DSpaceObject identified by this DOI * @param identifier - String containing the DOI to reserve - * @param skipFilter - boolean indicating whether to skip any filtering of items before performing reservation + * @param filter - Logical item filter to determine whether this identifier should be reserved * @throws IdentifierException * @throws IllegalArgumentException */ @Override - public void reserve(Context context, DSpaceObject dso, String identifier, boolean skipFilter) + public void reserve(Context context, DSpaceObject dso, String identifier, Filter filter) throws IdentifierException, IllegalArgumentException { String doi = doiService.formatIdentifier(identifier); DOI doiRow = null; try { - // if the doi is in our db already loadOrCreateDOI just returns. - // if it is not loadOrCreateDOI safes the doi. - doiRow = loadOrCreateDOI(context, dso, doi, skipFilter); + doiRow = loadOrCreateDOI(context, dso, doi, filter); } catch (SQLException sqle) { throw new RuntimeException(sqle); } @@ -359,7 +376,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { */ public void reserveOnline(Context context, DSpaceObject dso, String identifier) throws IdentifierException, IllegalArgumentException, SQLException { - reserveOnline(context, dso, identifier, false); + reserveOnline(context, dso, identifier, this.filter); } /** @@ -367,16 +384,16 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { * @param context - DSpace context * @param dso - DSpaceObject identified by this DOI * @param identifier - String containing the DOI to reserve - * @param skipFilter - skip the filters for {@link checkMintable(Context, DSpaceObject)} + * @param filter - Logical item filter to determine whether this identifier should be reserved online * @throws IdentifierException * @throws IllegalArgumentException * @throws SQLException */ - public void reserveOnline(Context context, DSpaceObject dso, String identifier, boolean skipFilter) + public void reserveOnline(Context context, DSpaceObject dso, String identifier, Filter filter) throws IdentifierException, IllegalArgumentException, SQLException { String doi = doiService.formatIdentifier(identifier); // get TableRow and ensure DOI belongs to dso regarding our db - DOI doiRow = loadOrCreateDOI(context, dso, doi, skipFilter); + DOI doiRow = loadOrCreateDOI(context, dso, doi, filter); if (DELETED.equals(doiRow.getStatus()) || TO_BE_DELETED.equals(doiRow.getStatus())) { throw new DOIIdentifierException("You tried to reserve a DOI that " @@ -402,7 +419,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { public void registerOnline(Context context, DSpaceObject dso, String identifier) throws IdentifierException, IllegalArgumentException, SQLException { - registerOnline(context, dso, identifier, false); + registerOnline(context, dso, identifier, this.filter); } @@ -411,18 +428,17 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { * @param context - DSpace context * @param dso - DSpaceObject identified by this DOI * @param identifier - String containing the DOI to register - * @param skipFilter - skip filters for {@link checkMintable(Context, DSpaceObject)} + * @param filter - Logical item filter to determine whether this identifier should be registered online * @throws IdentifierException * @throws IllegalArgumentException * @throws SQLException */ - public void registerOnline(Context context, DSpaceObject dso, String identifier, boolean skipFilter) + public void registerOnline(Context context, DSpaceObject dso, String identifier, Filter filter) throws IdentifierException, IllegalArgumentException, SQLException { - log.debug("registerOnline: skipFilter is " + skipFilter); String doi = doiService.formatIdentifier(identifier); // get TableRow and ensure DOI belongs to dso regarding our db - DOI doiRow = loadOrCreateDOI(context, dso, doi, skipFilter); + DOI doiRow = loadOrCreateDOI(context, dso, doi, filter); if (DELETED.equals(doiRow.getStatus()) || TO_BE_DELETED.equals(doiRow.getStatus())) { throw new DOIIdentifierException("You tried to register a DOI that " @@ -435,7 +451,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { } catch (DOIIdentifierException die) { // do we have to reserve DOI before we can register it? if (die.getCode() == DOIIdentifierException.RESERVE_FIRST) { - this.reserveOnline(context, dso, identifier, skipFilter); + this.reserveOnline(context, dso, identifier, filter); connector.registerDOI(context, dso, doi); } else { throw die; @@ -471,17 +487,23 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { throws IdentifierException, IllegalArgumentException, SQLException { String doi = doiService.formatIdentifier(identifier); - - boolean skipFilter = false; + // Use the default filter unless we find the object + Filter updateFilter = this.filter; if (doiService.findDOIByDSpaceObject(context, dso) != null) { // We can skip the filter here since we know the DOI already exists for the item log.debug("updateMetadata: found DOIByDSpaceObject: " + doiService.findDOIByDSpaceObject(context, dso).getDoi()); - skipFilter = true; + updateFilter = DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName( + "always_true_filter", TrueFilter.class); } - DOI doiRow = loadOrCreateDOI(context, dso, doi, skipFilter); + DOI doiRow = loadOrCreateDOI(context, dso, doi, updateFilter); + + if (PENDING.equals(doiRow.getStatus()) || MINTED.equals(doiRow.getStatus())) { + log.info("Not updating metadata for PENDING or MINTED doi: " + doi); + return; + } if (DELETED.equals(doiRow.getStatus()) || TO_BE_DELETED.equals(doiRow.getStatus())) { throw new DOIIdentifierException("You tried to register a DOI that " @@ -571,19 +593,19 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { @Override public String mint(Context context, DSpaceObject dso) throws IdentifierException { - return mint(context, dso, false); + return mint(context, dso, this.filter); } /** * Mint a new DOI in DSpace - this is usually the first step of registration * @param context - DSpace context * @param dso - DSpaceObject identified by the new identifier - * @param skipFilter - boolean indicating whether to skip any filtering of items before minting. + * @param filter - Logical item filter to determine whether this identifier should be registered * @return a String containing the new identifier * @throws IdentifierException */ @Override - public String mint(Context context, DSpaceObject dso, boolean skipFilter) throws IdentifierException { + public String mint(Context context, DSpaceObject dso, Filter filter) throws IdentifierException { String doi = null; try { @@ -597,7 +619,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { } if (null == doi) { try { - DOI doiRow = loadOrCreateDOI(context, dso, null, skipFilter); + DOI doiRow = loadOrCreateDOI(context, dso, null, filter); doi = DOI.SCHEME + doiRow.getDoi(); } catch (SQLException e) { @@ -895,7 +917,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { */ protected DOI loadOrCreateDOI(Context context, DSpaceObject dso, String doiIdentifier) throws SQLException, DOIIdentifierException, IdentifierNotApplicableException { - return loadOrCreateDOI(context, dso, doiIdentifier, false); + return loadOrCreateDOI(context, dso, doiIdentifier, this.filter); } /** @@ -910,13 +932,13 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { * @param context - DSpace context * @param dso - DSpaceObject to identify * @param doiIdentifier - DOI to load or create (null to mint a new one) - * @param skipFilter - Whether or not to skip the filters for the checkMintable() check + * @param filter - Logical item filter to determine whether this identifier should be registered * @return * @throws SQLException * @throws DOIIdentifierException * @throws org.dspace.identifier.IdentifierNotApplicableException passed through. */ - protected DOI loadOrCreateDOI(Context context, DSpaceObject dso, String doiIdentifier, boolean skipFilter) + protected DOI loadOrCreateDOI(Context context, DSpaceObject dso, String doiIdentifier, Filter filter) throws SQLException, DOIIdentifierException, IdentifierNotApplicableException { DOI doi = null; @@ -954,6 +976,8 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { // doi is assigned to a DSO; is it assigned to our specific dso? // check if DOI already belongs to dso if (dso.getID().equals(doi.getDSpaceObject().getID())) { + // Before we return this, check the filter + checkMintable(context, filter, dso); return doi; } else { throw new DOIIdentifierException("Trying to create a DOI " + @@ -963,15 +987,8 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { } } - // we did not find the doi in the database or shall reassign it. Before doing so, we should check if a - // filter is in place to prevent the creation of new DOIs for certain items. - if (skipFilter) { - log.warn("loadOrCreateDOI: Skipping default item filter"); - } else { - // Find out if we're allowed to create a DOI - // throws an exception if creation of a new DOI is prohibited by a filter - checkMintable(context, dso); - } + // Check if this item is eligible for minting. An IdentifierNotApplicableException will be thrown if not. + checkMintable(context, filter, dso); // check prefix if (!doiIdentifier.startsWith(this.getPrefix() + "/")) { @@ -984,15 +1001,8 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { doi = doiService.create(context); } } else { - // We need to generate a new DOI. Before doing so, we should check if a - // filter is in place to prevent the creation of new DOIs for certain items. - if (skipFilter) { - log.warn("loadOrCreateDOI: Skipping default item filter"); - } else { - // Find out if we're allowed to create a DOI - // throws an exception if creation of a new DOI is prohibited by a filter - checkMintable(context, dso); - } + // Check if this item is eligible for minting. An IdentifierNotApplicableException will be thrown if not. + checkMintable(context, filter, dso); doi = doiService.create(context); doiIdentifier = this.getPrefix() + "/" + this.getNamespaceSeparator() + @@ -1002,7 +1012,7 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { // prepare new doiRow doi.setDoi(doiIdentifier); doi.setDSpaceObject(dso); - doi.setStatus(null); + doi.setStatus(MINTED); try { doiService.update(context, doi); } catch (SQLException e) { @@ -1102,20 +1112,32 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { /** * Checks to see if an item can have a DOI minted, using the configured logical filter * @param context + * @param filter Logical item filter to apply * @param dso The item to be evaluated * @throws DOIIdentifierNotApplicableException */ @Override - public void checkMintable(Context context, DSpaceObject dso) throws DOIIdentifierNotApplicableException { + public void checkMintable(Context context, Filter filter, DSpaceObject dso) + throws DOIIdentifierNotApplicableException { + if (filter == null) { + Filter trueFilter = DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName( + "always_true_filter", TrueFilter.class); + // If a null filter was passed, and we have a good default filter to apply, apply it. + // Otherwise, set to TrueFilter which means "no filtering" + if (this.filter != null) { + filter = this.filter; + } else { + filter = trueFilter; + } + } // If the check fails, an exception will be thrown to be caught by the calling method - if (this.filterService != null && contentServiceFactory - .getDSpaceObjectService(dso).getTypeText(dso).equals("ITEM")) { + if (contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso).equals("ITEM")) { try { - boolean result = filterService.getResult(context, (Item) dso); + boolean result = filter.getResult(context, (Item) dso); log.debug("Result of filter for " + dso.getHandle() + " is " + result); if (!result) { throw new DOIIdentifierNotApplicableException("Item " + dso.getHandle() + - " was evaluated as 'false' by the item filter, not minting"); + " was evaluated as 'false' by the item filter, not minting"); } } catch (LogicalStatementException e) { log.error("Error evaluating item with logical filter: " + e.getLocalizedMessage()); @@ -1125,4 +1147,16 @@ public class DOIIdentifierProvider extends FilteredIdentifierProvider { log.debug("DOI Identifier Provider: filterService is null (ie. don't prevent DOI minting)"); } } + + /** + * Checks to see if an item can have a DOI minted, using the configured logical filter + * @param context + * @param dso The item to be evaluated + * @throws DOIIdentifierNotApplicableException + */ + @Override + public void checkMintable(Context context, DSpaceObject dso) throws DOIIdentifierNotApplicableException { + checkMintable(context, this.filter, dso); + } + } \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/identifier/FilteredIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/FilteredIdentifierProvider.java index e5f222ff29..c2254fa9a6 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/FilteredIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/FilteredIdentifierProvider.java @@ -12,8 +12,9 @@ import java.sql.SQLException; import org.dspace.content.DSpaceObject; import org.dspace.content.logic.Filter; +import org.dspace.content.logic.TrueFilter; import org.dspace.core.Context; -import org.springframework.beans.factory.annotation.Autowired; +import org.dspace.services.factory.DSpaceServicesFactory; /** * This abstract class adds extra method signatures so that implementing IdentifierProviders can @@ -24,26 +25,28 @@ import org.springframework.beans.factory.annotation.Autowired; */ public abstract class FilteredIdentifierProvider extends IdentifierProvider { - protected Filter filterService; + protected Filter filter = DSpaceServicesFactory.getInstance() + .getServiceManager().getServiceByName("always_true_filter", TrueFilter.class); /** - * Setter for spring to set the filter service from the property in configuration XML - * @param filterService - an object implementing the org.dspace.content.logic.Filter interface + * Setter for spring to set the default filter from the property in configuration XML + * @param filter - an object implementing the org.dspace.content.logic.Filter interface */ - @Autowired - public void setFilterService(Filter filterService) { - this.filterService = filterService; + public void setFilter(Filter filter) { + if (filter != null) { + this.filter = filter; + } } /** * Register a new identifier for a given DSpaceObject * @param context - DSpace context * @param dso - DSpaceObject to use for identifier registration - * @param skipFilter - boolean indicating whether to skip any filtering of items before performing registration + * @param filter - Logical item filter to determine whether this identifier should be registered * @return identifier * @throws IdentifierException */ - public abstract String register(Context context, DSpaceObject dso, boolean skipFilter) + public abstract String register(Context context, DSpaceObject dso, Filter filter) throws IdentifierException; /** @@ -51,10 +54,10 @@ public abstract class FilteredIdentifierProvider extends IdentifierProvider { * @param context - DSpace context * @param dso - DSpaceObject identified by the new identifier * @param identifier - String containing the identifier to register - * @param skipFilter - boolean indicating whether to skip any filtering of items before performing registration + * @param filter - Logical item filter to determine whether this identifier should be registered * @throws IdentifierException */ - public abstract void register(Context context, DSpaceObject dso, String identifier, boolean skipFilter) + public abstract void register(Context context, DSpaceObject dso, String identifier, Filter filter) throws IdentifierException; /** @@ -62,23 +65,23 @@ public abstract class FilteredIdentifierProvider extends IdentifierProvider { * @param context - DSpace context * @param dso - DSpaceObject identified by this identifier * @param identifier - String containing the identifier to reserve - * @param skipFilter - boolean indicating whether to skip any filtering of items before performing reservation + * @param filter - Logical item filter to determine whether this identifier should be reserved * @throws IdentifierException * @throws IllegalArgumentException * @throws SQLException */ - public abstract void reserve(Context context, DSpaceObject dso, String identifier, boolean skipFilter) + public abstract void reserve(Context context, DSpaceObject dso, String identifier, Filter filter) throws IdentifierException, IllegalArgumentException, SQLException; /** * Mint a new identifier in DSpace - this is usually the first step of registration * @param context - DSpace context * @param dso - DSpaceObject identified by the new identifier - * @param skipFilter - boolean indicating whether to skip any filtering of items before minting. + * @param filter - Logical item filter to determine whether this identifier should be registered * @return a String containing the new identifier * @throws IdentifierException */ - public abstract String mint(Context context, DSpaceObject dso, boolean skipFilter) throws IdentifierException; + public abstract String mint(Context context, DSpaceObject dso, Filter filter) throws IdentifierException; /** * Check configured item filters to see if this identifier is allowed to be minted @@ -88,5 +91,13 @@ public abstract class FilteredIdentifierProvider extends IdentifierProvider { */ public abstract void checkMintable(Context context, DSpaceObject dso) throws IdentifierException; + /** + * Check configured item filters to see if this identifier is allowed to be minted + * @param context - DSpace context + * @param filter - Logical item filter + * @param dso - DSpaceObject to be inspected + * @throws IdentifierException + */ + public abstract void checkMintable(Context context, Filter filter, DSpaceObject dso) throws IdentifierException; } diff --git a/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java b/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java index d0b6e4417e..b98aea24fa 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/identifier/IdentifierServiceImpl.java @@ -10,6 +10,7 @@ package org.dspace.identifier; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; @@ -17,6 +18,7 @@ import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.logic.Filter; import org.dspace.core.Context; import org.dspace.handle.service.HandleService; import org.dspace.identifier.service.IdentifierService; @@ -44,7 +46,6 @@ public class IdentifierServiceImpl implements IdentifierService { protected HandleService handleService; protected IdentifierServiceImpl() { - } @Autowired(required = true) @@ -98,7 +99,7 @@ public class IdentifierServiceImpl implements IdentifierService { @Override public void register(Context context, DSpaceObject dso) - throws AuthorizeException, SQLException, IdentifierException { + throws AuthorizeException, SQLException, IdentifierException { //We need to commit our context because one of the providers might require the handle created above // Next resolve all other services for (IdentifierProvider service : providers) { @@ -112,11 +113,99 @@ public class IdentifierServiceImpl implements IdentifierService { contentServiceFactory.getDSpaceObjectService(dso).update(context, dso); } + @Override + public void register(Context context, DSpaceObject dso, Class type, Filter filter) + throws AuthorizeException, SQLException, IdentifierException { + boolean registered = false; + // Iterate all services and register identifiers as appropriate + for (IdentifierProvider service : providers) { + if (service.supports(type)) { + try { + if (service instanceof FilteredIdentifierProvider) { + FilteredIdentifierProvider filteredService = (FilteredIdentifierProvider)service; + filteredService.register(context, dso, filter); + } else { + service.register(context, dso); + } + registered = true; + } catch (IdentifierNotApplicableException e) { + log.warn("Identifier not registered (inapplicable): " + e.getMessage()); + } + } + } + if (!registered) { + throw new IdentifierException("Cannot register identifier: Didn't " + + "find a provider that supports this identifier."); + } + // Update our item / collection / community + contentServiceFactory.getDSpaceObjectService(dso).update(context, dso); + } + + @Override + public void register(Context context, DSpaceObject dso, Class type) + throws AuthorizeException, SQLException, IdentifierException { + boolean registered = false; + // Iterate all services and register identifiers as appropriate + for (IdentifierProvider service : providers) { + if (service.supports(type)) { + try { + service.register(context, dso); + registered = true; + } catch (IdentifierNotApplicableException e) { + log.warn("Identifier not registered (inapplicable): " + e.getMessage()); + } + } + } + if (!registered) { + throw new IdentifierException("Cannot register identifier: Didn't " + + "find a provider that supports this identifier."); + } + // Update our item / collection / community + contentServiceFactory.getDSpaceObjectService(dso).update(context, dso); + } + + @Override + public void register(Context context, DSpaceObject dso, Map, Filter> typeFilters) + throws AuthorizeException, SQLException, IdentifierException { + // Iterate all services and register identifiers as appropriate + for (IdentifierProvider service : providers) { + try { + // If the service supports filtering, look through the map and the first supported class + // we find, set the filter and break. If no filter was seen for this type, just let the provider + // use its own implementation. + if (service instanceof FilteredIdentifierProvider) { + FilteredIdentifierProvider filteredService = (FilteredIdentifierProvider)service; + Filter filter = null; + for (Class type : typeFilters.keySet()) { + if (filteredService.supports(type)) { + filter = typeFilters.get(type); + break; + } + } + if (filter != null) { + // Pass the found filter to the provider + filteredService.register(context, dso, filter); + } else { + // Let the provider use the default filter / behaviour + filteredService.register(context, dso); + } + } else { + service.register(context, dso); + } + } catch (IdentifierNotApplicableException e) { + log.warn("Identifier not registered (inapplicable): " + e.getMessage()); + } + } + // Update our item / collection / community + contentServiceFactory.getDSpaceObjectService(dso).update(context, dso); + } + + + @Override public void register(Context context, DSpaceObject object, String identifier) throws AuthorizeException, SQLException, IdentifierException { - //We need to commit our context because one of the providers might require the handle created above - // Next resolve all other services + // Iterate all services and register identifiers as appropriate boolean registered = false; for (IdentifierProvider service : providers) { if (service.supports(identifier)) { @@ -132,7 +221,7 @@ public class IdentifierServiceImpl implements IdentifierService { throw new IdentifierException("Cannot register identifier: Didn't " + "find a provider that supports this identifier."); } - //Update our item / collection / community + // pdate our item / collection / community contentServiceFactory.getDSpaceObjectService(object).update(context, object); } diff --git a/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java index a864b4be4b..e7c786d5f8 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/VersionedDOIIdentifierProvider.java @@ -18,6 +18,7 @@ import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.MetadataValue; +import org.dspace.content.logic.Filter; import org.dspace.core.Context; import org.dspace.identifier.doi.DOIConnector; import org.dspace.identifier.doi.DOIIdentifierException; @@ -49,7 +50,12 @@ public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider { protected VersionHistoryService versionHistoryService; @Override - public String mint(Context context, DSpaceObject dso) + public String mint(Context context, DSpaceObject dso) throws IdentifierException { + return mint(context, dso, this.filter); + } + + @Override + public String mint(Context context, DSpaceObject dso, Filter filter) throws IdentifierException { if (!(dso instanceof Item)) { throw new IdentifierException("Currently only Items are supported for DOIs."); @@ -79,6 +85,9 @@ public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider { + " with ID " + dso.getID() + ".", ex); } + // Make a call to the filter here to throw an exception instead of carrying on with removal + creation + checkMintable(context, filter, dso); + // check whether we have a DOI in the metadata and if we have to remove it String metadataDOI = getDOIOutOfObject(dso); if (metadataDOI != null) { @@ -111,7 +120,7 @@ public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider { // ensure DOI exists in our database as well and return. // this also checks that the doi is not assigned to another dso already. try { - loadOrCreateDOI(context, dso, versionedDOI); + loadOrCreateDOI(context, dso, versionedDOI, filter); } catch (SQLException ex) { log.error( "A problem with the database connection occurd while processing DOI " + versionedDOI + ".", ex); @@ -127,7 +136,7 @@ public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider { // if we have a history, we have a item doi = makeIdentifierBasedOnHistory(context, dso, history); } else { - doi = loadOrCreateDOI(context, dso, null).getDoi(); + doi = loadOrCreateDOI(context, dso, null, filter).getDoi(); } } catch (SQLException ex) { log.error("SQLException while creating a new DOI: ", ex); @@ -140,7 +149,12 @@ public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider { } @Override - public void register(Context context, DSpaceObject dso, String identifier) + public void register(Context context, DSpaceObject dso, String identifier) throws IdentifierException { + register(context, dso, identifier, this.filter); + } + + @Override + public void register(Context context, DSpaceObject dso, String identifier, Filter filter) throws IdentifierException { if (!(dso instanceof Item)) { throw new IdentifierException("Currently only Items are supported for DOIs."); @@ -220,8 +234,14 @@ public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider { return doiPostfix; } - // Should never return null! protected String makeIdentifierBasedOnHistory(Context context, DSpaceObject dso, VersionHistory history) + throws AuthorizeException, SQLException, DOIIdentifierException, IdentifierNotApplicableException { + return makeIdentifierBasedOnHistory(context, dso, history, this.filter); + } + + // Should never return null! + protected String makeIdentifierBasedOnHistory(Context context, DSpaceObject dso, VersionHistory history, + Filter filter) throws AuthorizeException, SQLException, DOIIdentifierException, IdentifierNotApplicableException { // Mint foreach new version an identifier like: 12345/100.versionNumber // use the bare handle (g.e. 12345/100) for the first version. @@ -244,6 +264,9 @@ public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider { } if (previousVersionDOI == null) { + // Before continuing with any new DOI creation, apply the filter + checkMintable(context, filter, dso); + // We need to generate a new DOI. DOI doi = doiService.create(context); @@ -269,7 +292,7 @@ public class VersionedDOIIdentifierProvider extends DOIIdentifierProvider { String.valueOf(versionHistoryService.getVersion(context, history, item).getVersionNumber())); } - loadOrCreateDOI(context, dso, identifier); + loadOrCreateDOI(context, dso, identifier, filter); return identifier; } diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DOIConsumer.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DOIConsumer.java index 654d275d87..1961ce8274 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DOIConsumer.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DOIConsumer.java @@ -7,22 +7,32 @@ */ package org.dspace.identifier.doi; +import java.sql.SQLException; + import org.apache.logging.log4j.Logger; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.logic.Filter; +import org.dspace.content.logic.FilterUtils; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.event.Consumer; import org.dspace.event.Event; +import org.dspace.identifier.DOI; import org.dspace.identifier.DOIIdentifierProvider; import org.dspace.identifier.IdentifierException; -import org.dspace.identifier.IdentifierNotFoundException; +import org.dspace.identifier.IdentifierNotApplicableException; +import org.dspace.identifier.factory.IdentifierServiceFactory; +import org.dspace.identifier.service.DOIService; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.utils.DSpace; import org.dspace.workflow.factory.WorkflowServiceFactory; /** * @author Pascal-Nicolas Becker (p dot becker at tu hyphen berlin dot de) + * @author Kim Shepherd */ public class DOIConsumer implements Consumer { /** @@ -30,12 +40,15 @@ public class DOIConsumer implements Consumer { */ private static Logger log = org.apache.logging.log4j.LogManager.getLogger(DOIConsumer.class); + ConfigurationService configurationService; + @Override public void initialize() throws Exception { // nothing to do // we can ask spring to give as a properly setuped instance of // DOIIdentifierProvider. Doing so we don't have to configure it and // can load it in consume method as this is not very expensive. + configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); } @@ -62,36 +75,73 @@ public class DOIConsumer implements Consumer { return; } Item item = (Item) dso; - - if (ContentServiceFactory.getInstance().getWorkspaceItemService().findByItem(ctx, item) != null - || WorkflowServiceFactory.getInstance().getWorkflowItemService().findByItem(ctx, item) != null) { - // ignore workflow and workspace items, DOI will be minted when item is installed - return; + DOIIdentifierProvider provider = new DSpace().getSingletonService(DOIIdentifierProvider.class); + boolean inProgress = (ContentServiceFactory.getInstance().getWorkspaceItemService().findByItem(ctx, item) + != null || WorkflowServiceFactory.getInstance().getWorkflowItemService().findByItem(ctx, item) != null); + boolean identifiersInSubmission = configurationService.getBooleanProperty("identifiers.submission.register", + false); + DOIService doiService = IdentifierServiceFactory.getInstance().getDOIService(); + Filter workspaceFilter = null; + if (identifiersInSubmission) { + workspaceFilter = FilterUtils.getFilterFromConfiguration("identifiers.submission.filter.workspace"); } - DOIIdentifierProvider provider = new DSpace().getSingletonService( - DOIIdentifierProvider.class); - - String doi = null; + if (inProgress && !identifiersInSubmission) { + // ignore workflow and workspace items, DOI will be minted and updated when item is installed + // UNLESS special pending filter is set + return; + } + DOI doi = null; try { - doi = provider.lookup(ctx, dso); - } catch (IdentifierNotFoundException ex) { + doi = doiService.findDOIByDSpaceObject(ctx, dso); + } catch (SQLException ex) { // nothing to do here, next if clause will stop us from processing // items without dois. } if (doi == null) { - log.debug("DOIConsumer cannot handles items without DOIs, skipping: " - + event.toString()); - return; - } - try { - provider.updateMetadata(ctx, dso, doi); - } catch (IllegalArgumentException ex) { - // should not happen, as we got the DOI from the DOIProvider - log.warn("DOIConsumer caught an IdentifierException.", ex); - } catch (IdentifierException ex) { - log.warn("DOIConsumer cannot update metadata for Item with ID " - + item.getID() + " and DOI " + doi + ".", ex); + // No DOI. The only time something should be minted is if we have enabled submission reg'n and + // it passes the workspace filter. We also need to update status to PENDING straight after. + if (inProgress) { + provider.mint(ctx, dso, workspaceFilter); + DOI newDoi = doiService.findDOIByDSpaceObject(ctx, dso); + if (newDoi != null) { + newDoi.setStatus(DOIIdentifierProvider.PENDING); + doiService.update(ctx, newDoi); + } + } else { + log.debug("DOIConsumer cannot handles items without DOIs, skipping: " + event.toString()); + } + } else { + // If in progress, we can also switch PENDING and MINTED status depending on the latest filter + // evaluation + if (inProgress) { + try { + // Check the filter + provider.checkMintable(ctx, workspaceFilter, dso); + // If we made it here, the existing doi should be back to PENDING + if (DOIIdentifierProvider.MINTED.equals(doi.getStatus())) { + doi.setStatus(DOIIdentifierProvider.PENDING); + } + } catch (IdentifierNotApplicableException e) { + // Set status to MINTED if configured to downgrade existing DOIs + if (configurationService + .getBooleanProperty("identifiers.submission.strip_pending_during_submission", true)) { + doi.setStatus(DOIIdentifierProvider.MINTED); + } + } + doiService.update(ctx, doi); + } else { + try { + provider.updateMetadata(ctx, dso, doi.getDoi()); + } catch (IllegalArgumentException ex) { + // should not happen, as we got the DOI from the DOIProvider + log.warn("DOIConsumer caught an IdentifierException.", ex); + } catch (IdentifierException ex) { + log.warn("DOIConsumer cannot update metadata for Item with ID " + + item.getID() + " and DOI " + doi + ".", ex); + } + } + ctx.commit(); } } diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java index e0e0da9440..088e2b1cbc 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DOIOrganiser.java @@ -30,6 +30,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.DSpaceObject; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.logic.Filter; +import org.dspace.content.logic.FilterUtils; +import org.dspace.content.logic.TrueFilter; import org.dspace.content.service.ItemService; import org.dspace.core.Constants; import org.dspace.core.Context; @@ -61,7 +64,8 @@ public class DOIOrganiser { protected ItemService itemService; protected DOIService doiService; protected ConfigurationService configurationService; - protected boolean skipFilter; + // This filter will override the default provider filter / behaviour + protected Filter filter; /** * Constructor to be called within the main() method @@ -76,7 +80,8 @@ public class DOIOrganiser { this.itemService = ContentServiceFactory.getInstance().getItemService(); this.doiService = IdentifierServiceFactory.getInstance().getDOIService(); this.configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); - this.skipFilter = false; + this.filter = DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName( + "always_true_filter", TrueFilter.class); } /** @@ -121,12 +126,13 @@ public class DOIOrganiser { "Perform online metadata update for all identifiers queued for metadata update."); options.addOption("d", "delete-all", false, "Perform online deletion for all identifiers queued for deletion."); - options.addOption("q", "quiet", false, "Turn the command line output off."); - options.addOption(null, "skip-filter", false, - "Skip the configured item filter when registering or reserving."); + Option filterDoi = Option.builder().optionalArg(true).longOpt("filter").hasArg().argName("filterName") + .desc("Use the specified filter name instead of the provider's filter. Defaults to a special " + + "'always true' filter to force operations").build(); + options.addOption(filterDoi); Option registerDoi = Option.builder() .longOpt("register-doi") @@ -203,10 +209,12 @@ public class DOIOrganiser { } DOIService doiService = IdentifierServiceFactory.getInstance().getDOIService(); - // Should we skip the filter? - if (line.hasOption("skip-filter")) { - System.out.println("Skipping the item filter"); - organiser.skipFilter = true; + // Do we get a filter? + if (line.hasOption("filter")) { + String filter = line.getOptionValue("filter"); + if (null != filter) { + organiser.filter = FilterUtils.getFilterFromConfiguration(filter); + } } if (line.hasOption('s')) { @@ -394,19 +402,18 @@ public class DOIOrganiser { /** * Register DOI with the provider * @param doiRow - doi to register - * @param skipFilter - whether filters should be skipped before registration + * @param filter - logical item filter to override * @throws SQLException * @throws DOIIdentifierException */ - public void register(DOI doiRow, boolean skipFilter) throws SQLException, DOIIdentifierException { + public void register(DOI doiRow, Filter filter) throws SQLException, DOIIdentifierException { DSpaceObject dso = doiRow.getDSpaceObject(); if (Constants.ITEM != dso.getType()) { throw new IllegalArgumentException("Currenty DSpace supports DOIs for Items only."); } try { - provider.registerOnline(context, dso, - DOI.SCHEME + doiRow.getDoi()); + provider.registerOnline(context, dso, DOI.SCHEME + doiRow.getDoi(), filter); if (!quiet) { System.out.println("This identifier: " @@ -466,29 +473,23 @@ public class DOIOrganiser { } /** - * Register DOI with the provider, always applying (ie. never skipping) any configured filters + * Register DOI with the provider * @param doiRow - doi to register * @throws SQLException * @throws DOIIdentifierException */ public void register(DOI doiRow) throws SQLException, DOIIdentifierException { - if (this.skipFilter) { - System.out.println("Skipping the filter for " + doiRow.getDoi()); - } - register(doiRow, this.skipFilter); + register(doiRow, this.filter); } /** - * Reserve DOI with the provider, always applying (ie. never skipping) any configured filters + * Reserve DOI with the provider, * @param doiRow - doi to reserve * @throws SQLException * @throws DOIIdentifierException */ public void reserve(DOI doiRow) { - if (this.skipFilter) { - System.out.println("Skipping the filter for " + doiRow.getDoi()); - } - reserve(doiRow, this.skipFilter); + reserve(doiRow, this.filter); } /** @@ -497,14 +498,14 @@ public class DOIOrganiser { * @throws SQLException * @throws DOIIdentifierException */ - public void reserve(DOI doiRow, boolean skipFilter) { + public void reserve(DOI doiRow, Filter filter) { DSpaceObject dso = doiRow.getDSpaceObject(); if (Constants.ITEM != dso.getType()) { throw new IllegalArgumentException("Currently DSpace supports DOIs for Items only."); } try { - provider.reserveOnline(context, dso, DOI.SCHEME + doiRow.getDoi(), skipFilter); + provider.reserveOnline(context, dso, DOI.SCHEME + doiRow.getDoi(), filter); if (!quiet) { System.out.println("This identifier : " + DOI.SCHEME + doiRow.getDoi() + " is successfully reserved."); @@ -699,7 +700,7 @@ public class DOIOrganiser { //Check if this Item has an Identifier, mint one if it doesn't if (null == doiRow) { - doi = provider.mint(context, dso, this.skipFilter); + doi = provider.mint(context, dso, this.filter); doiRow = doiService.findByDoi(context, doi.substring(DOI.SCHEME.length())); return doiRow; @@ -723,7 +724,7 @@ public class DOIOrganiser { doiRow = doiService.findDOIByDSpaceObject(context, dso); if (null == doiRow) { - doi = provider.mint(context, dso, this.skipFilter); + doi = provider.mint(context, dso, this.filter); doiRow = doiService.findByDoi(context, doi.substring(DOI.SCHEME.length())); } diff --git a/dspace-api/src/main/java/org/dspace/identifier/service/IdentifierService.java b/dspace-api/src/main/java/org/dspace/identifier/service/IdentifierService.java index 64eee1dfcf..23005b6575 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/service/IdentifierService.java +++ b/dspace-api/src/main/java/org/dspace/identifier/service/IdentifierService.java @@ -9,9 +9,11 @@ package org.dspace.identifier.service; import java.sql.SQLException; import java.util.List; +import java.util.Map; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; +import org.dspace.content.logic.Filter; import org.dspace.core.Context; import org.dspace.identifier.Identifier; import org.dspace.identifier.IdentifierException; @@ -103,6 +105,52 @@ public interface IdentifierService { */ void register(Context context, DSpaceObject dso) throws AuthorizeException, SQLException, IdentifierException; + /** + * + * Register identifiers for a DSO, with a map of logical filters for each Identifier class to apply + * at the time of local registration. + * + * @param context The relevant DSpace Context. + * @param dso DSpace object to be registered + * @param typeFilters If a service supports a given Identifier implementation, apply the associated filter + * @throws AuthorizeException if authorization error + * @throws SQLException if database error + * @throws IdentifierException if identifier error + */ + void register(Context context, DSpaceObject dso, Map, Filter> typeFilters) + throws AuthorizeException, SQLException, IdentifierException; + + /** + * + * Register identifier(s) for the given DSO just with providers that support that Identifier class, and + * apply the given filter if that provider extends FilteredIdentifierProvider + * + * @param context The relevant DSpace Context. + * @param dso DSpace object to be registered + * @param type Type of identifier to register + * @param filter If a service supports a given Identifier implementation, apply this specific filter + * @throws AuthorizeException if authorization error + * @throws SQLException if database error + * @throws IdentifierException if identifier error + */ + void register(Context context, DSpaceObject dso, Class type, Filter filter) + throws AuthorizeException, SQLException, IdentifierException; + + /** + * + * Register identifier(s) for the given DSO just with providers that support that Identifier class, and + * apply the given filter if that provider extends FilteredIdentifierProvider + * + * @param context The relevant DSpace Context. + * @param dso DSpace object to be registered + * @param type Type of identifier to register + * @throws AuthorizeException if authorization error + * @throws SQLException if database error + * @throws IdentifierException if identifier error + */ + void register(Context context, DSpaceObject dso, Class type) + throws AuthorizeException, SQLException, IdentifierException; + /** * Used to Register a specific Identifier (for example a Handle, hdl:1234.5/6). * The provider is responsible for detecting and processing the appropriate diff --git a/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java b/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java index 4eb7cdbbc1..c8a7812a51 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java @@ -8,6 +8,7 @@ package org.dspace.scripts; import java.lang.reflect.InvocationTargetException; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; @@ -36,7 +37,9 @@ public class ScriptServiceImpl implements ScriptService { @Override public List getScriptConfigurations(Context context) { return serviceManager.getServicesByType(ScriptConfiguration.class).stream().filter( - scriptConfiguration -> scriptConfiguration.isAllowedToExecute(context)).collect(Collectors.toList()); + scriptConfiguration -> scriptConfiguration.isAllowedToExecute(context)) + .sorted(Comparator.comparing(ScriptConfiguration::getName)) + .collect(Collectors.toList()); } @Override diff --git a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java index 0bd71088da..b8a1a2e96a 100644 --- a/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/storage/bitstore/BitstreamStorageServiceImpl.java @@ -303,6 +303,10 @@ public class BitstreamStorageServiceImpl implements BitstreamStorageService, Ini commitCounter++; if (commitCounter % 100 == 0) { context.dispatchEvents(); + // Commit actual changes to DB after dispatch events + System.out.print("Performing incremental commit to the database..."); + context.commit(); + System.out.println(" Incremental commit done!"); } context.uncacheEntity(bitstream); diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/ContentGenerator.java b/dspace-api/src/main/java/org/dspace/subscriptions/ContentGenerator.java new file mode 100644 index 0000000000..a913f2504a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/ContentGenerator.java @@ -0,0 +1,97 @@ +/** + * 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.subscriptions; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.lang.StringUtils.EMPTY; + +import java.io.ByteArrayOutputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Item; +import org.dspace.content.crosswalk.StreamDisseminationCrosswalk; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.core.Email; +import org.dspace.core.I18nUtil; +import org.dspace.discovery.IndexableObject; +import org.dspace.eperson.EPerson; +import org.dspace.subscriptions.service.SubscriptionGenerator; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation class of SubscriptionGenerator + * which will handle the logic of sending the emails + * in case of 'content' subscriptionType + */ +@SuppressWarnings("rawtypes") +public class ContentGenerator implements SubscriptionGenerator { + + private final Logger log = LogManager.getLogger(ContentGenerator.class); + + @SuppressWarnings("unchecked") + private Map entityType2Disseminator = new HashMap(); + + @Autowired + private ItemService itemService; + + @Override + public void notifyForSubscriptions(Context context, EPerson ePerson, + List indexableComm, + List indexableColl) { + try { + if (Objects.nonNull(ePerson)) { + Locale supportedLocale = I18nUtil.getEPersonLocale(ePerson); + Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, "subscriptions_content")); + email.addRecipient(ePerson.getEmail()); + email.addArgument(generateBodyMail(context, indexableComm)); + email.addArgument(generateBodyMail(context, indexableColl)); + email.send(); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + log.warn("Cannot email user eperson_id: {} eperson_email: {}", ePerson::getID, ePerson::getEmail); + } + } + + private String generateBodyMail(Context context, List indexableObjects) { + try { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write("\n".getBytes(UTF_8)); + if (indexableObjects.size() > 0) { + for (IndexableObject indexableObject : indexableObjects) { + out.write("\n".getBytes(UTF_8)); + Item item = (Item) indexableObject.getIndexedObject(); + String entityType = itemService.getEntityTypeLabel(item); + Optional.ofNullable(entityType2Disseminator.get(entityType)) + .orElseGet(() -> entityType2Disseminator.get("Item")) + .disseminate(context, item, out); + } + return out.toString(); + } else { + out.write("No items".getBytes(UTF_8)); + } + return out.toString(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return EMPTY; + } + + public void setEntityType2Disseminator(Map entityType2Disseminator) { + this.entityType2Disseminator = entityType2Disseminator; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java new file mode 100644 index 0000000000..b429ecbd46 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotification.java @@ -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.subscriptions; + +import java.sql.SQLException; +import java.util.Objects; +import java.util.UUID; + +import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.FrequencyType; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.utils.DSpace; + +/** + * Implementation of {@link DSpaceRunnable} to find subscribed objects and send notification mails about them + * + * @author alba aliu + */ +public class SubscriptionEmailNotification + extends DSpaceRunnable> { + + private Context context; + private SubscriptionEmailNotificationService subscriptionEmailNotificationService; + + @Override + @SuppressWarnings("unchecked") + public SubscriptionEmailNotificationConfiguration getScriptConfiguration() { + return new DSpace().getServiceManager().getServiceByName("subscription-send", + SubscriptionEmailNotificationConfiguration.class); + } + + @Override + public void setup() throws ParseException { + this.subscriptionEmailNotificationService = new DSpace().getServiceManager().getServiceByName( + SubscriptionEmailNotificationServiceImpl.class.getName(), SubscriptionEmailNotificationServiceImpl.class); + } + + @Override + public void internalRun() throws Exception { + assignCurrentUserInContext(); + assignSpecialGroupsInContext(); + String frequencyOption = commandLine.getOptionValue("f"); + if (StringUtils.isBlank(frequencyOption)) { + throw new IllegalArgumentException("Option --frequency (-f) must be set"); + } + + if (!FrequencyType.isSupportedFrequencyType(frequencyOption)) { + throw new IllegalArgumentException( + "Option f must be one of following values D(Day), W(Week) or M(Month)"); + } + subscriptionEmailNotificationService.perform(getContext(), handler, "content", frequencyOption); + } + + private void assignCurrentUserInContext() throws SQLException { + context = new Context(); + UUID uuid = getEpersonIdentifier(); + if (Objects.nonNull(uuid)) { + EPerson ePerson = EPersonServiceFactory.getInstance().getEPersonService().find(context, uuid); + context.setCurrentUser(ePerson); + } + } + + private void assignSpecialGroupsInContext() throws SQLException { + for (UUID uuid : handler.getSpecialGroups()) { + context.setSpecialGroup(uuid); + } + } + + public SubscriptionEmailNotificationService getSubscriptionEmailNotificationService() { + return subscriptionEmailNotificationService; + } + + public void setSubscriptionEmailNotificationService(SubscriptionEmailNotificationService notificationService) { + this.subscriptionEmailNotificationService = notificationService; + } + + public Context getContext() { + return context; + } + + public void setContext(Context context) { + this.context = context; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationCli.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationCli.java new file mode 100644 index 0000000000..338e7ff0e1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationCli.java @@ -0,0 +1,15 @@ +/** + * 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.subscriptions; + +/** + * Extension of {@link SubscriptionEmailNotification} for CLI. + */ +public class SubscriptionEmailNotificationCli extends SubscriptionEmailNotification { + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationCliScriptConfiguration.java new file mode 100644 index 0000000000..f0eb2fd5c8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationCliScriptConfiguration.java @@ -0,0 +1,16 @@ +/** + * 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.subscriptions; + +/** + * Extension of {@link SubscriptionEmailNotificationCli} for CLI. + */ +public class SubscriptionEmailNotificationCliScriptConfiguration + extends SubscriptionEmailNotificationConfiguration { + +} diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java new file mode 100644 index 0000000000..52685b563d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationConfiguration.java @@ -0,0 +1,63 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.subscriptions; + +import java.sql.SQLException; +import java.util.Objects; + +import org.apache.commons.cli.Options; +import org.dspace.authorize.AuthorizeServiceImpl; +import org.dspace.core.Context; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.scripts.configuration.ScriptConfiguration; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link DSpaceRunnable} to find subscribed objects and send notification mails about them + */ +public class SubscriptionEmailNotificationConfiguration extends ScriptConfiguration { + + private Class dspaceRunnableClass; + + @Autowired + private AuthorizeServiceImpl authorizeService; + + @Override + public boolean isAllowedToExecute(Context context) { + try { + return authorizeService.isAdmin(context); + } catch (SQLException e) { + throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e); + } + } + + @Override + public Options getOptions() { + if (Objects.isNull(options)) { + Options options = new Options(); + options.addOption("f", "frequency", true, + "Subscription frequency. Valid values include: D (Day), W (Week) and M (Month)"); + options.getOption("f").setRequired(true); + super.options = options; + } + return options; + } + + @Override + public Class getDspaceRunnableClass() { + return dspaceRunnableClass; + } + + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationService.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationService.java new file mode 100644 index 0000000000..9527223509 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationService.java @@ -0,0 +1,37 @@ +/** + * 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.subscriptions; + +import java.util.Set; + +import org.dspace.core.Context; +import org.dspace.scripts.handler.DSpaceRunnableHandler; + +/** + * Service interface class for the subscription e-mail notification services + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public interface SubscriptionEmailNotificationService { + + /** + * Performs sending of e-mails to subscribers by frequency value and SubscriptionType + * + * @param context DSpace context object + * @param handler Applicable DSpaceRunnableHandler + * @param subscriptionType Currently supported only "content" + * @param frequency Valid values include: D (Day), W (Week) and M (Month) + */ + public void perform(Context context, DSpaceRunnableHandler handler, String subscriptionType, String frequency); + + /** + * returns a set of supported SubscriptionTypes + */ + public Set getSupportedSubscriptionTypes(); + +} diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java new file mode 100644 index 0000000000..8fb01cd36e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/SubscriptionEmailNotificationServiceImpl.java @@ -0,0 +1,172 @@ +/** + * 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.subscriptions; + +import static org.dspace.core.Constants.COLLECTION; +import static org.dspace.core.Constants.COMMUNITY; +import static org.dspace.core.Constants.READ; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.discovery.IndexableObject; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Subscription; +import org.dspace.eperson.service.SubscribeService; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.scripts.handler.DSpaceRunnableHandler; +import org.dspace.subscriptions.service.DSpaceObjectUpdates; +import org.dspace.subscriptions.service.SubscriptionGenerator; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link DSpaceRunnable} to find subscribed objects and send notification mails about them + * + * @author alba aliu + */ +public class SubscriptionEmailNotificationServiceImpl implements SubscriptionEmailNotificationService { + + private static final Logger log = LogManager.getLogger(SubscriptionEmailNotificationServiceImpl.class); + + private Map contentUpdates = new HashMap<>(); + @SuppressWarnings("rawtypes") + private Map subscriptionType2generators = new HashMap<>(); + + @Autowired + private AuthorizeService authorizeService; + @Autowired + private SubscribeService subscribeService; + + @SuppressWarnings("rawtypes") + public SubscriptionEmailNotificationServiceImpl(Map contentUpdates, + Map subscriptionType2generators) { + this.contentUpdates = contentUpdates; + this.subscriptionType2generators = subscriptionType2generators; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void perform(Context context, DSpaceRunnableHandler handler, String subscriptionType, String frequency) { + List communityItems = new ArrayList<>(); + List collectionsItems = new ArrayList<>(); + try { + List subscriptions = + findAllSubscriptionsBySubscriptionTypeAndFrequency(context, subscriptionType, frequency); + // Here is verified if SubscriptionType is "content" Or "statistics" as them are configured + if (subscriptionType2generators.keySet().contains(subscriptionType)) { + // the list of the person who has subscribed + int iterator = 0; + for (Subscription subscription : subscriptions) { + DSpaceObject dSpaceObject = subscription.getDSpaceObject(); + EPerson ePerson = subscription.getEPerson(); + + if (!authorizeService.authorizeActionBoolean(context, ePerson, dSpaceObject, READ, true)) { + iterator++; + continue; + } + + if (dSpaceObject.getType() == COMMUNITY) { + List indexableCommunityItems = contentUpdates + .get(Community.class.getSimpleName().toLowerCase()) + .findUpdates(context, dSpaceObject, frequency); + communityItems.addAll(getItems(context, ePerson, indexableCommunityItems)); + } else if (dSpaceObject.getType() == COLLECTION) { + List indexableCollectionItems = contentUpdates + .get(Collection.class.getSimpleName().toLowerCase()) + .findUpdates(context, dSpaceObject, frequency); + collectionsItems.addAll(getItems(context, ePerson, indexableCollectionItems)); + } else { + log.warn("found an invalid DSpace Object type ({}) among subscriptions to send", + dSpaceObject.getType()); + continue; + } + + if (iterator < subscriptions.size() - 1) { + // as the subscriptions are ordered by eperson id, so we send them by ePerson + if (ePerson.equals(subscriptions.get(iterator + 1).getEPerson())) { + iterator++; + continue; + } else { + subscriptionType2generators.get(subscriptionType) + .notifyForSubscriptions(context, ePerson, communityItems, collectionsItems); + communityItems.clear(); + collectionsItems.clear(); + } + } else { + //in the end of the iteration + subscriptionType2generators.get(subscriptionType) + .notifyForSubscriptions(context, ePerson, communityItems, collectionsItems); + } + iterator++; + } + } else { + throw new IllegalArgumentException("Currently this SubscriptionType:" + subscriptionType + + " is not supported!"); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + handler.handleException(e); + context.abort(); + } + } + + @SuppressWarnings("rawtypes") + private List getItems(Context context, EPerson ePerson, List indexableItems) + throws SQLException { + List items = new ArrayList(); + for (IndexableObject indexableitem : indexableItems) { + Item item = (Item) indexableitem.getIndexedObject(); + if (authorizeService.authorizeActionBoolean(context, ePerson, item, READ, true)) { + items.add(indexableitem); + } + } + return items; + } + + /** + * Return all Subscriptions by subscriptionType and frequency ordered by ePerson ID + * if there are none it returns an empty list + * + * @param context DSpace context + * @param subscriptionType Could be "content" or "statistics". NOTE: in DSpace we have only "content" + * @param frequency Could be "D" stand for Day, "W" stand for Week, and "M" stand for Month + * @return + */ + private List findAllSubscriptionsBySubscriptionTypeAndFrequency(Context context, + String subscriptionType, String frequency) { + try { + return subscribeService.findAllSubscriptionsBySubscriptionTypeAndFrequency(context, subscriptionType, + frequency) + .stream() + .sorted(Comparator.comparing(s -> s.getEPerson().getID())) + .collect(Collectors.toList()); + } catch (SQLException e) { + log.error(e.getMessage(), e); + } + return new ArrayList(); + } + + @Override + public Set getSupportedSubscriptionTypes() { + return subscriptionType2generators.keySet(); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/objectupdates/CollectionUpdates.java b/dspace-api/src/main/java/org/dspace/subscriptions/objectupdates/CollectionUpdates.java new file mode 100644 index 0000000000..12d056f368 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/objectupdates/CollectionUpdates.java @@ -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.subscriptions.objectupdates; + +import java.util.List; + +import org.dspace.content.DSpaceObject; +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.eperson.FrequencyType; +import org.dspace.subscriptions.service.DSpaceObjectUpdates; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Class which will be used to find + * all collection objects updated related with subscribed DSO + * + * @author Alba Aliu + */ +public class CollectionUpdates implements DSpaceObjectUpdates { + + @Autowired + private SearchService searchService; + + @Override + @SuppressWarnings("rawtypes") + public List findUpdates(Context context, DSpaceObject dSpaceObject, String frequency) + throws SearchServiceException { + DiscoverQuery discoverQuery = new DiscoverQuery(); + getDefaultFilterQueries().stream().forEach(fq -> discoverQuery.addFilterQueries(fq)); + discoverQuery.addFilterQueries("location.coll:(" + dSpaceObject.getID() + ")"); + discoverQuery.addFilterQueries("lastModified:" + FrequencyType.findLastFrequency(frequency)); + DiscoverResult discoverResult = searchService.search(context, discoverQuery); + return discoverResult.getIndexableObjects(); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/objectupdates/CommunityUpdates.java b/dspace-api/src/main/java/org/dspace/subscriptions/objectupdates/CommunityUpdates.java new file mode 100644 index 0000000000..0ae80d287a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/objectupdates/CommunityUpdates.java @@ -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.subscriptions.objectupdates; + +import java.util.List; + +import org.dspace.content.DSpaceObject; +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.eperson.FrequencyType; +import org.dspace.subscriptions.service.DSpaceObjectUpdates; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Class which will be used to find + * all community objects updated related with subscribed DSO + * + * @author Alba Aliu + */ +public class CommunityUpdates implements DSpaceObjectUpdates { + + @Autowired + private SearchService searchService; + + @Override + @SuppressWarnings("rawtypes") + public List findUpdates(Context context, DSpaceObject dSpaceObject, String frequency) + throws SearchServiceException { + DiscoverQuery discoverQuery = new DiscoverQuery(); + getDefaultFilterQueries().stream().forEach(fq -> discoverQuery.addFilterQueries(fq)); + discoverQuery.addFilterQueries("location.comm:(" + dSpaceObject.getID() + ")"); + discoverQuery.addFilterQueries("lastModified:" + FrequencyType.findLastFrequency(frequency)); + DiscoverResult discoverResult = searchService.search(context, discoverQuery); + return discoverResult.getIndexableObjects(); + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/service/DSpaceObjectUpdates.java b/dspace-api/src/main/java/org/dspace/subscriptions/service/DSpaceObjectUpdates.java new file mode 100644 index 0000000000..ec09b2a45f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/service/DSpaceObjectUpdates.java @@ -0,0 +1,41 @@ +/** + * 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.subscriptions.service; + +import java.util.Arrays; +import java.util.List; + +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.discovery.IndexableObject; +import org.dspace.discovery.SearchServiceException; + +/** + * Interface class which will be used to find all objects updated related with subscribed DSO + * + * @author Alba Aliu + */ +public interface DSpaceObjectUpdates { + + /** + * Send an email to some addresses, concerning a Subscription, using a given dso. + * + * @param context current DSpace session. + */ + @SuppressWarnings("rawtypes") + public List findUpdates(Context context, DSpaceObject dSpaceObject, String frequency) + throws SearchServiceException; + + default List getDefaultFilterQueries() { + return Arrays.asList("search.resourcetype:" + Item.class.getSimpleName(), + "-discoverable:" + false, + "-withdrawn:" + true); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/subscriptions/service/SubscriptionGenerator.java b/dspace-api/src/main/java/org/dspace/subscriptions/service/SubscriptionGenerator.java new file mode 100644 index 0000000000..1790513b9b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/subscriptions/service/SubscriptionGenerator.java @@ -0,0 +1,25 @@ +/** + * 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.subscriptions.service; + +import java.util.List; + +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; + +/** + * Interface Class which will be used to send email notifications to ePerson + * containing information for all list of objects. + * + * @author Alba Aliu + */ +public interface SubscriptionGenerator { + + public void notifyForSubscriptions(Context c, EPerson ePerson, List comm, List coll); + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/workflow/WorkflowService.java b/dspace-api/src/main/java/org/dspace/workflow/WorkflowService.java index 716b6cabd3..613c5821bc 100644 --- a/dspace-api/src/main/java/org/dspace/workflow/WorkflowService.java +++ b/dspace-api/src/main/java/org/dspace/workflow/WorkflowService.java @@ -18,6 +18,7 @@ import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.xmlworkflow.WorkflowConfigurationException; +import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; /** * Service interface class for the WorkflowService framework. @@ -100,6 +101,9 @@ public interface WorkflowService { String rejection_message) throws SQLException, AuthorizeException, IOException; + public void restartWorkflow(Context context, XmlWorkflowItem wi, EPerson decliner, String provenance) + throws SQLException, AuthorizeException, IOException, WorkflowException; + public String getMyDSpaceLink(); public void deleteCollection(Context context, Collection collection) diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/Role.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/Role.java index bfc5654cdd..5b5ba5c1d3 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/Role.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/Role.java @@ -41,6 +41,9 @@ public class Role implements BeanNameAware { @Autowired private WorkflowItemRoleService workflowItemRoleService; + // Whether or not to delete temporary group made attached to the WorkflowItemRole for this role in AutoAssignAction + private boolean deleteTemporaryGroup = false; + private String id; private String name; private String description; @@ -153,4 +156,17 @@ public class Role implements BeanNameAware { public void setInternal(boolean internal) { isInternal = internal; } + + public boolean isDeleteTemporaryGroup() { + return deleteTemporaryGroup; + } + + /** + * Setter for config that indicated whether or not to delete temporary group made attached to the + * WorkflowItemRole for this role in AutoAssignAction + * @param deleteTemporaryGroup + */ + public void setDeleteTemporaryGroup(boolean deleteTemporaryGroup) { + this.deleteTemporaryGroup = deleteTemporaryGroup; + } } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/XmlWorkflowServiceImpl.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/XmlWorkflowServiceImpl.java index 90f180ec87..da7910da29 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/XmlWorkflowServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/XmlWorkflowServiceImpl.java @@ -1076,6 +1076,53 @@ public class XmlWorkflowServiceImpl implements XmlWorkflowService { return wsi; } + @Override + public void restartWorkflow(Context context, XmlWorkflowItem wi, EPerson decliner, String provenance) + throws SQLException, AuthorizeException, IOException, WorkflowException { + if (!authorizeService.isAdmin(context)) { + throw new AuthorizeException("You must be an admin to restart a workflow"); + } + context.turnOffAuthorisationSystem(); + + // rejection provenance + Item myitem = wi.getItem(); + + // Here's what happened + String provDescription = + provenance + " Declined by " + getEPersonName(decliner) + " on " + DCDate.getCurrent().toString() + + " (GMT) "; + + // Add to item as a DC field + itemService + .addMetadata(context, myitem, MetadataSchemaEnum.DC.getName(), + "description", "provenance", "en", provDescription); + + //Clear any workflow schema related metadata + itemService + .clearMetadata(context, myitem, WorkflowRequirementsService.WORKFLOW_SCHEMA, Item.ANY, Item.ANY, Item.ANY); + + itemService.update(context, myitem); + + // remove policy for controller + removeUserItemPolicies(context, myitem, decliner); + revokeReviewerPolicies(context, myitem); + + // convert into personal workspace + WorkspaceItem wsi = returnToWorkspace(context, wi); + + // Because of issue of xmlWorkflowItemService not realising wfi wrapper has been deleted + context.commit(); + wsi = context.reloadEntity(wsi); + + log.info(LogHelper.getHeader(context, "decline_workflow", "workflow_item_id=" + + wi.getID() + "item_id=" + wi.getItem().getID() + "collection_id=" + wi.getCollection().getID() + + "eperson_id=" + decliner.getID())); + + // Restart workflow + this.startWithoutNotify(context, wsi); + context.restoreAuthSystemState(); + } + /** * Return the workflow item to the workspace of the submitter. The workflow * item is removed, and a workspace item created. diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/Action.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/Action.java index 0aabfab057..1cfa33b121 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/Action.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/Action.java @@ -14,10 +14,15 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DCDate; +import org.dspace.content.MetadataSchemaEnum; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; import org.dspace.core.Context; import org.dspace.workflow.WorkflowException; import org.dspace.xmlworkflow.RoleMembers; import org.dspace.xmlworkflow.WorkflowConfigurationException; +import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; import org.dspace.xmlworkflow.state.Step; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; @@ -37,6 +42,8 @@ public abstract class Action { private WorkflowActionConfig parent; private static final String ERROR_FIELDS_ATTRIBUTE = "dspace.workflow.error_fields"; + private List advancedOptions = new ArrayList<>(); + private List advancedInfo = new ArrayList<>(); /** * Called when a workflow item becomes eligible for this Action. @@ -192,4 +199,58 @@ public abstract class Action { //save updated list setErrorFields(request, errorFields); } + + /** + * Returns a list of advanced options that the user can select at this action + * @return A list of advanced options of this action, resulting in the next step of the workflow + */ + protected List getAdvancedOptions() { + return advancedOptions; + } + + /** + * Returns true if this Action has advanced options, false if it doesn't + * @return true if there are advanced options, false otherwise + */ + protected boolean isAdvanced() { + return !getAdvancedOptions().isEmpty(); + } + + /** + * Returns a list of advanced info required by the advanced options + * @return A list of advanced info required by the advanced options + */ + protected List getAdvancedInfo() { + return advancedInfo; + } + + + /** + * Adds info in the metadata field dc.description.provenance about item being approved containing in which step + * it was approved, which user approved it and the time + * + * @param c DSpace contect + * @param wfi Workflow item we're adding workflow accept provenance on + */ + public void addApprovedProvenance(Context c, XmlWorkflowItem wfi) throws SQLException, AuthorizeException { + ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + + //Add the provenance for the accept + String now = DCDate.getCurrent().toString(); + + // Get user's name + email address + String usersName = + XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService().getEPersonName(c.getCurrentUser()); + + String provDescription = getProvenanceStartId() + " Approved for entry into archive by " + usersName + " on " + + now + " (GMT) "; + + // Add to item as a DC field + c.turnOffAuthorisationSystem(); + itemService.addMetadata(c, wfi.getItem(), MetadataSchemaEnum.DC.getName(), "description", "provenance", "en", + provDescription); + itemService.update(c, wfi.getItem()); + c.restoreAuthSystemState(); + } + } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/ActionAdvancedInfo.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/ActionAdvancedInfo.java new file mode 100644 index 0000000000..b49fdb34f8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/ActionAdvancedInfo.java @@ -0,0 +1,42 @@ +/** + * 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.xmlworkflow.state.actions; + +/** + * Interface for the shared properties of an 'advancedInfo' section of an advanced workflow {@link Action} + * Implementations of this class will define the specific fields per action that will need to be defined/configured + * to pass along this info to REST endpoint + */ +public abstract class ActionAdvancedInfo { + + protected String type; + protected String id; + + protected final static String TYPE_PREFIX = "action_info_"; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = TYPE_PREFIX + type; + } + + public String getId() { + return id; + } + + /** + * Setter for the Action id to be set. + * This is an MD5 hash of the type and the stringified properties of the advanced info + * + * @param type The type of this Action to be included in the MD5 hash + */ + protected abstract void generateId(String type); + +} diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/WorkflowActionConfig.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/WorkflowActionConfig.java index 1dc61888b1..3475b04c74 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/WorkflowActionConfig.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/WorkflowActionConfig.java @@ -69,4 +69,28 @@ public class WorkflowActionConfig { return this.processingAction.getOptions(); } + /** + * Returns a list of advanced options this user has on this action, resulting in the next step of the workflow + * @return A list of advanced options of this action, resulting in the next step of the workflow + */ + public List getAdvancedOptions() { + return this.processingAction.getAdvancedOptions(); + } + + /** + * Returns a boolean depending on whether this action has advanced options + * @return The boolean indicating whether this action has advanced options + */ + public boolean isAdvanced() { + return this.processingAction.isAdvanced(); + } + + /** + * Returns a Map of info for the advanced options this user has on this action + * @return a Map of info for the advanced options this user has on this action + */ + public List getAdvancedInfo() { + return this.processingAction.getAdvancedInfo(); + } + } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/AcceptEditRejectAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/AcceptEditRejectAction.java index 743d00b2b6..67b400c659 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/AcceptEditRejectAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/AcceptEditRejectAction.java @@ -15,8 +15,6 @@ import javax.servlet.http.HttpServletRequest; import org.dspace.app.util.Util; import org.dspace.authorize.AuthorizeException; -import org.dspace.content.DCDate; -import org.dspace.content.MetadataSchemaEnum; import org.dspace.core.Context; import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; import org.dspace.xmlworkflow.state.Step; @@ -34,8 +32,6 @@ import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; */ public class AcceptEditRejectAction extends ProcessingAction { - private static final String SUBMIT_APPROVE = "submit_approve"; - private static final String SUBMIT_REJECT = "submit_reject"; private static final String SUBMITTER_IS_DELETED_PAGE = "submitter_deleted"; //TODO: rename to AcceptAndEditMetadataAction @@ -53,7 +49,7 @@ public class AcceptEditRejectAction extends ProcessingAction { case SUBMIT_APPROVE: return processAccept(c, wfi); case SUBMIT_REJECT: - return processRejectPage(c, wfi, request); + return super.processRejectPage(c, wfi, request); case SUBMITTER_IS_DELETED_PAGE: return processSubmitterIsDeletedPage(c, wfi, request); default: @@ -69,33 +65,18 @@ public class AcceptEditRejectAction extends ProcessingAction { options.add(SUBMIT_APPROVE); options.add(SUBMIT_REJECT); options.add(ProcessingAction.SUBMIT_EDIT_METADATA); + options.add(RETURN_TO_POOL); return options; } public ActionResult processAccept(Context c, XmlWorkflowItem wfi) throws SQLException, AuthorizeException { //Delete the tasks - addApprovedProvenance(c, wfi); + super.addApprovedProvenance(c, wfi); return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); } - public ActionResult processRejectPage(Context c, XmlWorkflowItem wfi, HttpServletRequest request) - throws SQLException, AuthorizeException, IOException { - String reason = request.getParameter("reason"); - if (reason == null || 0 == reason.trim().length()) { - addErrorField(request, "reason"); - return new ActionResult(ActionResult.TYPE.TYPE_ERROR); - } - - // We have pressed reject, so remove the task the user has & put it back - // to a workspace item - XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService().sendWorkflowItemBackSubmission(c, wfi, - c.getCurrentUser(), this.getProvenanceStartId(), reason); - - return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); - } - public ActionResult processSubmitterIsDeletedPage(Context c, XmlWorkflowItem wfi, HttpServletRequest request) throws SQLException, AuthorizeException, IOException { if (request.getParameter("submit_delete") != null) { @@ -111,21 +92,4 @@ public class AcceptEditRejectAction extends ProcessingAction { return new ActionResult(ActionResult.TYPE.TYPE_PAGE); } } - - private void addApprovedProvenance(Context c, XmlWorkflowItem wfi) throws SQLException, AuthorizeException { - //Add the provenance for the accept - String now = DCDate.getCurrent().toString(); - - // Get user's name + email address - String usersName = XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() - .getEPersonName(c.getCurrentUser()); - - String provDescription = getProvenanceStartId() + " Approved for entry into archive by " - + usersName + " on " + now + " (GMT) "; - - // Add to item as a DC field - itemService.addMetadata(c, wfi.getItem(), MetadataSchemaEnum.DC.getName(), "description", "provenance", "en", - provDescription); - itemService.update(c, wfi.getItem()); - } } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/FinalEditAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/FinalEditAction.java index 3c4e0ffc1d..9b83be5d7b 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/FinalEditAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/FinalEditAction.java @@ -14,10 +14,7 @@ import javax.servlet.http.HttpServletRequest; import org.dspace.app.util.Util; import org.dspace.authorize.AuthorizeException; -import org.dspace.content.DCDate; -import org.dspace.content.MetadataSchemaEnum; import org.dspace.core.Context; -import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; import org.dspace.xmlworkflow.state.Step; import org.dspace.xmlworkflow.state.actions.ActionResult; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; @@ -52,7 +49,7 @@ public class FinalEditAction extends ProcessingAction { switch (Util.getSubmitButton(request, SUBMIT_CANCEL)) { case SUBMIT_APPROVE: //Delete the tasks - addApprovedProvenance(c, wfi); + super.addApprovedProvenance(c, wfi); return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); default: //We pressed the leave button so return to our submissions page @@ -67,25 +64,8 @@ public class FinalEditAction extends ProcessingAction { List options = new ArrayList<>(); options.add(SUBMIT_APPROVE); options.add(ProcessingAction.SUBMIT_EDIT_METADATA); + options.add(RETURN_TO_POOL); return options; } - private void addApprovedProvenance(Context c, XmlWorkflowItem wfi) throws SQLException, AuthorizeException { - //Add the provenance for the accept - String now = DCDate.getCurrent().toString(); - - // Get user's name + email address - String usersName = XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() - .getEPersonName(c.getCurrentUser()); - - String provDescription = getProvenanceStartId() + " Approved for entry into archive by " - + usersName + " on " + now + " (GMT) "; - - // Add to item as a DC field - itemService.addMetadata(c, wfi.getItem(), MetadataSchemaEnum.DC.getName(), "description", "provenance", "en", - provDescription); - itemService.update(c, wfi.getItem()); - } - - } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ProcessingAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ProcessingAction.java index 8b8358a8d6..7a1c62adbd 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ProcessingAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ProcessingAction.java @@ -7,12 +7,16 @@ */ package org.dspace.xmlworkflow.state.actions.processingaction; +import java.io.IOException; import java.sql.SQLException; import javax.servlet.http.HttpServletRequest; +import org.dspace.authorize.AuthorizeException; import org.dspace.content.service.ItemService; import org.dspace.core.Context; +import org.dspace.xmlworkflow.service.XmlWorkflowService; import org.dspace.xmlworkflow.state.actions.Action; +import org.dspace.xmlworkflow.state.actions.ActionResult; import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; import org.dspace.xmlworkflow.storedcomponents.service.ClaimedTaskService; @@ -32,9 +36,15 @@ public abstract class ProcessingAction extends Action { protected ClaimedTaskService claimedTaskService; @Autowired(required = true) protected ItemService itemService; + @Autowired + protected XmlWorkflowService xmlWorkflowService; public static final String SUBMIT_EDIT_METADATA = "submit_edit_metadata"; public static final String SUBMIT_CANCEL = "submit_cancel"; + protected static final String SUBMIT_APPROVE = "submit_approve"; + protected static final String SUBMIT_REJECT = "submit_reject"; + protected static final String RETURN_TO_POOL = "return_to_pool"; + protected static final String REJECT_REASON = "reason"; @Override public boolean isAuthorized(Context context, HttpServletRequest request, XmlWorkflowItem wfi) throws SQLException { @@ -48,4 +58,31 @@ public abstract class ProcessingAction extends Action { task.getStepID().equals(getParent().getStep().getId()) && task.getActionID().equals(getParent().getId()); } + + /** + * Process result when option {@link this#SUBMIT_REJECT} is selected. + * - Sets the reason and workflow step responsible on item in dc.description.provenance + * - Send workflow back to the submission + * If reason is not given => error + */ + public ActionResult processRejectPage(Context c, XmlWorkflowItem wfi, HttpServletRequest request) + throws SQLException, AuthorizeException, IOException { + String reason = request.getParameter(REJECT_REASON); + if (reason == null || 0 == reason.trim().length()) { + addErrorField(request, REJECT_REASON); + return new ActionResult(ActionResult.TYPE.TYPE_ERROR); + } + + // We have pressed reject, so remove the task the user has & put it back + // to a workspace item + xmlWorkflowService.sendWorkflowItemBackSubmission(c, wfi, c.getCurrentUser(), this.getProvenanceStartId(), + reason); + + return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); + } + + @Override + protected boolean isAdvanced() { + return !getAdvancedOptions().isEmpty(); + } } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ReviewAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ReviewAction.java index 8474757be6..bd74ab3c71 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ReviewAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ReviewAction.java @@ -15,8 +15,6 @@ import javax.servlet.http.HttpServletRequest; import org.dspace.app.util.Util; import org.dspace.authorize.AuthorizeException; -import org.dspace.content.DCDate; -import org.dspace.content.MetadataSchemaEnum; import org.dspace.core.Context; import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; import org.dspace.xmlworkflow.state.Step; @@ -36,11 +34,8 @@ public class ReviewAction extends ProcessingAction { public static final int MAIN_PAGE = 0; public static final int REJECT_PAGE = 1; - private static final String SUBMIT_APPROVE = "submit_approve"; - private static final String SUBMIT_REJECT = "submit_reject"; private static final String SUBMITTER_IS_DELETED_PAGE = "submitter_deleted"; - @Override public void activate(Context c, XmlWorkflowItem wfItem) { @@ -54,7 +49,7 @@ public class ReviewAction extends ProcessingAction { case SUBMIT_APPROVE: return processAccept(c, wfi); case SUBMIT_REJECT: - return processRejectPage(c, wfi, step, request); + return super.processRejectPage(c, wfi, request); case SUBMITTER_IS_DELETED_PAGE: return processSubmitterIsDeletedPage(c, wfi, request); default: @@ -69,50 +64,15 @@ public class ReviewAction extends ProcessingAction { List options = new ArrayList<>(); options.add(SUBMIT_APPROVE); options.add(SUBMIT_REJECT); + options.add(RETURN_TO_POOL); return options; } public ActionResult processAccept(Context c, XmlWorkflowItem wfi) throws SQLException, AuthorizeException { - //Delete the tasks - addApprovedProvenance(c, wfi); + super.addApprovedProvenance(c, wfi); return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); } - private void addApprovedProvenance(Context c, XmlWorkflowItem wfi) throws SQLException, AuthorizeException { - //Add the provenance for the accept - String now = DCDate.getCurrent().toString(); - - // Get user's name + email address - String usersName = XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() - .getEPersonName(c.getCurrentUser()); - - String provDescription = getProvenanceStartId() + " Approved for entry into archive by " - + usersName + " on " + now + " (GMT) "; - - // Add to item as a DC field - itemService.addMetadata(c, wfi.getItem(), MetadataSchemaEnum.DC.getName(), "description", "provenance", "en", - provDescription); - itemService.update(c, wfi.getItem()); - } - - public ActionResult processRejectPage(Context c, XmlWorkflowItem wfi, Step step, HttpServletRequest request) - throws SQLException, AuthorizeException, IOException { - String reason = request.getParameter("reason"); - if (reason == null || 0 == reason.trim().length()) { - request.setAttribute("page", REJECT_PAGE); - addErrorField(request, "reason"); - return new ActionResult(ActionResult.TYPE.TYPE_ERROR); - } - - //We have pressed reject, so remove the task the user has & put it back to a workspace item - XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() - .sendWorkflowItemBackSubmission(c, wfi, c.getCurrentUser(), - this.getProvenanceStartId(), reason); - - - return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); - } - public ActionResult processSubmitterIsDeletedPage(Context c, XmlWorkflowItem wfi, HttpServletRequest request) throws SQLException, AuthorizeException, IOException { if (request.getParameter("submit_delete") != null) { diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreEvaluationAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreEvaluationAction.java index a834641111..16d35b3668 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreEvaluationAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreEvaluationAction.java @@ -7,6 +7,9 @@ */ package org.dspace.xmlworkflow.state.actions.processingaction; +import static org.dspace.xmlworkflow.state.actions.processingaction.ScoreReviewAction.REVIEW_FIELD; +import static org.dspace.xmlworkflow.state.actions.processingaction.ScoreReviewAction.SCORE_FIELD; + import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; @@ -19,7 +22,6 @@ import org.dspace.content.MetadataSchemaEnum; import org.dspace.content.MetadataValue; import org.dspace.core.Context; import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; -import org.dspace.xmlworkflow.service.WorkflowRequirementsService; import org.dspace.xmlworkflow.state.Step; import org.dspace.xmlworkflow.state.actions.ActionResult; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; @@ -37,6 +39,7 @@ import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; */ public class ScoreEvaluationAction extends ProcessingAction { + // Minimum aggregate of scores private int minimumAcceptanceScore; @Override @@ -47,43 +50,64 @@ public class ScoreEvaluationAction extends ProcessingAction { @Override public ActionResult execute(Context c, XmlWorkflowItem wfi, Step step, HttpServletRequest request) throws SQLException, AuthorizeException, IOException { - boolean hasPassed = false; - //Retrieve all our scores from the metadata & add em up + // Retrieve all our scores from the metadata & add em up + int scoreMean = getMeanScore(wfi); + //We have passed if we have at least gained our minimum score + boolean hasPassed = getMinimumAcceptanceScore() <= scoreMean; + //Whether or not we have passed, clear our score information + itemService.clearMetadata(c, wfi.getItem(), SCORE_FIELD.schema, SCORE_FIELD.element, SCORE_FIELD.qualifier, + Item.ANY); + if (hasPassed) { + this.addRatingInfoToProv(c, wfi, scoreMean); + return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); + } else { + //We haven't passed, reject our item + XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() + .sendWorkflowItemBackSubmission(c, wfi, c.getCurrentUser(), this.getProvenanceStartId(), + "The item was reject due to a bad review score."); + return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); + } + } + + private int getMeanScore(XmlWorkflowItem wfi) { List scores = itemService - .getMetadata(wfi.getItem(), WorkflowRequirementsService.WORKFLOW_SCHEMA, "score", null, Item.ANY); + .getMetadata(wfi.getItem(), SCORE_FIELD.schema, SCORE_FIELD.element, SCORE_FIELD.qualifier, Item.ANY); + int scoreMean = 0; if (0 < scores.size()) { int totalScoreCount = 0; for (MetadataValue score : scores) { totalScoreCount += Integer.parseInt(score.getValue()); } - int scoreMean = totalScoreCount / scores.size(); - //We have passed if we have at least gained our minimum score - hasPassed = getMinimumAcceptanceScore() <= scoreMean; - //Wether or not we have passed, clear our score information - itemService - .clearMetadata(c, wfi.getItem(), WorkflowRequirementsService.WORKFLOW_SCHEMA, "score", null, Item.ANY); + scoreMean = totalScoreCount / scores.size(); + } + return scoreMean; + } - String provDescription = getProvenanceStartId() + " Approved for entry into archive with a score of: " + - scoreMean; - itemService.addMetadata(c, wfi.getItem(), MetadataSchemaEnum.DC.getName(), - "description", "provenance", "en", provDescription); - itemService.update(c, wfi.getItem()); + private void addRatingInfoToProv(Context c, XmlWorkflowItem wfi, int scoreMean) + throws SQLException, AuthorizeException { + StringBuilder provDescription = new StringBuilder(); + provDescription.append(String.format("%s Approved for entry into archive with a score of: %s", + getProvenanceStartId(), scoreMean)); + List reviews = itemService + .getMetadata(wfi.getItem(), REVIEW_FIELD.schema, REVIEW_FIELD.element, REVIEW_FIELD.qualifier, Item.ANY); + if (!reviews.isEmpty()) { + provDescription.append(" | Reviews: "); } - if (hasPassed) { - return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); - } else { - //We haven't passed, reject our item - XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() - .sendWorkflowItemBackSubmission(c, wfi, c.getCurrentUser(), - this.getProvenanceStartId(), - "The item was reject due to a bad review score."); - return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); + for (MetadataValue review : reviews) { + provDescription.append(String.format("; %s", review.getValue())); } + c.turnOffAuthorisationSystem(); + itemService.addMetadata(c, wfi.getItem(), MetadataSchemaEnum.DC.getName(), + "description", "provenance", "en", provDescription.toString()); + itemService.update(c, wfi.getItem()); + c.restoreAuthSystemState(); } @Override public List getOptions() { - return new ArrayList<>(); + List options = new ArrayList<>(); + options.add(RETURN_TO_POOL); + return options; } public int getMinimumAcceptanceScore() { diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewAction.java index c28fe2d93e..43a3decacc 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewAction.java @@ -9,14 +9,20 @@ package org.dspace.xmlworkflow.state.actions.processingaction; import java.sql.SQLException; import java.util.Arrays; +import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.util.Util; import org.dspace.authorize.AuthorizeException; +import org.dspace.content.MetadataFieldName; import org.dspace.core.Context; import org.dspace.xmlworkflow.service.WorkflowRequirementsService; import org.dspace.xmlworkflow.state.Step; +import org.dspace.xmlworkflow.state.actions.ActionAdvancedInfo; import org.dspace.xmlworkflow.state.actions.ActionResult; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; @@ -24,40 +30,121 @@ import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; * This action will allow multiple users to rate a certain item * if the mean of this score is higher then the minimum score the * item will be sent to the next action/step else it will be rejected - * - * @author Bram De Schouwer (bram.deschouwer at dot com) - * @author Kevin Van de Velde (kevin at atmire dot com) - * @author Ben Bosman (ben at atmire dot com) - * @author Mark Diggory (markd at atmire dot com) */ public class ScoreReviewAction extends ProcessingAction { + private static final Logger log = LogManager.getLogger(ScoreReviewAction.class); - private static final String SUBMIT_SCORE = "submit_score"; + // Option(s) + public static final String SUBMIT_SCORE = "submit_score"; + + // Response param(s) + private static final String SCORE = "score"; + private static final String REVIEW = "review"; + + // Metadata fields to save params in + public static final MetadataFieldName SCORE_FIELD = + new MetadataFieldName(WorkflowRequirementsService.WORKFLOW_SCHEMA, SCORE, null); + public static final MetadataFieldName REVIEW_FIELD = + new MetadataFieldName(WorkflowRequirementsService.WORKFLOW_SCHEMA, REVIEW, null); + + // Whether or not it is required that a text review is added to the rating + private boolean descriptionRequired; + // Maximum value rating is allowed to be + private int maxValue; @Override public void activate(Context c, XmlWorkflowItem wf) { - + // empty } @Override public ActionResult execute(Context c, XmlWorkflowItem wfi, Step step, HttpServletRequest request) - throws SQLException, AuthorizeException { - if (request.getParameter(SUBMIT_SCORE) != null) { - int score = Util.getIntParameter(request, "score"); - //Add our score to the metadata - itemService.addMetadata(c, wfi.getItem(), WorkflowRequirementsService.WORKFLOW_SCHEMA, "score", null, null, - String.valueOf(score)); - itemService.update(c, wfi.getItem()); - - return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); - } else { - //We have pressed the leave button so return to our submission page - return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); + throws SQLException, AuthorizeException { + if (super.isOptionInParam(request) && + StringUtils.equalsIgnoreCase(Util.getSubmitButton(request, SUBMIT_CANCEL), SUBMIT_SCORE)) { + return processSetRating(c, wfi, request); } + return new ActionResult(ActionResult.TYPE.TYPE_CANCEL); + } + + private ActionResult processSetRating(Context c, XmlWorkflowItem wfi, HttpServletRequest request) + throws SQLException, AuthorizeException { + + int score = Util.getIntParameter(request, SCORE); + String review = request.getParameter(REVIEW); + if (!this.checkRequestValid(score, review)) { + return new ActionResult(ActionResult.TYPE.TYPE_ERROR); + } + //Add our rating and review to the metadata + itemService.addMetadata(c, wfi.getItem(), SCORE_FIELD.schema, SCORE_FIELD.element, SCORE_FIELD.qualifier, null, + String.valueOf(score)); + if (StringUtils.isNotBlank(review)) { + itemService.addMetadata(c, wfi.getItem(), REVIEW_FIELD.schema, REVIEW_FIELD.element, + REVIEW_FIELD.qualifier, null, String.format("%s - %s", score, review)); + } + itemService.update(c, wfi.getItem()); + + return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); + } + + /** + * Request is not valid if: + * - Given score is higher than configured maxValue + * - There is no review given and description is configured to be required + * Config in workflow-actions.xml + * + * @param score Given score rating from request + * @param review Given review/description from request + * @return True if valid request params with config, otherwise false + */ + private boolean checkRequestValid(int score, String review) { + if (score > this.maxValue) { + log.error("{} only allows max rating {} (config workflow-actions.xml), given rating of " + + "{} not allowed.", this.getClass().toString(), this.maxValue, score); + return false; + } + if (StringUtils.isBlank(review) && this.descriptionRequired) { + log.error("{} has config descriptionRequired=true (workflow-actions.xml), so rating " + + "requests without 'review' query param containing description are not allowed", + this.getClass().toString()); + return false; + } + return true; } @Override public List getOptions() { + return List.of(SUBMIT_SCORE, RETURN_TO_POOL); + } + + @Override + protected List getAdvancedOptions() { return Arrays.asList(SUBMIT_SCORE); } + + @Override + protected List getAdvancedInfo() { + ScoreReviewActionAdvancedInfo scoreReviewActionAdvancedInfo = new ScoreReviewActionAdvancedInfo(); + scoreReviewActionAdvancedInfo.setDescriptionRequired(descriptionRequired); + scoreReviewActionAdvancedInfo.setMaxValue(maxValue); + scoreReviewActionAdvancedInfo.setType(SUBMIT_SCORE); + scoreReviewActionAdvancedInfo.generateId(SUBMIT_SCORE); + return Collections.singletonList(scoreReviewActionAdvancedInfo); + } + + /** + * Setter that sets the descriptionRequired property from workflow-actions.xml + * @param descriptionRequired boolean whether a description is required + */ + public void setDescriptionRequired(boolean descriptionRequired) { + this.descriptionRequired = descriptionRequired; + } + + /** + * Setter that sets the maxValue property from workflow-actions.xml + * @param maxValue integer of the maximum allowed value + */ + public void setMaxValue(int maxValue) { + this.maxValue = maxValue; + } } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewActionAdvancedInfo.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewActionAdvancedInfo.java new file mode 100644 index 0000000000..5b97fe3195 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/ScoreReviewActionAdvancedInfo.java @@ -0,0 +1,45 @@ +/** + * 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.xmlworkflow.state.actions.processingaction; + +import org.dspace.xmlworkflow.state.actions.ActionAdvancedInfo; +import org.springframework.util.DigestUtils; + +/** + * Class that holds the advanced information needed for the + * {@link org.dspace.xmlworkflow.state.actions.processingaction.ScoreReviewAction} + * See config {@code workflow-actions.cfg} + */ +public class ScoreReviewActionAdvancedInfo extends ActionAdvancedInfo { + private boolean descriptionRequired; + private int maxValue; + + public boolean isDescriptionRequired() { + return descriptionRequired; + } + + public void setDescriptionRequired(boolean descriptionRequired) { + this.descriptionRequired = descriptionRequired; + } + + public int getMaxValue() { + return maxValue; + } + + public void setMaxValue(int maxValue) { + this.maxValue = maxValue; + } + + @Override + public void generateId(String type) { + String idString = type + + ";descriptionRequired," + descriptionRequired + + ";maxValue," + maxValue; + super.id = DigestUtils.md5DigestAsHex(idString.getBytes()); + } +} diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SelectReviewerAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SelectReviewerAction.java index 28f21cc418..0e8ab40a52 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SelectReviewerAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SelectReviewerAction.java @@ -9,17 +9,27 @@ package org.dspace.xmlworkflow.state.actions.processingaction; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.UUID; +import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.util.Util; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; import org.dspace.eperson.service.EPersonService; +import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; import org.dspace.xmlworkflow.Role; import org.dspace.xmlworkflow.state.Step; +import org.dspace.xmlworkflow.state.actions.ActionAdvancedInfo; import org.dspace.xmlworkflow.state.actions.ActionResult; import org.dspace.xmlworkflow.storedcomponents.WorkflowItemRole; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; @@ -37,13 +47,13 @@ import org.springframework.beans.factory.annotation.Autowired; */ public class SelectReviewerAction extends ProcessingAction { - public static final int SEARCH_RESULTS_PAGE = 1; - - public static final int RESULTS_PER_PAGE = 5; + private static final Logger log = LogManager.getLogger(SelectReviewerAction.class); private static final String SUBMIT_CANCEL = "submit_cancel"; - private static final String SUBMIT_SEARCH = "submit_search"; - private static final String SUBMIT_SELECT_REVIEWER = "submit_select_reviewer_"; + private static final String SUBMIT_SELECT_REVIEWER = "submit_select_reviewer"; + private static final String PARAM_REVIEWER = "eperson"; + + private static final String CONFIG_REVIEWER_GROUP = "action.selectrevieweraction.group"; private Role role; @@ -53,6 +63,15 @@ public class SelectReviewerAction extends ProcessingAction { @Autowired(required = true) private WorkflowItemRoleService workflowItemRoleService; + @Autowired + private ConfigurationService configurationService; + + @Autowired + private GroupService groupService; + + private static Group selectFromReviewsGroup; + private static boolean selectFromReviewsGroupInitialised = false; + @Override public void activate(Context c, XmlWorkflowItem wf) { @@ -60,56 +79,128 @@ public class SelectReviewerAction extends ProcessingAction { @Override public ActionResult execute(Context c, XmlWorkflowItem wfi, Step step, HttpServletRequest request) - throws SQLException, AuthorizeException { + throws SQLException, AuthorizeException { String submitButton = Util.getSubmitButton(request, SUBMIT_CANCEL); //Check if our user has pressed cancel if (submitButton.equals(SUBMIT_CANCEL)) { //Send us back to the submissions page return new ActionResult(ActionResult.TYPE.TYPE_CANCEL); - - } else if (submitButton.equals(SUBMIT_SEARCH)) { - //Perform the search - String query = request.getParameter("query"); - int page = Util.getIntParameter(request, "result-page"); - if (page == -1) { - page = 0; - } - - int resultCount = ePersonService.searchResultCount(c, query); - List epeople = ePersonService.search(c, query, page * RESULTS_PER_PAGE, RESULTS_PER_PAGE); - - - request.setAttribute("eperson-result-count", resultCount); - request.setAttribute("eperson-results", epeople); - request.setAttribute("result-page", page); - request.setAttribute("page", SEARCH_RESULTS_PAGE); - return new ActionResult(ActionResult.TYPE.TYPE_PAGE, SEARCH_RESULTS_PAGE); } else if (submitButton.startsWith(SUBMIT_SELECT_REVIEWER)) { - //Retrieve the identifier of the eperson which will do the reviewing - UUID reviewerId = UUID.fromString(submitButton.substring(submitButton.lastIndexOf("_") + 1)); - EPerson reviewer = ePersonService.find(c, reviewerId); - //Assign the reviewer. The workflowitemrole will be translated into a task in the autoassign - WorkflowItemRole workflowItemRole = workflowItemRoleService.create(c); - workflowItemRole.setEPerson(reviewer); - workflowItemRole.setRoleId(getRole().getId()); - workflowItemRole.setWorkflowItem(wfi); - workflowItemRoleService.update(c, workflowItemRole); - return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); + return processSelectReviewers(c, wfi, request); } //There are only 2 active buttons on this page, so if anything else happens just return an error return new ActionResult(ActionResult.TYPE.TYPE_ERROR); } + /** + * Method to handle the {@link this#SUBMIT_SELECT_REVIEWER} action: + * - will retrieve the reviewer(s) uuid from request (param {@link this#PARAM_REVIEWER}) + * - assign them to a {@link WorkflowItemRole} + * - In {@link org.dspace.xmlworkflow.state.actions.userassignment.AutoAssignAction} these reviewer(s) will get + * claimed task for this {@link XmlWorkflowItem} + * Will result in error if: + * - No reviewer(s) uuid in request (param {@link this#PARAM_REVIEWER}) + * - If none of the reviewer(s) uuid passed along result in valid EPerson + * - If the reviewer(s) passed along are not in {@link this#selectFromReviewsGroup} when it is set + * + * @param c current DSpace session + * @param wfi the item on which the action is to be performed + * @param request the current client request + * @return the result of performing the action + */ + private ActionResult processSelectReviewers(Context c, XmlWorkflowItem wfi, HttpServletRequest request) + throws SQLException, AuthorizeException { + //Retrieve the identifier of the eperson which will do the reviewing + String[] reviewerIds = request.getParameterValues(PARAM_REVIEWER); + if (ArrayUtils.isEmpty(reviewerIds)) { + return new ActionResult(ActionResult.TYPE.TYPE_ERROR); + } + List reviewers = new ArrayList<>(); + for (String reviewerId : reviewerIds) { + EPerson reviewer = ePersonService.find(c, UUID.fromString(reviewerId)); + if (reviewer == null) { + log.warn("No EPerson found with uuid {}", reviewerId); + } else { + reviewers.add(reviewer); + } + } + + if (!this.checkReviewersValid(c, reviewers)) { + return new ActionResult(ActionResult.TYPE.TYPE_ERROR); + } + + createWorkflowItemRole(c, wfi, reviewers); + return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); + } + + private boolean checkReviewersValid(Context c, List reviewers) throws SQLException { + if (reviewers.size() == 0) { + return false; + } + Group group = this.getGroup(c); + if (group != null) { + for (EPerson reviewer: reviewers) { + if (!groupService.isMember(c, reviewer, group)) { + log.error("Reviewers selected must be member of group {}", group.getID()); + return false; + } + } + } + return true; + } + + private WorkflowItemRole createWorkflowItemRole(Context c, XmlWorkflowItem wfi, List reviewers) + throws SQLException, AuthorizeException { + WorkflowItemRole workflowItemRole = workflowItemRoleService.create(c); + workflowItemRole.setRoleId(getRole().getId()); + workflowItemRole.setWorkflowItem(wfi); + if (reviewers.size() == 1) { + // 1 reviewer in workflowitemrole => will be translated into a claimed task in the autoassign + workflowItemRole.setEPerson(reviewers.get(0)); + } else { + // multiple reviewers, create a temporary group and assign this group, the workflowitemrole will be + // translated into a claimed task for reviewers in the autoassign, where group will be deleted + c.turnOffAuthorisationSystem(); + Group selectedReviewsGroup = groupService.create(c); + groupService.setName(selectedReviewsGroup, "selectedReviewsGroup_" + wfi.getID()); + for (EPerson reviewer : reviewers) { + groupService.addMember(c, selectedReviewsGroup, reviewer); + } + workflowItemRole.setGroup(selectedReviewsGroup); + c.restoreAuthSystemState(); + } + workflowItemRoleService.update(c, workflowItemRole); + return workflowItemRole; + } + @Override public List getOptions() { List options = new ArrayList<>(); - options.add(SUBMIT_SEARCH); options.add(SUBMIT_SELECT_REVIEWER); + options.add(RETURN_TO_POOL); return options; } + @Override + protected List getAdvancedOptions() { + return Arrays.asList(SUBMIT_SELECT_REVIEWER); + } + + @Override + protected List getAdvancedInfo() { + List advancedInfo = new ArrayList<>(); + SelectReviewerActionAdvancedInfo selectReviewerActionAdvancedInfo = new SelectReviewerActionAdvancedInfo(); + if (getGroup(null) != null) { + selectReviewerActionAdvancedInfo.setGroup(getGroup(null).getID().toString()); + } + selectReviewerActionAdvancedInfo.setType(SUBMIT_SELECT_REVIEWER); + selectReviewerActionAdvancedInfo.generateId(SUBMIT_SELECT_REVIEWER); + advancedInfo.add(selectReviewerActionAdvancedInfo); + return advancedInfo; + } + public Role getRole() { return role; } @@ -118,4 +209,49 @@ public class SelectReviewerAction extends ProcessingAction { public void setRole(Role role) { this.role = role; } + + /** + * Get the Reviewer group from the "action.selectrevieweraction.group" property in actions.cfg by its UUID or name + * Returns null if no (valid) group configured + * + * @return configured reviewers Group from property or null if none + */ + private Group getGroup(@Nullable Context context) { + if (selectFromReviewsGroupInitialised) { + return this.selectFromReviewsGroup; + } + if (context == null) { + context = new Context(); + } + String groupIdOrName = configurationService.getProperty(CONFIG_REVIEWER_GROUP); + + if (StringUtils.isNotBlank(groupIdOrName)) { + Group group = null; + try { + // try to get group by name + group = groupService.findByName(context, groupIdOrName); + if (group == null) { + // try to get group by uuid if not a name + group = groupService.find(context, UUID.fromString(groupIdOrName)); + } + } catch (Exception e) { + // There is an issue with the reviewer group that is set; if it is not set then can be chosen + // from all epeople + log.error("Issue with determining matching group for config {}={} for reviewer group of " + + "select reviewers workflow", CONFIG_REVIEWER_GROUP, groupIdOrName); + } + + this.selectFromReviewsGroup = group; + } + selectFromReviewsGroupInitialised = true; + return this.selectFromReviewsGroup; + } + + /** + * To be used by IT, e.g. {@code XmlWorkflowServiceIT}, when defining new 'Reviewers' group + */ + static public void resetGroup() { + selectFromReviewsGroup = null; + selectFromReviewsGroupInitialised = false; + } } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SelectReviewerActionAdvancedInfo.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SelectReviewerActionAdvancedInfo.java new file mode 100644 index 0000000000..7a86a0b03d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SelectReviewerActionAdvancedInfo.java @@ -0,0 +1,36 @@ +/** + * 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.xmlworkflow.state.actions.processingaction; + +import org.dspace.xmlworkflow.state.actions.ActionAdvancedInfo; +import org.springframework.util.DigestUtils; + +/** + * Class that holds the advanced information needed for the + * {@link org.dspace.xmlworkflow.state.actions.processingaction.SelectReviewerAction} + * See config {@code workflow-actions.cfg} + */ +public class SelectReviewerActionAdvancedInfo extends ActionAdvancedInfo { + private String group; + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + @Override + public void generateId(String type) { + String idString = type + + ";group," + group; + super.id = DigestUtils.md5DigestAsHex(idString.getBytes()); + } +} + diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SingleUserReviewAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SingleUserReviewAction.java index 27cf98f77f..b3fe896ace 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SingleUserReviewAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/processingaction/SingleUserReviewAction.java @@ -13,11 +13,15 @@ import java.util.ArrayList; import java.util.List; import javax.servlet.http.HttpServletRequest; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.util.Util; import org.dspace.authorize.AuthorizeException; -import org.dspace.content.DCDate; -import org.dspace.content.MetadataSchemaEnum; +import org.dspace.content.WorkspaceItem; +import org.dspace.content.factory.ContentServiceFactory; import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.workflow.WorkflowException; import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; import org.dspace.xmlworkflow.state.Step; import org.dspace.xmlworkflow.state.actions.ActionResult; @@ -34,39 +38,59 @@ import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; * @author Mark Diggory (markd at atmire dot com) */ public class SingleUserReviewAction extends ProcessingAction { - - public static final int MAIN_PAGE = 0; - public static final int REJECT_PAGE = 1; - public static final int SUBMITTER_IS_DELETED_PAGE = 2; + private static final Logger log = LogManager.getLogger(SingleUserReviewAction.class); public static final int OUTCOME_REJECT = 1; - protected static final String SUBMIT_APPROVE = "submit_approve"; - protected static final String SUBMIT_REJECT = "submit_reject"; protected static final String SUBMIT_DECLINE_TASK = "submit_decline_task"; @Override public void activate(Context c, XmlWorkflowItem wfItem) { - + // empty } @Override public ActionResult execute(Context c, XmlWorkflowItem wfi, Step step, HttpServletRequest request) - throws SQLException, AuthorizeException, IOException { - int page = Util.getIntParameter(request, "page"); - - switch (page) { - case MAIN_PAGE: - return processMainPage(c, wfi, step, request); - case REJECT_PAGE: - return processRejectPage(c, wfi, step, request); - case SUBMITTER_IS_DELETED_PAGE: - return processSubmitterIsDeletedPage(c, wfi, request); + throws SQLException, AuthorizeException, IOException, WorkflowException { + if (!super.isOptionInParam(request)) { + return new ActionResult(ActionResult.TYPE.TYPE_CANCEL); + } + switch (Util.getSubmitButton(request, SUBMIT_CANCEL)) { + case SUBMIT_APPROVE: + return processAccept(c, wfi); + case SUBMIT_REJECT: + return processReject(c, wfi, request); + case SUBMIT_DECLINE_TASK: + return processDecline(c, wfi); default: return new ActionResult(ActionResult.TYPE.TYPE_CANCEL); } } + /** + * Process {@link super#SUBMIT_REJECT} on this action, will either: + * - If submitter of item no longer exists => Permanently delete corresponding item (no wfi/wsi remaining) + * - Otherwise: reject item back to submission => becomes wsi of submitter again + */ + private ActionResult processReject(Context c, XmlWorkflowItem wfi, HttpServletRequest request) + throws SQLException, IOException, AuthorizeException { + if (wfi.getSubmitter() == null) { + // If the original submitter is no longer there, delete the task + return processDelete(c, wfi); + } else { + return super.processRejectPage(c, wfi, request); + } + } + + /** + * Accept the workflow item => last step in workflow so will be archived + * Info on step & reviewer will be added on metadata dc.description.provenance of resulting item + */ + public ActionResult processAccept(Context c, XmlWorkflowItem wfi) throws SQLException, AuthorizeException { + super.addApprovedProvenance(c, wfi); + return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); + } + @Override public List getOptions() { List options = new ArrayList<>(); @@ -76,87 +100,29 @@ public class SingleUserReviewAction extends ProcessingAction { return options; } - public ActionResult processMainPage(Context c, XmlWorkflowItem wfi, Step step, HttpServletRequest request) - throws SQLException, AuthorizeException { - if (request.getParameter(SUBMIT_APPROVE) != null) { - //Delete the tasks - addApprovedProvenance(c, wfi); - - return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, ActionResult.OUTCOME_COMPLETE); - } else if (request.getParameter(SUBMIT_REJECT) != null) { - // Make sure we indicate which page we want to process - if (wfi.getSubmitter() == null) { - request.setAttribute("page", SUBMITTER_IS_DELETED_PAGE); - } else { - request.setAttribute("page", REJECT_PAGE); - } - // We have pressed reject item, so take the user to a page where they can reject - return new ActionResult(ActionResult.TYPE.TYPE_PAGE); - } else if (request.getParameter(SUBMIT_DECLINE_TASK) != null) { - return new ActionResult(ActionResult.TYPE.TYPE_OUTCOME, OUTCOME_REJECT); - - } else { - //We pressed the leave button so return to our submissions page - return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); - } - } - - private void addApprovedProvenance(Context c, XmlWorkflowItem wfi) throws SQLException, AuthorizeException { - //Add the provenance for the accept - String now = DCDate.getCurrent().toString(); - - // Get user's name + email address - String usersName = XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() - .getEPersonName(c.getCurrentUser()); - - String provDescription = getProvenanceStartId() + " Approved for entry into archive by " - + usersName + " on " + now + " (GMT) "; - - // Add to item as a DC field - itemService.addMetadata(c, wfi.getItem(), MetadataSchemaEnum.DC.getName(), "description", "provenance", "en", - provDescription); - itemService.update(c, wfi.getItem()); - } - - public ActionResult processRejectPage(Context c, XmlWorkflowItem wfi, Step step, HttpServletRequest request) + /** + * Since original submitter no longer exists, workflow item is permanently deleted + */ + private ActionResult processDelete(Context c, XmlWorkflowItem wfi) throws SQLException, AuthorizeException, IOException { - if (request.getParameter("submit_reject") != null) { - String reason = request.getParameter("reason"); - if (reason == null || 0 == reason.trim().length()) { - request.setAttribute("page", REJECT_PAGE); - addErrorField(request, "reason"); - return new ActionResult(ActionResult.TYPE.TYPE_ERROR); - } - - //We have pressed reject, so remove the task the user has & put it back to a workspace item - XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() - .sendWorkflowItemBackSubmission(c, wfi, c.getCurrentUser(), - this.getProvenanceStartId(), reason); - - - return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); - } else { - //Cancel, go back to the main task page - request.setAttribute("page", MAIN_PAGE); - - return new ActionResult(ActionResult.TYPE.TYPE_PAGE); - } + EPerson user = c.getCurrentUser(); + c.turnOffAuthorisationSystem(); + WorkspaceItem workspaceItem = XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() + .abort(c, wfi, user); + ContentServiceFactory.getInstance().getWorkspaceItemService().deleteAll(c, workspaceItem); + c.restoreAuthSystemState(); + return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); } - public ActionResult processSubmitterIsDeletedPage(Context c, XmlWorkflowItem wfi, HttpServletRequest request) - throws SQLException, AuthorizeException, IOException { - if (request.getParameter("submit_delete") != null) { - XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() - .deleteWorkflowByWorkflowItem(c, wfi, c.getCurrentUser()); - // Delete and send user back to myDspace page - return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); - } else if (request.getParameter("submit_keep_it") != null) { - // Do nothing, just send it back to myDspace page - return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); - } else { - //Cancel, go back to the main task page - request.setAttribute("page", MAIN_PAGE); - return new ActionResult(ActionResult.TYPE.TYPE_PAGE); - } + /** + * Selected reviewer declines to review task, then the workflow is aborted and restarted + */ + private ActionResult processDecline(Context c, XmlWorkflowItem wfi) + throws SQLException, IOException, AuthorizeException, WorkflowException { + c.turnOffAuthorisationSystem(); + xmlWorkflowService.restartWorkflow(c, wfi, c.getCurrentUser(), this.getProvenanceStartId()); + c.restoreAuthSystemState(); + return new ActionResult(ActionResult.TYPE.TYPE_SUBMISSION_PAGE); } + } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/userassignment/AutoAssignAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/userassignment/AutoAssignAction.java index 51f4bf0a93..401a7c506b 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/userassignment/AutoAssignAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/userassignment/AutoAssignAction.java @@ -80,6 +80,10 @@ public class AutoAssignAction extends UserSelectionAction { } //Delete our workflow item role since the users have been assigned workflowItemRoleService.delete(c, workflowItemRole); + if (role.isDeleteTemporaryGroup() && workflowItemRole.getGroup() != null) { + // Delete temporary groups created after members have workflow task assigned + groupService.delete(c, workflowItemRole.getGroup()); + } } } else { log.warn(LogHelper.getHeader(c, "Error while executing auto assign action", @@ -127,7 +131,7 @@ public class AutoAssignAction extends UserSelectionAction { protected void createTaskForEPerson(Context c, XmlWorkflowItem wfi, Step step, WorkflowActionConfig actionConfig, EPerson user) throws SQLException, AuthorizeException, IOException { if (claimedTaskService.find(c, wfi, step.getId(), actionConfig.getId()) != null) { - workflowRequirementsService.addClaimedUser(c, wfi, step, c.getCurrentUser()); + workflowRequirementsService.addClaimedUser(c, wfi, step, user); XmlWorkflowServiceFactory.getInstance().getXmlWorkflowService() .createOwnedTask(c, wfi, step, actionConfig, user); } diff --git a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/userassignment/ClaimAction.java b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/userassignment/ClaimAction.java index c9c61908aa..21fcf6f309 100644 --- a/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/userassignment/ClaimAction.java +++ b/dspace-api/src/main/java/org/dspace/xmlworkflow/state/actions/userassignment/ClaimAction.java @@ -138,6 +138,10 @@ public class ClaimAction extends UserSelectionAction { RoleMembers roleMembers = role.getMembers(context, wfi); ArrayList epersons = roleMembers.getAllUniqueMembers(context); + if (epersons.isEmpty() || step.getRequiredUsers() > epersons.size()) { + log.warn(String.format("There must be at least %s ePerson(s) in the group", + step.getRequiredUsers())); + } return !(epersons.isEmpty() || step.getRequiredUsers() > epersons.size()); } else { // We don't have a role and do have a UI so throw a workflow exception diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.5_2022.12.01__add_table_subscriptionparamter_change_columns_subscription_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.5_2022.12.01__add_table_subscriptionparamter_change_columns_subscription_table.sql new file mode 100644 index 0000000000..dc187d3c27 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.5_2022.12.01__add_table_subscriptionparamter_change_columns_subscription_table.sql @@ -0,0 +1,44 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +----------------------------------------------------------------------------------- +-- ADD table subscription_parameter +----------------------------------------------------------------------------------- + + +CREATE SEQUENCE if NOT EXISTS subscription_parameter_seq; +------------------------------------------------------- +-- Create the subscription_parameter table +------------------------------------------------------- + +CREATE TABLE if NOT EXISTS subscription_parameter +( + subscription_parameter_id INTEGER NOT NULL, + name CHARACTER VARYING(255), + value CHARACTER VARYING(255), + subscription_id INTEGER NOT NULL, + CONSTRAINT subscription_parameter_pkey PRIMARY KEY (subscription_parameter_id), + CONSTRAINT subscription_parameter_subscription_fkey FOREIGN KEY (subscription_id) REFERENCES subscription (subscription_id) ON DELETE CASCADE +); + +-- +ALTER TABLE subscription ADD COLUMN if NOT EXISTS dspace_object_id UUID; +-- +ALTER TABLE subscription ADD COLUMN if NOT EXISTS type CHARACTER VARYING(255); +-- +ALTER TABLE subscription DROP CONSTRAINT IF EXISTS subscription_dspaceobject_fkey; +ALTER TABLE subscription ADD CONSTRAINT subscription_dspaceobject_fkey FOREIGN KEY (dspace_object_id) REFERENCES dspaceobject (uuid); +-- -- +UPDATE subscription set dspace_object_id = collection_id , type = 'content'; +-- +ALTER TABLE subscription DROP CONSTRAINT IF EXISTS Subscription_collection_id_fk; +-- +ALTER TABLE subscription DROP COLUMN IF EXISTS collection_id; + + + diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.5_2022.12.15__system_wide_alerts.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.5_2022.12.15__system_wide_alerts.sql new file mode 100644 index 0000000000..9d13138fda --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V7.5_2022.12.15__system_wide_alerts.sql @@ -0,0 +1,22 @@ +-- +-- 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/ +-- + +----------------------------------------------------------------------------------- +-- Create table for System wide alerts +----------------------------------------------------------------------------------- + +CREATE SEQUENCE alert_id_seq; + +CREATE TABLE systemwidealert +( + alert_id INTEGER NOT NULL PRIMARY KEY, + message VARCHAR(512), + allow_sessions VARCHAR(64), + countdown_to TIMESTAMP, + active BOOLEAN +); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.5_2022.12.01__add_table_subscriptionparamter_change_columns_subscription_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.5_2022.12.01__add_table_subscriptionparamter_change_columns_subscription_table.sql new file mode 100644 index 0000000000..3862830230 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.5_2022.12.01__add_table_subscriptionparamter_change_columns_subscription_table.sql @@ -0,0 +1,45 @@ +-- +-- 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/ +-- + +----------------------------------------------------------------------------------- +-- ADD table subscription_parameter +----------------------------------------------------------------------------------- + + +CREATE SEQUENCE if NOT EXISTS subscription_parameter_seq; +----------------------------------------------------------------------------------- +-- ADD table subscription_parameter +----------------------------------------------------------------------------------- +CREATE TABLE if NOT EXISTS subscription_parameter +( + subscription_parameter_id INTEGER NOT NULL, + name VARCHAR(255), + value VARCHAR(255), + subscription_id INTEGER NOT NULL, + CONSTRAINT subscription_parameter_pkey PRIMARY KEY (subscription_parameter_id), + CONSTRAINT subscription_parameter_subscription_fkey FOREIGN KEY (subscription_id) + REFERENCES subscription (subscription_id) ON DELETE CASCADE +); +-- -- + +ALTER TABLE subscription ADD COLUMN if NOT EXISTS dspace_object_id UUID; +---- -- +ALTER TABLE subscription ADD COLUMN if NOT EXISTS type CHARACTER VARYING(255); +-- +UPDATE subscription SET dspace_object_id = collection_id , type = 'content'; +-- +ALTER TABLE subscription DROP CONSTRAINT IF EXISTS subscription_dspaceobject_fkey; +ALTER TABLE subscription ADD CONSTRAINT subscription_dspaceobject_fkey FOREIGN KEY (dspace_object_id) REFERENCES dspaceobject (uuid); +-- +ALTER TABLE subscription DROP CONSTRAINT IF EXISTS subscription_collection_id_fkey; +---- -- +ALTER TABLE subscription DROP COLUMN IF EXISTS collection_id; +-- -- +INSERT INTO subscription_parameter (subscription_parameter_id, name, value, subscription_id) +SELECT getnextid('subscription_parameter'), 'frequency', 'D', subscription_id from "subscription" ; + diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.5_2022.12.15__system_wide_alerts.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.5_2022.12.15__system_wide_alerts.sql new file mode 100644 index 0000000000..9d13138fda --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/oracle/V7.5_2022.12.15__system_wide_alerts.sql @@ -0,0 +1,22 @@ +-- +-- 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/ +-- + +----------------------------------------------------------------------------------- +-- Create table for System wide alerts +----------------------------------------------------------------------------------- + +CREATE SEQUENCE alert_id_seq; + +CREATE TABLE systemwidealert +( + alert_id INTEGER NOT NULL PRIMARY KEY, + message VARCHAR(512), + allow_sessions VARCHAR(64), + countdown_to TIMESTAMP, + active BOOLEAN +); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.5_2022.12.01__add_table_subscriptionparamter_change_columns_subscription_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.5_2022.12.01__add_table_subscriptionparamter_change_columns_subscription_table.sql new file mode 100644 index 0000000000..61e01494fc --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.5_2022.12.01__add_table_subscriptionparamter_change_columns_subscription_table.sql @@ -0,0 +1,43 @@ +-- +-- 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/ +-- + +----------------------------------------------------------------------------------- +-- ADD table subscription_parameter +----------------------------------------------------------------------------------- + + +CREATE SEQUENCE if NOT EXISTS subscription_parameter_seq; +----------------------------------------------------------------------------------- +-- ADD table subscription_parameter +----------------------------------------------------------------------------------- +CREATE TABLE if NOT EXISTS subscription_parameter +( + subscription_parameter_id INTEGER NOT NULL, + name CHARACTER VARYING(255), + value CHARACTER VARYING(255), + subscription_id INTEGER NOT NULL, + CONSTRAINT subscription_parameter_pkey PRIMARY KEY (subscription_parameter_id), + CONSTRAINT subscription_parameter_subscription_fkey FOREIGN KEY (subscription_id) REFERENCES subscription (subscription_id) ON DELETE CASCADE +); +-- +ALTER TABLE subscription ADD COLUMN if NOT EXISTS dspace_object_id UUID; +-- -- +ALTER TABLE subscription ADD COLUMN if NOT EXISTS type CHARACTER VARYING(255); +---- -- +ALTER TABLE subscription DROP CONSTRAINT IF EXISTS subscription_dspaceobject_fkey; +ALTER TABLE subscription ADD CONSTRAINT subscription_dspaceobject_fkey FOREIGN KEY (dspace_object_id) REFERENCES dspaceobject (uuid); +-- +UPDATE subscription SET dspace_object_id = collection_id , type = 'content'; +-- +ALTER TABLE subscription DROP CONSTRAINT IF EXISTS subscription_collection_id_fkey; +-- -- +ALTER TABLE subscription DROP COLUMN IF EXISTS collection_id; +-- -- +INSERT INTO subscription_parameter (subscription_parameter_id, name, value, subscription_id) +SELECT getnextid('subscription_parameter'), 'frequency', 'D', subscription_id from "subscription" ; + diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.5_2022.12.15__system_wide_alerts.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.5_2022.12.15__system_wide_alerts.sql new file mode 100644 index 0000000000..9d13138fda --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V7.5_2022.12.15__system_wide_alerts.sql @@ -0,0 +1,22 @@ +-- +-- 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/ +-- + +----------------------------------------------------------------------------------- +-- Create table for System wide alerts +----------------------------------------------------------------------------------- + +CREATE SEQUENCE alert_id_seq; + +CREATE TABLE systemwidealert +( + alert_id INTEGER NOT NULL PRIMARY KEY, + message VARCHAR(512), + allow_sessions VARCHAR(64), + countdown_to TIMESTAMP, + active BOOLEAN +); diff --git a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index f40298db30..6d8ae0c2f0 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -143,6 +143,12 @@ org.dspace.app.rest.submit.step.SherpaPolicyStep sherpaPolicy + + + submit.progressbar.identifiers + org.dspace.app.rest.submit.step.ShowIdentifiersStep + identifiers + @@ -169,6 +175,8 @@ + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 6305b25d56..c009acb30e 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -43,7 +43,7 @@ dspace.server.url = http://localhost db.driver = org.h2.Driver db.dialect=org.hibernate.dialect.H2Dialect # Use a 10 second database lock timeout to avoid occasional JDBC lock timeout errors -db.url = jdbc:h2:mem:test;LOCK_TIMEOUT=10000; +db.url = jdbc:h2:mem:test;LOCK_TIMEOUT=10000;NON_KEYWORDS=VALUE db.username = sa db.password = # H2's default schema is PUBLIC diff --git a/dspace-api/src/test/data/dspaceFolder/config/modules/identifiers.cfg b/dspace-api/src/test/data/dspaceFolder/config/modules/identifiers.cfg new file mode 100644 index 0000000000..64512572ff --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/modules/identifiers.cfg @@ -0,0 +1,49 @@ +#----------------------------------------------------------------------# +#---------------------IDENTIFIER CONFIGURATIONS------------------------# +#----------------------------------------------------------------------# +# These configs are used for additional identifier configuration such # +# as the Show Identifiers step which can "pre-mint" DOIs and Handles # +#----------------------------------------------------------------------# + +# Should configured identifiers (eg handle and DOI) be minted for (future) registration at workspace item creation? +# A handle created at this stage will act just like a regular handle created at archive time. +# A DOI created at this stage will be in a 'PENDING' status while in workspace and workflow. +# At the time of item install, the DOI filter (if any) will be applied and if the item matches the filter, the DOI +# status will be updated to TO_BE_REGISTERED. An administrator can also manually progress the DOI status, overriding +# any filters, in the item status page. +# This option doesn't require the Show Identifiers submission step to be visible. +# Default: false +identifiers.submission.register = false + +# This configuration property can be set to a filter name to determine if a PENDING DOI for an item +# should be queued for registration. If the filter doesn't match, the DOI will stay in PENDING or MINTED status +# so that the identifier itself persists in case it is considered for registration in the future. +# See doi-filter and other example filters in item-filters.xml. +# Default (always_true_filter) +identifiers.submission.filter.install = doi_filter + +# This optional configuration property can be set to a filter name, in case there are some initial rules to apply +# when first deciding whether a DOI should be be created for a new workspace item with a PENDING status. +# This filter is only applied if identifiers.submission.register is true. +# This filter is updated as submission data is saved. +# Default: (always_true_filter) +identifiers.submission.filter.workspace = doi_filter + +# If true, the workspace filter will be applied as submission data is saved. If the filter no longer +# matches the item, the DOI will be shifted into a MINTED status and not displayed in the submission section. +# If false, then once a DOI has been created with PENDING status it will remain that way until final item install +# Default: true +#identifiers.submission.strip_pending_during_submission = true + +# This configuration property can be set to a filter name to determine if an item processed by RegisterDOI curation +# task should be eligible for a DOI +identifiers.submission.filter.curation = always_true_filter + +# Show Register DOI button in item status page? +# Default: false +identifiers.item-status.register-doi = true + +# Which identifier types to show in submission step? +# Default: handle, doi (currently the only supported identifier 'types') +identifiers.submission.display = handle +identifiers.submission.display = doi \ No newline at end of file diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/item-filters.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/item-filters.xml new file mode 100644 index 0000000000..836d4f0896 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/item-filters.xml @@ -0,0 +1,370 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + article$ + bachelorThesis$ + masterThesis$ + doctoralThesis$ + book$ + bookPart$ + review$ + conferenceObject$ + lecture$ + workingPaper$ + preprint$ + report$ + annotation$ + contributionToPeriodical$ + patent$ + dataset$ + other$ + + + + + + + + + + + + + 123456789/20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 123456789/3 + 123456789/4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml index 318d1ad3d7..0d07436227 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow-actions.xml @@ -15,9 +15,12 @@ - + + + + - + @@ -63,6 +66,12 @@ + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow.xml index 6e987ae8b0..a83be3fa33 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/workflow.xml @@ -153,6 +153,7 @@ + diff --git a/dspace-api/src/test/java/org/dspace/alerts/SystemWideAlertServiceTest.java b/dspace-api/src/test/java/org/dspace/alerts/SystemWideAlertServiceTest.java new file mode 100644 index 0000000000..5d8d6ac594 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/alerts/SystemWideAlertServiceTest.java @@ -0,0 +1,202 @@ +/** + * 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.alerts; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.dspace.alerts.dao.SystemWideAlertDAO; +import org.dspace.alerts.service.SystemWideAlertService; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SystemWideAlertServiceTest { + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(SystemWideAlertService.class); + + @InjectMocks + private SystemWideAlertServiceImpl systemWideAlertService; + + @Mock + private SystemWideAlertDAO systemWideAlertDAO; + + @Mock + private AuthorizeService authorizeService; + + @Mock + private Context context; + + @Mock + private SystemWideAlert systemWideAlert; + + @Mock + private EPerson eperson; + + + @Test + public void testCreate() throws Exception { + // Mock admin state + when(authorizeService.isAdmin(context)).thenReturn(true); + + // Declare objects utilized in unit test + SystemWideAlert systemWideAlert = new SystemWideAlert(); + systemWideAlert.setMessage("Test message"); + systemWideAlert.setAllowSessions(AllowSessionsEnum.ALLOW_ALL_SESSIONS); + systemWideAlert.setCountdownTo(null); + systemWideAlert.setActive(true); + + // Mock DAO to return our defined SystemWideAlert + when(systemWideAlertDAO.create(any(), any())).thenReturn(systemWideAlert); + + // The newly created SystemWideAlert's message should match our mocked SystemWideAlert's message + SystemWideAlert result = systemWideAlertService.create(context, "Test message", + AllowSessionsEnum.ALLOW_ALL_SESSIONS, null, true); + assertEquals("TestCreate 0", systemWideAlert.getMessage(), result.getMessage()); + // The newly created SystemWideAlert should match our mocked SystemWideAlert + assertEquals("TestCreate 1", systemWideAlert, result); + } + + + @Test + public void testFindAll() throws Exception { + // Declare objects utilized in unit test + List systemWideAlertList = new ArrayList<>(); + + // The SystemWideAlert(s) reported from our mocked state should match our systemWideAlertList + assertEquals("TestFindAll 0", systemWideAlertList, systemWideAlertService.findAll(context)); + } + + @Test + public void testFind() throws Exception { + // Mock DAO to return our mocked SystemWideAlert + when(systemWideAlertService.find(context, 0)).thenReturn(systemWideAlert); + + // The SystemWideAlert reported from our ID should match our mocked SystemWideAlert + assertEquals("TestFind 0", systemWideAlert, systemWideAlertService.find(context, 0)); + } + + @Test + public void testFindAllActive() throws Exception { + // Declare objects utilized in unit test + List systemWideAlertList = new ArrayList<>(); + + // The SystemWideAlert(s) reported from our mocked state should match our systemWideAlertList + assertEquals("TestFindAllActive 0", systemWideAlertList, systemWideAlertService.findAllActive(context, 10, 0)); + } + + + @Test + public void testUpdate() throws Exception { + // Mock admin state + when(authorizeService.isAdmin(context)).thenReturn(true); + + // Invoke impl of method update() + systemWideAlertService.update(context, systemWideAlert); + + // Verify systemWideAlertDAO.save was invoked twice to confirm proper invocation of both impls of update() + Mockito.verify(systemWideAlertDAO, times(1)).save(context, systemWideAlert); + } + + @Test + public void testDelete() throws Exception { + // Mock admin state + when(authorizeService.isAdmin(context)).thenReturn(true); + + // Invoke method delete() + systemWideAlertService.delete(context, systemWideAlert); + + // Verify systemWideAlertDAO.delete() ran once to confirm proper invocation of delete() + Mockito.verify(systemWideAlertDAO, times(1)).delete(context, systemWideAlert); + } + + @Test + public void canNonAdminUserLoginTrueTest() throws Exception { + // Mock the alert state + when(systemWideAlert.getAllowSessions()).thenReturn(AllowSessionsEnum.ALLOW_ALL_SESSIONS); + + // Mock DAO to return our defined systemWideAlertList + List systemWideAlertList = new ArrayList<>(); + systemWideAlertList.add(systemWideAlert); + when(systemWideAlertDAO.findAllActive(context, 1, 0)).thenReturn(systemWideAlertList); + + // Assert the non admin users can log in + assertTrue("CanNonAdminUserLogin 0", systemWideAlertService.canNonAdminUserLogin(context)); + } + + @Test + public void canNonAdminUserLoginFalseTest() throws Exception { + // Mock the alert state + when(systemWideAlert.getAllowSessions()).thenReturn(AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY); + + // Mock DAO to return our defined systemWideAlertList + List systemWideAlertList = new ArrayList<>(); + systemWideAlertList.add(systemWideAlert); + when(systemWideAlertDAO.findAllActive(context, 1, 0)).thenReturn(systemWideAlertList); + + // Assert the non admin users can log in + assertFalse("CanNonAdminUserLogin 1", systemWideAlertService.canNonAdminUserLogin(context)); + } + + @Test + public void canUserMaintainSessionAdminTest() throws Exception { + // Assert the admin user can log in + assertTrue("CanUserMaintainSession 0", systemWideAlertService.canNonAdminUserLogin(context)); + } + @Test + public void canUserMaintainSessionTrueTest() throws Exception { + // Mock admin state + when(authorizeService.isAdmin(context, eperson)).thenReturn(false); + + // Mock the alert state + when(systemWideAlert.getAllowSessions()).thenReturn(AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY); + + // Mock DAO to return our defined systemWideAlertList + List systemWideAlertList = new ArrayList<>(); + systemWideAlertList.add(systemWideAlert); + when(systemWideAlertDAO.findAllActive(context, 1, 0)).thenReturn(systemWideAlertList); + + // Assert the non admin users can main session + assertTrue("CanUserMaintainSession 1", systemWideAlertService.canUserMaintainSession(context, eperson)); + } + + @Test + public void canUserMaintainSessionFalseTest() throws Exception { + // Mock admin state + when(authorizeService.isAdmin(context, eperson)).thenReturn(false); + + // Mock the alert state + when(systemWideAlert.getAllowSessions()).thenReturn(AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY); + + // Mock DAO to return our defined systemWideAlertList + List systemWideAlertList = new ArrayList<>(); + systemWideAlertList.add(systemWideAlert); + when(systemWideAlertDAO.findAllActive(context, 1, 0)).thenReturn(systemWideAlertList); + + // Assert the non admin users cannot main session + assertFalse("CanUserMaintainSession 2", systemWideAlertService.canUserMaintainSession(context, eperson)); + } + + + +} diff --git a/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java b/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java index 3306ced8f4..e045162e8c 100644 --- a/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java @@ -13,6 +13,7 @@ import java.util.List; import org.apache.commons.collections4.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.alerts.service.SystemWideAlertService; import org.dspace.app.requestitem.factory.RequestItemServiceFactory; import org.dspace.app.requestitem.service.RequestItemService; import org.dspace.authorize.AuthorizeException; @@ -42,6 +43,7 @@ import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.GroupService; import org.dspace.eperson.service.RegistrationDataService; +import org.dspace.eperson.service.SubscribeService; import org.dspace.orcid.factory.OrcidServiceFactory; import org.dspace.orcid.service.OrcidHistoryService; import org.dspace.orcid.service.OrcidQueueService; @@ -102,6 +104,8 @@ public abstract class AbstractBuilder { static OrcidHistoryService orcidHistoryService; static OrcidQueueService orcidQueueService; static OrcidTokenService orcidTokenService; + static SystemWideAlertService systemWideAlertService; + static SubscribeService subscribeService; protected Context context; @@ -161,6 +165,9 @@ public abstract class AbstractBuilder { orcidHistoryService = OrcidServiceFactory.getInstance().getOrcidHistoryService(); orcidQueueService = OrcidServiceFactory.getInstance().getOrcidQueueService(); orcidTokenService = OrcidServiceFactory.getInstance().getOrcidTokenService(); + systemWideAlertService = DSpaceServicesFactory.getInstance().getServiceManager() + .getServicesByType(SystemWideAlertService.class).get(0); + subscribeService = ContentServiceFactory.getInstance().getSubscribeService(); } @@ -194,7 +201,8 @@ public abstract class AbstractBuilder { requestItemService = null; versioningService = null; orcidTokenService = null; - + systemWideAlertService = null; + subscribeService = null; } public static void cleanupObjects() throws Exception { diff --git a/dspace-api/src/test/java/org/dspace/builder/SubscribeBuilder.java b/dspace-api/src/test/java/org/dspace/builder/SubscribeBuilder.java new file mode 100644 index 0000000000..40e890a8c9 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/builder/SubscribeBuilder.java @@ -0,0 +1,111 @@ +/** + * 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.builder; + +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; +import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Subscription; +import org.dspace.eperson.SubscriptionParameter; +import org.dspace.eperson.service.SubscribeService; + +public class SubscribeBuilder extends AbstractBuilder { + + /* Log4j logger*/ + private static final Logger log = LogManager.getLogger(); + + private Subscription subscription; + + protected SubscribeBuilder(Context context) { + super(context); + } + + @Override + protected SubscribeService getService() { + return subscribeService; + } + + @Override + public void cleanup() throws Exception { + try (Context c = new Context()) { + c.turnOffAuthorisationSystem(); + // Ensure object and any related objects are reloaded before checking to see what needs cleanup + subscription = c.reloadEntity(subscription); + if (subscription != null) { + delete(c, subscription); + } + c.complete(); + indexingService.commit(); + } + } + + public static void deleteSubscription(int id) throws Exception { + try (Context c = new Context()) { + c.turnOffAuthorisationSystem(); + Subscription subscription = subscribeService.findById(c, id); + if (Objects.nonNull(subscription)) { + try { + subscribeService.deleteSubscription(c, subscription); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + c.complete(); + } + indexingService.commit(); + } + + @Override + public Subscription build() { + try { + + context.dispatchEvents(); + + indexingService.commit(); + } catch (SearchServiceException e) { + log.error(e); + } + return subscription; + } + + public static SubscribeBuilder subscribeBuilder(final Context context, String type, DSpaceObject dSpaceObject, + EPerson ePerson, List subscriptionParameterList) { + SubscribeBuilder builder = new SubscribeBuilder(context); + return builder.create(context, type, dSpaceObject, ePerson, subscriptionParameterList); + } + + private SubscribeBuilder create(Context context, String type, DSpaceObject dSpaceObject, EPerson ePerson, + List subscriptionParameterList) { + try { + + this.context = context; + this.subscription = subscribeService.subscribe(context, ePerson, dSpaceObject, + subscriptionParameterList, type); + + } catch (SQLException | AuthorizeException e) { + log.warn("Failed to create the Subscription", e); + } + return this; + } + + @Override + public void delete(Context c, Subscription dso) throws Exception { + if (Objects.nonNull(dso)) { + getService().deleteSubscription(c, dso); + } + } + +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/builder/SystemWideAlertBuilder.java b/dspace-api/src/test/java/org/dspace/builder/SystemWideAlertBuilder.java new file mode 100644 index 0000000000..cb64898152 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/builder/SystemWideAlertBuilder.java @@ -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.builder; + +import java.sql.SQLException; +import java.util.Date; + +import org.dspace.alerts.AllowSessionsEnum; +import org.dspace.alerts.SystemWideAlert; +import org.dspace.alerts.service.SystemWideAlertService; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; + +public class SystemWideAlertBuilder extends AbstractBuilder { + + private SystemWideAlert systemWideAlert; + + protected SystemWideAlertBuilder(Context context) { + super(context); + } + + public static SystemWideAlertBuilder createSystemWideAlert(Context context, String message) + throws SQLException, AuthorizeException { + SystemWideAlertBuilder systemWideAlertBuilder = new SystemWideAlertBuilder(context); + return systemWideAlertBuilder.create(context, message, AllowSessionsEnum.ALLOW_ALL_SESSIONS, null, false); + } + + private SystemWideAlertBuilder create(Context context, String message, AllowSessionsEnum allowSessionsType, + Date countdownTo, boolean active) + throws SQLException, AuthorizeException { + this.context = context; + this.systemWideAlert = systemWideAlertService.create(context, message, allowSessionsType, countdownTo, active); + return this; + } + + public SystemWideAlertBuilder withAllowSessions(AllowSessionsEnum allowSessionsType) { + systemWideAlert.setAllowSessions(allowSessionsType); + return this; + } + + public SystemWideAlertBuilder withCountdownDate(Date countdownTo) { + systemWideAlert.setCountdownTo(countdownTo); + return this; + } + + public SystemWideAlertBuilder isActive(boolean isActive) { + systemWideAlert.setActive(isActive); + return this; + } + + @Override + public void cleanup() throws Exception { + try (Context c = new Context()) { + c.setDispatcher("noindex"); + c.turnOffAuthorisationSystem(); + // Ensure object and any related objects are reloaded before checking to see what needs cleanup + systemWideAlert = c.reloadEntity(systemWideAlert); + if (systemWideAlert != null) { + delete(c, systemWideAlert); + } + c.complete(); + indexingService.commit(); + } + } + + @Override + public SystemWideAlert build() { + try { + systemWideAlertService.update(context, systemWideAlert); + context.dispatchEvents(); + indexingService.commit(); + } catch (Exception e) { + return null; + } + return systemWideAlert; + } + + + @Override + protected SystemWideAlertService getService() { + return systemWideAlertService; + } + + public void delete(Context c, SystemWideAlert alert) throws Exception { + if (alert != null) { + getService().delete(c, alert); + } + } +} diff --git a/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java b/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java new file mode 100644 index 0000000000..945dd481d0 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/eperson/SubscribeServiceIT.java @@ -0,0 +1,417 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +package org.dspace.eperson; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.dspace.builder.SubscribeBuilder.subscribeBuilder; +import static org.dspace.matcher.SubscribeMatcher.matches; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertEquals; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.lang.StringUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.SubscribeBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.eperson.service.SubscribeService; +import org.junit.Before; +import org.junit.Test; + +public class SubscribeServiceIT extends AbstractIntegrationTestWithDatabase { + + private final SubscribeService subscribeService = ContentServiceFactory.getInstance().getSubscribeService(); + + private Collection firstCollection; + private Collection secondCollection; + + @Before + public void init() throws Exception { + context.turnOffAuthorisationSystem(); + Community parentCommunity = CommunityBuilder.createCommunity(context).build(); + firstCollection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("First Collection").build(); + secondCollection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Second Collection").build(); + context.restoreAuthSystemState(); + } + + @Test + public void findAllWithoutAndWithLimit() throws Exception { + + String resourceType = "Collection"; + + EPerson subscribingUser = context.getCurrentUser(); + + createSubscription("content", firstCollection, subscribingUser, weekly()); + createSubscription("content", secondCollection, subscribingUser, daily(), annual()); + + // unlimited search returns all subscriptions + + List subscriptions = subscribeService.findAll(context, resourceType, 10, 0); + assertThat(subscriptions, containsInAnyOrder( + asList(matches(firstCollection, subscribingUser, "content", + singletonList(weekly())), + matches(secondCollection, subscribingUser, "content", + asList(daily(), annual()))))); + + // limited search returns first + + subscriptions = subscribeService.findAll(context, resourceType, 1, 0); + + assertThat(subscriptions, containsInAnyOrder( + singletonList(matches(firstCollection, subscribingUser, "content", + singletonList(weekly()))))); + + // search with offset returns second + + subscriptions = subscribeService.findAll(context, resourceType, 100, 1); + + assertThat(subscriptions, containsInAnyOrder( + singletonList(matches(secondCollection, subscribingUser, "content", + asList(daily(), annual()))))); + + // lookup without resource type + subscriptions = subscribeService.findAll(context, StringUtils.EMPTY, 100, 0); + + assertThat(subscriptions, containsInAnyOrder( + asList(matches(firstCollection, subscribingUser, "content", + singletonList(weekly())), + matches(secondCollection, subscribingUser, "content", + asList(daily(), annual()))))); + + } + + private static SubscriptionParameter annual() { + return createSubscriptionParameter("frequency", "A"); + } + + private static SubscriptionParameter daily() { + return createSubscriptionParameter("frequency", "D"); + } + + @Test(expected = Exception.class) + public void findAllWithInvalidResource() throws Exception { + + String resourceType = "INVALID"; + Integer limit = 10; + Integer offset = 0; + + createSubscription("content", firstCollection, context.getCurrentUser(), + weekly()); + + subscribeService.findAll(context, resourceType, limit, offset); + + } + + @Test + public void newSubscriptionCreatedByAdmin() throws Exception { + + SubscriptionParameter monthly = createSubscriptionParameter("frequency", "M"); + + List parameters = Collections.singletonList( + monthly); + + EPerson currentUser = context.getCurrentUser(); + context.setCurrentUser(admin); + Subscription subscription = subscribeService.subscribe(context, eperson, + firstCollection, parameters, "content"); + + assertThat(subscription, is(matches(firstCollection, eperson, + "content", singletonList(monthly)))); + + SubscribeBuilder.deleteSubscription(subscription.getID()); + context.setCurrentUser(currentUser); + + } + + @Test + public void newSubscriptionCreatedByCurrentUser() throws Exception { + + EPerson currentUser = context.getCurrentUser(); + Subscription subscription = subscribeService.subscribe(context, currentUser, + secondCollection, + asList(daily(), weekly()), "content"); + + assertThat(subscription, matches(secondCollection, currentUser, "content", + asList(daily(), weekly()))); + + SubscribeBuilder.deleteSubscription(subscription.getID()); + } + + @Test(expected = AuthorizeException.class) + public void nonAdminDifferentUserTriesToSubscribe() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson notAdmin = EPersonBuilder.createEPerson(context).withEmail("not-admin@example.com").build(); + context.restoreAuthSystemState(); + EPerson currentUser = context.getCurrentUser(); + context.setCurrentUser(notAdmin); + try { + subscribeService.subscribe(context, admin, firstCollection, + singletonList( + daily()), "content"); + } finally { + context.setCurrentUser(currentUser); + } + + } + + @Test + public void unsubscribeByAdmin() throws Exception { + + EPerson subscribingUser = context.getCurrentUser(); + createSubscription("content", secondCollection, subscribingUser, + weekly()); + + List subscriptions = + subscribeService.findSubscriptionsByEPersonAndDso(context, subscribingUser, + secondCollection, 100, 0); + + assertEquals(subscriptions.size(), 1); + + context.setCurrentUser(admin); + subscribeService.unsubscribe(context, subscribingUser, secondCollection); + context.setCurrentUser(subscribingUser); + + subscriptions = + subscribeService.findSubscriptionsByEPersonAndDso(context, subscribingUser, + secondCollection, 100, 0); + + assertEquals(subscriptions.size(), 0); + } + + @Test + public void subscribingUserUnsubscribesTheirSubscription() throws Exception { + + EPerson subscribingUser = context.getCurrentUser(); + createSubscription("content", secondCollection, subscribingUser, + weekly()); + + List subscriptions = + subscribeService.findSubscriptionsByEPersonAndDso(context, subscribingUser, + secondCollection, 100, 0); + + assertEquals(subscriptions.size(), 1); + + + subscribeService.unsubscribe(context, subscribingUser, secondCollection); + + subscriptions = + subscribeService.findSubscriptionsByEPersonAndDso(context, subscribingUser, + secondCollection, 100, 0); + + assertEquals(subscriptions.size(), 0); + } + + @Test(expected = AuthorizeException.class) + public void nonAdminDifferentUserTriesToUnSubscribeAnotherUser() throws Exception { + EPerson subscribingUser = context.getCurrentUser(); + Subscription subscription = createSubscription("content", secondCollection, subscribingUser, + weekly()); + + context.turnOffAuthorisationSystem(); + EPerson nonAdmin = EPersonBuilder.createEPerson(context).build(); + context.restoreAuthSystemState(); + + + try { + context.setCurrentUser(nonAdmin); + subscribeService.unsubscribe(context, subscribingUser, secondCollection); + } finally { + context.setCurrentUser(subscribingUser); + SubscribeBuilder.deleteSubscription(subscription.getID()); + } + + } + + @Test + public void updateSubscription() throws Exception { + + EPerson currentUser = context.getCurrentUser(); + Subscription subscription = createSubscription("original", + firstCollection, currentUser, + createSubscriptionParameter("frequency", "M")); + + String updatedType = "updated"; + List updatedParameters = Collections.singletonList( + annual() + ); + + try { + Subscription updated = subscribeService.updateSubscription(context, subscription.getID(), + updatedType, updatedParameters); + + assertThat(updated, is(matches(firstCollection, currentUser, updatedType, updatedParameters))); + + List subscriptions = + subscribeService.findSubscriptionsByEPersonAndDso(context, currentUser, firstCollection, 10, 0); + + assertThat(subscriptions, contains( + matches(firstCollection, currentUser, updatedType, updatedParameters))); + + } finally { + SubscribeBuilder.deleteSubscription(subscription.getID()); + } + + } + + @Test + public void parametersAdditionAndRemoval() throws Exception { + + SubscriptionParameter firstParameter = createSubscriptionParameter("key1", "value1"); + SubscriptionParameter secondParameter = createSubscriptionParameter("key2", "value2"); + + EPerson currentUser = context.getCurrentUser(); + Subscription subscription = createSubscription("type", secondCollection, currentUser, + firstParameter, secondParameter); + int subscriptionId = subscription.getID(); + + SubscriptionParameter addedParameter = createSubscriptionParameter("added", "add"); + + + try { + Subscription updatedSubscription = subscribeService.addSubscriptionParameter(context, subscriptionId, + addedParameter); + assertThat(updatedSubscription, is(matches(secondCollection, currentUser, "type", + asList(firstParameter, secondParameter, addedParameter)))); + updatedSubscription = subscribeService.removeSubscriptionParameter(context, subscriptionId, + secondParameter); + assertThat(updatedSubscription, is(matches(secondCollection, currentUser, "type", + asList(firstParameter, addedParameter)))); + } finally { + SubscribeBuilder.deleteSubscription(subscriptionId); + } + } + + @Test + public void findersAndDeletionsTest() throws SQLException { + // method to test all find and delete methods exposed by SubscribeService + context.turnOffAuthorisationSystem(); + EPerson firstSubscriber = EPersonBuilder.createEPerson(context).withEmail("first-user@example.com").build(); + EPerson secondSubscriber = EPersonBuilder.createEPerson(context).withEmail("second-user@example.com").build(); + EPerson thirdSubscriber = EPersonBuilder.createEPerson(context).withEmail("third-user@example.com").build(); + context.restoreAuthSystemState(); + + EPerson currentUser = context.getCurrentUser(); + try { + context.setCurrentUser(firstSubscriber); + createSubscription("type1", firstCollection, firstSubscriber, daily(), + weekly()); + createSubscription("type1", secondCollection, firstSubscriber, + daily(), + annual()); + createSubscription("type2", secondCollection, firstSubscriber, + daily()); + + context.setCurrentUser(secondSubscriber); + createSubscription("type1", firstCollection, secondSubscriber, + daily()); + createSubscription("type1", secondCollection, secondSubscriber, + daily(), + annual()); + + context.setCurrentUser(thirdSubscriber); + createSubscription("type1", firstCollection, thirdSubscriber, daily()); + createSubscription("type1", secondCollection, thirdSubscriber, + daily(), + annual()); + + } finally { + context.setCurrentUser(currentUser); + } + + List firstUserSubscriptions = + subscribeService.findSubscriptionsByEPerson(context, firstSubscriber, 100, 0); + + assertThat(firstUserSubscriptions, containsInAnyOrder( + matches(firstCollection, firstSubscriber, "type1", asList(daily(), + weekly())), + matches(secondCollection, firstSubscriber, "type1", asList(daily(), + annual())), + matches(secondCollection, firstSubscriber, "type2", singletonList( + daily())) + )); + + List firstUserSubscriptionsLimited = + subscribeService.findSubscriptionsByEPerson(context, firstSubscriber, 1, 0); + + assertThat(firstUserSubscriptionsLimited.size(), is(1)); + + List firstUserSubscriptionsWithOffset = + subscribeService.findSubscriptionsByEPerson(context, firstSubscriber, 100, 1); + + assertThat(firstUserSubscriptionsWithOffset.size(), is(2)); + + subscribeService.deleteByEPerson(context, firstSubscriber); + assertThat(subscribeService.findSubscriptionsByEPerson(context, firstSubscriber, 100, 0), + is(List.of())); + + List secondSubscriberSecondCollectionSubscriptions = + subscribeService.findSubscriptionsByEPersonAndDso(context, secondSubscriber, firstCollection, 10, 0); + + assertThat(secondSubscriberSecondCollectionSubscriptions, contains( + matches(firstCollection, secondSubscriber, "type1", singletonList(daily())) + )); + + List byTypeAndFrequency = + subscribeService.findAllSubscriptionsBySubscriptionTypeAndFrequency(context, "type1", + "D"); + assertThat(byTypeAndFrequency, containsInAnyOrder( + matches(firstCollection, secondSubscriber, "type1", singletonList( + daily())), + matches(secondCollection, secondSubscriber, "type1", asList(daily(), + annual())), + matches(firstCollection, thirdSubscriber, "type1", singletonList( + daily())), + matches(secondCollection, thirdSubscriber, "type1", asList(daily(), + annual())) + )); + + assertThat(subscribeService.countAll(context), is(4L)); + assertThat(subscribeService.countByEPersonAndDSO(context, secondSubscriber, secondCollection), is(1L)); + assertThat(subscribeService.countSubscriptionsByEPerson(context, thirdSubscriber), is(2L)); + + + } + + private static SubscriptionParameter weekly() { + return createSubscriptionParameter("frequency", "W"); + } + + private Subscription createSubscription(String type, DSpaceObject dso, EPerson ePerson, + SubscriptionParameter... parameters) { + return subscribeBuilder(context, type, + dso, ePerson, + Arrays.stream(parameters).collect(Collectors.toList())).build(); + } + + + private static SubscriptionParameter createSubscriptionParameter(String name, String value) { + SubscriptionParameter parameter = new SubscriptionParameter(); + parameter.setName(name); + parameter.setValue(value); + return parameter; + } + +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java b/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java index b9dbbba647..09387acd3e 100644 --- a/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java @@ -9,7 +9,9 @@ package org.dspace.identifier; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeNotNull; @@ -36,6 +38,7 @@ import org.dspace.content.WorkspaceItem; import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.logic.DefaultFilter; import org.dspace.content.logic.LogicalStatement; +import org.dspace.content.logic.TrueFilter; import org.dspace.content.service.CollectionService; import org.dspace.content.service.CommunityService; import org.dspace.content.service.ItemService; @@ -128,7 +131,7 @@ public class DOIIdentifierProviderTest provider.itemService = itemService; provider.setConfigurationService(config); provider.setDOIConnector(connector); - provider.setFilterService(null); + provider.setFilter(null); } catch (AuthorizeException ex) { log.error("Authorization Error in init", ex); fail("Authorization Error in init: " + ex.getMessage()); @@ -504,7 +507,7 @@ public class DOIIdentifierProviderTest String doi = null; try { // get a DOI (skipping any filters) - doi = provider.mint(context, item, true); + doi = provider.mint(context, item); } catch (IdentifierException e) { e.printStackTrace(System.err); fail("Got an IdentifierException: " + e.getMessage()); @@ -544,23 +547,18 @@ public class DOIIdentifierProviderTest Item item = newItem(); boolean wasFiltered = false; try { - // Temporarily set the provider to have a filter that always returns false for an item - // (therefore, the item should be 'filtered' out and not apply to this minting request) + // Mint this with the filter DefaultFilter doiFilter = new DefaultFilter(); LogicalStatement alwaysFalse = (context, i) -> false; doiFilter.setStatement(alwaysFalse); - provider.setFilterService(doiFilter); // get a DOI with the method that applies filters by default - provider.mint(context, item); + provider.mint(context, item, doiFilter); } catch (DOIIdentifierNotApplicableException e) { // This is what we wanted to see - we can return safely wasFiltered = true; } catch (IdentifierException e) { e.printStackTrace(); fail("Got an IdentifierException: " + e.getMessage()); - } finally { - // Set filter service back to null - provider.setFilterService(null); } // Fail the test if the filter didn't throw a "not applicable" exception assertTrue("DOI minting attempt was not filtered by filter service", wasFiltered); @@ -583,17 +581,14 @@ public class DOIIdentifierProviderTest DefaultFilter doiFilter = new DefaultFilter(); LogicalStatement alwaysTrue = (context, i) -> true; doiFilter.setStatement(alwaysTrue); - provider.setFilterService(doiFilter); // get a DOI with the method that applies filters by default - doi = provider.mint(context, item); + doi = provider.mint(context, item, doiFilter); } catch (DOIIdentifierNotApplicableException e) { // This is what we wanted to see - we can return safely wasFiltered = true; } catch (IdentifierException e) { e.printStackTrace(); fail("Got an IdentifierException: " + e.getMessage()); - } finally { - provider.setFilterService(null); } // If the attempt was filtered, fail assertFalse("DOI minting attempt was incorrectly filtered by filter service", wasFiltered); @@ -665,7 +660,9 @@ public class DOIIdentifierProviderTest Item item = newItem(); // Register, skipping the filter - String doi = provider.register(context, item, true); + String doi = provider.register(context, item, + DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName( + "always_true_filter", TrueFilter.class)); // we want the created DOI to be returned in the following format: // doi:10./. @@ -763,6 +760,104 @@ public class DOIIdentifierProviderTest DOIIdentifierProvider.TO_BE_DELETED.equals(doiRow2.getStatus())); } + @Test + public void testUpdateMetadataSkippedForPending() + throws SQLException, AuthorizeException, IOException, IdentifierException, IllegalAccessException, + WorkflowException { + context.turnOffAuthorisationSystem(); + Item item = newItem(); + // Mint a new DOI with PENDING status + String doi1 = this.createDOI(item, DOIIdentifierProvider.PENDING, true); + // Update metadata for the item. + // This would normally shift status to UPDATE_REGISTERED, UPDATE_BEFORE_REGISTERING or UPDATE_RESERVED. + // But if the DOI is just pending, it should return without changing anything. + provider.updateMetadata(context, item, doi1); + // Get the DOI from the service + DOI doi = doiService.findDOIByDSpaceObject(context, item); + // Ensure it is still PENDING + assertEquals("Status of updated DOI did not remain PENDING", + DOIIdentifierProvider.PENDING, doi.getStatus()); + context.restoreAuthSystemState(); + } + + + @Test + public void testMintDoiAfterOrphanedPendingDOI() + throws SQLException, AuthorizeException, IOException, IdentifierException, IllegalAccessException, + WorkflowException { + context.turnOffAuthorisationSystem(); + Item item1 = newItem(); + // Mint a new DOI with PENDING status + String doi1 = this.createDOI(item1, DOIIdentifierProvider.PENDING, true); + // remove the item + itemService.delete(context, item1); + // Get the DOI from the service + DOI doi = doiService.findDOIByDSpaceObject(context, item1); + // ensure DOI has no state + assertNull("Orphaned DOI was not set deleted", doi); + // create a new item and a new DOI + Item item2 = newItem(); + String doi2 = null; + try { + // get a DOI (skipping any filters) + doi2 = provider.mint(context, item2); + } catch (IdentifierException e) { + e.printStackTrace(System.err); + fail("Got an IdentifierException: " + e.getMessage()); + } + + assertNotNull("Minted DOI is null?!", doi2); + assertFalse("Minted DOI is empty!", doi2.isEmpty()); + assertNotEquals("Minted DOI equals previously orphaned DOI.", doi1, doi2); + + try { + doiService.formatIdentifier(doi2); + } catch (DOIIdentifierException e) { + e.printStackTrace(System.err); + fail("Minted an unrecognizable DOI: " + e.getMessage()); + } + + context.restoreAuthSystemState(); + } + + @Test + public void testUpdateMetadataSkippedForMinted() + throws SQLException, AuthorizeException, IOException, IdentifierException, IllegalAccessException, + WorkflowException { + context.turnOffAuthorisationSystem(); + Item item = newItem(); + // Mint a new DOI with MINTED status + String doi1 = this.createDOI(item, DOIIdentifierProvider.MINTED, true); + // Update metadata for the item. + // This would normally shift status to UPDATE_REGISTERED, UPDATE_BEFORE_REGISTERING or UPDATE_RESERVED. + // But if the DOI is just minted, it should return without changing anything. + provider.updateMetadata(context, item, doi1); + // Get the DOI from the service + DOI doi = doiService.findDOIByDSpaceObject(context, item); + // Ensure it is still MINTED + assertEquals("Status of updated DOI did not remain PENDING", + DOIIdentifierProvider.MINTED, doi.getStatus()); + context.restoreAuthSystemState(); + } + + @Test + public void testLoadOrCreateDOIReturnsMintedStatus() + throws SQLException, AuthorizeException, IOException, IdentifierException, IllegalAccessException, + WorkflowException { + Item item = newItem(); + // Mint a DOI without an explicit reserve or register context + String mintedDoi = provider.mint(context, item, DSpaceServicesFactory.getInstance() + .getServiceManager().getServiceByName("always_true_filter", TrueFilter.class)); + DOI doi = doiService.findByDoi(context, mintedDoi.substring(DOI.SCHEME.length())); + // This should be minted + assertEquals("DOI is not of 'minted' status", DOIIdentifierProvider.MINTED, doi.getStatus()); + provider.updateMetadata(context, item, mintedDoi); + DOI secondFind = doiService.findByDoi(context, mintedDoi.substring(DOI.SCHEME.length())); + // After an update, this should still be minted + assertEquals("DOI is not of 'minted' status", + DOIIdentifierProvider.MINTED, secondFind.getStatus()); + + } // test the following methods using the MockDOIConnector. // updateMetadataOnline diff --git a/dspace-api/src/test/java/org/dspace/matcher/SubscribeMatcher.java b/dspace-api/src/test/java/org/dspace/matcher/SubscribeMatcher.java new file mode 100644 index 0000000000..4671e65d38 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/matcher/SubscribeMatcher.java @@ -0,0 +1,79 @@ +/** + * 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.matcher; + +import java.util.List; +import java.util.stream.Collectors; + +import org.dspace.content.DSpaceObject; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Subscription; +import org.dspace.eperson.SubscriptionParameter; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; + +public class SubscribeMatcher extends BaseMatcher { + + private final DSpaceObject dso; + private final EPerson eperson; + private final List parameters; + private final String type; + + private SubscribeMatcher(DSpaceObject dso, EPerson eperson, String type, List parameters) { + this.dso = dso; + this.eperson = eperson; + this.parameters = parameters; + this.type = type; + } + + public static SubscribeMatcher matches(DSpaceObject dso, EPerson ePerson, String type, + List parameters) { + return new SubscribeMatcher(dso, ePerson, type, parameters); + } + + @Override + public boolean matches(Object subscription) { + Subscription s = (Subscription) subscription; + return s.getEPerson().equals(eperson) + && s.getDSpaceObject().equals(dso) + && s.getSubscriptionType().equals(type) + && checkParameters(s.getSubscriptionParameterList()); + } + + private Boolean checkParameters(List parameters) { + if (parameters.size() != this.parameters.size()) { + return false; + } + // FIXME: for check purpose we rely on name and value. Evaluate to extend or refactor this part + for (int i = 0; i < parameters.size(); i++) { + SubscriptionParameter parameter = parameters.get(i); + SubscriptionParameter match = this.parameters.get(i); + boolean differentName = !parameter.getName().equals((match.getName())); + if (differentName) { + return false; + } + boolean differentValue = !parameter.getValue().equals((match.getValue())); + if (differentValue) { + return false; + } + } + return true; + } + + @Override + public void describeTo(Description description) { + String subscription = String.format("Type: %s, eperson: %s, dso: %s, params: %s", + type, eperson.getID(), dso.getID(), parameters.stream() + .map(p -> "{ name: " + p.getName() + + ", value: " + p.getValue() + + "}") + .collect(Collectors.joining(", "))); + description.appendText("Subscription matching: " + subscription); + } +} diff --git a/dspace-api/src/test/java/org/dspace/xmlworkflow/XmlWorkflowServiceIT.java b/dspace-api/src/test/java/org/dspace/xmlworkflow/XmlWorkflowServiceIT.java index 69c4dc16f4..865abaca21 100644 --- a/dspace-api/src/test/java/org/dspace/xmlworkflow/XmlWorkflowServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/xmlworkflow/XmlWorkflowServiceIT.java @@ -9,11 +9,13 @@ package org.dspace.xmlworkflow; import static org.junit.Assert.assertTrue; +import java.io.IOException; import java.sql.SQLException; import java.util.List; import javax.servlet.http.HttpServletRequest; import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.AuthorizeService; @@ -21,17 +23,24 @@ import org.dspace.builder.ClaimedTaskBuilder; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.GroupBuilder; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.core.Constants; import org.dspace.discovery.IndexingService; import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; import org.dspace.xmlworkflow.service.XmlWorkflowService; import org.dspace.xmlworkflow.state.Workflow; +import org.dspace.xmlworkflow.state.actions.processingaction.SelectReviewerAction; import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; +import org.junit.After; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -47,6 +56,22 @@ public class XmlWorkflowServiceIT extends AbstractIntegrationTestWithDatabase { .getServiceByName(IndexingService.class.getName(), IndexingService.class); protected AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + protected GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); + protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + + /** + * Cleans up the created workflow role groups after each test + * @throws SQLException + * @throws AuthorizeException + * @throws IOException + */ + @After + public void cleanup() throws SQLException, AuthorizeException, IOException { + Group reviewManagers = groupService.findByName(context, "ReviewManagers"); + if (reviewManagers != null) { + groupService.delete(context, reviewManagers); + } + } /** * Test to verify that if a user submits an item into the workflow, then it gets rejected that the submitter gets @@ -85,6 +110,93 @@ public class XmlWorkflowServiceIT extends AbstractIntegrationTestWithDatabase { assertTrue(this.containsRPForUser(taskToReject.getWorkflowItem().getItem(), submitter, Constants.WRITE)); } + /** + * Test to verify that if a user submits an item into the workflow, a reviewmanager can select a single reviewer + * eperson + */ + @Test + public void workflowUserSingleSelectedReviewer_ItemShouldBeEditable() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson submitter = EPersonBuilder.createEPerson(context).withEmail("submitter@example.org").build(); + context.setCurrentUser(submitter); + EPerson reviewManager = + EPersonBuilder.createEPerson(context).withEmail("reviewmanager-test@example.org").build(); + Community community = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection colWithWorkflow = CollectionBuilder.createCollection(context, community, "123456789/workflow-test-1") + .withName("Collection WITH workflow") + .withWorkflowGroup("reviewmanagers", reviewManager) + .build(); + Workflow workflow = XmlWorkflowServiceFactory.getInstance().getWorkflowFactory().getWorkflow(colWithWorkflow); + ClaimedTask task = ClaimedTaskBuilder.createClaimedTask(context, colWithWorkflow, reviewManager) + .withTitle("Test workflow item to reject").build(); + // Set reviewer group property and add reviewer to group + SelectReviewerAction.resetGroup(); + configurationService.setProperty("action.selectrevieweraction.group", "Reviewers"); + Group reviewerGroup = GroupBuilder.createGroup(context).withName("Reviewers").build(); + EPerson reviewer = EPersonBuilder.createEPerson(context).withEmail("reviewer@example.org").build(); + groupService.addMember(context, reviewerGroup, reviewer); + context.restoreAuthSystemState(); + + // Review Manager should have access to workflow item + assertTrue(this.containsRPForUser(task.getWorkflowItem().getItem(), reviewManager, Constants.WRITE)); + + // select 1 reviewer + MockHttpServletRequest httpSelectReviewerRequest = new MockHttpServletRequest(); + httpSelectReviewerRequest.setParameter("submit_select_reviewer", "true"); + httpSelectReviewerRequest.setParameter("eperson", reviewer.getID().toString()); + executeWorkflowAction(httpSelectReviewerRequest, workflow, task); + + // Reviewer should have access to workflow item + assertTrue(this.containsRPForUser(task.getWorkflowItem().getItem(), reviewer, Constants.WRITE)); + } + + /** + * Test to verify that if a user submits an item into the workflow, a reviewmanager can select a multiple reviewer + * epersons + */ + @Test + public void workflowUserMultipleSelectedReviewer_ItemShouldBeEditable() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson submitter = EPersonBuilder.createEPerson(context).withEmail("submitter@example.org").build(); + context.setCurrentUser(submitter); + EPerson reviewManager = + EPersonBuilder.createEPerson(context).withEmail("reviewmanager-test@example.org").build(); + Community community = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection colWithWorkflow = CollectionBuilder.createCollection(context, community, "123456789/workflow-test-1") + .withName("Collection WITH workflow") + .withWorkflowGroup("reviewmanagers", reviewManager) + .build(); + Workflow workflow = XmlWorkflowServiceFactory.getInstance().getWorkflowFactory().getWorkflow(colWithWorkflow); + ClaimedTask task = ClaimedTaskBuilder.createClaimedTask(context, colWithWorkflow, reviewManager) + .withTitle("Test workflow item to reject").build(); + // Set reviewer group property and add reviewer to group + SelectReviewerAction.resetGroup(); + configurationService.setProperty("action.selectrevieweraction.group", "Reviewers"); + Group reviewerGroup = GroupBuilder.createGroup(context).withName("Reviewers").build(); + EPerson reviewer1 = EPersonBuilder.createEPerson(context).withEmail("reviewer1@example.org").build(); + EPerson reviewer2 = EPersonBuilder.createEPerson(context).withEmail("reviewer2@example.org").build(); + groupService.addMember(context, reviewerGroup, reviewer1); + groupService.addMember(context, reviewerGroup, reviewer2); + context.restoreAuthSystemState(); + + // Review Manager should have access to workflow item + assertTrue(this.containsRPForUser(task.getWorkflowItem().getItem(), reviewManager, Constants.WRITE)); + + // Select multiple reviewers + MockHttpServletRequest httpSelectMultipleReviewers = new MockHttpServletRequest(); + httpSelectMultipleReviewers.setParameter("submit_select_reviewer", "true"); + httpSelectMultipleReviewers.setParameter("eperson", reviewer1.getID().toString(), reviewer2.getID().toString()); + executeWorkflowAction(httpSelectMultipleReviewers, workflow, task); + + // Reviewers should have access to workflow item + assertTrue(this.containsRPForUser(task.getWorkflowItem().getItem(), reviewer1, Constants.WRITE)); + assertTrue(this.containsRPForUser(task.getWorkflowItem().getItem(), reviewer2, Constants.WRITE)); + } + private boolean containsRPForUser(Item item, EPerson user, int action) throws SQLException { List rps = authorizeService.getPolicies(context, item); for (ResourcePolicy rp : rps) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/IdentifierRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/IdentifierRestController.java deleted file mode 100644 index 5f7a473b91..0000000000 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/IdentifierRestController.java +++ /dev/null @@ -1,108 +0,0 @@ -/** - * 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; - -import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; - -import java.io.IOException; -import java.net.URI; -import java.sql.SQLException; -import java.util.Arrays; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.atteo.evo.inflector.English; -import org.dspace.app.rest.converter.ConverterService; -import org.dspace.app.rest.model.DSpaceObjectRest; -import org.dspace.app.rest.utils.ContextUtil; -import org.dspace.app.rest.utils.Utils; -import org.dspace.content.DSpaceObject; -import org.dspace.core.Context; -import org.dspace.identifier.IdentifierNotFoundException; -import org.dspace.identifier.IdentifierNotResolvableException; -import org.dspace.identifier.factory.IdentifierServiceFactory; -import org.dspace.identifier.service.IdentifierService; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.hateoas.Link; -import org.springframework.hateoas.TemplateVariable; -import org.springframework.hateoas.TemplateVariable.VariableType; -import org.springframework.hateoas.TemplateVariables; -import org.springframework.hateoas.UriTemplate; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/" + IdentifierRestController.CATEGORY) -public class IdentifierRestController implements InitializingBean { - public static final String CATEGORY = "pid"; - - public static final String ACTION = "find"; - - public static final String PARAM = "id"; - - private static final Logger log = LogManager.getLogger(); - - @Autowired - private ConverterService converter; - - @Autowired - private Utils utils; - - @Autowired - private DiscoverableEndpointsService discoverableEndpointsService; - - @Override - public void afterPropertiesSet() throws Exception { - discoverableEndpointsService - .register(this, - Arrays.asList( - Link.of( - UriTemplate.of("/api/" + CATEGORY + "/" + ACTION, - new TemplateVariables( - new TemplateVariable(PARAM, VariableType.REQUEST_PARAM))), - CATEGORY))); - } - - @RequestMapping(method = RequestMethod.GET, value = ACTION, params = PARAM) - @SuppressWarnings("unchecked") - public void getDSObyIdentifier(HttpServletRequest request, - HttpServletResponse response, - @RequestParam(PARAM) String id) - throws IOException, SQLException { - - DSpaceObject dso = null; - Context context = ContextUtil.obtainContext(request); - IdentifierService identifierService = IdentifierServiceFactory - .getInstance().getIdentifierService(); - try { - dso = identifierService.resolve(context, id); - if (dso != null) { - DSpaceObjectRest dsor = converter.toRest(dso, utils.obtainProjection()); - URI link = linkTo(dsor.getController(), dsor.getCategory(), - English.plural(dsor.getType())) - .slash(dsor.getId()).toUri(); - response.setStatus(HttpServletResponse.SC_FOUND); - response.sendRedirect(link.toString()); - } else { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - } - } catch (IdentifierNotFoundException e) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - } catch (IdentifierNotResolvableException e) { - response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); - } finally { - context.abort(); - } - } - -} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanRegisterDOIFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanRegisterDOIFeature.java new file mode 100644 index 0000000000..7b1493d56d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanRegisterDOIFeature.java @@ -0,0 +1,59 @@ +/** + * 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.authorization.impl; +import java.sql.SQLException; + +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; +import org.dspace.app.rest.authorization.AuthorizeServiceRestUtil; +import org.dspace.app.rest.model.BaseObjectRest; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.security.DSpaceRestPermission; +import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Can the current user register a DOI for this item? + * + * @author Kim Shepherd + */ +@Component +@AuthorizationFeatureDocumentation(name = CanRegisterDOIFeature.NAME, + description = "It can be used to verify if the user can register a DOI for this item") +public class CanRegisterDOIFeature implements AuthorizationFeature { + + @Autowired + private AuthorizeServiceRestUtil authorizeServiceRestUtil; + @Autowired + private ConfigurationService configurationService; + + public static final String NAME = "canRegisterDOI"; + + @Override + @SuppressWarnings("rawtypes") + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { + // Check configuration to see if this REST operation is allowed + if (!configurationService.getBooleanProperty("identifiers.item-status.register-doi", false)) { + return false; + } + if (object instanceof ItemRest) { + return authorizeServiceRestUtil.authorizeActionBoolean(context, object, DSpaceRestPermission.ADMIN); + } + return false; + } + + @Override + public String[] getSupportedTypes() { + return new String[]{ + ItemRest.CATEGORY + "." + ItemRest.NAME + }; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSubscribeFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSubscribeFeature.java new file mode 100644 index 0000000000..2e0e27b057 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanSubscribeFeature.java @@ -0,0 +1,62 @@ +/** + * 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.authorization.impl; + +import static org.dspace.core.Constants.READ; + +import java.sql.SQLException; +import java.util.Objects; + +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; +import org.dspace.app.rest.model.BaseObjectRest; +import org.dspace.app.rest.model.CollectionRest; +import org.dspace.app.rest.model.CommunityRest; +import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.DSpaceObject; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Checks if the given user can subscribe to a DSpace object + * + * @author Alba Aliu (alba.aliu at atis.al) + */ +@Component +@AuthorizationFeatureDocumentation(name = CanSubscribeFeature.NAME, + description = "Used to verify if the given user can subscribe to a DSpace object") +public class CanSubscribeFeature implements AuthorizationFeature { + + public static final String NAME = "canSubscribeDso"; + + @Autowired + private Utils utils; + @Autowired + private AuthorizeService authorizeService; + + @Override + @SuppressWarnings("rawtypes") + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { + if (Objects.isNull(context.getCurrentUser())) { + return false; + } + DSpaceObject dSpaceObject = (DSpaceObject) utils.getDSpaceAPIObjectFromRest(context, object); + return authorizeService.authorizeActionBoolean(context, context.getCurrentUser(), dSpaceObject, READ, true); + } + + @Override + public String[] getSupportedTypes() { + return new String[]{ + CommunityRest.CATEGORY + "." + CommunityRest.NAME, + CollectionRest.CATEGORY + "." + CollectionRest.NAME + }; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ScoreReviewActionAdvancedInfoConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ScoreReviewActionAdvancedInfoConverter.java new file mode 100644 index 0000000000..44800f6e50 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ScoreReviewActionAdvancedInfoConverter.java @@ -0,0 +1,36 @@ +/** + * 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 org.dspace.app.rest.model.ScoreReviewActionAdvancedInfoRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.xmlworkflow.state.actions.processingaction.ScoreReviewActionAdvancedInfo; + +/** + * This converter is responsible for transforming the model representation of a ScoreReviewActionAdvancedInfo to + * the REST representation of a ScoreReviewActionAdvancedInfo + */ +public class ScoreReviewActionAdvancedInfoConverter + implements DSpaceConverter { + + @Override + public ScoreReviewActionAdvancedInfoRest convert(ScoreReviewActionAdvancedInfo modelObject, + Projection projection) { + ScoreReviewActionAdvancedInfoRest restModel = new ScoreReviewActionAdvancedInfoRest(); + restModel.setDescriptionRequired(modelObject.isDescriptionRequired()); + restModel.setMaxValue(modelObject.getMaxValue()); + restModel.setType(modelObject.getType()); + restModel.setId(modelObject.getId()); + return restModel; + } + + @Override + public Class getModelClass() { + return ScoreReviewActionAdvancedInfo.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SelectReviewerActionAdvancedInfoConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SelectReviewerActionAdvancedInfoConverter.java new file mode 100644 index 0000000000..3dd8f0b3b7 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SelectReviewerActionAdvancedInfoConverter.java @@ -0,0 +1,35 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.converter; + +import org.dspace.app.rest.model.SelectReviewerActionAdvancedInfoRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.xmlworkflow.state.actions.processingaction.SelectReviewerActionAdvancedInfo; + +/** + * This converter is responsible for transforming the model representation of a SelectReviewerActionAdvancedInfo to + * the REST representation of a SelectReviewerActionAdvancedInfo + */ +public class SelectReviewerActionAdvancedInfoConverter + implements DSpaceConverter { + + @Override + public SelectReviewerActionAdvancedInfoRest convert(SelectReviewerActionAdvancedInfo modelObject, + Projection projection) { + SelectReviewerActionAdvancedInfoRest restModel = new SelectReviewerActionAdvancedInfoRest(); + restModel.setGroup(modelObject.getGroup()); + restModel.setType(modelObject.getType()); + restModel.setId(modelObject.getId()); + return restModel; + } + + @Override + public Class getModelClass() { + return SelectReviewerActionAdvancedInfo.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubscriptionConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubscriptionConverter.java new file mode 100644 index 0000000000..cd491ecb17 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubscriptionConverter.java @@ -0,0 +1,59 @@ +/** + * 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 java.util.ArrayList; +import java.util.List; + +import org.dspace.app.rest.model.SubscriptionParameterRest; +import org.dspace.app.rest.model.SubscriptionRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.Utils; +import org.dspace.eperson.Subscription; +import org.dspace.eperson.SubscriptionParameter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * This is the converter from Entity Subscription to the REST data model + * + * @author Alba Aliu at atis.al + * + */ +@Component +public class SubscriptionConverter implements DSpaceConverter { + + @Autowired + protected Utils utils; + + @Override + public SubscriptionRest convert(Subscription subscription, Projection projection) { + SubscriptionRest rest = new SubscriptionRest(); + rest.setProjection(projection); + rest.setId(subscription.getID()); + List subscriptionParameterRestList = new ArrayList<>(); + for (SubscriptionParameter subscriptionParameter : subscription.getSubscriptionParameterList()) { + SubscriptionParameterRest subscriptionParameterRest = new SubscriptionParameterRest(); + subscriptionParameterRest.setName(subscriptionParameter.getName()); + subscriptionParameterRest.setValue(subscriptionParameter.getValue()); + subscriptionParameterRestList.add(subscriptionParameterRest); + } + rest.setSubscriptionParameterList(subscriptionParameterRestList); + rest.setSubscriptionType(subscription.getSubscriptionType()); + return rest; + } + + /* (non-Javadoc) + * @see org.dspace.app.rest.converter.DSpaceConverter#getModelClass() + */ + @Override + public Class getModelClass() { + return Subscription.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SystemWideAlertConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SystemWideAlertConverter.java new file mode 100644 index 0000000000..419f2cf1d1 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SystemWideAlertConverter.java @@ -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.rest.converter; + +import org.dspace.alerts.SystemWideAlert; +import org.dspace.app.rest.model.SystemWideAlertRest; +import org.dspace.app.rest.projection.Projection; +import org.springframework.stereotype.Component; + +/** + * This converter will convert an object of {@Link SystemWideAlert} to an object of {@link SystemWideAlertRest} + */ +@Component +public class SystemWideAlertConverter implements DSpaceConverter { + + + @Override + public SystemWideAlertRest convert(SystemWideAlert systemWideAlert, Projection projection) { + SystemWideAlertRest systemWideAlertRest = new SystemWideAlertRest(); + systemWideAlertRest.setProjection(projection); + systemWideAlertRest.setId(systemWideAlert.getID()); + systemWideAlertRest.setAlertId(systemWideAlert.getID()); + systemWideAlertRest.setMessage(systemWideAlert.getMessage()); + systemWideAlertRest.setAllowSessions(systemWideAlert.getAllowSessions().getValue()); + systemWideAlertRest.setCountdownTo(systemWideAlert.getCountdownTo()); + systemWideAlertRest.setActive(systemWideAlert.isActive()); + return systemWideAlertRest; + } + + @Override + public Class getModelClass() { + return SystemWideAlert.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/WorkflowActionConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/WorkflowActionConverter.java index ee6479433e..f905bbf1b3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/WorkflowActionConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/WorkflowActionConverter.java @@ -26,6 +26,10 @@ public class WorkflowActionConverter implements DSpaceConverter + */ +public class IdentifierRest extends BaseObjectRest implements RestModel { + + // Set names used in component wiring + public static final String NAME = "identifier"; + public static final String PLURAL_NAME = "identifiers"; + private String value; + private String identifierType; + private String identifierStatus; + + // Empty constructor + public IdentifierRest() { + } + + /** + * Constructor that takes a value, type and status for an identifier + * @param value the identifier value eg. https://doi.org/123/234 + * @param identifierType identifier type eg. doi + * @param identifierStatus identifier status eg. TO_BE_REGISTERED + */ + public IdentifierRest(String value, String identifierType, String identifierStatus) { + this.value = value; + this.identifierType = identifierType; + this.identifierStatus = identifierStatus; + } + + /** + * Return name for getType() - this is the section name + * and not the type of identifier, see: identifierType string + * @return + */ + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + @Override + public String getTypePlural() { + return PLURAL_NAME; + } + + /** + * Get the identifier value eg full DOI URL + * @return identifier value eg. https://doi.org/123/234 + */ + public String getValue() { + return value; + } + + /** + * Set the identifier value + * @param value identifier value, eg. https://doi.org/123/234 + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Get type of identifier eg 'doi' or 'handle' + * @return type string + */ + public String getIdentifierType() { + return identifierType; + } + + /** + * Set type of identifier + * @param identifierType type string eg 'doi' + */ + public void setIdentifierType(String identifierType) { + this.identifierType = identifierType; + } + + /** + * Get status of identifier, if relevant + * @return identifierStatus eg. null or TO_BE_REGISTERED + */ + public String getIdentifierStatus() { + return identifierStatus; + } + + /** + * Set status of identifier, if relevant + * @param identifierStatus eg. null or TO_BE_REGISTERED + */ + public void setIdentifierStatus(String identifierStatus) { + this.identifierStatus = identifierStatus; + } + + @Override + public String getCategory() { + return "pid"; + } + + @Override + public String getId() { + return getValue(); + } + + @Override + public Class getController() { + return RestResourceController.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/IdentifiersRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/IdentifiersRest.java new file mode 100644 index 0000000000..169e40979c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/IdentifiersRest.java @@ -0,0 +1,57 @@ +/** + * 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 java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Implementation of IdentifierRest REST resource, representing a list of all identifiers + * for use with the REST API + * + * @author Kim Shepherd + */ +public class IdentifiersRest extends BaseObjectRest { + + // Set names used in component wiring + public static final String NAME = "identifiers"; + private List identifiers; + + // Empty constructor + public IdentifiersRest() { + identifiers = new ArrayList<>(); + } + + // Return name for getType() + // Note this is the section name, NOT the identifier type + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + public List getIdentifiers() { + return identifiers; + } + + public void setIdentifiers(List identifiers) { + this.identifiers = identifiers; + } + + @Override + public String getCategory() { + return null; + } + + @Override + public Class getController() { + return null; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemRest.java index 63004b68d2..1254ef8f93 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemRest.java @@ -25,6 +25,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; name = ItemRest.BUNDLES, method = "getBundles" ), + @LinkRest( + name = ItemRest.IDENTIFIERS, + method = "getIdentifiers" + ), @LinkRest( name = ItemRest.MAPPED_COLLECTIONS, method = "getMappedCollections" @@ -57,6 +61,7 @@ public class ItemRest extends DSpaceObjectRest { public static final String ACCESS_STATUS = "accessStatus"; public static final String BUNDLES = "bundles"; + public static final String IDENTIFIERS = "identifiers"; public static final String MAPPED_COLLECTIONS = "mappedCollections"; public static final String OWNING_COLLECTION = "owningCollection"; public static final String RELATIONSHIPS = "relationships"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ScoreReviewActionAdvancedInfoRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ScoreReviewActionAdvancedInfoRest.java new file mode 100644 index 0000000000..14644be151 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ScoreReviewActionAdvancedInfoRest.java @@ -0,0 +1,35 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.model; + +/** + * The ScoreReviewActionAdvancedInfo REST Resource, + * see {@link org.dspace.xmlworkflow.state.actions.processingaction.ScoreReviewActionAdvancedInfo} + */ +public class ScoreReviewActionAdvancedInfoRest extends AdvancedInfoRest { + + private boolean descriptionRequired; + private int maxValue; + + public boolean isDescriptionRequired() { + return descriptionRequired; + } + + public void setDescriptionRequired(boolean descriptionRequired) { + this.descriptionRequired = descriptionRequired; + } + + public int getMaxValue() { + return maxValue; + } + + public void setMaxValue(int maxValue) { + this.maxValue = maxValue; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SelectReviewerActionAdvancedInfoRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SelectReviewerActionAdvancedInfoRest.java new file mode 100644 index 0000000000..86b2003b07 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SelectReviewerActionAdvancedInfoRest.java @@ -0,0 +1,25 @@ +/** + * 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; + +/** + * The SelectReviewerActionAdvancedInfoRest REST Resource, + * see {@link org.dspace.xmlworkflow.state.actions.processingaction.SelectReviewerActionAdvancedInfo} + */ +public class SelectReviewerActionAdvancedInfoRest extends AdvancedInfoRest { + + private String groupId; + + public String getGroup() { + return groupId; + } + + public void setGroup(String groupId) { + this.groupId = groupId; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubscriptionParameterRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubscriptionParameterRest.java new file mode 100644 index 0000000000..ab815f0d3b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubscriptionParameterRest.java @@ -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.rest.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.dspace.eperson.Subscription; + +/** + * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) + */ +public class SubscriptionParameterRest { + + @JsonIgnore + private Integer id; + private String name; + private String value; + + public SubscriptionParameterRest() {} + + public SubscriptionParameterRest(Integer id, String name, String value, Subscription subscription) { + this.id = id; + this.name = name; + this.value = value; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubscriptionRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubscriptionRest.java new file mode 100644 index 0000000000..78a81c38b1 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubscriptionRest.java @@ -0,0 +1,74 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.model; + +import java.util.ArrayList; +import java.util.List; + +import org.dspace.app.rest.RestResourceController; + +@LinksRest(links = { + @LinkRest(name = SubscriptionRest.DSPACE_OBJECT, method = "getDSpaceObject"), + @LinkRest(name = SubscriptionRest.EPERSON, method = "getEPerson") +}) +public class SubscriptionRest extends BaseObjectRest { + + private static final long serialVersionUID = 1L; + + public static final String NAME = "subscription"; + public static final String NAME_PLURAL = "subscriptions"; + public static final String CATEGORY = "core"; + public static final String DSPACE_OBJECT = "resource"; + public static final String EPERSON = "eperson"; + + private Integer id; + private String subscriptionType; + private List subscriptionParameterList = new ArrayList<>(); + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + @Override + public String getType() { + return NAME; + } + + public void setSubscriptionType(String type) { + this.subscriptionType = type; + } + + public List getSubscriptionParameterList() { + return subscriptionParameterList; + } + + public void setSubscriptionParameterList(List subscriptionParameterList) { + this.subscriptionParameterList = subscriptionParameterList; + } + + public String getSubscriptionType() { + return this.subscriptionType; + } + + @Override + public Integer getId() { + return id; + } + + @Override + public void setId(Integer id) { + this.id = id; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SystemWideAlertRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SystemWideAlertRest.java new file mode 100644 index 0000000000..995ec8e934 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SystemWideAlertRest.java @@ -0,0 +1,88 @@ +/** + * 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 java.util.Date; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.dspace.alerts.SystemWideAlert; +import org.dspace.app.rest.RestResourceController; + +/** + * This class serves as a REST representation for the {@link SystemWideAlert} class + */ +public class SystemWideAlertRest extends BaseObjectRest { + public static final String NAME = "systemwidealert"; + public static final String CATEGORY = RestAddressableModel.SYSTEM; + + public String getCategory() { + return CATEGORY; + } + + public Class getController() { + return RestResourceController.class; + } + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + private Integer alertId; + private String message; + private String allowSessions; + private Date countdownTo; + private boolean active; + + public Integer getAlertId() { + return alertId; + } + + public void setAlertId(final Integer alertID) { + this.alertId = alertID; + } + + public String getMessage() { + return message; + } + + public void setMessage(final String message) { + this.message = message; + } + + public String getAllowSessions() { + return allowSessions; + } + + public void setAllowSessions(final String allowSessions) { + this.allowSessions = allowSessions; + } + + public Date getCountdownTo() { + return countdownTo; + } + + public void setCountdownTo(final Date countdownTo) { + this.countdownTo = countdownTo; + } + + public boolean isActive() { + return active; + } + + public void setActive(final boolean active) { + this.active = active; + } + + @JsonIgnore + @Override + public Integer getId() { + return id; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowActionRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowActionRest.java index e998df6bc2..07a2c36cff 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowActionRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowActionRest.java @@ -9,7 +9,10 @@ package org.dspace.app.rest.model; import java.util.List; +import com.fasterxml.jackson.annotation.JsonInclude; +import org.apache.commons.collections4.CollectionUtils; import org.dspace.app.rest.RestResourceController; +import org.dspace.xmlworkflow.state.actions.ActionAdvancedInfo; /** * The rest resource used for workflow actions @@ -23,6 +26,8 @@ public class WorkflowActionRest extends BaseObjectRest { public static final String NAME_PLURAL = "workflowactions"; private List options; + private List advancedOptions; + private List advancedInfo; @Override public String getCategory() { @@ -39,21 +44,33 @@ public class WorkflowActionRest extends BaseObjectRest { return NAME; } - /** - * Generic getter for the options - * - * @return the options value of this WorkflowActionRest - */ public List getOptions() { return options; } - /** - * Generic setter for the options - * - * @param options The options to be set on this WorkflowActionRest - */ public void setOptions(List options) { this.options = options; } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public List getAdvancedOptions() { + return advancedOptions; + } + + public void setAdvancedOptions(List advancedOptions) { + this.advancedOptions = advancedOptions; + } + + public boolean getAdvanced() { + return CollectionUtils.isNotEmpty(getAdvancedOptions()); + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public List getAdvancedInfo() { + return advancedInfo; + } + + public void setAdvancedInfo(List advancedInfo) { + this.advancedInfo = advancedInfo; + } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/IdentifierResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/IdentifierResource.java new file mode 100644 index 0000000000..e25f04f6c9 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/IdentifierResource.java @@ -0,0 +1,25 @@ +/** + * 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.IdentifierRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * + * Simple HAL wrapper for IdentifierRest model + * + * @author Kim Shepherd + */ +@RelNameDSpaceResource(IdentifierRest.NAME) +public class IdentifierResource extends DSpaceResource { + public IdentifierResource(IdentifierRest model, Utils utils) { + super(model, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/IdentifiersResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/IdentifiersResource.java new file mode 100644 index 0000000000..2c6453d8cb --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/IdentifiersResource.java @@ -0,0 +1,24 @@ +/** + * 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.IdentifiersRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * Boilerplate hateos resource for IdentifiersRest + * + * @author Kim Shepherd + */ +@RelNameDSpaceResource(IdentifiersRest.NAME) +public class IdentifiersResource extends HALResource { + public IdentifiersResource(IdentifiersRest data, Utils utils) { + super(data); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubscriptionResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubscriptionResource.java new file mode 100644 index 0000000000..2e7ea1618a --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubscriptionResource.java @@ -0,0 +1,22 @@ +/** + * 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.SubscriptionRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * The Resource representation of a Subscription object + */ +@RelNameDSpaceResource(SubscriptionRest.NAME) +public class SubscriptionResource extends DSpaceResource { + public SubscriptionResource(SubscriptionRest data, Utils utils) { + super(data, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SystemWideAlertResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SystemWideAlertResource.java new file mode 100644 index 0000000000..9089103d2b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SystemWideAlertResource.java @@ -0,0 +1,23 @@ +/** + * 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.alerts.SystemWideAlert; +import org.dspace.app.rest.model.SystemWideAlertRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * The Resource representation of a {@link SystemWideAlert} object + */ +@RelNameDSpaceResource(SystemWideAlertRest.NAME) +public class SystemWideAlertResource extends DSpaceResource { + public SystemWideAlertResource(SystemWideAlertRest content, Utils utils) { + super(content, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataIdentifiers.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataIdentifiers.java new file mode 100644 index 0000000000..01e0eabdd3 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataIdentifiers.java @@ -0,0 +1,63 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.model.step; + +import java.util.ArrayList; +import java.util.List; + +import org.dspace.app.rest.model.IdentifierRest; +/** + * Java bean with basic DOI / Handle / other identifier data for + * display in submission step + * + * @author Kim Shepherd (kim@shepherd.nz) + */ +public class DataIdentifiers implements SectionData { + // Map of identifier types and values + List identifiers; + // Types to display, a hint for te UI + List displayTypes; + + public DataIdentifiers() { + identifiers = new ArrayList<>(); + displayTypes = new ArrayList<>(); + } + + public List getIdentifiers() { + return identifiers; + } + + public void setIdentifiers(List identifiers) { + this.identifiers = identifiers; + } + + public void addIdentifier(String type, String value, String status) { + IdentifierRest identifier = new IdentifierRest(); + identifier.setValue(value); + identifier.setIdentifierType(type); + identifier.setIdentifierStatus(status); + this.identifiers.add(identifier); + } + + public List getDisplayTypes() { + return displayTypes; + } + + public void setDisplayTypes(List displayTypes) { + this.displayTypes = displayTypes; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + for (IdentifierRest identifier : identifiers) { + sb.append(identifier.getType()).append(": ").append(identifier.getValue()).append("\n"); + } + return sb.toString(); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/IdentifierRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/IdentifierRestRepository.java new file mode 100644 index 0000000000..1be569d18e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/IdentifierRestRepository.java @@ -0,0 +1,306 @@ +/** + * 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 static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; + +import java.io.IOException; +import java.net.URI; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.NotSupportedException; + +import org.atteo.evo.inflector.English; +import org.dspace.app.rest.DiscoverableEndpointsService; +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.LinkNotFoundException; +import org.dspace.app.rest.exception.RESTAuthorizationException; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.DSpaceObjectRest; +import org.dspace.app.rest.model.IdentifierRest; +import org.dspace.app.rest.repository.handler.service.UriListHandlerService; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.logic.TrueFilter; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.handle.service.HandleService; +import org.dspace.identifier.DOI; +import org.dspace.identifier.DOIIdentifierProvider; +import org.dspace.identifier.IdentifierException; +import org.dspace.identifier.IdentifierNotFoundException; +import org.dspace.identifier.IdentifierNotResolvableException; +import org.dspace.identifier.factory.IdentifierServiceFactory; +import org.dspace.identifier.service.DOIService; +import org.dspace.identifier.service.IdentifierService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.TemplateVariable; +import org.springframework.hateoas.TemplateVariables; +import org.springframework.hateoas.UriTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Item REST Repository and Controller for persistent identifiers. + * The controller annotation and endpoint registration allows the "find DSO by identifier" method which was + * previously implmented in org.dspace.app.rest.IdentifierRestController + * + * @author Kim Shepherd + */ +@RestController +@RequestMapping("/api/" + IdentifierRestRepository.CATEGORY) +@Component(IdentifierRestRepository.CATEGORY + "." + IdentifierRestRepository.NAME) +public class IdentifierRestRepository extends DSpaceRestRepository implements InitializingBean { + @Autowired + private DiscoverableEndpointsService discoverableEndpointsService; + @Autowired + private UriListHandlerService uriListHandlerService; + @Autowired + private DOIService doiService; + @Autowired + private HandleService handleService; + @Autowired + private ItemService itemService; + + // Set category and name for routing + public static final String CATEGORY = "pid"; + public static final String NAME = IdentifierRest.NAME; + + /** + * Register /api/pid/find?id=... as a discoverable endpoint service + * + * @throws Exception + */ + @Override + public void afterPropertiesSet() throws Exception { + discoverableEndpointsService + .register(this, + Arrays.asList(Link.of(UriTemplate.of("/api/pid/find", + new TemplateVariables( + new TemplateVariable("id", + TemplateVariable.VariableType.REQUEST_PARAM))), + CATEGORY))); + } + + /** + * Find all identifiers. Not implemented. + * @param context + * the dspace context + * @param pageable + * object embedding the requested pagination info + * @return + */ + @PreAuthorize("permitAll()") + @Override + public Page findAll(Context context, Pageable pageable) { + throw new RepositoryMethodNotImplementedException(IdentifierRest.NAME, "findAll"); + } + + /** + * Find the identifier object for a given identifier string (eg. doi). + * Not implemented -- Tomcat interprets %2F as path separators which means + * parameters are a safer way to handle these operations + * + * @param context + * the dspace context + * @param identifier + * the rest object id + * @return + */ + @PreAuthorize("permitAll()") + @Override + public IdentifierRest findOne(Context context, String identifier) { + throw new RepositoryMethodNotImplementedException(IdentifierRest.NAME, "findOne"); + } + + /** + * Find identifiers associated with a given item + * @param uuid + * @param pageable + * @return + */ + @SearchRestMethod(name = "findByItem") + @PreAuthorize("permitAll()") + public Page findByItem(@Parameter(value = "uuid", required = true) + String uuid, Pageable pageable) { + Context context = obtainContext(); + List results = new ArrayList<>(); + try { + DSpaceObject dso = itemService.find(context, UUID.fromString(uuid)); + String handle = dso.getHandle(); + DOI doi = doiService.findDOIByDSpaceObject(context, dso); + if (doi != null) { + String doiUrl = doiService.DOIToExternalForm(doi.getDoi()); + results.add(new IdentifierRest(doiUrl, "doi", DOIIdentifierProvider.statusText[doi.getStatus()])); + } + if (handle != null) { + String handleUrl = handleService.getCanonicalForm(handle); + results.add(new IdentifierRest(handleUrl, "handle", null)); + } + } catch (SQLException | IdentifierException e) { + throw new LinkNotFoundException(IdentifierRestRepository.CATEGORY, IdentifierRest.NAME, uuid); + } + // Return list of identifiers for this DSpaceObject + return new PageImpl<>(results, pageable, results.size()); + } + + /** + * Create (mint / queue for registration) a new persistent identifier of a given type (eg DOI) for an item + * Currently, the only supported identifier type for this operation is "doi" + * + * @param context + * the dspace context + * @param list + * A uri-list with the item URI for which to create an identifier + * @return 201 Created with object JSON on success + * @throws AuthorizeException + * @throws SQLException + * @throws RepositoryMethodNotImplementedException + */ + @Override + protected IdentifierRest createAndReturn(Context context, List list) + throws AuthorizeException, SQLException, RepositoryMethodNotImplementedException { + HttpServletRequest request = getRequestService().getCurrentRequest().getHttpServletRequest(); + // Extract 'type' from request + String type = request.getParameter("type"); + if (!"doi".equals(type)) { + throw new NotSupportedException("Only identifiers of type 'doi' are supported"); + } + IdentifierRest identifierRest = new IdentifierRest(); + try { + Item item = uriListHandlerService.handle(context, request, list, Item.class); + if (item == null) { + throw new UnprocessableEntityException( + "No DSpace Item found, the uri-list does not contain a valid resource"); + } + // Does this item have a DOI already? If the DOI doesn't exist or has a null, MINTED or PENDING status + // then we proceed with a typical create operation and return 201 success with the object + DOI doi = doiService.findDOIByDSpaceObject(context, item); + if (doi == null || null == doi.getStatus() || DOIIdentifierProvider.MINTED.equals(doi.getStatus()) + || DOIIdentifierProvider.PENDING.equals(doi.getStatus())) { + // Proceed with creation + // Register things + identifierRest = registerDOI(context, item); + } else { + // Return bad request exception, as per other createAndReturn implementations (eg EPerson) + throw new DSpaceBadRequestException("The DOI is already registered or queued to be registered"); + } + } catch (AuthorizeException e) { + throw new RESTAuthorizationException(e); + } + return identifierRest; + } + + /** + * Perform DOI registration, skipping any other filters used. + * + * @param context + * @param item + * @return + * @throws SQLException + * @throws AuthorizeException + */ + private IdentifierRest registerDOI(Context context, Item item) + throws SQLException, AuthorizeException { + String identifier = null; + IdentifierRest identifierRest = new IdentifierRest(); + identifierRest.setIdentifierType("doi"); + try { + DOIIdentifierProvider doiIdentifierProvider = DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName("org.dspace.identifier.DOIIdentifierProvider", DOIIdentifierProvider.class); + if (doiIdentifierProvider != null) { + String doiValue = doiIdentifierProvider.register(context, item, new TrueFilter()); + identifierRest.setValue(doiValue); + // Get new status + DOI doi = doiService.findByDoi(context, doiValue); + if (doi != null) { + identifierRest.setIdentifierStatus(DOIIdentifierProvider.statusText[doi.getStatus()]); + } + } else { + throw new IllegalStateException("No DOI provider is configured"); + } + } catch (IdentifierException e) { + throw new IllegalStateException("Failed to register identifier: " + identifier); + } + // We didn't exactly change the item, but we did queue an identifier which is closely associated with it, + // so we should update the last modified date here + itemService.updateLastModified(context, item); + context.complete(); + return identifierRest; + } + + + /** + * Redirect to a DSO page, given an identifier + * + * @param request HTTP request + * @param response HTTP response + * @param id The persistent identifier (eg. handle, DOI) to search for + * @throws IOException + * @throws SQLException + */ + @RequestMapping(method = RequestMethod.GET, value = "find", params = "id") + @SuppressWarnings("unchecked") + public void getDSObyIdentifier(HttpServletRequest request, + HttpServletResponse response, + @RequestParam("id") String id) + throws IOException, SQLException { + + DSpaceObject dso; + Context context = ContextUtil.obtainContext(request); + IdentifierService identifierService = IdentifierServiceFactory + .getInstance().getIdentifierService(); + try { + // Resolve identifier to a DSpace object + dso = identifierService.resolve(context, id); + if (dso != null) { + // Convert and respond with a redirect to the object itself + DSpaceObjectRest dsor = converter.toRest(dso, utils.obtainProjection()); + URI link = linkTo(dsor.getController(), dsor.getCategory(), + English.plural(dsor.getType())) + .slash(dsor.getId()).toUri(); + response.setStatus(HttpServletResponse.SC_FOUND); + response.sendRedirect(link.toString()); + } else { + // No object could be found + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } catch (IdentifierNotFoundException e) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } catch (IdentifierNotResolvableException e) { + response.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED); + } finally { + context.abort(); + } + } + + @Override + public Class getDomainClass() { + return IdentifierRest.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemIdentifierLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemIdentifierLinkRepository.java new file mode 100644 index 0000000000..0714b7329b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemIdentifierLinkRepository.java @@ -0,0 +1,84 @@ +/** + * 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.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.IdentifierRest; +import org.dspace.app.rest.model.IdentifiersRest; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.content.Item; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.handle.factory.HandleServiceFactory; +import org.dspace.handle.service.HandleService; +import org.dspace.identifier.DOI; +import org.dspace.identifier.DOIIdentifierProvider; +import org.dspace.identifier.IdentifierException; +import org.dspace.identifier.service.DOIService; +import org.dspace.identifier.service.IdentifierService; +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 the identifier of an Item + */ +@Component(ItemRest.CATEGORY + "." + ItemRest.NAME + "." + ItemRest.IDENTIFIERS) +public class ItemIdentifierLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + @Autowired + ItemService itemService; + + @Autowired + IdentifierService identifierService; + + @Autowired + DOIService doiService; + @Autowired + HandleService handleService; + + @PreAuthorize("hasPermission(#itemId, 'ITEM', 'READ')") + public IdentifiersRest getIdentifiers(@Nullable HttpServletRequest request, + UUID itemId, + @Nullable Pageable optionalPageable, + Projection projection) throws SQLException { + Context context = ContextUtil.obtainCurrentRequestContext(); + Item item = itemService.find(context, itemId); + if (item == null) { + throw new ResourceNotFoundException("Could not find item with id " + itemId); + } + IdentifiersRest identifiersRest = new IdentifiersRest(); + List identifierRestList = new ArrayList<>(); + DOI doi = doiService.findDOIByDSpaceObject(context, item); + String handle = HandleServiceFactory.getInstance().getHandleService().findHandle(context, item); + try { + if (doi != null) { + String doiUrl = doiService.DOIToExternalForm(doi.getDoi()); + identifierRestList.add(new IdentifierRest( + doiUrl, "doi", DOIIdentifierProvider.statusText[doi.getStatus()])); + } + if (handle != null) { + identifierRestList.add(new IdentifierRest(handleService.getCanonicalForm(handle), "handle", null)); + } + } catch (IdentifierException e) { + throw new IllegalStateException("Failed to register identifier: " + e.getMessage()); + } + identifiersRest.setIdentifiers(identifierRestList); + return identifiersRest; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java index 13d5a560dc..d974a6d78a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ScriptRestRepository.java @@ -80,7 +80,8 @@ public class ScriptRestRepository extends DSpaceRestRepository findAll(Context context, Pageable pageable) { - List scriptConfigurations = scriptService.getScriptConfigurations(context); + List scriptConfigurations = + scriptService.getScriptConfigurations(context); return converter.toRestPage(scriptConfigurations, pageable, utils.obtainProjection()); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubscriptionDSpaceObjectLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubscriptionDSpaceObjectLinkRepository.java new file mode 100644 index 0000000000..95c4714e9c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubscriptionDSpaceObjectLinkRepository.java @@ -0,0 +1,49 @@ +/** + * 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.Objects; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.DSpaceObjectRest; +import org.dspace.app.rest.model.SubscriptionRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.eperson.Subscription; +import org.dspace.eperson.service.SubscribeService; +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 "DSpaceObject" of subscription + */ +@Component(SubscriptionRest.CATEGORY + "." + SubscriptionRest.NAME + "." + SubscriptionRest.DSPACE_OBJECT) +public class SubscriptionDSpaceObjectLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private SubscribeService subscribeService; + + @PreAuthorize("hasPermission(#subscriptionId, 'subscription', 'READ')") + public DSpaceObjectRest getDSpaceObject(@Nullable HttpServletRequest request, Integer subscriptionId, + @Nullable Pageable optionalPageable, Projection projection) { + try { + Subscription subscription = subscribeService.findById(obtainContext(), subscriptionId); + if (Objects.isNull(subscription)) { + throw new ResourceNotFoundException("No such subscription: " + subscriptionId); + } + return converter.toRest(subscription.getDSpaceObject(), projection); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubscriptionEPersonLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubscriptionEPersonLinkRepository.java new file mode 100644 index 0000000000..dcf612e52d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubscriptionEPersonLinkRepository.java @@ -0,0 +1,49 @@ +/** + * 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.Objects; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.EPersonRest; +import org.dspace.app.rest.model.SubscriptionRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.eperson.Subscription; +import org.dspace.eperson.service.SubscribeService; +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" of subscription + */ +@Component(SubscriptionRest.CATEGORY + "." + SubscriptionRest.NAME + "." + SubscriptionRest.EPERSON) +public class SubscriptionEPersonLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private SubscribeService subscribeService; + + @PreAuthorize("hasPermission(#subscriptionId, 'subscription', 'READ')") + public EPersonRest getEPerson(@Nullable HttpServletRequest request, Integer subscriptionId, + @Nullable Pageable optionalPageable, Projection projection) { + try { + Subscription subscription = subscribeService.findById(obtainContext(), subscriptionId); + if (Objects.isNull(subscription)) { + throw new ResourceNotFoundException("No such subscription: " + subscriptionId); + } + return converter.toRest(subscription.getEPerson(), projection); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubscriptionRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubscriptionRestRepository.java new file mode 100644 index 0000000000..ce1bcff11f --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubscriptionRestRepository.java @@ -0,0 +1,284 @@ +/** + * 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 static org.dspace.app.rest.model.SubscriptionRest.CATEGORY; +import static org.dspace.app.rest.model.SubscriptionRest.NAME; +import static org.dspace.core.Constants.COLLECTION; +import static org.dspace.core.Constants.COMMUNITY; +import static org.dspace.core.Constants.READ; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.DiscoverableEndpointsService; +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.converter.ConverterService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.SubscriptionParameterRest; +import org.dspace.app.rest.model.SubscriptionRest; +import org.dspace.app.rest.utils.DSpaceObjectUtils; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.DSpaceObject; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.FrequencyType; +import org.dspace.eperson.Subscription; +import org.dspace.eperson.SubscriptionParameter; +import org.dspace.eperson.service.EPersonService; +import org.dspace.eperson.service.SubscribeService; +import org.dspace.subscriptions.SubscriptionEmailNotificationService; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.hateoas.Link; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * This is the repository responsible to manage SubscriptionRest object + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +@Component(SubscriptionRest.CATEGORY + "." + SubscriptionRest.NAME) +public class SubscriptionRestRepository extends DSpaceRestRepository + implements InitializingBean { + + @Autowired + private ConverterService converter; + @Autowired + private EPersonService ePersonService; + @Autowired + private AuthorizeService authorizeService; + @Autowired + private SubscribeService subscribeService; + @Autowired + private DSpaceObjectUtils dspaceObjectUtil; + @Autowired + private DiscoverableEndpointsService discoverableEndpointsService; + @Autowired + private SubscriptionEmailNotificationService subscriptionEmailNotificationService; + + @Override + @PreAuthorize("hasPermission(#id, 'subscription', 'READ')") + public SubscriptionRest findOne(Context context, Integer id) { + Subscription subscription = null; + try { + subscription = subscribeService.findById(context, id); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return Objects.isNull(subscription) ? null : converter.toRest(subscription, utils.obtainProjection()); + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public Page findAll(Context context, Pageable pageable) { + try { + List subscriptionList = subscribeService.findAll(context, null, + Math.toIntExact(pageable.getPageSize()), + Math.toIntExact(pageable.getOffset())); + Long total = subscribeService.countAll(context); + return converter.toRestPage(subscriptionList, pageable, total, utils.obtainProjection()); + } catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @SearchRestMethod(name = "findByEPerson") + @PreAuthorize("hasPermission(#epersonId, 'EPERSON', 'READ')") + public Page findSubscriptionsByEPerson(@Parameter(value = "uuid", required = true) UUID epersonId, + Pageable pageable) throws Exception { + Long total = null; + List subscriptions = null; + try { + Context context = obtainContext(); + EPerson ePerson = ePersonService.find(context, epersonId); + subscriptions = subscribeService.findSubscriptionsByEPerson(context, ePerson, + Math.toIntExact(pageable.getPageSize()), + Math.toIntExact(pageable.getOffset())); + total = subscribeService.countSubscriptionsByEPerson(context, ePerson); + } catch (SQLException e) { + throw new SQLException(e.getMessage(), e); + } + return converter.toRestPage(subscriptions, pageable, total, utils.obtainProjection()); + } + + @SearchRestMethod(name = "findByEPersonAndDso") + @PreAuthorize("hasPermission(#epersonId, 'EPERSON', 'READ')") + public Page findByEPersonAndDso(@Parameter(value = "eperson_id", required = true) UUID epersonId, + @Parameter(value = "resource",required = true) UUID dsoId, + Pageable pageable) throws Exception { + Long total = null; + List subscriptions = null; + try { + Context context = obtainContext(); + DSpaceObject dSpaceObject = dspaceObjectUtil.findDSpaceObject(context, dsoId); + EPerson ePerson = ePersonService.find(context, epersonId); + subscriptions = subscribeService.findSubscriptionsByEPersonAndDso(context, ePerson, dSpaceObject, + Math.toIntExact(pageable.getPageSize()), + Math.toIntExact(pageable.getOffset())); + total = subscribeService.countByEPersonAndDSO(context, ePerson, dSpaceObject); + } catch (SQLException e) { + throw new SQLException(e.getMessage(), e); + } + return converter.toRestPage(subscriptions, pageable, total, utils.obtainProjection()); + } + + @Override + @PreAuthorize("isAuthenticated()") + protected SubscriptionRest createAndReturn(Context context) throws SQLException, AuthorizeException { + HttpServletRequest req = getRequestService().getCurrentRequest().getHttpServletRequest(); + String epersonId = req.getParameter("eperson_id"); + String dsoId = req.getParameter("resource"); + + if (StringUtils.isBlank(dsoId) || StringUtils.isBlank(epersonId)) { + throw new UnprocessableEntityException("Both eperson than DSpaceObject uuids must be provieded!"); + } + + try { + DSpaceObject dSpaceObject = dspaceObjectUtil.findDSpaceObject(context, UUID.fromString(dsoId)); + EPerson ePerson = ePersonService.findByIdOrLegacyId(context, epersonId); + if (Objects.isNull(ePerson) || Objects.isNull(dSpaceObject)) { + throw new DSpaceBadRequestException("Id of person or dspace object must represents reals ids"); + } + + // user must have read permissions to dSpaceObject object + if (!authorizeService.authorizeActionBoolean(context, ePerson, dSpaceObject, READ, true)) { + throw new AuthorizeException("The user has not READ rights on this DSO"); + } + + // if user is Admin don't make this control, + // otherwise make this control because normal user can only subscribe with their own ID of user. + if (!authorizeService.isAdmin(context)) { + if (!ePerson.equals(context.getCurrentUser())) { + throw new AuthorizeException("Only administrator can subscribe for other persons"); + } + } + + if (dSpaceObject.getType() == COMMUNITY || dSpaceObject.getType() == COLLECTION) { + Subscription subscription = null; + ServletInputStream input = req.getInputStream(); + SubscriptionRest subscriptionRest = new ObjectMapper().readValue(input, SubscriptionRest.class); + List subscriptionParameterList = subscriptionRest + .getSubscriptionParameterList(); + if (CollectionUtils.isNotEmpty(subscriptionParameterList)) { + List subscriptionParameters = new ArrayList<>(); + validateParameters(subscriptionRest, subscriptionParameterList, subscriptionParameters); + subscription = subscribeService.subscribe(context, ePerson, dSpaceObject, subscriptionParameters, + subscriptionRest.getSubscriptionType()); + } + context.commit(); + return converter.toRest(subscription, utils.obtainProjection()); + } else { + throw new DSpaceBadRequestException( + "Currently subscription is supported only for Community and Collection"); + } + } catch (SQLException sqlException) { + throw new SQLException(sqlException.getMessage(), sqlException); + } catch (IOException ioException) { + throw new UnprocessableEntityException("error parsing the body"); + } + } + + private void validateParameters(SubscriptionRest subscriptionRest, + List subscriptionParameterList, + List subscriptionParameters) { + for (SubscriptionParameterRest subscriptionParameterRest : subscriptionParameterList) { + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + var name = subscriptionParameterRest.getName(); + var value = subscriptionParameterRest.getValue(); + if (!StringUtils.equals("frequency", name) || !FrequencyType.isSupportedFrequencyType(value)) { + throw new UnprocessableEntityException("Provided SubscriptionParameter name:" + name + + " or value: " + value + " is not supported!"); + } + subscriptionParameter.setName(name); + subscriptionParameter.setValue(value); + subscriptionParameters.add(subscriptionParameter); + } + + var type = subscriptionRest.getSubscriptionType(); + if (!subscriptionEmailNotificationService.getSupportedSubscriptionTypes().contains(type)) { + throw new UnprocessableEntityException("Provided subscriptionType:" + type + " is not supported!" + + " Must be one of: " + subscriptionEmailNotificationService.getSupportedSubscriptionTypes()); + } + } + + @Override + @PreAuthorize("hasPermission(#id, 'subscription', 'WRITE')") + protected SubscriptionRest put(Context context, HttpServletRequest request, String apiCategory, String model, + Integer id, JsonNode jsonNode) throws SQLException { + + SubscriptionRest subscriptionRest; + try { + subscriptionRest = new ObjectMapper().readValue(jsonNode.toString(), SubscriptionRest.class); + } catch (IOException e) { + throw new UnprocessableEntityException("Error parsing subscription json: " + e.getMessage(), e); + } + + Subscription subscription = subscribeService.findById(context, id); + if (Objects.isNull(subscription)) { + throw new ResourceNotFoundException(apiCategory + "." + model + " with id: " + id + " not found"); + } + + if (id.equals(subscription.getID())) { + List subscriptionParameters = new ArrayList<>(); + List subscriptionParameterList = subscriptionRest.getSubscriptionParameterList(); + validateParameters(subscriptionRest, subscriptionParameterList, subscriptionParameters); + subscription = subscribeService.updateSubscription(context, id, subscriptionRest.getSubscriptionType(), + subscriptionParameters); + context.commit(); + return converter.toRest(subscription, utils.obtainProjection()); + } else { + throw new IllegalArgumentException("The id in the Json and the id in the url do not match: " + id + ", " + + subscription.getID()); + } + } + + @Override + @PreAuthorize("hasPermission(#id, 'subscription', 'DELETE')") + protected void delete(Context context, Integer id) { + try { + Subscription subscription = subscribeService.findById(context, id); + if (Objects.isNull(subscription)) { + throw new ResourceNotFoundException(CATEGORY + "." + NAME + " with id: " + id + " not found"); + } + subscribeService.deleteSubscription(context, subscription); + } catch (SQLException e) { + throw new RuntimeException("Unable to delete Subscription with id = " + id, e); + } + } + + @Override + public Class getDomainClass() { + return SubscriptionRest.class; + } + + @Override + public void afterPropertiesSet() throws Exception { + discoverableEndpointsService.register(this, Arrays.asList(Link.of("/api/" + SubscriptionRest.CATEGORY + + "/" + SubscriptionRest.NAME_PLURAL + "/search", SubscriptionRest.NAME_PLURAL + "-search"))); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SystemWideAlertRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SystemWideAlertRestRepository.java new file mode 100644 index 0000000000..73544145b2 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SystemWideAlertRestRepository.java @@ -0,0 +1,208 @@ +/** + * 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 static org.apache.commons.lang3.StringUtils.isBlank; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.List; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.alerts.AllowSessionsEnum; +import org.dspace.alerts.SystemWideAlert; +import org.dspace.alerts.service.SystemWideAlertService; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.SystemWideAlertRest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +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.stereotype.Component; + +/** + * The repository for the SystemWideAlert workload + */ +@Component(SystemWideAlertRest.CATEGORY + "." + SystemWideAlertRest.NAME) +public class SystemWideAlertRestRepository extends DSpaceRestRepository { + + private static final Logger log = LogManager.getLogger(); + + @Autowired + private SystemWideAlertService systemWideAlertService; + + @Autowired + private AuthorizeService authorizeService; + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + protected SystemWideAlertRest createAndReturn(Context context) throws SQLException, AuthorizeException { + SystemWideAlert systemWideAlert = createSystemWideAlert(context); + return converter.toRest(systemWideAlert, utils.obtainProjection()); + } + + + /** + * This method will retrieve the system-wide alert for the provided ID + * However, only admins will be able to retrieve the inactive alerts. Non-admin users will only be able to retrieve + * active alerts. This is necessary also to be able to return the results through the search endpoint, since the + * PreAuthorization will be checked when converting the results to a list. Therefore, closing this endpoint fully + * off will + * prevent results from being displayed in the search endpoint + * + * @param context the dspace context + * @param id the rest object id + * @return retrieve the system-wide alert for the provided ID + */ + @Override + @PreAuthorize("permitAll()") + public SystemWideAlertRest findOne(Context context, Integer id) { + try { + SystemWideAlert systemWideAlert = systemWideAlertService.find(context, id); + if (systemWideAlert == null) { + throw new ResourceNotFoundException( + "systemWideAlert with id " + systemWideAlert.getID() + " was not found"); + } + if (!systemWideAlert.isActive() && !authorizeService.isAdmin(context)) { + throw new AuthorizeException("Non admin users are not allowed to retrieve inactive alerts"); + } + return converter.toRest(systemWideAlert, utils.obtainProjection()); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return null; + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public Page findAll(Context context, Pageable pageable) { + try { + List systemWideAlerts = systemWideAlertService.findAll(context, pageable.getPageSize(), + Math.toIntExact( + pageable.getOffset())); + return converter.toRestPage(systemWideAlerts, pageable, utils.obtainProjection()); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + protected SystemWideAlertRest put(Context context, HttpServletRequest request, String apiCategory, String model, + Integer id, JsonNode jsonNode) throws SQLException, AuthorizeException { + + SystemWideAlertRest systemWideAlertRest; + try { + systemWideAlertRest = new ObjectMapper().readValue(jsonNode.toString(), SystemWideAlertRest.class); + } catch (JsonProcessingException e) { + throw new UnprocessableEntityException("Cannot parse JSON in request body", e); + } + + if (systemWideAlertRest == null || isBlank(systemWideAlertRest.getMessage())) { + throw new UnprocessableEntityException("system alert message cannot be blank"); + } + + SystemWideAlert systemWideAlert = systemWideAlertService.find(context, id); + if (systemWideAlert == null) { + throw new ResourceNotFoundException("system wide alert with id: " + id + " not found"); + } + + systemWideAlert.setMessage(systemWideAlertRest.getMessage()); + systemWideAlert.setAllowSessions(AllowSessionsEnum.fromString(systemWideAlertRest.getAllowSessions())); + systemWideAlert.setCountdownTo(systemWideAlertRest.getCountdownTo()); + systemWideAlert.setActive(systemWideAlertRest.isActive()); + + systemWideAlertService.update(context, systemWideAlert); + context.commit(); + + return converter.toRest(systemWideAlert, utils.obtainProjection()); + } + + + /** + * Helper method to create a system-wide alert and deny creation when one already exists + * + * @param context The database context + * @return the created system-wide alert + * @throws SQLException + */ + private SystemWideAlert createSystemWideAlert(Context context) + throws SQLException, AuthorizeException { + List all = systemWideAlertService.findAll(context); + if (!all.isEmpty()) { + throw new DSpaceBadRequestException("A system wide alert already exists, no new value can be created. " + + "Try updating the existing one."); + } + + + HttpServletRequest req = getRequestService().getCurrentRequest().getHttpServletRequest(); + ObjectMapper mapper = new ObjectMapper(); + SystemWideAlertRest systemWideAlertRest; + try { + ServletInputStream input = req.getInputStream(); + systemWideAlertRest = mapper.readValue(input, SystemWideAlertRest.class); + } catch (IOException e1) { + throw new UnprocessableEntityException("Error parsing request body.", e1); + } + + SystemWideAlert systemWideAlert; + + try { + systemWideAlert = systemWideAlertService.create(context, systemWideAlertRest.getMessage(), + AllowSessionsEnum.fromString( + systemWideAlertRest.getAllowSessions()), + systemWideAlertRest.getCountdownTo(), + systemWideAlertRest.isActive()); + systemWideAlertService.update(context, systemWideAlert); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return systemWideAlert; + } + + /** + * Search method to retrieve all active system-wide alerts + * + * @param pageable The page object + * @return all active system-wide alerts for the provided page + */ + @PreAuthorize("permitAll()") + @SearchRestMethod(name = "active") + public Page findAllActive(Pageable pageable) { + Context context = obtainContext(); + try { + List systemWideAlerts = + systemWideAlertService.findAllActive(context, + pageable.getPageSize(), + Math.toIntExact( + pageable.getOffset())); + return converter.toRestPage(systemWideAlerts, pageable, utils.obtainProjection()); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + + } + + + @Override + public Class getDomainClass() { + return SystemWideAlertRest.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubscriptionRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubscriptionRestPermissionEvaluatorPlugin.java new file mode 100644 index 0000000000..c93d966e73 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubscriptionRestPermissionEvaluatorPlugin.java @@ -0,0 +1,84 @@ +/** + * 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 static org.dspace.app.rest.model.SubscriptionRest.NAME; +import static org.dspace.app.rest.security.DSpaceRestPermission.DELETE; +import static org.dspace.app.rest.security.DSpaceRestPermission.READ; +import static org.dspace.app.rest.security.DSpaceRestPermission.WRITE; + +import java.io.Serializable; +import java.sql.SQLException; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Subscription; +import org.dspace.eperson.service.SubscribeService; +import org.dspace.services.RequestService; +import org.dspace.services.model.Request; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +/** + * {@link RestPermissionEvaluatorPlugin} class that evaluate READ, WRITE and DELETE permissions over a Subscription + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +@Component +public class SubscriptionRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { + + private static final Logger log = LoggerFactory.getLogger(SubscriptionRestPermissionEvaluatorPlugin.class); + + @Autowired + private RequestService requestService; + @Autowired + private SubscribeService subscribeService; + @Autowired + private AuthorizeService authorizeService; + + @Override + public boolean hasDSpacePermission(Authentication authentication, Serializable targetId, String targetType, + DSpaceRestPermission permission) { + + DSpaceRestPermission restPermission = DSpaceRestPermission.convert(permission); + + if (!READ.equals(restPermission) && !WRITE.equals(restPermission) && !DELETE.equals(restPermission) + || !StringUtils.equalsIgnoreCase(targetType, NAME)) { + return false; + } + + Request request = requestService.getCurrentRequest(); + Context context = ContextUtil.obtainContext(request.getHttpServletRequest()); + + try { + EPerson currentUser = context.getCurrentUser(); + // anonymous user + if (Objects.isNull(currentUser)) { + return false; + } + // Admin user + if (authorizeService.isAdmin(context, currentUser)) { + return true; + } + + Subscription subscription = subscribeService.findById(context, Integer.parseInt(targetId.toString())); + return Objects.nonNull(subscription) ? currentUser.equals(subscription.getEPerson()) : false; + } catch (SQLException e) { + log.error(e.getMessage(), e); + } + return false; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/DataProcessingStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/DataProcessingStep.java index 8eb03acde0..99af309cdb 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/DataProcessingStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/DataProcessingStep.java @@ -38,6 +38,7 @@ public interface DataProcessingStep extends RestProcessingStep { public static final String CCLICENSE_STEP_OPERATION_ENTRY = "cclicense/uri"; public static final String ACCESS_CONDITION_STEP_OPERATION_ENTRY = "discoverable"; public static final String ACCESS_CONDITION_POLICY_STEP_OPERATION_ENTRY = "accessConditions"; + public static final String SHOW_IDENTIFIERS_ENTRY = "identifiers"; public static final String UPLOAD_STEP_METADATA_PATH = "metadata"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/ShowIdentifiersStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/ShowIdentifiersStep.java new file mode 100644 index 0000000000..e63d38ab2e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/ShowIdentifiersStep.java @@ -0,0 +1,170 @@ +/** + * 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.submit.step; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.step.DataIdentifiers; +import org.dspace.app.rest.submit.AbstractProcessingStep; +import org.dspace.app.rest.submit.SubmissionService; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.core.Context; +import org.dspace.handle.factory.HandleServiceFactory; +import org.dspace.handle.service.HandleService; +import org.dspace.identifier.DOI; +import org.dspace.identifier.DOIIdentifierProvider; +import org.dspace.identifier.Handle; +import org.dspace.identifier.IdentifierException; +import org.dspace.identifier.factory.IdentifierServiceFactory; +import org.dspace.identifier.service.IdentifierService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.services.model.Request; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Submission processing step to return identifier data for use in the 'identifiers' submission section component + * in dspace-angular. For effective use, the "identifiers.submission.register" configuration property + * in identifiers.cfg should be enabled so that the WorkspaceItemService will register identifiers for the new item + * at the time of creation, and the DOI consumer will allow workspace and workflow items to have their DOIs minted + * or deleted as per item filter results. + * + * This method can be extended to allow (if authorised) an operation to be sent which will + * override an item filter and force reservation of an identifier. + * + * @author Kim Shepherd + */ +public class ShowIdentifiersStep extends AbstractProcessingStep { + + private static final Logger log = LogManager.getLogger(ShowIdentifiersStep.class); + + @Autowired(required = true) + protected HandleService handleService; + @Autowired(required = true) + protected ContentServiceFactory contentServiceFactory; + + /** + * Override DataProcessing.getData, return data identifiers from getIdentifierData() + * + * @param submissionService The submission service + * @param obj The workspace or workflow item + * @param config The submission step configuration + * @return A simple DataIdentifiers bean containing doi, handle and list of other identifiers + */ + @Override + public DataIdentifiers getData(SubmissionService submissionService, InProgressSubmission obj, + SubmissionStepConfig config) throws Exception { + // If configured, WorkspaceItemService item creation will also call IdentifierService.register() + // for the new item, and the DOI consumer (if also configured) will mint or delete DOI entries as appropriate + // while the item is saved in the submission / workflow process + + // This step simply looks for existing identifier data and returns it in section data for rendering + return getIdentifierData(obj); + } + + /** + * Get data about existing identifiers for this in-progress submission item - this method doesn't require + * submissionService or step config, so can be more easily called from doPatchProcessing as well + * + * @param obj The workspace or workflow item + * @return A simple DataIdentifiers bean containing doi, handle and list of other identifiers + */ + private DataIdentifiers getIdentifierData(InProgressSubmission obj) { + Context context = getContext(); + DataIdentifiers result = new DataIdentifiers(); + // Load identifier service + IdentifierService identifierService = + IdentifierServiceFactory.getInstance().getIdentifierService(); + // Attempt to look up handle and DOI identifiers for this item + String[] defaultTypes = {"handle", "doi"}; + List displayTypes = Arrays.asList(configurationService.getArrayProperty( + "identifiers.submission.display", + defaultTypes)); + result.setDisplayTypes(displayTypes); + String handle = identifierService.lookup(context, obj.getItem(), Handle.class); + DOI doi = null; + String doiString = null; + try { + doi = IdentifierServiceFactory.getInstance().getDOIService().findDOIByDSpaceObject(context, obj.getItem()); + if (doi != null && !DOIIdentifierProvider.MINTED.equals(doi.getStatus()) + && !DOIIdentifierProvider.DELETED.equals(doi.getStatus())) { + doiString = doi.getDoi(); + } + } catch (SQLException e) { + log.error(e.getMessage()); + } + + // Other identifiers can be looked up / resolved through identifier service or + // its own specific service here + + // If we got a DOI, format it to its external form + if (StringUtils.isNotEmpty(doiString)) { + try { + doiString = IdentifierServiceFactory.getInstance().getDOIService().DOIToExternalForm(doiString); + } catch (IdentifierException e) { + log.error("Error formatting DOI: " + doi); + } + } + // If we got a handle, format it to its canonical form + if (StringUtils.isNotEmpty(handle)) { + handle = HandleServiceFactory.getInstance().getHandleService().getCanonicalForm(handle); + } + + // Populate bean with data and return, if the identifier type is configured for exposure + result.addIdentifier("doi", doiString, + doi != null ? DOIIdentifierProvider.statusText[doi.getStatus()] : null); + result.addIdentifier("handle", handle, null); + return result; + } + + /** + * Utility method to get DSpace context from the HTTP request + * @return DSpace context + */ + private Context getContext() { + Context context; + Request currentRequest = DSpaceServicesFactory.getInstance().getRequestService().getCurrentRequest(); + if (currentRequest != null) { + HttpServletRequest request = currentRequest.getHttpServletRequest(); + context = ContextUtil.obtainContext(request); + } else { + context = new Context(); + } + + return context; + } + + /** + * This step is currently just for displaying identifiers and does not take additional patch operations + * @param context + * the DSpace context + * @param currentRequest + * the http request + * @param source + * the in progress submission + * @param op + * the json patch operation + * @param stepConf + * @throws Exception + */ + @Override + public void doPatchProcessing(Context context, HttpServletRequest currentRequest, InProgressSubmission source, + Operation op, SubmissionStepConfig stepConf) throws Exception { + log.warn("Not implemented"); + } + +} diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml index cc23dbc09e..eca9acf79f 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml @@ -162,6 +162,13 @@ submission-form + + + submit.progressbar.identifiers + org.dspace.app.rest.submit.step.ShowIdentifiersStep + identifiers + + Sample @@ -194,6 +201,8 @@ + + diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/identifier-service.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/identifier-service.xml new file mode 100644 index 0000000000..447d0a59dd --- /dev/null +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/identifier-service.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/IdentifierRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/IdentifierRestControllerIT.java deleted file mode 100644 index 9927d37286..0000000000 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/IdentifierRestControllerIT.java +++ /dev/null @@ -1,101 +0,0 @@ -/** - * 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; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.dspace.app.rest.test.AbstractControllerIntegrationTest; -import org.dspace.builder.CollectionBuilder; -import org.dspace.builder.CommunityBuilder; -import org.dspace.builder.ItemBuilder; -import org.dspace.content.Collection; -import org.dspace.content.Item; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -/** - * Integration test for the identifier resolver - * - * @author Andrea Bollini (andrea.bollini at 4science.it) - */ -public class IdentifierRestControllerIT extends AbstractControllerIntegrationTest { - - @Before - public void setup() throws Exception { - super.setUp(); - } - - @Test - public void testValidIdentifier() throws Exception { - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - // We create a top community to receive an identifier - parentCommunity = CommunityBuilder.createCommunity(context) - .withName("Parent Community") - .build(); - - context.restoreAuthSystemState(); - - String handle = parentCommunity.getHandle(); - String communityDetail = REST_SERVER_URL + "core/communities/" + parentCommunity.getID(); - - getClient().perform(get("/api/pid/find?id={handle}",handle)) - .andExpect(status().isFound()) - //We expect a Location header to redirect to the community details - .andExpect(header().string("Location", communityDetail)); - } - - @Test - - public void testValidIdentifierItemHandlePrefix() throws Exception { - //We turn off the authorization system in order to create the structure as defined below - context.turnOffAuthorisationSystem(); - - // Create an item with a handle identifier - parentCommunity = CommunityBuilder.createCommunity(context) - .withName("Parent Community") - .build(); - Collection owningCollection = CollectionBuilder.createCollection(context, parentCommunity) - .withName("Owning Collection") - .build(); - Item item = ItemBuilder.createItem(context, owningCollection) - .withTitle("Test item") - .build(); - - String handle = item.getHandle(); - String itemLocation = REST_SERVER_URL + "core/items/" + item.getID(); - - getClient().perform(get("/api/pid/find?id=hdl:{handle}", handle)) - .andExpect(status().isFound()) - // We expect a Location header to redirect to the item's page - .andExpect(header().string("Location", itemLocation)); - } - - @Test - public void testUnexistentIdentifier() throws Exception { - getClient().perform(get("/api/pid/find?id={id}","fakeIdentifier")) - .andExpect(status().isNotFound()); - } - - @Test - @Ignore - /** - * This test will check the return status code when no id is supplied. It currently fails as our - * RestResourceController take the precedence over the pid controller returning a 404 Repository not found - * - * @throws Exception - */ - public void testMissingIdentifierParameter() throws Exception { - getClient().perform(get("/api/pid/find")) - .andExpect(status().isUnprocessableEntity()); - } -} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/IdentifierRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/IdentifierRestRepositoryIT.java new file mode 100644 index 0000000000..27e21e4776 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/IdentifierRestRepositoryIT.java @@ -0,0 +1,334 @@ +/** + * 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; + +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.handle.factory.HandleServiceFactory; +import org.dspace.handle.service.HandleService; +import org.dspace.identifier.DOI; +import org.dspace.identifier.DOIIdentifierProvider; +import org.dspace.identifier.factory.IdentifierServiceFactory; +import org.dspace.identifier.service.DOIService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.http.MediaType; + +/** + * Test getting and registering item identifiers + * + * @author Kim Shepherd + */ +public class IdentifierRestRepositoryIT extends AbstractControllerIntegrationTest { + @Before + public void setup() throws Exception { + super.setUp(); + } + + @Test + public void testValidIdentifier() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + // We create a top community to receive an identifier + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + context.restoreAuthSystemState(); + + String handle = parentCommunity.getHandle(); + String communityDetail = REST_SERVER_URL + "core/communities/" + parentCommunity.getID(); + + getClient().perform(get("/api/pid/find?id={handle}",handle)) + .andExpect(status().isFound()) + //We expect a Location header to redirect to the community details + .andExpect(header().string("Location", communityDetail)); + } + + @Test + + public void testValidIdentifierItemHandlePrefix() throws Exception { + //We turn off the authorization system in order to create the structure as defined below + context.turnOffAuthorisationSystem(); + + // Create an item with a handle identifier + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection owningCollection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Owning Collection") + .build(); + Item item = ItemBuilder.createItem(context, owningCollection) + .withTitle("Test item") + .build(); + + String handle = item.getHandle(); + String itemLocation = REST_SERVER_URL + "core/items/" + item.getID(); + + getClient().perform(get("/api/pid/find?id=hdl:{handle}", handle)) + .andExpect(status().isFound()) + // We expect a Location header to redirect to the item's page + .andExpect(header().string("Location", itemLocation)); + } + + @Test + public void testUnexistentIdentifier() throws Exception { + getClient().perform(get("/api/pid/find?id={id}","fakeIdentifier")) + .andExpect(status().isNotFound()); + } + + @Test + @Ignore + /** + * This test will check the return status code when no id is supplied. It currently fails as our + * RestResourceController take the precedence over the pid controller returning a 404 Repository not found + * + * @throws Exception + */ + public void testMissingIdentifierParameter() throws Exception { + getClient().perform(get("/api/pid/find")) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void testRegisterDoiForItem() throws Exception { + //Turn off the authorization system, otherwise we can't make the objects + context.turnOffAuthorisationSystem(); + + DOIService doiService = IdentifierServiceFactory.getInstance().getDOIService(); + + //** GIVEN ** + //1. A community-collection structure with one parent community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + + //2. A public item that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald") + .build(); + + // This item should not have a DOI + DOIIdentifierProvider doiIdentifierProvider = + DSpaceServicesFactory.getInstance().getServiceManager() + .getServiceByName("org.dspace.identifier.DOIIdentifierProvider", + org.dspace.identifier.DOIIdentifierProvider.class); + doiIdentifierProvider.delete(context, publicItem1); + + // Body of POST to create an identifier for public item 1 + String uriList = "https://localhost:8080/server/api/core/items/" + publicItem1.getID(); + + // A non-admin should get an unauthorised error from REST method preauth + // Expect first forbidden + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(post("/api/pid/identifiers") + .queryParam("type", "doi") + .contentType(MediaType.parseMediaType("text/uri-list")) + .content(uriList)) + .andExpect(status().isForbidden()); + + // Set token to admin credentials + token = getAuthToken(admin.getEmail(), password); + + // Expect a successful 201 CREATED for this item with no DOI + getClient(token).perform(post("/api/pid/identifiers") + .queryParam("type", "doi") + .contentType(MediaType.parseMediaType("text/uri-list")) + .content(uriList)) + .andExpect(status().isCreated()); + + // Expected 400 BAD REQUEST status code for a DOI already in REGISTERED / TO_BE_REGISTERED state + getClient(token).perform(post("/api/pid/identifiers") + .queryParam("type", "doi") + .contentType(MediaType.parseMediaType("text/uri-list")) + .content(uriList)) + .andExpect(status().isBadRequest()); + + // Get the doi we minted and queued for registration + DOI doi = doiService.findDOIByDSpaceObject(context, publicItem1); + // The DOI should not be null + assertNotNull(doi); + // The DOI status should be TO_BE_REGISTERED + Assert.assertEquals(DOIIdentifierProvider.TO_BE_REGISTERED, doi.getStatus()); + + // Now, set the DOI status back to pending and update + doi.setStatus(DOIIdentifierProvider.PENDING); + doiService.update(context, doi); + + // Do another POST, again this should return 201 CREATED as we shift the DOI from PENDING to TO_BE_REGISTERED + getClient(token).perform(post("/api/pid/identifiers") + .queryParam("type", "doi") + .contentType(MediaType.parseMediaType("text/uri-list")) + .content(uriList)) + .andExpect(status().isCreated()); + + context.restoreAuthSystemState(); + } + + @Test + public void testGetIdentifiersForItemByLink() throws Exception { + //Turn off the authorization system, otherwise we can't make the objects + context.turnOffAuthorisationSystem(); + + DOIService doiService = IdentifierServiceFactory.getInstance().getDOIService(); + HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); + + //1. A community-collection structure with one parent community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + + //2. A public item that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald") + .build(); + + String doiString = "10.5072/dspace-identifier-test-" + publicItem1.getID(); + + // Use the DOI service to directly manipulate the DOI on this object so that we can predict and + // test values via the REST request + DOI doi = doiService.findDOIByDSpaceObject(context, publicItem1); + + // Assert non-null DOI, since we should be minting them automatically here + assertNotNull(doi); + + // Set specific string and state we expect to get back from a REST request + doi.setDoi(doiString); + doi.setStatus(DOIIdentifierProvider.IS_REGISTERED); + doiService.update(context, doi); + + context.restoreAuthSystemState(); + + String token = getAuthToken(eperson.getEmail(), password); + + // Get identifiers for this item - we expect a 200 OK response and the type of the resource is plural + // "identifiers" + getClient(token).perform(get("/api/core/items/" + + publicItem1.getID().toString() + "/identifiers")) + .andExpect(status().isOk()).andExpect(jsonPath("$.type").value("identifiers")); + + // Expect an array of identifiers + getClient(token).perform(get("/api/core/items/" + + publicItem1.getID().toString() + "/identifiers")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.identifiers").isArray()); + + // Expect a valid DOI with the value, type and status we expect + getClient(token).perform(get("/api/core/items/" + + publicItem1.getID().toString() + "/identifiers")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.identifiers[0].type").value("identifier")) + .andExpect(jsonPath("$.identifiers[0].value").value(doiService.DOIToExternalForm(doiString))) + .andExpect(jsonPath("$.identifiers[0].identifierType").value("doi")) + .andExpect(jsonPath("$.identifiers[0].identifierStatus") + .value(DOIIdentifierProvider.statusText[DOIIdentifierProvider.IS_REGISTERED])); + + // Expect a valid Handle with the value, type we expect + getClient(token).perform(get("/api/core/items/" + + publicItem1.getID().toString() + "/identifiers")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.identifiers[1].type").value("identifier")) + .andExpect(jsonPath("$.identifiers[1].value") + .value(handleService.getCanonicalForm(publicItem1.getHandle()))) + .andExpect(jsonPath("$.identifiers[1].identifierType").value("handle")); + + } + + @Test + public void testFindIdentifiersByItem() throws Exception { + //Turn off the authorization system, otherwise we can't make the objects + context.turnOffAuthorisationSystem(); + + DOIService doiService = IdentifierServiceFactory.getInstance().getDOIService(); + HandleService handleService = HandleServiceFactory.getInstance().getHandleService(); + + //1. A community-collection structure with one parent community and two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + + //2. A public item that is readable by Anonymous + Item publicItem1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald") + .build(); + + String doiString = "10.5072/dspace-identifier-test-" + publicItem1.getID(); + + // Use the DOI service to directly manipulate the DOI on this object so that we can predict and + // test values via the REST request + DOI doi = doiService.findDOIByDSpaceObject(context, publicItem1); + + // Assert non-null DOI, since we should be minting them automatically here + assertNotNull(doi); + + // Set specific string and state we expect to get back from a REST request + doi.setDoi(doiString); + doi.setStatus(DOIIdentifierProvider.IS_REGISTERED); + doiService.update(context, doi); + + context.restoreAuthSystemState(); + + String token = getAuthToken(eperson.getEmail(), password); + + // Get identifiers for this item - we expect a 200 OK response and the type of the resource is plural + // "identifiers" + getClient(token).perform(get("/api/pid/identifiers/search/findByItem").queryParam("uuid", + publicItem1.getID().toString())) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.identifiers").exists()); + + // Expect an array of identifiers + getClient(token).perform(get("/api/pid/identifiers/search/findByItem").queryParam("uuid", + publicItem1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.identifiers").isArray()); + + // Expect a valid DOI with the value, type and status we expect + getClient(token).perform(get("/api/pid/identifiers/search/findByItem").queryParam("uuid", + publicItem1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.identifiers[0].type").value("identifier")) + .andExpect(jsonPath("$._embedded.identifiers[0].value").value(doiService.DOIToExternalForm(doiString))) + .andExpect(jsonPath("$._embedded.identifiers[0].identifierType").value("doi")) + .andExpect(jsonPath("$._embedded.identifiers[0].identifierStatus") + .value(DOIIdentifierProvider.statusText[DOIIdentifierProvider.IS_REGISTERED])); + + // Expect a valid Handle with the value, type we expect + getClient(token).perform(get("/api/pid/identifiers/search/findByItem").queryParam("uuid", + publicItem1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.identifiers[1].type").value("identifier")) + .andExpect(jsonPath("$._embedded.identifiers[1].value") + .value(handleService.getCanonicalForm(publicItem1.getHandle()))) + .andExpect(jsonPath("$._embedded.identifiers[1].identifierType").value("handle")); + + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java index 0513a0d5f4..07edfeec33 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ScriptRestRepositoryIT.java @@ -10,6 +10,7 @@ package org.dspace.app.rest; import static com.jayway.jsonpath.JsonPath.read; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; @@ -101,6 +102,25 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { ))); } + @Test + public void findAllScriptsSortedAlphabeticallyTest() throws Exception { + String token = getAuthToken(admin.getEmail(), password); + + getClient(token).perform(get("/api/system/scripts") + .param("size", String.valueOf(scriptConfigurations.size()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.scripts", contains( + scriptConfigurations + .stream() + .sorted(Comparator.comparing(ScriptConfiguration::getName)) + .map(scriptConfiguration -> ScriptMatcher.matchScript( + scriptConfiguration.getName(), + scriptConfiguration.getDescription() + )) + .collect(Collectors.toList()) + ))); + } + @Test public void findAllScriptsUnauthorizedTest() throws Exception { @@ -115,7 +135,7 @@ public class ScriptRestRepositoryIT extends AbstractControllerIntegrationTest { public void findAllScriptsPaginationTest() throws Exception { List alphabeticScripts = scriptConfigurations.stream() - .sorted(Comparator.comparing(s -> s.getClass().getName())) + .sorted(Comparator.comparing(ScriptConfiguration::getName)) .collect(Collectors.toList()); int totalPages = scriptConfigurations.size(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index 5f93411bb9..e7d43ec4d6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -205,7 +205,7 @@ public class SubmissionDefinitionsControllerIT extends AbstractControllerIntegra // We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) // Match only that a section exists with a submission configuration behind - .andExpect(jsonPath("$._embedded.submissionsections", hasSize(8))) + .andExpect(jsonPath("$._embedded.submissionsections", hasSize(9))) .andExpect(jsonPath("$._embedded.submissionsections", Matchers.hasItem( allOf( diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionShowIdentifiersRestIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionShowIdentifiersRestIT.java new file mode 100644 index 0000000000..8d95f4627b --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionShowIdentifiersRestIT.java @@ -0,0 +1,135 @@ +/** + * 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; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.IOException; +import java.sql.SQLException; + +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.WorkspaceItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.WorkspaceItem; +import org.dspace.content.service.WorkspaceItemService; +import org.dspace.eperson.EPerson; +import org.dspace.handle.service.HandleService; +import org.dspace.identifier.DOIIdentifierProvider; +import org.dspace.services.ConfigurationService; +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Test suite for testing the Show Identifiers submission step + * + * @author Kim Shepherd + * + */ +public class SubmissionShowIdentifiersRestIT extends AbstractControllerIntegrationTest { + + @Autowired + private WorkspaceItemService workspaceItemService; + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private HandleService handleService; + + private Collection collection; + private EPerson submitter; + + @Override + public void setUp() throws Exception { + super.setUp(); + + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Root community").build(); + + submitter = EPersonBuilder.createEPerson(context) + .withEmail("submitter.em@test.com") + .withPassword(password) + .build(); + + collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .withEntityType("Publication") + .withSubmitterGroup(submitter).build(); + + // Manually set configuration to allow registration handles, DOIs at workspace item creation + configurationService.setProperty("identifiers.submission.register", true); + + context.restoreAuthSystemState(); + } + + @After + public void after() throws SQLException, IOException, AuthorizeException { + context.turnOffAuthorisationSystem(); + workspaceItemService.findAll(context).forEach(this::deleteWorkspaceItem); + // Manually restore identifiers configuration + configurationService.setProperty("identifiers.submission.register", false); + context.restoreAuthSystemState(); + } + + private void deleteWorkspaceItem(WorkspaceItem workspaceItem) { + try { + workspaceItemService.deleteAll(context, workspaceItem); + } catch (SQLException | AuthorizeException | IOException e) { + throw new RuntimeException(); + } + } + + @Test + public void testItemHandleReservation() throws Exception { + // Test publication that should get Handle and DOI + context.turnOffAuthorisationSystem(); + WorkspaceItem workspaceItem = createWorkspaceItem("Test publication", collection); + context.restoreAuthSystemState(); + // Expected handle + String expectedHandle = handleService.resolveToURL(context, workspaceItem.getItem().getHandle()); + String submitterToken = getAuthToken(submitter.getEmail(), password); + getClient(submitterToken).perform(get("/api/submission/workspaceitems/" + workspaceItem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.identifiers.identifiers[1].type").value("identifier")) + .andExpect(jsonPath("$.sections.identifiers.identifiers[1].value").value(expectedHandle)) + .andExpect(jsonPath("$.sections.identifiers.identifiers[1].identifierType").value("handle")); + } + + @Test + public void testItemDoiReservation() throws Exception { + // Test publication that should get Handle and DOI + context.turnOffAuthorisationSystem(); + WorkspaceItem workspaceItem = createWorkspaceItem("Test publication", collection); + context.restoreAuthSystemState(); + + String submitterToken = getAuthToken(submitter.getEmail(), password); + getClient(submitterToken).perform(get("/api/submission/workspaceitems/" + workspaceItem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.identifiers.identifiers[0].type").value("identifier")) + .andExpect(jsonPath("$.sections.identifiers.identifiers[0].identifierType").value("doi")) + .andExpect(jsonPath("$.sections.identifiers.identifiers[0].identifierStatus") + .value(DOIIdentifierProvider.statusText[DOIIdentifierProvider.PENDING])); + } + + private WorkspaceItem createWorkspaceItem(String title, Collection collection) { + WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle(title) + .withSubmitter(submitter) + .build(); + return workspaceItem; + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubscriptionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubscriptionRestRepositoryIT.java new file mode 100644 index 0000000000..038acf7e73 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubscriptionRestRepositoryIT.java @@ -0,0 +1,1249 @@ +/** + * 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; + +import static com.jayway.jsonpath.JsonPath.read; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dspace.app.rest.matcher.EPersonMatcher; +import org.dspace.app.rest.matcher.SubscriptionMatcher; +import org.dspace.app.rest.model.SubscriptionParameterRest; +import org.dspace.app.rest.model.SubscriptionRest; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.SubscribeBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Subscription; +import org.dspace.eperson.SubscriptionParameter; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; + +/** + * Integration test to test the /api/config/subscriptions endpoint + * (Class has to start or end with IT to be picked up by the failsafe plugin) + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class SubscriptionRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Autowired + private ResourcePolicyService resourcePolicyService; + + private Community subCommunity; + private Collection collection; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + subCommunity = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Test Sub Community") + .build(); + collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .withSubmitterGroup(eperson) + .build(); + + context.restoreAuthSystemState(); + } + + @Test + public void findAll() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription1 = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + List subscriptionParameterList2 = new ArrayList<>(); + SubscriptionParameter subscriptionParameter2 = new SubscriptionParameter(); + subscriptionParameter2.setName("frequency"); + subscriptionParameter2.setValue("W"); + subscriptionParameterList2.add(subscriptionParameter2); + Subscription subscription2 = SubscribeBuilder.subscribeBuilder(context, + "content", collection, admin, subscriptionParameterList2).build(); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.subscriptions", Matchers.containsInAnyOrder( + SubscriptionMatcher.matchSubscription(subscription1), + SubscriptionMatcher.matchSubscription(subscription2) + ))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(2))) + .andExpect(jsonPath("$.page.totalPages", greaterThanOrEqualTo(1))) + .andExpect(jsonPath("$.page.number", is(0))); + } + + @Test + public void findAllAnonymous() throws Exception { + getClient().perform(get("/api/core/subscriptions")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findAllAsUser() throws Exception { + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions")) + .andExpect(status().isForbidden()); + } + + @Test + public void findOneWithOwnerTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("M"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions/" + subscription.getID())) + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //By default we expect at least 1 submission forms so this to be reflected in the page object + .andExpect(jsonPath("$.subscriptionType", is("content"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].name", is("frequency"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].value", is("M"))) + .andExpect(jsonPath("$._links.eperson.href", Matchers.endsWith("/eperson"))) + .andExpect(jsonPath("$._links.resource.href", Matchers.endsWith("/resource"))) + .andExpect(jsonPath("$._links.self.href", + Matchers.startsWith(REST_SERVER_URL + "core/subscriptions/" + subscription.getID()))) + .andExpect(jsonPath("$._links.resource.href", + Matchers.startsWith(REST_SERVER_URL + "core/subscriptions"))) + .andExpect(jsonPath("$._links.eperson.href", + Matchers.startsWith(REST_SERVER_URL + "core/subscriptions"))); + } + + @Test + public void findOneAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("W"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, admin, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/" + subscription.getID())) + .andExpect(status().isOk()) + //We expect the content type to be "application/hal+json;charset=UTF-8" + .andExpect(content().contentType(contentType)) + //By default we expect at least 1 submission forms so this to be reflected in the page object + .andExpect(jsonPath("$.subscriptionType", is("content"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].name", is("frequency"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].value", is("W"))) + .andExpect(jsonPath("$._links.self.href", + Matchers.startsWith(REST_SERVER_URL + "core/subscriptions/" + subscription.getID()))) + .andExpect(jsonPath("$._links.resource.href", + Matchers.startsWith(REST_SERVER_URL + "core/subscriptions"))) + .andExpect(jsonPath("$._links.resource.href", Matchers.endsWith("/resource"))) + .andExpect(jsonPath("$._links.eperson.href", + Matchers.startsWith(REST_SERVER_URL + "core/subscriptions"))) + .andExpect(jsonPath("$._links.eperson.href", Matchers.endsWith("/eperson"))); + } + + @Test + public void findOneAnonymousTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, admin, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/subscriptions/" + subscription.getID())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findOneForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("W"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, admin, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions/" + subscription.getID())) + .andExpect(status().isForbidden()); + } + + @Test + public void findOneNotFoundTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/" + Integer.MAX_VALUE)) + .andExpect(status().isNotFound()); + } + + @Test + public void findSubscriptionsByEPersonAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription1 = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + List subscriptionParameterList2 = new ArrayList<>(); + SubscriptionParameter subscriptionParameter2 = new SubscriptionParameter(); + subscriptionParameter2.setName("frequency"); + subscriptionParameter2.setValue("W"); + subscriptionParameterList2.add(subscriptionParameter2); + Subscription subscription2 = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList2).build(); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/search/findByEPerson") + .param("uuid", eperson.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.subscriptions", Matchers.containsInAnyOrder( + SubscriptionMatcher.matchSubscription(subscription1), + SubscriptionMatcher.matchSubscription(subscription2) + ))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(2))) + .andExpect(jsonPath("$.page.totalPages", greaterThanOrEqualTo(1))) + .andExpect(jsonPath("$.page.number", is(0))); + } + + @Test + public void findSubscriptionsByEPersonOwnerTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("M"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription1 = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + List subscriptionParameterList2 = new ArrayList<>(); + SubscriptionParameter subscriptionParameter2 = new SubscriptionParameter(); + subscriptionParameter2.setName("frequency"); + subscriptionParameter2.setValue("D"); + subscriptionParameterList2.add(subscriptionParameter2); + Subscription subscription2 = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList2).build(); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(eperson.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/search/findByEPerson") + .param("uuid", eperson.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.subscriptions", Matchers.containsInAnyOrder( + SubscriptionMatcher.matchSubscription(subscription1), + SubscriptionMatcher.matchSubscription(subscription2) + ))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", greaterThanOrEqualTo(2))) + .andExpect(jsonPath("$.page.totalPages", greaterThanOrEqualTo(1))) + .andExpect(jsonPath("$.page.number", is(0))); + } + + @Test + public void findSubscriptionsByEPersonUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + SubscribeBuilder.subscribeBuilder(context, "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/subscriptions/search/findByEPerson") + .param("uuid", eperson.getID().toString())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findSubscriptionsByEPersonForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson user = EPersonBuilder.createEPerson(context) + .withEmail("user1@mail.com") + .withPassword(password) + .build(); + + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + SubscribeBuilder.subscribeBuilder(context, "content", collection, user, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions/search/findByEPerson") + .param("uuid", user.getID().toString())) + .andExpect(status().isForbidden()); + } + + @Test + public void findByEPersonAndDsoAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription1 = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + + List subscriptionParameterList2 = new ArrayList<>(); + SubscriptionParameter subscriptionParameter2 = new SubscriptionParameter(); + subscriptionParameter2.setName("frequency"); + subscriptionParameter2.setValue("W"); + subscriptionParameterList2.add(subscriptionParameter2); + Subscription subscription2 = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList2).build(); + + List subscriptionParameterList3 = new ArrayList<>(); + SubscriptionParameter subscriptionParameter3 = new SubscriptionParameter(); + subscriptionParameter3.setName("frequency"); + subscriptionParameter3.setValue("M"); + subscriptionParameterList3.add(subscriptionParameter3); + Subscription subscription3 = SubscribeBuilder.subscribeBuilder(context, + "content", subCommunity, eperson, subscriptionParameterList3).build(); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/search/findByEPersonAndDso") + .param("eperson_id", eperson.getID().toString()) + .param("resource", collection.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.subscriptions", Matchers.containsInAnyOrder( + SubscriptionMatcher.matchSubscription(subscription1), + SubscriptionMatcher.matchSubscription(subscription2) + ))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(2))) + .andExpect(jsonPath("$.page.totalPages", is(1))) + .andExpect(jsonPath("$.page.number", is(0))); + + getClient(tokenAdmin).perform(get("/api/core/subscriptions/search/findByEPersonAndDso") + .param("eperson_id", eperson.getID().toString()) + .param("resource", subCommunity.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.subscriptions", Matchers.contains( + SubscriptionMatcher.matchSubscription(subscription3) + ))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))) + .andExpect(jsonPath("$.page.totalPages", is(1))) + .andExpect(jsonPath("$.page.number", is(0))); + } + + @Test + public void findByEPersonAndDsoOwnerTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription1 = SubscribeBuilder.subscribeBuilder(context, + "content", subCommunity, eperson, subscriptionParameterList).build(); + + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions/search/findByEPersonAndDso") + .param("eperson_id", eperson.getID().toString()) + .param("resource", subCommunity.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.subscriptions", Matchers.contains( + SubscriptionMatcher.matchSubscription(subscription1) + ))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))) + .andExpect(jsonPath("$.page.totalPages", is(1))) + .andExpect(jsonPath("$.page.number", is(0))); + } + + @Test + public void findByEPersonAndDsoUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + SubscribeBuilder.subscribeBuilder(context, "content", subCommunity, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/subscriptions/search/findByEPersonAndDso") + .param("eperson_id", eperson.getID().toString()) + .param("resource", subCommunity.getID().toString())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findByEPersonAndDsoForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + SubscribeBuilder.subscribeBuilder(context, "content", subCommunity, admin, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions/search/findByEPersonAndDso") + .param("eperson_id", admin.getID().toString()) + .param("resource", subCommunity.getID().toString())) + .andExpect(status().isForbidden()); + } + + @Test + public void createSubscriptionUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + + SubscriptionParameterRest subscriptionParameterRest = new SubscriptionParameterRest(); + subscriptionParameterRest.setValue("frequency"); + subscriptionParameterRest.setName("D"); + List subscriptionParameterRestList = new ArrayList<>(); + subscriptionParameterRestList.add(subscriptionParameterRest); + + SubscriptionRest subscriptionRest = new SubscriptionRest(); + subscriptionRest.setSubscriptionType("content"); + + context.restoreAuthSystemState(); + + ObjectMapper objectMapper = new ObjectMapper(); + + getClient().perform(post("/api/core/subscriptions") + .param("resource", collection.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(objectMapper.writeValueAsString(subscriptionRest)) + .contentType(contentType)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void createSubscriptionAdminForOtherPersonTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "D"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + AtomicReference idRef = new AtomicReference(); + + try { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(post("/api/core/subscriptions") + .param("resource", collection.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.subscriptionType", is("content"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].name", is("frequency"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].value", is("D"))) + .andExpect(jsonPath("$._links.eperson.href", Matchers.endsWith("eperson"))) + .andExpect(jsonPath("$._links.resource.href", Matchers.endsWith("resource"))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + } finally { + SubscribeBuilder.deleteSubscription(idRef.get()); + } + } + + @Test + public void createSubscriptionByEPersonTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "W"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + AtomicReference idRef = new AtomicReference(); + + try { + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(post("/api/core/subscriptions") + .param("resource", collection.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.subscriptionType", is("content"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].name", is("frequency"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].value", is("W"))) + .andExpect(jsonPath("$._links.eperson.href", Matchers.endsWith("eperson"))) + .andExpect(jsonPath("$._links.resource.href", Matchers.endsWith("resource"))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + } finally { + SubscribeBuilder.deleteSubscription(idRef.get()); + } + } + + @Test + public void createSubscriptionForItemByEPersonTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item1 = ItemBuilder.createItem(context, collection) + .withTitle("Public item") + .build(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "W"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(post("/api/core/subscriptions") + .param("resource", item1.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + public void createSubscriptionForItemByAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item1 = ItemBuilder.createItem(context, collection) + .withTitle("Public item") + .build(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "W"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(post("/api/core/subscriptions") + .param("resource", item1.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + public void createSubscriptionWrongResourceUUIDTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "W"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(post("/api/core/subscriptions") + .param("resource", UUID.randomUUID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + public void createSubscriptionMissingResourceUUIDTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "W"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(post("/api/core/subscriptions") + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void createSubscriptionWithWrongSubscriptionParameterNameTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "TestName"); + sub_list.put("value", "X"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(post("/api/core/subscriptions") + .param("resource", collection.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void createSubscriptionWithInvalidSubscriptionParameterValueTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "X"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(post("/api/core/subscriptions") + .param("resource", collection.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void createSubscriptionWithInvalidSubscriptionTypeValueTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "InvalidValue"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "W"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(post("/api/core/subscriptions") + .param("resource", collection.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void createSubscriptionInvalidJsonTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Map map = new HashMap<>(); + map.put("type", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "daily"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(post("/api/core/subscriptions") + .param("resource", collection.getID().toString()) + .param("eperson_id", eperson.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void createSubscriptionPersonForAnotherPersonTest() throws Exception { + context.turnOffAuthorisationSystem(); + + EPerson user = EPersonBuilder.createEPerson(context) + .withEmail("user1@mail.com") + .withPassword(password) + .build(); + + Map map = new HashMap<>(); + map.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "D"); + list.add(sub_list); + map.put("subscriptionParameterList", list); + + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(post("/api/core/subscriptions") + .param("resource", collection.getID().toString()) + .param("eperson_id", user.getID().toString()) + .content(new ObjectMapper().writeValueAsString(map)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isForbidden()); + } + + @Test + public void deleteSubscriptionUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + getClient().perform(delete("/api/core/subscriptions/" + subscription.getID())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void deleteSubscriptionAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(delete("/api/core/subscriptions/" + subscription.getID())) + .andExpect(status().isNoContent()); + + getClient(tokenAdmin).perform(get("/api/core/subscriptions/" + subscription.getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteSubscriptionForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, admin, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(delete("/api/core/subscriptions/" + subscription.getID())) + .andExpect(status().isForbidden()); + } + + @Test + public void deleteSubscriptionNotFoundTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(delete("/api/core/subscriptions/" + Integer.MAX_VALUE)) + .andExpect(status().isNotFound()); + } + + @Test + public void putSubscriptionUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("Parameter1"); + subscriptionParameter.setValue("ValueParameter1"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, admin, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + ObjectMapper objectMapper = new ObjectMapper(); + Map newSubscription = new HashMap<>(); + newSubscription.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "daily"); + list.add(sub_list); + newSubscription.put("subscriptionParameterList", list); + + getClient().perform(put("/api/core/subscriptions/" + subscription.getID()) + .content(objectMapper.writeValueAsString(newSubscription)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnauthorized()); + } + + @Test + public void putSubscriptionForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, admin, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + ObjectMapper objectMapper = new ObjectMapper(); + Map newSubscription = new HashMap<>(); + newSubscription.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "W"); + list.add(sub_list); + newSubscription.put("subscriptionParameterList", list); + + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(put("/api/core/subscriptions/" + subscription.getID()) + .content(objectMapper.writeValueAsString(newSubscription)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isForbidden()); + } + + @Test + public void putSubscriptionTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + ObjectMapper objectMapper = new ObjectMapper(); + Map newSubscription = new HashMap<>(); + newSubscription.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "W"); + list.add(sub_list); + newSubscription.put("subscriptionParameterList", list); + + String tokenSubscriber = getAuthToken(eperson.getEmail(), password); + getClient(tokenSubscriber).perform(put("/api/core/subscriptions/" + subscription.getID()) + .content(objectMapper.writeValueAsString(newSubscription)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.subscriptionType", is("content"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].name", is("frequency"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].value", is("W"))) + .andExpect(jsonPath("$._links.eperson.href", Matchers.endsWith("/eperson"))) + .andExpect(jsonPath("$._links.resource.href",Matchers.endsWith("/resource"))); + } + + @Test + public void putSubscriptionInvalidSubscriptionParameterNameTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + ObjectMapper objectMapper = new ObjectMapper(); + Map newSubscription = new HashMap<>(); + newSubscription.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "InvalidName"); + sub_list.put("value", "W"); + list.add(sub_list); + newSubscription.put("subscriptionParameterList", list); + + String tokenSubscriber = getAuthToken(eperson.getEmail(), password); + getClient(tokenSubscriber).perform(put("/api/core/subscriptions/" + subscription.getID()) + .content(objectMapper.writeValueAsString(newSubscription)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void putSubscriptionInvalidSubscriptionParameterValueTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + ObjectMapper objectMapper = new ObjectMapper(); + Map newSubscription = new HashMap<>(); + newSubscription.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "Y"); + list.add(sub_list); + newSubscription.put("subscriptionParameterList", list); + + String tokenSubscriber = getAuthToken(eperson.getEmail(), password); + getClient(tokenSubscriber).perform(put("/api/core/subscriptions/" + subscription.getID()) + .content(objectMapper.writeValueAsString(newSubscription)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void putSubscriptionInvalidSubscriptionTypeValueTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + ObjectMapper objectMapper = new ObjectMapper(); + Map newSubscription = new HashMap<>(); + newSubscription.put("subscriptionType", "InvalidType"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "D"); + list.add(sub_list); + newSubscription.put("subscriptionParameterList", list); + + String tokenSubscriber = getAuthToken(eperson.getEmail(), password); + getClient(tokenSubscriber).perform(put("/api/core/subscriptions/" + subscription.getID()) + .content(objectMapper.writeValueAsString(newSubscription)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void putSubscriptionAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + ObjectMapper objectMapper = new ObjectMapper(); + Map newSubscription = new HashMap<>(); + newSubscription.put("subscriptionType", "content"); + List> list = new ArrayList<>(); + Map sub_list = new HashMap<>(); + sub_list.put("name", "frequency"); + sub_list.put("value", "W"); + list.add(sub_list); + newSubscription.put("subscriptionParameterList", list); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(put("/api/core/subscriptions/" + subscription.getID()) + .content(objectMapper.writeValueAsString(newSubscription)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$.subscriptionType", is("content"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].name", is("frequency"))) + .andExpect(jsonPath("$.subscriptionParameterList[0].value", is("W"))) + .andExpect(jsonPath("$._links.eperson.href", Matchers.endsWith("/eperson"))) + .andExpect(jsonPath("$._links.resource.href", Matchers.endsWith("/resource"))); + } + + @Test + public void linkedEpersonOfSubscriptionAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("M"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/" + subscription.getID() + "/eperson")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is(EPersonMatcher.matchEPersonEntry(eperson)))); + } + + @Test + public void linkedEpersonOfSubscriptionTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("M"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions/" + subscription.getID() + "/eperson")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", is(EPersonMatcher.matchEPersonEntry(eperson)))); + } + + @Test + public void linkedEpersonOfSubscriptionUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("M"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/subscriptions/" + subscription.getID() + "/eperson")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void linkedEpersonOfSubscriptionForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("W"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, admin, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions/" + subscription.getID() + "/eperson")) + .andExpect(status().isForbidden()); + } + + @Test + public void linkedEpersonOfSubscriptionNotFoundTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/" + Integer.MAX_VALUE + "/eperson")) + .andExpect(status().isNotFound()); + } + + @Test + public void linkedDSpaceObjectOfSubscriptionAdminTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("W"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/" + subscription.getID() + "/resource")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.uuid", Matchers.is(collection.getID().toString()))) + .andExpect(jsonPath("$.name", Matchers.is(collection.getName()))) + .andExpect(jsonPath("$.type", Matchers.is("collection"))); + } + + @Test + public void linkedDSpaceObjectOfSubscriptionTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("W"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(eperson.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/" + subscription.getID() + "/resource")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.uuid", Matchers.is(collection.getID().toString()))) + .andExpect(jsonPath("$.name", Matchers.is(collection.getName()))) + .andExpect(jsonPath("$.type", Matchers.is("collection"))); + } + + @Test + public void linkedDSpaceObjectOfSubscriptionUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("W"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + getClient().perform(get("/api/core/subscriptions/" + subscription.getID() + "/resource")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void linkedDSpaceObjectOfSubscriptionForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", collection, admin, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions/" + subscription.getID() + "/resource")) + .andExpect(status().isForbidden()); + } + + @Test + public void linkedDSpaceObjectAndRestrictedAccessAfterYouHaveSubscriptionToItTest() throws Exception { + context.turnOffAuthorisationSystem(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1 Test") + .withSubmitterGroup(eperson) + .build(); + + List subscriptionParameterList = new ArrayList<>(); + SubscriptionParameter subscriptionParameter = new SubscriptionParameter(); + subscriptionParameter.setName("frequency"); + subscriptionParameter.setValue("D"); + subscriptionParameterList.add(subscriptionParameter); + Subscription subscription = SubscribeBuilder.subscribeBuilder(context, + "content", col1, eperson, subscriptionParameterList).build(); + context.restoreAuthSystemState(); + + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + getClient(tokenEPerson).perform(get("/api/core/subscriptions/" + subscription.getID() + "/resource")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.uuid", Matchers.is(col1.getID().toString()))) + .andExpect(jsonPath("$.name", Matchers.is(col1.getName()))) + .andExpect(jsonPath("$.type", Matchers.is("collection"))); + + context.turnOffAuthorisationSystem(); + // remove all policies for col1 + resourcePolicyService.removeAllPolicies(context, col1); + context.restoreAuthSystemState(); + + // prove that col1 become not accessible + getClient(tokenEPerson).perform(get("/api/core/subscriptions/" + subscription.getID() + "/resource")) + .andExpect(status().isNoContent()); + } + + @Test + public void linkedDSpaceObjectOfSubscriptionNotFoundTest() throws Exception { + String tokenAdmin = getAuthToken(admin.getEmail(), password); + getClient(tokenAdmin).perform(get("/api/core/subscriptions/" + Integer.MAX_VALUE + "/resource")) + .andExpect(status().isNotFound()); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SystemWideAlertRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SystemWideAlertRestRepositoryIT.java new file mode 100644 index 0000000000..beb979dfe6 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SystemWideAlertRestRepositoryIT.java @@ -0,0 +1,500 @@ +/** + * 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; + +import static com.jayway.jsonpath.JsonPath.read; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.atomic.AtomicReference; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dspace.alerts.AllowSessionsEnum; +import org.dspace.alerts.SystemWideAlert; +import org.dspace.app.rest.model.SystemWideAlertRest; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.SystemWideAlertBuilder; +import org.junit.Test; + +/** + * Test class to test the operations in the SystemWideAlertRestRepository + */ +public class SystemWideAlertRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Test + public void findAllTest() throws Exception { + // Create two alert entries in the db to fully test the findAll method + // Note: It is not possible to create two alerts through the REST API + context.turnOffAuthorisationSystem(); + Date countdownDate = new Date(); + SystemWideAlert systemWideAlert1 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 1") + .withAllowSessions( + AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY) + .withCountdownDate(countdownDate) + .isActive(true) + .build(); + + SystemWideAlert systemWideAlert2 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 2") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(false) + .build(); + context.restoreAuthSystemState(); + + DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(get("/api/system/systemwidealerts/")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.systemwidealerts", containsInAnyOrder( + allOf( + hasJsonPath("$.alertId", is(systemWideAlert1.getID())), + hasJsonPath("$.message", is(systemWideAlert1.getMessage())), + hasJsonPath("$.allowSessions", is(systemWideAlert1.getAllowSessions().getValue())), + hasJsonPath("$.countdownTo", + startsWith(sdf.format(systemWideAlert1.getCountdownTo()))), + hasJsonPath("$.active", is(systemWideAlert1.isActive())) + ), + allOf( + hasJsonPath("$.alertId", is(systemWideAlert2.getID())), + hasJsonPath("$.message", is(systemWideAlert2.getMessage())), + hasJsonPath("$.allowSessions", is(systemWideAlert2.getAllowSessions().getValue())), + hasJsonPath("$.countdownTo", is(systemWideAlert2.getCountdownTo())), + hasJsonPath("$.active", is(systemWideAlert2.isActive())) + ) + ))); + } + + @Test + public void findAllUnauthorizedTest() throws Exception { + // Create two alert entries in the db to fully test the findAll method + // Note: It is not possible to create two alerts through the REST API + context.turnOffAuthorisationSystem(); + Date countdownDate = new Date(); + SystemWideAlert systemWideAlert1 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 1") + .withAllowSessions( + AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY) + .withCountdownDate(countdownDate) + .isActive(true) + .build(); + + SystemWideAlert systemWideAlert2 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 2") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(false) + .build(); + context.restoreAuthSystemState(); + + getClient().perform(get("/api/system/systemwidealerts/")) + .andExpect(status().isUnauthorized()); + + } + + @Test + public void findAllForbiddenTest() throws Exception { + // Create two alert entries in the db to fully test the findAll method + // Note: It is not possible to create two alerts through the REST API + context.turnOffAuthorisationSystem(); + Date countdownDate = new Date(); + SystemWideAlert systemWideAlert1 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 1") + .withAllowSessions( + AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY) + .withCountdownDate(countdownDate) + .isActive(true) + .build(); + + SystemWideAlert systemWideAlert2 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 2") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(false) + .build(); + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/system/systemwidealerts/")) + .andExpect(status().isForbidden()); + + } + + @Test + public void findOneTest() throws Exception { + // Create two alert entries in the db to fully test the findOne method + // Note: It is not possible to create two alerts through the REST API + context.turnOffAuthorisationSystem(); + Date countdownDate = new Date(); + SystemWideAlert systemWideAlert1 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 1") + .withAllowSessions( + AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY) + .withCountdownDate(countdownDate) + .isActive(true) + .build(); + SystemWideAlert systemWideAlert2 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 2") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(false) + .build(); + context.restoreAuthSystemState(); + + DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + String authToken = getAuthToken(admin.getEmail(), password); + + // When the alert is active and the user is not an admin, the user will be able to see the alert + getClient(authToken).perform(get("/api/system/systemwidealerts/" + systemWideAlert1.getID())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", allOf( + hasJsonPath("$.alertId", is(systemWideAlert1.getID())), + hasJsonPath("$.message", is(systemWideAlert1.getMessage())), + hasJsonPath("$.allowSessions", + is(systemWideAlert1.getAllowSessions().getValue())), + hasJsonPath("$.countdownTo", + startsWith(sdf.format(systemWideAlert1.getCountdownTo()))), + hasJsonPath("$.active", is(systemWideAlert1.isActive())) + ) + )); + + } + + + @Test + public void findOneUnauthorizedTest() throws Exception { + // Create two alert entries in the db to fully test the findOne method + // Note: It is not possible to create two alerts through the REST API + context.turnOffAuthorisationSystem(); + Date countdownDate = new Date(); + SystemWideAlert systemWideAlert1 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 1") + .withAllowSessions( + AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY) + .withCountdownDate(countdownDate) + .isActive(true) + .build(); + + SystemWideAlert systemWideAlert2 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 2") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(false) + .build(); + context.restoreAuthSystemState(); + + DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + // When the alert is active and the user is not an admin, the user will be able to see the alert + getClient().perform(get("/api/system/systemwidealerts/" + systemWideAlert1.getID())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", allOf( + hasJsonPath("$.alertId", is(systemWideAlert1.getID())), + hasJsonPath("$.message", is(systemWideAlert1.getMessage())), + hasJsonPath("$.allowSessions", + is(systemWideAlert1.getAllowSessions().getValue())), + hasJsonPath("$.countdownTo", + startsWith(sdf.format(systemWideAlert1.getCountdownTo()))), + hasJsonPath("$.active", is(systemWideAlert1.isActive())) + ) + )); + + // When the alert is inactive and the user is not an admin, the user will not be able to see the presence of the + // alert and a 404 will be returned by the findOne endpoint + getClient().perform(get("/api/system/systemwidealerts/" + systemWideAlert2.getID())) + .andExpect(status().isNotFound()); + + } + + @Test + public void findOneForbiddenTest() throws Exception { + // Create two alert entries in the db to fully test the findOne method + // Note: It is not possible to create two alerts through the REST API + context.turnOffAuthorisationSystem(); + Date countdownDate = new Date(); + SystemWideAlert systemWideAlert1 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 1") + .withAllowSessions( + AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY) + .withCountdownDate(countdownDate) + .isActive(true) + .build(); + + SystemWideAlert systemWideAlert2 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 2") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(false) + .build(); + context.restoreAuthSystemState(); + + DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + String authToken = getAuthToken(eperson.getEmail(), password); + + + getClient(authToken).perform(get("/api/system/systemwidealerts/" + systemWideAlert1.getID())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", allOf( + hasJsonPath("$.alertId", is(systemWideAlert1.getID())), + hasJsonPath("$.message", is(systemWideAlert1.getMessage())), + hasJsonPath("$.allowSessions", + is(systemWideAlert1.getAllowSessions().getValue())), + hasJsonPath("$.countdownTo", + startsWith(sdf.format(systemWideAlert1.getCountdownTo()))), + hasJsonPath("$.active", is(systemWideAlert1.isActive())) + ) + )); + + // When the alert is inactive and the user is not an admin, the user will not be able to see the presence of the + // alert and a 404 will be returned by the findOne endpoint + getClient(authToken).perform(get("/api/system/systemwidealerts/" + systemWideAlert2.getID())) + .andExpect(status().isNotFound()); + + } + + @Test + public void findAllActiveTest() throws Exception { + // Create three alert entries in the db to fully test the findActive search method + // Note: It is not possible to create two alerts through the REST API + context.turnOffAuthorisationSystem(); + Date countdownDate = new Date(); + SystemWideAlert systemWideAlert1 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 1") + .withAllowSessions( + AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY) + .withCountdownDate(countdownDate) + .isActive(true) + .build(); + + SystemWideAlert systemWideAlert2 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 2") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(false) + .build(); + + SystemWideAlert systemWideAlert3 = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert 3") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(true) + .build(); + context.restoreAuthSystemState(); + + DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + getClient().perform(get("/api/system/systemwidealerts/search/active")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.systemwidealerts", containsInAnyOrder( + allOf( + hasJsonPath("$.alertId", is(systemWideAlert1.getID())), + hasJsonPath("$.message", is(systemWideAlert1.getMessage())), + hasJsonPath("$.allowSessions", is(systemWideAlert1.getAllowSessions().getValue())), + hasJsonPath("$.countdownTo", + startsWith(sdf.format(systemWideAlert1.getCountdownTo()))), + hasJsonPath("$.active", is(systemWideAlert1.isActive())) + ), + allOf( + hasJsonPath("$.alertId", is(systemWideAlert3.getID())), + hasJsonPath("$.message", is(systemWideAlert3.getMessage())), + hasJsonPath("$.allowSessions", is(systemWideAlert3.getAllowSessions().getValue())), + hasJsonPath("$.countdownTo", is(systemWideAlert3.getCountdownTo())), + hasJsonPath("$.active", is(systemWideAlert3.isActive())) + ) + ))); + + } + + @Test + public void createTest() throws Exception { + SystemWideAlertRest systemWideAlertRest = new SystemWideAlertRest(); + systemWideAlertRest.setMessage("Alert test message"); + systemWideAlertRest.setCountdownTo(new Date()); + systemWideAlertRest.setAllowSessions(AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY.getValue()); + systemWideAlertRest.setActive(true); + + ObjectMapper mapper = new ObjectMapper(); + + String authToken = getAuthToken(admin.getEmail(), password); + + AtomicReference idRef = new AtomicReference<>(); + + + DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + getClient(authToken).perform(post("/api/system/systemwidealerts/") + .content(mapper.writeValueAsBytes(systemWideAlertRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andExpect( + jsonPath("$", allOf( + hasJsonPath("$.alertId"), + hasJsonPath("$.message", is(systemWideAlertRest.getMessage())), + hasJsonPath("$.allowSessions", + is(systemWideAlertRest.getAllowSessions())), + hasJsonPath("$.countdownTo", + startsWith(sdf.format(systemWideAlertRest.getCountdownTo()))), + hasJsonPath("$.active", is(systemWideAlertRest.isActive())) + ) + )) + .andDo(result -> idRef + .set((read(result.getResponse().getContentAsString(), "$.alertId")))); + + getClient(authToken).perform(get("/api/system/systemwidealerts/" + idRef.get())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", allOf( + hasJsonPath("$.alertId", is(idRef.get())), + hasJsonPath("$.message", is(systemWideAlertRest.getMessage())), + hasJsonPath("$.allowSessions", is(systemWideAlertRest.getAllowSessions())), + hasJsonPath("$.countdownTo", + startsWith(sdf.format(systemWideAlertRest.getCountdownTo()))), + hasJsonPath("$.active", is(systemWideAlertRest.isActive())) + ) + )); + + } + + @Test + public void createForbiddenTest() throws Exception { + + SystemWideAlertRest systemWideAlertRest = new SystemWideAlertRest(); + systemWideAlertRest.setMessage("Alert test message"); + systemWideAlertRest.setCountdownTo(new Date()); + systemWideAlertRest.setAllowSessions(AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY.getValue()); + systemWideAlertRest.setActive(true); + + ObjectMapper mapper = new ObjectMapper(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + + getClient(authToken).perform(post("/api/system/systemwidealerts/") + .content(mapper.writeValueAsBytes(systemWideAlertRest)) + .contentType(contentType)) + .andExpect(status().isForbidden()); + } + + @Test + public void createUnAuthorizedTest() throws Exception { + + SystemWideAlertRest systemWideAlertRest = new SystemWideAlertRest(); + systemWideAlertRest.setMessage("Alert test message"); + systemWideAlertRest.setCountdownTo(new Date()); + systemWideAlertRest.setAllowSessions(AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY.getValue()); + systemWideAlertRest.setActive(true); + + ObjectMapper mapper = new ObjectMapper(); + + getClient().perform(post("/api/system/systemwidealerts/") + .content(mapper.writeValueAsBytes(systemWideAlertRest)) + .contentType(contentType)) + .andExpect(status().isUnauthorized()); + + } + + + @Test + public void createWhenAlreadyExistsTest() throws Exception { + context.turnOffAuthorisationSystem(); + + SystemWideAlert systemWideAlert = SystemWideAlertBuilder.createSystemWideAlert(context, "Test alert") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(false) + .build(); + + context.restoreAuthSystemState(); + + SystemWideAlertRest systemWideAlertRest = new SystemWideAlertRest(); + systemWideAlertRest.setMessage("Alert test message"); + systemWideAlertRest.setCountdownTo(new Date()); + systemWideAlertRest.setAllowSessions(AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY.getValue()); + systemWideAlertRest.setActive(true); + + ObjectMapper mapper = new ObjectMapper(); + + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(post("/api/system/systemwidealerts/") + .content(mapper.writeValueAsBytes(systemWideAlertRest)) + .contentType(contentType)) + .andExpect(status().isBadRequest()); + + } + + @Test + public void putTest() throws Exception { + context.turnOffAuthorisationSystem(); + SystemWideAlert systemWideAlert = SystemWideAlertBuilder.createSystemWideAlert(context, "Alert test message") + .withAllowSessions( + AllowSessionsEnum.ALLOW_ADMIN_SESSIONS_ONLY) + .withCountdownDate(null) + .isActive(false) + .build(); + context.restoreAuthSystemState(); + + SystemWideAlertRest systemWideAlertRest = new SystemWideAlertRest(); + systemWideAlertRest.setAlertId(systemWideAlert.getID()); + systemWideAlertRest.setMessage("Updated alert test message"); + systemWideAlertRest.setCountdownTo(new Date()); + systemWideAlertRest.setAllowSessions(AllowSessionsEnum.ALLOW_CURRENT_SESSIONS_ONLY.getValue()); + systemWideAlertRest.setActive(true); + + ObjectMapper mapper = new ObjectMapper(); + + String authToken = getAuthToken(admin.getEmail(), password); + + + DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + getClient(authToken).perform(put("/api/system/systemwidealerts/" + systemWideAlert.getID()) + .content(mapper.writeValueAsBytes(systemWideAlertRest)) + .contentType(contentType)) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", allOf( + hasJsonPath("$.alertId"), + hasJsonPath("$.message", is(systemWideAlertRest.getMessage())), + hasJsonPath("$.allowSessions", + is(systemWideAlertRest.getAllowSessions())), + hasJsonPath("$.countdownTo", + startsWith(sdf.format(systemWideAlertRest.getCountdownTo()))), + hasJsonPath("$.active", is(systemWideAlertRest.isActive())) + ) + )); + + getClient(authToken).perform(get("/api/system/systemwidealerts/" + systemWideAlert.getID())) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", allOf( + hasJsonPath("$.alertId", is(systemWideAlert.getID())), + hasJsonPath("$.message", is(systemWideAlertRest.getMessage())), + hasJsonPath("$.allowSessions", is(systemWideAlertRest.getAllowSessions())), + hasJsonPath("$.countdownTo", + startsWith(sdf.format(systemWideAlertRest.getCountdownTo()))), + hasJsonPath("$.active", is(systemWideAlertRest.isActive())) + ) + )); + + + } + + +} + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/TaskRestRepositoriesIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/TaskRestRepositoriesIT.java index 17bc8edf46..a9b5c6a582 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/TaskRestRepositoriesIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/TaskRestRepositoriesIT.java @@ -44,6 +44,7 @@ import org.dspace.builder.ClaimedTaskBuilder; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.GroupBuilder; import org.dspace.builder.ItemBuilder; import org.dspace.builder.PoolTaskBuilder; import org.dspace.builder.WorkflowItemBuilder; @@ -52,11 +53,11 @@ import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; -import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.GroupService; import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; import org.dspace.xmlworkflow.state.Step; import org.dspace.xmlworkflow.state.actions.WorkflowActionConfig; +import org.dspace.xmlworkflow.state.actions.processingaction.SelectReviewerAction; import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; import org.dspace.xmlworkflow.storedcomponents.PoolTask; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; @@ -78,6 +79,9 @@ public class TaskRestRepositoriesIT extends AbstractControllerIntegrationTest { @Autowired private XmlWorkflowFactory xmlWorkflowFactory; + @Autowired + GroupService groupService; + @Test /** * Retrieve a specific pooltask @@ -4174,9 +4178,6 @@ public class TaskRestRepositoriesIT extends AbstractControllerIntegrationTest { @Test public void addReviewerToRunningWorkflowTest() throws Exception { - - GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); - context.turnOffAuthorisationSystem(); EPerson reviewer1 = EPersonBuilder.createEPerson(context) @@ -4329,4 +4330,384 @@ public class TaskRestRepositoriesIT extends AbstractControllerIntegrationTest { .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); } + /** + * Test the run of the selectSingleReviewer workflow + * - Creates ‘ReviewManagers’ and ‘Reviewers’, each with some members + * - Creates a normal user, not member of either group, this user is set on context + * - Tests selecting a single reviewer, multiple reviewers and selecting a non-reviewer + * + * @throws Exception + */ + @Test + public void selectReviewerWorkflowTest() throws Exception { + context.turnOffAuthorisationSystem(); + + // Create normal user, not member of "ReviewManagers" or "Reviewers" and set as current user + EPerson user = EPersonBuilder.createEPerson(context) + .withEmail("user@example.com") + .withPassword(password).build(); + context.setCurrentUser(user); + + // Create creator as this user is member of "ReviewManagers" for this item + EPerson creator = EPersonBuilder.createEPerson(context) + .withEmail("creator@example.com") + .withPassword(password).build(); + + // Create with some members to be added to "ReviewManagers" + EPerson reviewManager1 = EPersonBuilder.createEPerson(context) + .withEmail("reviewManager1@example.com") + .withPassword(password).build(); + + EPerson reviewManager2 = EPersonBuilder.createEPerson(context) + .withEmail("reviewManager2@example.com") + .withPassword(password).build(); + + EPerson reviewManager3 = EPersonBuilder.createEPerson(context) + .withEmail("reviewManager3@example.com") + .withPassword(password).build(); + + // The "selectSingleReviewer" requires the "ReviewManagers" repository group to be present with at least 1 + // member + GroupBuilder.createGroup(context) + .withName("ReviewManagers") + .addMember(reviewManager1) + .addMember(reviewManager2) + .addMember(reviewManager3) + .build(); + + // Create "Reviewers" with some members + Group reviewerGroup = GroupBuilder.createGroup(context).withName("Reviewers").build(); + + EPerson reviewer1 = EPersonBuilder.createEPerson(context) + .withEmail("reviewer1@example.com") + .withPassword(password) + .withGroupMembership(reviewerGroup).build(); + + EPerson reviewer2 = EPersonBuilder.createEPerson(context) + .withEmail("reviewer2@example.com") + .withPassword(password) + .withGroupMembership(reviewerGroup).build(); + + EPerson reviewer3 = EPersonBuilder.createEPerson(context) + .withEmail("reviewer3@example.com") + .withPassword(password) + .withGroupMembership(reviewerGroup).build(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community").build(); + + // Create collection with handle "123456789/workflow-test-1" to use "selectSingleReviewer" + Collection collection = + CollectionBuilder.createCollection(context, parentCommunity, "123456789/workflow-test-1") + .withName("Collection 1") + .build(); + + // Create 3 pool tasks + // First one for selecting a single reviewer + PoolTask poolTask1 = PoolTaskBuilder.createPoolTask(context, collection, reviewManager1) + .withTitle("Workflow Item 1") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald") + .withAuthor("Doe, John") + .withSubject("ExtraEntry").build(); + XmlWorkflowItem witem1 = poolTask1.getWorkflowItem(); + // Second one for selecting multiple reviewers + PoolTask poolTask2 = PoolTaskBuilder.createPoolTask(context, collection, reviewManager2) + .withTitle("Workflow Item 2") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald") + .withAuthor("Doe, John") + .withSubject("ExtraEntry").build(); + XmlWorkflowItem witem2 = poolTask2.getWorkflowItem(); + // Third one for trying to add user not in "Reviewers" group + PoolTask poolTask3 = PoolTaskBuilder.createPoolTask(context, collection, reviewManager3) + .withTitle("Workflow Item 3") + .withIssueDate("2017-10-17") + .withAuthor("Smith, Donald") + .withAuthor("Doe, John") + .withSubject("ExtraEntry").build(); + XmlWorkflowItem witem3 = poolTask3.getWorkflowItem(); + + context.restoreAuthSystemState(); + + String reviewManager1Token = getAuthToken(reviewManager1.getEmail(), password); + String reviewManager2Token = getAuthToken(reviewManager2.getEmail(), password); + String reviewManager3Token = getAuthToken(reviewManager3.getEmail(), password); + + String reviewer1Token = getAuthToken(reviewer1.getEmail(), password); + String reviewer2Token = getAuthToken(reviewer2.getEmail(), password); + String reviewer3Token = getAuthToken(reviewer3.getEmail(), password); + + String adminToken = getAuthToken(admin.getEmail(), password); + String userToken = getAuthToken(user.getEmail(), password); + + AtomicReference idRef = new AtomicReference<>(); + + // Verify as member of "ReviewManagers" you can find these pool tasks + getClient(reviewManager1Token).perform(get("/api/workflow/pooltasks/" + poolTask1.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is(PoolTaskMatcher.matchPoolTask(poolTask1, "selectReviewerStep")))); + + // Verify as member of "Reviewers" you can not find these pool tasks + getClient(reviewer1Token).perform(get("/api/workflow/pooltasks/" + poolTask1.getID())) + .andExpect(status().isForbidden()); + + // Verify as member of "ReviewManagers" you can claim in this tasks + getClient(reviewManager1Token).perform(post("/api/workflow/claimedtasks") + .contentType( + MediaType.parseMediaType(RestMediaTypes.TEXT_URI_LIST_VALUE)) + .content("/api/workflow/pooltasks/" + poolTask1.getID())) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf(hasJsonPath("$.type", is("claimedtask"))))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + // Verify that pool task 1 no longer exists + getClient(reviewManager1Token).perform(get("/api/workflow/pooltasks/" + poolTask1.getID())) + .andExpect(status().isNotFound()); + + // Verify items now in claimed tasks /api/workflow/claimedtasks for user reviewManager1 + getClient(reviewManager1Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewManager1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.claimedtasks", Matchers.contains( + Matchers.allOf( + hasJsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks/")), + hasJsonPath("$.type", Matchers.is("claimedtask")), + hasJsonPath("$._embedded.owner", + Matchers.is(EPersonMatcher.matchEPersonOnEmail(reviewManager1.getEmail()))), + hasJsonPath("$._embedded.action.id", Matchers.is("selectrevieweraction")), + hasJsonPath("$._embedded.workflowitem", + Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( + witem1, "Workflow Item 1", "2017-10-17", "ExtraEntry"))) + )))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Verify items now not in claimed tasks /api/workflow/claimedtasks for user reviewer1 + getClient(reviewer1Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewer1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test for single reviewer + SelectReviewerAction.resetGroup(); + // Select reviewer1 as a reviewer, wf step 1 + getClient(reviewManager1Token).perform(post("/api/workflow/claimedtasks/" + idRef.get()) + .param("submit_select_reviewer", "true") + .param("eperson", reviewer1.getID().toString()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andExpect(status().isNoContent()); + + // Verify reviewer1 has the claimed task + getClient(reviewer1Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewer1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.claimedtasks", Matchers.contains( + Matchers.allOf( + hasJsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks/")), + hasJsonPath("$.type", Matchers.is("claimedtask")), + hasJsonPath("$._embedded.owner", + Matchers.is(EPersonMatcher.matchEPersonOnEmail(reviewer1.getEmail()))), + hasJsonPath("$._embedded.action.id", Matchers.is("singleuserreviewaction")), + hasJsonPath("$._embedded.workflowitem", + Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( + witem1, "Workflow Item 1", "2017-10-17", "ExtraEntry"))) + )))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Verify other members of "Reviewers" don't have this task claimed + getClient(reviewer2Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewer2.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + getClient(reviewer3Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewer3.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + // Verify members of "ReviewManagers" don't have this task claimed + getClient(reviewManager1Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewManager1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + getClient(reviewManager2Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewManager2.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + getClient(reviewManager3Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewManager3.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + // Verify other users don't have this task claimed + getClient(userToken).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", user.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test for multiple reviewers + + // Claim pooltask2 as member of "ReviewManagers" + getClient(reviewManager2Token).perform(post("/api/workflow/claimedtasks") + .contentType( + MediaType.parseMediaType(RestMediaTypes.TEXT_URI_LIST_VALUE)) + .content("/api/workflow/pooltasks/" + poolTask2.getID())) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf(hasJsonPath("$.type", is("claimedtask"))))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + // Select reviewer2 and reviewer3 as reviewers, wf step 1 + getClient(reviewManager2Token).perform(post("/api/workflow/claimedtasks/" + idRef.get()) + .param("submit_select_reviewer", "true") + .param("eperson", reviewer2.getID().toString()) + .param("eperson", reviewer3.getID().toString()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andExpect(status().isNoContent()); + + // Verify reviewer2 has the claimed task + getClient(reviewer2Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewer2.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.claimedtasks", Matchers.contains( + Matchers.allOf( + hasJsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks/")), + hasJsonPath("$.type", Matchers.is("claimedtask")), + hasJsonPath("$._embedded.owner", + Matchers.is(EPersonMatcher.matchEPersonOnEmail(reviewer2.getEmail()))), + hasJsonPath("$._embedded.action.id", Matchers.is("singleuserreviewaction")), + hasJsonPath("$._embedded.workflowitem", + Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( + witem2, "Workflow Item 2", "2017-10-17", "ExtraEntry"))) + )))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Verify reviewer3 has the claimed task too + getClient(reviewer3Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewer3.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.claimedtasks", Matchers.contains( + Matchers.allOf( + hasJsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks/")), + hasJsonPath("$.type", Matchers.is("claimedtask")), + hasJsonPath("$._embedded.owner", + Matchers.is(EPersonMatcher.matchEPersonOnEmail(reviewer3.getEmail()))), + hasJsonPath("$._embedded.action.id", Matchers.is("singleuserreviewaction")), + hasJsonPath("$._embedded.workflowitem", + Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( + witem2, "Workflow Item 2", "2017-10-17", "ExtraEntry"))) + )))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // Verify reviewer1 of "Reviewers" doesn't have this task claimed, only the first task + getClient(reviewer1Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewer1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.claimedtasks", Matchers.contains( + Matchers.allOf( + hasJsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks/")), + hasJsonPath("$.type", Matchers.is("claimedtask")), + hasJsonPath("$._embedded.owner", + Matchers.is(EPersonMatcher.matchEPersonOnEmail(reviewer1.getEmail()))), + hasJsonPath("$._embedded.action.id", Matchers.is("singleuserreviewaction")), + hasJsonPath("$._embedded.workflowitem", + Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( + witem1, "Workflow Item 1", "2017-10-17", "ExtraEntry"))), + hasJsonPath("$._embedded.workflowitem", + Matchers.not(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( + witem2, "Workflow Item 2", "2017-10-17", "ExtraEntry"))) + )))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + // Verify members of "ReviewManagers" don't have this task claimed + getClient(reviewManager1Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewManager1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + getClient(reviewManager2Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewManager2.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + getClient(reviewManager3Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewManager3.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + // Verify other users don't have this task claimed + getClient(userToken).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", user.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Test to assign non-reviewer + + // Claim pooltask3 as member of "ReviewManagers" + getClient(reviewManager3Token).perform(post("/api/workflow/claimedtasks") + .contentType( + MediaType.parseMediaType(RestMediaTypes.TEXT_URI_LIST_VALUE)) + .content("/api/workflow/pooltasks/" + poolTask3.getID())) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", Matchers.allOf(hasJsonPath("$.type", is("claimedtask"))))) + .andDo(result -> idRef.set(read(result.getResponse().getContentAsString(), "$.id"))); + + // Select (non-reviewer) user as a reviewer, wf step 1 + getClient(reviewManager3Token).perform(post("/api/workflow/claimedtasks/" + idRef.get()) + .param("submit_select_reviewer", "true") + .param("eperson", user.getID().toString()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED)) + .andExpect(status().isNoContent()); + + // Verify user does not have this task claimed + getClient(userToken).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", user.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // Verify item still in claimed tasks for user reviewManager3 on step "selectrevieweraction" + getClient(reviewManager3Token).perform(get("/api/workflow/claimedtasks/search/findByUser") + .param("uuid", reviewManager3.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.claimedtasks", Matchers.contains( + Matchers.allOf( + hasJsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks/")), + hasJsonPath("$.type", Matchers.is("claimedtask")), + hasJsonPath("$._embedded.owner", + Matchers.is(EPersonMatcher.matchEPersonOnEmail(reviewManager3.getEmail()))), + hasJsonPath("$._embedded.action.id", Matchers.is("selectrevieweraction")), + hasJsonPath("$._embedded.workflowitem", + Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( + witem3, "Workflow Item 3", "2017-10-17", "ExtraEntry"))) + )))) + .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowActionRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowActionRestRepositoryIT.java index de687ebd9d..ec963fd2f3 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowActionRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowActionRestRepositoryIT.java @@ -7,7 +7,9 @@ */ package org.dspace.app.rest; +import static org.dspace.xmlworkflow.state.actions.processingaction.ScoreReviewAction.SUBMIT_SCORE; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -17,9 +19,18 @@ import org.dspace.app.rest.matcher.WorkflowActionMatcher; import org.dspace.app.rest.model.WorkflowActionRest; import org.dspace.app.rest.repository.WorkflowActionRestRepository; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.GroupBuilder; +import org.dspace.eperson.Group; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.eperson.service.GroupService; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; import org.dspace.xmlworkflow.factory.XmlWorkflowServiceFactory; import org.dspace.xmlworkflow.state.actions.WorkflowActionConfig; +import org.dspace.xmlworkflow.state.actions.processingaction.ScoreReviewActionAdvancedInfo; +import org.dspace.xmlworkflow.state.actions.processingaction.SelectReviewerAction; +import org.dspace.xmlworkflow.state.actions.processingaction.SelectReviewerActionAdvancedInfo; import org.hamcrest.Matchers; import org.junit.Test; @@ -31,6 +42,8 @@ import org.junit.Test; public class WorkflowActionRestRepositoryIT extends AbstractControllerIntegrationTest { private XmlWorkflowFactory xmlWorkflowFactory = XmlWorkflowServiceFactory.getInstance().getWorkflowFactory(); + private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + private GroupService groupService = EPersonServiceFactory.getInstance().getGroupService(); private static final String WORKFLOW_ACTIONS_ENDPOINT = "/api/" + WorkflowActionRest.CATEGORY + "/" + WorkflowActionRest.NAME_PLURAL; @@ -82,6 +95,7 @@ public class WorkflowActionRestRepositoryIT extends AbstractControllerIntegratio .andExpect(status().isOk()) // has options .andExpect(jsonPath("$.options", not(empty()))) + .andExpect(jsonPath("$.advanced", is(false))) //Matches expected corresponding rest action values .andExpect(jsonPath("$", Matchers.is( WorkflowActionMatcher.matchWorkflowActionEntry(existentWorkflow) @@ -99,6 +113,7 @@ public class WorkflowActionRestRepositoryIT extends AbstractControllerIntegratio .andExpect(status().isOk()) // has no options .andExpect(jsonPath("$.options", empty())) + .andExpect(jsonPath("$.advanced", is(false))) //Matches expected corresponding rest action values .andExpect(jsonPath("$", Matchers.is( WorkflowActionMatcher.matchWorkflowActionEntry(existentWorkflowNoOptions) @@ -125,4 +140,68 @@ public class WorkflowActionRestRepositoryIT extends AbstractControllerIntegratio //We expect a 401 Unauthorized .andExpect(status().isUnauthorized()); } + + @Test + public void getWorkflowActionByName_ExistentWithOptions_ratingreviewaction() throws Exception { + String token = getAuthToken(eperson.getEmail(), password); + String nameActionWithOptions = "scorereviewaction"; + WorkflowActionConfig existentWorkflow = xmlWorkflowFactory.getActionByName(nameActionWithOptions); + + // create ScoreReviewActionAdvancedInfo to compare with output + ScoreReviewActionAdvancedInfo scoreReviewActionAdvancedInfo = new ScoreReviewActionAdvancedInfo(); + scoreReviewActionAdvancedInfo.setDescriptionRequired(true); + scoreReviewActionAdvancedInfo.setMaxValue(5); + scoreReviewActionAdvancedInfo.setType(SUBMIT_SCORE); + scoreReviewActionAdvancedInfo.generateId(SUBMIT_SCORE); + + //When we call this facets endpoint + getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT + "/" + nameActionWithOptions)) + //We expect a 200 is ok status + .andExpect(status().isOk()) + // has options + .andExpect(jsonPath("$.options", not(empty()))) + .andExpect(jsonPath("$.advancedOptions", not(empty()))) + .andExpect(jsonPath("$.advanced", is(true))) + .andExpect(jsonPath("$.advancedInfo", Matchers.contains( + WorkflowActionMatcher.matchScoreReviewActionAdvancedInfo(scoreReviewActionAdvancedInfo)))) + //Matches expected corresponding rest action values + .andExpect(jsonPath("$", Matchers.is( + WorkflowActionMatcher.matchWorkflowActionEntry(existentWorkflow) + ))); + } + + @Test + public void getWorkflowActionByName_ExistentWithOptions_selectrevieweraction() throws Exception { + String token = getAuthToken(eperson.getEmail(), password); + String nameActionWithOptions = "selectrevieweraction"; + // create reviewers group + SelectReviewerAction.resetGroup(); + context.turnOffAuthorisationSystem(); + Group group = GroupBuilder.createGroup(context).withName("ReviewersUUIDConfig").build(); + configurationService.setProperty("action.selectrevieweraction.group", group.getID()); + context.restoreAuthSystemState(); + + // create SelectReviewerActionAdvancedInfo to compare with output + SelectReviewerActionAdvancedInfo selectReviewerActionAdvancedInfo = new SelectReviewerActionAdvancedInfo(); + selectReviewerActionAdvancedInfo.setGroup(group.getID().toString()); + selectReviewerActionAdvancedInfo.setType("submit_select_reviewer"); + selectReviewerActionAdvancedInfo.generateId("submit_select_reviewer"); + + WorkflowActionConfig existentWorkflow = xmlWorkflowFactory.getActionByName(nameActionWithOptions); + //When we call this facets endpoint + getClient(token).perform(get(WORKFLOW_ACTIONS_ENDPOINT + "/" + nameActionWithOptions)) + //We expect a 200 is ok status + .andExpect(status().isOk()) + // has options + .andExpect(jsonPath("$.options", not(empty()))) + .andExpect(jsonPath("$.advancedOptions", not(empty()))) + .andExpect(jsonPath("$.advanced", is(true))) + .andExpect(jsonPath("$.advancedInfo", Matchers.contains( + WorkflowActionMatcher.matchSelectReviewerActionAdvancedInfo(selectReviewerActionAdvancedInfo)))) + //Matches expected corresponding rest action values + .andExpect(jsonPath("$", Matchers.is( + WorkflowActionMatcher.matchWorkflowActionEntry(existentWorkflow) + ))); + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSubscribeFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSubscribeFeatureIT.java new file mode 100644 index 0000000000..7eb0960566 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/CanSubscribeFeatureIT.java @@ -0,0 +1,310 @@ +/** + * 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.authorization; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.authorization.impl.CanSubscribeFeature; +import org.dspace.app.rest.converter.CollectionConverter; +import org.dspace.app.rest.converter.CommunityConverter; +import org.dspace.app.rest.converter.ItemConverter; +import org.dspace.app.rest.matcher.AuthorizationMatcher; +import org.dspace.app.rest.model.CollectionRest; +import org.dspace.app.rest.model.CommunityRest; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.projection.DefaultProjection; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.ResourcePolicy; +import org.dspace.authorize.service.ResourcePolicyService; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.GroupBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.ResourcePolicyBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.core.Constants; +import org.dspace.discovery.SearchServiceException; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Test of Subscribe Dso Feature implementation. + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) + */ +public class CanSubscribeFeatureIT extends AbstractControllerIntegrationTest { + + private static final Logger log = LogManager.getLogger(CanSubscribeFeatureIT.class); + + @Autowired + private ItemConverter itemConverter; + @Autowired + private CollectionConverter collectionConverter; + @Autowired + private CommunityConverter communityConverter; + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + @Autowired + private ResourcePolicyService resourcePolicyService; + + private Community communityAuthorized; + private Collection collectionAuthorized; + private AuthorizationFeature canSubscribeFeature; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Community") + .build(); + communityAuthorized = CommunityBuilder.createCommunity(context) + .withName("communityA") + .build(); + collectionAuthorized = CollectionBuilder.createCollection(context, communityAuthorized) + .withName("Collection A") + .build(); + context.restoreAuthSystemState(); + canSubscribeFeature = authorizationFeatureService.find(CanSubscribeFeature.NAME); + } + + @Test + public void canSubscribeCommunityAndCollectionTest() throws Exception { + context.turnOffAuthorisationSystem(); + CommunityRest comRest = communityConverter.convert(parentCommunity, DefaultProjection.DEFAULT); + CollectionRest colRest = collectionConverter.convert(collectionAuthorized, DefaultProjection.DEFAULT); + + // define authorizations that we know must exists + Authorization epersonToCommunity = new Authorization(eperson, canSubscribeFeature, comRest); + Authorization adminToCommunity = new Authorization(admin, canSubscribeFeature, comRest); + Authorization epersonToCollection = new Authorization(eperson, canSubscribeFeature, colRest); + Authorization adminToCollection = new Authorization(admin, canSubscribeFeature, colRest); + + // define authorization that we know not exists + Authorization anonymousToCommunity = new Authorization(null, canSubscribeFeature, comRest); + Authorization anonymousToCollection = new Authorization(null, canSubscribeFeature, colRest); + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + + getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + epersonToCommunity.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is( + AuthorizationMatcher.matchAuthorization(epersonToCommunity)))); + + getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + adminToCommunity.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is( + AuthorizationMatcher.matchAuthorization(adminToCommunity)))); + + getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + epersonToCollection.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is( + AuthorizationMatcher.matchAuthorization(epersonToCollection)))); + + getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + adminToCollection.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is( + AuthorizationMatcher.matchAuthorization(adminToCollection)))); + + getClient().perform(get("/api/authz/authorizations/" + anonymousToCommunity.getID())) + .andExpect(status().isNotFound()); + + getClient().perform(get("/api/authz/authorizations/" + anonymousToCollection.getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void canNotSubscribeItemTest() throws Exception { + context.turnOffAuthorisationSystem(); + EPerson ePersonNotSubscribePermission = EPersonBuilder.createEPerson(context) + .withCanLogin(true) + .withPassword(password) + .withEmail("test@email.it") + .build(); + // the user to be tested is not part of the group with read permission + Group groupWithReadPermission = GroupBuilder.createGroup(context) + .withName("Group A") + .addMember(eperson) + .build(); + Item item = ItemBuilder.createItem(context, collectionAuthorized) + .withTitle("Test item") + .build(); + + cleanUpPermissions(resourcePolicyService.find(context, item)); + setPermissions(item, groupWithReadPermission, Constants.READ); + + ItemRest itemRest = itemConverter.convert(item, Projection.DEFAULT); + + // define authorization that we know not exists + Authorization anonymousToItem = new Authorization(null, canSubscribeFeature, itemRest); + Authorization epersonToItem = new Authorization(eperson, canSubscribeFeature, itemRest); + Authorization adminToItem = new Authorization(admin, canSubscribeFeature, itemRest); + Authorization ePersonNotSubscribePermissionToItem = new Authorization(ePersonNotSubscribePermission, + canSubscribeFeature, itemRest); + + context.restoreAuthSystemState(); + + String token1 = getAuthToken(eperson.getEmail(), password); + String token2 = getAuthToken(admin.getEmail(), password); + String token3 = getAuthToken(ePersonNotSubscribePermission.getEmail(), password); + + getClient(token1).perform(get("/api/authz/authorizations/" + epersonToItem.getID())) + .andExpect(status().isNotFound()); + + getClient(token2).perform(get("/api/authz/authorizations/" + adminToItem.getID())) + .andExpect(status().isNotFound()); + + getClient(token3).perform(get("/api/authz/authorizations/" + ePersonNotSubscribePermissionToItem.getID())) + .andExpect(status().isNotFound()); + + getClient().perform(get("/api/authz/authorizations/" + anonymousToItem.getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void canNotSubscribeCollectionTest() throws Exception { + context.turnOffAuthorisationSystem(); + + EPerson ePersonNotSubscribePermission = EPersonBuilder.createEPerson(context) + .withCanLogin(true) + .withPassword(password) + .withEmail("test@email.it") + .build(); + + // the user to be tested is not part of the group with read permission + Group groupWithReadPermission = GroupBuilder.createGroup(context) + .withName("Group A") + .addMember(eperson) + .build(); + + cleanUpPermissions(resourcePolicyService.find(context, collectionAuthorized)); + setPermissions(collectionAuthorized, groupWithReadPermission, Constants.READ); + + CollectionRest collectionRest = collectionConverter.convert(collectionAuthorized, Projection.DEFAULT); + + // define authorizations that we know must exists + Authorization epersonToCollection = new Authorization(eperson, canSubscribeFeature, collectionRest); + Authorization adminToCollection = new Authorization(admin, canSubscribeFeature, collectionRest); + + // define authorization that we know not exists + Authorization ePersonNotSubscribePermissionToColl = new Authorization(ePersonNotSubscribePermission, + canSubscribeFeature, collectionRest); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + String token = getAuthToken(ePersonNotSubscribePermission.getEmail(), password); + + getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + epersonToCollection.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is( + AuthorizationMatcher.matchAuthorization(epersonToCollection)))); + + getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + adminToCollection.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is( + AuthorizationMatcher.matchAuthorization(adminToCollection)))); + + getClient(token).perform(get("/api/authz/authorizations/" + ePersonNotSubscribePermissionToColl.getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void canNotSubscribeCommunityTest() throws Exception { + context.turnOffAuthorisationSystem(); + + EPerson ePersonNotSubscribePermission = EPersonBuilder.createEPerson(context) + .withCanLogin(true) + .withPassword(password) + .withEmail("test@email.it") + .build(); + + // the user to be tested is not part of the group with read permission + Group groupWithReadPermission = GroupBuilder.createGroup(context) + .withName("Group A") + .addMember(eperson) + .build(); + + cleanUpPermissions(resourcePolicyService.find(context, communityAuthorized)); + setPermissions(communityAuthorized, groupWithReadPermission, Constants.READ); + + CommunityRest communityRest = communityConverter.convert(communityAuthorized, Projection.DEFAULT); + + // define authorizations that we know must exists + Authorization epersonToComm = new Authorization(eperson, canSubscribeFeature, communityRest); + Authorization adminToComm = new Authorization(admin, canSubscribeFeature, communityRest); + + // define authorization that we know not exists + Authorization ePersonNotSubscribePermissionToComm = new Authorization(ePersonNotSubscribePermission, + canSubscribeFeature, communityRest); + + context.restoreAuthSystemState(); + + String tokenAdmin = getAuthToken(admin.getEmail(), password); + String tokenEPerson = getAuthToken(eperson.getEmail(), password); + String token = getAuthToken(ePersonNotSubscribePermission.getEmail(), password); + + getClient(tokenEPerson).perform(get("/api/authz/authorizations/" + epersonToComm.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is( + AuthorizationMatcher.matchAuthorization(epersonToComm)))); + + getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + adminToComm.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", Matchers.is( + AuthorizationMatcher.matchAuthorization(adminToComm)))); + + getClient(token).perform(get("/api/authz/authorizations/" + ePersonNotSubscribePermissionToComm.getID())) + .andExpect(status().isNotFound()); + } + + private void setPermissions(DSpaceObject dSpaceObject, Group group, Integer permissions) { + try { + ResourcePolicyBuilder.createResourcePolicy(context) + .withDspaceObject(dSpaceObject) + .withAction(permissions) + .withGroup(group) + .build(); + } catch (SQLException | AuthorizeException sqlException) { + log.error(sqlException.getMessage()); + } + } + + private void cleanUpPermissions(List resourcePolicies) { + try { + for (ResourcePolicy resourcePolicy : resourcePolicies) { + ResourcePolicyBuilder.delete(resourcePolicy.getID()); + } + } catch (SQLException | SearchServiceException | IOException sqlException) { + log.error(sqlException.getMessage()); + } + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ItemMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ItemMatcher.java index 371ad6b4b4..64905f90ea 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ItemMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ItemMatcher.java @@ -52,6 +52,7 @@ public class ItemMatcher { return matchEmbeds( "accessStatus", "bundles[]", + "identifiers", "mappedCollections[]", "owningCollection", "version", @@ -68,6 +69,7 @@ public class ItemMatcher { return HalMatcher.matchLinks(REST_SERVER_URL + "core/items/" + uuid, "accessStatus", "bundles", + "identifiers", "mappedCollections", "owningCollection", "relationships", diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubscriptionMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubscriptionMatcher.java new file mode 100644 index 0000000000..88349c73a3 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubscriptionMatcher.java @@ -0,0 +1,63 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.dspace.app.rest.test.AbstractControllerIntegrationTest.REST_SERVER_URL; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; + +import java.util.stream.Collectors; + +import org.dspace.eperson.Subscription; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; + +/** + * Provide convenient org.hamcrest.Matcher to verify a SubscriptionRest json response + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) + */ +public class SubscriptionMatcher { + + private SubscriptionMatcher() {} + + public static Matcher matchSubscription(Subscription subscription) { + return allOf( + hasJsonPath("$.id", is(subscription.getID())), + hasJsonPath("$.type", is("subscription")), + hasJsonPath("$.subscriptionType", is(subscription.getSubscriptionType())), + hasJsonPath("$.subscriptionParameterList", Matchers.containsInAnyOrder( + subscription.getSubscriptionParameterList().stream() + .map(x -> SubscriptionMatcher.matchSubscriptionParameter(x.getName(), x.getValue())) + .collect(Collectors.toList()) + )), + //Check links + matchLinks(subscription.getID()) + ); + } + + public static Matcher matchSubscriptionParameter(String name, String value) { + return allOf( + hasJsonPath("$.name", is(name)), + hasJsonPath("$.value", is(value)) + ); + } + + /** + * Gets a matcher for all expected links. + */ + public static Matcher matchLinks(Integer id) { + return HalMatcher.matchLinks(REST_SERVER_URL + "core/subscriptions/" + id, + "resource", + "eperson", + "self" + ); + } + +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/WorkflowActionMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/WorkflowActionMatcher.java index 69f9c501aa..e727eb8acc 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/WorkflowActionMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/WorkflowActionMatcher.java @@ -14,7 +14,10 @@ import static org.hamcrest.Matchers.is; import org.dspace.app.rest.model.WorkflowActionRest; import org.dspace.xmlworkflow.state.actions.WorkflowActionConfig; +import org.dspace.xmlworkflow.state.actions.processingaction.ScoreReviewActionAdvancedInfo; +import org.dspace.xmlworkflow.state.actions.processingaction.SelectReviewerActionAdvancedInfo; import org.hamcrest.Matcher; +import org.hamcrest.Matchers; /** * @author Maria Verdonck (Atmire) on 06/01/2020 @@ -32,7 +35,35 @@ public class WorkflowActionMatcher { return allOf( hasJsonPath("$.id", is(workflowAction.getId())), hasJsonPath("$.options", is(workflowAction.getOptions())), + hasJsonPath("$.advanced", is(workflowAction.isAdvanced())), hasJsonPath("$._links.self.href", containsString(WORKFLOW_ACTIONS_ENDPOINT + workflowAction.getId())) ); } + + /** + * Matcher to check the contents of the advancedInfo for "ratingreviewaction" + * @param scoreReviewActionAdvancedInfo identical ScoreReviewActionAdvancedInfo object + */ + public static Matcher matchScoreReviewActionAdvancedInfo( + ScoreReviewActionAdvancedInfo scoreReviewActionAdvancedInfo) { + return Matchers.allOf( + hasJsonPath("$.descriptionRequired", is(scoreReviewActionAdvancedInfo.isDescriptionRequired())), + hasJsonPath("$.maxValue", is(scoreReviewActionAdvancedInfo.getMaxValue())), + hasJsonPath("$.type", is(scoreReviewActionAdvancedInfo.getType())), + hasJsonPath("$.id", is(scoreReviewActionAdvancedInfo.getId())) + ); + } + + /** + * Matcher to check the contents of the advancedInfo for "selectrevieweraction" + * @param selectReviewerActionAdvancedInfo identical SelectReviewerActionAdvancedInfo object + */ + public static Matcher matchSelectReviewerActionAdvancedInfo( + SelectReviewerActionAdvancedInfo selectReviewerActionAdvancedInfo) { + return Matchers.allOf( + hasJsonPath("$.group", is(selectReviewerActionAdvancedInfo.getGroup())), + hasJsonPath("$.type", is(selectReviewerActionAdvancedInfo.getType())), + hasJsonPath("$.id", is(selectReviewerActionAdvancedInfo.getId())) + ); + } } diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index 129ad0ca56..2a35e89459 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -1660,6 +1660,7 @@ include = ${module_dir}/discovery.cfg include = ${module_dir}/doi-curation.cfg include = ${module_dir}/google-analytics.cfg include = ${module_dir}/healthcheck.cfg +include = ${module_dir}/identifiers.cfg include = ${module_dir}/irus-statistics.cfg include = ${module_dir}/oai.cfg include = ${module_dir}/openaire-client.cfg diff --git a/dspace/config/emails/subscription b/dspace/config/emails/subscription deleted file mode 100644 index 2879e57907..0000000000 --- a/dspace/config/emails/subscription +++ /dev/null @@ -1,12 +0,0 @@ -## E-mail sent to DSpace users when new items appear in collections they are -## subscribed to -## -## Parameters: {0} is the details of the new collections and items -## See org.dspace.core.Email for information on the format of this file. -## -#set($subject = 'DSpace Subscription') -New items are available in the collections you have subscribed to: - -${params[0]} - -DSpace diff --git a/dspace/config/emails/subscriptions_content b/dspace/config/emails/subscriptions_content new file mode 100644 index 0000000000..86612bac54 --- /dev/null +++ b/dspace/config/emails/subscriptions_content @@ -0,0 +1,16 @@ +## E-mail sent to designated address about updates on subscribed items +## +## Parameters: {0} Collections updates +## {1} Communities updates +#set($subject = 'DSpace Subscription') + +This email is sent from DSpace based on the chosen subscription preferences. + +Communities +----------- +List of changed items : ${params[0]} + +Collections +----------- +List of changed items : ${params[1]} + diff --git a/dspace/config/hibernate.cfg.xml b/dspace/config/hibernate.cfg.xml index 085ed0bd6e..21aa74c515 100644 --- a/dspace/config/hibernate.cfg.xml +++ b/dspace/config/hibernate.cfg.xml @@ -54,6 +54,7 @@ + @@ -66,7 +67,7 @@ - + diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index 299a5fdfce..2ab26dcf57 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -209,6 +209,13 @@ org.dspace.app.rest.submit.step.SherpaPolicyStep sherpaPolicy + + + + submit.progressbar.identifiers + org.dspace.app.rest.submit.step.ShowIdentifiersStep + identifiers + @@ -233,7 +240,9 @@ - + + diff --git a/dspace/config/launcher.xml b/dspace/config/launcher.xml index be8564a0e6..13d8e0bef8 100644 --- a/dspace/config/launcher.xml +++ b/dspace/config/launcher.xml @@ -289,13 +289,6 @@ org.dspace.administer.StructBuilder - - sub-daily - Send daily subscription notices - - org.dspace.eperson.SubscribeCLITool - - test-email Test the DSpace email server settings are OK diff --git a/dspace/config/modules/identifiers.cfg b/dspace/config/modules/identifiers.cfg new file mode 100644 index 0000000000..63a9cda30f --- /dev/null +++ b/dspace/config/modules/identifiers.cfg @@ -0,0 +1,51 @@ +#----------------------------------------------------------------------# +#---------------------IDENTIFIER CONFIGURATIONS------------------------# +#----------------------------------------------------------------------# +# These configs are used for additional identifier configuration such # +# as the Show Identifiers step which can "pre-mint" DOIs and Handles # +#----------------------------------------------------------------------# + +# Should configured identifiers (eg handle and DOI) be minted for (future) registration at workspace item creation? +# A handle created at this stage will act just like a regular handle created at archive time. +# A DOI created at this stage will be in a 'PENDING' status while in workspace and workflow. +# At the time of item install, the DOI filter (if any) will be applied and if the item matches the filter, the DOI +# status will be updated to TO_BE_REGISTERED. An administrator can also manually progress the DOI status, overriding +# any filters, in the item status page. +# This option doesn't require the Show Identifiers submission step to be visible. +# Default: false +#identifiers.submission.register = true + +# This configuration property can be set to a filter name to determine if a PENDING DOI for an item +# should be queued for registration. If the filter doesn't match, the DOI will stay in PENDING or MINTED status +# so that the identifier itself persists in case it is considered for registration in the future. +# See doi-filter and other example filters in item-filters.xml. +# Default (always_true_filter) +#identifiers.submission.filter.install = doi-filter + +# This optional configuration property can be set to a filter name, in case there are some initial rules to apply +# when first deciding whether a DOI should be be created for a new workspace item with a PENDING status. +# This filter is only applied if identifiers.submission.register is true. +# This filter is updated as submission data is saved. +# Default: (always_true_filter) +#identifiers.submission.filter.workspace = always_true_filter + +# If true, the workspace filter will be applied as submission data is saved. If the filter no longer +# matches the item, the DOI will be shifted into a MINTED status and not displayed in the submission section. +# If false, then once a DOI has been created with PENDING status it will remain that way until final item install +# Default: true +#identifiers.submission.strip_pending_during_submission = true + +# This configuration property can be set to a filter name to determine if an item processed by RegisterDOI curation +# task should be eligible for a DOI +#identifiers.submission.filter.curation = always_true_filter + +# Show Register DOI button in item status page? +# Default: false +# This configuration property is exposed over rest. For dspace-angular to work, +# this property must always have a true or false value. Do not comment it out! +identifiers.item-status.register-doi = false + +# Which identifier types to show in submission step? +# Default: handle, doi (currently the only supported identifier 'types') +#identifiers.submission.display = handle +#identifiers.submission.display = doi diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 5bb58b0eee..6421258c57 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -46,6 +46,7 @@ rest.properties.exposed = google.recaptcha.key.site rest.properties.exposed = google.recaptcha.version rest.properties.exposed = google.recaptcha.mode rest.properties.exposed = cc.license.jurisdiction +rest.properties.exposed = identifiers.item-status.register-doi rest.properties.exposed = authentication-password.domain.valid #---------------------------------------------------------------# diff --git a/dspace/config/modules/workflow.cfg b/dspace/config/modules/workflow.cfg index 8d11df03d5..c77050d719 100644 --- a/dspace/config/modules/workflow.cfg +++ b/dspace/config/modules/workflow.cfg @@ -16,4 +16,9 @@ workflow.reviewer.file-edit=false # Notify reviewers about tasks returned to the pool -#workflow.notify.returned.tasks = true \ No newline at end of file +#workflow.notify.returned.tasks = true + +# Reviewer group for the select reviewer workflow (can be UUID or group name) +# This determines the group from which reviewers can be chosen +# If this is not set, the review manager can choose reviewers from all e-people instead of this selected group +action.selectrevieweraction.group = Reviewers diff --git a/dspace/config/registries/workflow-types.xml b/dspace/config/registries/workflow-types.xml index 3f26849bf2..a6417e3894 100644 --- a/dspace/config/registries/workflow-types.xml +++ b/dspace/config/registries/workflow-types.xml @@ -17,7 +17,13 @@ workflow score - Metadata field used for the score review + Metadata field used for the score review rating + + + + workflow + review + Metadata field used for the score review description diff --git a/dspace/config/spring/api/core-dao-services.xml b/dspace/config/spring/api/core-dao-services.xml index ae4b5e6e3b..fa8cd13ef5 100644 --- a/dspace/config/spring/api/core-dao-services.xml +++ b/dspace/config/spring/api/core-dao-services.xml @@ -34,11 +34,14 @@ + + + diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index a124ec830f..1854d8eee3 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -60,6 +60,8 @@ + + diff --git a/dspace/config/spring/api/identifier-service.xml b/dspace/config/spring/api/identifier-service.xml index e9f08003bd..dbcd49df0e 100644 --- a/dspace/config/spring/api/identifier-service.xml +++ b/dspace/config/spring/api/identifier-service.xml @@ -13,7 +13,7 @@ scope="singleton"/> @@ -42,7 +42,7 @@ a DOIConnector that handles all API calls to your DOI registration agency. Please configure a DOIConnector as well! --> - - + - - + + + + + + - - - + @@ -226,16 +241,12 @@ - - - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dc.title + + + + + + + diff --git a/dspace/config/spring/api/workflow-actions.xml b/dspace/config/spring/api/workflow-actions.xml index c1252f4b17..d01f1b6b4c 100644 --- a/dspace/config/spring/api/workflow-actions.xml +++ b/dspace/config/spring/api/workflow-actions.xml @@ -13,9 +13,12 @@ - + + + + - + @@ -61,6 +64,12 @@ + + + + + + diff --git a/dspace/config/spring/api/workflow.xml b/dspace/config/spring/api/workflow.xml index 004ff1d757..448e10fd46 100644 --- a/dspace/config/spring/api/workflow.xml +++ b/dspace/config/spring/api/workflow.xml @@ -151,6 +151,7 @@ +