diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java
index a49461a9..77e6810c 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/Connection.java
@@ -16,6 +16,7 @@ package org.shredzone.acme4j.connector;
import java.io.IOException;
import java.net.URI;
import java.security.cert.X509Certificate;
+import java.util.Collection;
import java.util.Date;
import java.util.Map;
@@ -85,7 +86,8 @@ public interface Connection extends AutoCloseable {
/**
* Gets a relation link from the header.
*
- * Relative links are resolved against the last request's URL.
+ * Relative links are resolved against the last request's URL. If there is more than
+ * one relation, the first one is returned.
*
* @param relation
* Link relation
@@ -93,6 +95,17 @@ public interface Connection extends AutoCloseable {
*/
URI getLink(String relation);
+ /**
+ * Gets one or more relation link from the header.
+ *
+ * Relative links are resolved against the last request's URL.
+ *
+ * @param relation
+ * Link relation
+ * @return Collection of links, or {@code null} if there was no such relation link
+ */
+ Collection getLinks(String relation);
+
/**
* Returns the moment returned in a "Retry-After" header.
*
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java
index 2947187a..54eca085 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java
@@ -27,6 +27,8 @@ import java.security.KeyPair;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -252,8 +254,24 @@ public class DefaultConnection implements Connection {
@Override
public URI getLink(String relation) {
+ Collection links = getLinks(relation);
+ if (links == null) {
+ return null;
+ }
+
+ if (links.size() > 1) {
+ LOG.debug("Link: {} - using the first of {}", relation, links.size());
+ }
+
+ return links.iterator().next();
+ }
+
+ @Override
+ public Collection getLinks(String relation) {
assertConnectionIsOpen();
+ List result = new ArrayList<>();
+
List links = conn.getHeaderFields().get("Link");
if (links != null) {
Pattern p = Pattern.compile("<(.*?)>\\s*;\\s*rel=\"?"+ Pattern.quote(relation) + "\"?");
@@ -262,12 +280,12 @@ public class DefaultConnection implements Connection {
if (m.matches()) {
String location = m.group(1);
LOG.debug("Link: {} -> {}", relation, location);
- return resolveRelative(location);
+ result.add(resolveRelative(location));
}
}
}
- return null;
+ return (!result.isEmpty() ? result : null);
}
@Override
@@ -324,7 +342,8 @@ public class DefaultConnection implements Connection {
case ACME_ERROR_PREFIX + "rateLimited":
case ACME_ERROR_PREFIX_DEPRECATED + "rateLimited":
- throw new AcmeRateLimitExceededException(type, detail, getRetryAfterHeader());
+ throw new AcmeRateLimitExceededException(
+ type, detail, getRetryAfterHeader(), getLinks("rate-limit"));
default:
throw new AcmeServerException(type, detail);
diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java
index d8a282f5..19102d45 100644
--- a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java
+++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeRateLimitExceededException.java
@@ -13,6 +13,9 @@
*/
package org.shredzone.acme4j.exception;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Date;
/**
@@ -22,6 +25,7 @@ public class AcmeRateLimitExceededException extends AcmeServerException {
private static final long serialVersionUID = 4150484059796413069L;
private final Date retryAfter;
+ private final Collection documents;
/**
* Creates a new {@link AcmeRateLimitExceededException}.
@@ -34,10 +38,13 @@ public class AcmeRateLimitExceededException extends AcmeServerException {
* @param retryAfter
* The moment the request is expected to succeed again, may be {@code null}
* if not known
+ * @param documents
+ * URIs pointing to documents about the rate limit that was hit
*/
- public AcmeRateLimitExceededException(String type, String detail, Date retryAfter) {
+ public AcmeRateLimitExceededException(String type, String detail, Date retryAfter, Collection documents) {
super(type, detail);
this.retryAfter = retryAfter;
+ this.documents = (documents != null ? Collections.unmodifiableCollection(documents) : null);
}
/**
@@ -48,4 +55,12 @@ public class AcmeRateLimitExceededException extends AcmeServerException {
return (retryAfter != null ? new Date(retryAfter.getTime()) : null);
}
+ /**
+ * Collection of URIs pointing to documents about the rate limit that was hit.
+ * {@code null} if the server did not provide such URIs.
+ */
+ public Collection getDocuments() {
+ return documents;
+ }
+
}
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java
index fe21b14d..8c4806da 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java
@@ -189,6 +189,30 @@ public class DefaultConnectionTest {
}
}
+ /**
+ * Test that multiple link headers are evaluated.
+ */
+ @Test
+ public void testGetMultiLink() {
+ Map> headers = new HashMap<>();
+ headers.put("Link", Arrays.asList(
+ "; rel=\"terms-of-service\"",
+ "; rel=\"terms-of-service\"",
+ "; rel=\"terms-of-service\""
+ ));
+
+ when(mockUrlConnection.getHeaderFields()).thenReturn(headers);
+
+ try (DefaultConnection conn = new DefaultConnection(mockHttpConnection)) {
+ conn.conn = mockUrlConnection;
+ assertThat(conn.getLinks("terms-of-service"), containsInAnyOrder(
+ URI.create("https://example.com/acme/terms1"),
+ URI.create("https://example.com/acme/terms2"),
+ URI.create("https://example.com/acme/terms3")
+ ));
+ }
+ }
+
/**
* Test that no Location header returns {@code null}.
*/
diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java
index 80a9f79e..e356e794 100644
--- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java
+++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DummyConnection.java
@@ -15,6 +15,7 @@ package org.shredzone.acme4j.connector;
import java.net.URI;
import java.security.cert.X509Certificate;
+import java.util.Collection;
import java.util.Date;
import java.util.Map;
@@ -63,6 +64,11 @@ public class DummyConnection implements Connection {
throw new UnsupportedOperationException();
}
+ @Override
+ public Collection getLinks(String relation) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public Date getRetryAfterHeader() {
throw new UnsupportedOperationException();