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}