如何用 HATEOAS 链接替换子实体?
How do you replace sub entities with HATEOAS links?
我正在尝试弄清楚如何在我的控制器中处理一系列结果,但仍然 return 内联子资源的链接。
当我点击暴露的存储库端点时,我得到如下所示的响应:
{
"_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
}
}
然而,当我访问实体的缓存版本时,我得到以下内容
{
"_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
}
}
知道我怎样才能 return 看起来像前者的东西吗?
以下是我准备控制器响应的方式:
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));
}
我最终不得不构建自己的注释并将其与@JsonProperty(access=Access.WRITE_ONLY) 结合使用,并且我构建了一个基本的基于内省的处理器。
这不是特别优雅,也不完整,但它适用于我当前的用例,这些用例涉及所有具有 UUID id 的子实体,并且大多数具有派生的 getter 显示标签:
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;
}
}
我正在尝试弄清楚如何在我的控制器中处理一系列结果,但仍然 return 内联子资源的链接。
当我点击暴露的存储库端点时,我得到如下所示的响应:
{
"_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
}
}
然而,当我访问实体的缓存版本时,我得到以下内容
{
"_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
}
}
知道我怎样才能 return 看起来像前者的东西吗?
以下是我准备控制器响应的方式:
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));
}
我最终不得不构建自己的注释并将其与@JsonProperty(access=Access.WRITE_ONLY) 结合使用,并且我构建了一个基本的基于内省的处理器。
这不是特别优雅,也不完整,但它适用于我当前的用例,这些用例涉及所有具有 UUID id 的子实体,并且大多数具有派生的 getter 显示标签:
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;
}
}