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}