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