diff --git a/.gitattributes b/.gitattributes index 03b42152e9..c00b03e686 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,12 @@ # Auto detect text files and perform LF normalization * text=auto +# Ensure Unix files always keep Unix line endings +*.sh text eol=lf + +# Ensure Windows files always keep Windows line endings +*.bat text eol=crlf + # Standard to msysgit *.doc diff=astextplain *.DOC diff=astextplain diff --git a/dspace-api/src/main/java/org/dspace/authority/AuthorityValueServiceImpl.java b/dspace-api/src/main/java/org/dspace/authority/AuthorityValueServiceImpl.java index 194da3eba5..fc62f0df19 100644 --- a/dspace-api/src/main/java/org/dspace/authority/AuthorityValueServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authority/AuthorityValueServiceImpl.java @@ -153,7 +153,7 @@ public class AuthorityValueServiceImpl implements AuthorityValueService { public List findByValue(Context context, String schema, String element, String qualifier, String value) { String field = fieldParameter(schema, element, qualifier); - return findByValue(context, field, qualifier); + return findByValue(context, field, value); } @Override diff --git a/dspace-api/src/main/java/org/dspace/submit/model/AccessConditionOption.java b/dspace-api/src/main/java/org/dspace/submit/model/AccessConditionOption.java index ca8efc09c4..398097a9ec 100644 --- a/dspace-api/src/main/java/org/dspace/submit/model/AccessConditionOption.java +++ b/dspace-api/src/main/java/org/dspace/submit/model/AccessConditionOption.java @@ -8,22 +8,59 @@ package org.dspace.submit.model; /** + * This class represents an option available in the submission upload section to + * set permission on a file. An option is defined by a name such as "open + * access", "embargo", "restricted access", etc. and some optional attributes to + * better clarify the constraints and input available to the user. For instance + * an embargo option could allow to set a start date not longer than 3 years, + * etc + * * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) */ public class AccessConditionOption { - + /** An unique name identifying the access contion option **/ private String name; + /** + * the name of the group that will be bound to the resource policy created if + * such option is used + */ private String groupName; + /** + * this is in alternative to the {@link #groupName}. The sub-groups listed in + * the DSpace group identified by the name here specified will be available to + * the user to personalize the access condition. They can be for instance + * University Staff, University Students, etc. so that a "restricted access" + * option can be further specified without the need to create separate access + * condition options for each group + */ private String selectGroupName; + /** + * set to true if this option requires a start date to be indicated + * for the underlying resource policy to create + */ private Boolean hasStartDate; + /** + * set to true if this option requires an end date to be indicated + * for the underlying resource policy to create + */ private Boolean hasEndDate; + /** + * It contains, if applicable, the maximum start date (i.e. when the "embargo + * expires") that can be selected. It accepts date math via joda library (such as + * +3years) + */ private String startDateLimit; + /** + * It contains, if applicable, the maximum end date (i.e. when the "lease + * expires") that can be selected. It accepts date math via joda library (such as + * +3years) + */ private String endDateLimit; public String getName() { @@ -81,6 +118,4 @@ public class AccessConditionOption { public void setSelectGroupName(String selectGroupName) { this.selectGroupName = selectGroupName; } - - } diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml new file mode 100644 index 0000000000..b734c78937 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/access-conditions.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/resources/DSpaceResourceResolver.java b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/resources/DSpaceResourceResolver.java index 1738373420..d668261a3c 100644 --- a/dspace-oai/src/main/java/org/dspace/xoai/services/impl/resources/DSpaceResourceResolver.java +++ b/dspace-oai/src/main/java/org/dspace/xoai/services/impl/resources/DSpaceResourceResolver.java @@ -22,7 +22,7 @@ import org.dspace.core.ConfigurationManager; public class DSpaceResourceResolver implements ResourceResolver { private static final TransformerFactory transformerFactory = TransformerFactory - .newInstance(); + .newInstance("net.sf.saxon.TransformerFactoryImpl", null); private final String basePath = ConfigurationManager.getProperty("oai", "config.dir"); diff --git a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/AbstractXSLTest.java b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/AbstractXSLTest.java index 2262c1bbe8..6fab56b526 100644 --- a/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/AbstractXSLTest.java +++ b/dspace-oai/src/test/java/org/dspace/xoai/tests/stylesheets/AbstractXSLTest.java @@ -19,7 +19,8 @@ import javax.xml.transform.stream.StreamSource; import org.apache.commons.io.IOUtils; public abstract class AbstractXSLTest { - private static final TransformerFactory factory = TransformerFactory.newInstance(); + private static final TransformerFactory factory = TransformerFactory + .newInstance("net.sf.saxon.TransformerFactoryImpl", null); protected TransformBuilder apply(String xslLocation) throws Exception { return new TransformBuilder(xslLocation); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ClaimedTaskConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ClaimedTaskConverter.java index 42ae152461..cc7459955f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ClaimedTaskConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ClaimedTaskConverter.java @@ -10,6 +10,7 @@ package org.dspace.app.rest.converter; import org.dspace.app.rest.model.ClaimedTaskRest; import org.dspace.app.rest.projection.Projection; import org.dspace.discovery.IndexableObject; +import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; import org.springframework.beans.factory.annotation.Autowired; @@ -28,6 +29,9 @@ public class ClaimedTaskConverter @Autowired private ConverterService converter; + @Autowired + protected XmlWorkflowFactory xmlWorkflowFactory; + @Override public ClaimedTaskRest convert(ClaimedTask obj, Projection projection) { ClaimedTaskRest taskRest = new ClaimedTaskRest(); @@ -35,8 +39,7 @@ public class ClaimedTaskConverter XmlWorkflowItem witem = obj.getWorkflowItem(); taskRest.setId(obj.getID()); taskRest.setWorkflowitem(converter.toRest(witem, projection)); - taskRest.setAction(obj.getActionID()); - taskRest.setStep(obj.getStepID()); + taskRest.setAction(converter.toRest(xmlWorkflowFactory.getActionByName(obj.getActionID()), projection)); taskRest.setOwner(converter.toRest(obj.getOwner(), projection)); return taskRest; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ConverterService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ConverterService.java index 9e4f6c00f1..295634599b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ConverterService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ConverterService.java @@ -32,6 +32,7 @@ import org.springframework.core.type.filter.AssignableTypeFilter; 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.Resource; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; @@ -155,9 +156,30 @@ public class ConverterService { * @throws ClassCastException if the resource type is not compatible with the inferred return type. */ public T toResource(RestModel restObject) { + return toResource(restObject, new Link[] {}); + } + + /** + * Converts the given rest object to a {@link HALResource} object. + *

