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}