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}