Refactor ACME resource classes

A new AcmeJsonResource takes care of fetching and keeping the resource state.
A lot of boilerplate code could be removed that way.
pull/55/head
Richard Körber 2018-01-14 14:07:25 +01:00
parent 816f0825c0
commit 252d886b3f
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
22 changed files with 423 additions and 363 deletions

View File

@ -13,6 +13,7 @@
*/
package org.shredzone.acme4j;
import static java.util.stream.Collectors.toList;
import static org.shredzone.acme4j.toolbox.AcmeUtils.*;
import java.net.HttpURLConnection;
@ -33,11 +34,10 @@ import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.connector.ResourceIterator;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.exception.AcmeServerException;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSON.Value;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -45,7 +45,7 @@ import org.slf4j.LoggerFactory;
/**
* Represents an account at the ACME server.
*/
public class Account extends AcmeResource {
public class Account extends AcmeJsonResource {
private static final long serialVersionUID = 7042863483428051319L;
private static final Logger LOG = LoggerFactory.getLogger(Account.class);
@ -54,12 +54,6 @@ public class Account extends AcmeResource {
private static final String KEY_CONTACT = "contact";
private static final String KEY_STATUS = "status";
private final List<URI> contacts = new ArrayList<>();
private Status status;
private Boolean termsOfServiceAgreed;
private URL orders;
private boolean loaded = false;
protected Account(Session session, URL location) {
super(session);
setLocation(location);
@ -86,24 +80,25 @@ public class Account extends AcmeResource {
* {@code null} if the server did not provide such an information.
*/
public Boolean getTermsOfServiceAgreed() {
load();
return termsOfServiceAgreed;
return getJSON().get(KEY_TOS_AGREED).optional().map(Value::asBoolean).orElse(null);
}
/**
* List of contact addresses (emails, phone numbers etc).
*/
public List<URI> getContacts() {
load();
return Collections.unmodifiableList(contacts);
return Collections.unmodifiableList(getJSON().get(KEY_CONTACT)
.asArray()
.stream()
.map(JSON.Value::asURI)
.collect(toList()));
}
/**
* Returns the current status of the account.
*/
public Status getStatus() {
load();
return status;
return getJSON().get(KEY_STATUS).asStatusOrElse(Status.UNKNOWN);
}
/**
@ -117,23 +112,17 @@ public class Account extends AcmeResource {
* fetched from the server.
*/
public Iterator<Order> getOrders() throws AcmeException {
LOG.debug("getOrders");
load();
return new ResourceIterator<>(getSession(), KEY_ORDERS, orders, Order::bind);
URL ordersUrl = getJSON().get(KEY_ORDERS).asURL();
return new ResourceIterator<>(getSession(), KEY_ORDERS, ordersUrl, Order::bind);
}
/**
* Updates the account to the current account status.
*/
@Override
public void update() throws AcmeException {
LOG.debug("update");
LOG.debug("update Account");
try (Connection conn = getSession().provider().connect()) {
JSONBuilder claims = new JSONBuilder();
conn.sendSignedRequest(getLocation(), claims, getSession());
unmarshal(conn.readJsonResponse());
}
conn.sendSignedRequest(getLocation(), new JSONBuilder(), getSession());
setJSON(conn.readJsonResponse());
}
}
/**
@ -181,10 +170,8 @@ public class Account extends AcmeResource {
conn.sendSignedRequest(newAuthzUrl, claims, getSession(), HttpURLConnection.HTTP_CREATED);
JSON json = conn.readJsonResponse();
Authorization auth = new Authorization(getSession(), conn.getLocation());
auth.unmarshalAuthorization(json);
auth.setJSON(conn.readJsonResponse());
return auth;
}
}
@ -250,53 +237,10 @@ public class Account extends AcmeResource {
conn.sendSignedRequest(getLocation(), claims, getSession());
unmarshal(conn.readJsonResponse());
setJSON(conn.readJsonResponse());
}
}
/**
* Lazily updates the object's state when one of the getters is invoked.
*/
protected void load() {
if (!loaded) {
try {
update();
} catch (AcmeRetryAfterException ex) {
// ignore... The object was still updated.
LOG.debug("Retry-After", ex);
} catch (AcmeException ex) {
throw new AcmeLazyLoadingException(this, ex);
}
}
}
/**
* Sets account properties according to the given JSON data.
*
* @param json
* JSON data
*/
protected void unmarshal(JSON json) {
if (json.contains(KEY_TOS_AGREED)) {
this.termsOfServiceAgreed = json.get(KEY_TOS_AGREED).asBoolean();
}
if (json.contains(KEY_CONTACT)) {
contacts.clear();
json.get(KEY_CONTACT).asArray().stream()
.map(JSON.Value::asURI)
.forEach(contacts::add);
}
this.orders = json.get(KEY_ORDERS).asURL();
if (json.contains(KEY_STATUS)) {
this.status = Status.parse(json.get(KEY_STATUS).asString());
}
loaded = true;
}
/**
* Modifies the account data of the account.
*
@ -313,7 +257,7 @@ public class Account extends AcmeResource {
private final List<URI> editContacts = new ArrayList<>();
private EditableAccount() {
editContacts.addAll(Account.this.contacts);
editContacts.addAll(Account.this.getContacts());
}
/**
@ -363,8 +307,7 @@ public class Account extends AcmeResource {
conn.sendSignedRequest(getLocation(), claims, getSession());
JSON json = conn.readJsonResponse();
unmarshal(json);
setJSON(conn.readJsonResponse());
}
}
}

View File

@ -187,7 +187,7 @@ public class AccountBuilder {
}
if (resp == HttpURLConnection.HTTP_CREATED) {
account.unmarshal(conn.readJsonResponse());
account.setJSON(conn.readJsonResponse());
}
return account;
}

View File

@ -0,0 +1,132 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2018 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j;
import java.util.Objects;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.toolbox.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An ACME resource that stores its state in a JSON structure.
*/
public abstract class AcmeJsonResource extends AcmeResource {
private static final long serialVersionUID = -5060364275766082345L;
private static final Logger LOG = LoggerFactory.getLogger(AcmeJsonResource.class);
private JSON data = null;
/**
* Create a new {@link AcmeJsonResource}.
*
* @param session
* {@link Session} the resource is bound with
*/
protected AcmeJsonResource(Session session) {
super(session);
}
/**
* Create a new {@link AcmeJsonResource} and use the given data.
*
* @param session
* {@link Session} the resource is bound with
* @param data
* Initial {@link JSON} data
*/
protected AcmeJsonResource(Session session, JSON data) {
super(session);
setJSON(data);
}
/**
* Returns the JSON representation of the resource data.
* <p>
* If there is no data, {@link #update()} is invoked to fetch it from the server.
* <p>
* This method can be used to read proprietary data from the resources.
*
* @return Resource data, as {@link JSON}.
*/
public JSON getJSON() {
if (data == null) {
try {
update();
} catch (AcmeRetryAfterException ex) {
// ignore... The object was still updated.
LOG.debug("Retry-After", ex);
} catch (AcmeException ex) {
throw new AcmeLazyLoadingException(this, ex);
}
}
return data;
}
/**
* Sets the JSON representation of the resource data.
*
* @param data
* New {@link JSON} data, must not be {@code null}.
*/
protected void setJSON(JSON data) {
this.data = Objects.requireNonNull(data, "data");
}
/**
* Checks if this resource is valid.
*
* @return {@code true} if the resource state has been loaded from the server. If
* {@code false}, {@link #getJSON()} would implicitly call {@link #update()}
* to fetch the current state from the server.
*/
protected boolean isValid() {
return data != null;
}
/**
* Invalidates the state of this resource. Enforces an {@link #update()} when
* {@link #getJSON()} is invoked.
*/
protected void invalidate() {
data = null;
}
/**
* Updates this resource, by fetching the current resource data from the server.
*
* @throws AcmeException
* if the resource could not be fetched.
* @throws AcmeRetryAfterException
* the resource is still being processed, and the server returned an
* estimated date when the process will be completed. If you are polling
* for the resource to complete, you should wait for the date given in
* {@link AcmeRetryAfterException#getRetryAfter()}. Note that the status
* of the resource is updated even if this exception was thrown.
*/
public void update() throws AcmeException {
String resourceType = getClass().getSimpleName();
LOG.debug("update {}", resourceType);
try (Connection conn = getSession().provider().connect()) {
conn.sendRequest(getLocation(), getSession());
setJSON(conn.readJsonResponse());
conn.handleRetryAfter(resourceType + " is not completed yet");
}
}
}

View File

@ -14,7 +14,6 @@
package org.shredzone.acme4j;
import static java.util.stream.Collectors.toList;
import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp;
import java.net.URL;
import java.time.Instant;
@ -24,10 +23,10 @@ import java.util.List;
import org.shredzone.acme4j.challenge.Challenge;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.toolbox.AcmeUtils;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSON.Value;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -35,16 +34,10 @@ import org.slf4j.LoggerFactory;
/**
* Represents an authorization request at the ACME server.
*/
public class Authorization extends AcmeResource {
public class Authorization extends AcmeJsonResource {
private static final long serialVersionUID = -3116928998379417741L;
private static final Logger LOG = LoggerFactory.getLogger(Authorization.class);
private String domain;
private Status status;
private Instant expires;
private List<Challenge> challenges;
private boolean loaded = false;
protected Authorization(Session session, URL location) {
super(session);
setLocation(location);
@ -68,32 +61,43 @@ public class Authorization extends AcmeResource {
* Gets the domain name to be authorized.
*/
public String getDomain() {
load();
return domain;
JSON jsonIdentifier = getJSON().get("identifier").required().asObject();
String type = jsonIdentifier.get("type").required().asString();
if (!"dns".equals(type)) {
throw new AcmeProtocolException("Unknown authorization type: " + type);
}
return jsonIdentifier.get("value").required().asString();
}
/**
* Gets the authorization status.
*/
public Status getStatus() {
load();
return status;
return getJSON().get("status").asStatusOrElse(Status.PENDING);
}
/**
* Gets the expiry date of the authorization, if set by the server.
*/
public Instant getExpires() {
load();
return expires;
return getJSON().get("expires").optional()
.map(Value::asString)
.map(AcmeUtils::parseTimestamp)
.orElse(null);
}
/**
* Gets a list of all challenges offered by the server.
*/
public List<Challenge> getChallenges() {
load();
return challenges;
Session session = getSession();
return Collections.unmodifiableList(getJSON().get("challenges")
.asArray()
.stream()
.map(Value::asObject)
.map(session::createChallenge)
.collect(toList()));
}
/**
@ -115,28 +119,6 @@ public class Authorization extends AcmeResource {
.orElse(null);
}
/**
* Updates the {@link Authorization}. After invocation, the {@link Authorization}
* reflects the current state at the ACME server.
*
* @throws AcmeRetryAfterException
* the auhtorization is still being validated, and the server returned an
* estimated date when the validation will be completed. If you are
* polling for the authorization to complete, you should wait for the date
* given in {@link AcmeRetryAfterException#getRetryAfter()}. Note that the
* authorization status is updated even if this exception was thrown.
*/
public void update() throws AcmeException {
LOG.debug("update");
try (Connection conn = getSession().provider().connect()) {
conn.sendRequest(getLocation(), getSession());
unmarshalAuthorization(conn.readJsonResponse());
conn.handleRetryAfter("authorization is not completed yet");
}
}
/**
* Permanently deactivates the {@link Authorization}.
*/
@ -148,68 +130,8 @@ public class Authorization extends AcmeResource {
conn.sendSignedRequest(getLocation(), claims, getSession());
unmarshalAuthorization(conn.readJsonResponse());
setJSON(conn.readJsonResponse());
}
}
/**
* Lazily updates the object's state when one of the getters is invoked.
*/
protected void load() {
if (!loaded) {
try {
update();
} catch (AcmeRetryAfterException ex) {
// ignore... The object was still updated.
LOG.debug("Retry-After", ex);
} catch (AcmeException ex) {
throw new AcmeLazyLoadingException(this, ex);
}
}
}
/**
* Sets the properties according to the given JSON data.
*
* @param json
* JSON data
*/
protected void unmarshalAuthorization(JSON json) {
this.status = json.get("status").asStatusOrElse(Status.PENDING);
String jsonExpires = json.get("expires").asString();
if (jsonExpires != null) {
expires = parseTimestamp(jsonExpires);
}
JSON jsonIdentifier = json.get("identifier").asObject();
if (jsonIdentifier != null) {
String type = jsonIdentifier.get("type").asString();
if (type != null && !"dns".equals(type)) {
throw new AcmeProtocolException("Unknown authorization type: " + type);
}
domain = jsonIdentifier.get("value").asString();
}
challenges = fetchChallenges(json);
loaded = true;
}
/**
* Fetches all {@link Challenge} that are defined in the JSON.
*
* @param json
* {@link JSON} to read
* @return List of {@link Challenge}
*/
private List<Challenge> fetchChallenges(JSON json) {
Session session = getSession();
return Collections.unmodifiableList(json.get("challenges").asArray().stream()
.map(JSON.Value::asObject)
.map(session::createChallenge)
.collect(toList()));
}
}

View File

@ -17,12 +17,12 @@ import static java.util.stream.Collectors.toList;
import java.net.URL;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeLazyLoadingException;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSON.Value;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -30,21 +30,10 @@ import org.slf4j.LoggerFactory;
/**
* Represents a certificate order.
*/
public class Order extends AcmeResource {
public class Order extends AcmeJsonResource {
private static final long serialVersionUID = 5435808648658292177L;
private static final Logger LOG = LoggerFactory.getLogger(Order.class);
private Status status;
private Instant expires;
private List<String> identifiers;
private Instant notBefore;
private Instant notAfter;
private Problem error;
private List<URL> authorizations;
private URL finalizeUrl;
private Certificate certificate;
private boolean loaded = false;
protected Order(Session session, URL location) {
super(session);
setLocation(location);
@ -67,58 +56,60 @@ public class Order extends AcmeResource {
* Returns the current status of the order.
*/
public Status getStatus() {
load();
return status;
return getJSON().get("status").asStatusOrElse(Status.UNKNOWN);
}
/**
* Returns a {@link Problem} document if the order failed.
*/
public Problem getError() {
load();
return error;
return getJSON().get("error").asProblem(getLocation());
}
/**
* Gets the expiry date of the authorization, if set by the server.
*/
public Instant getExpires() {
load();
return expires;
return getJSON().get("expires").asInstant();
}
/**
* Gets the list of domain names to be ordered.
*/
public List<String> getDomains() {
return identifiers;
return Collections.unmodifiableList(getJSON().get("identifiers")
.asArray()
.stream()
.map(Value::asObject)
.map(it -> it.get("value").asString())
.collect(toList()));
}
/**
* Gets the "not before" date that was used for the order, or {@code null}.
*/
public Instant getNotBefore() {
load();
return notBefore;
return getJSON().get("notBefore").asInstant();
}
/**
* Gets the "not after" date that was used for the order, or {@code null}.
*/
public Instant getNotAfter() {
load();
return notAfter;
return getJSON().get("notAfter").asInstant();
}
/**
* Gets the {@link Authorization} required for this order.
*/
public List<Authorization> getAuthorizations() {
load();
Session session = getSession();
return authorizations.stream()
.map(loc -> Authorization.bind(session, loc))
.collect(toList());
return Collections.unmodifiableList(getJSON().get("authorizations")
.asArray()
.stream()
.map(Value::asURL)
.map(loc -> Authorization.bind(session, loc))
.collect(toList()));
}
/**
@ -127,8 +118,17 @@ public class Order extends AcmeResource {
* For internal purposes. Use {@link #execute(byte[])} to finalize an order.
*/
public URL getFinalizeLocation() {
load();
return finalizeUrl;
return getJSON().get("finalize").asURL();
}
/**
* Gets the {@link Certificate} if it is available. {@code null} otherwise.
*/
public Certificate getCertificate() {
return getJSON().get("certificate").optional()
.map(Value::asURL)
.map(certUrl -> Certificate.bind(getSession(), certUrl))
.orElse(null);
}
/**
@ -153,71 +153,7 @@ public class Order extends AcmeResource {
conn.sendSignedRequest(getFinalizeLocation(), claims, getSession());
}
loaded = false; // invalidate this object
}
/**
* Gets the {@link Certificate} if it is available. {@code null} otherwise.
*/
public Certificate getCertificate() {
load();
return certificate;
}
/**
* Updates the order to the current account status.
*/
public void update() throws AcmeException {
LOG.debug("update");
try (Connection conn = getSession().provider().connect()) {
conn.sendRequest(getLocation(), getSession());
JSON json = conn.readJsonResponse();
unmarshal(json);
}
}
/**
* Lazily updates the object's state when one of the getters is invoked.
*/
protected void load() {
if (!loaded) {
try {
update();
} catch (AcmeException ex) {
throw new AcmeLazyLoadingException(this, ex);
}
}
}
/**
* Sets order properties according to the given JSON data.
*
* @param json
* JSON data
*/
public void unmarshal(JSON json) {
this.status = json.get("status").asStatusOrElse(Status.UNKNOWN);
this.expires = json.get("expires").asInstant();
this.notBefore = json.get("notBefore").asInstant();
this.notAfter = json.get("notAfter").asInstant();
this.finalizeUrl = json.get("finalize").asURL();
URL certUrl = json.get("certificate").asURL();
certificate = certUrl != null ? Certificate.bind(getSession(), certUrl) : null;
this.error = json.get("error").asProblem(getLocation());
this.identifiers = json.get("identifiers").asArray().stream()
.map(JSON.Value::asObject)
.map(it -> it.get("value").asString())
.collect(toList());
this.authorizations = json.get("authorizations").asArray().stream()
.map(JSON.Value::asURL)
.collect(toList());
loaded = true;
invalidate();
}
}

View File

@ -26,7 +26,6 @@ import java.util.Set;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -150,10 +149,8 @@ public class OrderBuilder {
conn.sendSignedRequest(session.resourceUrl(Resource.NEW_ORDER), claims, session, HttpURLConnection.HTTP_CREATED);
JSON json = conn.readJsonResponse();
Order order = new Order(session, conn.getLocation());
order.unmarshal(json);
order.setJSON(conn.readJsonResponse());
return order;
}
}

View File

@ -186,7 +186,7 @@ public class Session {
challenge = new Challenge(this);
}
}
challenge.unmarshall(data);
challenge.setJSON(data);
return challenge;
}

View File

@ -21,14 +21,13 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.shredzone.acme4j.AcmeResource;
import org.shredzone.acme4j.AcmeJsonResource;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Session;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.exception.AcmeRetryAfterException;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSON.Array;
import org.shredzone.acme4j.toolbox.JSONBuilder;
@ -44,7 +43,7 @@ import org.slf4j.LoggerFactory;
* own type. {@link Challenge#respond(JSONBuilder)} should be overridden to put all
* required data to the response.
*/
public class Challenge extends AcmeResource {
public class Challenge extends AcmeJsonResource {
private static final long serialVersionUID = 2338794776848388099L;
private static final Logger LOG = LoggerFactory.getLogger(Challenge.class);
@ -54,8 +53,6 @@ public class Challenge extends AcmeResource {
protected static final String KEY_VALIDATED = "validated";
protected static final String KEY_ERRORS = "errors";
private JSON data = JSON.empty();
/**
* Creates a new generic {@link Challenge} object.
*
@ -97,29 +94,21 @@ public class Challenge extends AcmeResource {
* Returns the challenge type by name (e.g. "http-01").
*/
public String getType() {
return data.get(KEY_TYPE).asString();
}
/**
* Returns the location {@link URL} of the challenge.
*/
@Override
public URL getLocation() {
return data.get(KEY_URL).asURL();
return getJSON().get(KEY_TYPE).asString();
}
/**
* Returns the current status of the challenge.
*/
public Status getStatus() {
return data.get(KEY_STATUS).asStatusOrElse(Status.UNKNOWN);
return getJSON().get(KEY_STATUS).asStatusOrElse(Status.UNKNOWN);
}
/**
* Returns the validation date, if returned by the server.
*/
public Instant getValidated() {
return data.get(KEY_VALIDATED).asInstant();
return getJSON().get(KEY_VALIDATED).asInstant();
}
/**
@ -128,9 +117,11 @@ public class Challenge extends AcmeResource {
*/
public List<Problem> getErrors() {
URL location = getLocation();
return Collections.unmodifiableList(data.get(KEY_ERRORS).asArray().stream()
.map(it -> it.asProblem(location))
.collect(toList()));
return Collections.unmodifiableList(getJSON().get(KEY_ERRORS)
.asArray()
.stream()
.map(it -> it.asProblem(location))
.collect(toList()));
}
/**
@ -138,7 +129,7 @@ public class Challenge extends AcmeResource {
* {@code null} if there are no errors.
*/
public Problem getLastError() {
Array errors = data.get(KEY_ERRORS).asArray();
Array errors = getJSON().get(KEY_ERRORS).asArray();
if (!errors.isEmpty()) {
return errors.get(errors.size() - 1).asProblem(getLocation());
} else {
@ -146,13 +137,6 @@ public class Challenge extends AcmeResource {
}
}
/**
* Returns the JSON representation of the challenge data.
*/
protected JSON getJSON() {
return data;
}
/**
* Exports the response state, as preparation for triggering the challenge.
*
@ -174,13 +158,8 @@ public class Challenge extends AcmeResource {
return type != null && !type.trim().isEmpty();
}
/**
* Sets the challenge state to the given JSON map.
*
* @param json
* JSON containing the challenge data
*/
public void unmarshall(JSON json) {
@Override
public void setJSON(JSON json) {
String type = json.get(KEY_TYPE).asString();
if (type == null) {
throw new IllegalArgumentException("map does not contain a type");
@ -189,7 +168,9 @@ public class Challenge extends AcmeResource {
throw new AcmeProtocolException("wrong type: " + type);
}
data = json;
setLocation(json.get(KEY_URL).asURL());
super.setJSON(json);
}
/**
@ -208,28 +189,7 @@ public class Challenge extends AcmeResource {
conn.sendSignedRequest(getLocation(), claims, getSession());
unmarshall(conn.readJsonResponse());
}
}
/**
* Updates the state of this challenge.
*
* @throws AcmeRetryAfterException
* the challenge is still being validated, and the server returned an
* estimated date when the challenge will be completed. If you are polling
* for the challenge to complete, you should wait for the date given in
* {@link AcmeRetryAfterException#getRetryAfter()}. Note that the
* challenge status is updated even if this exception was thrown.
*/
public void update() throws AcmeException {
LOG.debug("update");
try (Connection conn = getSession().provider().connect()) {
conn.sendRequest(getLocation(), getSession());
unmarshall(conn.readJsonResponse());
conn.handleRetryAfter("challenge is not completed yet");
setJSON(conn.readJsonResponse());
}
}

View File

@ -433,6 +433,7 @@ public class AccountTest {
};
Account account = new Account(provider.createSession(), locationUrl);
account.setJSON(getJSON("newAccount"));
EditableAccount editable = account.modify();
assertThat(editable, notNullValue());
@ -442,9 +443,10 @@ public class AccountTest {
editable.commit();
assertThat(account.getLocation(), is(locationUrl));
assertThat(account.getContacts().size(), is(2));
assertThat(account.getContacts().get(0), is(URI.create("mailto:foo2@example.com")));
assertThat(account.getContacts().get(1), is(URI.create("mailto:foo3@example.com")));
assertThat(account.getContacts().size(), is(3));
assertThat(account.getContacts().get(0), is(URI.create("mailto:foo@example.com")));
assertThat(account.getContacts().get(1), is(URI.create("mailto:foo2@example.com")));
assertThat(account.getContacts().get(2), is(URI.create("mailto:foo3@example.com")));
provider.close();
}

View File

@ -0,0 +1,154 @@
/*
* acme4j - Java ACME client
*
* Copyright (C) 2018 Richard "Shred" Körber
* http://acme4j.shredzone.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/
package org.shredzone.acme4j;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.shredzone.acme4j.toolbox.TestUtils.getJSON;
import org.junit.Test;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.TestUtils;
/**
* Unit tests for {@link AcmeJsonResource}.
*/
public class AcmeJsonResourceTest {
private static final JSON JSON_DATA = getJSON("newAccountResponse");
/**
* Test {@link AcmeJsonResource#AcmeJsonResource(Session)}.
*/
@Test
public void testSessionConstructor() throws Exception {
Session session = TestUtils.session();
AcmeJsonResource resource = new DummyJsonResource(session);
assertThat(resource.getSession(), is(session));
assertThat(resource.isValid(), is(false));
assertUpdateInvoked(resource, 0);
assertThat(resource.getJSON(), is(JSON_DATA));
assertThat(resource.isValid(), is(true));
assertUpdateInvoked(resource, 1);
}
/**
* Test {@link AcmeJsonResource#AcmeJsonResource(Session, JSON)}.
*/
@Test
public void testSessionAndJsonConstructor() throws Exception {
Session session = TestUtils.session();
AcmeJsonResource resource = new DummyJsonResource(session, JSON_DATA);
assertThat(resource.getSession(), is(session));
assertThat(resource.isValid(), is(true));
assertThat(resource.getJSON(), is(JSON_DATA));
assertUpdateInvoked(resource, 0);
}
/**
* Test {@link AcmeJsonResource#setJSON(JSON)}.
*/
@Test
public void testSetJson() throws Exception {
Session session = TestUtils.session();
JSON jsonData2 = getJSON("requestOrderResponse");
AcmeJsonResource resource = new DummyJsonResource(session);
assertThat(resource.isValid(), is(false));
assertUpdateInvoked(resource, 0);
resource.setJSON(JSON_DATA);
assertThat(resource.getJSON(), is(JSON_DATA));
assertThat(resource.isValid(), is(true));
assertUpdateInvoked(resource, 0);
resource.setJSON(jsonData2);
assertThat(resource.getJSON(), is(jsonData2));
assertThat(resource.isValid(), is(true));
assertUpdateInvoked(resource, 0);
}
/**
* Test {@link AcmeJsonResource#invalidate()}.
*/
@Test
public void testInvalidate() throws Exception {
Session session = TestUtils.session();
AcmeJsonResource resource = new DummyJsonResource(session, JSON_DATA);
assertThat(resource.isValid(), is(true));
assertUpdateInvoked(resource, 0);
resource.invalidate();
assertThat(resource.isValid(), is(false));
assertUpdateInvoked(resource, 0);
resource.setJSON(JSON_DATA);
assertThat(resource.isValid(), is(true));
assertUpdateInvoked(resource, 0);
resource.invalidate();
assertThat(resource.isValid(), is(false));
assertUpdateInvoked(resource, 0);
assertThat(resource.getJSON(), is(JSON_DATA));
assertThat(resource.isValid(), is(true));
assertUpdateInvoked(resource, 1);
}
/**
* Assert that {@link AcmeJsonResource#update()} has been invoked a given number of
* times.
*
* @param resource
* {@link AcmeJsonResource} to test
* @param count
* Expected number of times
*/
private static void assertUpdateInvoked(AcmeJsonResource resource, int count) {
DummyJsonResource dummy = (DummyJsonResource) resource;
assertThat("update counter", dummy.updateCount, is(count));
}
/**
* Minimum implementation of {@link AcmeJsonResource}.
*/
private static class DummyJsonResource extends AcmeJsonResource {
private static final long serialVersionUID = -6459238185161771948L;
private int updateCount = 0;
public DummyJsonResource(Session session) {
super(session);
}
public DummyJsonResource(Session session, JSON json) {
super(session, json);
}
@Override
public void update() throws AcmeException {
// update() is tested individually in all AcmeJsonResource subclasses.
// Here we just simulate the update, by setting a JSON.
updateCount++;
setJSON(JSON_DATA);
}
}
}

View File

@ -273,7 +273,7 @@ public class AuthorizationTest {
provider.putTestChallenge(DUPLICATE_TYPE, new Challenge(session));
Authorization authorization = new Authorization(session, locationUrl);
authorization.unmarshalAuthorization(getJSON("authorizationChallenges"));
authorization.setJSON(getJSON("authorizationChallenges"));
return authorization;
}
}

View File

@ -26,6 +26,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.TestableConnectionProvider;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSONBuilder;
@ -54,6 +55,11 @@ public class OrderTest {
public JSON readJsonResponse() {
return getJSON("updateOrderResponse");
}
@Override
public void handleRetryAfter(String message) throws AcmeException {
assertThat(message, not(nullValue()));
}
};
Session session = provider.createSession();
@ -103,6 +109,11 @@ public class OrderTest {
public JSON readJsonResponse() {
return getJSON("updateOrderResponse");
}
@Override
public void handleRetryAfter(String message) throws AcmeException {
assertThat(message, not(nullValue()));
}
};
Session session = provider.createSession();
@ -153,6 +164,11 @@ public class OrderTest {
public JSON readJsonResponse() {
return getJSON(isFinalized ? "finalizeResponse" : "updateOrderResponse");
}
@Override
public void handleRetryAfter(String message) throws AcmeException {
assertThat(message, not(nullValue()));
}
};
Session session = provider.createSession();

View File

@ -87,20 +87,12 @@ public class ChallengeTest {
}
/**
* Test that after unmarshalling, the challenge properties are set correctly.
* Test that after unmarshaling, the challenge properties are set correctly.
*/
@Test
public void testUnmarshall() throws URISyntaxException {
public void testUnmarshal() throws URISyntaxException {
Challenge challenge = new Challenge(session);
// Test default values
assertThat(challenge.getType(), is(nullValue()));
assertThat(challenge.getStatus(), is(Status.UNKNOWN));
assertThat(challenge.getLocation(), is(nullValue()));
assertThat(challenge.getValidated(), is(nullValue()));
// Unmarshall a challenge JSON
challenge.unmarshall(getJSON("genericChallenge"));
challenge.setJSON(getJSON("genericChallenge"));
// Test unmarshalled values
assertThat(challenge.getType(), is("generic-01"));
@ -135,7 +127,7 @@ public class ChallengeTest {
@Test
public void testRespond() throws JoseException {
Challenge challenge = new Challenge(session);
challenge.unmarshall(getJSON("genericChallenge"));
challenge.setJSON(getJSON("genericChallenge"));
JSONBuilder cb = new JSONBuilder();
challenge.respond(cb);
@ -149,7 +141,7 @@ public class ChallengeTest {
@Test(expected = AcmeProtocolException.class)
public void testNotAcceptable() throws URISyntaxException {
Http01Challenge challenge = new Http01Challenge(session);
challenge.unmarshall(getJSON("dnsChallenge"));
challenge.setJSON(getJSON("dnsChallenge"));
}
/**
@ -176,7 +168,7 @@ public class ChallengeTest {
Session session = provider.createSession();
Http01Challenge challenge = new Http01Challenge(session);
challenge.unmarshall(getJSON("triggerHttpChallenge"));
challenge.setJSON(getJSON("triggerHttpChallenge"));
challenge.trigger();
@ -211,7 +203,7 @@ public class ChallengeTest {
Session session = provider.createSession();
Challenge challenge = new Http01Challenge(session);
challenge.unmarshall(getJSON("triggerHttpChallengeResponse"));
challenge.setJSON(getJSON("triggerHttpChallengeResponse"));
challenge.update();
@ -249,7 +241,7 @@ public class ChallengeTest {
Session session = provider.createSession();
Challenge challenge = new Http01Challenge(session);
challenge.unmarshall(getJSON("triggerHttpChallengeResponse"));
challenge.setJSON(getJSON("triggerHttpChallengeResponse"));
try {
challenge.update();
@ -313,7 +305,7 @@ public class ChallengeTest {
@Test(expected = IllegalArgumentException.class)
public void testBadUnmarshall() {
Challenge challenge = new Challenge(session);
challenge.unmarshall(getJSON("updateAccountResponse"));
challenge.setJSON(getJSON("updateAccountResponse"));
}
}

View File

@ -47,7 +47,7 @@ public class DnsChallengeTest {
@Test
public void testDnsChallenge() throws IOException {
Dns01Challenge challenge = new Dns01Challenge(session);
challenge.unmarshall(getJSON("dnsChallenge"));
challenge.setJSON(getJSON("dnsChallenge"));
assertThat(challenge.getType(), is(Dns01Challenge.TYPE));
assertThat(challenge.getStatus(), is(Status.PENDING));

View File

@ -50,7 +50,7 @@ public class HttpChallengeTest {
@Test
public void testHttpChallenge() throws IOException {
Http01Challenge challenge = new Http01Challenge(session);
challenge.unmarshall(getJSON("httpChallenge"));
challenge.setJSON(getJSON("httpChallenge"));
assertThat(challenge.getType(), is(Http01Challenge.TYPE));
assertThat(challenge.getStatus(), is(Status.PENDING));
@ -70,7 +70,7 @@ public class HttpChallengeTest {
@Test(expected = AcmeProtocolException.class)
public void testNoTokenSet() {
Http01Challenge challenge = new Http01Challenge(session);
challenge.unmarshall(getJSON("httpNoTokenChallenge"));
challenge.setJSON(getJSON("httpNoTokenChallenge"));
challenge.getToken();
}

View File

@ -47,7 +47,7 @@ public class TlsSni02ChallengeTest {
@Test
public void testTlsSni02Challenge() throws IOException {
TlsSni02Challenge challenge = new TlsSni02Challenge(session);
challenge.unmarshall(getJSON("tlsSni02Challenge"));
challenge.setJSON(getJSON("tlsSni02Challenge"));
assertThat(challenge.getType(), is(TlsSni02Challenge.TYPE));
assertThat(challenge.getStatus(), is(Status.PENDING));

View File

@ -1,5 +1,6 @@
{
"type": "dns-01",
"url": "https://example.com/acme/authz/0",
"status": "pending",
"token": "pNvmJivs0WCko2suV7fhe-59oFqyYx_yB7tx6kIMAyE"
}

View File

@ -1,5 +1,6 @@
{
"type": "http-01",
"url": "https://example.com/acme/authz/0",
"status": "pending",
"token": "rSoI9JpyvFi-ltdnBW0W1DjKstzG7cHixjzcOjwzAEQ"
}

View File

@ -1,4 +1,5 @@
{
"type": "http-01",
"url": "https://example.com/acme/authz/0",
"status": "pending"
}

View File

@ -1,5 +1,6 @@
{
"contact": [
"mailto:foo@example.com",
"mailto:foo2@example.com",
"mailto:foo3@example.com"
]

View File

@ -1,6 +1,7 @@
{
"termsOfServiceAgreed": true,
"contact": [
"mailto:foo@example.com",
"mailto:foo2@example.com",
"mailto:foo3@example.com"
]

View File

@ -1,5 +1,6 @@
{
"type": "tls-sni-02",
"url": "https://example.com/acme/authz/0",
"status": "pending",
"token": "VNLBdSiZ3LppU2CRG8bilqlwq4DuApJMg3ZJowU6JhQ"
}