Skip to content

Commit

Permalink
#786 - Added documentation for HTTP Problem Details media type support.
Browse files Browse the repository at this point in the history
  • Loading branch information
odrotbohm committed Jan 13, 2020
1 parent 1764446 commit 37896bc
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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"]
}
60 changes: 48 additions & 12 deletions src/main/asciidoc/mediatypes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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[]
----
Expand All @@ -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[]
----
Expand All @@ -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[]
----
Expand All @@ -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]
----
Expand All @@ -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]
----
Expand Down Expand Up @@ -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[]
----
Expand All @@ -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]
----
Expand All @@ -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[]
----
Expand Down Expand Up @@ -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

Expand All @@ -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]
----
Expand All @@ -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[]
----
Expand Down Expand Up @@ -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]
----
Expand All @@ -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[]
----
Expand Down

0 comments on commit 37896bc

Please sign in to comment.