mirror of
https://github.com/DSpace/DSpace.git
synced 2025-10-17 23:13:10 +00:00
Merge remote-tracking branch 'origin/DS-4433_Specify-embeds-Continued-2' into w2p-69133_refactoring-and-its-specific-embeds
This commit is contained in:
@@ -32,6 +32,7 @@ import org.springframework.core.type.filter.AssignableTypeFilter;
|
|||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageImpl;
|
import org.springframework.data.domain.PageImpl;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.hateoas.Link;
|
||||||
import org.springframework.hateoas.Resource;
|
import org.springframework.hateoas.Resource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -155,9 +156,12 @@ public class ConverterService {
|
|||||||
* @throws ClassCastException if the resource type is not compatible with the inferred return type.
|
* @throws ClassCastException if the resource type is not compatible with the inferred return type.
|
||||||
*/
|
*/
|
||||||
public <T extends HALResource> T toResource(RestModel restObject) {
|
public <T extends HALResource> T toResource(RestModel restObject) {
|
||||||
|
return toResource(restObject, new Link[] {});
|
||||||
|
}
|
||||||
|
public <T extends HALResource> T toResource(RestModel restObject, Link... oldLinks) {
|
||||||
T halResource = getResource(restObject);
|
T halResource = getResource(restObject);
|
||||||
if (restObject instanceof RestAddressableModel) {
|
if (restObject instanceof RestAddressableModel) {
|
||||||
utils.embedOrLinkClassLevelRels(halResource);
|
utils.embedOrLinkClassLevelRels(halResource, oldLinks);
|
||||||
halLinkService.addLinks(halResource);
|
halLinkService.addLinks(halResource);
|
||||||
Projection projection = ((RestAddressableModel) restObject).getProjection();
|
Projection projection = ((RestAddressableModel) restObject).getProjection();
|
||||||
return projection.transformResource(halResource);
|
return projection.transformResource(halResource);
|
||||||
|
@@ -8,8 +8,10 @@
|
|||||||
package org.dspace.app.rest.projection;
|
package org.dspace.app.rest.projection;
|
||||||
|
|
||||||
import org.dspace.app.rest.model.LinkRest;
|
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.RestModel;
|
||||||
import org.dspace.app.rest.model.hateoas.HALResource;
|
import org.dspace.app.rest.model.hateoas.HALResource;
|
||||||
|
import org.springframework.hateoas.Link;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract base class for projections.
|
* Abstract base class for projections.
|
||||||
@@ -34,7 +36,8 @@ public abstract class AbstractProjection implements Projection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean allowEmbedding(HALResource halResource, LinkRest linkRest) {
|
public boolean allowEmbedding(HALResource<? extends RestAddressableModel> halResource, LinkRest linkRest,
|
||||||
|
Link... oldLinks) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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<Projection> projections;
|
||||||
|
|
||||||
|
public CompositeProjection(List<Projection> projections) {
|
||||||
|
this.projections = projections;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T transformModel(T modelObject) {
|
||||||
|
for (Projection projection : projections) {
|
||||||
|
modelObject = projection.transformModel(modelObject);
|
||||||
|
}
|
||||||
|
return modelObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends RestModel> T transformRest(T restObject) {
|
||||||
|
for (Projection projection : projections) {
|
||||||
|
restObject = projection.transformRest(restObject);
|
||||||
|
}
|
||||||
|
return restObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends HALResource> T transformResource(T halResource) {
|
||||||
|
for (Projection projection : projections) {
|
||||||
|
halResource = projection.transformResource(halResource);
|
||||||
|
}
|
||||||
|
return halResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowEmbedding(HALResource<? extends RestAddressableModel> 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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.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.
|
||||||
|
*/
|
||||||
|
public class EmbedRelsProjection extends AbstractProjection {
|
||||||
|
|
||||||
|
public final static String NAME = "embedrels";
|
||||||
|
|
||||||
|
private final Set<String> embedRels;
|
||||||
|
|
||||||
|
public EmbedRelsProjection(Set<String> embedRels) {
|
||||||
|
this.embedRels = embedRels;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowEmbedding(HALResource<? extends RestAddressableModel> 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -8,7 +8,10 @@
|
|||||||
package org.dspace.app.rest.projection;
|
package org.dspace.app.rest.projection;
|
||||||
|
|
||||||
import org.dspace.app.rest.model.LinkRest;
|
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.app.rest.model.hateoas.HALResource;
|
||||||
|
import org.dspace.services.factory.DSpaceServicesFactory;
|
||||||
|
import org.springframework.hateoas.Link;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,14 +21,17 @@ import org.springframework.stereotype.Component;
|
|||||||
public class FullProjection extends AbstractProjection {
|
public class FullProjection extends AbstractProjection {
|
||||||
|
|
||||||
public final static String NAME = "full";
|
public final static String NAME = "full";
|
||||||
|
private final int maxEmbed = DSpaceServicesFactory.getInstance().getConfigurationService()
|
||||||
|
.getIntProperty("projections.full.max", 2);
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return NAME;
|
return NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean allowEmbedding(HALResource halResource, LinkRest linkRest) {
|
public boolean allowEmbedding(HALResource<? extends RestAddressableModel> halResource, LinkRest linkRest,
|
||||||
return true;
|
Link... oldLinks) {
|
||||||
|
return halResource.getContent().getEmbedLevel() < maxEmbed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -10,9 +10,12 @@ package org.dspace.app.rest.projection;
|
|||||||
import javax.persistence.Entity;
|
import javax.persistence.Entity;
|
||||||
|
|
||||||
import org.dspace.app.rest.model.LinkRest;
|
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.RestModel;
|
||||||
import org.dspace.app.rest.model.hateoas.HALResource;
|
import org.dspace.app.rest.model.hateoas.HALResource;
|
||||||
import org.dspace.app.rest.repository.DSpaceRestRepository;
|
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.stereotype.Component;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@@ -44,7 +47,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
* <li> After it is converted to a {@link RestModel}, the projection may modify it
|
* <li> After it is converted to a {@link RestModel}, the projection may modify it
|
||||||
* via {@link #transformRest(RestModel)}.</li>
|
* via {@link #transformRest(RestModel)}.</li>
|
||||||
* <li> During conversion to a {@link HALResource}, the projection may opt in to certain annotation-discovered
|
* <li> 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)}</li>
|
* and {@link #allowLinking(HALResource, LinkRest)}</li>
|
||||||
* <li> After conversion to a {@link HALResource}, the projection may modify it
|
* <li> After conversion to a {@link HALResource}, the projection may modify it
|
||||||
* via {@link #transformResource(HALResource)}.</li>
|
* via {@link #transformResource(HALResource)}.</li>
|
||||||
@@ -52,8 +55,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
*
|
*
|
||||||
* <h2>How a projection is chosen</h2>
|
* <h2>How a projection is chosen</h2>
|
||||||
*
|
*
|
||||||
* When a REST request is made, the projection argument, if present, is used to look up the projection to use,
|
* See {@link Utils#obtainProjection()}.
|
||||||
* by name. If no argument is present, {@link DefaultProjection} will be used.
|
|
||||||
*/
|
*/
|
||||||
public interface Projection {
|
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 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 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.
|
* @return true if allowed, false otherwise.
|
||||||
*/
|
*/
|
||||||
boolean allowEmbedding(HALResource halResource, LinkRest linkRest);
|
boolean allowEmbedding(HALResource<? extends RestAddressableModel> halResource, LinkRest linkRest,
|
||||||
|
Link... oldLinks);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tells whether this projection permits the linking of a particular linkable subresource.
|
* 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}.
|
* 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.
|
* 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.
|
* @param halResource the resource from which the link may or may not be made.
|
||||||
|
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
package org.dspace.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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Catch-all projection that allows embedding of all subresources.
|
||||||
|
*/
|
||||||
|
public class SpecificLevelProjection extends AbstractProjection {
|
||||||
|
|
||||||
|
public final static String NAME = "level.";
|
||||||
|
|
||||||
|
private int maxEmbed = DSpaceServicesFactory.getInstance().getConfigurationService()
|
||||||
|
.getIntProperty("projections.full.max", 2);
|
||||||
|
|
||||||
|
public int getMaxEmbed() {
|
||||||
|
return maxEmbed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxEmbed(int maxEmbed) {
|
||||||
|
this.maxEmbed = maxEmbed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return NAME + getMaxEmbed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowEmbedding(HALResource<? extends RestAddressableModel> halResource, LinkRest linkRest,
|
||||||
|
Link... oldLinks) {
|
||||||
|
return halResource.getContent().getEmbedLevel() < maxEmbed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowLinking(HALResource halResource, LinkRest linkRest) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -27,6 +27,7 @@ import java.sql.SQLException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -37,6 +38,7 @@ import java.util.Set;
|
|||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
@@ -56,7 +58,9 @@ import org.dspace.app.rest.model.RestModel;
|
|||||||
import org.dspace.app.rest.model.hateoas.DSpaceResource;
|
import org.dspace.app.rest.model.hateoas.DSpaceResource;
|
||||||
import org.dspace.app.rest.model.hateoas.EmbeddedPage;
|
import org.dspace.app.rest.model.hateoas.EmbeddedPage;
|
||||||
import org.dspace.app.rest.model.hateoas.HALResource;
|
import org.dspace.app.rest.model.hateoas.HALResource;
|
||||||
|
import org.dspace.app.rest.projection.CompositeProjection;
|
||||||
import org.dspace.app.rest.projection.DefaultProjection;
|
import org.dspace.app.rest.projection.DefaultProjection;
|
||||||
|
import org.dspace.app.rest.projection.EmbedRelsProjection;
|
||||||
import org.dspace.app.rest.projection.Projection;
|
import org.dspace.app.rest.projection.Projection;
|
||||||
import org.dspace.app.rest.repository.DSpaceRestRepository;
|
import org.dspace.app.rest.repository.DSpaceRestRepository;
|
||||||
import org.dspace.app.rest.repository.LinkRestRepository;
|
import org.dspace.app.rest.repository.LinkRestRepository;
|
||||||
@@ -98,7 +102,7 @@ public class Utils {
|
|||||||
/**
|
/**
|
||||||
* The maximum number of embed levels to allow.
|
* The maximum number of embed levels to allow.
|
||||||
*/
|
*/
|
||||||
private static final int EMBED_MAX_LEVELS = 2;
|
private static final int EMBED_MAX_LEVELS = 10;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
ApplicationContext applicationContext;
|
ApplicationContext applicationContext;
|
||||||
@@ -439,28 +443,87 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the projection requested by the current servlet request, or {@link DefaultProjection} if none
|
* Gets the effective projection requested by the current servlet request, or {@link DefaultProjection} if none
|
||||||
* is specified.
|
* is specified.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p><p>
|
||||||
|
* 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}.
|
||||||
|
* </p>
|
||||||
*
|
*
|
||||||
* @return the requested or default projection, never {@code null}.
|
* @return the requested or default projection, never {@code null}.
|
||||||
* @throws IllegalArgumentException if the request specifies an unknown projection name.
|
* @throws IllegalArgumentException if the request specifies an unknown projection name.
|
||||||
*/
|
*/
|
||||||
public Projection obtainProjection() {
|
public Projection obtainProjection() {
|
||||||
String projectionName = requestService.getCurrentRequest().getServletRequest().getParameter("projection");
|
ServletRequest servletRequest = requestService.getCurrentRequest().getServletRequest();
|
||||||
return converter.getProjection(projectionName);
|
List<String> projectionNames = getValues(servletRequest, "projection");
|
||||||
|
Set<String> embedRels = new HashSet<>(getValues(servletRequest, "embed"));
|
||||||
|
|
||||||
|
List<Projection> 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p><p>
|
||||||
|
* It provides the values in the order they were given in the request, and automatically de-dupes them.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param servletRequest the servlet request.
|
||||||
|
* @param parameterName the parameter name.
|
||||||
|
* @return the ordered, de-duped values, possibly empty, never {@code null}.
|
||||||
|
*/
|
||||||
|
private List<String> getValues(ServletRequest servletRequest, String parameterName) {
|
||||||
|
String[] rawValues = servletRequest.getParameterValues(parameterName);
|
||||||
|
List<String> 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.
|
* Adds embeds or links for all class-level LinkRel annotations for which embeds or links are allowed.
|
||||||
*
|
*
|
||||||
* @param halResource the resource.
|
* @param halResource the resource.
|
||||||
|
* @param oldLinks previously traversed links
|
||||||
*/
|
*/
|
||||||
public void embedOrLinkClassLevelRels(HALResource<RestAddressableModel> halResource) {
|
public void embedOrLinkClassLevelRels(HALResource<RestAddressableModel> halResource, Link... oldLinks) {
|
||||||
Projection projection = halResource.getContent().getProjection();
|
Projection projection = halResource.getContent().getProjection();
|
||||||
getLinkRests(halResource.getContent().getClass()).stream().forEach((linkRest) -> {
|
getLinkRests(halResource.getContent().getClass()).stream().forEach((linkRest) -> {
|
||||||
Link link = linkToSubResource(halResource.getContent(), linkRest.name());
|
Link link = linkToSubResource(halResource.getContent(), linkRest.name());
|
||||||
if (projection.allowEmbedding(halResource, linkRest)) {
|
if (projection.allowEmbedding(halResource, linkRest, oldLinks)) {
|
||||||
embedRelFromRepository(halResource, linkRest.name(), link, linkRest);
|
embedRelFromRepository(halResource, linkRest.name(), link, linkRest, oldLinks);
|
||||||
halResource.add(link); // unconditionally link if embedding was allowed
|
halResource.add(link); // unconditionally link if embedding was allowed
|
||||||
} else if (projection.allowLinking(halResource, linkRest)) {
|
} else if (projection.allowLinking(halResource, linkRest)) {
|
||||||
halResource.add(link);
|
halResource.add(link);
|
||||||
@@ -499,6 +562,32 @@ public class Utils {
|
|||||||
*/
|
*/
|
||||||
void embedRelFromRepository(HALResource<? extends RestAddressableModel> resource,
|
void embedRelFromRepository(HALResource<? extends RestAddressableModel> resource,
|
||||||
String rel, Link link, LinkRest linkRest) {
|
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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* </p><p>
|
||||||
|
* Implementation note: The caller is responsible for ensuring that the projection allows the embed
|
||||||
|
* before calling this method.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @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<? extends RestAddressableModel> resource,
|
||||||
|
String rel, Link link, LinkRest linkRest, Link... oldLinks) {
|
||||||
if (resource.getContent().getEmbedLevel() == EMBED_MAX_LEVELS) {
|
if (resource.getContent().getEmbedLevel() == EMBED_MAX_LEVELS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -510,7 +599,7 @@ public class Utils {
|
|||||||
Object contentId = getContentIdForLinkMethod(resource.getContent(), method);
|
Object contentId = getContentIdForLinkMethod(resource.getContent(), method);
|
||||||
try {
|
try {
|
||||||
Object linkedObject = method.invoke(linkRepository, null, contentId, null, projection);
|
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) {
|
} catch (InvocationTargetException e) {
|
||||||
if (e.getTargetException() instanceof RuntimeException) {
|
if (e.getTargetException() instanceof RuntimeException) {
|
||||||
throw (RuntimeException) e.getTargetException();
|
throw (RuntimeException) e.getTargetException();
|
||||||
@@ -615,17 +704,37 @@ public class Utils {
|
|||||||
*/
|
*/
|
||||||
private Object wrapForEmbedding(HALResource<? extends RestAddressableModel> resource,
|
private Object wrapForEmbedding(HALResource<? extends RestAddressableModel> resource,
|
||||||
Object linkedObject, Link link) {
|
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<? extends RestAddressableModel> resource,
|
||||||
|
Object linkedObject, Link link, Link... oldLinks) {
|
||||||
int childEmbedLevel = resource.getContent().getEmbedLevel() + 1;
|
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) {
|
if (linkedObject instanceof RestAddressableModel) {
|
||||||
RestAddressableModel restObject = (RestAddressableModel) linkedObject;
|
RestAddressableModel restObject = (RestAddressableModel) linkedObject;
|
||||||
restObject.setEmbedLevel(childEmbedLevel);
|
restObject.setEmbedLevel(childEmbedLevel);
|
||||||
return converter.toResource(restObject);
|
return converter.toResource(restObject, newList);
|
||||||
} else if (linkedObject instanceof Page) {
|
} else if (linkedObject instanceof Page) {
|
||||||
// The first page has already been constructed by a link repository and we only need to wrap it
|
// The first page has already been constructed by a link repository and we only need to wrap it
|
||||||
Page<RestAddressableModel> page = (Page<RestAddressableModel>) linkedObject;
|
Page<RestAddressableModel> page = (Page<RestAddressableModel>) linkedObject;
|
||||||
return new EmbeddedPage(link.getHref(), page.map((restObject) -> {
|
return new EmbeddedPage(link.getHref(), page.map((restObject) -> {
|
||||||
restObject.setEmbedLevel(childEmbedLevel);
|
restObject.setEmbedLevel(childEmbedLevel);
|
||||||
return converter.toResource(restObject);
|
return converter.toResource(restObject, newList);
|
||||||
}), null, link.getRel());
|
}), null, link.getRel());
|
||||||
} else if (linkedObject instanceof List) {
|
} else if (linkedObject instanceof List) {
|
||||||
// The full list has been retrieved and we need to provide the first page for embedding
|
// The full list has been retrieved and we need to provide the first page for embedding
|
||||||
@@ -637,7 +746,7 @@ public class Utils {
|
|||||||
return new EmbeddedPage(link.getHref(),
|
return new EmbeddedPage(link.getHref(),
|
||||||
page.map((restObject) -> {
|
page.map((restObject) -> {
|
||||||
restObject.setEmbedLevel(childEmbedLevel);
|
restObject.setEmbedLevel(childEmbedLevel);
|
||||||
return converter.toResource(restObject);
|
return converter.toResource(restObject, newList);
|
||||||
}),
|
}),
|
||||||
list, link.getRel());
|
list, link.getRel());
|
||||||
} else {
|
} else {
|
||||||
|
@@ -108,4 +108,20 @@
|
|||||||
</property>
|
</property>
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
|
<bean class="org.dspace.app.rest.projection.SpecificLevelProjection">
|
||||||
|
<property name="maxEmbed" value="1"/>
|
||||||
|
</bean>
|
||||||
|
<bean class="org.dspace.app.rest.projection.SpecificLevelProjection">
|
||||||
|
<property name="maxEmbed" value="2"/>
|
||||||
|
</bean>
|
||||||
|
<bean class="org.dspace.app.rest.projection.SpecificLevelProjection">
|
||||||
|
<property name="maxEmbed" value="3"/>
|
||||||
|
</bean>
|
||||||
|
<bean class="org.dspace.app.rest.projection.SpecificLevelProjection">
|
||||||
|
<property name="maxEmbed" value="4"/>
|
||||||
|
</bean>
|
||||||
|
<bean class="org.dspace.app.rest.projection.SpecificLevelProjection">
|
||||||
|
<property name="maxEmbed" value="5"/>
|
||||||
|
</bean>
|
||||||
|
|
||||||
</beans>
|
</beans>
|
||||||
|
@@ -251,6 +251,16 @@ public class ItemRestRepositoryIT extends AbstractControllerIntegrationTest {
|
|||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$", HalMatcher.matchNoEmbeds()))
|
.andExpect(jsonPath("$", HalMatcher.matchNoEmbeds()))
|
||||||
.andExpect(jsonPath("$", publicItem1Matcher));
|
.andExpect(jsonPath("$", publicItem1Matcher));
|
||||||
|
|
||||||
|
// When exact embeds are requested, response should include expected properties, links, and exact embeds.
|
||||||
|
getClient().perform(get("/api/core/items/" + publicItem1.getID())
|
||||||
|
.param("embed", "bundles,owningCollection"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$", HalMatcher.matchEmbeds(
|
||||||
|
"bundles[]",
|
||||||
|
"owningCollection"
|
||||||
|
)))
|
||||||
|
.andExpect(jsonPath("$", publicItem1Matcher));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@@ -12,6 +12,7 @@ import javax.annotation.Nullable;
|
|||||||
import org.dspace.app.rest.model.LinkRest;
|
import org.dspace.app.rest.model.LinkRest;
|
||||||
import org.dspace.app.rest.model.MockObject;
|
import org.dspace.app.rest.model.MockObject;
|
||||||
import org.dspace.app.rest.model.MockObjectRest;
|
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.RestModel;
|
||||||
import org.dspace.app.rest.model.hateoas.HALResource;
|
import org.dspace.app.rest.model.hateoas.HALResource;
|
||||||
import org.springframework.hateoas.Link;
|
import org.springframework.hateoas.Link;
|
||||||
@@ -87,8 +88,9 @@ public class MockProjection implements Projection {
|
|||||||
return halResource;
|
return halResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean allowEmbedding(HALResource halResource, LinkRest linkRest) {
|
public boolean allowEmbedding(HALResource<? extends RestAddressableModel> halResource, LinkRest linkRest,
|
||||||
return true;
|
Link... oldLinks) {
|
||||||
|
return halResource.getContent().getEmbedLevel() < 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean allowLinking(HALResource halResource, LinkRest linkRest) {
|
public boolean allowLinking(HALResource halResource, LinkRest linkRest) {
|
||||||
|
Reference in New Issue
Block a user