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