Using #JsonSerialize hides HATEOAS links in Spring Data REST - java

I'm using Spring Boot 2.x, Spring Data REST, Spring HATEOAS.
I've a simple bean:
#Entity
public class Printer extends AbstractEntity {
#NotBlank
#Column(nullable = false)
private String name;
// Ip address or hostname
#NotBlank
#Column(nullable = false)
private String remoteAddress;
#NotNull
#Enumerated(EnumType.STRING)
#Column(nullable = false)
#JsonSerialize(using = PrinterModelSerializer.class)
private PrinterModel model;
#NotNull
#Column(nullable = false, columnDefinition = "BIT DEFAULT 0")
private boolean ssl = false;
// The store where the device is connected
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Store store;
I added a custom Serializer for the enum PrinterModel:
public class PrinterModelSerializer extends JsonSerializer<PrinterModel> {
#Override
public void serialize(PrinterModel value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject(); // {
gen.writeStringField("key", value.toString());
gen.writeStringField("name", value.getName());
gen.writeBooleanField("fiscal", value.isFiscal());
gen.writeStringField("imageUrl", value.getImageUrl());
gen.writeEndObject(); // }
gen.close();
}
}
When I get the resource printer using Spring Data REST repositories I've:
{
"sid" : "",
"createdBy" : "admin",
"createdDate" : "2018-10-16T12:29:24Z",
"lastModifiedDate" : "2018-10-16T14:12:08.671566Z",
"lastModifiedBy" : "ab48d95f-09f3-40ba-b8ba-e6fd206a2fe6",
"createdByName" : null,
"lastModifiedByName" : null,
"name" : "m30",
"remoteAddress" : "111.222.333.456",
"model" : {
"key" : "EPSON_M30",
"name" : "Epson TM-m30",
"fiscal" : false,
"imageUrl" : "https://www.epson.it/files/assets/converted/550m-550m/0/0/1/d/001d0815_pictures_hires_en_int_tm-m30_w_frontpaperloading_paper.tif.jpg"
}
}
As you can see I don't have HATEOAS links. If I remove my custom serializer instead I've the right reply:
{
"sid" : "",
"createdBy" : "admin",
"createdDate" : "2018-10-16T12:29:24Z",
"lastModifiedDate" : "2018-10-16T14:12:08.671566Z",
"lastModifiedBy" : "ab48d95f-09f3-40ba-b8ba-e6fd206a2fe6",
"createdByName" : null,
"lastModifiedByName" : null,
"name" : "m30",
"remoteAddress" : "111.222.333.456",
"model" : "EPSON_M30",
"ssl" : true,
"_links" : {
"self" : {
"href" : "http://x.x.x.x:8082/api/v1/printers/3"
},
"printer" : {
"href" : "http://x.x.x.x:8082/api/v1/printers/3{?projection}",
"templated" : true
},
"store" : {
"href" : "http://x.x.x.x:8082/api/v1/printers/3/store{?projection}",
"templated" : true
}
}
}
How can I prevent #JsonSerialize to break HATEOAS links?

Related

How can i generate JSON schema with a required filed or default values?

im using jackson to handle JSON objects, and i want a way to generate a schema from a class that i can enforce on the objects.
i found out that i can use this :
JsonSchemaGenerator generator = new JsonSchemaGenerator(mapper);
JsonSchema jsonSchema = generator.generateSchema(DBUser.class);
mapper.writeValueAsString(jsonSchema);
using this com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator.
generated from this class:
public class DBUser {
private String database="stu";
private int role;
private String username;
private String password;
//setters and getters defined
}
which gives me this :
{
"type" : "object",
"id" : "urn:jsonschema:DBUser",
"properties" : {
"database" : {
"type" : "string"
},
"role" : {
"type" : "integer"
},
"username" : {
"type" : "string"
},
"password" : {
"type" : "string"
}
}
}
what i need is a required field like this :
"required": ["database","role","username"]
but it has no required field, or initial values, which i need.
so how can i get that ?
You can annotate your pojo DBUser class fields with the JsonProperty#required set to true to generate a jsonschema with required fields:
public class DBUser {
#JsonProperty(required = true)
private String database = "stu";
#JsonProperty(required = true)
private int role;
#JsonProperty(required = true)
private String username;
private String password;
}
//generate the new jsonschema with the required fields
JsonSchemaGenerator generator = new JsonSchemaGenerator(mapper);
JsonSchema jsonSchema = generator.generateSchema(DBUser.class);
System.out.println(mapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(jsonSchema));
//json generated
{
"type" : "object",
"id" : "urn:jsonschema:DBUser",
"properties" : {
"database" : {
"type" : "string",
"required" : true
},
"role" : {
"type" : "integer",
"required" : true
},
"username" : {
"type" : "string",
"required" : true
},
"password" : {
"type" : "string"
}
}
}
Note that according to the documentation the jsonschema module supports the creation of a JSON Schema (v3) and not upper JSON Schema versions, so keywords introduced in later versions are not supported.

Spring-data-elasticsearch "nested query throws [nested] failed to find nested object under path" Exception

I have 2 POJOs (Person and Car) where one is referred by the other
#Document(indexName = "person", type = "user")
public class Person {
#Id
private String id;
private String name;
#Field(type = FieldType.Nested)
private Car car;
//getter and setter
}
This is the Car object which is referred as a nested in the Person object
public class Car {
private String name;
private String model;
//getter and setter
}
This is my REST end point. Here I am trying to return the person who has the given car model. I am sending the car model as a path variable and I am creating a QueryBuilder object
#RequestMapping(value = "/api/{carModel}")
public List<Map<String,Object>> search(#PathVariable final String carModel) {
QueryBuilder queryBuilder = QueryBuilders.nestedQuery(
"car",
QueryBuilders.boolQuery().must(QueryBuilders.matchQuery("car.model", carModel)),
ScoreMode.None);
final SearchRequestBuilder searchRequestBuilder = client.prepareSearch("person")
.setTypes("user")
.setSearchType(SearchType.QUERY_THEN_FETCH)
.setQuery(queryBuilder);
final SearchResponse response = searchRequestBuilder.get();
List<Map<String,Object>> resultList = new ArrayList<>();
List<SearchHit> searchHits = Arrays.asList(response.getHits().getHits());
for (SearchHit hit : searchHits) {
resultList.add(hit.getSourceAsMap());
}
return resultList;
}
There is an exception occurred at final SearchResponse response = searchRequestBuilder.get(); saying java.lang.IllegalStateException: [nested] failed to find nested object under path [car]
"nested" : {
"query" : {
"bool" : {
"must" : [
{
"match" : {
"car.model" : {
"query" : "gt200",
"operator" : "OR",
"prefix_length" : 0,
"max_expansions" : 50,
"fuzzy_transpositions" : true,
"lenient" : false,
"zero_terms_query" : "NONE",
"auto_generate_synonyms_phrase_query" : true,
"boost" : 1.0
}
}
}
],
"adjust_pure_negative" : true,
"boost" : 1.0
}
},
"path" : "car",
"ignore_unmapped" : false,
"score_mode" : "none",
"boost" : 1.0
}
}]; nested: IllegalStateException[[nested] failed to find nested object under path [car]]; }{[5uefqk2YT0ahmj3s-S1cvw][person][1]:
How Could I solve this problem ?
Please set "ignore_unmapped" : true, most probably it will solve your problem

Set default entity ID variable for HATEOAS spring rest api

I have a HATEOAS Spring rest API that connects to a mysql database. I have no control over the database schema and it changes periodically so I am periodically generating the entity classes and updating the service.
I have three files per route. A generated entity class, a controller and a repository file that uses the PagingAndSortingRepository class to automatically serve my entities without much configuration.
Entity class
package hello.models;
import javax.persistence.*;
import java.util.Objects;
#Entity
#Table(name = "animals", schema = "xyz123", catalog = "")
public class AnimalsEntity {
private Integer id;
private String name;
private String description;
#Id
#Column(name = "id", nullable = false)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#Basic
#Column(name = "name", nullable = true, length = 80)
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Basic
#Column(name = "description", nullable = true, length = 255)
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RoleEntity that = (RoleEntity) o;
return Objects.equals(id, that.id) &&
Objects.equals(name, that.name) &&
Objects.equals(description, that.description);
}
#Override
public int hashCode() {
return Objects.hash(id, name, description);
}
}
Repository class
package hello.repositories;
#RepositoryRestResource(collectionResourceRel = "animals", path = "animals")
public interface AnimalsRepository extends PagingAndSortingRepository<AnimalEntity, String> {
// Allows /animal/cheetah for example.
AnimalEntity findByName(String name);
// Prevents POST /element and PATCH /element/:id
#Override
#RestResource(exported = false)
public AnimalEntity save(AnimalEntity s);
// Prevents DELETE /element/:id
#Override
#RestResource(exported = false)
public void delete(AnimalEntity t);
}
Controller class
package hello.controllers;
import hello.models.AnimalsEntity;
import hello.repositories.AnimalsRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
#RepositoryRestController
#RequestMapping("/animals")
class PrinterController {
#Autowired
private AnimalsRepository animalsRepo;
#RequestMapping("/{name}")
public #ResponseBody
List<AnimalsEntity> findAnimal(#PathVariable(value = "name") String name) {
return animalsRepo.findByName(name);
}
}
I want my HATEOAS API to serve things with the pagination/sorting options. They currently serves responses like..
{
"links" : [ {
"rel" : "first",
"href" : "http://localhost:8080/animals?page=0&size=20",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
}, {
"rel" : "self",
"href" : "http://localhost:8080/animals{?page,size,sort}",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
}, {
"rel" : "next",
"href" : "http://localhost:8080/animals?page=1&size=20",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
}, {
"rel" : "last",
"href" : "http://localhost:8080/animals?page=252&size=20",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
}, {
"rel" : "profile",
"href" : "http://localhost:8080/profile/animals",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
}, {
"rel" : "search",
"href" : "http://localhost:8080/animals/search",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
} ],
"content" : [ {
"id" : 1,
"name" : "cheetah",
"description": "xyz
]
},{
"id" : 2,
"name" : "tortise",
"description": "xyz
]
}],
"page" : {
"size" : 20,
"totalElements" : 5049,
"totalPages" : 253,
"number" : 0
}
}
This is awesome. But I need my javascript applications to access the rest api like (GET) /animals/cheetah.
Normally I would change the schema and set #Id on the name property in the entity class but I cannot do that in this instance. I cant change the database schema and eventually I want to dynamically generate these entity classes to allow for easy schema changes.
I've figured out that I can override the endpoint and serve it manually but I lose the pagination/HATEOAS formatting.
[
{
"id": 1,
"name": "cheetah",
"description": "xyz"
},
{
"id": 2,
"name": "tortise",
"description": "xyz"
}
]
How do I accomplish the #Id change without losing the JSON format or changing the entity class?
I found my answer here: https://docs.spring.io/spring-data/rest/docs/current/reference/html/#_customizing_item_resource_uris
You can map other fields as the default lookup.

