From 1e4c10cef1618302f7f5fb0d6c28f3bc00d66f51 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 1 Dec 2022 15:47:40 +0100 Subject: [PATCH] Add equals/hashCode methods to ProblemDetail Closes gh-29606 --- .../springframework/http/ProblemDetail.java | 123 +++++++++++------- .../http/ProblemDetailTests.java | 53 ++++++++ 2 files changed, 128 insertions(+), 48 deletions(-) create mode 100644 spring-web/src/test/java/org/springframework/http/ProblemDetailTests.java diff --git a/spring-web/src/main/java/org/springframework/http/ProblemDetail.java b/spring-web/src/main/java/org/springframework/http/ProblemDetail.java index 3e45dfb10e..e6e6ea9647 100644 --- a/spring-web/src/main/java/org/springframework/http/ProblemDetail.java +++ b/spring-web/src/main/java/org/springframework/http/ProblemDetail.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * Representation for an RFC 7807 problem detail. Includes spec-defined @@ -40,8 +41,8 @@ import org.springframework.util.Assert; * {@link org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler}. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 6.0 - * * @see RFC 7807 * @see org.springframework.web.ErrorResponse * @see org.springframework.web.ErrorResponseException @@ -108,6 +109,13 @@ public class ProblemDetail { this.type = type; } + /** + * Return the configured {@link #setType(URI) problem type}. + */ + public URI getType() { + return this.type; + } + /** * Setter for the {@link #getTitle() problem title}. *

By default, if not explicitly set and the status is well-known, this @@ -118,6 +126,20 @@ public class ProblemDetail { this.title = title; } + /** + * Return the configured {@link #setTitle(String) problem title}. + */ + @Nullable + public String getTitle() { + if (this.title == null) { + HttpStatus httpStatus = HttpStatus.resolve(this.status); + if (httpStatus != null) { + return httpStatus.getReasonPhrase(); + } + } + return this.title; + } + /** * Setter for the {@link #getStatus() problem status}. * @param httpStatus the problem status @@ -134,6 +156,14 @@ public class ProblemDetail { this.status = status; } + /** + * Return the status associated with the problem, provided either to the + * constructor or configured via {@link #setStatus(int)}. + */ + public int getStatus() { + return this.status; + } + /** * Setter for the {@link #getDetail() problem detail}. *

By default, this is not set. @@ -143,6 +173,14 @@ public class ProblemDetail { this.detail = detail; } + /** + * Return the configured {@link #setDetail(String) problem detail}. + */ + @Nullable + public String getDetail() { + return this.detail; + } + /** * Setter for the {@link #getInstance() problem instance}. *

By default, when {@code ProblemDetail} is returned from an @@ -153,6 +191,14 @@ public class ProblemDetail { this.instance = instance; } + /** + * Return the configured {@link #setInstance(URI) problem instance}. + */ + @Nullable + public URI getInstance() { + return this.instance; + } + /** * Set a "dynamic" property to be added to a generic {@link #getProperties() * properties map}. @@ -168,52 +214,6 @@ public class ProblemDetail { this.properties.put(name, value); } - - /** - * Return the configured {@link #setType(URI) problem type}. - */ - public URI getType() { - return this.type; - } - - /** - * Return the configured {@link #setTitle(String) problem title}. - */ - @Nullable - public String getTitle() { - if (this.title == null) { - HttpStatus httpStatus = HttpStatus.resolve(this.status); - if (httpStatus != null) { - return httpStatus.getReasonPhrase(); - } - } - return this.title; - } - - /** - * Return the status associated with the problem, provided either to the - * constructor or configured via {@link #setStatus(int)}. - */ - public int getStatus() { - return this.status; - } - - /** - * Return the configured {@link #setDetail(String) problem detail}. - */ - @Nullable - public String getDetail() { - return this.detail; - } - - /** - * Return the configured {@link #setInstance(URI) problem instance}. - */ - @Nullable - public URI getInstance() { - return this.instance; - } - /** * Return a generic map of properties that are not known ahead of time, * possibly {@code null} if no properties have been added. To add a property, @@ -229,6 +229,33 @@ public class ProblemDetail { } + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof ProblemDetail otherDetail)) { + return false; + } + return (this.type.equals(otherDetail.type) && + ObjectUtils.nullSafeEquals(this.title, otherDetail.title) && + this.status == otherDetail.status && + ObjectUtils.nullSafeEquals(this.detail, otherDetail.detail) && + ObjectUtils.nullSafeEquals(this.instance, otherDetail.instance) && + ObjectUtils.nullSafeEquals(this.properties, otherDetail.properties)); + } + + @Override + public int hashCode() { + int result = this.type.hashCode(); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.title); + result = 31 * result + this.status; + result = 31 * result + ObjectUtils.nullSafeHashCode(this.detail); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.instance); + result = 31 * result + ObjectUtils.nullSafeHashCode(this.properties); + return result; + } + @Override public String toString() { return getClass().getSimpleName() + "[" + initToStringContent() + "]"; @@ -239,7 +266,7 @@ public class ProblemDetail { * Subclasses can override this to append additional fields. */ protected String initToStringContent() { - return "type='" + this.type + "'" + + return "type='" + getType() + "'" + ", title='" + getTitle() + "'" + ", status=" + getStatus() + ", detail='" + getDetail() + "'" + diff --git a/spring-web/src/test/java/org/springframework/http/ProblemDetailTests.java b/spring-web/src/test/java/org/springframework/http/ProblemDetailTests.java new file mode 100644 index 0000000000..4b94d9786d --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/ProblemDetailTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2022 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.http; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ProblemDetail}. + * + * @author Juergen Hoeller + */ +class ProblemDetailTests { + + @Test + void equalsAndHashCode() { + ProblemDetail pd1 = ProblemDetail.forStatus(500); + ProblemDetail pd2 = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR); + ProblemDetail pd3 = ProblemDetail.forStatus(HttpStatus.NOT_FOUND); + ProblemDetail pd4 = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "some detail"); + + assertThat(pd1).isEqualTo(pd2); + assertThat(pd2).isEqualTo(pd1); + assertThat(pd1.hashCode()).isEqualTo(pd2.hashCode()); + + assertThat(pd3).isNotEqualTo(pd4); + assertThat(pd4).isNotEqualTo(pd3); + assertThat(pd3.hashCode()).isNotEqualTo(pd4.hashCode()); + + assertThat(pd1).isNotEqualTo(pd3); + assertThat(pd1).isNotEqualTo(pd4); + assertThat(pd2).isNotEqualTo(pd3); + assertThat(pd2).isNotEqualTo(pd4); + assertThat(pd1.hashCode()).isNotEqualTo(pd3.hashCode()); + assertThat(pd1.hashCode()).isNotEqualTo(pd4.hashCode()); + } + +}