DS-3483 add support to embed linked resources collection in the HAL document

This is an initial draft that require further refinements. By default now all the collection properties
are embedded in response, linked entities listed in the LinksRest annotation of the repository are
included only if specified in the resource wrapper instantiation and supported for embedding by
the link repository (i.e the relation have suitable default or don't depend on additional parameters)
This commit is contained in:
Andrea Bollini
2017-06-20 12:29:20 +02:00
parent 0d21ce0c7d
commit e4bc0f028e
9 changed files with 308 additions and 25 deletions

View File

@@ -27,6 +27,7 @@ import org.dspace.app.rest.model.LinkRest;
import org.dspace.app.rest.model.LinksRest;
import org.dspace.app.rest.model.RestModel;
import org.dspace.app.rest.model.hateoas.DSpaceResource;
import org.dspace.app.rest.model.hateoas.EmbeddedPage;
import org.dspace.app.rest.repository.DSpaceRestRepository;
import org.dspace.app.rest.repository.LinkRestRepository;
import org.dspace.app.rest.utils.Utils;
@@ -175,9 +176,26 @@ public class RestResourceController implements InitializingBean {
//TODO create a custom exception
throw new ResourceNotFoundException(rel + "undefined for "+ model);
}
ResourceSupport resu = (ResourceSupport) result.getEmbedded().get(rel);
return resu;
else if (result.getEmbedded().get(rel) instanceof EmbeddedPage){
// this is a very inefficient scenario. We have an embedded list
// already fully retrieved that we need to limit with pagination
// parameter. BTW change the default sorting is not implemented at
// the current stage and could be overcompex to implement
// if we really want to implement pagination we should implement a
// link repository so to fall in the previous block code
EmbeddedPage ep = (EmbeddedPage) result.getEmbedded().get(rel);
List<? extends RestModel> fullList = ep.getFullList();
if (fullList == null || fullList.size() == 0) return null;
int start = page.getOffset();
int end = (start + page.getPageSize()) > fullList.size() ? fullList.size() : (start + page.getPageSize());
DSpaceRestRepository<RestModel, ?> resourceRepository = utils.getResourceRepository(fullList.get(0).getCategory(), fullList.get(0).getType());
PageImpl<RestModel> pageResult = new PageImpl(fullList.subList(start, end), page, fullList.size());
return assembler.toResource(pageResult .map(resourceRepository::wrapResource));
}
else {
ResourceSupport resu = (ResourceSupport) result.getEmbedded().get(rel);
return resu;
}
}
@RequestMapping(method = RequestMethod.GET)

View File