Self-referencing JSON with Spring Data REST

Self-referencing JSON with Spring Data REST
In a Spring Data REST data model with Lombok, how can we export and import data to self-referencing JSON without creating a parallel entities or DTOs?
For example, a self-referencing JSON using JSONPath for a simple music manager might look like:
{ "id" : 1,
"albums" : [ {
"id" : 1,
"title" : "Kind Of Blue",
"artist" : "$..artists[?(#.id=1)]",
"tracks" : [ {
"id" : 1,
"title" : "So What",
"duration" : "PT9M5S",
"musicians" : [ {
"musician" : "$..artists[?(#.id=1)]",
}, {
"musician" : "$..artists[?(#.id=2)]",
} ]
}, {
"id" : 3,
"title" : "Blue in Green",
"duration" : "PT5M29S",
"musicians" : [ {
"musician" : "$..artists[?(#.id=1)]",
}, {
"musician" : "$..artists[?(#.id=2)]",
} ]
} ]
} ],
"artists" : [ {
"id" : 1,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
}, {
"id" : 2,
"firstName" : "Bill",
"lastName" : "Evans",
"birthDate" : "1929-09-16"
} ]
}
How can we create the import and export functionality for this representation while retaining the Spring Data REST HATEOAS functionality? The musicians container in an export/import has an array of string JSONPath expressions and in the REST APIs the musicians is an array of Person objects (see below) -- so how can Jackson be configured to select the correct serializer and deserializer the export and import operations?
Details: music manager example
Here's a Spring Boot 1.5, Spring Data Rest, Lombok, and Jackson implementation (GitHub).
Music
#Data
#NoArgsConstructor
#Entity
public class Music {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
#OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true)
private List<Album> albums;
#OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true)
private List<Person> artists;
}
Album
#Data
#NoArgsConstructor
#Entity
public class Album {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String title;
#ManyToOne(cascade = { CascadeType.ALL })
private Person artist;
#OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true)
private List<Track> tracks;
}
Track
#Data
#NoArgsConstructor
#Entity
public class Track {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String title;
#JsonSerialize(using = MyDurationSerializer.class)
private Duration duration;
#ManyToMany(cascade = { CascadeType.ALL })
private List<Person> musicians;
public Track(String title, String duration, List<Person> musicians) {
this.title = title;
this.duration = Duration.parse(duration);
this.musicians = musicians;
}
}
Person
#Data
#NoArgsConstructor
#Entity
public class Person {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
#JsonSerialize(using = MyLocalDateSerializer.class)
private LocalDate birthDate;
public Person(String firstName, String lastName, String birthDate) {
this.firstName = firstName;
this.lastName = lastName;
this.birthDate = LocalDate.parse(birthDate);
}
}
MyDurationSerializer
public class MyDurationSerializer extends StdSerializer<Duration> {
private static final long serialVersionUID = 1L;
protected MyDurationSerializer() {
super(Duration.class);
}
#Override
public void serialize(Duration value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(value.toString());
}
}
MyLocalDateSerializer
public class MyLocalDateSerializer extends StdSerializer<LocalDate> {
private static final long serialVersionUID = 1L;
private DateTimeFormatter FORMATTER = ofPattern("yyyy-MM-dd");
protected MyLocalDateSerializer() {
super(LocalDate.class);
}
#Override
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(value.format(FORMATTER));
}
}
Spring Data REST HATEOAS representation
curl http://localhost:4000/albums/1/tracks
{ "id" : 1,
"albums" : [ {
"id" : 1,
"title" : "Kind Of Blue",
"artist" : "#{
"id" : 1,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
},
"tracks" : [ {
"id" : 1,
"title" : "So What",
"duration" : "PT9M5S",
"musicians" : [ {
"id" : 1,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
}, {
"id" : 2,
"firstName" : "Bill",
"lastName" : "Evans",
"birthDate" : "1929-09-16"
} ]
}, {
"id" : 3,
"title" : "Blue in Green",
"duration" : "PT5M29S",
"musicians" : [ {
"id" : 1,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
}, {
"id" : 2,
"firstName" : "Bill",
"lastName" : "Evans",
"birthDate" : "1929-09-16"
} ]
} ]
} ],
"artists" : [ {
"id" : 15,
"firstName" : "Miles",
"lastName" : "Davis",
"birthDate" : "1926-05-26"
}, {
"id" : 16,
"firstName" : "Bill",
"lastName" : "Evans",
"birthDate" : "1929-09-16"
} ]
}

