I am trying to figure out how to process a series of results in my controller, yet still return links for inline sub resources.
when I hit the exposed repository endpoint, I get responses that look like the following:
{ "_embedded": { "resources": [ { "id": "b10a978a-8304-4849-9bc5-c33e4082a9f3", "creationTimestamp": null, "createdBy": null, "lastEntityUpdate": 1366212764045, "lastModifiedBy": null, "archive": true, "contractStatus": "OFF", "callsign": "108", "registration": "C-GRIK", "username": null, "latitude": 47.011356, "longitude": -65.438644, "esn": "70001108", "imageURL": null, "description": "AFF", "phoneNumber": null, "speed": 14.0, "heading": 22.0, "altitude": 104.98560333252, "trackingPositionTimestamp": 1307552845000, "positionTimestamp": 1307552845000, "positionSource": "LMC-SBD", "userPosition": false, "hourlyRate": null, "dailyRate": null, "alertSchedule": null, "note": null, "checkinTime": null, "defaultCheckinInterval": null, "capacity": 0.0, "tracking": null, "lostContact": null, "currentContractId": null, "hireable": false, "supplierNumber": null, "siteNumber": null, "postalCode": null, "homePhoneNumber": null, "cellPhoneNumber": null, "hemanAgreementNumber": null, "displayLabel": "C-GRIK", "_links": { "self": { "href": "http://localhost:8080/resource-api/v1/resources/b10a978a-8304-4849-9bc5-c33e4082a9f3" }, "selkirkResource": { "href": "http://localhost:8080/resource-api/v1/resources/b10a978a-8304-4849-9bc5-c33e4082a9f3" }, "homeOrg": { "href": "https://example.com/dispatch/rest/organizations/2ef519bc-3353-4d6d-b935-a756d39e5b9f" }, "carrier": { "href": "http://localhost:8080/resource-api/v1/carriers/9c78a84d-e2dd-48b6-a328-aa78549a91d2", "title": "Conair Group Inc." }, "alertStatus": { "href": "http://localhost:8080/resource-api/v1/alertStatuses/6b44353a-c650-43a0-aaad-299c798c85f2", "title": "GREEN" }, "resourceStatus": { "href": "http://localhost:8080/resource-api/v1/resourceStatuses/e4b41c84-e5b8-4b0f-adf1-684980d0ac98", "title": "OFF DUTY" }, "type": { "href": "http://localhost:8080/resource-api/v1/types/378b3ad1-d2e0-4975-9186-df81b4a362b3", "title": "Birddog" } } }, { "id": "65122dc0-0121-4b73-8f9c-4f1002da3243", "creationTimestamp": null, "createdBy": null, "lastEntityUpdate": 1366992331928, "lastModifiedBy": null, "archive": true, "contractStatus": "OFF", "callsign": "Noltcho,Hart", "registration": "Noltcho,Hart", "username": null, "latitude": 48.0, "longitude": -148.0, "esn": null, "imageURL": null, "description": null, "phoneNumber": null, "speed": 0.0, "heading": 0.0, "altitude": 0.0, "trackingPositionTimestamp": null, "positionTimestamp": 1366131487406, "positionSource": "User", "userPosition": true, "hourlyRate": null, "dailyRate": null, "alertSchedule": null, "note": null, "checkinTime": null, "defaultCheckinInterval": null, "capacity": 0.0, "tracking": null, "lostContact": null, "currentContractId": null, "hireable": false, "supplierNumber": null, "siteNumber": null, "postalCode": null, "homePhoneNumber": null, "cellPhoneNumber": null, "hemanAgreementNumber": null, "displayLabel": "Noltcho,Hart", "_links": { "self": { "href": "http://localhost:8080/resource-api/v1/resources/65122dc0-0121-4b73-8f9c-4f1002da3243" }, "selkirkResource": { "href": "http://localhost:8080/resource-api/v1/resources/65122dc0-0121-4b73-8f9c-4f1002da3243" }, "homeOrg": { "href": "https://example.com/dispatch/rest/organizations/afbccc45-6ed7-40b5-ab99-1e9a1f71996f" }, "resourceStatus": { "href": "http://localhost:8080/resource-api/v1/resourceStatuses/e4b41c84-e5b8-4b0f-adf1-684980d0ac98", "title": "OFF DUTY" }, "type": { "href": "http://localhost:8080/resource-api/v1/types/0c43e4bf-eae7-405c-a0d9-7bddd378cb10", "title": "Type 1 Member" } } } ] }, "_links": { "first": { "href": "http://localhost:8080/resource-api/v1/resources?page=0&size=2&sort=lastEntityUpdate,asc" }, "self": { "href": "http://localhost:8080/resource-api/v1/resources?size=2&sort=lastEntityUpdate,asc" }, "next": { "href": "http://localhost:8080/resource-api/v1/resources?page=1&size=2&sort=lastEntityUpdate,asc" }, "last": { "href": "http://localhost:8080/resource-api/v1/resources?page=2788&size=2&sort=lastEntityUpdate,asc" }, "profile": { "href": "http://localhost:8080/resource-api/v1/profile/resources" }, "search": { "href": "http://localhost:8080/resource-api/v1/resources/search" } }, "page": { "size": 2, "totalElements": 5578, "totalPages": 2789, "number": 0 } }
however when I hit my cached version of the entity, I get the following
{ "_embedded": { "resources": [ { "id": "b10a978a-8304-4849-9bc5-c33e4082a9f3", "creationTimestamp": null, "createdBy": null, "lastEntityUpdate": 1366212764045, "lastModifiedBy": null, "archive": true, "type": { "id": "378b3ad1-d2e0-4975-9186-df81b4a362b3", "displayLabel": "Birddog", "lastEntityUpdate": 1366212758677, "lastModifiedBy": null, "archive": false, "parent": { "id": "4dd05765-9fe6-441a-b2bc-d3199e3d69e0", "displayLabel": "Fixed Wing", "lastEntityUpdate": 1366212794520, "lastModifiedBy": null, "archive": false, "parent": { "id": "fe90eedb-9ae4-422c-84aa-ac55a0900fbf", "displayLabel": "Aircraft", "lastEntityUpdate": 1366212749915, "lastModifiedBy": null, "archive": false, "parent": null, "pathLabel": "Aircraft", "nodeDepth": 0, "defaultCheckinInterval": 1800000, "iconURL": null, "hourlyRate": 0.0, "dailyRate": null, "reportSituation": false }, "pathLabel": "Aircraft:Fixed Wing", "nodeDepth": 1, "defaultCheckinInterval": 1800000, "iconURL": null, "hourlyRate": 0.0, "dailyRate": null, "reportSituation": false }, "pathLabel": "Aircraft:Fixed Wing:Birddog", "nodeDepth": 2, "defaultCheckinInterval": 1800000, "iconURL": null, "hourlyRate": 0.0, "dailyRate": null, "reportSituation": true }, "makeModel": null, "alertStatus": { "id": "6b44353a-c650-43a0-aaad-299c798c85f2", "displayLabel": "GREEN", "lastEntityUpdate": 1300921860738, "lastModifiedBy": null, "archive": false, "colorCode": null, "sortIndex": null }, "carrier": { "id": "9c78a84d-e2dd-48b6-a328-aa78549a91d2", "displayLabel": "Conair Group Inc.", "lastEntityUpdate": 1446148848536, "lastModifiedBy": null, "archive": false }, "resourceStatus": { "id": "e4b41c84-e5b8-4b0f-adf1-684980d0ac98", "displayLabel": "OFF DUTY", "lastEntityUpdate": 1300832420067, "lastModifiedBy": null, "archive": false, "clearCheckin": true }, "contractStatus": "OFF", "callsign": "108", "registration": "C-GRIK", "username": null, "latitude": 47.011356, "longitude": -65.438644, "esn": "70001108", "imageURL": null, "description": "AFF", "phoneNumber": null, "speed": 14.0, "heading": 22.0, "altitude": 104.98560333252, "trackingPositionTimestamp": 1307552845000, "positionTimestamp": 1307552845000, "positionSource": "LMC-SBD", "userPosition": false, "hourlyRate": null, "dailyRate": null, "alertSchedule": null, "note": null, "checkinTime": null, "defaultCheckinInterval": null, "capacity": 0.0, "tracking": null, "lostContact": null, "currentContractId": null, "hireable": false, "supplierNumber": null, "siteNumber": null, "postalCode": null, "homePhoneNumber": null, "cellPhoneNumber": null, "hemanAgreementNumber": null, "displayLabel": "C-GRIK", "_links": { "self": { "href": "http://localhost:8080/resource-api/v1/resources/b10a978a-8304-4849-9bc5-c33e4082a9f3" }, "homeOrg": { "href": "https://example.com/dispatch/rest/organizations/2ef519bc-3353-4d6d-b935-a756d39e5b9f" } } }, { "id": "65122dc0-0121-4b73-8f9c-4f1002da3243", "creationTimestamp": null, "createdBy": null, "lastEntityUpdate": 1366992331928, "lastModifiedBy": null, "archive": true, "type": { "id": "0c43e4bf-eae7-405c-a0d9-7bddd378cb10", "displayLabel": "Type 1 Member", "lastEntityUpdate": 1366992330601, "lastModifiedBy": null, "archive": false, "parent": { "id": "96c7d47c-e783-4a8b-9fe3-3eed6a14cb66", "displayLabel": "Crew", "lastEntityUpdate": 1351104068068, "lastModifiedBy": null, "archive": false, "parent": null, "pathLabel": "Crew", "nodeDepth": 0, "defaultCheckinInterval": 0, "iconURL": "/crew.resource.16.png", "hourlyRate": 0.0, "dailyRate": null, "reportSituation": false }, "pathLabel": "Crew:Type 1 Member", "nodeDepth": 1, "defaultCheckinInterval": 0, "iconURL": "/crew.resource.16.png", "hourlyRate": 0.0, "dailyRate": null, "reportSituation": true }, "makeModel": null, "alertStatus": null, "carrier": null, "resourceStatus": { "id": "e4b41c84-e5b8-4b0f-adf1-684980d0ac98", "displayLabel": "OFF DUTY", "lastEntityUpdate": 1300832420067, "lastModifiedBy": null, "archive": false, "clearCheckin": true }, "contractStatus": "OFF", "callsign": "Noltcho,Hart", "registration": "Noltcho,Hart", "username": null, "latitude": 48.0, "longitude": -148.0, "esn": null, "imageURL": null, "description": null, "phoneNumber": null, "speed": 0.0, "heading": 0.0, "altitude": 0.0, "trackingPositionTimestamp": null, "positionTimestamp": 1366131487406, "positionSource": "User", "userPosition": true, "hourlyRate": null, "dailyRate": null, "alertSchedule": null, "note": null, "checkinTime": null, "defaultCheckinInterval": null, "capacity": 0.0, "tracking": null, "lostContact": null, "currentContractId": null, "hireable": false, "supplierNumber": null, "siteNumber": null, "postalCode": null, "homePhoneNumber": null, "cellPhoneNumber": null, "hemanAgreementNumber": null, "displayLabel": "Noltcho,Hart", "_links": { "self": { "href": "http://localhost:8080/resource-api/v1/resources/65122dc0-0121-4b73-8f9c-4f1002da3243" }, "homeOrg": { "href": "https://example.com/dispatch/rest/organizations/afbccc45-6ed7-40b5-ab99-1e9a1f71996f" } } } ] }, "_links": { "first": { "href": "http://localhost:8080/resource-api/v1/resources/getAllCached?page=0&size=2" }, "self": { "href": "http://localhost:8080/resource-api/v1/resources/getAllCached" }, "next": { "href": "http://localhost:8080/resource-api/v1/resources/getAllCached?page=1&size=2" }, "last": { "href": "http://localhost:8080/resource-api/v1/resources/getAllCached?page=2788&size=2" } }, "page": { "size": 2, "totalElements": 5578, "totalPages": 2789, "number": 0 } }
any idea how I can return something that looks like the former?
below is how I a preparing my controller’s response:
private ResponseEntity<PagedModel<CollectionModel<MyEntity>>> buildResponse(List<MyEntity> filteredList, Pageable pageable, Link link){ filteredList.sort(new PageableComparator<>(pageable)); int size = pageable.getPageSize(); int offset = pageable.getPageNumber() * size; int end = Math.min((offset + size), filteredList.size()); List<MyEntity> pageElements = filteredList.subList(offset, end); Page page = new PageImpl(pageElements, pageable, filteredCache.size()); if (pageElements.size() == 0) { throw new GeneralControllerException("No resources match your query", HttpStatus.NOT_FOUND); } return ResponseEntity.ok(pagedResourcesAssembler.toModel(page,myEntityRepresentationModelAssembler,link)); }
Advertisement
Answer
I ended up having to build my own annotation and use it in conjunction with @JsonProperty(access=Access.WRITE_ONLY) And I built a rudimentary introspection-based processor.
This is not particularly elegant, nor is it complete, but it works for my current use cases which involve all subentities having a UUID id and most having a derived getter for displayLabel:
import com.selkirksystems.hateoas.annotation.RenderAsInternalMicroserviceHateoasLink; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.data.rest.webmvc.PersistentEntityResource; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; import org.springframework.hateoas.server.EntityLinks; import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.http.HttpStatus; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; public class InternalEntityLinksResourceProcessor<T> implements RepresentationModelProcessor<EntityModel<T>> { private final EntityLinks entityLinks; private static final ConcurrentHashMap<Class<?>, ConcurrentHashMap<String, Method>> allMethods = new ConcurrentHashMap<>(); private final Log logger = LogFactory.getLog(InternalEntityLinksResourceProcessor.class); public InternalEntityLinksResourceProcessor(EntityLinks entityLinks) { this.entityLinks = entityLinks; } private void addInternalEntityLinks(EntityModel<T> resource) throws Exception { try { T resourceContent = resource.getContent(); for (Field field : Objects.requireNonNull(resourceContent).getClass().getDeclaredFields()) { field.setAccessible(true); RenderAsInternalMicroserviceHateoasLink annotation = field.getAnnotation(RenderAsInternalMicroserviceHateoasLink.class); if (annotation != null) { addLinkForField(field, resource); } } } catch (Exception ex) { throw new Exception("Error while adding external microservice references", ex); } } private void addLinkForField(Field field, EntityModel<T> resource) throws Exception { if (!(resource instanceof PersistentEntityResource)) { //dirty hack #27, prevent double adding links Object o = field.get(resource.getContent()); if (o != null) { UUID uuid = (UUID) getValue(o, "id"); if (uuid == null) { throw new Exception(field.getType().getName() + " Does not have a UUID field named id. It does not support the RenderAsInternalMicroserviceHateoasLink annotation"); } Link link = entityLinks.linkToItemResource(field.getType(), uuid); String title = (String) getValue(o, "displayLabel"); if (title != null) { link = link.withTitle(title); } resource.add(link); } } } private Object getValue(Object o, String property) throws IntrospectionException, InvocationTargetException, IllegalAccessException { Method m = getGetter(o, property); if (m == null) return null; return m.invoke(o); } private Method getGetter(Object o, String property) throws IntrospectionException { ConcurrentHashMap<String, Method> methods = allMethods.computeIfAbsent(o.getClass(), k -> new ConcurrentHashMap<>()); Method m = methods.get(property.toUpperCase()); if (m != null) { return m; } PropertyDescriptor[] pds = Introspector.getBeanInfo(o.getClass(), Object.class).getPropertyDescriptors(); for (PropertyDescriptor pd : pds) { if (pd.getName().equalsIgnoreCase(property.toUpperCase())) { m =pd.getReadMethod(); } methods.put(pd.getName().toUpperCase(), pd.getReadMethod()); } if (m == null) { throw new IntrospectionException("Property " + property + " does not exist"); } return m; } @Override public EntityModel<T> process(EntityModel<T> model) { try { addInternalEntityLinks(model); } catch (Exception ex) { logger.error(ex, ex); throw new MyHateoasException("Error converting sub-entities to links: " + ex.getMessage(), HttpStatus.BAD_REQUEST); } return model; } }