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