I´m using Spring Boot and HATEOAS to build a REST API and I am struggling with the curie creation. The Spring HATEOAS guide says that in order to automatically insert a curie in the responses, you should do the following:
#Configuration
#EnableWebMvc
#EnableHypermediaSupport(type= {HypermediaType.HAL})
public class Config {
#Bean
public CurieProvider curieProvider() {
return new DefaultCurieProvider("ex", new UriTemplate("http://www.example.com{#rel}"));
}
}
My config class is like this:
#SpringBootApplication
public class ApiApplication {
public static void main(String[] args) {
SpringApplication.run(ApiApplication.class, args);
}
#Bean
public CurieProvider curieProvider() {
return new DefaultCurieProvider("xpto", new UriTemplate("http://www.xpto.com{#rel}"));
}
}
I tried to add the #EnableWebMvc to my config class but it changes the rendering of the response (hal) and the curie doesn´t appear. I have to do something on the controller to create the curie?
Update:
I updated the Spring Hateoas (to 0.17.0.RELEASE) and now my collection name is rendered with the curie but the curie does not appear in the _links section:
{
"_links": {
"self": {
"href": "http://localhost:8080/technologies"
}
},
"_embedded": {
"mycurie:technology": [
{
"id": 1,
"description": "A",
"_links": {
"self": {
"href": "http://localhost:8080/technologies/1"
}
}
},
{
"id": 2,
"description": "B",
"_links": {
"self": {
"href": "http://localhost:8080/technologies/2"
}
}
}
]
}
}
If I add one link to the _links section then the curie link appears:
{
"_links": {
"self": {
"href": "http://localhost:8080/technologies"
},
"mycurie:xpto": {
"href": "http://localhost:8080/xpto"
},
"curies": [
{
"href": "http://localhost:8080/rels/{rel}",
"name": "mycurie",
"templated": true
}
]
},
"_embedded": {
"mycurie:technology": [
{
"id": 1,
"description": "A",
"_links": {
"self": {
"href": "http://localhost:8080/technologies/1"
}
}
},
{
"id": 2,
"description": "B",
"_links": {
"self": {
"href": "http://localhost:8080/technologies/2"
}
}
}
]
}
}
This is my controller:
#RestController
#ExposesResourceFor(Technology.class)
#RequestMapping(value = "/technologies")
public class TechnologyRestController {
...
#RequestMapping(method = RequestMethod.GET, produces = "application/vnd.xpto-technologies.text+json")
public Resources<TechnologyResource> getAllTechnologies() {
List<Technology> technologies = technologyGateway.getAllTechnologies();
Resources<TechnologyResource> technologiesResources = new Resources<TechnologyResource>(technologyResourceAssembler.toResources(technologies));
technologiesResources.add(linkTo(methodOn(TechnologyRestController.class).getAllTechnologies()).withSelfRel());
return technologiesResources;
}
}
Your config class looks correct to me, however Spring Boot's HypermediaAutoConfiguration requires org.springframework.plugin:spring-plugin-core, an optional dependency of Spring HATEOAS, to be on the classpath for it to be enabled. I'd guess that you're missing this dependency. Try adding a dependency on org.springframework.plugin:spring-plugin-core:1.1.0.RELEASE.
Related
I am generating a JavaClient using an OpenAPISpec document. I have used swagger-codegen 3.0 to generate the code. The OpenAPISpec version is 3.0.1.
Below is the OpenAPI snippet I am facing problems with:
"RequestWithInsuranceInfo": {
"type": "object",
"description": "This request schema will produce a response containing an out of pocket estimate for the given service using the patient's insurance information.",
"additionalProperties": false,
"properties": {
"insuranceInfo": {
"$ref": "#/components/schemas/InsuranceInfo"
},
"service": {
"type": "object",
"additionalProperties": false,
"description": "Schema to use when the patient's benefit info is not given in the request.",
"properties": {
"codes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ServiceCode"
}
},
"provider": {
"$ref": "#/components/schemas/Provider"
},
"costs": {
"$ref": "#/components/schemas/ServiceCosts"
}
},
"required": [
"codes",
"provider",
"costs"
]
}
}
},
"InsuranceInfo": {
"description": "Information about the payer, plan, and members.",
"additionalProperties": false,
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"title": "Option 1: Patient Is Policy Holder",
"description": "Schema to use when the patient the primary on the insurance plan.",
"properties": {
"payer": {
"$ref": "#/components/schemas/Payer"
},
"policyHolderInfo": {
"$ref": "#/components/schemas/PolicyHolderInfo"
}
},
"required": [
"payer",
"policyHolderInfo"
]
},
{
"type": "object",
"additionalProperties": false,
"title": "Option 2: Patient Is Dependent",
"description": "Schema to use when the patient is a dependent on the insurance plan.",
"properties": {
"payer": {
"$ref": "#/components/schemas/Payer"
},
"dependentMemberInfo": {
"$ref": "#/components/schemas/DependentMemberInfo"
},
"policyHolderInfo": {
"$ref": "#/components/schemas/PolicyHolderInfo"
}
},
"required": [
"payer",
"dependentMemberInfo",
"policyHolderInfo"
]
}
]
},
Below is the code which gets generated:
public class InsuranceInfo implements OneOfInsuranceInfo {
#Override
public boolean equals(java.lang.Object o) {..}
#Override
public int hashCode() {..}
#Override
public String toString() {..}
private String toIndentedString(java.lang.Object o) {..}
}
public interface OneOfInsuranceInfo {
}
public class RequestWithInsuranceInfo implements OneOfRequest {
#SerializedName("insuranceInfo")
private InsuranceInfo insuranceInfo = null;
#SerializedName("service")
private RequestWithInsuranceInfoService service = null;
..
}
public class Payer {
#SerializedName("id")
private String id = null;
..
}
public class PolicyHolderInfo {
#SerializedName("memberId")
private String memberId = null;
#SerializedName("firstName")
private String firstName = null;
#SerializedName("lastName")
private String lastName = null;
#SerializedName("dateOfBirth")
private LocalDate dateOfBirth = null;
..
}
public class DependentMemberInfo {
#SerializedName("memberId")
private String memberId = null;
#SerializedName("firstName")
private String firstName = null;
#SerializedName("lastName")
private String lastName = null;
#SerializedName("dateOfBirth")
private LocalDate dateOfBirth = null;
..
}
As shown, the InsuranceInfo object implements the OneOfInsuranceInfo interface but has no variables. Payer, PolicyHolderInfo and dependentMemberInfo class are generated but they are not linked to the InsuranceInfo class anyhow. How do I populate the InsuranceInfo class?
The issue is probably that the InsuranceInfo schema
"InsuranceInfo": {
"description": "Information about the payer, plan, and members.",
"additionalProperties": false,
"oneOf": [
{ ... },
{ ... }
]
}
effectively disallows ALL properties. This is because additionalProperties: false only knows about the properties defined directly alongside it and has no visibility into oneOf subschemas.
To resolve the issue, you can rewrite the InsuranceInfo schema without oneOf, as follows. This schema is basically "Option 2" from the original schema, except the dependentMemberInfo property is defined as optional.
"InsuranceInfo": {
"description": "Information about the payer, plan, and members.",
"additionalProperties": false,
"type": "object",
"required": [
"payer",
"policyHolderInfo"
],
"properties": {
"payer": {
"$ref": "#/components/schemas/Payer"
},
"dependentMemberInfo": {
"$ref": "#/components/schemas/DependentMemberInfo"
},
"policyHolderInfo": {
"$ref": "#/components/schemas/PolicyHolderInfo"
}
}
}
I have the following ResponseHandler to override a Data Rest Response Handlers. Instead of returning all data, the handler returns only data for the owner (logged in user- currently hardcoded).
package com.osde.prepo.controller;
import com.osde.prepo.entity.Company;
import com.osde.prepo.repository.CompanyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.hateoas.CollectionModel;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.List;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
/**
* customer controller to override some Data Rest Response Handlers
*/
#RepositoryRestController
public class CompanyController {
Logger logger = LoggerFactory.getLogger(CompanyController.class);
private final CompanyRepository companyRepository;
#Autowired
public CompanyController(CompanyRepository repo) {
companyRepository = repo;
}
#RequestMapping(method = RequestMethod.GET, value = "/companies")
public #ResponseBody
ResponseEntity<?> getAllCompaniesForCurrentUser() {
logger.info("custom implementation for get called!!");
List<Company> companies = new ArrayList<>();
companies = companyRepository.findByOwnerId("google-oauth2|107634743108791790006");
// convert to HATEOAS
CollectionModel<Company> resources = CollectionModel.of(companies);
resources.add(linkTo(methodOn(CompanyController.class)
.getAllCompaniesForCurrentUser())
.withSelfRel());
return ResponseEntity.ok(resources);
}
}
I get the following response, when consuming the API:
{
"_embedded": {
"companies": [
{
"ownerId": "google-oauth2|107634743108791790006",
"name": "Company 1",
"city": "Ort 1",
"country": "Germany",
"profile": "We are 1 ...",
"logoUrl": null
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/companies"
}
}
}
The original API has the following response (including self-links to the items) How do I have to change my code to get this response (paging is not of interest so far!)?
{
"_embedded": {
"companies": [
{
"ownerId": "google-oauth2|107634743108791790006",
"name": "Company 1",
"city": "Ort 1",
"country": "Germany",
"profile": "We are 1 ...",
"logoUrl": null,
"_links": {
"self": {
"href": "http://localhost:8080/api/companies/1"
},
"company": {
"href": "http://localhost:8080/api/companies/1"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/api/companies/"
},
"profile": {
"href": "http://localhost:8080/api/profile/companies"
},
"search": {
"href": "http://localhost:8080/api/companies/search"
}
},
"page": {
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
}
}
Here is a sample code to achieve this. I have assumed that your Entity has a getter for id as getId().It can be changed as per the code.
There is a need to extend this RepresentationModel on the entity class to get the add method.
eg
public class Company extends RepresentationModel<Company> implements Serializable {
#RequestMapping(method = RequestMethod.GET, value = "/companies")
public #ResponseBody
ResponseEntity<?> getAllCompaniesForCurrentUser() {
logger.info("custom implementation for get called!!");
List<Company> companies = companyRepository.findByOwnerId("google-oauth2|107634743108791790006");
for (Company company : companies) {
Link selfLink = linkTo(methodOn(CompanyController.class)
.getCompaniesById(company.getId())).withSelfRel();
company.add(selfLink);
}
Link link = linkTo(methodOn(CompanyController.class).getAllCompaniesForCurrentUser()).withSelfRel();
CollectionModel<Company> result = CollectionModel.of(companies, link);
return ResponseEntity.ok().body(companies);
}
There is an another way to do this as well. This won't require any change on the Entity class.
import org.springframework.hateoas.EntityModel;
#RequestMapping(method = RequestMethod.GET, value = "/companies")
public #ResponseBody
ResponseEntity<?> getAllCompaniesForCurrentUser() {
logger.info("custom implementation for get called!!");
List<EntityModel<Company>> companies = companyRepository
.findByOwnerId("google-oauth2|107634743108791790006")
.stream()
.map(this::generateLinks)
.collect(Collectors.toList());
CollectionModel<EntityModel<Company>> resource = CollectionModel.of(companies);
resource.add(entityLinks.linkToCollectionResource(Company.class));
resource.add(entityLinks.linksToSearchResources(Company.class));
return ResponseEntity.ok().body(resource);
}
private EntityModel<Company> generateLinks(Company company) {
EntityModel<Company> resource = EntityModel.of(company);
resource.add(entityLinks.linkToItemResource(Company.class, company.getId()).withSelfRel());
resource.add(entityLinks.linkToCollectionResource(Company.class));
resource.add(entityLinks.linksToSearchResources(Company.class));
return resource;
}
That points to the correct direction!
Now I get the following output:
[
{
"companyId": 1,
"ownerId": "google-oauth2|107634743108791790006",
"name": "Company 1",
"city": "Ort 1",
"country": "Germany",
"profile": "We are 1 ...",
"logoUrl": null,
"links": [
{
"rel": "self",
"href": "http://localhost:8080/api/companies/1"
},
{
"rel": "companies",
"href": "http://localhost:8080/api/companies{?page,size,sort}"
}
]
}
]
How to adjust the following aspects:
link to companies should be on the collection-level and just be http://localhost:8080/api/companies
rename links to _links
I am working on trying to correctly document our rest endpoints. As an example to get this working I created a sample "Healthcheck getStatus()" endpoint which is returning an object called "EndpointStatus" which has 3 fields (class is below). I was able to get this object documenting correctly and using the camel-swagger-java component and the below rest configuration / definition;
restConfiguration()
.apiContextPath(apiContextPath)
.apiProperty("api.title", "Camel Service").apiProperty("api.version", "1.0.0")
// and enable CORS
.apiProperty("cors", "true");
rest()
.path("/healthcheck")
.description("Health Check REST service")
.get("getStatus/{endpointName}")
.param()
.name("endpointName")
.type(RestParamType.path)
.allowableValues(
Stream.of(EndpointName.values())
.map(EndpointName::name)
.collect(Collectors.toList()))
.required(true)
.endParam()
.description("Get Camel Status")
.id("getStatus")
.outType(EndpointStatus.class)
.bindingMode(RestBindingMode.auto)
.responseMessage().code(200).message("Returns an EndpointStatus object representing state of a camel endpoint").endResponseMessage()
.to(CAMEL_STATUS_URI);
Here are the annotations I used on this class:
#ApiModel(description = "Endpoint Status Model")
public class EndpointStatus {
private boolean isAvailable;
private EndpointName name;
private long timestamp;
#ApiModelProperty(value = "Is the endpoint available", required = true)
public boolean isAvailable() {
return isAvailable;
}
public void setAvailable(boolean available) {
isAvailable = available;
}
#ApiModelProperty(value = "The name of the endpoint", required = true)
public EndpointName getName() {
return name;
}
public void setName(EndpointName name) {
this.name = name;
}
#ApiModelProperty(value = "The timestamp the endpoint was checked", required = true)
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}
Along with the generated swagger documentation:
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Camel Service"
},
"host": "localhost:9000",
"tags": [
{
"name": "healthcheck",
"description": "Health Check REST service"
}
],
"schemes": [
"http"
],
"paths": {
"/healthcheck/getStatus/{endpointName}": {
"get": {
"tags": [
"healthcheck"
],
"summary": "Get Camel Status",
"operationId": "getStatus",
"parameters": [
{
"name": "endpointName",
"in": "path",
"required": true,
"type": "string",
"enum": [
"ENDPOINTA",
"ENDPOINTB"
]
}
],
"responses": {
"200": {
"description": "Returns an EndpointStatus object representing state of a camel endpoint",
"schema": {
"$ref": "#/definitions/EndpointStatus"
}
}
}
}
}
},
"definitions": {
"EndpointStatus": {
"type": "object",
"required": [
"available",
"name",
"timestamp"
],
"properties": {
"name": {
"type": "string",
"description": "The name of the endpoint",
"enum": [
"ENDPOINTA",
"ENDPOINTB"
]
},
"timestamp": {
"type": "integer",
"format": "int64",
"description": "The timestamp the endpoint was checked"
},
"available": {
"type": "boolean",
"description": "Is the endpoint available"
}
},
"description": "Endpoint Status Model"
}
}
}
However, when trying to move to use camel-openapi-java which supports OpenAPI Specification v3 with the same setup I am getting EndpointStatus without any fields / descriptions in my documentation.
{
"openapi": "3.0.2",
"info": {
"title": "SurePath Camel Service",
"version": "1.0.0"
},
"servers": [
{
"url": ""
}
],
"paths": {
"/healthcheck/getStatus/{endpointName}": {
"get": {
"tags": [
"healthcheck"
],
"parameters": [
{
"name": "endpointName",
"schema": {
"enum": [
"ENDPOINTA",
"ENDPOINTB"
],
"type": "string"
},
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Returns an EndpointStatus object representing state of a camel endpoint"
}
},
"operationId": "getStatus",
"summary": "Get Camel Status",
"x-camelContextId": "camel-1",
"x-routeId": "getStatus"
}
},
"/healthcheck/isAvailable": {
"get": {
"tags": [
"healthcheck"
],
"responses": {
"200": {
"description": "Returns status code 200 when Camel is available"
}
},
"operationId": "verb1",
"summary": "Is Camel Available",
"x-camelContextId": "camel-1",
"x-routeId": "route4"
}
}
},
"components": {
"schemas": {
"EndpointStatus": {
"type": "EndpointStatus",
"x-className": {
"format": "com.sample.bean.EndpointStatus",
"type": "string"
}
}
}
},
"tags": [
{
"name": "healthcheck",
"description": "Health Check REST service"
}
]
}
I have tried adding this into my responseMessage and it is still not documenting correctly;
responseMessage().code(200).responseModel(EndpointStatus.class).message("Returns an EndpointStatus object representing state of a camel endpoint").endResponseMessage()
Do I need different annotations / RestDefinition config to get this EndpointStatus class appearing correctly in the OpenAPI documentation?
This looks to be an issue at the moment with the camel-openapi-java component; waiting for a resolution from this jira https://issues.apache.org/jira/browse/CAMEL-15158
I have 2 jpa entities Document and DispatchDetail which have one-to-many relationship. i.e. a document can have a list of dispatchDetails. I have created 2 repositories for each entity.
Now I'm gonna try a document GET.
http://localhost:7070/booking-documents-service/docs/5999571
{
"docType": "SAP_ACCOUNTS_PAYABLE",
"docStoreId": 456651,
"qualityChecked": true,
"format": "pdf",
"bookingId": -1,
"_links": {
"self": {
"href": "http://localhost:7070/booking-documents-service/docs/5999571"
},
"generatedDocument": {
"href": "http://localhost:7070/booking-documents-service/docs/5999571"
},
"dispatchDetails": {
"href": "http://localhost:7070/booking-documents-service/docs/5999571/dispatchDetails"
}
}
}
Now when I try GET request for the link listed as dispatchDetails. It is like this.
http://localhost:7070/booking-documents-service/docs/5999571/dispatchDetails
{
"_embedded": {
"dispatchDetails": [
{
"dispatchQueId": 207443,
"dispatchStatus": "S",
"recipient": "fldcvisla12678.wdw.disney.com|#|/opt/apps/shared/shuttle/SAP/OUT/|#|f-tbxshuttlenp|#|D1$NeY984|#|SFTP|#|22|#|null",
"description": "Upload :FileUploadDispatcher; FTP:null/null;\n2d89df3d-ca51-4d35-9528-439923fa48d4..",
"dispatcher": "AD",
"_links": {
"self": {
"href": "http://localhost:7070/booking-documents-service/dispatchDetails/1"
},
"dispatchDetail": {
"href": "http://localhost:7070/booking-documents-service/dispatchDetails/1"
},
"generatedDocument": {
"href": "http://localhost:7070/booking-documents-service/dispatchDetails/1/generatedDocument"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:7070/booking-documents-service/docs/5999571/dispatchDetails"
}
}
}
But I don't want dispatch details as a stand alone resource (listed in links above).
i.e. I don't want this endpoint
http://localhost:7070/booking-documents-service/dispatchDetails
Instead I only need this.
http://localhost:7070/booking-documents-service/docs/5999571/dispatchDetails
How to achieve this? i.e. allow only sub resource level operations.
I am using Spring Data JPA and Spring Data Rest with Spring Boot 1.5.10. I have three classes annotated with #Entity: Message, AMessage, and BMessage (Figure 2, 3, 4). Classes AMessage and BMessage extend Message class (Figure 1). I have one MessageRepository that is exposed via Spring Data Rest. When I make a request to get all messages at http://host:port/messages, I get a response that contains two separate arrays under _embedded object (one for AMessage and one for BMessage) even though I am retrieving data from the /messages endpoint (Figure 5). I only want to retrieve columns from the Message Entity. How can this be achieved?
I have uploaded my code to github.com
Figure 1: Hierarchy
Message
|
----------------------
| |
AMessage BMessage
Figure 2: Message Class (Parent)
import javax.persistence.*;
#Entity
#Table(name = "MSG")
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name = "TYPE")
public class Message {
#Id #GeneratedValue
private Long id;
private String messageColumn1;
#Column(name = "TYPE", updatable = false, insertable = false)
private String messageType;
public String getMessageColumn1() {
return messageColumn1;
}
public void setMessageColumn1(String messageColumn1) {
this.messageColumn1 = messageColumn1;
}
public String getMessageType() {
return messageType;
}
}
Figure 3: AMessage Class (Child)
import javax.persistence.*;
#Entity
#Table(name = "MSG_A")
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorValue("A")
public class AMessage extends Message {
private String messageAColumn1;
public String getMessageAColumn1() {
return messageAColumn1;
}
public void setMessageAColumn1(String messageAColumn1) {
this.messageAColumn1 = messageAColumn1;
}
}
Figure 4: BMessage Class (Child)
import javax.persistence.*;
#Entity
#Table(name = "MSG_B")
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorValue("B")
public class BMessage extends Message {
private String messageBColumn1;
public String getMessageBColumn1() {
return messageBColumn1;
}
public void setMessageBColumn1(String messageBColumn1) {
this.messageBColumn1 = messageBColumn1;
}
}
Figure 5: Get messages response (aMessages and bMessages separated)
http://localhost:8080/messages
{
"_embedded": {
"aMessages": [
{
"messageColumn1": "MColumn1",
"messageType": "A",
"messageAColumn1": "AColumn1",
"_links": {
"self": {
"href": "http://localhost:8080/aMessage/1"
},
"aMessage": {
"href": "http://localhost:8080/aMessage/1"
}
}
}
],
"bMessages": [
{
"messageColumn1": "MColumn1",
"messageType": "B",
"messageBColumn1": "BColumn1",
"_links": {
"self": {
"href": "http://localhost:8080/bMessage/2"
},
"bMessage": {
"href": "http://localhost:8080/bMessage/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/messages{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile/messages"
}
},
"page": {
"size": 20,
"totalElements": 2,
"totalPages": 1,
"number": 0
}
}
Resolved! Created new repositories for AMessage and BMessage. Annotated each repository with #RepositoryRestResource(collectionResourceRel = "messages"). This caused the messages to be combined into a single array under the _embedded object, when performing a GET on http://localhost:8080/messages
{
"_embedded": {
"messages": [
{
"messageColumn1": "MColumn1",
"messageType": "A",
"messageAColumn1": "AColumn1",
"_links": {
"self": {
"href": "http://localhost:8080/aMessages/1"
},
"aMessage": {
"href": "http://localhost:8080/aMessages/1"
}
}
},
{
"messageColumn1": "MColumn1",
"messageType": "B",
"messageBColumn1": "BColumn1",
"_links": {
"self": {
"href": "http://localhost:8080/bMessages/2"
},
"aMessages": {
"href": "http://localhost:8080/bMessages/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/messages{?page,size,sort}",
"templated": true
},
"profile": {
"href": "http://localhost:8080/profile/messages"
}
},
"page": {
"size": 20,
"totalElements": 2,
"totalPages": 1,
"number": 0
}
}