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    @SuppressWarnings("PMD.FieldNamingConventions")
058    private static final String JSONRPC_VERSION = "2.0";
059    private static Map<Class<? extends JsonRpc>,
060            Map<String, List<Type>>> paramTypes = new ConcurrentHashMap<>();
061    private JsonNode id;
062    private String method;
063    private List<Object> params = new ArrayList<>();
064
065    /**
066     * Instantiates a new json rpc.
067     */
068    public JsonRpc() {
069        // Default constructor
070    }
071
072    /**
073     * Instantiates a new json rpc.
074     *
075     * @param method the method
076     */
077    public JsonRpc(String method) {
078        this.method = method;
079    }
080
081    /**
082     * Specify the types of the parameters for specific methods.
083     * Used for deserialization.
084     *
085     * @param clazz the clazz
086     * @param types the param types
087     */
088    public static void setParamTypes(Class<? extends JsonRpc> clazz,
089            Map<String, List<Type>> types) {
090        paramTypes.put(clazz, new HashMap<>(types));
091    }
092
093    /**
094     * Adds the parameter types for the given method.
095     * Used for deserialization.
096     *
097     * @param clazz the clazz
098     * @param method the method
099     * @param types the types
100     */
101    public static void addParamTypes(Class<? extends JsonRpc> clazz,
102            String method, List<Type> types) {
103        paramTypes.computeIfAbsent(clazz, k -> new HashMap<>())
104            .put(method, types);
105    }
106
107    /**
108     * An optional request id.
109     * 
110     * @return the id
111     */
112    @SuppressWarnings("PMD.ShortMethodName")
113    public Optional<JsonNode> id() {
114        return Optional.ofNullable(id);
115    }
116
117    /**
118     * Sets the id.
119     *
120     * @param id the id
121     * @return the json rpc
122     */
123    public JsonRpc setId(Number id) {
124        this.id = JsonNodeFactory.instance
125            .numberNode(new BigDecimal(id.toString()));
126        return this;
127    }
128
129    /**
130     * Sets the id.
131     *
132     * @param id the id
133     * @return the json rpc
134     */
135    public JsonRpc setId(String id) {
136        this.id = JsonNodeFactory.instance.textNode(id);
137        return this;
138    }
139
140    /**
141     * The invoked method.
142     * 
143     * @return the method
144     */
145    public String method() {
146        return method;
147    }
148
149    /**
150     * The parameters.
151     * 
152     * @return the params
153     */
154    public Object[] params() {
155        return params.toArray();
156    }
157
158    /**
159     * Sets the params.
160     *
161     * @param params the params
162     * @return the json rpc
163     */
164    @SuppressWarnings("PMD.LinguisticNaming")
165    public JsonRpc setParams(Object... params) {
166        this.params = new ArrayList<>(Arrays.asList(params));
167        return this;
168    }
169
170    /**
171     * Adds the param.
172     *
173     * @param param the param
174     * @return the json rpc
175     */
176    public JsonRpc addParam(Object param) {
177        params.add(param);
178        return this;
179    }
180
181    /**
182     * Returns the parameter's value as string.
183     *
184     * @param index the index
185     * @return the string
186     */
187    public String asString(int index) {
188        return params.get(index).toString();
189    }
190
191    /**
192     * Returns the parameter's value as the requested type.
193     *
194     * @param <T> the generic type
195     * @param index the index
196     * @return the string
197     */
198    @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
199    public <T> T param(int index) {
200        return (T) params.get(index);
201    }
202
203    /**
204     * Returns the parameter's value as stream.
205     *
206     * @param <T> the generic type
207     * @param cls the class of the stream elements
208     * @param index the index of the parameter
209     * @return the stream
210     */
211    @SuppressWarnings("unchecked")
212    public <T> Stream<T> streamOf(Class<T> cls, int index) {
213        return Arrays.stream((T[]) params.get(index));
214    }
215
216    /**
217     * To string.
218     *
219     * @return the string
220     */
221    @Override
222    public String toString() {
223        return "JsonRpc [method=" + method + ", params=" + params + "]";
224    }
225
226    /**
227     * A Serializer for a {@link JsonRpc}.
228     */
229    @SuppressWarnings("serial")
230    public static class Serializer extends StdSerializer<JsonRpc> {
231
232        /**
233         * Instantiates a new serializer.
234         */
235        public Serializer() {
236            super((Class<JsonRpc>) null);
237        }
238
239        /**
240         * Instantiates a new serializer.
241         *
242         * @param type the type
243         */
244        public Serializer(Class<JsonRpc> type) {
245            super(type);
246        }
247
248        /**
249         * Serialize.
250         *
251         * @param obj the obj
252         * @param generator the generator
253         * @param ctx the ctx
254         * @throws IOException Signals that an I/O exception has occurred.
255         */
256        @Override
257        public void serialize(JsonRpc obj, JsonGenerator generator,
258                SerializerProvider ctx) throws IOException {
259            generator.writeStartObject();
260            generator.writeFieldName("jsonrpc");
261            generator.writeString(JSONRPC_VERSION);
262            generator.writeFieldName("method");
263            generator.writeString(obj.method);
264            generator.writeFieldName("params");
265            ctx.defaultSerializeValue(obj.params, generator);
266            generator.writeEndObject();
267        }
268    }
269
270    /**
271     * A Deserializer for a {@link JsonRpc}.
272     */
273    public static class Deserializer extends JsonDeserializer<JsonRpc>
274            implements ContextualDeserializer {
275
276        private JavaType type;
277
278        /**
279         * Creates a new instance for calling
280         * {@link #createContextual(DeserializationContext, BeanProperty)},
281         * which is the real deserializer.
282         */
283        public Deserializer() {
284            // Default constructor
285        }
286
287        /**
288         * Instantiates a new deserializer for the given type.
289         *
290         * @param type the type
291         */
292        public Deserializer(JavaType type) {
293            this.type = type;
294        }
295
296        /**
297         * Creates the contextual.
298         *
299         * @param deserializationContext the deserialization context
300         * @param beanProperty the bean property
301         * @return the json deserializer
302         * @throws JsonMappingException the json mapping exception
303         */
304        @Override
305        public JsonDeserializer<?> createContextual(
306                DeserializationContext deserializationContext,
307                BeanProperty beanProperty) throws JsonMappingException {
308            // beanProperty is null when the type to deserialize is the
309            // top-level type or a generic type, not a type of a bean property
310            JavaType type = deserializationContext.getContextualType() != null
311                ? deserializationContext.getContextualType()
312                : beanProperty.getMember().getType();
313            return new Deserializer(type);
314        }
315
316        /**
317         * Deserialize.
318         *
319         * @param parser the parser
320         * @param ctx the ctx
321         * @return the json rpc
322         * @throws IOException Signals that an I/O exception has occurred.
323         */
324        @Override
325        public JsonRpc deserialize(JsonParser parser,
326                DeserializationContext ctx) throws IOException {
327            JsonRpc jsonRpc;
328            @SuppressWarnings("unchecked")
329            var clazz = (Class<? extends JsonRpc>) type.getRawClass();
330            try {
331                jsonRpc = clazz.getDeclaredConstructor().newInstance();
332            } catch (InstantiationException | IllegalAccessException
333                    | IllegalArgumentException | InvocationTargetException
334                    | NoSuchMethodException | SecurityException e) {
335                throw new IOException("Failed to create "
336                    + type.getTypeName(), e);
337            }
338            while (true) {
339                var token = parser.nextToken();
340                if (token == null) {
341                    return null;
342                }
343                if (token == JsonToken.END_OBJECT) {
344                    return jsonRpc;
345                }
346                if (token == JsonToken.FIELD_NAME) {
347                    handleField(parser, ctx, jsonRpc);
348                }
349            }
350        }
351
352        private void handleField(JsonParser parser, DeserializationContext ctx,
353                JsonRpc jsonRpc) throws IOException {
354            var key = parser.getValueAsString();
355            parser.nextToken();
356            switch (key) {
357            case "id":
358                jsonRpc.id = ctx.readTree(parser);
359                break;
360            case "method":
361                jsonRpc.method = ctx.readValue(parser, String.class);
362                break;
363            case "params":
364                jsonRpc.params = handleParams(parser, ctx, jsonRpc);
365                break;
366            default:
367                ctx.readTree(parser);
368                break;
369            }
370        }
371
372        private List<Object> handleParams(JsonParser parser,
373                DeserializationContext ctx, JsonRpc jsonRpc)
374                throws IOException {
375            List<Object> params = new ArrayList<>();
376            @SuppressWarnings("unchecked")
377            var clazz = (Class<? extends JsonRpc>) type.getRawClass();
378            var typeIter = Optional.ofNullable(paramTypes.get(clazz))
379                .map(m -> m.get(jsonRpc.method))
380                .map(List::iterator).orElse(Collections.emptyIterator());
381
382            // Process array elements
383            while (true) {
384                if (parser.nextToken() == JsonToken.END_ARRAY) {
385                    break;
386                }
387                Type paramType = typeIter.hasNext() ? typeIter.next()
388                    : ctx.getTypeFactory().constructType(Object.class);
389                var item = ctx.readValue(parser,
390                    ctx.getTypeFactory().constructType(paramType));
391                params.add(item);
392            }
393            return params;
394        }
395
396    }
397}