JSON getters never return null

pull/61/head
Richard Körber 2018-03-17 18:18:44 +01:00
parent 4b3eb22eef
commit 4de82be5f3
No known key found for this signature in database
GPG Key ID: AAB9FD19C78AA3E0
13 changed files with 179 additions and 154 deletions

View File

@ -64,7 +64,7 @@ public class Account extends AcmeJsonResource {
* {@code null} if the server did not provide such an information.
*/
public Boolean getTermsOfServiceAgreed() {
return getJSON().get(KEY_TOS_AGREED).optional().map(Value::asBoolean).orElse(null);
return getJSON().get(KEY_TOS_AGREED).map(Value::asBoolean).orElse(null);
}
/**
@ -85,7 +85,7 @@ public class Account extends AcmeJsonResource {
* {@link Status#REVOKED}.
*/
public Status getStatus() {
return getJSON().get(KEY_STATUS).asStatusOrElse(Status.UNKNOWN);
return getJSON().get(KEY_STATUS).asStatus();
}
/**

View File

@ -50,12 +50,12 @@ public class Authorization extends AcmeJsonResource {
* order, check the {@link #isWildcard()} method.
*/
public String getDomain() {
JSON jsonIdentifier = getJSON().get("identifier").required().asObject();
String type = jsonIdentifier.get("type").required().asString();
JSON jsonIdentifier = getJSON().get("identifier").asObject();
String type = jsonIdentifier.get("type").asString();
if (!"dns".equals(type)) {
throw new AcmeProtocolException("Unknown authorization type: " + type);
}
return jsonIdentifier.get("value").required().asString();
return jsonIdentifier.get("value").asString();
}
/**
@ -65,14 +65,14 @@ public class Authorization extends AcmeJsonResource {
* {@link Status#INVALID}, {@link Status#DEACTIVATED}, {@link Status#REVOKED}.
*/
public Status getStatus() {
return getJSON().get("status").asStatusOrElse(Status.UNKNOWN);
return getJSON().get("status").asStatus();
}
/**
* Gets the expiry date of the authorization, if set by the server.
*/
public Instant getExpires() {
return getJSON().get("expires").optional()
return getJSON().get("expires")
.map(Value::asString)
.map(AcmeUtils::parseTimestamp)
.orElse(null);
@ -83,7 +83,7 @@ public class Authorization extends AcmeJsonResource {
* {@code false} otherwise.
*/
public boolean isWildcard() {
return getJSON().get("wildcard").optional()
return getJSON().get("wildcard")
.map(Value::asBoolean)
.orElse(false);
}

View File

@ -44,7 +44,7 @@ public class Metadata {
* available.
*/
public URI getTermsOfService() {
return meta.get("termsOfService").asURI();
return meta.get("termsOfService").map(Value::asURI).orElse(null);
}
/**
@ -52,7 +52,7 @@ public class Metadata {
* server. {@code null} if not available.
*/
public URL getWebsite() {
return meta.get("website").asURL();
return meta.get("website").map(Value::asURL).orElse(null);
}
/**
@ -60,7 +60,9 @@ public class Metadata {
* itself for the purposes of CAA record validation. Empty if not available.
*/
public Collection<String> getCaaIdentities() {
return meta.get("caaIdentities").asArray().stream()
return meta.get("caaIdentities")
.asArray()
.stream()
.map(Value::asString)
.collect(toList());
}
@ -69,7 +71,7 @@ public class Metadata {
* Returns whether an external account is required by this CA.
*/
public boolean isExternalAccountRequired() {
return meta.get("externalAccountRequired").orElse(false).asBoolean();
return meta.get("externalAccountRequired").map(Value::asBoolean).orElse(false);
}
/**

View File

@ -45,21 +45,21 @@ public class Order extends AcmeJsonResource {
* {@link Status#PROCESSING}, {@link Status#VALID}, {@link Status#INVALID}.
*/
public Status getStatus() {
return getJSON().get("status").asStatusOrElse(Status.UNKNOWN);
return getJSON().get("status").asStatus();
}
/**
* Returns a {@link Problem} document if the order failed.
*/
public Problem getError() {
return getJSON().get("error").asProblem(getLocation());
return getJSON().get("error").map(v -> v.asProblem(getLocation())).orElse(null);
}
/**
* Gets the expiry date of the authorization, if set by the server.
*/
public Instant getExpires() {
return getJSON().get("expires").asInstant();
return getJSON().get("expires").map(Value::asInstant).orElse(null);
}
/**
@ -78,14 +78,14 @@ public class Order extends AcmeJsonResource {
* Gets the "not before" date that was used for the order, or {@code null}.
*/
public Instant getNotBefore() {
return getJSON().get("notBefore").asInstant();
return getJSON().get("notBefore").map(Value::asInstant).orElse(null);
}
/**
* Gets the "not after" date that was used for the order, or {@code null}.
*/
public Instant getNotAfter() {
return getJSON().get("notAfter").asInstant();
return getJSON().get("notAfter").map(Value::asInstant).orElse(null);
}
/**
@ -114,7 +114,7 @@ public class Order extends AcmeJsonResource {
* Gets the {@link Certificate} if it is available. {@code null} otherwise.
*/
public Certificate getCertificate() {
return getJSON().get("certificate").optional()
return getJSON().get("certificate")
.map(Value::asURL)
.map(getLogin()::bindCertificate)
.orElse(null);

View File

@ -24,6 +24,7 @@ import java.util.List;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSON.Value;
/**
* Represents a JSON Problem.
@ -53,12 +54,16 @@ public class Problem implements Serializable {
* Returns the problem type. It is always an absolute URI.
*/
public URI getType() {
try {
String type = problemJson.get("type").asString();
return type != null ? baseUrl.toURI().resolve(type) : null;
} catch (URISyntaxException ex) {
throw new IllegalArgumentException("Bad base URL", ex);
}
return problemJson.get("type")
.map(Value::asString)
.map(it -> {
try {
return baseUrl.toURI().resolve(it);
} catch (URISyntaxException ex) {
throw new IllegalArgumentException("Bad base URL", ex);
}
})
.orElse(null);
}
/**
@ -68,7 +73,7 @@ public class Problem implements Serializable {
* @see #toString()
*/
public String getTitle() {
return problemJson.get("title").asString();
return problemJson.get("title").map(Value::asString).orElse(null);
}
/**
@ -78,7 +83,7 @@ public class Problem implements Serializable {
* @see #toString()
*/
public String getDetail() {
return problemJson.get("detail").asString();
return problemJson.get("detail").map(Value::asString).orElse(null);
}
/**
@ -86,29 +91,35 @@ public class Problem implements Serializable {
* an absolute URI.
*/
public URI getInstance() {
try {
String instance = problemJson.get("instance").asString();
return instance != null ? baseUrl.toURI().resolve(instance) : null;
} catch (URISyntaxException ex) {
throw new IllegalArgumentException("Bad base URL", ex);
}
return problemJson.get("instance")
.map(Value::asString)
.map(it -> {
try {
return baseUrl.toURI().resolve(it);
} catch (URISyntaxException ex) {
throw new IllegalArgumentException("Bad base URL", ex);
}
})
.orElse(null);
}
/**
* Returns the domain this problem relates to. May be {@code null}.
*/
public String getDomain() {
JSON identifier = problemJson.get("identifier").asObject();
if (identifier == null) {
Value identifier = problemJson.get("identifier");
if (!identifier.isPresent()) {
return null;
}
String type = identifier.get("type").asString();
JSON json = identifier.asObject();
String type = json.get("type").asString();
if (!"dns".equals(type)) {
throw new AcmeProtocolException("Cannot process a " + type + " identifier");
}
return identifier.get("value").asString();
return json.get("value").asString();
}
/**
@ -117,7 +128,8 @@ public class Problem implements Serializable {
public List<Problem> getSubProblems() {
return unmodifiableList(
problemJson.get("subproblems")
.asArray().stream()
.asArray()
.stream()
.map(o -> o.asProblem(baseUrl))
.collect(toList())
);

View File

@ -31,6 +31,7 @@ import org.shredzone.acme4j.connector.Resource;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.provider.AcmeProvider;
import org.shredzone.acme4j.toolbox.JSON;
import org.shredzone.acme4j.toolbox.JSON.Value;
/**
* A session stores the ACME server URI. It also tracks communication parameters.
@ -193,19 +194,18 @@ public class Session {
JSON directoryJson = provider().directory(this, getServerUri());
JSON meta = directoryJson.get("meta").asObject();
if (meta != null) {
metadata.set(new Metadata(meta));
Value meta = directoryJson.get("meta");
if (meta.isPresent()) {
metadata.set(new Metadata(meta.asObject()));
} else {
metadata.set(new Metadata(JSON.empty()));
}
Map<Resource, URL> map = new EnumMap<>(Resource.class);
for (Resource res : Resource.values()) {
URL url = directoryJson.get(res.path()).asURL();
if (url != null) {
map.put(res, url);
}
directoryJson.get(res.path())
.map(Value::asURL)
.ifPresent(url -> map.put(res, url));
}
resourceMap.set(map);

View File

@ -23,6 +23,7 @@ import org.shredzone.acme4j.connector.Connection;
import org.shredzone.acme4j.exception.AcmeException;
import org.shredzone.acme4j.exception.AcmeProtocolException;
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;
@ -55,7 +56,7 @@ public class Challenge extends AcmeJsonResource {
* {@link JSON} challenge data
*/
public Challenge(Login login, JSON data) {
super(login, data.get(KEY_URL).required().asURL());
super(login, data.get(KEY_URL).asURL());
setJSON(data);
}
@ -73,14 +74,14 @@ public class Challenge extends AcmeJsonResource {
* {@link Status#VALID}, {@link Status#INVALID}.
*/
public Status getStatus() {
return getJSON().get(KEY_STATUS).asStatusOrElse(Status.UNKNOWN);
return getJSON().get(KEY_STATUS).asStatus();
}
/**
* Returns the validation date, if returned by the server.
*/
public Instant getValidated() {
return getJSON().get(KEY_VALIDATED).asInstant();
return getJSON().get(KEY_VALIDATED).map(Value::asInstant).orElse(null);
}
/**
@ -89,7 +90,9 @@ public class Challenge extends AcmeJsonResource {
* {@link Problem#getSubProblems()}.
*/
public Problem getError() {
return getJSON().get(KEY_ERROR).asProblem(getLocation());
return getJSON().get(KEY_ERROR)
.map(it -> it.asProblem(getLocation()))
.orElse(null);
}
/**
@ -115,13 +118,13 @@ public class Challenge extends AcmeJsonResource {
@Override
protected void setJSON(JSON json) {
String type = json.get(KEY_TYPE).required().asString();
String type = json.get(KEY_TYPE).asString();
if (!acceptable(type)) {
throw new AcmeProtocolException("incompatible type " + type + " for this challenge");
}
String loc = json.get(KEY_URL).required().asString();
String loc = json.get(KEY_URL).asString();
if (loc != null && !loc.equals(getLocation().toString())) {
throw new AcmeProtocolException("challenge has changed its location");
}

View File

@ -56,7 +56,7 @@ public class TokenChallenge extends Challenge {
* Gets the token.
*/
protected String getToken() {
return getJSON().get(KEY_TOKEN).required().asString();
return getJSON().get(KEY_TOKEN).asString();
}
/**

View File

@ -86,7 +86,7 @@ public abstract class AbstractAcmeProvider implements AcmeProvider {
Objects.requireNonNull(login, "login");
Objects.requireNonNull(data, "data");
String type = data.get("type").required().asString();
String type = data.get("type").asString();
BiFunction<Login, JSON, Challenge> constructor = CHALLENGES.get(type);
if (constructor != null) {

View File

@ -37,6 +37,7 @@ import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@ -247,6 +248,9 @@ public final class JSON implements Serializable {
/**
* A single JSON value. This instance also covers {@code null} values.
* <p>
* All return values are never {@code null} unless specified otherwise. For optional
* parameters, use {@link Value#optional()}.
*/
public static final class Value {
private final String path;
@ -265,58 +269,53 @@ public final class JSON implements Serializable {
this.val = val;
}
/**
* Checks if this value is {@code null}.
*
* @return {@code true} if this value is present, {@code false} if {@code null}.
*/
public boolean isPresent() {
return val != null;
}
/**
* Returns this value as {@link Optional}, for further mapping and filtering.
*
* @return {@link Optional} of this value, or {@link Optional#empty()} if this
* value is {@code null}.
* @see #map(Function)
*/
public Optional<Value> optional() {
return val != null ? Optional.of(this) : Optional.empty();
}
/**
* Checks if the value is present. An {@link AcmeProtocolException} is thrown if
* the value is {@code null}.
* Returns this value as an {@link Optional} of the desired type, for further
* mapping and filtering.
*
* @return itself
* @param mapper
* A {@link Function} that converts a {@link Value} to the desired type
* @return {@link Optional} of this value, or {@link Optional#empty()} if this
* value is {@code null}.
* @see #optional()
*/
public Value required() {
if (val == null) {
throw new AcmeProtocolException(path + ": required, but not set");
}
return this;
}
/**
* Checks if the value is present. If not, the default value is used instead.
*
* @param def Default value
* @return itself
*/
public Value orElse(Object def) {
return val != null ? this : new Value(path, def);
public <T> Optional<T> map(Function <Value, T> mapper) {
return optional().map(mapper);
}
/**
* Returns the value as {@link String}.
*
* @return {@link String}, or {@code null} if the value was not set.
*/
public String asString() {
return val != null ? val.toString() : null;
required();
return val.toString();
}
/**
* Returns the value as JSON object.
*
* @return {@link JSON}, or {@code null} if the value was not set.
*/
public JSON asObject() {
if (val == null) {
return null;
}
required();
try {
return new JSON(path, (Map<String, Object>) val);
} catch (ClassCastException ex) {
@ -329,20 +328,17 @@ public final class JSON implements Serializable {
*
* @param baseUrl
* Base {@link URL} to resolve relative links against
* @return {@link Problem}, or {@code null} if the value was not set.
*/
public Problem asProblem(URL baseUrl) {
if (val == null) {
return null;
}
required();
return new Problem(asObject(), baseUrl);
}
/**
* Returns the value as JSON array.
*
* @return {@link JSON.Array}, which is empty if the value was not set.
* Returns the value as {@link JSON.Array}.
* <p>
* Unlike the other getters, this method returns an empty array if the value is
* not set. Use {@link #isPresent()} to find out if the value was actually set.
*/
public Array asArray() {
if (val == null) {
@ -358,12 +354,9 @@ public final class JSON implements Serializable {
/**
* Returns the value as int.
*
* @return integer value
*/
public int asInt() {
required();
try {
return ((Number) val).intValue();
} catch (ClassCastException ex) {
@ -373,12 +366,9 @@ public final class JSON implements Serializable {
/**
* Returns the value as boolean.
*
* @return integer value
*/
public boolean asBoolean() {
required();
try {
return (Boolean) val;
} catch (ClassCastException ex) {
@ -388,14 +378,9 @@ public final class JSON implements Serializable {
/**
* Returns the value as {@link URI}.
*
* @return {@link URI}, or {@code null} if the value was not set.
*/
public URI asURI() {
if (val == null) {
return null;
}
required();
try {
return new URI(val.toString());
} catch (URISyntaxException ex) {
@ -405,14 +390,9 @@ public final class JSON implements Serializable {
/**
* Returns the value as {@link URL}.
*
* @return {@link URL}, or {@code null} if the value was not set.
*/
public URL asURL() {
if (val == null) {
return null;
}
required();
try {
return new URL(val.toString());
} catch (MalformedURLException ex) {
@ -422,14 +402,9 @@ public final class JSON implements Serializable {
/**
* Returns the value as {@link Instant}.
*
* @return {@link Instant}, or {@code null} if the value was not set.
*/
public Instant asInstant() {
if (val == null) {
return null;
}
required();
try {
return parseTimestamp(val.toString());
} catch (IllegalArgumentException ex) {
@ -439,32 +414,30 @@ public final class JSON implements Serializable {
/**
* Returns the value as base64 decoded byte array.
*
* @return byte array, or {@code null} if the value was not set.
*/
public byte[] asBinary() {
if (val == null) {
return null; //NOSONAR: we want to return null here
}
required();
return AcmeUtils.base64UrlDecode(val.toString());
}
/**
* Returns the parsed status.
*
* @param def
* Default status if value is not present or {@code null}
* @return {@link Status}
* Returns the parsed {@link Status}.
*/
public Status asStatusOrElse(Status def) {
if (val == null) {
return def;
}
public Status asStatus() {
required();
return Status.parse(val.toString());
}
/**
* Checks if the value is present. An {@link AcmeProtocolException} is thrown if
* the value is {@code null}.
*/
private void required() {
if (!isPresent()) {
throw new AcmeProtocolException(path + ": required, but not set");
}
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof Value)) {

View File

@ -121,7 +121,6 @@ public class AccountBuilderTest {
JSON binding = claims.toJSON()
.get("externalAccountBinding")
.required()
.asObject();
String encodedHeader = binding.get("protected").asString();

View File

@ -60,8 +60,6 @@ public class ChallengeTest {
assertThat(challenge.getValidated(), is(parseTimestamp("2015-12-12T17:19:36.336785823Z")));
assertThat(challenge.getJSON().get("type").asString(), is("generic-01"));
assertThat(challenge.getJSON().get("url").asURL(), is(url("http://example.com/challenge/123")));
assertThat(challenge.getJSON().get("notPresent").asString(), is(nullValue()));
assertThat(challenge.getJSON().get("notPresentUrl").asURL(), is(nullValue()));
Problem error = challenge.getError();
assertThat(error, is(notNullValue()));

View File

@ -40,6 +40,7 @@ import org.junit.Test;
import org.shredzone.acme4j.Problem;
import org.shredzone.acme4j.Status;
import org.shredzone.acme4j.exception.AcmeProtocolException;
import org.shredzone.acme4j.toolbox.JSON.Value;
/**
* Unit test for {@link JSON}.
@ -198,10 +199,12 @@ public class JSONTest {
assertThat(json.get("uri").asURI(), is(URI.create("mailto:foo@example.com")));
assertThat(json.get("url").asURL(), is(url("http://example.com")));
assertThat(json.get("date").asInstant(), is(date));
assertThat(json.get("status").asStatusOrElse(Status.INVALID), is(Status.VALID));
assertThat(json.get("status").asStatus(), is(Status.VALID));
assertThat(json.get("binary").asBinary(), is("Chainsaw".getBytes()));
assertThat(json.get("text").isPresent(), is(true));
assertThat(json.get("text").optional().isPresent(), is(true));
assertThat(json.get("text").map(Value::asString).isPresent(), is(true));
JSON.Array array = json.get("array").asArray();
assertThat(array.get(0).asString(), is("foo"));
@ -230,20 +233,65 @@ public class JSONTest {
JSON json = TestUtils.getJSON("datatypes");
assertThat(json.get("none"), is(notNullValue()));
assertThat(json.get("none").asString(), is(nullValue()));
assertThat(json.get("none").asURI(), is(nullValue()));
assertThat(json.get("none").asURL(), is(nullValue()));
assertThat(json.get("none").asInstant(), is(nullValue()));
assertThat(json.get("none").asObject(), is(nullValue()));
assertThat(json.get("none").asStatusOrElse(Status.INVALID), is(Status.INVALID));
assertThat(json.get("none").asBinary(), is(nullValue()));
assertThat(json.get("none").asProblem(BASE_URL), is(nullValue()));
assertThat(json.get("none").orElse("foo").asString(), is("foo"));
assertThat(json.get("none").orElse(42).asInt(), is(42));
assertThat(json.get("none").orElse(true).asBoolean(), is(true));
assertThat(json.get("none").isPresent(), is(false));
assertThat(json.get("none").optional().isPresent(), is(false));
assertThat(json.get("none").map(Value::asString).isPresent(), is(false));
try {
json.get("none").asString();
fail("asString did not fail");
} catch (AcmeProtocolException ex) {
// expected
}
try {
json.get("none").asURI();
fail("asURI did not fail");
} catch (AcmeProtocolException ex) {
// expected
}
try {
json.get("none").asURL();
fail("asURL did not fail");
} catch (AcmeProtocolException ex) {
// expected
}
try {
json.get("none").asInstant();
fail("asInstant did not fail");
} catch (AcmeProtocolException ex) {
// expected
}
try {
json.get("none").asObject();
fail("asObject did not fail");
} catch (AcmeProtocolException ex) {
// expected
}
try {
json.get("none").asStatus();
fail("asStatus did not fail");
} catch (AcmeProtocolException ex) {
// expected
}
try {
json.get("none").asBinary();
fail("asBinary did not fail");
} catch (AcmeProtocolException ex) {
// expected
}
try {
json.get("none").asProblem(BASE_URL);
fail("asProblem did not fail");
} catch (AcmeProtocolException ex) {
// expected
}
try {
json.get("none").asInt();
@ -258,16 +306,6 @@ public class JSONTest {
} catch (AcmeProtocolException ex) {
// expected
}
try {
json.get("none").required();
fail("required did not fail");
} catch (AcmeProtocolException ex) {
// expected
}
JSON.Value textv = json.get("text");
assertThat(textv.required(), is(textv));
}
/**