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": [
{