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