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
Related
I need to build an API to validate the request body against the registered schema for the respective type & subType.
API Contract:
{
"id": "<any-uuid>",
"type": "<some-type>",
"subType": "<some-sub-type>",
"data": {
}
}
Here, OpenAPI schema will be fetched based on the type and subType and then need to validate the data element against the respective OpenAPI schema.
Wrote the below snippet:
Map<String, Object> data = //get the data object from API request body;
JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V7);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node = objectMapper.convertValue(data, JsonNode.class);
String schemaJson = // fetch the registered schema for type and subtype
JsonSchema schema = jsonSchemaFactory.getSchema(schemaJson);
Set<ValidationMessage> errors = schema.validate(node);
// Throw exception when errors present in the Json Payload
if (errors.size() > 0) {
// throw the exception with errors
}
This code is working, when the schema don't have:
Few elements such as openapi, paths, info, components.
When one object not referring other.
API Schema in our database as follows:
{
"openapi": "3.0.0",
"paths": {},
"info": {
"title": "Patient Info API",
"version": "v0.1.0"
},
"components": {
"schemas": {
"Data": {
"type": "object",
"required": [
"action",
"patient"
],
"properties": {
"action": {
"type": "string",
"enum": [
"ADMIT",
"DISCHARGE",
"TRANSFER"
]
},
"patient": {
"$ref": "#/components/schemas/Patient"
}
}
},
"Patient": {
"type": "object",
"required": [
"firstName",
"lastName"
],
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
}
}
}
}
}
}
The data element in the API request body looks like this.
{
"action": "ADMIT",
"patient": {
"firstName": "John",
"lastName": "Doe"
}
}
Can json-schema-validator can help to achieve this?
For simplicity, wrote all the logic inside the Spring Controller itself.
package com.schema.validator.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.io.Files;
import com.networknt.schema.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A REST API to validate the request body against Open API Schema
*
*/
#RestController
#RequestMapping("schema-validate")
public class SchemaValidatorController {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final JsonSchemaFactory jsonSchemaFactory =
JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V6);
#Value("classpath:schema/json-schema.json")
private Resource schemaResource;
#PostMapping
public ResponseEntity<List<String>> validateSchema(#RequestBody Map<String, Object> jsonRequest) throws IOException {
String schemaJson = Files.asCharSource(schemaResource.getFile(), Charset.defaultCharset()).read();
final JsonNode requestJsonNode = objectMapper.valueToTree(jsonRequest);
JsonNode componentNode = objectMapper.readTree(schemaJson).get("components");
// Get first name in the JsonNode
JsonNode schemaNode = componentNode.get("schemas").elements().next();
((ObjectNode) schemaNode).set("components", componentNode);
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setTypeLoose(false);
config.setHandleNullableField(true);
JsonSchema jsonSchema = jsonSchemaFactory.getSchema(schemaNode, config);
Set<ValidationMessage> errors =
jsonSchema.validate(requestJsonNode, requestJsonNode, "data");
if (CollectionUtils.isEmpty(errors)) {
return ResponseEntity.ok(Collections.emptyList());
}
return ResponseEntity.ok(errors.stream()
.map(ValidationMessage::getMessage)
.collect(Collectors.toList()));
}
}
Here, json-schema.json created under resources/schema folder.
Limitation: The root element in Open API schema should be declared as a first child of schemas in Open API spec.
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 need to return the response of custom web api as JSON.
i need it to be like this:
{
"data": [
{
"id": "1",
"name": "Tiger Nixon",
"position": "System Architect",
"salary": "$320,800",
"start_date": "2011/04/25",
"office": "Edinburgh",
"extn": "5421"
},
{
"id": "2",
"name": "Garrett Winters",
"position": "Accountant",
"salary": "$170,750",
"start_date": "2011/07/25",
"office": "Tokyo",
"extn": "8422"
}
]
}
however, my module is coming out as
[
{
"id": "1",
"name": "Tiger Nixon",
"position": "System Architect",
"salary": "$320,800",
"start_date": "2011/04/25",
"office": "Edinburgh",
"extn": "5421"
},
{
"id": "2",
"name": "Garrett Winters",
"position": "Accountant",
"salary": "$170,750",
"start_date": "2011/07/25",
"office": "Tokyo",
"extn": "8422"
}
]
My Interface is as follows:
<?php
namespace Mag\Rest\Api;
interface OrdersManagementInterface
{
/**
* GET for Stock api
* #param mixed $items
* #return string
*/
public function getOrders();
}
My model is as follows:
<?php
namespace Mag\Rest\Model;
class OrdersManagement
{
public function __construct(
) {
}
/**
* {#inheritdoc}
*/
public function getOrders()
{
//... sql code to get data goes here
$results = $this->connection->fetchAll($sql);
return $response ;
}
}
any idea on how to be able to format the output of the rest api ?
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
}
}
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.