diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java index a4e9a0d9..8e79d0ae 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Problem.java @@ -62,7 +62,20 @@ public class Problem implements Serializable { } /** - * Returns a human-readable description of the problem. + * Returns a short, human-readable summary of the problem. The text may be localized + * if supported by the server. {@code null} if the server did not provide a title. + * + * @see #toString() + */ + public String getTitle() { + return problemJson.get("title").asString(); + } + + /** + * Returns a detailed and specific human-readable explanation of the problem. The + * text may be localized if supported by the server. + * + * @see #toString() */ public String getDetail() { return problemJson.get("detail").asString(); @@ -120,11 +133,42 @@ public class Problem implements Serializable { } /** - * Returns the problem as JSON string. + * Returns a human-readable description of the problem, that is as specific as + * possible. The description may be localized if supported by the server. + *

+ * If {@link #getSubProblems()} exist, they will be appended. + *

+ * Technically, it returns {@link #getDetail()}. If not set, {@link #getTitle()} is + * returned instead. As a last resort, {@link #getType()} is returned. */ @Override public String toString() { - return problemJson.toString(); + StringBuilder sb = new StringBuilder(); + + if (getDetail() != null) { + sb.append(getDetail()); + } else if (getTitle() != null) { + sb.append(getTitle()); + } else { + sb.append(getType()); + } + + List subproblems = getSubProblems(); + + if (!subproblems.isEmpty()) { + sb.append(" ("); + boolean first = true; + for (Problem sub : subproblems) { + if (!first) { + sb.append(" ‒ "); + } + sb.append(sub.toString()); + first = false; + } + sb.append(')'); + } + + return sb.toString(); } } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java index c36c227f..e299b692 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeServerException.java @@ -34,7 +34,7 @@ public class AcmeServerException extends AcmeException { * {@link Problem} that caused the exception */ public AcmeServerException(Problem problem) { - super(Objects.requireNonNull(problem).getDetail()); + super(Objects.requireNonNull(problem).toString()); this.problem = problem; } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java index 796c6c8a..62dad494 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/ProblemTest.java @@ -25,6 +25,7 @@ import java.util.List; import org.junit.Test; import org.shredzone.acme4j.toolbox.JSON; +import org.shredzone.acme4j.toolbox.JSONBuilder; import org.shredzone.acme4j.toolbox.TestUtils; /** @@ -40,11 +41,15 @@ public class ProblemTest { Problem problem = new Problem(original, baseUrl); assertThat(problem.getType(), is(URI.create("urn:ietf:params:acme:error:malformed"))); - assertThat(problem.getDetail(), is("Some of the identifiers requested were rejected")); + assertThat(problem.getTitle(), is("Some of the identifiers requested were rejected")); + assertThat(problem.getDetail(), is("Identifier \"abc12_\" is malformed")); assertThat(problem.getInstance(), is(URI.create("https://example.com/documents/error.html"))); assertThat(problem.getDomain(), is(nullValue())); assertThat(problem.asJSON().toString(), is(sameJSONAs(original.toString()))); - assertThat(problem.toString(), is(sameJSONAs(original.toString()))); + assertThat(problem.toString(), is( + "Identifier \"abc12_\" is malformed (" + + "Invalid underscore in DNS name \"_example.com\" ‒ " + + "This CA will not issue for \"example.net\")")); List subs = problem.getSubProblems(); assertThat(subs, not(nullValue())); @@ -52,13 +57,40 @@ public class ProblemTest { Problem p1 = subs.get(0); assertThat(p1.getType(), is(URI.create("urn:ietf:params:acme:error:malformed"))); + assertThat(p1.getTitle(), is(nullValue())); assertThat(p1.getDetail(), is("Invalid underscore in DNS name \"_example.com\"")); assertThat(p1.getDomain(), is("_example.com")); + assertThat(p1.toString(), is("Invalid underscore in DNS name \"_example.com\"")); Problem p2 = subs.get(1); assertThat(p2.getType(), is(URI.create("urn:ietf:params:acme:error:rejectedIdentifier"))); + assertThat(p2.getTitle(), is(nullValue())); assertThat(p2.getDetail(), is("This CA will not issue for \"example.net\"")); assertThat(p2.getDomain(), is("example.net")); + assertThat(p2.toString(), is("This CA will not issue for \"example.net\"")); + } + + /** + * Test that {@link Problem#toString()} always returns the most specific message. + */ + @Test + public void testToString() { + URL baseUrl = url("https://example.com/acme/1"); + URI typeUri = URI.create("urn:ietf:params:acme:error:malformed"); + + JSONBuilder jb = new JSONBuilder(); + + jb.put("type", typeUri); + Problem p1 = new Problem(jb.toJSON(), baseUrl); + assertThat(p1.toString(), is(typeUri.toString())); + + jb.put("title", "Some of the identifiers requested were rejected"); + Problem p2 = new Problem(jb.toJSON(), baseUrl); + assertThat(p2.toString(), is("Some of the identifiers requested were rejected")); + + jb.put("detail", "Identifier \"abc12_\" is malformed"); + Problem p3 = new Problem(jb.toJSON(), baseUrl); + assertThat(p3.toString(), is("Identifier \"abc12_\" is malformed")); } } diff --git a/acme4j-client/src/test/resources/json/problem.json b/acme4j-client/src/test/resources/json/problem.json index a680103b..2b3a18a7 100644 --- a/acme4j-client/src/test/resources/json/problem.json +++ b/acme4j-client/src/test/resources/json/problem.json @@ -1,6 +1,7 @@ { "type": "urn:ietf:params:acme:error:malformed", - "detail": "Some of the identifiers requested were rejected", + "title": "Some of the identifiers requested were rejected", + "detail": "Identifier \"abc12_\" is malformed", "instance": "/documents/error.html", "subproblems": [ {