@@ -11,6 +11,8 @@ import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
/**
* The Item REST Resource
@@ -79,6 +81,9 @@ public class ItemRest extends DSpaceObjectRest {
public void setTemplateItemOf(CollectionRest templateItemOf){
this.templateItemOf = templateItemOf;
}
@LinkRest(linkClass = BitstreamRest.class)
@JsonIgnore
public List<BitstreamRest> getBitstreams() {
return bitstreams;
}

View File

@@ -18,12 +18,12 @@ import java.lang.annotation.Target;
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
@Target({ElementType.TYPE, ElementType.FIELD})
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LinkRest {
String name();
String method();
String name() default "";
String method() default "";
Class linkClass();
boolean optional() default false;
}

View File

@@ -21,6 +21,10 @@ import org.dspace.app.rest.utils.Utils;
public class BrowseIndexResource extends DSpaceResource<BrowseIndexRest> {
public BrowseIndexResource(BrowseIndexRest bix, Utils utils, String... rels) {
super(bix, utils, rels);
// TODO: the following code will force the embedding of items and
// entries in the browseIndex we need to find a way to populate the rels
// array from the request/projection right now it is always null
// super(bix, utils, "items", "entries");
if (bix.isMetadataBrowse()) {
add(utils.linkToSubResource(bix, BrowseIndexRest.ENTRIES));
}

View File

@@ -7,18 +7,38 @@
*/
package org.dspace.app.rest.model.hateoas;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;
import org.atteo.evo.inflector.English;
import org.dspace.app.rest.model.BaseObjectRest;
import org.dspace.app.rest.model.LinkRest;
import org.dspace.app.rest.model.LinksRest;
import org.dspace.app.rest.model.RestModel;
import org.dspace.app.rest.repository.DSpaceRestRepository;
import org.dspace.app.rest.repository.LinkRestRepository;
import org.dspace.app.rest.utils.Utils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.rest.webmvc.EmbeddedResourcesAssembler;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.core.EmbeddedWrappers;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -47,18 +67,131 @@ public abstract class DSpaceResource<T extends RestModel> extends ResourceSuppor
if (data != null) {
try {
LinksRest links = data.getClass().getDeclaredAnnotation(LinksRest.class);
if (links != null && rels != null) {
List<String> relsList = Arrays.asList(rels);
for (LinkRest linkAnnotation : links.links()) {
if (!relsList.contains(linkAnnotation.name())) {
continue;
}
String name = linkAnnotation.name();
Link linkToSubResource = utils.linkToSubResource(data, name);
String apiCategory = data.getCategory();
String model = data.getType();
LinkRestRepository linkRepository = utils.getLinkResourceRepository(apiCategory, model, linkAnnotation.name());
if (!linkRepository.isEmbbeddableRelation(data, linkAnnotation.name())) {
continue;
}
try {
//RestModel linkClass = linkAnnotation.linkClass().newInstance();
Method[] methods = linkRepository.getClass().getMethods();
boolean found = false;
for (Method m : methods) {
if (StringUtils.equals(m.getName(), linkAnnotation.method())) {
// TODO add support for single linked object other than for collections
Page<? extends Serializable> pageResult = (Page<? extends RestModel>) m.invoke(linkRepository, null, ((BaseObjectRest) data).getId(), null, null);
EmbeddedPage ep = new EmbeddedPage(linkToSubResource.getHref(), pageResult, null);
embedded.put(name, ep);
found = true;
}
}
// TODO custom exception
if (!found) {
throw new RuntimeException("Method for relation " + linkAnnotation.name() + " not found: " + linkAnnotation.method());
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
for (PropertyDescriptor pd : Introspector.getBeanInfo(data.getClass()).getPropertyDescriptors()) {
Method readMethod = pd.getReadMethod();
if (readMethod != null && !"class".equals(pd.getName())) {
if (RestModel.class.isAssignableFrom(readMethod.getReturnType())) {
this.add(utils.linkToSubResource(data, pd.getName()));
String name = pd.getName();
if (readMethod != null && !"class".equals(name)) {
LinkRest linkAnnotation = readMethod.getAnnotation(LinkRest.class);
if (linkAnnotation != null) {
if (StringUtils.isNotBlank(linkAnnotation.name())) {
name = linkAnnotation.name();
}
Link linkToSubResource = utils.linkToSubResource(data, name);
// no method is specified to retrieve the linked object(s) so check if it is already here
if (StringUtils.isBlank(linkAnnotation.method())) {
this.add(linkToSubResource);
Object linkedObject = readMethod.invoke(data);
Object wrapObject = linkedObject;
if (linkedObject instanceof RestModel) {
RestModel linkedRM = (RestModel) linkedObject;
wrapObject = utils.getResourceRepository(linkedRM.getCategory(), linkedRM.getType())
.wrapResource(linkedRM);
}
else {
if (linkedObject instanceof List) {
List<RestModel> linkedRMList = (List<RestModel>) linkedObject;
if (linkedRMList.size() > 0) {
DSpaceRestRepository<RestModel, ?> resourceRepository = utils.getResourceRepository(linkedRMList.get(0).getCategory(), linkedRMList.get(0).getType());
// TODO should we force pagination also of embedded resource?
// This will force a pagination with size 10 for embedded collections as well
// int pageSize = 1;
// PageImpl<RestModel> page = new PageImpl(
// linkedRMList.subList(0,
// linkedRMList.size() > pageSize ? pageSize : linkedRMList.size()), new PageRequest(0, pageSize), linkedRMList.size());
PageImpl<RestModel> page = new PageImpl(linkedRMList);
wrapObject = new EmbeddedPage(linkToSubResource.getHref(), page.map(resourceRepository::wrapResource), linkedRMList);
}
else {
wrapObject = null;
}
}
}
if (linkedObject != null) {
embedded.put(name, wrapObject);
} else {
embedded.put(name, null);
}
Method writeMethod = pd.getWriteMethod();
writeMethod.invoke(data, new Object[] { null });
}
else {
// call the link repository
try {
//RestModel linkClass = linkAnnotation.linkClass().newInstance();
String apiCategory = data.getCategory();
String model = data.getType();
LinkRestRepository linkRepository = utils.getLinkResourceRepository(apiCategory, model, linkAnnotation.name());
Method[] methods = linkRepository.getClass().getMethods();
boolean found = false;
for (Method m : methods) {
if (StringUtils.equals(m.getName(), linkAnnotation.method())) {
// TODO add support for single linked object other than for collections
Page<? extends Serializable> pageResult = (Page<? extends RestModel>) m.invoke(linkRepository, null, ((BaseObjectRest) data).getId(), null, null);
EmbeddedPage ep = new EmbeddedPage(linkToSubResource.getHref(), pageResult, null);
embedded.put(name, ep);
found = true;
}
}
// TODO custom exception
if (!found) {
throw new RuntimeException("Method for relation " + linkAnnotation.name() + " not found: " + linkAnnotation.method());
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
else if (RestModel.class.isAssignableFrom(readMethod.getReturnType())) {
Link linkToSubResource = utils.linkToSubResource(data, name);
this.add(linkToSubResource);
RestModel linkedObject = (RestModel) readMethod.invoke(data);
if (linkedObject != null) {
embedded.put(pd.getName(),
embedded.put(name,
utils.getResourceRepository(linkedObject.getCategory(), linkedObject.getType())
.wrapResource(linkedObject));
} else {
embedded.put(pd.getName(), null);
embedded.put(name, null);
}
Method writeMethod = pd.getWriteMethod();
@@ -74,6 +207,11 @@ public abstract class DSpaceResource<T extends RestModel> extends ResourceSuppor
}
}
@Override
public void add(Link link) {
System.out.println("Chiamato "+link.getRel());
super.add(link);
}
public Map<String, Object> getEmbedded() {
return embedded;
}

View File

@@ -0,0 +1,78 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.hateoas;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.data.domain.Page;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
public class EmbeddedPage {
private Page page;
private List fullList;
private UriComponentsBuilder self;
public EmbeddedPage(String self, Page page, List fullList) {
this.page = page;
this.fullList = fullList;
this.self = UriComponentsBuilder.fromUriString(self);
}
@JsonProperty(value = "_embedded")
public List getPageContent() {
return page.getContent();
}
@JsonProperty(value = "page")
public Map<String, Long> getPageInfo() {
Map<String, Long> pageInfo = new HashMap<String, Long>();
pageInfo.put("number", (long) page.getNumber());
pageInfo.put("size", (long) page.getSize() != 0?page.getSize():page.getTotalElements());
pageInfo.put("totalPages", (long) page.getTotalPages());
pageInfo.put("totalElements", page.getTotalElements());
return pageInfo;
}
@JsonProperty(value = "_links")
public Map<String, String> getLinks() {
Map<String, String> links = new HashMap<String, String>();
if (!page.isFirst()) {
links.put("first", _link(0));
links.put("self", _link(page.getNumber()));
}
else {
links.put("self", self.toUriString());
}
if (!page.isLast()) {
links.put("last", _link(page.getTotalPages()-1));
}
if (page.hasPrevious()) {
links.put("prev", _link(page.getNumber()-1));
}
if (page.hasNext()) {
links.put("next", _link(page.getNumber()+1));
}
return links;
}
private String _link(int i) {
UriComponentsBuilder uriComp = self.cloneBuilder();
return uriComp.queryParam("page", i).build().toString();
}
@JsonIgnore
public List getFullList() {
return fullList;
}
}

View File

@@ -65,7 +65,10 @@ public class BrowseEntryLinkRepository extends AbstractDSpaceRestRepository
Pageable pageable, String projection) throws BrowseException, SQLException {
// FIXME this should be bind automatically and available as method
// argument
String scope = request.getParameter("scope");
String scope = null;
if (request != null) {
request.getParameter("scope");
}
Context context = obtainContext();
BrowseEngine be = new BrowseEngine(context);
@@ -95,7 +98,10 @@ public class BrowseEntryLinkRepository extends AbstractDSpaceRestRepository
// set up a BrowseScope and start loading the values into it
bs.setBrowseIndex(bi);
Sort sort = pageable.getSort();
Sort sort = null;
if (pageable != null) {
sort = pageable.getSort();
}
if (sort != null) {
Iterator<Order> orders = sort.iterator();
while (orders.hasNext()) {
@@ -108,8 +114,10 @@ public class BrowseEntryLinkRepository extends AbstractDSpaceRestRepository
// bs.setJumpToValue(valueFocus);
// bs.setJumpToValueLang(valueFocusLang);
// bs.setStartsWith(startsWith);
bs.setOffset(pageable.getOffset());
bs.setResultsPerPage(pageable.getPageSize());
if (pageable != null) {
bs.setOffset(pageable.getOffset());
bs.setResultsPerPage(pageable.getPageSize());
}
// bs.setEtAl(etAl);
// bs.setAuthorityValue(authority);
@@ -119,7 +127,7 @@ public class BrowseEntryLinkRepository extends AbstractDSpaceRestRepository
BrowseInfo binfo = be.browse(bs);
Pageable pageResultInfo = new PageRequest((binfo.getStart() - 1) / binfo.getResultsPerPage(),
pageable.getPageSize());
binfo.getResultsPerPage());
Page<BrowseEntryRest> page = new PageImpl<String[]>(Arrays.asList(binfo.getStringResults()), pageResultInfo,
binfo.getTotal()).map(converter);
page.forEach(new Consumer<BrowseEntryRest>() {
@@ -135,4 +143,13 @@ public class BrowseEntryLinkRepository extends AbstractDSpaceRestRepository
public BrowseEntryResource wrapResource(BrowseEntryRest entry, String... rels) {
return new BrowseEntryResource(entry);
}
@Override
public boolean isEmbbeddableRelation(Object data, String name) {
BrowseIndexRest bir = (BrowseIndexRest) data;
if (bir.isMetadataBrowse() && "entries".equals(name)) {
return true;
}
return false;
}
}

View File

@@ -63,10 +63,15 @@ public class BrowseItemLinkRepository extends AbstractDSpaceRestRepository
public Page<ItemRest> listBrowseItems(HttpServletRequest request, String browseName, Pageable pageable, String projection)
throws BrowseException, SQLException {
//FIXME these should be bind automatically and available as method arguments
String scope = request.getParameter("scope");
String filterValue = request.getParameter("filterValue");
String filterAuthority = request.getParameter("filterAuthority");
String scope = null;
String filterValue = null;
String filterAuthority = null;
if (request != null) {
scope = request.getParameter("scope");
filterValue = request.getParameter("filterValue");
filterAuthority = request.getParameter("filterAuthority");
}
Context context = obtainContext();
BrowseEngine be = new BrowseEngine(context);
BrowserScope bs = new BrowserScope(context);
@@ -97,7 +102,10 @@ public class BrowseItemLinkRepository extends AbstractDSpaceRestRepository
// set up a BrowseScope and start loading the values into it
bs.setBrowseIndex(bi);
Sort sort = pageable.getSort();
Sort sort = null;
if (pageable != null) {
sort = pageable.getSort();
}
if (sort != null) {
Iterator<Order> orders = sort.iterator();
while (orders.hasNext()) {
@@ -132,9 +140,11 @@ public class BrowseItemLinkRepository extends AbstractDSpaceRestRepository
// bs.setJumpToValue(valueFocus);
// bs.setJumpToValueLang(valueFocusLang);
// bs.setStartsWith(startsWith);
bs.setOffset(pageable.getOffset());
bs.setResultsPerPage(pageable.getPageSize());
if (pageable != null) {
bs.setOffset(pageable.getOffset());
bs.setResultsPerPage(pageable.getPageSize());
}
if (scopeObj != null) {
bs.setBrowseContainer(scopeObj);
}
@@ -147,7 +157,7 @@ public class BrowseItemLinkRepository extends AbstractDSpaceRestRepository
BrowseInfo binfo = be.browse(bs);
Pageable pageResultInfo = new PageRequest((binfo.getStart() -1) / binfo.getResultsPerPage(), pageable.getPageSize());
Pageable pageResultInfo = new PageRequest((binfo.getStart() -1) / binfo.getResultsPerPage(), binfo.getResultsPerPage());
Page<ItemRest> page = new PageImpl<Item>(binfo.getBrowseItemResults(), pageResultInfo, binfo.getTotal())
.map(converter);
return page;
@@ -157,4 +167,13 @@ public class BrowseItemLinkRepository extends AbstractDSpaceRestRepository
public ItemResource wrapResource(ItemRest item, String... rels) {
return itemRestRepository.wrapResource(item, rels);
}
@Override
public boolean isEmbbeddableRelation(Object data, String name) {
BrowseIndexRest bir = (BrowseIndexRest) data;
if (!bir.isMetadataBrowse() && "items".equals(name)) {
return true;
}
return false;
}
}

View File

@@ -19,4 +19,8 @@ import org.springframework.hateoas.ResourceSupport;
*/
public interface LinkRestRepository<L extends Serializable> {
public abstract ResourceSupport wrapResource(L model, String... rels);
public default boolean isEmbbeddableRelation(Object data, String name) {
return true;
}
}