Generate JsonSchema where properties have more than one type

When producing the schema for a Java type I would like to specify that certain String values (or other types) could be null or "Nullable", for example, if there is a "name" property with this requirement I'd like to see something like this in the output:
"name" : {
"type" : ["string", "null"]
}
Additionally, what if I want to allow a value to be either integer or String? How can Jackson's schema generation be informed about it? I mean something like this:
"id" : {
"type" : ["string", "integer"]
"required" : true
}
But so far the things that I've attempted with Jackson are not working. I have the following method that at runtime will create a JsonSchema for the input type:
public String getJsonSchema(Class<Message> type) throws IOException{
ObjectMapper m = new ObjectMapper();
JsonSchemaGenerator generator = new JsonSchemaGenerator(m);
JsonSchema schema = generator.generateSchema(type);
return m.writerWithDefaultPrettyPrinter().writeValueAsString(schema);
}
Message.java is something like this (I removed the setters to keep the code shorter):
public class Message {
private String name;
private Long id;
#JsonProperty(value = "name", required = false)
#JsonPropertyDescription("User provided name.")
public String getName() {
return name;
}
#JsonProperty(value = "id", required = true)
public Long getId() {
return id;
}
}
The output for the previous bean is this:
{
"type" : "object",
"id" : "urn:jsonschema:spring:integration:quickstart:Message",
"properties" : {
"name" : {
"type" : "string",
"description" : "User provided name."
},
"id" : {
"type" : "integer",
"required" : true
}
}
}
The question is, what annotation or configuration option is required to make "null" one of the possible types for name?

Categories

Resources