Search code examples
javaspringresttemplate

Spring can't construct query params from complex object for rest template


The question is somewhat different from Spring's RestTemplate: complex object to query params Suppose there is a request that takes some parameters in the form of an object:

@GetMapping("getRoute")
public ActionResult<Response> get(@Validated GetRequest req) {
//do some logic
}

The request itself looks like this:

public class GetRequest{
    private String firstField;
    private Long secondField;
    private ComplexObject thirdField;

    private static class ComplexObject{
        private String subFirstField;
        private Long subSecondField;
    }
}

Therefore, when I execute a query with this object from RestTemplate, I want to get a URI like this:

/getRoute?firstField=val&secondField=val&thirdField.subFirstField=val&thirdField.subSecondField=val

How can i do this? The object can be absolutely anything. The bigger question is how to translate such an this object into a MultiValueMap for UriComponentsBuilder.

The solution would be simple if it were a POST request, but I need it for GET.

I only know that the Springfox library uses this approach when it generates the Swagger API, but the logic inside is too complex. My scenario:

@SuppressWarnings("unchecked")
public <T> ActionResult<T> getAction(String relativeUrl, Class<T> responseType, @Nullable Object paramsObject) {
    UriComponentsBuilder builder = createUriComponentsBuilder(relativeUrl, paramsObject);
    final ApiErrorCode errorCode;
    try {
        ResponseEntity<ActionResult> responseEntity = this.restTemplate.getForEntity(builder.build().toUri(), ActionResult.class);
        ActionResult responseBody = responseEntity.getBody();

        if (!Objects.requireNonNull(responseBody).isSuccess()) {
            return responseBody;
        }
        return ActionResult.ok(this.mapper.convertValue(responseBody.getValue(), responseType));
    } catch (RestClientException | HttpMessageNotReadableException e) {
        errorCode = ApiErrorCode.API_CONNECTION_ERROR;
    } catch (Exception e) {
        errorCode = ApiErrorCode.INTERNAL_SERVER_ERROR;
    }
    return ActionResult.fail(errorCode);
}

private UriComponentsBuilder createUriComponentsBuilder(String relativeUrl, @Nullable Object object) {
    String url = this.baseUrl;
    if (StringUtils.hasText(relativeUrl)) {
        url += relativeUrl;
    }

    UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
    if (object != null) {
        builder.queryParams(this.convertToMultiValueMap(object));
    }

    return builder;
}

private MultiValueMap<String, String> convertToMultiValueMap(Object object) {
    //todo object to params
}

Solution

  • I do not know why I got a downvote. But I tried to make my implementation, perhaps somewhere there may be unforeseen errors.

    public static LinkedMultiValueMap<String, String> toUrlParams(Object value) {
        final LinkedMultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        final List<Field> declaredFields = getAllFields(value);
        for (Field field : declaredFields) {
            ReflectionUtils.makeAccessible(field);
            final String name = field.getName();
            final Object fieldVal = ReflectionUtils.getField(field, value);
            mapFields(params, name, fieldVal);
        }
        return params;
    }
    
    private static List<Field> getAllFields(Object t) {
        final List<Field> fields = new ArrayList<>();
        Class clazz = t.getClass();
        while (clazz.getSuperclass() != null) {
            final Field[] declaredFields = clazz.getDeclaredFields();
            for (Field field : declaredFields) {
                final int modifiers = field.getModifiers();
                if (!(Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers))) {
                    fields.add(field);
                }
            }
            clazz = clazz.getSuperclass();
        }
        return fields;
    }
    
    private static void mapFields(LinkedMultiValueMap<String, String> params,
                                  String fieldName,
                                  @Nullable Object fieldVal) {
        if (fieldVal != null) {
            final Class<?> fieldClass = fieldVal.getClass();
            if (BeanUtils.isSimpleValueType(fieldClass) || fieldVal instanceof Number || fieldVal instanceof UUID) {
                if (fieldClass.isEnum()) {
                    params.add(fieldName, ((Enum) fieldVal).name());
                } else {
                    params.add(fieldName, fieldVal.toString());
                }
            } else {
                if (fieldVal instanceof Map) {
                    throw new IllegalArgumentException("Map is not allowed for url params");
                }
                if (fieldVal instanceof List) {
                    final Iterator iterator = ((Iterable) fieldVal).iterator();
                    int i = 0;
                    while (iterator.hasNext()) {
                        final Object iterElement = iterator.next();
                        mapFields(params, fieldName + "[" + i + "]", iterElement);
                        i++;
                    }
                } else {
                    if (fieldVal instanceof Set) {
                        for (Object iterElement : ((Iterable) fieldVal)) {
                            mapFields(params, fieldName, iterElement);
                        }
                    } else {
                        if (fieldVal instanceof Collection) {
                            throw new IllegalArgumentException("Unknown collection, expected List or Set, but was " + fieldVal.getClass());
                        }
                        if (fieldVal.getClass().isArray()) {
                            final int length = Array.getLength(fieldVal);
                            for (int i = 0; i < length; i++) {
                                Object arrayElement = Array.get(fieldVal, i);
                                mapFields(params, fieldName + "[" + i + "]", arrayElement);
                            }
                        } else {
                            final List<Field> declaredFields = getAllFields(fieldVal);
    
                            for (Field field : declaredFields) {
                                ReflectionUtils.makeAccessible(field);
                                final String name = field.getName();
                                final Object nestedField = ReflectionUtils.getField(field, fieldVal);
                                mapFields(params, fieldName + "." + name, nestedField);
                            }
                        }
                    }
                }
            }
        }
    }