+ * If the rest object is a {@link RestAddressableModel}, the projection returned by + * {@link RestAddressableModel#getProjection()} will be used to determine which optional + * embeds and links will be added, and {@link Projection#transformResource(HALResource)} + * will be automatically called before returning the final, fully converted resource. + *

+ * In all cases, the {@link HalLinkService} will be used immediately after the resource is constructed, + * to ensure all {@link HalLinkFactory}s have had a chance to add links as needed. + *

+ * + * @param restObject the input rest object. + * @param oldLinks The old links fo the Resource Object + * @param the return type, a subclass of {@link HALResource}. + * @return the fully converted resource, with all automatic links and embeds applied. + */ + public T toResource(RestModel restObject, Link... oldLinks) { T halResource = getResource(restObject); if (restObject instanceof RestAddressableModel) { - utils.embedOrLinkClassLevelRels(halResource); + utils.embedOrLinkClassLevelRels(halResource, oldLinks); halLinkService.addLinks(halResource); Projection projection = ((RestAddressableModel) restObject).getProjection(); return projection.transformResource(halResource); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/PoolTaskConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/PoolTaskConverter.java index 7a58fda864..48299dd362 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/PoolTaskConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/PoolTaskConverter.java @@ -43,7 +43,6 @@ public class PoolTaskConverter taskRest.setGroup(converter.toRest(obj.getGroup(), projection)); } taskRest.setAction(obj.getActionID()); - taskRest.setStep(obj.getStepID()); return taskRest; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResourcePolicyConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResourcePolicyConverter.java index 1e7834d7ed..ab8694874c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResourcePolicyConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResourcePolicyConverter.java @@ -26,12 +26,6 @@ public class ResourcePolicyConverter implements DSpaceConverter { public static final String NAME = "claimedtask"; public static final String CATEGORY = RestAddressableModel.WORKFLOW; - private String step; + public static final String STEP = "step"; - private String action; + @JsonIgnore + private WorkflowActionRest action; @JsonIgnore private EPersonRest owner; @@ -45,27 +51,15 @@ public class ClaimedTaskRest extends BaseObjectRest { return RestResourceController.class; } - /** - * @see ClaimedTask#getStepID() - * @return the step - */ - public String getStep() { - return step; - } - - public void setStep(String step) { - this.step = step; - } - /** * @see ClaimedTaskRest#getAction() * @return the action */ - public String getAction() { + public WorkflowActionRest getAction() { return action; } - public void setAction(String action) { + public void setAction(WorkflowActionRest action) { this.action = action; } @@ -82,7 +76,7 @@ public class ClaimedTaskRest extends BaseObjectRest { } /** - * + * * @return the WorkflowItemRest that belong to this claimed task */ public WorkflowItemRest getWorkflowitem() { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PoolTaskRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PoolTaskRest.java index 6ab445659e..b3dedd23b9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PoolTaskRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/PoolTaskRest.java @@ -16,11 +16,17 @@ import org.dspace.xmlworkflow.storedcomponents.PoolTask; * * @author Andrea Bollini (andrea.bollini at 4science.it) */ +@LinksRest(links = { + @LinkRest( + name = PoolTaskRest.STEP, + method = "getStep" + ) +}) public class PoolTaskRest extends BaseObjectRest { public static final String NAME = "pooltask"; public static final String CATEGORY = RestAddressableModel.WORKFLOW; - private String step; + public static final String STEP = "step"; private String action; @@ -48,18 +54,6 @@ public class PoolTaskRest extends BaseObjectRest { return RestResourceController.class; } - /** - * @see PoolTask#getStepID() - * @return - */ - public String getStep() { - return step; - } - - public void setStep(String step) { - this.step = step; - } - /** * @see PoolTask#getActionID() * @return diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResourcePolicyRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResourcePolicyRest.java index 606bd8c273..656d9049fa 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResourcePolicyRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResourcePolicyRest.java @@ -8,7 +8,6 @@ package org.dspace.app.rest.model; import java.util.Date; -import java.util.UUID; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; @@ -34,12 +33,6 @@ public class ResourcePolicyRest extends BaseObjectRest { private String description; - @JsonInclude(Include.NON_NULL) - private UUID groupUUID; - - @JsonInclude(Include.NON_NULL) - private UUID epersonUUID; - @JsonIgnore private EPersonRest eperson; @@ -55,14 +48,6 @@ public class ResourcePolicyRest extends BaseObjectRest { private Date endDate; - public UUID getGroupUUID() { - return groupUUID; - } - - public void setGroupUUID(UUID groupUuid) { - this.groupUUID = groupUuid; - } - public Date getEndDate() { return endDate; } @@ -111,14 +96,6 @@ public class ResourcePolicyRest extends BaseObjectRest { this.description = description; } - public UUID getEpersonUUID() { - return epersonUUID; - } - - public void setEpersonUUID(UUID epersonUUID) { - this.epersonUUID = epersonUUID; - } - public EPersonRest getEperson() { return eperson; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/UploadBitstreamAccessConditionDTO.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/UploadBitstreamAccessConditionDTO.java new file mode 100644 index 0000000000..120cb6a9d2 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/UploadBitstreamAccessConditionDTO.java @@ -0,0 +1,101 @@ +/** + * 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 java.util.UUID; + +import org.dspace.app.rest.model.step.UploadBitstreamRest; + +/** + * The UploadAccessConditionDTO is a partial representation of the DSpace + * {@link ResourcePolicyRest} as used in the patch payload for the upload + * submission section (see {@link UploadBitstreamRest}. The main reason for this + * class is to have a DTO to use serialize/deserialize the REST model, that + * include reference to the GroupRest and EPersonRest object, in the upload + * section data in a simpler way where such reference are just UUID. Indeed, due + * to the fact that the RestModel class are serialized according to the HAL + * format and the reference are only exposed in the _links section of the + * RestResource it was not possible to use the {@link ResourcePolicyRest} class + * directly in the upload section + * + * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) + */ +public class UploadBitstreamAccessConditionDTO { + + private Integer id; + + private UUID groupUUID; + + private UUID epersonUUID; + + private String name; + + private String description; + + private Date startDate; + + private Date endDate; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public UUID getGroupUUID() { + return groupUUID; + } + + public void setGroupUUID(UUID groupUUID) { + this.groupUUID = groupUUID; + } + + public UUID getEpersonUUID() { + return epersonUUID; + } + + public void setEpersonUUID(UUID epersonUUID) { + this.epersonUUID = epersonUUID; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getStartDate() { + return startDate; + } + + public void setStartDate(Date startDate) { + this.startDate = startDate; + } + + public Date getEndDate() { + return endDate; + } + + public void setEndDate(Date endDate) { + this.endDate = endDate; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowItemRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowItemRest.java index bf00ba0e4f..8f580f4414 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowItemRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/WorkflowItemRest.java @@ -14,10 +14,18 @@ import org.dspace.app.rest.RestResourceController; * * @author Andrea Bollini (andrea.bollini at 4science.it) */ +@LinksRest(links = { + @LinkRest( + name = WorkflowItemRest.STEP, + method = "getStep" + ) +}) public class WorkflowItemRest extends AInprogressSubmissionRest { public static final String NAME = "workflowitem"; public static final String CATEGORY = RestAddressableModel.WORKFLOW; + public static final String STEP = "step"; + @Override public String getCategory() { return CATEGORY; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/UploadBitstreamRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/UploadBitstreamRest.java index 80e36fdb59..7024e5cdb1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/UploadBitstreamRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/UploadBitstreamRest.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.model.step; import java.util.ArrayList; + import java.util.HashMap; import java.util.List; import java.util.Map; @@ -16,13 +17,19 @@ import java.util.UUID; import org.dspace.app.rest.model.BitstreamFormatRest; import org.dspace.app.rest.model.CheckSumRest; import org.dspace.app.rest.model.MetadataValueRest; -import org.dspace.app.rest.model.ResourcePolicyRest; +import org.dspace.app.rest.model.UploadBitstreamAccessConditionDTO; +/** + * This Java Bean is used to represent a single bitstream with all its metadata + * and access conditions ({@link UploadBitstreamAccessConditionDTO}) inside an + * upload submission section ({@link DataUpload} + * + */ public class UploadBitstreamRest extends UploadStatusResponse { private UUID uuid; private Map> metadata = new HashMap<>(); - private List accessConditions; + private List accessConditions; private BitstreamFormatRest format; private Long sizeBytes; private CheckSumRest checkSum; @@ -68,14 +75,14 @@ public class UploadBitstreamRest extends UploadStatusResponse { this.metadata = metadata; } - public List getAccessConditions() { + public List getAccessConditions() { if (accessConditions == null) { accessConditions = new ArrayList<>(); } return accessConditions; } - public void setAccessConditions(List accessConditions) { + public void setAccessConditions(List accessConditions) { this.accessConditions = accessConditions; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/AbstractProjection.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/AbstractProjection.java index 12a492191d..bbf38fd97f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/AbstractProjection.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/AbstractProjection.java @@ -8,8 +8,10 @@ package org.dspace.app.rest.projection; import org.dspace.app.rest.model.LinkRest; +import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.RestModel; import org.dspace.app.rest.model.hateoas.HALResource; +import org.springframework.hateoas.Link; /** * Abstract base class for projections. @@ -34,7 +36,8 @@ public abstract class AbstractProjection implements Projection { } @Override - public boolean allowEmbedding(HALResource halResource, LinkRest linkRest) { + public boolean allowEmbedding(HALResource halResource, LinkRest linkRest, + Link... oldLinks) { return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/CompositeProjection.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/CompositeProjection.java new file mode 100644 index 0000000000..d9a28cec04 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/CompositeProjection.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.projection; + +import java.util.List; + +import org.dspace.app.rest.model.LinkRest; +import org.dspace.app.rest.model.RestAddressableModel; +import org.dspace.app.rest.model.RestModel; +import org.dspace.app.rest.model.hateoas.HALResource; +import org.springframework.hateoas.Link; + +/** + * A projection that combines the behavior of multiple projections. + * + * Model, rest, and resource transformations will be performed in the order of the projections given in + * the constructor. Embedding will be allowed if any of the given projections allow them. Linking will + * be allowed if all of the given projections allow them. + */ +public class CompositeProjection implements Projection { + + public final static String NAME = "composite"; + + private final List projections; + + public CompositeProjection(List projections) { + this.projections = projections; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public T transformModel(T modelObject) { + for (Projection projection : projections) { + modelObject = projection.transformModel(modelObject); + } + return modelObject; + } + + @Override + public T transformRest(T restObject) { + for (Projection projection : projections) { + restObject = projection.transformRest(restObject); + } + return restObject; + } + + @Override + public T transformResource(T halResource) { + for (Projection projection : projections) { + halResource = projection.transformResource(halResource); + } + return halResource; + } + + @Override + public boolean allowEmbedding(HALResource halResource, LinkRest linkRest, + Link... oldLinks) { + for (Projection projection : projections) { + if (projection.allowEmbedding(halResource, linkRest, oldLinks)) { + return true; + } + } + return false; + } + + @Override + public boolean allowLinking(HALResource halResource, LinkRest linkRest) { + for (Projection projection : projections) { + if (!projection.allowLinking(halResource, linkRest)) { + return false; + } + } + return true; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/EmbedRelsProjection.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/EmbedRelsProjection.java new file mode 100644 index 0000000000..1db0c6a74d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/EmbedRelsProjection.java @@ -0,0 +1,65 @@ +/** + * 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.projection; + +import java.util.Set; + +import org.dspace.app.rest.model.LinkRest; +import org.dspace.app.rest.model.RestAddressableModel; +import org.dspace.app.rest.model.hateoas.HALResource; +import org.springframework.hateoas.Link; + +/** + * Projection that allows a given set of rels to be embedded. + * A Rel refers to a Link Relation, this is an Embedded Object of the HalResource and the HalResource contains + * a link to this + */ +public class EmbedRelsProjection extends AbstractProjection { + + public final static String NAME = "embedrels"; + + private final Set embedRels; + + public EmbedRelsProjection(Set embedRels) { + this.embedRels = embedRels; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public boolean allowEmbedding(HALResource halResource, LinkRest linkRest, + Link... oldLinks) { + // If level 0, and the name is present, the link can be embedded (e.g. the logo on a collection page) + if (halResource.getContent().getEmbedLevel() == 0 && embedRels.contains(linkRest.name())) { + return true; + } + + StringBuilder fullName = new StringBuilder(); + for (Link oldLink : oldLinks) { + fullName.append(oldLink.getRel()).append("/"); + } + fullName.append(linkRest.name()); + // If the full name matches, the link can be embedded (e.g. mappedItems/owningCollection on a collection page) + if (embedRels.contains(fullName.toString())) { + return true; + } + + fullName.append("/"); + // If the full name starts with the allowed embed, but the embed goes deeper, the link can be embedded + // (e.g. making sure mappedItems/owningCollection also embeds mappedItems on a collection page) + for (String embedRel : embedRels) { + if (embedRel.startsWith(fullName.toString())) { + return true; + } + } + return false; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/FullProjection.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/FullProjection.java index 9c5a4a9f9e..99719c8ac3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/FullProjection.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/FullProjection.java @@ -8,7 +8,10 @@ package org.dspace.app.rest.projection; import org.dspace.app.rest.model.LinkRest; +import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.hateoas.HALResource; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.springframework.hateoas.Link; import org.springframework.stereotype.Component; /** @@ -18,14 +21,17 @@ import org.springframework.stereotype.Component; public class FullProjection extends AbstractProjection { public final static String NAME = "full"; + private final int maxEmbed = DSpaceServicesFactory.getInstance().getConfigurationService() + .getIntProperty("rest.projections.full.max", 2); public String getName() { return NAME; } @Override - public boolean allowEmbedding(HALResource halResource, LinkRest linkRest) { - return true; + public boolean allowEmbedding(HALResource halResource, LinkRest linkRest, + Link... oldLinks) { + return halResource.getContent().getEmbedLevel() < maxEmbed; } @Override diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/Projection.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/Projection.java index d9e0b10261..cb16093464 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/Projection.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/Projection.java @@ -10,9 +10,12 @@ package org.dspace.app.rest.projection; import javax.persistence.Entity; import org.dspace.app.rest.model.LinkRest; +import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.RestModel; import org.dspace.app.rest.model.hateoas.HALResource; import org.dspace.app.rest.repository.DSpaceRestRepository; +import org.dspace.app.rest.utils.Utils; +import org.springframework.hateoas.Link; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RestController; @@ -44,7 +47,7 @@ import org.springframework.web.bind.annotation.RestController; *
  • After it is converted to a {@link RestModel}, the projection may modify it * via {@link #transformRest(RestModel)}.
  • *
  • During conversion to a {@link HALResource}, the projection may opt in to certain annotation-discovered - * HAL embeds and links via {@link #allowEmbedding(HALResource, LinkRest)} + * HAL embeds and links via {@link #allowEmbedding(HALResource, LinkRest, Link...)} * and {@link #allowLinking(HALResource, LinkRest)}
  • *
  • After conversion to a {@link HALResource}, the projection may modify it * via {@link #transformResource(HALResource)}.
  • @@ -52,8 +55,7 @@ import org.springframework.web.bind.annotation.RestController; * *

    How a projection is chosen

    * - * When a REST request is made, the projection argument, if present, is used to look up the projection to use, - * by name. If no argument is present, {@link DefaultProjection} will be used. + * See {@link Utils#obtainProjection()}. */ public interface Projection { @@ -115,16 +117,18 @@ public interface Projection { * * @param halResource the resource from which the embed may or may not be made. * @param linkRest the LinkRest annotation through which the related resource was discovered on the rest object. + * @param oldLinks The previously traversed links * @return true if allowed, false otherwise. */ - boolean allowEmbedding(HALResource halResource, LinkRest linkRest); + boolean allowEmbedding(HALResource halResource, LinkRest linkRest, + Link... oldLinks); /** * Tells whether this projection permits the linking of a particular linkable subresource. * * This gives the projection an opportunity to opt in to to certain links, by returning {@code true}. * - * Note: If {@link #allowEmbedding(HALResource, LinkRest)} returns {@code true} for a given subresource, + * Note: If {@link #allowEmbedding(HALResource, LinkRest, Link...)} returns {@code true} for a given subresource, * it will be automatically linked regardless of what this method returns. * * @param halResource the resource from which the link may or may not be made. diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/SpecificLevelProjection.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/SpecificLevelProjection.java new file mode 100644 index 0000000000..7ef603b23c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/projection/SpecificLevelProjection.java @@ -0,0 +1,69 @@ +/** + * 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.projection; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.exception.MissingParameterException; +import org.dspace.app.rest.model.LinkRest; +import org.dspace.app.rest.model.RestAddressableModel; +import org.dspace.app.rest.model.hateoas.HALResource; +import org.dspace.services.RequestService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.hateoas.Link; + +/** + * This Projection will allow us to specify how many levels deep we're going to embed resources onto the requested + * HalResource. + * The projection is used by using the name combined with the embedLevelDepth parameter to specify how deep the embeds + * have to go. There is an upperlimit in place for this, which is specified on the bean through the maxEmbed property + */ +public class SpecificLevelProjection extends AbstractProjection { + + @Autowired + private RequestService requestService; + + public final static String NAME = "level"; + + private int maxEmbed = DSpaceServicesFactory.getInstance().getConfigurationService() + .getIntProperty("rest.projections.full.max", 2); + + public int getMaxEmbed() { + return maxEmbed; + } + + public void setMaxEmbed(int maxEmbed) { + this.maxEmbed = maxEmbed; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public boolean allowEmbedding(HALResource halResource, LinkRest linkRest, + Link... oldLinks) { + String embedLevelDepthString = requestService.getCurrentRequest().getHttpServletRequest() + .getParameter("embedLevelDepth"); + if (StringUtils.isBlank(embedLevelDepthString)) { + throw new MissingParameterException("The embedLevelDepth parameter needs to be specified" + + " for this Projection"); + } + Integer embedLevelDepth = Integer.parseInt(embedLevelDepthString); + if (embedLevelDepth > maxEmbed) { + throw new IllegalArgumentException("The embedLevelDepth may not exceed the configured max: " + maxEmbed); + } + return halResource.getContent().getEmbedLevel() < embedLevelDepth; + } + + @Override + public boolean allowLinking(HALResource halResource, LinkRest linkRest) { + return true; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClaimedTaskStepLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClaimedTaskStepLinkRepository.java new file mode 100644 index 0000000000..9ee277171e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ClaimedTaskStepLinkRepository.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.repository; + +import java.sql.SQLException; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.ClaimedTaskRest; +import org.dspace.app.rest.model.WorkflowStepRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.core.Context; +import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; +import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; +import org.dspace.xmlworkflow.storedcomponents.service.ClaimedTaskService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.stereotype.Component; + +/** + * Link repository for the Steps subresources for an individual ClaimedTask + */ +@Component(ClaimedTaskRest.CATEGORY + "." + ClaimedTaskRest.NAME + "." + ClaimedTaskRest.STEP) +public class ClaimedTaskStepLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private ClaimedTaskService claimedTaskService; + + @Autowired + private XmlWorkflowFactory xmlWorkflowFactory; + + /** + * This method will retrieve the {@link WorkflowStepRest} object for the {@link ClaimedTask} with the given id + * @param request The current request + * @param claimedTaskId The id for the ClaimedTask to be used + * @param optionalPageable The pageable if relevant + * @param projection The Projection + * @return The {@link WorkflowStepRest} object related to the {@link ClaimedTask} specified by + * the given ID + */ + public WorkflowStepRest getStep(@Nullable HttpServletRequest request, + Integer claimedTaskId, + @Nullable Pageable optionalPageable, + Projection projection) { + + Context context = obtainContext(); + try { + ClaimedTask claimedTask = claimedTaskService.find(context, claimedTaskId); + if (claimedTask == null) { + throw new ResourceNotFoundException("ClaimedTask with id: " + claimedTaskId + " wasn't found"); + } + return converter.toRest(xmlWorkflowFactory.getStepByName(claimedTask.getStepID()), projection); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/PoolTaskStepLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/PoolTaskStepLinkRepository.java new file mode 100644 index 0000000000..6e7f4f84ac --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/PoolTaskStepLinkRepository.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.repository; + +import java.sql.SQLException; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.PoolTaskRest; +import org.dspace.app.rest.model.WorkflowStepRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.core.Context; +import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; +import org.dspace.xmlworkflow.storedcomponents.PoolTask; +import org.dspace.xmlworkflow.storedcomponents.service.PoolTaskService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.stereotype.Component; + +/** + * Link repositoy for the Steps subresources of an individual PoolTask + */ +@Component(PoolTaskRest.CATEGORY + "." + PoolTaskRest.NAME + "." + PoolTaskRest.STEP) +public class PoolTaskStepLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private PoolTaskService poolTaskService; + + @Autowired + private XmlWorkflowFactory xmlWorkflowFactory; + + /** + * This method will retrieve the {@link WorkflowStepRest} object for the {@link PoolTask} with the given id + * @param request The current request + * @param poolTaskId The id for the PoolTask to be used + * @param optionalPageable The pageable if relevant + * @param projection The Projection + * @return The {@link WorkflowStepRest} object related to the {@link PoolTask} specified by + * the given ID + */ + public WorkflowStepRest getStep(@Nullable HttpServletRequest request, + Integer poolTaskId, + @Nullable Pageable optionalPageable, + Projection projection) { + + Context context = obtainContext(); + try { + PoolTask poolTask = poolTaskService.find(context, poolTaskId); + if (poolTask == null) { + throw new ResourceNotFoundException("ClaimedTask with id: " + poolTaskId + " wasn't found"); + } + return converter.toRest(xmlWorkflowFactory.getStepByName(poolTask.getStepID()), projection); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemStepLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemStepLinkRepository.java new file mode 100644 index 0000000000..30aac1579c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/WorkflowItemStepLinkRepository.java @@ -0,0 +1,86 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository; + +import java.sql.SQLException; +import java.util.List; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.WorkflowItemRest; +import org.dspace.app.rest.model.WorkflowStepRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.core.Context; +import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; +import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; +import org.dspace.xmlworkflow.storedcomponents.PoolTask; +import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; +import org.dspace.xmlworkflow.storedcomponents.service.ClaimedTaskService; +import org.dspace.xmlworkflow.storedcomponents.service.PoolTaskService; +import org.dspace.xmlworkflow.storedcomponents.service.XmlWorkflowItemService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.stereotype.Component; + +/** + * Link Repository for the Steps subresources of an individual WorkflowItem + */ +@Component(WorkflowItemRest.CATEGORY + "." + WorkflowItemRest.NAME + "." + WorkflowItemRest.STEP) +public class WorkflowItemStepLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private XmlWorkflowItemService xmlWorkflowItemService; + + @Autowired + private PoolTaskService poolTaskService; + + @Autowired + private ClaimedTaskService claimedTaskService; + + @Autowired + private XmlWorkflowFactory xmlWorkflowFactory; + + /** + * This method will retrieve the {@link WorkflowStepRest} object for the {@link org.dspace.workflow.WorkflowItem} + * with the given id + * @param request The current request + * @param workflowItemId The id for the WorkflowItem to be used + * @param optionalPageable The pageable if relevant + * @param projection The Projection + * @return The {@link WorkflowStepRest} object related to the + * {@link org.dspace.workflow.WorkflowItem} specified by the given ID + */ + public WorkflowStepRest getStep(@Nullable HttpServletRequest request, + Integer workflowItemId, + @Nullable Pageable optionalPageable, + Projection projection) { + + Context context = obtainContext(); + try { + XmlWorkflowItem xmlWorkflowItem = xmlWorkflowItemService.find(context, workflowItemId); + if (xmlWorkflowItem == null) { + throw new ResourceNotFoundException("XmlWorkflowItem with id: " + workflowItemId + " wasn't found"); + } + List poolTasks = poolTaskService.find(context, xmlWorkflowItem); + List claimedTasks = claimedTaskService.find(context, xmlWorkflowItem); + for (PoolTask poolTask : poolTasks) { + return converter.toRest(xmlWorkflowFactory.getStepByName(poolTask.getStepID()), projection); + } + for (ClaimedTask claimedTask : claimedTasks) { + return converter.toRest(xmlWorkflowFactory.getStepByName(claimedTask.getStepID()), projection); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + throw new ResourceNotFoundException("No workflowStep for this workflowItem with id: " + workflowItemId + + " was found"); + + + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java index c5e9541976..e0e5b880fa 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java @@ -85,7 +85,7 @@ public class AuthorizeServicePermissionEvaluatorPlugin extends RestObjectPermiss } return authorizeService.authorizeActionBoolean(context, ePerson, dSpaceObject, - restPermission.getDspaceApiActionId(), false); + restPermission.getDspaceApiActionId(), true); } } catch (SQLException e) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java index 6a4ed0b542..d8a86fa41a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/SubmissionService.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.submit; import java.io.IOException; + import java.sql.SQLException; import java.util.ArrayList; import java.util.List; @@ -23,8 +24,9 @@ import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.BitstreamRest; import org.dspace.app.rest.model.CheckSumRest; import org.dspace.app.rest.model.MetadataValueRest; -import org.dspace.app.rest.model.ResourcePolicyRest; +import org.dspace.app.rest.model.UploadBitstreamAccessConditionDTO; import org.dspace.app.rest.model.WorkspaceItemRest; +import org.dspace.app.rest.model.step.DataUpload; import org.dspace.app.rest.model.step.UploadBitstreamRest; import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.utils.ContextUtil; @@ -81,7 +83,7 @@ public class SubmissionService { private org.dspace.app.rest.utils.Utils utils; /** - * Create a workspaceitem using the information in the reqest + * Create a workspaceitem using the information in the request * * @param context * the dspace context @@ -135,7 +137,17 @@ public class SubmissionService { } } - +/** + * Build the rest representation of a bitstream as used in the upload section + * ({@link DataUpload}. It contains all its metadata and the list of applied + * access conditions (@link {@link UploadBitstreamAccessConditionDTO} + * + * @param configurationService the DSpace ConfigurationService + * @param source the bitstream to translate in its rest submission + * representation + * @return + * @throws SQLException + */ public UploadBitstreamRest buildUploadBitstream(ConfigurationService configurationService, Bitstream source) throws SQLException { UploadBitstreamRest data = new UploadBitstreamRest(); @@ -170,8 +182,8 @@ public class SubmissionService { for (ResourcePolicy rp : source.getResourcePolicies()) { if (ResourcePolicy.TYPE_CUSTOM.equals(rp.getRpType())) { - ResourcePolicyRest resourcePolicyRest = converter.toRest(rp, projection); - data.getAccessConditions().add(resourcePolicyRest); + UploadBitstreamAccessConditionDTO uploadAccessCondition = createAccessConditionFromResourcePolicy(rp); + data.getAccessConditions().add(uploadAccessCondition); } } @@ -187,7 +199,7 @@ public class SubmissionService { } /** - * Create a workflowitem using the information in the reqest + * Create a workflowitem using the information in the request * * @param context * the dspace context @@ -237,6 +249,23 @@ public class SubmissionService { return wi; } + private UploadBitstreamAccessConditionDTO createAccessConditionFromResourcePolicy(ResourcePolicy rp) { + UploadBitstreamAccessConditionDTO accessCondition = new UploadBitstreamAccessConditionDTO(); + + accessCondition.setId(rp.getID()); + accessCondition.setName(rp.getRpName()); + accessCondition.setDescription(rp.getRpDescription()); + accessCondition.setStartDate(rp.getStartDate()); + accessCondition.setEndDate(rp.getEndDate()); + if (rp.getGroup() != null) { + accessCondition.setGroupUUID(rp.getGroup().getID()); + } + if (rp.getEPerson() != null) { + accessCondition.setEpersonUUID(rp.getEPerson().getID()); + } + return accessCondition; + } + public void saveWorkflowItem(Context context, XmlWorkflowItem source) throws SQLException, AuthorizeException { workflowItemService.update(context, source); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ResourcePolicyAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamResourcePolicyAddPatchOperation.java similarity index 83% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ResourcePolicyAddPatchOperation.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamResourcePolicyAddPatchOperation.java index 7367ab2bd1..05e7c5052a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ResourcePolicyAddPatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamResourcePolicyAddPatchOperation.java @@ -8,10 +8,11 @@ package org.dspace.app.rest.submit.factory.impl; import java.util.ArrayList; + import java.util.Date; import java.util.List; -import org.dspace.app.rest.model.ResourcePolicyRest; +import org.dspace.app.rest.model.UploadBitstreamAccessConditionDTO; import org.dspace.app.rest.model.patch.LateObjectEvaluator; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.AuthorizeService; @@ -35,7 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; * * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) */ -public class ResourcePolicyAddPatchOperation extends AddPatchOperation { +public class BitstreamResourcePolicyAddPatchOperation extends AddPatchOperation { @Autowired BitstreamService bitstreamService; @@ -48,6 +49,7 @@ public class ResourcePolicyAddPatchOperation extends AddPatchOperation newAccessConditions = new ArrayList(); + List newAccessConditions = + new ArrayList(); if (split.length == 3) { authorizeService.removePoliciesActionFilter(context, b, Constants.READ); newAccessConditions = evaluateArrayObject((LateObjectEvaluator) value); @@ -74,7 +77,7 @@ public class ResourcePolicyAddPatchOperation extends AddPatchOperation getArrayClassForEvaluation() { - return ResourcePolicyRest[].class; + protected Class getArrayClassForEvaluation() { + return UploadBitstreamAccessConditionDTO[].class; } @Override - protected Class getClassForEvaluation() { - return ResourcePolicyRest.class; + protected Class getClassForEvaluation() { + return UploadBitstreamAccessConditionDTO.class; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ResourcePolicyRemovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamResourcePolicyRemovePatchOperation.java similarity index 86% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ResourcePolicyRemovePatchOperation.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamResourcePolicyRemovePatchOperation.java index 18b15dfba0..fcf96578a1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ResourcePolicyRemovePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamResourcePolicyRemovePatchOperation.java @@ -9,7 +9,7 @@ package org.dspace.app.rest.submit.factory.impl; import java.util.List; -import org.dspace.app.rest.model.ResourcePolicyRest; +import org.dspace.app.rest.model.UploadBitstreamAccessConditionDTO; import org.dspace.authorize.ResourcePolicy; import org.dspace.authorize.service.ResourcePolicyService; import org.dspace.content.Bitstream; @@ -28,7 +28,8 @@ import org.springframework.beans.factory.annotation.Autowired; * * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) */ -public class ResourcePolicyRemovePatchOperation extends RemovePatchOperation { +public class BitstreamResourcePolicyRemovePatchOperation + extends RemovePatchOperation { @Autowired ItemService itemService; @@ -83,12 +84,12 @@ public class ResourcePolicyRemovePatchOperation extends RemovePatchOperation getArrayClassForEvaluation() { - return ResourcePolicyRest[].class; + protected Class getArrayClassForEvaluation() { + return UploadBitstreamAccessConditionDTO[].class; } @Override - protected Class getClassForEvaluation() { - return ResourcePolicyRest.class; + protected Class getClassForEvaluation() { + return UploadBitstreamAccessConditionDTO.class; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ResourcePolicyReplacePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamResourcePolicyReplacePatchOperation.java similarity index 88% rename from dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ResourcePolicyReplacePatchOperation.java rename to dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamResourcePolicyReplacePatchOperation.java index c03ea98271..7f0a23dbb9 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/ResourcePolicyReplacePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/BitstreamResourcePolicyReplacePatchOperation.java @@ -9,6 +9,7 @@ package org.dspace.app.rest.submit.factory.impl; import java.util.Date; import java.util.List; +import java.util.UUID; import org.dspace.app.rest.model.ResourcePolicyRest; import org.dspace.app.rest.model.patch.LateObjectEvaluator; @@ -35,7 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; * * @author Luigi Andrea Pascarelli (luigiandrea.pascarelli at 4science.it) */ -public class ResourcePolicyReplacePatchOperation extends ReplacePatchOperation { +public class BitstreamResourcePolicyReplacePatchOperation extends ReplacePatchOperation { @Autowired BitstreamService bitstreamService; @@ -89,12 +90,14 @@ public class ResourcePolicyReplacePatchOperation extends ReplacePatchOperation + * Any number of individual {@code Projections} that are spring-registered {@link Component}s may be specified + * by name via the {@code projection} parameter. If multiple projections are specified, they will be wrapped in a + * {@link CompositeProjection} and applied in order as described there. + *

    + * In addition, any number of embeds may be specified by rel name via the {@code embed} parameter. + * When provided, these act as a whitelist of embeds that may be included in the response, as described + * and implemented by {@link EmbedRelsProjection}. + *

    * * @return the requested or default projection, never {@code null}. * @throws IllegalArgumentException if the request specifies an unknown projection name. */ public Projection obtainProjection() { - String projectionName = requestService.getCurrentRequest().getServletRequest().getParameter("projection"); - return converter.getProjection(projectionName); + ServletRequest servletRequest = requestService.getCurrentRequest().getServletRequest(); + List projectionNames = getValues(servletRequest, "projection"); + Set embedRels = new HashSet<>(getValues(servletRequest, "embed")); + + List projections = new ArrayList<>(); + + for (String projectionName : projectionNames) { + projections.add(converter.getProjection(projectionName)); + } + + if (!embedRels.isEmpty()) { + projections.add(new EmbedRelsProjection(embedRels)); + } + + if (projections.isEmpty()) { + return Projection.DEFAULT; + } else if (projections.size() == 1) { + return projections.get(0); + } else { + return new CompositeProjection(projections); + } + } + + /** + * Gets zero or more values for the given servlet request parameter. + *

    + * This convenience method reads multiple values that have been specified as request parameter in multiple ways: + * via * {@code ?paramName=value1¶mName=value2}, via {@code ?paramName=value1,value2}, + * or a combination. + *

    + * It provides the values in the order they were given in the request, and automatically de-dupes them. + *

    + * + * @param servletRequest the servlet request. + * @param parameterName the parameter name. + * @return the ordered, de-duped values, possibly empty, never {@code null}. + */ + private List getValues(ServletRequest servletRequest, String parameterName) { + String[] rawValues = servletRequest.getParameterValues(parameterName); + List values = new ArrayList<>(); + if (rawValues != null) { + for (String rawValue : rawValues) { + for (String value : rawValue.split(",")) { + String trimmedValue = value.trim(); + if (trimmedValue.length() > 0 && !values.contains(trimmedValue)) { + values.add(trimmedValue); + } + } + } + } + return values; } /** * Adds embeds or links for all class-level LinkRel annotations for which embeds or links are allowed. * * @param halResource the resource. + * @param oldLinks previously traversed links */ - public void embedOrLinkClassLevelRels(HALResource halResource) { + public void embedOrLinkClassLevelRels(HALResource halResource, Link... oldLinks) { Projection projection = halResource.getContent().getProjection(); getLinkRests(halResource.getContent().getClass()).stream().forEach((linkRest) -> { Link link = linkToSubResource(halResource.getContent(), linkRest.name()); - if (projection.allowEmbedding(halResource, linkRest)) { - embedRelFromRepository(halResource, linkRest.name(), link, linkRest); + if (projection.allowEmbedding(halResource, linkRest, oldLinks)) { + embedRelFromRepository(halResource, linkRest.name(), link, linkRest, oldLinks); halResource.add(link); // unconditionally link if embedding was allowed } else if (projection.allowLinking(halResource, linkRest)) { halResource.add(link); @@ -503,6 +566,32 @@ public class Utils { */ void embedRelFromRepository(HALResource resource, String rel, Link link, LinkRest linkRest) { + embedRelFromRepository(resource, rel, link, linkRest, new Link[] {}); + } + + /** + * Embeds a rel whose value comes from a {@link LinkRestRepository}, if the maximum embed level has not + * been exceeded yet. + *

    + * The embed will be skipped if 1) the link repository reports that it is not embeddable or 2) the returned + * value is null and the LinkRest annotation has embedOptional = true. + *

    + * Implementation note: The caller is responsible for ensuring that the projection allows the embed + * before calling this method. + *

    + * + * @param resource the resource from which the embed will be made. + * @param rel the name of the rel. + * @param link the link. + * @param linkRest the LinkRest annotation (must have method defined). + * @param oldLinks The previously traversed links + * @throws RepositoryNotFoundException if the link repository could not be found. + * @throws IllegalArgumentException if the method specified by the LinkRest could not be found in the + * link repository. + * @throws RuntimeException if any other problem occurs when trying to invoke the method. + */ + void embedRelFromRepository(HALResource resource, + String rel, Link link, LinkRest linkRest, Link... oldLinks) { if (resource.getContent().getEmbedLevel() == EMBED_MAX_LEVELS) { return; } @@ -514,7 +603,7 @@ public class Utils { Object contentId = getContentIdForLinkMethod(resource.getContent(), method); try { Object linkedObject = method.invoke(linkRepository, null, contentId, null, projection); - resource.embedResource(rel, wrapForEmbedding(resource, linkedObject, link)); + resource.embedResource(rel, wrapForEmbedding(resource, linkedObject, link, oldLinks)); } catch (InvocationTargetException e) { if (e.getTargetException() instanceof RuntimeException) { throw (RuntimeException) e.getTargetException(); @@ -619,17 +708,37 @@ public class Utils { */ private Object wrapForEmbedding(HALResource resource, Object linkedObject, Link link) { + return wrapForEmbedding(resource, linkedObject, link, new Link[] {}); + } + + /** + * Wraps the given linked object (retrieved from a link repository or link method on the rest item) + * in an object that is appropriate for embedding, if needed. Does not perform the actual embed; the + * caller is responsible for that. + * + * @param resource the resource from which the embed will be made. + * @param linkedObject the linked object. + * @param link the link, which is used if the linked object is a list or page, to determine the self link + * and embed property name to use for the subresource. + * @param oldLinks The previously traversed links + * @return the wrapped object, which will have an "embed level" one greater than the given parent resource. + */ + private Object wrapForEmbedding(HALResource resource, + Object linkedObject, Link link, Link... oldLinks) { int childEmbedLevel = resource.getContent().getEmbedLevel() + 1; + //Add the latest link to the list + Link[] newList = Arrays.copyOf(oldLinks, oldLinks.length + 1); + newList[oldLinks.length] = link; if (linkedObject instanceof RestAddressableModel) { RestAddressableModel restObject = (RestAddressableModel) linkedObject; restObject.setEmbedLevel(childEmbedLevel); - return converter.toResource(restObject); + return converter.toResource(restObject, newList); } else if (linkedObject instanceof Page) { // The first page has already been constructed by a link repository and we only need to wrap it Page page = (Page) linkedObject; return new EmbeddedPage(link.getHref(), page.map((restObject) -> { restObject.setEmbedLevel(childEmbedLevel); - return converter.toResource(restObject); + return converter.toResource(restObject, newList); }), null, link.getRel()); } else if (linkedObject instanceof List) { // The full list has been retrieved and we need to provide the first page for embedding @@ -641,7 +750,7 @@ public class Utils { return new EmbeddedPage(link.getHref(), page.map((restObject) -> { restObject.setEmbedLevel(childEmbedLevel); - return converter.toResource(restObject); + return converter.toResource(restObject, newList); }), list, link.getRel()); } else { diff --git a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml index ed3e185839..61459f11d6 100644 --- a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml +++ b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml @@ -50,7 +50,7 @@ + class="org.dspace.app.rest.submit.factory.impl.BitstreamResourcePolicyAddPatchOperation"/> @@ -75,7 +75,7 @@ + class="org.dspace.app.rest.submit.factory.impl.BitstreamResourcePolicyRemovePatchOperation"/> @@ -96,7 +96,7 @@ + class="org.dspace.app.rest.submit.factory.impl.BitstreamResourcePolicyReplacePatchOperation"/> idRef = new AtomicReference(); + Step step = xmlWorkflowFactory.getStepByName("reviewstep"); // step 1 getClient(reviewer1Token).perform(get("/api/workflow/pooltasks/search/findByUser") - .param("uuid", reviewer1.getID().toString())) + .param("uuid", reviewer1.getID().toString()).param("projection", "full")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.pooltasks", Matchers.contains( Matchers.allOf( Matchers.is(PoolTaskMatcher.matchPoolTask(null, "reviewstep")), + hasJsonPath("$._embedded.step", WorkflowStepMatcher.matchWorkflowStepEntry(step)), hasJsonPath("$._embedded.workflowitem", Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( witem, "Test item full workflow", "2019-03-06", "ExtraEntry"))) @@ -1821,17 +1833,22 @@ public class TaskRestRepositoriesIT extends AbstractControllerIntegrationTest { .contentType(MediaType.APPLICATION_FORM_URLENCODED)) .andExpect(status().isNoContent()); + WorkflowActionConfig workflowAction = xmlWorkflowFactory.getActionByName("reviewaction"); + // get the id of the claimed task getClient(reviewer1Token).perform(get("/api/workflow/claimedtasks/search/findByUser") - .param("uuid", reviewer1.getID().toString())) + .param("uuid", reviewer1.getID().toString()).param("projection", "full")) .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.step", WorkflowStepMatcher.matchWorkflowStepEntry(step)), hasJsonPath("$._embedded.workflowitem", Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( - witem, "Test item full workflow", "2019-03-06", "ExtraEntry"))) + witem, "Test item full workflow", "2019-03-06", "ExtraEntry"))), + hasJsonPath("$._embedded.action", + WorkflowActionMatcher.matchWorkflowActionEntry(workflowAction)) )))) .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) .andExpect(jsonPath("$.page.size", is(20))) @@ -1850,13 +1867,16 @@ public class TaskRestRepositoriesIT extends AbstractControllerIntegrationTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.inArchive", is(false))); + step = xmlWorkflowFactory.getStepByName("editstep"); + // step 2 getClient(reviewer2Token).perform(get("/api/workflow/pooltasks/search/findByUser") - .param("uuid", reviewer2.getID().toString())) + .param("uuid", reviewer2.getID().toString()).param("projection", "full")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.pooltasks", Matchers.contains( Matchers.allOf( Matchers.is(PoolTaskMatcher.matchPoolTask(null, "editstep")), + hasJsonPath("$._embedded.step", WorkflowStepMatcher.matchWorkflowStepEntry(step)), hasJsonPath("$._embedded.workflowitem", Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( witem, "Test item full workflow", "2019-03-06", "ExtraEntry"))) @@ -1872,17 +1892,22 @@ public class TaskRestRepositoriesIT extends AbstractControllerIntegrationTest { .contentType(MediaType.APPLICATION_FORM_URLENCODED)) .andExpect(status().isNoContent()); + workflowAction = xmlWorkflowFactory.getActionByName("editaction"); + // get the id of the claimed task getClient(reviewer2Token).perform(get("/api/workflow/claimedtasks/search/findByUser") - .param("uuid", reviewer2.getID().toString())) + .param("uuid", reviewer2.getID().toString()).param("projection", "full")) .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.step", WorkflowStepMatcher.matchWorkflowStepEntry(step)), hasJsonPath("$._embedded.workflowitem", Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( - witem, "Test item full workflow", "2019-03-06", "ExtraEntry"))) + witem, "Test item full workflow", "2019-03-06", "ExtraEntry"))), + hasJsonPath("$._embedded.action", + WorkflowActionMatcher.matchWorkflowActionEntry(workflowAction)) )))) .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) .andExpect(jsonPath("$.page.size", is(20))) @@ -1901,17 +1926,20 @@ public class TaskRestRepositoriesIT extends AbstractControllerIntegrationTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.inArchive", is(false))); + step = xmlWorkflowFactory.getStepByName("finaleditstep"); + // step 3 getClient(reviewer3Token).perform(get("/api/workflow/pooltasks/search/findByUser") - .param("uuid", reviewer3.getID().toString())) + .param("uuid", reviewer3.getID().toString()).param("projection", "full")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.pooltasks", Matchers.contains( Matchers.allOf( Matchers.is(PoolTaskMatcher.matchPoolTask(null, "finaleditstep")), + hasJsonPath("$._embedded.step", WorkflowStepMatcher.matchWorkflowStepEntry(step)), hasJsonPath("$._embedded.workflowitem", Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( witem, "Test item full workflow", "2019-03-06", "ExtraEntry"))) - )))) + )))) .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/pooltasks"))) .andExpect(jsonPath("$.page.size", is(20))) .andExpect(jsonPath("$.page.totalElements", is(1))) @@ -1923,17 +1951,21 @@ public class TaskRestRepositoriesIT extends AbstractControllerIntegrationTest { .contentType(MediaType.APPLICATION_FORM_URLENCODED)) .andExpect(status().isNoContent()); + workflowAction = xmlWorkflowFactory.getActionByName("finaleditaction"); // get the id of the claimed task getClient(reviewer3Token).perform(get("/api/workflow/claimedtasks/search/findByUser") - .param("uuid", reviewer3.getID().toString())) + .param("uuid", reviewer3.getID().toString()).param("projection", "full")) .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.step", WorkflowStepMatcher.matchWorkflowStepEntry(step)), hasJsonPath("$._embedded.workflowitem", Matchers.is(WorkflowItemMatcher.matchItemWithTitleAndDateIssuedAndSubject( - witem, "Test item full workflow", "2019-03-06", "ExtraEntry"))) + witem, "Test item full workflow", "2019-03-06", "ExtraEntry"))), + hasJsonPath("$._embedded.action", + WorkflowActionMatcher.matchWorkflowActionEntry(workflowAction)) )))) .andExpect(jsonPath("$._links.self.href", Matchers.containsString("/api/workflow/claimedtasks"))) .andExpect(jsonPath("$.page.size", is(20))) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java index 69abdb6c93..9e12fb3c74 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkflowItemRestRepositoryIT.java @@ -40,6 +40,7 @@ import org.dspace.app.rest.builder.WorkspaceItemBuilder; import org.dspace.app.rest.matcher.CollectionMatcher; import org.dspace.app.rest.matcher.ItemMatcher; import org.dspace.app.rest.matcher.WorkflowItemMatcher; +import org.dspace.app.rest.matcher.WorkflowStepMatcher; import org.dspace.app.rest.matcher.WorkspaceItemMatcher; import org.dspace.app.rest.model.patch.AddOperation; import org.dspace.app.rest.model.patch.Operation; @@ -53,6 +54,8 @@ import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; import org.dspace.eperson.EPerson; import org.dspace.services.ConfigurationService; +import org.dspace.xmlworkflow.factory.XmlWorkflowFactory; +import org.dspace.xmlworkflow.state.Step; import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; import org.hamcrest.Matchers; @@ -71,6 +74,8 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT @Autowired private ConfigurationService configurationService; + @Autowired + private XmlWorkflowFactory xmlWorkflowFactory; @Before @Override @@ -85,7 +90,7 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT @Test /** * All the workflowitems should be returned regardless of the collection where they were created - * + * * @throws Exception */ public void findAllTest() throws Exception { @@ -140,7 +145,7 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT @Test /** * The workflowitem endpoint must provide proper pagination - * + * * @throws Exception */ public void findAllWithPaginationTest() throws Exception { @@ -208,7 +213,7 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT @Test /** * The findAll should be available only to admins regardless to having or less a role in the workflow - * + * * @throws Exception */ public void findAllForbiddenTest() throws Exception { @@ -259,7 +264,7 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT @Test /** * The workflowitem resource endpoint must expose the proper structure - * + * * @throws Exception */ public void findOneTest() throws Exception { @@ -297,7 +302,7 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT @Test /** * The workflowitem resource endpoint should be visible only to member of the corresponding workflow step - * + * * @throws Exception */ public void findOneForbiddenTest() throws Exception { @@ -430,7 +435,7 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT @Test /** * The workflowitem resource endpoint must expose the proper structure - * + * * @throws Exception */ public void findOneRelsTest() throws Exception { @@ -478,7 +483,7 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT @Test /** * Check the response code for unexistent workflowitem - * + * * @throws Exception */ public void findOneWrongIDTest() throws Exception { @@ -495,7 +500,7 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT /** * Create three workflowitem with two different submitter and verify that the findBySubmitter return the proper * list of workflowitem for each submitter also paginating - * + * * @throws Exception */ public void findBySubmitterTest() throws Exception { @@ -602,7 +607,7 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT /** * A delete request over a workflowitem should result in abort the workflow sending the item back to the submitter * workspace - * + * * @throws Exception */ public void deleteOneTest() throws Exception { @@ -1682,4 +1687,84 @@ public class WorkflowItemRestRepositoryIT extends AbstractControllerIntegrationT .param("uuid", String.valueOf(witem.getItem().getID()))) .andExpect(status().isUnauthorized()); } + + @Test + public void stepEmbedTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + // 1. A community-collection structure with one parent community with sub-community and three collections + // (different workflow steps and reviewers). + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + EPerson reviewer1 = EPersonBuilder.createEPerson(context).withEmail("reviewer1@example.com") + .withPassword(password).build(); + + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1") + .withWorkflowGroup(1, reviewer1).build(); + + EPerson reviewer2 = EPersonBuilder.createEPerson(context).withEmail("reviewer2@example.com") + .withPassword(password).build(); + + Collection col2 = CollectionBuilder.createCollection(context, child1).withName("Collection 2") + .withWorkflowGroup(2, reviewer2).build(); + + EPerson reviewer3 = EPersonBuilder.createEPerson(context).withEmail("reviewer3@example.com") + .withPassword(password).build(); + + Collection col3 = CollectionBuilder.createCollection(context, child1).withName("Collection 3") + .withWorkflowGroup(3, reviewer3).build(); + + //2. three workflow items in the three collections (this will lead to pool task) + XmlWorkflowItem witem1 = WorkflowItemBuilder.createWorkflowItem(context, col1) + .withTitle("Workflow Item 1") + .withIssueDate("2016-02-13") + .build(); + + XmlWorkflowItem witem2 = WorkflowItemBuilder.createWorkflowItem(context, col2) + .withTitle("Workflow Item 2") + .withIssueDate("2016-02-13") + .build(); + + XmlWorkflowItem witem3 = WorkflowItemBuilder.createWorkflowItem(context, col3) + .withTitle("Workflow Item 3") + .withIssueDate("2016-02-13") + .build(); + + Step step = xmlWorkflowFactory.getStepByName("reviewstep"); + String token = getAuthToken(admin.getEmail(), password); + + getClient(token).perform(get("/api/workflow/workflowitems/" + witem1.getID()) + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + WorkflowItemMatcher.matchItemWithTitleAndDateIssued(witem1, + "Workflow Item 1", "2016-02-13"))) + .andExpect(jsonPath("$._embedded.step", WorkflowStepMatcher.matchWorkflowStepEntry(step))); + + step = xmlWorkflowFactory.getStepByName("editstep"); + + getClient(token).perform(get("/api/workflow/workflowitems/" + witem2.getID()) + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + WorkflowItemMatcher.matchItemWithTitleAndDateIssued(witem2, + "Workflow Item 2", "2016-02-13"))) + .andExpect(jsonPath("$._embedded.step", WorkflowStepMatcher.matchWorkflowStepEntry(step))); + + step = xmlWorkflowFactory.getStepByName("finaleditstep"); + + getClient(token).perform(get("/api/workflow/workflowitems/" + witem3.getID()) + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + WorkflowItemMatcher.matchItemWithTitleAndDateIssued(witem3, + "Workflow Item 3", "2016-02-13"))) + .andExpect(jsonPath("$._embedded.step", WorkflowStepMatcher.matchWorkflowStepEntry(step))); + } } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index cd8abfe233..bfedc0d372 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -8,6 +8,7 @@ package org.dspace.app.rest; import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST_VALUE; import static org.springframework.http.MediaType.parseMediaType; @@ -35,6 +36,7 @@ import org.dspace.app.rest.builder.BitstreamBuilder; import org.dspace.app.rest.builder.CollectionBuilder; import org.dspace.app.rest.builder.CommunityBuilder; import org.dspace.app.rest.builder.EPersonBuilder; +import org.dspace.app.rest.builder.GroupBuilder; import org.dspace.app.rest.builder.ItemBuilder; import org.dspace.app.rest.builder.WorkspaceItemBuilder; import org.dspace.app.rest.matcher.CollectionMatcher; @@ -52,6 +54,8 @@ import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.content.WorkspaceItem; import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; +import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; import org.junit.Before; @@ -70,14 +74,34 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration @Autowired private ConfigurationService configurationService; + private Group embargoedGroups; + private Group embargoedGroup1; + private Group embargoedGroup2; + private Group anonymousGroup; + @Before @Override public void setUp() throws Exception { - super.setUp(); + context.turnOffAuthorisationSystem(); - //disable file upload mandatory - configurationService.setProperty("webui.submit.upload.required", false); + embargoedGroups = GroupBuilder.createGroup(context) + .withName("Embargoed Groups") + .build(); + + embargoedGroup1 = GroupBuilder.createGroup(context) + .withName("Embargoed Group 1") + .withParent(embargoedGroups) + .build(); + + embargoedGroup2 = GroupBuilder.createGroup(context) + .withName("Embargoed Group 2") + .withParent(embargoedGroups) + .build(); + + anonymousGroup = EPersonServiceFactory.getInstance().getGroupService().findByName(context, Group.ANONYMOUS); + + context.restoreAuthSystemState(); } @Test @@ -637,6 +661,9 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .withIssueDate("2017-10-17") .build(); + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + getClient(authToken).perform(get("/api/submission/workspaceitems/" + workspaceItem1.getID())) .andExpect(status().isOk()) .andExpect(jsonPath("$.errors").doesNotExist()) @@ -697,6 +724,9 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .withSubject("ExtraEntry") .build(); + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + // a simple patch to update an existent metadata List updateTitle = new ArrayList(); Map value = new HashMap(); @@ -803,6 +833,9 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration null, "2017-10-17", "ExtraEntry")))) ; + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + // try to remove a metadata in a specific position List removeMidSubject = new ArrayList(); removeMidSubject.add(new RemoveOperation("/sections/traditionalpagetwo/dc.subject/1")); @@ -932,6 +965,8 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .withSubject("ExtraEntry") .build(); + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); // try to add the title List addTitle = new ArrayList(); @@ -989,6 +1024,9 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .withIssueDate("2017-10-17") .build(); + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + // try to add multiple subjects at once List addSubjects = new ArrayList(); // create a list of values to use in add operation @@ -1210,6 +1248,9 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .andExpect(jsonPath("$.sections.license.url").isEmpty()) ; + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + // try to grant the license with an add operation List addGrant = new ArrayList(); addGrant.add(new AddOperation("/sections/license/granted", true)); @@ -1359,6 +1400,9 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .grantLicense() .build(); + //disable file upload mandatory + configurationService.setProperty("webui.submit.upload.required", false); + // check that our workspaceitems come with a license (all are build in the same way, just check the first) getClient().perform(get("/api/submission/workspaceitems/" + witem.getID())) .andExpect(status().isOk()) @@ -1601,6 +1645,147 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration ; } + @Test + public void patchUploadAddAccessConditionTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection collection1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + InputStream pdf = getClass().getResourceAsStream("simple-article.pdf"); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, collection1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2019-10-01") + .withFulltext("simple-article.pdf", "/local/path/simple-article.pdf", pdf) + .build(); + + context.restoreAuthSystemState(); + + // create a list of values to use in add accessCondition + List addAccessCondition = new ArrayList(); + Map value = new HashMap(); + value.put("name", "embargoedWithGroupSelect"); + value.put("groupUUID", embargoedGroup1.getID().toString()); + value.put("endDate", "2030-10-02"); + addAccessCondition.add(new AddOperation("/sections/upload/files/0/accessConditions/-", value)); + + String patchBody = getPatchContent(addAccessCondition); + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$",Matchers.allOf( + hasJsonPath("$.sections.upload.files[0].accessConditions[0].name", + is("embargoedWithGroupSelect")), + hasJsonPath("$.sections.upload.files[0].accessConditions[0].groupUUID", + is(embargoedGroup1.getID().toString())) + ))); + + // verify that the patch changes have been persisted + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$",Matchers.allOf( + hasJsonPath("$.sections.upload.files[0].accessConditions[0].name", + is("embargoedWithGroupSelect")), + hasJsonPath("$.sections.upload.files[0].accessConditions[0].groupUUID", + is(embargoedGroup1.getID().toString())) + ))); + } + + @Test + public void patchUploadRemoveAccessConditionTest() throws Exception { + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + + Collection collection1 = CollectionBuilder.createCollection(context, child1) + .withName("Collection 1") + .build(); + + InputStream pdf = getClass().getResourceAsStream("simple-article.pdf"); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, collection1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2019-10-01") + .withFulltext("simple-article.pdf", "/local/path/simple-article.pdf", pdf) + .build(); + + context.restoreAuthSystemState(); + + // create a list of values to use in add operation + List addAccessCondition = new ArrayList(); + Map value = new HashMap(); + value.put("name", "embargoedWithGroupSelect"); + value.put("groupUUID", embargoedGroup1.getID().toString()); + value.put("endDate", "2020-01-01"); + addAccessCondition.add(new AddOperation("/sections/upload/files/0/accessConditions/-", value)); + + String patchBody = getPatchContent(addAccessCondition); + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$",Matchers.allOf( + hasJsonPath("$.sections.upload.files[0].accessConditions[0].name", + is("embargoedWithGroupSelect")), + hasJsonPath("$.sections.upload.files[0].accessConditions[0].endDate", + is("2020-01-01")), + hasJsonPath("$.sections.upload.files[0].accessConditions[0].groupUUID", + is(embargoedGroup1.getID().toString())) + ))); + + // verify that the patch changes have been persisted + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$",Matchers.allOf( + hasJsonPath("$.sections.upload.files[0].accessConditions[0].name", + is("embargoedWithGroupSelect")), + hasJsonPath("$.sections.upload.files[0].accessConditions[0].endDate", + is("2020-01-01")), + hasJsonPath("$.sections.upload.files[0].accessConditions[0].groupUUID", + is(embargoedGroup1.getID().toString())) + ))); + + // create a list of values to use in remove operation + List removeAccessCondition = new ArrayList(); + removeAccessCondition.add(new RemoveOperation("/sections/upload/files/0/accessConditions")); + + String patchReplaceBody = getPatchContent(removeAccessCondition); + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchReplaceBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$",Matchers.allOf( + hasJsonPath("$.sections.upload.files[0].accessConditions",hasSize(0))))); + + // verify that the patch changes have been persisted + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$",Matchers.allOf( + hasJsonPath("$.sections.upload.files[0].accessConditions", hasSize(0)) + ))); + } + @Test /** * Test the upload of files in the upload over section @@ -1672,8 +1857,6 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .withIssueDate("2017-10-17") .build(); - configurationService.setProperty("webui.submit.upload.required", true); - InputStream pdf = getClass().getResourceAsStream("simple-article.pdf"); final MockMultipartFile pdfFile = new MockMultipartFile("file", "/local/path/simple-article.pdf", "application/pdf", pdf); @@ -1715,8 +1898,6 @@ public class WorkspaceItemRestRepositoryIT extends AbstractControllerIntegration .withIssueDate("2017-10-17") .build(); - configurationService.setProperty("webui.submit.upload.required", true); - //Verify there is an error since no file was uploaded (with upload required set to true) getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) .andExpect(status().isOk()) diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/builder/CommunityBuilder.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/builder/CommunityBuilder.java index 229ff0d0fd..788aa502a6 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/builder/CommunityBuilder.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/builder/CommunityBuilder.java @@ -19,6 +19,8 @@ import org.dspace.content.Community; import org.dspace.content.MetadataSchemaEnum; import org.dspace.content.service.DSpaceObjectService; import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.Group; /** * Builder to construct Community objects @@ -74,6 +76,23 @@ public class CommunityBuilder extends AbstractDSpaceObjectBuilder { return this; } + /** + * Create an admin group for the community with the specified members + * + * @param members epersons to add to the admin group + * @return this builder + * @throws SQLException + * @throws AuthorizeException + */ + public CommunityBuilder withAdminGroup(EPerson... members) throws SQLException, AuthorizeException { + Group g = communityService.createAdministrators(context, community); + for (EPerson e : members) { + groupService.addMember(context, g, e); + } + groupService.update(context, g); + return this; + } + public CommunityBuilder addParentCommunity(final Context context, final Community parent) throws SQLException, AuthorizeException { communityService.addSubcommunity(context, parent, community); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java index 58840bb798..d9497f182b 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/BitstreamMatcher.java @@ -66,6 +66,19 @@ public class BitstreamMatcher { ); } + public static Matcher matchBitstreamEntryWithoutEmbed(UUID uuid, long size) { + return allOf( + //Check ID and size + hasJsonPath("$.uuid", is(uuid.toString())), + hasJsonPath("$.sizeBytes", is((int) size)), + //Make sure we have a checksum + hasJsonPath("$.checkSum", matchChecksum()), + //Make sure we have a valid format + //Check links + matchLinks(uuid) + ); + } + private static Matcher matchChecksum() { return allOf( hasJsonPath("$.checkSumAlgorithm", not(empty())), diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ClaimedTaskMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ClaimedTaskMatcher.java index 317544b570..0d2ba67f16 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ClaimedTaskMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ClaimedTaskMatcher.java @@ -36,7 +36,6 @@ public class ClaimedTaskMatcher { */ public static Matcher matchClaimedTask(ClaimedTask cTask, String step) { return allOf( - hasJsonPath("$.step", is(step)), // Check workflowitem properties matchProperties(cTask), // Check links diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/PoolTaskMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/PoolTaskMatcher.java index 9ebca9d572..aeed70e527 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/PoolTaskMatcher.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/PoolTaskMatcher.java @@ -36,7 +36,6 @@ public class PoolTaskMatcher { */ public static Matcher matchPoolTask(PoolTask pTask, String step) { return allOf( - hasJsonPath("$.step", is(step)), // Check workflowitem properties matchProperties(pTask), // Check links diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/projection/MockProjection.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/projection/MockProjection.java index 0c8c976a03..b34c945065 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/projection/MockProjection.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/projection/MockProjection.java @@ -12,6 +12,7 @@ import javax.annotation.Nullable; import org.dspace.app.rest.model.LinkRest; import org.dspace.app.rest.model.MockObject; import org.dspace.app.rest.model.MockObjectRest; +import org.dspace.app.rest.model.RestAddressableModel; import org.dspace.app.rest.model.RestModel; import org.dspace.app.rest.model.hateoas.HALResource; import org.springframework.hateoas.Link; @@ -87,8 +88,9 @@ public class MockProjection implements Projection { return halResource; } - public boolean allowEmbedding(HALResource halResource, LinkRest linkRest) { - return true; + public boolean allowEmbedding(HALResource halResource, LinkRest linkRest, + Link... oldLinks) { + return halResource.getContent().getEmbedLevel() < 2; } public boolean allowLinking(HALResource halResource, LinkRest linkRest) { diff --git a/dspace/config/crosswalks/oai/metadataFormats/mets.xsl b/dspace/config/crosswalks/oai/metadataFormats/mets.xsl index 41d683f075..ca41fcdb2b 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/mets.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/mets.xsl @@ -1,9 +1,7 @@ + xmlns:doc="http://www.lyncode.com/xoai" version="2.0"> @@ -20,7 +18,7 @@ - + diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 2d1149c4d3..2e13d73f97 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -8,6 +8,12 @@ # (Requires reboot of servlet container, e.g. Tomcat, to reload) rest.cors.allowed-origins = * +# This property determines the max embeddepth for a FullProjection. This is also used by the SpecificLevelProjection +# as a fallback incase the property is defined on the bean +rest.projections.full.max = 2 + +# This property determines the max embed depth for a SpecificLevelProjection +rest.projection.specificLevel.maxEmbed = 5 #---------------------------------------------------------------# # These configs are used by the deprecated REST (v4-6) module # diff --git a/dspace/config/spring/rest/projections.xml b/dspace/config/spring/rest/projections.xml new file mode 100644 index 0000000000..6f082b7b5b --- /dev/null +++ b/dspace/config/spring/rest/projections.xml @@ -0,0 +1,9 @@ + + + + + + +