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