#786 - Added documentation for HTTP Problem Details media type support.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.mediatype.problem;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.support.MessageSourceAccessor;
|
||||
import org.springframework.hateoas.UriTemplate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
|
||||
@RequiredArgsConstructor
|
||||
// tag::header[]
|
||||
@RestController
|
||||
class PaymentController {
|
||||
// end::header[]
|
||||
|
||||
private static final URI OUT_OF_CREDIT_URI = null;
|
||||
private static final UriTemplate PAYMENT_ERROR_INSTANCE = UriTemplate.of("/incidents/{id}");
|
||||
private static final UriTemplate ACCOUNTS = UriTemplate.of("/accounts/{id}");
|
||||
|
||||
private final PaymentService payments;
|
||||
private final MessageSourceAccessor messages;
|
||||
// tag::method[]
|
||||
|
||||
@PutMapping
|
||||
ResponseEntity<?> issuePayment(@RequestBody PaymentRequest request) {
|
||||
|
||||
PaymentResult result = payments.issuePayment(request.orderId, request.amount);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
String title = messages.getMessage("payment.out-of-credit");
|
||||
String detail = messages.getMessage("payment.out-of-credit.details", //
|
||||
new Object[] { result.getBalance(), result.getCost() });
|
||||
|
||||
Problem problem = Problem.create() // <1>
|
||||
.withType(OUT_OF_CREDIT_URI) //
|
||||
.withTitle(title) // <2>
|
||||
.withDetail(detail) //
|
||||
.withInstance(PAYMENT_ERROR_INSTANCE.expand(result.getPaymentId())) //
|
||||
.withProperties(map -> { // <3>
|
||||
map.put("balance", result.getBalance());
|
||||
map.put("accounts", Arrays.asList( //
|
||||
ACCOUNTS.expand(result.getSourceAccountId()), //
|
||||
ACCOUNTS.expand(result.getTargetAccountId()) //
|
||||
));
|
||||
});
|
||||
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN) //
|
||||
.body(problem);
|
||||
}
|
||||
// end::method[]
|
||||
|
||||
ResponseEntity<?> issuePaymentAlternative() {
|
||||
|
||||
Problem problem = Problem.create();
|
||||
PaymentResult result = new PaymentResult();
|
||||
|
||||
// tag::alternative[]
|
||||
|
||||
class AccountDetails {
|
||||
int balance;
|
||||
List<URI> accounts;
|
||||
}
|
||||
|
||||
problem.withProperties(result.getDetails());
|
||||
|
||||
// or
|
||||
|
||||
Problem.create(result.getDetails());
|
||||
// end::alternative[]
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// tag::footer[]
|
||||
}
|
||||
// end::footer[]
|
||||
|
||||
class PaymentRequest {
|
||||
long orderId;
|
||||
int amount;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.mediatype.problem;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class PaymentResult {
|
||||
|
||||
UUID getPaymentId() {
|
||||
return UUID.randomUUID();
|
||||
}
|
||||
|
||||
boolean isSuccess() {
|
||||
return false;
|
||||
}
|
||||
|
||||
int getBalance() {
|
||||
return 30;
|
||||
}
|
||||
|
||||
UUID getSourceAccountId() {
|
||||
return UUID.randomUUID();
|
||||
}
|
||||
|
||||
UUID getTargetAccountId() {
|
||||
return UUID.randomUUID();
|
||||
}
|
||||
|
||||
AccountDetails getDetails() {
|
||||
return new AccountDetails();
|
||||
}
|
||||
|
||||
static class AccountDetails {
|
||||
int balance;
|
||||
List<URI> accounts;
|
||||
}
|
||||
|
||||
int getCost() {
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.hateoas.mediatype.problem;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
public interface PaymentService {
|
||||
|
||||
PaymentResult issuePayment(long orderId, int amount);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "https://example.com/probs/out-of-credit",
|
||||
"title": "You do not have enough credit.",
|
||||
"detail": "Your current balance is 30, but that costs 50.",
|
||||
"instance": "/account/12345/msgs/abc",
|
||||
"balance": 30,
|
||||
"accounts": ["/account/12345",
|
||||
"/account/67890"]
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
:resource-dir: ../../../src/docs/resources/org/springframework/hateoas
|
||||
:test-dir: ../../../src/test/java/org/springframework/hateoas
|
||||
:linkattrs:
|
||||
:tabsize: 2
|
||||
|
||||
[[mediatypes.hal]]
|
||||
== HAL – Hypertext Application Language
|
||||
@@ -23,7 +24,7 @@ For a given link relation that has two or more links, the spec is clear on repre
|
||||
|
||||
.HAL document with two links associated with one relation
|
||||
====
|
||||
[source, json, tabsize=2]
|
||||
[source, json]
|
||||
----
|
||||
include::{resource-dir}/docs/mediatype/hal/hal-multiple-entry-link-relation.json[]
|
||||
----
|
||||
@@ -36,7 +37,7 @@ By default, Spring HATEOAS uses the most terse approach and renders a single-lin
|
||||
|
||||
.HAL document with single link rendered as an object
|
||||
====
|
||||
[source, json, tabsize=2]
|
||||
[source, json]
|
||||
----
|
||||
include::{resource-dir}/docs/mediatype/hal/hal-single-entry-link-relation-object.json[]
|
||||
----
|
||||
@@ -46,7 +47,7 @@ Some users prefer to not switch between arrays and objects when consuming HAL. T
|
||||
|
||||
.HAL with single link rendered as an array
|
||||
====
|
||||
[source, json, tabsize=2]
|
||||
[source, json]
|
||||
----
|
||||
include::{resource-dir}/docs/mediatype/hal/hal-single-entry-link-relation-array.json[]
|
||||
----
|
||||
@@ -57,7 +58,7 @@ There are multiple choices.
|
||||
|
||||
.Global HAL single-link rendering policy
|
||||
====
|
||||
[source, java, indent=0, tabsize=2]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{code-dir}/SampleAppConfiguration.java[tag=1]
|
||||
----
|
||||
@@ -69,7 +70,7 @@ bean like this:
|
||||
|
||||
.Link relation-based HAL single-link rendering policy
|
||||
====
|
||||
[source, java, indent=0, tabsize=2]
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{code-dir}/SampleAppConfiguration.java[tag=2]
|
||||
----
|
||||
@@ -161,7 +162,7 @@ Note that now the `ex:` prefix automatically appears before all rel values that
|
||||
The following example shows how to do so:
|
||||
|
||||
====
|
||||
[source, json, tabsize=2]
|
||||
[source, json]
|
||||
----
|
||||
include::{resource-dir}/docs/mediatype/hal/hal-with-curies.json[]
|
||||
----
|
||||
@@ -184,7 +185,7 @@ To enable this media type, put the following configuration in your code:
|
||||
|
||||
.HAL-FORMS enabled application
|
||||
====
|
||||
[source, java, tabsize=2]
|
||||
[source, java]
|
||||
----
|
||||
include::{code-dir}/HalFormsApplication.java[tag=code]
|
||||
----
|
||||
@@ -194,7 +195,7 @@ Anytime a client supplies an `Accept` header with `application/prs.hal-forms+jso
|
||||
|
||||
.HAL-FORMS sample document
|
||||
====
|
||||
[source, json, tabsize=2]
|
||||
[source, json]
|
||||
----
|
||||
include::{resource-dir}/docs/mediatype/hal/forms/hal-forms-sample.json[]
|
||||
----
|
||||
@@ -312,6 +313,41 @@ A sample document with both template titles and property prompts defined would t
|
||||
----
|
||||
====
|
||||
|
||||
[[mediatypes.http-problem]]
|
||||
== HTTP Problem Details
|
||||
|
||||
https://tools.ietf.org/html/rfc7807[Problem Details for HTTP APIs] is a media type to carry machine-readable details of errors in a HTTP response to avoid the need to define new error response formats for HTTP APIs.
|
||||
|
||||
HTTP Problem Details defines a set of JSON properties that carry additional information to describe error details to HTTP clients.
|
||||
Find more details about those properties in particular in the relevant section of the https://tools.ietf.org/html/rfc7807#section-3.1[RFC document].
|
||||
|
||||
You can create such a JSON response by using the `Problem` media type domain type in your Spring MVC Controller:
|
||||
|
||||
.Reporting problem details using Spring HATEOAS' `Problem` type
|
||||
[source, java]
|
||||
----
|
||||
include::{code-dir}/mediatype/problem/PaymentController.java[tags=header;method;footer]
|
||||
----
|
||||
<1> You start by creating an instance of `Problem` using the factory methods exposed.
|
||||
<2> You can define the values for the default properties defined by the media type, e.g. the type URI, the title and details using internationalization features of Spring (see above).
|
||||
<3> Custom properties can be added via a `Map` or an explicit object (see below).
|
||||
|
||||
To use a dedicated object for custom properties, declare a type, create and populate an instance of it and hand this into the `Problem` instance either via `….withProperties(…)` or on instance creation via `Problem.create(…)`.
|
||||
|
||||
.Using a dedicated type to capture extended problem properties
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{code-dir}/mediatype/problem/PaymentController.java[tags=alternative]
|
||||
----
|
||||
|
||||
This will result in a response looking like this:
|
||||
|
||||
.A sample HTTP Problem Details response
|
||||
[source, java, indent=0]
|
||||
----
|
||||
include::{resource-dir}/docs/mediatype/problem/response.json[]
|
||||
----
|
||||
|
||||
[[mediatypes.collection-json]]
|
||||
== Collection+JSON
|
||||
|
||||
@@ -328,7 +364,7 @@ To enable this media type, put the following configuration in your code:
|
||||
|
||||
.Collection+JSON enabled application
|
||||
====
|
||||
[source, java, tabsize=2]
|
||||
[source, java]
|
||||
----
|
||||
include::{code-dir}/CollectionJsonApplication.java[tag=code]
|
||||
----
|
||||
@@ -341,7 +377,7 @@ The following example from the spec shows a single item:
|
||||
|
||||
.Collection+JSON single item example
|
||||
====
|
||||
[source, json, tabsize=2]
|
||||
[source, json]
|
||||
----
|
||||
include::{resource-dir}/docs/mediatype/collectionjson/spec-part3.json[]
|
||||
----
|
||||
@@ -387,7 +423,7 @@ UBER provides a uniform way to represent both single item resources as well as c
|
||||
|
||||
.UBER+JSON enabled application
|
||||
====
|
||||
[source, java, tabsize=2]
|
||||
[source, java]
|
||||
----
|
||||
include::{code-dir}/UberApplication.java[tag=code]
|
||||
----
|
||||
@@ -398,7 +434,7 @@ as show below:
|
||||
|
||||
.UBER sample document
|
||||
====
|
||||
[source, json, tabsize=2]
|
||||
[source, json]
|
||||
----
|
||||
include::{resource-dir}/docs/mediatype/uber/uber-sample.json[]
|
||||
----
|
||||
|
||||
Reference in New Issue
Block a user