diff --git a/src/docs/java/org/springframework/hateoas/mediatype/problem/PaymentController.java b/src/docs/java/org/springframework/hateoas/mediatype/problem/PaymentController.java new file mode 100644 index 00000000..4cba0bcb --- /dev/null +++ b/src/docs/java/org/springframework/hateoas/mediatype/problem/PaymentController.java @@ -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 accounts; + } + + problem.withProperties(result.getDetails()); + + // or + + Problem.create(result.getDetails()); + // end::alternative[] + + return null; + } + + // tag::footer[] +} +// end::footer[] + +class PaymentRequest { + long orderId; + int amount; +} diff --git a/src/docs/java/org/springframework/hateoas/mediatype/problem/PaymentResult.java b/src/docs/java/org/springframework/hateoas/mediatype/problem/PaymentResult.java new file mode 100644 index 00000000..3a318001 --- /dev/null +++ b/src/docs/java/org/springframework/hateoas/mediatype/problem/PaymentResult.java @@ -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 accounts; + } + + int getCost() { + return 50; + } +} diff --git a/src/docs/java/org/springframework/hateoas/mediatype/problem/PaymentService.java b/src/docs/java/org/springframework/hateoas/mediatype/problem/PaymentService.java new file mode 100644 index 00000000..539c6710 --- /dev/null +++ b/src/docs/java/org/springframework/hateoas/mediatype/problem/PaymentService.java @@ -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); +} diff --git a/src/docs/resources/org/springframework/hateoas/docs/mediatype/problem/response.json b/src/docs/resources/org/springframework/hateoas/docs/mediatype/problem/response.json new file mode 100644 index 00000000..232bd5b8 --- /dev/null +++ b/src/docs/resources/org/springframework/hateoas/docs/mediatype/problem/response.json @@ -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"] +} diff --git a/src/main/asciidoc/mediatypes.adoc b/src/main/asciidoc/mediatypes.adoc index 5bae8f3f..9daf7da7 100644 --- a/src/main/asciidoc/mediatypes.adoc +++ b/src/main/asciidoc/mediatypes.adoc @@ -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[] ----