001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2024 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public 
013 * License for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.webconsole.base;
020
021import com.fasterxml.jackson.core.JsonGenerator;
022import com.fasterxml.jackson.core.JsonParser;
023import com.fasterxml.jackson.core.JsonToken;
024import com.fasterxml.jackson.databind.BeanProperty;
025import com.fasterxml.jackson.databind.DeserializationContext;
026import com.fasterxml.jackson.databind.JavaType;
027import com.fasterxml.jackson.databind.JsonDeserializer;
028import com.fasterxml.jackson.databind.JsonMappingException;
029import com.fasterxml.jackson.databind.JsonNode;
030import com.fasterxml.jackson.databind.SerializerProvider;
031import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
032import com.fasterxml.jackson.databind.annotation.JsonSerialize;
033import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
034import com.fasterxml.jackson.databind.node.JsonNodeFactory;
035import com.fasterxml.jackson.databind.ser.std.StdSerializer;
036import java.io.IOException;
037import java.lang.reflect.InvocationTargetException;
038import java.lang.reflect.Type;
039import java.math.BigDecimal;
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.Collections;
043import java.util.HashMap;
044import java.util.List;
045import java.util.Map;
046import java.util.Optional;
047import java.util.concurrent.ConcurrentHashMap;
048import java.util.stream.Stream;
049
050/**
051 * A class that serializes/deserializes as a JSON RPC.
052 */
053@JsonDeserialize(using = JsonRpc.Deserializer.class)
054@JsonSerialize(using = JsonRpc.Serializer.class)
055public class JsonRpc {
056
057    private static final String JSONRPC_VERSION = "2.0";
058    private static Map<Class<? extends JsonRpc>,
059            Map<String, List<Type>>> paramTypes = new ConcurrentHashMap<>();
060    private JsonNode id;
061    private String method;
062    private List<Object> params = new ArrayList<>();
063
064    /**
065     * Instantiates a new json rpc.
066     */
067    public JsonRpc() {
068        // Default constructor
069    }
070
071    /**
072     * Instantiates a new json rpc.
073     *
074     * @param method the method
075     */
076    public JsonRpc(String method) {
077        this.method = method;
078    }
079
080    /**
081     * Specify the types of the parameters for specific methods.
082     * Used for deserialization.
083     *
084     * @param clazz the clazz
085     * @param types the param types
086     */
087    public static void setParamTypes(Class<? extends JsonRpc> clazz,
088            Map<String, List<Type>> types) {
089        paramTypes.put(clazz, new HashMap<>(types));
090    }
091
092    /**
093     * Adds the parameter types for the given method.
094     * Used for deserialization.
095     *
096     * @param clazz the clazz
097     * @param method the method
098     * @param types the types
099     */
100    public static void addParamTypes(Class<? extends JsonRpc> clazz,
101            String method, List<Type> types) {
102        paramTypes.computeIfAbsent(clazz, k -> new HashMap<>())
103            .put(method, types);
104    }
105
106    /**
107     * An optional request id.
108     * 
109     * @return the id
110     */
111    @SuppressWarnings("PMD.ShortMethodName")
112    public Optional<JsonNode> id() {
113        return Optional.ofNullable(id);
114    }
115
116    /**
117     * Sets the id.
118     *
119     * @param id the id
120     * @return the json rpc
121     */
122    public JsonRpc setId(Number id) {
123        this.id = JsonNodeFactory.instance
124            .numberNode(new BigDecimal(id.toString()));
125        return this;
126    }
127
128    /**
129     * Sets the id.
130     *
131     * @param id the id
132     * @return the json rpc
133     */
134    public JsonRpc setId(String id) {
135        this.id = JsonNodeFactory.instance.textNode(id);
136        return this;
137    }
138
139    /**
140     * The invoked method.
141     * 
142     * @return the method
143     */
144    public String method() {
145        return method;
146    }
147
148    /**
149     * The parameters.
150     * 
151     * @return the params
152     */
153    public Object[] params() {
154        return params.toArray();
155    }
156
157    /**
158     * Sets the params.
159     *
160     * @param params the params
161     * @return the json rpc
162     */
163    public JsonRpc setParams(Object... params) {
164        this.params = new ArrayList<>(Arrays.asList(params));
165        return this;
166    }
167
168    /**
169     * Adds the param.
170     *
171     * @param param the param
172     * @return the json rpc
173     */
174    public JsonRpc addParam(Object param) {
175        params.add(param);
176        return this;
177    }
178
179    /**
180     * Returns the parameter's value as string.
181     *
182     * @param index the index
183     * @return the string
184     */
185    public String asString(int index) {
186        return params.get(index).toString();
187    }
188
189    /**
190     * Returns the parameter's value as the requested type.
191     *
192     * @param <T> the generic type
193     * @param index the index
194     * @return the string
195     */
196    @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
197    public <T> T param(int index) {
198        return (T) params.get(index);
199    }
200
201    /**
202     * Returns the parameter's value as stream.
203     *
204     * @param <T> the generic type
205     * @param cls the class of the stream elements
206     * @param index the index of the parameter
207     * @return the stream
208     */
209    @SuppressWarnings("unchecked")
210    public <T> Stream<T> streamOf(Class<T> cls, int index) {
211        return Arrays.stream((T[]) params.get(index));
212    }
213
214    /**
215     * To string.
216     *
217     * @return the string
218     */
219    @Override
220    public String toString() {
221        return "JsonRpc [method=" + method + ", params=" + params + "]";
222    }
223
224    /**
225     * A Serializer for a {@link JsonRpc}.
226     */
227    @SuppressWarnings("serial")
228    public static class Serializer extends StdSerializer<JsonRpc> {
229
230        /**
231         * Instantiates a new serializer.
232         */
233        public Serializer() {
234            super((Class<JsonRpc>) null);
235        }
236
237        /**
238         * Instantiates a new serializer.
239         *
240         * @param type the type
241         */
242        public Serializer(Class<JsonRpc> type) {
243            super(type);
244        }
245
246        /**
247         * Serialize.
248         *
249         * @param obj the obj
250         * @param generator the generator
251         * @param ctx the ctx
252         * @throws IOException Signals that an I/O exception has occurred.
253         */
254        @Override
255        public void serialize(JsonRpc obj, JsonGenerator generator,
256                SerializerProvider ctx) throws IOException {
257            generator.writeStartObject();
258            generator.writeFieldName("jsonrpc");
259            generator.writeString(JSONRPC_VERSION);
260            generator.writeFieldName("method");
261            generator.writeString(obj.method);
262            generator.writeFieldName("params");
263            ctx.defaultSerializeValue(obj.params, generator);
264            generator.writeEndObject();
265        }
266    }
267
268    /**
269     * A Deserializer for a {@link JsonRpc}.
270     */
271    public static class Deserializer extends JsonDeserializer<JsonRpc>
272            implements ContextualDeserializer {
273
274        private JavaType type;
275
276        /**
277         * Creates a new instance for calling
278         * {@link #createContextual(DeserializationContext, BeanProperty)},
279         * which is the real deserializer.
280         */
281        public Deserializer() {
282            // Default constructor
283        }
284
285        /**
286         * Instantiates a new deserializer for the given type.
287         *
288         * @param type the type
289         */
290        public Deserializer(JavaType type) {
291            this.type = type;
292        }
293
294        /**
295         * Creates the contextual.
296         *
297         * @param deserializationContext the deserialization context
298         * @param beanProperty the bean property
299         * @return the json deserializer
300         * @throws JsonMappingException the json mapping exception
301         */
302        @Override
303        public JsonDeserializer<?> createContextual(
304                DeserializationContext deserializationContext,
305                BeanProperty beanProperty) throws JsonMappingException {
306            // beanProperty is null when the type to deserialize is the
307            // top-level type or a generic type, not a type of a bean property
308            JavaType type = deserializationContext.getContextualType() != null
309                ? deserializationContext.getContextualType()
310                : beanProperty.getMember().getType();
311            return new Deserializer(type);
312        }
313
314        /**
315         * Deserialize.
316         *
317         * @param parser the parser
318         * @param ctx the ctx
319         * @return the json rpc
320         * @throws IOException Signals that an I/O exception has occurred.
321         */
322        @Override
323        public JsonRpc deserialize(JsonParser parser,
324                DeserializationContext ctx) throws IOException {
325            JsonRpc jsonRpc;
326            @SuppressWarnings("unchecked")
327            var clazz = (Class<? extends JsonRpc>) type.getRawClass();
328            try {
329                jsonRpc = clazz.getDeclaredConstructor().newInstance();
330            } catch (InstantiationException | IllegalAccessException
331                    | IllegalArgumentException | InvocationTargetException
332                    | NoSuchMethodException | SecurityException e) {
333                throw new IOException("Failed to create "
334                    + type.getTypeName(), e);
335            }
336            while (true) {
337                var token = parser.nextToken();
338                if (token == null) {
339                    return null;
340                }
341                if (token == JsonToken.END_OBJECT) {
342                    return jsonRpc;
343                }
344                if (token == JsonToken.FIELD_NAME) {
345                    handleField(parser, ctx, jsonRpc);
346                }
347            }
348        }
349
350        private void handleField(JsonParser parser, DeserializationContext ctx,
351                JsonRpc jsonRpc) throws IOException {
352            var key = parser.getValueAsString();
353            parser.nextToken();
354            switch (key) {
355            case "id":
356                jsonRpc.id = ctx.readTree(parser);
357                break;
358            case "method":
359                jsonRpc.method = ctx.readValue(parser, String.class);
360                break;
361            case "params":
362                jsonRpc.params = handleParams(parser, ctx, jsonRpc);
363                break;
364            default:
365                ctx.readTree(parser);
366                break;
367            }
368        }
369
370        private List<Object> handleParams(JsonParser parser,
371                DeserializationContext ctx, JsonRpc jsonRpc)
372                throws IOException {
373            List<Object> params = new ArrayList<>();
374            @SuppressWarnings("unchecked")
375            var clazz = (Class<? extends JsonRpc>) type.getRawClass();
376            var typeIter = Optional.ofNullable(paramTypes.get(clazz))
377                .map(m -> m.get(jsonRpc.method))
378                .map(List::iterator).orElse(Collections.emptyIterator());
379
380            // Process array elements
381            while (true) {
382                if (parser.nextToken() == JsonToken.END_ARRAY) {
383                    break;
384                }
385                Type paramType = typeIter.hasNext() ? typeIter.next()
386                    : ctx.getTypeFactory().constructType(Object.class);
387                var item = ctx.readValue(parser,
388                    ctx.getTypeFactory().constructType(paramType));
389                params.add(item);
390            }
391            return params;
392        }
393
394    }
395}