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}