Skip to content

Commit 20bff3e

Browse files
committed
#786 - Implement vendor neutral error handling with RFC-7807.
1 parent 4f3be11 commit 20bff3e

File tree

17 files changed

+637
-4
lines changed

17 files changed

+637
-4
lines changed

src/main/java/org/springframework/hateoas/MediaTypes.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ public class MediaTypes {
7676
* Public constant media type for {@code application/vnd.amundsen-uber+json}.
7777
*/
7878
public static final MediaType UBER_JSON = MediaType.parseMediaType(UBER_JSON_VALUE);
79-
80-
79+
8180
/**
8281
* A String equivalent of {@link MediaTypes#VND_ERROR_JSON}.
8382
*/
@@ -87,4 +86,14 @@ public class MediaTypes {
8786
* Public constant media type for {@code application/vnd.error+json}.
8887
*/
8988
public static final MediaType VND_ERROR_JSON = MediaType.valueOf(VND_ERROR_JSON_VALUE);
89+
90+
/**
91+
* A String equivalent of {@link MediaTypes#PROBLEM_JSON_VALUE}.
92+
*/
93+
public static final String PROBLEM_JSON_VALUE = "application/problem+json";
94+
95+
/**
96+
* Public constant media type for {@code application/problem+json}.
97+
*/
98+
public static final MediaType PROBLEM_JSON = MediaType.parseMediaType(PROBLEM_JSON_VALUE);
9099
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.mediatype.problem;
17+
18+
import java.net.URI;
19+
import java.util.Objects;
20+
21+
import org.springframework.http.HttpStatus;
22+
23+
import com.fasterxml.jackson.annotation.JsonCreator;
24+
import com.fasterxml.jackson.annotation.JsonInclude;
25+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
26+
import com.fasterxml.jackson.annotation.JsonProperty;
27+
28+
/**
29+
* Encapsulation of an RFC-7807 {@literal Problem} code. While it complies out-of-the-box, it may also be extended to
30+
* support domain-specific details.
31+
*
32+
* @author Greg Turnquist
33+
*/
34+
public class Problem<T extends Problem<? extends T>> {
35+
36+
private URI type;
37+
private String title;
38+
private HttpStatus status;
39+
private String detail;
40+
private URI instance;
41+
42+
public Problem() {
43+
this(null, null, null, null, null);
44+
}
45+
46+
public Problem(URI type, String title, HttpStatus status, String detail, URI instance) {
47+
48+
this.type = type;
49+
this.title = title;
50+
this.status = status;
51+
this.detail = detail;
52+
this.instance = instance;
53+
}
54+
55+
@JsonCreator
56+
public Problem(@JsonProperty("type") URI type, @JsonProperty("title") String title,
57+
@JsonProperty("status") int status, @JsonProperty("detail") String detail,
58+
@JsonProperty("instance") URI instance) {
59+
this(type, title, HttpStatus.resolve(status), detail, instance);
60+
}
61+
62+
/**
63+
* A {@link Problem} that reflects an {@link HttpStatus} code.
64+
*
65+
* @see https://tools.ietf.org/html/rfc7807#section-4.2
66+
*/
67+
public Problem(HttpStatus httpStatus) {
68+
this(URI.create("about:blank"), httpStatus.getReasonPhrase(), httpStatus, null, null);
69+
}
70+
71+
@SuppressWarnings("unchecked")
72+
public T withType(URI type) {
73+
this.type = type;
74+
return (T) this;
75+
}
76+
77+
@SuppressWarnings("unchecked")
78+
public T withTitle(String title) {
79+
this.title = title;
80+
return (T) this;
81+
}
82+
83+
@SuppressWarnings("unchecked")
84+
public T withStatus(HttpStatus status) {
85+
this.status = status;
86+
return (T) this;
87+
}
88+
89+
@SuppressWarnings("unchecked")
90+
public T withDetail(String detail) {
91+
this.detail = detail;
92+
return (T) this;
93+
}
94+
95+
@SuppressWarnings("unchecked")
96+
public T withInstance(URI instance) {
97+
this.instance = instance;
98+
return (T) this;
99+
}
100+
101+
@JsonInclude(Include.NON_NULL)
102+
public URI getType() {
103+
return this.type;
104+
}
105+
106+
@JsonInclude(Include.NON_NULL)
107+
public String getTitle() {
108+
return this.title;
109+
}
110+
111+
@JsonInclude(Include.NON_NULL)
112+
public Integer getStatus() {
113+
if (status != null) {
114+
return status.value();
115+
}
116+
117+
return null;
118+
}
119+
120+
@JsonInclude(Include.NON_NULL)
121+
public String getDetail() {
122+
return detail;
123+
}
124+
125+
@JsonInclude(Include.NON_NULL)
126+
public URI getInstance() {
127+
return instance;
128+
}
129+
130+
@Override
131+
public boolean equals(Object o) {
132+
133+
if (this == o)
134+
return true;
135+
if (o == null || getClass() != o.getClass())
136+
return false;
137+
Problem problem = (Problem) o;
138+
return Objects.equals(type, problem.type) && //
139+
Objects.equals(title, problem.title) && //
140+
status == problem.status && //
141+
Objects.equals(detail, problem.detail) && //
142+
Objects.equals(instance, problem.instance); //
143+
}
144+
145+
@Override
146+
public int hashCode() {
147+
return Objects.hash(type, title, status, detail, instance);
148+
}
149+
150+
@Override
151+
public String toString() {
152+
153+
return "Problem{" + //
154+
"type=" + type + //
155+
", title='" + title + '\'' + //
156+
", status=" + status + //
157+
", detail='" + detail + '\'' + //
158+
", instance=" + instance + //
159+
'}';
160+
}
161+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Value objects to build Problem representations.
3+
*/
4+
@org.springframework.lang.NonNullApi
5+
package org.springframework.hateoas.mediatype.problem;

src/test/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsWebFluxIntegrationTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
*/
1616
package org.springframework.hateoas.mediatype.hal.forms;
1717

18+
import static org.assertj.core.api.Assertions.*;
1819
import static org.hamcrest.CoreMatchers.*;
1920
import static org.hamcrest.collection.IsCollectionWithSize.*;
2021
import static org.springframework.hateoas.support.JsonPathUtils.*;
2122

23+
import java.net.URI;
24+
2225
import org.junit.jupiter.api.BeforeEach;
2326
import org.junit.jupiter.api.Test;
2427
import org.junit.jupiter.api.extension.ExtendWith;
@@ -31,9 +34,11 @@
3134
import org.springframework.hateoas.config.EnableHypermediaSupport;
3235
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
3336
import org.springframework.hateoas.config.WebClientConfigurer;
37+
import org.springframework.hateoas.mediatype.problem.Problem;
3438
import org.springframework.hateoas.support.MappingUtils;
3539
import org.springframework.hateoas.support.WebFluxEmployeeController;
3640
import org.springframework.http.HttpHeaders;
41+
import org.springframework.http.HttpStatus;
3742
import org.springframework.test.context.ContextConfiguration;
3843
import org.springframework.test.context.junit.jupiter.SpringExtension;
3944
import org.springframework.test.context.web.WebAppConfiguration;
@@ -128,6 +133,23 @@ void createNewEmployee() throws Exception {
128133
.expectHeader().valueEquals(HttpHeaders.LOCATION, "http://localhost/employees/2");
129134
}
130135

136+
@Test
137+
void problemReturningControllerMethod() {
138+
139+
Problem<?> problem = this.testClient.get().uri("http://localhost/employees/problem").accept(MediaTypes.PROBLEM_JSON) //
140+
.exchange() //
141+
.expectStatus().isBadRequest() //
142+
.expectHeader().contentType(MediaTypes.PROBLEM_JSON) //
143+
.expectBody(Problem.class) //
144+
.returnResult().getResponseBody();
145+
146+
assertThat(problem).isNotNull();
147+
assertThat(problem.getType()).isEqualTo(URI.create("http://example.com/problem"));
148+
assertThat(problem.getTitle()).isEqualTo("Employee-based problem");
149+
assertThat(problem.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
150+
assertThat(problem.getDetail()).isEqualTo("This is a test case");
151+
}
152+
131153
@Configuration
132154
@EnableWebFlux
133155
@EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS })

0 commit comments

Comments
 (0)