From db6c829ec7b7b89f09e960065717972d52540ae3 Mon Sep 17 00:00:00 2001 From: gromov_p Date: Tue, 15 Feb 2022 21:42:08 +0300 Subject: [PATCH 1/2] Validate required params by swagger annotations --- build.gradle | 2 + .../jsonrpc4j/JsonRpcBasicServer.java | 126 +++++++++++++++--- .../JsonRpcServerAnnotatedParamTest.java | 115 +++++++++++++--- 3 files changed, 212 insertions(+), 31 deletions(-) diff --git a/build.gradle b/build.gradle index 186e3886..c36b1245 100644 --- a/build.gradle +++ b/build.gradle @@ -109,6 +109,8 @@ dependencies { implementation 'commons-codec:commons-codec:1.10' implementation 'org.apache.httpcomponents:httpcore-nio:4.4.5' + implementation 'io.swagger:swagger-core:1.5.12' + testImplementation 'junit:junit:4.12' testImplementation 'org.easymock:easymock:3.4' testImplementation("org.springframework.boot:spring-boot-starter-web:${springBotVersion}") { diff --git a/src/main/java/com/googlecode/jsonrpc4j/JsonRpcBasicServer.java b/src/main/java/com/googlecode/jsonrpc4j/JsonRpcBasicServer.java index aa1db675..e4b8c027 100644 --- a/src/main/java/com/googlecode/jsonrpc4j/JsonRpcBasicServer.java +++ b/src/main/java/com/googlecode/jsonrpc4j/JsonRpcBasicServer.java @@ -1,33 +1,64 @@ package com.googlecode.jsonrpc4j; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.node.*; -import com.googlecode.jsonrpc4j.ErrorResolver.JsonError; -import net.iharder.Base64; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static com.googlecode.jsonrpc4j.ErrorResolver.JsonError.ERROR_NOT_HANDLED; +import static com.googlecode.jsonrpc4j.ErrorResolver.JsonError.INTERNAL_ERROR; +import static com.googlecode.jsonrpc4j.ReflectionUtil.findCandidateMethods; +import static com.googlecode.jsonrpc4j.ReflectionUtil.getParameterTypes; +import static com.googlecode.jsonrpc4j.Util.hasNonNullData; + +import javax.validation.ValidationException; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.annotation.Annotation; -import java.lang.reflect.*; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.lang.reflect.UndeclaredThrowableException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; +import java.util.stream.Collectors; -import static com.googlecode.jsonrpc4j.ErrorResolver.JsonError.ERROR_NOT_HANDLED; -import static com.googlecode.jsonrpc4j.ErrorResolver.JsonError.INTERNAL_ERROR; -import static com.googlecode.jsonrpc4j.ReflectionUtil.findCandidateMethods; -import static com.googlecode.jsonrpc4j.ReflectionUtil.getParameterTypes; -import static com.googlecode.jsonrpc4j.Util.hasNonNullData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.googlecode.jsonrpc4j.ErrorResolver.JsonError; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import net.iharder.Base64; /** * A JSON-RPC request server reads JSON-RPC requests from an input stream and writes responses to an output stream. @@ -256,6 +287,11 @@ public int handleRequest(final InputStream input, final OutputStream output) thr JsonResponse responseError = createResponseError(VERSION, NULL, JsonError.PARSE_ERROR); writeAndFlushValue(output, responseError.getResponse()); return responseError.getCode(); + } catch (ValidationException e) { + JsonResponse responseError = + createResponseError(VERSION, NULL, JsonError.METHOD_PARAMS_INVALID); + writeAndFlushValue(output, responseError.getResponse()); + return responseError.getCode(); } } @@ -462,7 +498,7 @@ private JsonResponse handleObject(final ObjectNode node) return createResponseSuccess(jsonRpc, id, handler.result); } return new JsonResponse(null, JsonError.OK.code); - } catch (JsonParseException | JsonMappingException e) { + } catch (JsonParseException | JsonMappingException | ValidationException e) { throw e; // rethrow this, it will be handled as PARSE_ERROR later } catch (Throwable e) { handler.error = e; @@ -583,6 +619,9 @@ private JsonNode invoke(Object target, Method method, List params) thr if (convertedParameterTransformer != null) { convertedParams = convertedParameterTransformer.transformConvertedParameters(target, convertedParams); } + if (!allowLessParams) { + collectApiModelsAndValidate(convertedParams); + } result = method.invoke(target, convertedParams); } @@ -591,6 +630,61 @@ private JsonNode invoke(Object target, Method method, List params) thr return hasReturnValue(method) ? mapper.valueToTree(result) : null; } + private void collectApiModelsAndValidate(Object[] params) { + + List requestModels = Arrays.stream(params) + .filter(model -> Arrays.stream(model.getClass().getAnnotations()) + .anyMatch(annotation -> annotation.annotationType() == ApiModel.class)) + .collect(Collectors.toList()); + requestModels.forEach(this::validateFields); + } + + private void validateFields(Object requestModel) { + + Arrays.stream(requestModel.getClass().getDeclaredFields()).forEach(field -> { + validateField(requestModel, field); + }); + + } + + private void validateField(Object requestModel, Field field) { + + if (fieldIsRequired(field)) { + try { + for (PropertyDescriptor pd : Introspector.getBeanInfo(requestModel.getClass()) + .getPropertyDescriptors()) { + if (pd.getReadMethod() != null && !"class".equals(pd.getName()) + && Objects.equals(pd.getName(), field.getName())) { + invokeGetterAndValidate(requestModel, pd.getReadMethod(), field.getName()); + } + } + } catch (IntrospectionException e) { + logger.warn("Unable to find getter for field {} in class {}", field.getName(), + requestModel.getClass().getName()); + } + + } + } + + private void invokeGetterAndValidate(Object o, Method gett, String fieldName) { + + try { + if (gett.invoke(o) == null) { + + throw new ValidationException( + String.format("Field %s cannot be empty", fieldName)); + } + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + } + + private boolean fieldIsRequired(Field field) { + + return Arrays.stream(field.getAnnotationsByType(ApiModelProperty.class)) + .anyMatch(ApiModelProperty::required); + } + private Object invokePrimitiveVarargs(Object target, Method method, List params, Class componentType) throws IllegalAccessException, InvocationTargetException { // need to cast to object here in order to support primitives. Object convertedParams = Array.newInstance(componentType, params.size()); diff --git a/src/test/java/com/googlecode/jsonrpc4j/server/JsonRpcServerAnnotatedParamTest.java b/src/test/java/com/googlecode/jsonrpc4j/server/JsonRpcServerAnnotatedParamTest.java index 6aad452f..9c155bc8 100644 --- a/src/test/java/com/googlecode/jsonrpc4j/server/JsonRpcServerAnnotatedParamTest.java +++ b/src/test/java/com/googlecode/jsonrpc4j/server/JsonRpcServerAnnotatedParamTest.java @@ -1,20 +1,5 @@ package com.googlecode.jsonrpc4j.server; -import com.fasterxml.jackson.databind.JsonNode; -import com.googlecode.jsonrpc4j.JsonRpcBasicServer; -import com.googlecode.jsonrpc4j.JsonRpcParam; -import com.googlecode.jsonrpc4j.util.Util; -import org.easymock.EasyMock; -import org.easymock.EasyMockRunner; -import org.easymock.Mock; -import org.easymock.MockType; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - import static com.googlecode.jsonrpc4j.ErrorResolver.JsonError.METHOD_PARAMS_INVALID; import static com.googlecode.jsonrpc4j.ErrorResolver.JsonError.PARSE_ERROR; import static com.googlecode.jsonrpc4j.JsonRpcBasicServer.RESULT; @@ -31,6 +16,24 @@ import static com.googlecode.jsonrpc4j.util.Util.param4; import static org.junit.Assert.assertEquals; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.easymock.EasyMock; +import org.easymock.EasyMockRunner; +import org.easymock.Mock; +import org.easymock.MockType; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import com.fasterxml.jackson.databind.JsonNode; +import com.googlecode.jsonrpc4j.JsonRpcBasicServer; +import com.googlecode.jsonrpc4j.JsonRpcParam; +import com.googlecode.jsonrpc4j.util.Util; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + @RunWith(EasyMockRunner.class) public class JsonRpcServerAnnotatedParamTest { @@ -180,9 +183,35 @@ public void callParseErrorJson() throws Exception { jsonRpcServerAnnotatedParam.handleRequest(Util.invalidJsonStream(), byteArrayOutputStream); assertEquals(PARSE_ERROR.code, errorCode(error(byteArrayOutputStream)).asInt()); } + + @Test + public void callMethodWithAllRequiredParametersInObjectAsParam() throws Exception { + EasyMock.expect(mockService.testMethodWithObjParam(EasyMock.anyObject(String.class),EasyMock.anyObject(TestRequestObj.class))).andReturn("success"); + EasyMock.replay(mockService); + jsonRpcServerAnnotatedParam.handleRequest(messageWithMapParamsStream("testMethodWithObjParam", param1, param2,"obj",new TestRequestObj("1","2","3")), byteArrayOutputStream); + assertEquals("success", result().textValue()); + } + + @Test + public void callMethodWithNullInRequiredParametersInObjectAsParam() throws Exception { + EasyMock.expect(mockService.testMethodWithObjParam(EasyMock.anyObject(String.class),EasyMock.anyObject(TestRequestObj.class))).andReturn("success"); + EasyMock.replay(mockService); + jsonRpcServerAnnotatedParam.handleRequest(messageWithMapParamsStream("testMethodWithObjParam", param1, param2,"obj",new TestRequestObj(null,"2","3")), byteArrayOutputStream); + assertEquals(METHOD_PARAMS_INVALID.code, errorCode(error(byteArrayOutputStream)).intValue()); + } + + @Test + public void callMethodWithNullInNonRequiredParametersInObjectAsParam() throws Exception { + EasyMock.expect(mockService.testMethodWithObjParam(EasyMock.anyObject(String.class),EasyMock.anyObject(TestRequestObj.class))).andReturn("success"); + EasyMock.replay(mockService); + jsonRpcServerAnnotatedParam.handleRequest(messageWithMapParamsStream("testMethodWithObjParam", param1, param2,"obj",new TestRequestObj("1","2",null)), byteArrayOutputStream); + assertEquals("success", result().textValue()); + } public interface ServiceInterfaceWithParamNameAnnotation { String testMethod(@JsonRpcParam("param1") String param1); + + String testMethodWithObjParam(@JsonRpcParam("param1") String param1,@JsonRpcParam("obj") TestRequestObj obj); String overloadedMethod(); @@ -196,4 +225,60 @@ public interface ServiceInterfaceWithParamNameAnnotation { String methodWithoutRequiredParam(@JsonRpcParam("param1") String stringParam1, @JsonRpcParam(value = "param2") String stringParam2); } + + @ApiModel + public static class TestRequestObj { + + public TestRequestObj(String requiredValue, String anotherRequiredValue, + String nonRequiredValue) { + + this.requiredValue = requiredValue; + this.anotherRequiredValue = anotherRequiredValue; + this.nonRequiredValue = nonRequiredValue; + } + + // for serialization + public TestRequestObj() { + + } + + @ApiModelProperty(required = true) + public String requiredValue; + + @ApiModelProperty(required = true) + public String anotherRequiredValue; + + @ApiModelProperty(required = false) + public String nonRequiredValue; + + public String getRequiredValue() { + + return requiredValue; + } + + public void setRequiredValue(String requiredValue) { + + this.requiredValue = requiredValue; + } + + public String getAnotherRequiredValue() { + + return anotherRequiredValue; + } + + public void setAnotherRequiredValue(String anotherRequiredValue) { + + this.anotherRequiredValue = anotherRequiredValue; + } + + public String getNonRequiredValue() { + + return nonRequiredValue; + } + + public void setNonRequiredValue(String nonRequiredValue) { + + this.nonRequiredValue = nonRequiredValue; + } + } } From 8e09dbda09199702f8b44912ba6941aa5ede21e0 Mon Sep 17 00:00:00 2001 From: gromov_p Date: Tue, 15 Feb 2022 22:22:37 +0300 Subject: [PATCH 2/2] Update for javax.validation api --- build.gradle | 2 +- .../com/googlecode/jsonrpc4j/JsonRpcBasicServer.java | 9 +++------ .../server/JsonRpcServerAnnotatedParamTest.java | 10 ++++------ 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index c36b1245..8eee7c63 100644 --- a/build.gradle +++ b/build.gradle @@ -109,7 +109,7 @@ dependencies { implementation 'commons-codec:commons-codec:1.10' implementation 'org.apache.httpcomponents:httpcore-nio:4.4.5' - implementation 'io.swagger:swagger-core:1.5.12' + implementation 'javax.validation:validation-api:2.0.1.Final' testImplementation 'junit:junit:4.12' testImplementation 'org.easymock:easymock:3.4' diff --git a/src/main/java/com/googlecode/jsonrpc4j/JsonRpcBasicServer.java b/src/main/java/com/googlecode/jsonrpc4j/JsonRpcBasicServer.java index e4b8c027..78f56a01 100644 --- a/src/main/java/com/googlecode/jsonrpc4j/JsonRpcBasicServer.java +++ b/src/main/java/com/googlecode/jsonrpc4j/JsonRpcBasicServer.java @@ -7,6 +7,7 @@ import static com.googlecode.jsonrpc4j.Util.hasNonNullData; import javax.validation.ValidationException; +import javax.validation.constraints.NotNull; import java.beans.IntrospectionException; import java.beans.Introspector; @@ -56,8 +57,6 @@ import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.googlecode.jsonrpc4j.ErrorResolver.JsonError; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; import net.iharder.Base64; /** @@ -633,8 +632,7 @@ private JsonNode invoke(Object target, Method method, List params) thr private void collectApiModelsAndValidate(Object[] params) { List requestModels = Arrays.stream(params) - .filter(model -> Arrays.stream(model.getClass().getAnnotations()) - .anyMatch(annotation -> annotation.annotationType() == ApiModel.class)) + .filter(param -> !param.getClass().isPrimitive()) .collect(Collectors.toList()); requestModels.forEach(this::validateFields); } @@ -681,8 +679,7 @@ private void invokeGetterAndValidate(Object o, Method gett, String fieldName) { private boolean fieldIsRequired(Field field) { - return Arrays.stream(field.getAnnotationsByType(ApiModelProperty.class)) - .anyMatch(ApiModelProperty::required); + return field.getAnnotationsByType(NotNull.class).length > 0; } private Object invokePrimitiveVarargs(Object target, Method method, List params, Class componentType) throws IllegalAccessException, InvocationTargetException { diff --git a/src/test/java/com/googlecode/jsonrpc4j/server/JsonRpcServerAnnotatedParamTest.java b/src/test/java/com/googlecode/jsonrpc4j/server/JsonRpcServerAnnotatedParamTest.java index 9c155bc8..5ebdfcd3 100644 --- a/src/test/java/com/googlecode/jsonrpc4j/server/JsonRpcServerAnnotatedParamTest.java +++ b/src/test/java/com/googlecode/jsonrpc4j/server/JsonRpcServerAnnotatedParamTest.java @@ -16,6 +16,8 @@ import static com.googlecode.jsonrpc4j.util.Util.param4; import static org.junit.Assert.assertEquals; +import javax.validation.constraints.NotNull; + import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -31,8 +33,6 @@ import com.googlecode.jsonrpc4j.JsonRpcBasicServer; import com.googlecode.jsonrpc4j.JsonRpcParam; import com.googlecode.jsonrpc4j.util.Util; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; @RunWith(EasyMockRunner.class) public class JsonRpcServerAnnotatedParamTest { @@ -226,7 +226,6 @@ public interface ServiceInterfaceWithParamNameAnnotation { String methodWithoutRequiredParam(@JsonRpcParam("param1") String stringParam1, @JsonRpcParam(value = "param2") String stringParam2); } - @ApiModel public static class TestRequestObj { public TestRequestObj(String requiredValue, String anotherRequiredValue, @@ -242,13 +241,12 @@ public TestRequestObj() { } - @ApiModelProperty(required = true) + @NotNull public String requiredValue; - @ApiModelProperty(required = true) + @NotNull public String anotherRequiredValue; - @ApiModelProperty(required = false) public String nonRequiredValue; public String getRequiredValue() {