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.util.stream.Collectors.joining; 017import static org.shredzone.acme4j.toolbox.AcmeUtils.parseTimestamp; 018 019import java.io.BufferedReader; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.InputStreamReader; 023import java.io.ObjectInputStream; 024import java.io.ObjectOutputStream; 025import java.io.Serializable; 026import java.net.MalformedURLException; 027import java.net.URI; 028import java.net.URISyntaxException; 029import java.net.URL; 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.Set; 039import java.util.stream.Stream; 040import java.util.stream.StreamSupport; 041 042import org.jose4j.json.JsonUtil; 043import org.jose4j.lang.JoseException; 044import org.shredzone.acme4j.Problem; 045import org.shredzone.acme4j.exception.AcmeProtocolException; 046 047/** 048 * A model containing a JSON result. The content is immutable. 049 */ 050@SuppressWarnings("unchecked") 051public final class JSON implements Serializable { 052 private static final long serialVersionUID = 3091273044605709204L; 053 054 private static final JSON EMPTY_JSON = new JSON(new HashMap<String, Object>()); 055 056 private final String path; 057 private Map<String, Object> data; 058 059 /** 060 * Creates a new {@link JSON} root object. 061 * 062 * @param data 063 * {@link Map} containing the parsed JSON data 064 */ 065 private JSON(Map<String, Object> data) { 066 this("", data); 067 } 068 069 /** 070 * Creates a new {@link JSON} branch object. 071 * 072 * @param path 073 * Path leading to this branch. 074 * @param data 075 * {@link Map} containing the parsed JSON data 076 */ 077 private JSON(String path, Map<String, Object> data) { 078 this.path = path; 079 this.data = data; 080 } 081 082 /** 083 * Parses JSON from an {@link InputStream}. 084 * 085 * @param in 086 * {@link InputStream} to read from. Will be closed after use. 087 * @return {@link JSON} of the read content. 088 */ 089 public static JSON parse(InputStream in) throws IOException { 090 try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, "utf-8"))) { 091 String json = reader.lines().map(String::trim).collect(joining()); 092 return parse(json); 093 } 094 } 095 096 /** 097 * Parses JSON from a String. 098 * 099 * @param json 100 * JSON string 101 * @return {@link JSON} of the read content. 102 */ 103 public static JSON parse(String json) { 104 try { 105 return new JSON(JsonUtil.parseJson(json)); 106 } catch (JoseException ex) { 107 throw new AcmeProtocolException("Bad JSON: " + json, ex); 108 } 109 } 110 111 /** 112 * Returns a {@link JSON} of an empty document. 113 * 114 * @return Empty {@link JSON} 115 */ 116 public static JSON empty() { 117 return EMPTY_JSON; 118 } 119 120 /** 121 * Returns a set of all keys of this object. 122 * 123 * @return {@link Set} of keys 124 */ 125 public Set<String> keySet() { 126 return Collections.unmodifiableSet(data.keySet()); 127 } 128 129 /** 130 * Checks if this object contains the given key. 131 * 132 * @param key 133 * Name of the key to check 134 * @return {@code true} if the key is present 135 */ 136 public boolean contains(String key) { 137 return data.containsKey(key); 138 } 139 140 /** 141 * Returns the {@link Value} of the given key. 142 * 143 * @param key 144 * Key to read 145 * @return {@link Value} of the key 146 */ 147 public Value get(String key) { 148 return new Value( 149 path.isEmpty() ? key : path + '.' + key, 150 data.get(key)); 151 } 152 153 /** 154 * Returns the content as JSON string. 155 */ 156 @Override 157 public String toString() { 158 return JsonUtil.toJson(data); 159 } 160 161 /** 162 * Serialize the data map in JSON. 163 */ 164 private void writeObject(ObjectOutputStream out) throws IOException { 165 out.writeUTF(JsonUtil.toJson(data)); 166 out.defaultWriteObject(); 167 } 168 169 /** 170 * Deserialize the JSON representation of the data map. 171 */ 172 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { 173 try { 174 data = new HashMap<>(JsonUtil.parseJson(in.readUTF())); 175 in.defaultReadObject(); 176 } catch (JoseException ex) { 177 throw new AcmeProtocolException("Cannot deserialize", ex); 178 } 179 } 180 181 /** 182 * Represents a JSON array. 183 */ 184 public static final class Array implements Iterable<Value> { 185 private final String path; 186 private final List<Object> data; 187 188 /** 189 * Creates a new {@link Array} object. 190 * 191 * @param path 192 * JSON path to this array. 193 * @param data 194 * Array data 195 */ 196 private Array(String path, List<Object> data) { 197 this.path = path; 198 this.data = data; 199 } 200 201 /** 202 * Returns the array size. 203 * 204 * @return Size of the array 205 */ 206 public int size() { 207 return data.size(); 208 } 209 210 /** 211 * Gets the {@link Value} at the given index. 212 * 213 * @param index 214 * Array index to read from 215 * @return {@link Value} at this index 216 */ 217 public Value get(int index) { 218 return new Value(path + '[' + index + ']', data.get(index)); 219 } 220 221 /** 222 * Returns a stream of values. 223 * 224 * @return {@link Stream} of all {@link Value} of this array 225 */ 226 public Stream<Value> stream() { 227 return StreamSupport.stream(spliterator(), false); 228 } 229 230 /** 231 * Creates a new {@link Iterator} that iterates over the array {@link Value}. 232 */ 233 @Override 234 public Iterator<Value> iterator() { 235 return new ValueIterator(this); 236 } 237 } 238 239 /** 240 * A single JSON value. This instance also covers {@code null} values. 241 */ 242 public static final class Value { 243 private final String path; 244 private final Object val; 245 246 /** 247 * Creates a new {@link Value}. 248 * 249 * @param path 250 * JSON path to this value 251 * @param val 252 * Value, may be {@code null} 253 */ 254 private Value(String path, Object val) { 255 this.path = path; 256 this.val = val; 257 } 258 259 /** 260 * Checks if the value is present. An {@link AcmeProtocolException} is thrown if 261 * the value is {@code null}. 262 * 263 * @return itself 264 */ 265 public Value required() { 266 if (val == null) { 267 throw new AcmeProtocolException(path + ": required, but not set"); 268 } 269 return this; 270 } 271 272 /** 273 * Returns the value as {@link String}. 274 * 275 * @return {@link String}, or {@code null} if the value was not set. 276 */ 277 public String asString() { 278 return val != null ? val.toString() : null; 279 } 280 281 /** 282 * Returns the value as JSON object. 283 * 284 * @return {@link JSON}, or {@code null} if the value was not set. 285 */ 286 public JSON asObject() { 287 if (val == null) { 288 return null; 289 } 290 291 try { 292 return new JSON(path, (Map<String, Object>) val); 293 } catch (ClassCastException ex) { 294 throw new AcmeProtocolException(path + ": expected an object", ex); 295 } 296 } 297 298 /** 299 * Returns the value as {@link Problem}. 300 * 301 * @return {@link Problem}, or {@code null} if the value was not set. 302 */ 303 public Problem asProblem() { 304 if (val == null) { 305 return null; 306 } 307 308 return new Problem(asObject()); 309 } 310 311 /** 312 * Returns the value as JSON array. 313 * 314 * @return {@link JSON.Array}, or {@code null} if the value was not set. 315 */ 316 public Array asArray() { 317 if (val == null) { 318 return null; 319 } 320 321 try { 322 return new Array(path, (List<Object>) val); 323 } catch (ClassCastException ex) { 324 throw new AcmeProtocolException(path + ": expected an array", ex); 325 } 326 } 327 328 /** 329 * Returns the value as int. 330 * 331 * @return integer value 332 */ 333 public int asInt() { 334 required(); 335 336 try { 337 return ((Number) val).intValue(); 338 } catch (ClassCastException ex) { 339 throw new AcmeProtocolException(path + ": bad number " + val, ex); 340 } 341 } 342 343 /** 344 * Returns the value as {@link URI}. 345 * 346 * @return {@link URI}, or {@code null} if the value was not set. 347 */ 348 public URI asURI() { 349 if (val == null) { 350 return null; 351 } 352 353 try { 354 return new URI(val.toString()); 355 } catch (URISyntaxException ex) { 356 throw new AcmeProtocolException(path + ": bad URI " + val, ex); 357 } 358 } 359 360 /** 361 * Returns the value as {@link URL}. 362 * 363 * @return {@link URL}, or {@code null} if the value was not set. 364 */ 365 public URL asURL() { 366 if (val == null) { 367 return null; 368 } 369 370 try { 371 return new URL(val.toString()); 372 } catch (MalformedURLException ex) { 373 throw new AcmeProtocolException(path + ": bad URL " + val, ex); 374 } 375 } 376 377 /** 378 * Returns the value as {@link Instant}. 379 * 380 * @return {@link Instant}, or {@code null} if the value was not set. 381 */ 382 public Instant asInstant() { 383 if (val == null) { 384 return null; 385 } 386 387 try { 388 return parseTimestamp(val.toString()); 389 } catch (IllegalArgumentException ex) { 390 throw new AcmeProtocolException(path + ": bad date " + val, ex); 391 } 392 } 393 394 @Override 395 public boolean equals(Object obj) { 396 if (obj == null || !(obj instanceof Value)) { 397 return false; 398 } 399 return Objects.equals(val, ((Value) obj).val); 400 } 401 402 @Override 403 public int hashCode() { 404 return val != null ? val.hashCode() : 0; 405 } 406 } 407 408 /** 409 * An {@link Iterator} over array {@link Value}. 410 */ 411 private static class ValueIterator implements Iterator<Value> { 412 private final Array array; 413 private int index = 0; 414 415 public ValueIterator(Array array) { 416 this.array = array; 417 } 418 419 @Override 420 public boolean hasNext() { 421 return index < array.size(); 422 } 423 424 @Override 425 public Value next() { 426 if (!hasNext()) { 427 throw new NoSuchElementException(); 428 } 429 return array.get(index++); 430 } 431 432 @Override 433 public void remove() { 434 throw new UnsupportedOperationException(); 435 } 436 } 437 438}