Skip to content

Commit

Permalink
#786 - Implement vendor neutral error handling with RFC-7807.
Browse files Browse the repository at this point in the history
  • Loading branch information
gregturn committed Dec 10, 2019
1 parent 4f3be11 commit 20bff3e
Show file tree
Hide file tree
Showing 17 changed files with 637 additions and 4 deletions.
13 changes: 11 additions & 2 deletions src/main/java/org/springframework/hateoas/MediaTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ public class MediaTypes {
* Public constant media type for {@code application/vnd.amundsen-uber+json}.
*/
public static final MediaType UBER_JSON = MediaType.parseMediaType(UBER_JSON_VALUE);



/**
* A String equivalent of {@link MediaTypes#VND_ERROR_JSON}.
*/
Expand All @@ -87,4 +86,14 @@ public class MediaTypes {
* Public constant media type for {@code application/vnd.error+json}.
*/
public static final MediaType VND_ERROR_JSON = MediaType.valueOf(VND_ERROR_JSON_VALUE);

/**
* A String equivalent of {@link MediaTypes#PROBLEM_JSON_VALUE}.
*/
public static final String PROBLEM_JSON_VALUE = "application/problem+json";

/**
* Public constant media type for {@code application/problem+json}.
*/
public static final MediaType PROBLEM_JSON = MediaType.parseMediaType(PROBLEM_JSON_VALUE);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright 2019 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.Objects;

import org.springframework.http.HttpStatus;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Encapsulation of an RFC-7807 {@literal Problem} code. While it complies out-of-the-box, it may also be extended to
* support domain-specific details.
*
* @author Greg Turnquist
*/
public class Problem<T extends Problem<? extends T>> {

private URI type;
private String title;
private HttpStatus status;
private String detail;
private URI instance;

public Problem() {
this(null, null, null, null, null);
}

public Problem(URI type, String title, HttpStatus status, String detail, URI instance) {

this.type = type;
this.title = title;
this.status = status;
this.detail = detail;
this.instance = instance;
}

@JsonCreator
public Problem(@JsonProperty("type") URI type, @JsonProperty("title") String title,
@JsonProperty("status") int status, @JsonProperty("detail") String detail,
@JsonProperty("instance") URI instance) {
this(type, title, HttpStatus.resolve(status), detail, instance);
}

/**
* A {@link Problem} that reflects an {@link HttpStatus} code.
*
* @see https://tools.ietf.org/html/rfc7807#section-4.2
*/
public Problem(HttpStatus httpStatus) {
this(URI.create("about:blank"), httpStatus.getReasonPhrase(), httpStatus, null, null);
}

@SuppressWarnings("unchecked")
public T withType(URI type) {
this.type = type;
return (T) this;
}

@SuppressWarnings("unchecked")
public T withTitle(String title) {
this.title = title;
return (T) this;
}

@SuppressWarnings("unchecked")
public T withStatus(HttpStatus status) {
this.status = status;
return (T) this;
}

@SuppressWarnings("unchecked")
public T withDetail(String detail) {
this.detail = detail;
return (T) this;
}

@SuppressWarnings("unchecked")
public T withInstance(URI instance) {
this.instance = instance;
return (T) this;
}

@JsonInclude(Include.NON_NULL)
public URI getType() {
return this.type;
}

@JsonInclude(Include.NON_NULL)
public String getTitle() {
return this.title;
}

@JsonInclude(Include.NON_NULL)
public Integer getStatus() {
if (status != null) {
return status.value();
}

return null;
}

@JsonInclude(Include.NON_NULL)
public String getDetail() {
return detail;
}

@JsonInclude(Include.NON_NULL)
public URI getInstance() {
return instance;
}

@Override
public boolean equals(Object o) {

if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Problem problem = (Problem) o;
return Objects.equals(type, problem.type) && //
Objects.equals(title, problem.title) && //
status == problem.status && //
Objects.equals(detail, problem.detail) && //
Objects.equals(instance, problem.instance); //
}

@Override
public int hashCode() {
return Objects.hash(type, title, status, detail, instance);
}

@Override
public String toString() {

return "Problem{" + //
"type=" + type + //
", title='" + title + '\'' + //
", status=" + status + //
", detail='" + detail + '\'' + //
", instance=" + instance + //
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Value objects to build Problem representations.
*/
@org.springframework.lang.NonNullApi
package org.springframework.hateoas.mediatype.problem;
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@
*/
package org.springframework.hateoas.mediatype.hal.forms;

import static org.assertj.core.api.Assertions.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;

import java.net.URI;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -31,9 +34,11 @@
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.config.WebClientConfigurer;
import org.springframework.hateoas.mediatype.problem.Problem;
import org.springframework.hateoas.support.MappingUtils;
import org.springframework.hateoas.support.WebFluxEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
Expand Down Expand Up @@ -128,6 +133,23 @@ void createNewEmployee() throws Exception {
.expectHeader().valueEquals(HttpHeaders.LOCATION, "http://localhost/employees/2");
}

@Test
void problemReturningControllerMethod() {

Problem<?> problem = this.testClient.get().uri("http://localhost/employees/problem").accept(MediaTypes.PROBLEM_JSON) //
.exchange() //
.expectStatus().isBadRequest() //
.expectHeader().contentType(MediaTypes.PROBLEM_JSON) //
.expectBody(Problem.class) //
.returnResult().getResponseBody();

assertThat(problem).isNotNull();
assertThat(problem.getType()).isEqualTo(URI.create("http://example.com/problem"));
assertThat(problem.getTitle()).isEqualTo("Employee-based problem");
assertThat(problem.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
assertThat(problem.getDetail()).isEqualTo("This is a test case");
}

@Configuration
@EnableWebFlux
@EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS })
Expand Down
Loading

0 comments on commit 20bff3e

Please sign in to comment.