I'm currently building an API using spring hateoas. Most of my controllers provide a list method which returns a PagedResources<>. For some reason the selfrel does not contain the {?page,size,sort} template which is found in all examples. Instead I only get the base URI.
So my e.g. my ProjectContoller looks like
#GetMapping
public PagedResources<ProjectResource> list(Pageable pageable, PagedResourcesAssembler<Project> pagedResourcesAssembler){
Page<Project> projects = service.findAll(pageable);
return pagedResourcesAssembler.toResource(projects, assembler);
}
and returns
{"_embedded":{
"projectResourceList":[
{
"begin":1462053600000,
"end":1469829600000,
"name":"Cool Big Project",
"_links":{"self":{"href":"http://localhost/projects/1"}}
}
]
},
"_links":{"self":{"href":"http://localhost/projects"}},
"page":{
"size":20,
"totalElements":1,
"totalPages":1,
"number":0
}
}
I guess I'm missing something trivial but can't find out what :-/
After reading how HAL should behave in Spring Data Rest it seems there could be some bug.
This is the standard Spring Data Rest links output for a tasks collection resource:
"_links": {
"first": {
"href": "http://localhost:8080/tasks?page=0&size=20"
},
"self": {
"href": "http://localhost:8080/tasks"
},
"next": {
"href": "http://localhost:8080/tasks?page=1&size=20"
},
"last": {
"href": "http://localhost:8080/tasks?page=2&size=20"
},
"profile": {
"href": "http://localhost:8080/profile/tasks"
},
"search": {
"href": "http://localhost:8080/tasks/search"
}
No link is being templated in out of the box HAL responses rendered by Spring Data Rest, contrary to what is shown in docs.
If I follow next link, the self link is not correct:
"_links": {
"first": {
"href": "http://localhost:8080/tasks?page=0&size=20"
},
"prev": {
"href": "http://localhost:8080/tasks?page=0&size=20"
},
"self": {
"href": "http://localhost:8080/tasks"
},
"next": {
"href": "http://localhost:8080/tasks?page=2&size=20"
},
"last": {
"href": "http://localhost:8080/tasks?page=2&size=20"
},
"profile": {
"href": "http://localhost:8080/profile/tasks"
},
"search": {
"href": "http://localhost:8080/tasks/search"
}
}
If I override the Controller:
#RequestMapping(method = RequestMethod.GET, path = "/tasks")
public ResponseEntity<Page<Task>> read(Pageable pageRequest, PersistentEntityResourceAssembler assembler) {
Page<Task> pendingTasks = taskService.read(pageRequest);
return new ResponseEntity(pageAssembler.toResource(pendingTasks, (ResourceAssembler) assembler),
HttpStatus.OK);
}
And add a service between the controller and repository that initializes a Pageable instance to default values even if none is specified:
public Page<Task> read(Pageable pageRequest) {
Pageable effectivePageRequest = pageRequest;
if (effectivePageRequest == null) {
effectivePageRequest = new PageRequest(0, 20, DEFAULT_SORT);
}
if (effectivePageRequest.getSort() == null) {
//override Sort
effectivePageRequest = new PageRequest(effectivePageRequest.getPageNumber(),
effectivePageRequest.getPageSize(), DEFAULT_SORT);
}
return taskRepository.findByStatus(Task.Status.PENDING, effectivePageRequest);
}
I can overcome the problem with the self link. But have seen no way of generating the templlated URIs. These are the links for the second page:
"_links": {
"first": {
"href": "http://localhost:8080/tasks?page=0&size=20&sort=created,desc"
},
"prev": {
"href": "http://localhost:8080/tasks?page=0&size=20&sort=created,desc"
},
"self": {
"href": "http://localhost:8080/tasks?page=1&size=20&sort=created,desc"
},
"next": {
"href": "http://localhost:8080/tasks?page=2&size=20&sort=created,desc"
},
"last": {
"href": "http://localhost:8080/tasks?page=2&size=20&sort=created,desc"
}
Hope this helps discarding possibilities, but default behavior shown here suggests that there may be some problem with Spring HATEOAS/Spring Data Rest when generating collection links.
I'm using org.springframework.boot:spring-boot-starter-data-rest:jar:1.4.0.RELEASE, org.springframework.data:spring-data-rest-webmvc:jar:2.5.2.RELEASE, org.springframework.hateoas:spring-hateoas:jar:0.20.0.RELEASE
Related
We're using Spring Boot 2.7.0 with spring-boot-starter-actuator that we're exposing on port 8081 under the /management context path. The proxy sets several X-Forwarded-* headers, including the X-Forwarded-Prefix header that is set to /service. But when navigating to https://www.company.com/management this is what is returned:
{
"_links": {
"self": {
"href": "https://www.company.com/management",
"templated": false
},
"beans": {
"href": "https://www.company.com/management/beans",
"templated": false
},
"caches-cache": {
"href": "https://www.company.com/management/caches/{cache}",
"templated": true
},
"caches": {
"href": "https://www.company.com/management/caches",
"templated": false
},
"health": {
"href": "https://www.company.com/management/health",
"templated": false
},
"health-path": {
"href": "https://www.company.com/management/health/{*path}",
"templated": true
},
"info": {
"href": "https://www.company.com/management/info",
"templated": false
},
"conditions": {
"href": "https://www.company.com/management/conditions",
"templated": false
},
"configprops": {
"href": "https://www.company.com/management/configprops",
"templated": false
},
"configprops-prefix": {
"href": "https://www.company.com/management/configprops/{prefix}",
"templated": true
},
"env": {
"href": "https://www.company.com/management/env",
"templated": false
},
"env-toMatch": {
"href": "https://www.company.com/management/env/{toMatch}",
"templated": true
},
"integrationgraph": {
"href": "https://www.company.com/management/integrationgraph",
"templated": false
},
"loggers": {
"href": "https://www.company.com/management/loggers",
"templated": false
},
"loggers-name": {
"href": "https://www.company.com/management/loggers/{name}",
"templated": true
},
"heapdump": {
"href": "https://www.company.com/management/heapdump",
"templated": false
},
"threaddump": {
"href": "https://www.company.com/management/threaddump",
"templated": false
},
"metrics-requiredMetricName": {
"href": "https://www.company.com/management/metrics/{requiredMetricName}",
"templated": true
},
"metrics": {
"href": "https://www.company.com/management/metrics",
"templated": false
},
"scheduledtasks": {
"href": "https://www.company.com/management/scheduledtasks",
"templated": false
},
"sessions-sessionId": {
"href": "https://www.company.com/management/sessions/{sessionId}",
"templated": true
},
"sessions": {
"href": "https://www.company.com/management/sessions",
"templated": false
},
"mappings": {
"href": "https://www.company.com/management/mappings",
"templated": false
},
"refresh": {
"href": "https://www.company.com/management/refresh",
"templated": false
},
"features": {
"href": "https://www.company.com/management/features",
"templated": false
},
"traces": {
"href": "https://www.company.com/management/traces",
"templated": false
}
}
}
I'm expecting the href's in the response to start with https://www.company.com/service due to the supplied X-Forwarded-Prefix header, but this is not the case. I've tried to add the ForwardedHeaderFilter like this:
#Bean
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilterFilterRegistrationBean() {
ForwardedHeaderFilter forwardedHeaderFilter = new ForwardedHeaderFilter();
FilterRegistrationBean<ForwardedHeaderFilter> bean = new FilterRegistrationBean<>(forwardedHeaderFilter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
but it makes no difference.
How can I make actuator take the X-Forwarded-Prefix header into account when generating the links to the endpoints behind a proxy?
I got the same problem. I found that ManagementContextAutoConfiguration uses separate WebApplicationContext if management port is different. That is why declaring of ForwardedHeaderFilter has no effect. Maybe there is an issue in spring actuator. I made this hack and it seems to be working. But I hope somebody will find better solution.
#Component
#ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
public class ManagementContextFactoryBeanPostProcessor
implements BeanPostProcessor {
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof ManagementContextFactory managementContextFactory) {
return (ManagementContextFactory) (parent, configurationClasses) -> {
var context = managementContextFactory.createManagementContext(parent, configurationClasses);
if (context instanceof GenericWebApplicationContext genericWebApplicationContext) {
genericWebApplicationContext.registerBean(ForwardedHeaderFilterRegistrationBean.class);
}
return context;
};
}
return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
}
public static class ForwardedHeaderFilterRegistrationBean
extends FilterRegistrationBean<ForwardedHeaderFilter> {
public ForwardedHeaderFilterRegistrationBean() {
setFilter(new ForwardedHeaderFilter());
setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
}
If you use Spring Security, you can add ForwardedHeaderFilter through endpoint security configuration like this:
#Bean
public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
return new WebSecurityConfigurerAdapter() {
#Override
protected void configure(HttpSecurity http) {
http.requestMatcher(EndpointRequest.toAnyEndpoint())
.addFilterBefore(new ForwardedHeaderFilter(), HeaderWriterFilter.class);
}
};
}
I was able to add the X-Forwarded-* headers to the hrefs returned from the actuator endpoint using below bean which processes the X-Forwarded-* headers and adds them to the request.
#Bean
public ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}
A hypermedia response that needs to be consumed by one of my services looks like this:
{
"_embedded": {
"content": [
{
"createdBy": "...",
"createdDate": "2020-03-07T14:21:27.507Z",
"id": "...",
"name": "item1",
"_links": {
"self": {
"href": ".."
}
}
}
]
},
"_links": {
"self": {
"href": "..."
},
},
"pageNumber": 1,
"totalItems": 20,
"pageSize": 10
}
See how the paging related info is not what's expected by Spring Hateoas PagedModel which should have a single "page" property instead of individual ones for pageNumber, totalItems and pageSize:
"page": {
"size": 2,
"totalElements": 1000,
"totalPages": 500,
"number": 5
}
What I did in the end was to extend CollectionModel by adding those individual properties. This does work correctly deserialising a response shown above. But, all the CollectionModel constructors are now deprecated and the alternative is to use "CollectionModel.of", which however returns just CollectionModel.
What's the right way of customising paging information in using Spring Hateoas?
Many thanks!
Pretty sure I am missing something clearly obvious but not seeing it.
How can I use my updated swagger.json file?
I took my boilerplate swagger/v1/swagger.json code and pasted it into the editor.swagger.io system. I then updated the descriptions etc, added examples to my models and then saved the contents as swagger.json.
Moved the file into the root of my api application, set the file to copy always.
public void ConfigureServices(IServiceCollection services)
{...
services.AddSwaggerGen(c => { c.SwaggerDoc("V1", new Info {Title = "Decrypto", Version = "0.0"}); });
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseSwagger();
//--the default works fine
// app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/V1/swagger.json", "Decrypto v1"); });
app.UseSwaggerUI(c => { c.SwaggerEndpoint("swagger.json", "Decrypto v1"); });
app.UseMvc();
}
I have tried a few different variation but none seem to be the trick. I don't really want to rewrite the work in SwaggerDoc as it seems dirty to me put documentation in the runtime.
the custom swagger.json file I want to use looks like this:
{
"swagger": "2.0",
"info": {
"version": "0.0",
"title": "My Title"
},
"paths": {
"/api/Decryption": {
"post": {
"tags": [
"API for taking encrypted values and getting the decrypted values back"
],
"summary": "",
"description": "",
"operationId": "Post",
"consumes": [
"application/json-patch+json",
"application/json",
"text/json",
"application/*+json"
],
"produces": [
"text/plain",
"application/json",
"text/json"
],
"parameters": [
{
"name": "units",
"in": "body",
"required": true,
"schema": {
"uniqueItems": false,
"type": "array",
"items": {
"$ref": "#/definitions/EncryptedUnit"
}
}
}
],
"responses": {
"200": {
"description": "Success",
"schema": {
"uniqueItems": false,
"type": "array",
"items": {
"$ref": "#/definitions/DecryptedUnit"
}
}
}
}
}
}
},
"definitions": {
"EncryptedUnit": {
"type": "object",
"properties": {
"value": {
"type": "string",
"example": "7OjLFw=="
},
"initializeVector": {
"type": "string",
"example": "5YVg="
},
"cipherText": {
"type": "string",
"example": "596F5AA48A882"
}
}
},
"DecryptedUnit": {
"type": "object",
"properties": {
"encrypted": {
"type": "string",
"example": "7OjLV="
},
"decrypted": {
"type": "string",
"example": "555-55-5555"
}
}
}
}
}
you need to configure PhysicalFileProvider and put your swagger.json into wwwroot or anywhere accessible by PhysicalFileProvider. After that you can access it using IFileProvider
Reference: https://www.c-sharpcorner.com/article/file-providers-in-asp-net-core/
Edit If you just add app.UseStaticFiles(); into your StartUp, you can access wwwroot without hastle.
Reference
Completely Different Approach
you may also consider to serve your file using Controller/Action
public IActionResult GetSwaggerDoc()
{
var file = Path.Combine(Directory.GetCurrentDirectory(),
"MyStaticFiles", "swagger.json");
return PhysicalFile(file, "application/json");
}
.NET Core 2.2 could server physical file to url resource like below.
But if you use custom swagger json, your api is fixed except you change it every time.
public void Configure(IApplicationBuilder app, IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
...
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(),
"swagger/v1/swagger.json")),
RequestPath = "swagger/v1/swagger.json"
});
}
This question already has an answer here:
Spring Data ReST ref link omission when null or empty
(1 answer)
Closed 5 years ago.
I developed a Spring Boot, Spring Data REST, Hibernate application that exposes REST endpoints.
A typical response is something like this:
{
"sid": "f6dddaaa-2713-4b92-844b-6f0d3cefad3f",
"createdBy": "admin",
"createdDate": "2018-01-30T15:56:38.417Z",
"lastModifiedDate": "2018-01-30T15:57:53.963Z",
"lastModifiedBy": "admin",
"status": "Annullato",
"number": "51",
"dailyCode": "VS",
"entryDate": "2018-01-30T15:56:00Z",
"exitDate": "2018-01-31T15:56:00Z",
"totalDays": 1,
"standard": true,
"minibus": false,
"schoolTrip": false,
"price": 400,
"fareRow": "Standard",
"fareColumn": "Euro 0 3",
"extSyncCode": null,
"payments": [],
"passengers": 44,
"agency": null,
"paperBlock": null,
"paperReceipt": null,
"payer": null,
"checkedMedia": false,
"checkedLicensePlate": false,
"_links": {
"self": {
"href": "http://localhost:8080/api/v1/tickets/1"
},
"ticket": {
"href": "http://localhost:8080/api/v1/tickets/1{?projection}",
"templated": true
},
"area": {
"href": "http://localhost:8080/api/v1/tickets/1/area"
},
"fareException": {
"href": "http://localhost:8080/api/v1/tickets/1/fareException"
},
"block": {
"href": "http://localhost:8080/api/v1/tickets/1/block"
},
"customer": {
"href": "http://localhost:8080/api/v1/tickets/1/customer"
},
"transitCertificate": {
"href": "http://localhost:8080/api/v1/tickets/1/transitCertificate"
},
"passengersCountry": {
"href": "http://localhost:8080/api/v1/tickets/1/passengersCountry"
},
"refund": {
"href": "http://localhost:8080/api/v1/tickets/1/refund"
},
"fine": {
"href": "http://localhost:8080/api/v1/tickets/1/fine"
},
"hotel": {
"href": "http://localhost:8080/api/v1/tickets/1/hotel"
},
"workShift": {
"href": "http://localhost:8080/api/v1/tickets/1/workShift"
}
}
}
As you can see this entity has a lot of links. These links represent bound entities. Unfortunately some of these entities are optional.
I created a Angular 5 application that consumes my server side API. When I want to display data (let's say the entity shown in the example) I need to get related entities and I've to visit all links. Because not all related entities are mandatories, some of these links return HTTP 404 and the browser display these calls as errors (see the picture).
Is my approach right? Should I visit all these links and consider the 404 response perfectly fine (I think so) even if the browser consided that as an error? Is there a better approach otherwise?
That’s a lot of work to discover a related entity doesn’t exist. HTTP requests are slow. Admittedly they will run in parallel but it still seems inefficient.
Instead, when you load the parent entity, can you check at that time which children entities will be valid? If the child will not be valid then, instead of a link, return null. Or don’t return the value at all.
is it possible to output a return Value totalAmount of an Entity ShoppingCart that is not a Value in the Class but a Method? So for example I have a Class Shoppingcart with a List of Items. and a Method totalAmount. Now when I make a request to the API with the URL http://localhost:8082/carts/1 I want to get a response like the following:
{
"creationDate": "2016-12-07T09:45:38.000+0000",
"items": [
{
"itemName": "Nintendo 2DS",
"description": "Konsole from Nintendo",
"price": 300.5,
"quantity": 3
},
{
"itemName": "Nintendo Classic",
"description": "Classic nintendo Console from the 80th...",
"price": 75,
"quantity": 2
}
],
"totalAmount": "1051,50",
"_links": {
"self": {
"href": "http://localhost:8082/carts/2"
},
"cart": {
"href": "http://localhost:8082/carts/2"
},
"checkout": {
"href": "http://localhost:8083/order"
}
}
}
Currently the response of an API request looks like the following:
{
"creationDate": "2016-12-07T09:45:38.000+0000",
"items": [
{
"itemName": "Nintendo 2DS",
"description": "Konsole from Nintendo",
"price": 300.5,
"quantity": 3
},
{
"itemName": "Nintendo Classic",
"description": "Classic nintendo Console from the 80th...",
"price": 75,
"quantity": 2
}
],
"_links": {
"self": {
"href": "http://localhost:8082/carts/2"
},
"cart": {
"href": "http://localhost:8082/carts/2"
},
"checkout": {
"href": "http://localhost:8083/order"
}
}
}
Is there an Annotation that do this job or something other. I tried to add it in the CartResourceProcessor (org.springframework.hateoas.ResourceProcessor) but there is only the possibility to add additional links. Or do I need to add a Class value totalAmount?
Yes you can achieve that by annotating your method with Jackson #JsonProperty annotation
Code sample
#JsonProperty("totalAmount")
public double computeTotalAmount()
{
// compute totalAmout and return it
}
And to answer the possible next question you get after reading this. How the totalAmount is calculated. Here the snippet
public Class Cart{
// some Class values
#JsonProperty("totalAmount")
public BigDecimal total(){
return items.stream()
.map(Item::total)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
public class Item{
// some Item values
#JsonProperty("totalAmount")
public BigDecimal total(){
return price.multiply(new BigDecimal(this.quantity));
}
}
Outputs something similar to this:
{
"creationDate": "2016-12-07T09:45:38.000+0000",
"items": [
{
"itemName": "Nintendo 2DS",
"description": "Konsole from Nintendo",
"price": 300.5,
"quantity": 3,
"totalAmount": 901.5
},
{
"itemName": "Nintendo Classic",
"description": "Classic nintendo Console from the 80th...",
"price": 75,
"quantity": 2,
"totalAmount": 150
}
],
"totalAmount": 1051.5,
"_links": {
"self": {
"href": "http://localhost:8082/carts/2"
},
"cart": {
"href": "http://localhost:8082/carts/2"
},
"checkout": {
"href": "http://localhost:8083/order"
}
}
}
Hope it helps you :)