001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2015 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 org.shredzone.acme4j.toolbox.AcmeUtils.base64UrlEncode;
017
018import java.security.Key;
019import java.security.PublicKey;
020import java.time.Instant;
021import java.time.format.DateTimeFormatter;
022import java.util.Collections;
023import java.util.Map;
024import java.util.Objects;
025import java.util.TreeMap;
026
027import org.jose4j.json.JsonUtil;
028import org.jose4j.jwk.JsonWebKey;
029import org.jose4j.jwk.PublicJsonWebKey;
030import org.jose4j.lang.JoseException;
031import org.shredzone.acme4j.connector.Resource;
032import org.shredzone.acme4j.exception.AcmeProtocolException;
033
034/**
035 * Builder for JSON structures.
036 * <p>
037 * Example:
038 * <pre>
039 * JSONBuilder cb = new JSONBuilder();
040 * cb.put("foo", 123).put("bar", "hello world");
041 * cb.object("sub").put("data", "subdata");
042 * cb.array("array", 123, 456, 789);
043 * </pre>
044 */
045public class JSONBuilder {
046
047    private final Map<String, Object> data = new TreeMap<>();
048
049    /**
050     * Puts a property. If a property with the key exists, it will be replaced.
051     *
052     * @param key
053     *            Property key
054     * @param value
055     *            Property value
056     * @return {@code this}
057     */
058    public JSONBuilder put(String key, Object value) {
059        data.put(Objects.requireNonNull(key, "key"), value);
060        return this;
061    }
062
063    /**
064     * Puts an {@link Instant} to the JSON. If a property with the key exists, it will be
065     * replaced.
066     *
067     * @param key
068     *            Property key
069     * @param value
070     *            Property {@link Instant} value
071     * @return {@code this}
072     */
073    public JSONBuilder put(String key, Instant value) {
074        if (value == null) {
075            put(key, (Object) null);
076            return this;
077        }
078
079        put(key, DateTimeFormatter.ISO_INSTANT.format(value));
080        return this;
081    }
082
083    /**
084     * Puts a resource.
085     *
086     * @param resource
087     *            Resource name
088     * @return {@code this}
089     */
090    public JSONBuilder putResource(String resource) {
091        return put("resource", resource);
092    }
093
094    /**
095     * Puts a resource.
096     *
097     * @param resource
098     *            {@link Resource}
099     * @return {@code this}
100     */
101    public JSONBuilder putResource(Resource resource) {
102        return putResource(resource.path());
103    }
104
105    /**
106     * Puts binary data to the JSON. The data is base64 url encoded.
107     *
108     * @param key
109     *            Property key
110     * @param data
111     *            Property data
112     * @return {@code this}
113     */
114    public JSONBuilder putBase64(String key, byte[] data) {
115        return put(key, base64UrlEncode(data));
116    }
117
118    /**
119     * Puts a {@link Key} into the claim. The key is serializied as JWK.
120     *
121     * @param key
122     *            Property key
123     * @param publickey
124     *            {@link PublicKey} to serialize
125     * @return {@code this}
126     */
127    public JSONBuilder putKey(String key, PublicKey publickey) {
128        Objects.requireNonNull(publickey, "publickey");
129
130        try {
131            final PublicJsonWebKey jwk = PublicJsonWebKey.Factory.newPublicJwk(publickey);
132            Map<String, Object> jwkParams = jwk.toParams(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
133            object(key).data.putAll(jwkParams);
134            return this;
135        } catch (JoseException ex) {
136            throw new AcmeProtocolException("Invalid key", ex);
137        }
138    }
139
140    /**
141     * Creates an object for the given key.
142     *
143     * @param key
144     *            Key of the object
145     * @return Newly created {@link JSONBuilder} for the object.
146     */
147    public JSONBuilder object(String key) {
148        JSONBuilder subBuilder = new JSONBuilder();
149        data.put(key, subBuilder.data);
150        return subBuilder;
151    }
152
153    /**
154     * Puts an array.
155     *
156     * @param key
157     *            Property key
158     * @param values
159     *            Array of property values
160     * @return {@code this}
161     */
162    public JSONBuilder array(String key, Object... values) {
163        data.put(key, values);
164        return this;
165    }
166
167    /**
168     * Returns a {@link Map} representation of the current state.
169     *
170     * @return {@link Map} of the current state
171     */
172    public Map<String, Object> toMap() {
173        return Collections.unmodifiableMap(data);
174    }
175
176    /**
177     * Returns a {@link JSON} representation of the current state.
178     *
179     * @return {@link JSON} of the current state
180     */
181    public JSON toJSON() {
182        return JSON.parse(toString());
183    }
184
185    /**
186     * Returns a JSON string representation of the current state.
187     */
188    @Override
189    public String toString() {
190        return JsonUtil.toJson(data);
191    }
192
193}