|
17 | 17 | import java.util.*; |
18 | 18 | import java.util.Map.Entry; |
19 | 19 | import java.util.regex.Pattern; |
| 20 | +import java.lang.reflect.ParameterizedType; |
| 21 | +import java.lang.reflect.Type; |
| 22 | +import java.lang.reflect.GenericArrayType; |
20 | 23 |
|
21 | 24 | /** |
22 | 25 | * A JSONObject is an unordered collection of name/value pairs. Its external |
@@ -3207,4 +3210,250 @@ private static JSONException recursivelyDefinedObjectException(String key) { |
3207 | 3210 | "JavaBean object contains recursively defined member variable of key " + quote(key) |
3208 | 3211 | ); |
3209 | 3212 | } |
| 3213 | + |
| 3214 | + /** |
| 3215 | + * Helper method to extract the raw Class from Type. |
| 3216 | + */ |
| 3217 | + private Class<?> getRawType(Type type) { |
| 3218 | + if (type instanceof Class) { |
| 3219 | + return (Class<?>) type; |
| 3220 | + } else if (type instanceof ParameterizedType) { |
| 3221 | + return (Class<?>) ((ParameterizedType) type).getRawType(); |
| 3222 | + } else if (type instanceof GenericArrayType) { |
| 3223 | + return Object[].class; // Simplified handling for arrays |
| 3224 | + } |
| 3225 | + return Object.class; // Fallback |
| 3226 | + } |
| 3227 | + |
| 3228 | + /** |
| 3229 | + * Extracts the element Type for a Collection Type. |
| 3230 | + */ |
| 3231 | + private Type getElementType(Type type) { |
| 3232 | + if (type instanceof ParameterizedType) { |
| 3233 | + Type[] args = ((ParameterizedType) type).getActualTypeArguments(); |
| 3234 | + return args.length > 0 ? args[0] : Object.class; |
| 3235 | + } |
| 3236 | + return Object.class; |
| 3237 | + } |
| 3238 | + |
| 3239 | + /** |
| 3240 | + * Extracts the key and value Types for a Map Type. |
| 3241 | + */ |
| 3242 | + private Type[] getMapTypes(Type type) { |
| 3243 | + if (type instanceof ParameterizedType) { |
| 3244 | + Type[] args = ((ParameterizedType) type).getActualTypeArguments(); |
| 3245 | + if (args.length == 2) { |
| 3246 | + return args; |
| 3247 | + } |
| 3248 | + } |
| 3249 | + return new Type[]{Object.class, Object.class}; // Default: String keys, Object values |
| 3250 | + } |
| 3251 | + |
| 3252 | + /** |
| 3253 | + * Deserializes a JSON string into an instance of the specified class. |
| 3254 | + * |
| 3255 | + * <p>This method attempts to map JSON key-value pairs to the corresponding fields |
| 3256 | + * of the given class. It supports basic data types including int, double, float, |
| 3257 | + * long, and boolean (as well as their boxed counterparts). The class must have a |
| 3258 | + * no-argument constructor, and the field names in the class must match the keys |
| 3259 | + * in the JSON string. |
| 3260 | + * |
| 3261 | + * @param jsonString json in string format |
| 3262 | + * @param clazz the class of the object to be returned |
| 3263 | + * @return an instance of Object T with fields populated from the JSON string |
| 3264 | + */ |
| 3265 | + public static <T> T fromJson(String jsonString, Class<T> clazz) { |
| 3266 | + JSONObject jsonObject = new JSONObject(jsonString); |
| 3267 | + return jsonObject.fromJson(clazz); |
| 3268 | + } |
| 3269 | + |
| 3270 | + /** |
| 3271 | + * Deserializes a JSON string into an instance of the specified class. |
| 3272 | + * |
| 3273 | + * <p>This method attempts to map JSON key-value pairs to the corresponding fields |
| 3274 | + * of the given class. It supports basic data types including {@code int}, {@code double}, |
| 3275 | + * {@code float}, {@code long}, and {@code boolean}, as well as their boxed counterparts. |
| 3276 | + * The target class must have a no-argument constructor, and its field names must match |
| 3277 | + * the keys in the JSON string. |
| 3278 | + * |
| 3279 | + * <p><strong>Note:</strong> Only classes that are explicitly supported and registered within |
| 3280 | + * the {@code JSONObject} context can be deserialized. If the provided class is not among those, |
| 3281 | + * this method will not be able to deserialize it. This ensures that only a limited and |
| 3282 | + * controlled set of types can be instantiated from JSON for safety and predictability. |
| 3283 | + * |
| 3284 | + * @param clazz the class of the object to be returned |
| 3285 | + * @param <T> the type of the object |
| 3286 | + * @return an instance of type {@code T} with fields populated from the JSON string |
| 3287 | + * @throws IllegalArgumentException if the class is not supported for deserialization |
| 3288 | + */ |
| 3289 | + @SuppressWarnings("unchecked") |
| 3290 | + public <T> T fromJson(Class<T> clazz) { |
| 3291 | + try { |
| 3292 | + T obj = clazz.getDeclaredConstructor().newInstance(); |
| 3293 | + for (Field field : clazz.getDeclaredFields()) { |
| 3294 | + field.setAccessible(true); |
| 3295 | + String fieldName = field.getName(); |
| 3296 | + if (has(fieldName)) { |
| 3297 | + Object value = get(fieldName); |
| 3298 | + Type fieldType = field.getGenericType(); |
| 3299 | + Object convertedValue = convertValue(value, fieldType); |
| 3300 | + field.set(obj, convertedValue); |
| 3301 | + } |
| 3302 | + } |
| 3303 | + return obj; |
| 3304 | + } catch (NoSuchMethodException e) { |
| 3305 | + throw new JSONException("No no-arg constructor for class: " + clazz.getName(), e); |
| 3306 | + } catch (Exception e) { |
| 3307 | + throw new JSONException("Failed to instantiate or set field for class: " + clazz.getName(), e); |
| 3308 | + } |
| 3309 | + } |
| 3310 | + |
| 3311 | + /** |
| 3312 | + * Recursively converts a value to the target Type, handling nested generics for Collections and Maps. |
| 3313 | + */ |
| 3314 | + private Object convertValue(Object value, Type targetType) throws JSONException { |
| 3315 | + if (value == null) { |
| 3316 | + return null; |
| 3317 | + } |
| 3318 | + |
| 3319 | + Class<?> rawType = getRawType(targetType); |
| 3320 | + |
| 3321 | + // Direct assignment |
| 3322 | + if (rawType.isAssignableFrom(value.getClass())) { |
| 3323 | + return value; |
| 3324 | + } |
| 3325 | + |
| 3326 | + if (rawType == int.class || rawType == Integer.class) { |
| 3327 | + return ((Number) value).intValue(); |
| 3328 | + } else if (rawType == double.class || rawType == Double.class) { |
| 3329 | + return ((Number) value).doubleValue(); |
| 3330 | + } else if (rawType == float.class || rawType == Float.class) { |
| 3331 | + return ((Number) value).floatValue(); |
| 3332 | + } else if (rawType == long.class || rawType == Long.class) { |
| 3333 | + return ((Number) value).longValue(); |
| 3334 | + } else if (rawType == boolean.class || rawType == Boolean.class) { |
| 3335 | + return value; |
| 3336 | + } else if (rawType == String.class) { |
| 3337 | + return value; |
| 3338 | + } else if (rawType == BigDecimal.class) { |
| 3339 | + return new BigDecimal((String) value); |
| 3340 | + } else if (rawType == BigInteger.class) { |
| 3341 | + return new BigInteger((String) value); |
| 3342 | + } |
| 3343 | + |
| 3344 | + // Enum conversion |
| 3345 | + if (rawType.isEnum() && value instanceof String) { |
| 3346 | + return stringToEnum(rawType, (String) value); |
| 3347 | + } |
| 3348 | + |
| 3349 | + // Collection handling (e.g., List<List<Map<String, Integer>>>) |
| 3350 | + if (Collection.class.isAssignableFrom(rawType)) { |
| 3351 | + if (value instanceof JSONArray) { |
| 3352 | + Type elementType = getElementType(targetType); |
| 3353 | + return fromJsonArray((JSONArray) value, rawType, elementType); |
| 3354 | + } |
| 3355 | + } |
| 3356 | + // Map handling (e.g., Map<Integer, List<String>>) |
| 3357 | + else if (Map.class.isAssignableFrom(rawType) && value instanceof JSONObject) { |
| 3358 | + Type[] mapTypes = getMapTypes(targetType); |
| 3359 | + Type keyType = mapTypes[0]; |
| 3360 | + Type valueType = mapTypes[1]; |
| 3361 | + return convertToMap((JSONObject) value, keyType, valueType, rawType); |
| 3362 | + } |
| 3363 | + // POJO handling (including custom classes like Tuple<Integer, String, Integer>) |
| 3364 | + else if (!rawType.isPrimitive() && !rawType.isEnum() && value instanceof JSONObject) { |
| 3365 | + // Recurse with the raw class for POJO deserialization |
| 3366 | + return ((JSONObject) value).fromJson(rawType); |
| 3367 | + } |
| 3368 | + |
| 3369 | + // Fallback |
| 3370 | + return value.toString(); |
| 3371 | + } |
| 3372 | + |
| 3373 | + /** |
| 3374 | + * Converts a JSONObject to a Map with the specified generic key and value Types. |
| 3375 | + * Supports nested types via recursive convertValue. |
| 3376 | + */ |
| 3377 | + private Map<?, ?> convertToMap(JSONObject jsonMap, Type keyType, Type valueType, Class<?> mapType) throws JSONException { |
| 3378 | + try { |
| 3379 | + @SuppressWarnings("unchecked") |
| 3380 | + Map<Object, Object> createdMap = new HashMap(); |
| 3381 | + |
| 3382 | + for (Object keyObj : jsonMap.keySet()) { |
| 3383 | + String keyStr = (String) keyObj; |
| 3384 | + Object mapValue = jsonMap.get(keyStr); |
| 3385 | + // Convert key (e.g., String to Integer for Map<Integer, ...>) |
| 3386 | + Object convertedKey = convertValue(keyStr, keyType); |
| 3387 | + // Convert value recursively (handles nesting) |
| 3388 | + Object convertedValue = convertValue(mapValue, valueType); |
| 3389 | + createdMap.put(convertedKey, convertedValue); |
| 3390 | + } |
| 3391 | + return createdMap; |
| 3392 | + } catch (Exception e) { |
| 3393 | + throw new JSONException("Failed to convert JSONObject to Map: " + mapType.getName(), e); |
| 3394 | + } |
| 3395 | + } |
| 3396 | + |
| 3397 | + /** |
| 3398 | + * Converts a String to an Enum value. |
| 3399 | + */ |
| 3400 | + private <E> E stringToEnum(Class<?> enumClass, String value) throws JSONException { |
| 3401 | + try { |
| 3402 | + @SuppressWarnings("unchecked") |
| 3403 | + Class<E> enumType = (Class<E>) enumClass; |
| 3404 | + Method valueOfMethod = enumType.getMethod("valueOf", String.class); |
| 3405 | + return (E) valueOfMethod.invoke(null, value); |
| 3406 | + } catch (Exception e) { |
| 3407 | + throw new JSONException("Failed to convert string to enum: " + value + " for " + enumClass.getName(), e); |
| 3408 | + } |
| 3409 | + } |
| 3410 | + |
| 3411 | + /** |
| 3412 | + * Deserializes a JSONArray into a Collection, supporting nested generics. |
| 3413 | + * Uses recursive convertValue for elements. |
| 3414 | + */ |
| 3415 | + @SuppressWarnings("unchecked") |
| 3416 | + private <T> Collection<T> fromJsonArray(JSONArray jsonArray, Class<?> collectionType, Type elementType) throws JSONException { |
| 3417 | + try { |
| 3418 | + Collection<T> collection = getCollection(collectionType); |
| 3419 | + |
| 3420 | + for (int i = 0; i < jsonArray.length(); i++) { |
| 3421 | + Object jsonElement = jsonArray.get(i); |
| 3422 | + // Recursively convert each element using the full element Type (handles nesting) |
| 3423 | + Object convertedValue = convertValue(jsonElement, elementType); |
| 3424 | + collection.add((T) convertedValue); |
| 3425 | + } |
| 3426 | + return collection; |
| 3427 | + } catch (Exception e) { |
| 3428 | + throw new JSONException("Failed to convert JSONArray to Collection: " + collectionType.getName(), e); |
| 3429 | + } |
| 3430 | + } |
| 3431 | + |
| 3432 | + /** |
| 3433 | + * Creates and returns a new instance of a supported {@link Collection} implementation |
| 3434 | + * based on the specified collection type. |
| 3435 | + * <p> |
| 3436 | + * This method currently supports the following collection types: |
| 3437 | + * <ul> |
| 3438 | + * <li>{@code List.class}</li> |
| 3439 | + * <li>{@code ArrayList.class}</li> |
| 3440 | + * <li>{@code Set.class}</li> |
| 3441 | + * <li>{@code HashSet.class}</li> |
| 3442 | + * </ul> |
| 3443 | + * If the provided type does not match any of the supported types, a {@link JSONException} |
| 3444 | + * is thrown. |
| 3445 | + * |
| 3446 | + * @param collectionType the {@link Class} object representing the desired collection type |
| 3447 | + * @return a new empty instance of the specified collection type |
| 3448 | + * @throws JSONException if the specified type is not a supported collection type |
| 3449 | + */ |
| 3450 | + private Collection getCollection(Class<?> collectionType) throws JSONException { |
| 3451 | + if (collectionType == List.class || collectionType == ArrayList.class) { |
| 3452 | + return new ArrayList(); |
| 3453 | + } else if (collectionType == Set.class || collectionType == HashSet.class) { |
| 3454 | + return new HashSet(); |
| 3455 | + } else { |
| 3456 | + throw new JSONException("Unsupported Collection type: " + collectionType.getName()); |
| 3457 | + } |
| 3458 | + } |
3210 | 3459 | } |
0 commit comments