001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2016 Richard "Shred" Körber 005 * http://acme4j.shredzone.org 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 013 */ 014package org.shredzone.acme4j.toolbox; 015 016import static java.nio.charset.StandardCharsets.UTF_8; 017import static java.util.stream.Collectors.joining; 018import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp; 019 020import java.io.BufferedReader; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InputStreamReader; 024import java.io.Serializable; 025import java.net.MalformedURLException; 026import java.net.URI; 027import java.net.URISyntaxException; 028import java.net.URL; 029import java.time.Duration; 030import java.time.Instant; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.Iterator; 034import java.util.List; 035import java.util.Map; 036import java.util.NoSuchElementException; 037import java.util.Objects; 038import java.util.Optional; 039import java.util.Set; 040import java.util.function.Function; 041import java.util.stream.Stream; 042import java.util.stream.StreamSupport; 043 044import edu.umd.cs.findbugs.annotations.Nullable; 045import org.jose4j.json.JsonUtil; 046import org.jose4j.lang.JoseException; 047import org.shredzone.acme4j.Identifier; 048import org.shredzone.acme4j.Problem; 049import org.shredzone.acme4j.Status; 050import org.shredzone.acme4j.exception.AcmeNotSupportedException; 051import org.shredzone.acme4j.exception.AcmeProtocolException; 052 053/** 054 * A model containing a JSON result. The content is immutable. 055 */ 056public final class JSON implements Serializable { 057 private static final long serialVersionUID = 418332625174149030L; 058 059 private static final JSON EMPTY_JSON = new JSON(new HashMap<>()); 060 061 private final String path; 062 private final Map<String, Object> data; 063 064 /** 065 * Creates a new {@link JSON} root object. 066 * 067 * @param data 068 * {@link Map} containing the parsed JSON data 069 */ 070 private JSON(Map<String, Object> data) { 071 this("", data); 072 } 073 074 /** 075 * Creates a new {@link JSON} branch object. 076 * 077 * @param path 078 * Path leading to this branch. 079 * @param data 080 * {@link Map} containing the parsed JSON data 081 */ 082 private JSON(String path, Map<String, Object> data) { 083 this.path = path; 084 this.data = data; 085 } 086 087 /** 088 * Parses JSON from an {@link InputStream}. 089 * 090 * @param in 091 * {@link InputStream} to read from. Will be closed after use. 092 * @return {@link JSON} of the read content. 093 */ 094 public static JSON parse(InputStream in) throws IOException { 095 try (var reader = new BufferedReader(new InputStreamReader(in, UTF_8))) { 096 var json = reader.lines().map(String::trim).collect(joining()); 097 return parse(json); 098 } 099 } 100 101 /** 102 * Parses JSON from a String. 103 * 104 * @param json 105 * JSON string 106 * @return {@link JSON} of the read content. 107 */ 108 public static JSON parse(String json) { 109 try { 110 return new JSON(JsonUtil.parseJson(json)); 111 } catch (JoseException ex) { 112 throw new AcmeProtocolException("Bad JSON: " + json, ex); 113 } 114 } 115 116 /** 117 * Creates a JSON object from a map. 118 * <p> 119 * The map's content is deeply copied. Changes to the map won't reflect in the created 120 * JSON structure. 121 * 122 * @param data 123 * Map structure 124 * @return {@link JSON} of the map's content. 125 * @since 3.2.0 126 */ 127 public static JSON fromMap(Map<String, Object> data) { 128 return JSON.parse(JsonUtil.toJson(data)); 129 } 130 131 /** 132 * Returns a {@link JSON} of an empty document. 133 * 134 * @return Empty {@link JSON} 135 */ 136 public static JSON empty() { 137 return EMPTY_JSON; 138 } 139 140 /** 141 * Returns a set of all keys of this object. 142 * 143 * @return {@link Set} of keys 144 */ 145 public Set<String> keySet() { 146 return Collections.unmodifiableSet(data.keySet()); 147 } 148 149 /** 150 * Checks if this object contains the given key. 151 * 152 * @param key 153 * Name of the key to check 154 * @return {@code true} if the key is present 155 */ 156 public boolean contains(String key) { 157 return data.containsKey(key); 158 } 159 160 /** 161 * Returns the {@link Value} of the given key. 162 * 163 * @param key 164 * Key to read 165 * @return {@link Value} of the key 166 */ 167 public Value get(String key) { 168 return new Value( 169 path.isEmpty() ? key : path + '.' + key, 170 data.get(key)); 171 } 172 173 /** 174 * Returns the {@link Value} of the given key. 175 * 176 * @param key 177 * Key to read 178 * @return {@link Value} of the key 179 * @throws AcmeNotSupportedException 180 * if the key is not present. The key is used as feature name. 181 */ 182 public Value getFeature(String key) { 183 return new Value( 184 path.isEmpty() ? key : path + '.' + key, 185 data.get(key)).onFeature(key); 186 } 187 188 /** 189 * Returns the content as JSON string. 190 */ 191 @Override 192 public String toString() { 193 return JsonUtil.toJson(data); 194 } 195 196 /** 197 * Returns the content as unmodifiable Map. 198 * 199 * @since 2.8 200 */ 201 public Map<String,Object> toMap() { 202 return Collections.unmodifiableMap(data); 203 } 204 205 /** 206 * Represents a JSON array. 207 */ 208 public static final class Array implements Iterable<Value> { 209 private final String path; 210 private final List<Object> data; 211 212 /** 213 * Creates a new {@link Array} object. 214 * 215 * @param path 216 * JSON path to this array. 217 * @param data 218 * Array data 219 */ 220 private Array(String path, List<Object> data) { 221 this.path = path; 222 this.data = data; 223 } 224 225 /** 226 * Returns the array size. 227 * 228 * @return Size of the array 229 */ 230 public int size() { 231 return data.size(); 232 } 233 234 /** 235 * Returns {@code true} if the array is empty. 236 */ 237 public boolean isEmpty() { 238 return data.isEmpty(); 239 } 240 241 /** 242 * Gets the {@link Value} at the given index. 243 * 244 * @param index 245 * Array index to read from 246 * @return {@link Value} at this index 247 */ 248 public Value get(int index) { 249 return new Value(path + '[' + index + ']', data.get(index)); 250 } 251 252 /** 253 * Returns a stream of values. 254 * 255 * @return {@link Stream} of all {@link Value} of this array 256 */ 257 public Stream<Value> stream() { 258 return StreamSupport.stream(spliterator(), false); 259 } 260 261 /** 262 * Creates a new {@link Iterator} that iterates over the array {@link Value}. 263 */ 264 @Override 265 public Iterator<Value> iterator() { 266 return new ValueIterator(this); 267 } 268 } 269 270 /** 271 * A single JSON value. This instance also covers {@code null} values. 272 * <p> 273 * All return values are never {@code null} unless specified otherwise. For optional 274 * parameters, use {@link Value#optional()}. 275 */ 276 public static final class Value { 277 private final String path; 278 private final @Nullable Object val; 279 280 /** 281 * Creates a new {@link Value}. 282 * 283 * @param path 284 * JSON path to this value 285 * @param val 286 * Value, may be {@code null} 287 */ 288 private Value(String path, @Nullable Object val) { 289 this.path = path; 290 this.val = val; 291 } 292 293 /** 294 * Checks if this value is {@code null}. 295 * 296 * @return {@code true} if this value is present, {@code false} if {@code null}. 297 */ 298 public boolean isPresent() { 299 return val != null; 300 } 301 302 /** 303 * Returns this value as {@link Optional}, for further mapping and filtering. 304 * 305 * @return {@link Optional} of this value, or {@link Optional#empty()} if this 306 * value is {@code null}. 307 * @see #map(Function) 308 */ 309 public Optional<Value> optional() { 310 return val != null ? Optional.of(this) : Optional.empty(); 311 } 312 313 /** 314 * Returns this value. If the value was {@code null}, an 315 * {@link AcmeNotSupportedException} is thrown. This method is used for mandatory 316 * fields that are only present if a certain feature is supported by the server. 317 * 318 * @param feature 319 * Feature name 320 * @return itself 321 */ 322 public Value onFeature(String feature) { 323 if (val == null) { 324 throw new AcmeNotSupportedException(feature); 325 } 326 return this; 327 } 328 329 /** 330 * Returns this value as an {@link Optional} of the desired type, for further 331 * mapping and filtering. 332 * 333 * @param mapper 334 * A {@link Function} that converts a {@link Value} to the desired type 335 * @return {@link Optional} of this value, or {@link Optional#empty()} if this 336 * value is {@code null}. 337 * @see #optional() 338 */ 339 public <T> Optional<T> map(Function <Value, T> mapper) { 340 return optional().map(mapper); 341 } 342 343 /** 344 * Returns the value as {@link String}. 345 */ 346 public String asString() { 347 return required().toString(); 348 } 349 350 /** 351 * Returns the value as JSON object. 352 */ 353 @SuppressWarnings("unchecked") 354 public JSON asObject() { 355 return new JSON(path, (Map<String, Object>) required(Map.class)); 356 } 357 358 /** 359 * Returns the value as JSON object that was Base64 URL encoded. 360 * 361 * @since 2.8 362 */ 363 public JSON asEncodedObject() { 364 try { 365 var raw = AcmeUtils.base64UrlDecode(asString()); 366 return new JSON(path, JsonUtil.parseJson(new String(raw, UTF_8))); 367 } catch (IllegalArgumentException | JoseException ex) { 368 throw new AcmeProtocolException(path + ": expected an encoded object", ex); 369 } 370 } 371 372 /** 373 * Returns the value as {@link Problem}. 374 * 375 * @param baseUrl 376 * Base {@link URL} to resolve relative links against 377 */ 378 public Problem asProblem(URL baseUrl) { 379 return new Problem(asObject(), baseUrl); 380 } 381 382 /** 383 * Returns the value as {@link Identifier}. 384 * 385 * @since 2.3 386 */ 387 public Identifier asIdentifier() { 388 return new Identifier(asObject()); 389 } 390 391 /** 392 * Returns the value as {@link JSON.Array}. 393 * <p> 394 * Unlike the other getters, this method returns an empty array if the value is 395 * not set. Use {@link #isPresent()} to find out if the value was actually set. 396 */ 397 @SuppressWarnings("unchecked") 398 public Array asArray() { 399 if (val == null) { 400 return new Array(path, Collections.emptyList()); 401 } 402 403 try { 404 return new Array(path, (List<Object>) val); 405 } catch (ClassCastException ex) { 406 throw new AcmeProtocolException(path + ": expected an array", ex); 407 } 408 } 409 410 /** 411 * Returns the value as int. 412 */ 413 public int asInt() { 414 return (required(Number.class)).intValue(); 415 } 416 417 /** 418 * Returns the value as boolean. 419 */ 420 public boolean asBoolean() { 421 return required(Boolean.class); 422 } 423 424 /** 425 * Returns the value as {@link URI}. 426 */ 427 public URI asURI() { 428 try { 429 return new URI(asString()); 430 } catch (URISyntaxException ex) { 431 throw new AcmeProtocolException(path + ": bad URI " + val, ex); 432 } 433 } 434 435 /** 436 * Returns the value as {@link URL}. 437 */ 438 public URL asURL() { 439 try { 440 return new URL(asString()); 441 } catch (MalformedURLException ex) { 442 throw new AcmeProtocolException(path + ": bad URL " + val, ex); 443 } 444 } 445 446 /** 447 * Returns the value as {@link Instant}. 448 */ 449 public Instant asInstant() { 450 try { 451 return parseTimestamp(asString()); 452 } catch (IllegalArgumentException ex) { 453 throw new AcmeProtocolException(path + ": bad date " + val, ex); 454 } 455 } 456 457 /** 458 * Returns the value as {@link Duration}. 459 * 460 * @since 2.3 461 */ 462 public Duration asDuration() { 463 return Duration.ofSeconds(required(Number.class).longValue()); 464 } 465 466 /** 467 * Returns the value as base64 decoded byte array. 468 */ 469 public byte[] asBinary() { 470 return AcmeUtils.base64UrlDecode(asString()); 471 } 472 473 /** 474 * Returns the parsed {@link Status}. 475 */ 476 public Status asStatus() { 477 return Status.parse(asString()); 478 } 479 480 /** 481 * Checks if the value is present. An {@link AcmeProtocolException} is thrown if 482 * the value is {@code null}. 483 * 484 * @return val that is guaranteed to be non-{@code null} 485 */ 486 private Object required() { 487 if (val == null) { 488 throw new AcmeProtocolException(path + ": required, but not set"); 489 } 490 return val; 491 } 492 493 /** 494 * Checks if the value is present. An {@link AcmeProtocolException} is thrown if 495 * the value is {@code null} or is not of the expected type. 496 * 497 * @param type 498 * expected type 499 * @return val that is guaranteed to be non-{@code null} 500 */ 501 private <T> T required(Class<T> type) { 502 if (val == null) { 503 throw new AcmeProtocolException(path + ": required, but not set"); 504 } 505 if (!type.isInstance(val)) { 506 throw new AcmeProtocolException(path + ": cannot convert to " + type.getSimpleName()); 507 } 508 return type.cast(val); 509 } 510 511 @Override 512 public boolean equals(Object obj) { 513 if (!(obj instanceof Value)) { 514 return false; 515 } 516 return Objects.equals(val, ((Value) obj).val); 517 } 518 519 @Override 520 public int hashCode() { 521 return val != null ? val.hashCode() : 0; 522 } 523 } 524 525 /** 526 * An {@link Iterator} over array {@link Value}. 527 */ 528 private static class ValueIterator implements Iterator<Value> { 529 private final Array array; 530 private int index = 0; 531 532 public ValueIterator(Array array) { 533 this.array = array; 534 } 535 536 @Override 537 public boolean hasNext() { 538 return index < array.size(); 539 } 540 541 @Override 542 public Value next() { 543 if (!hasNext()) { 544 throw new NoSuchElementException(); 545 } 546 return array.get(index++); 547 } 548 549 @Override 550 public void remove() { 551 throw new UnsupportedOperationException(); 552 } 553 } 554 555}