diff --git a/mvc/build.gradle b/mvc/build.gradle index 142ca323..c5cc1578 100644 --- a/mvc/build.gradle +++ b/mvc/build.gradle @@ -35,3 +35,11 @@ dependencies { test { useJUnitPlatform() } + +compileJava { + options.compilerArgs << '-parameters' +} + +compileTestJava { + options.compilerArgs << '-parameters' +} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java index 046c2644..28887167 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java @@ -4,7 +4,13 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.Map; + +import static com.interface21.webmvc.servlet.mvc.tobe.support.ParameterConverter.convertToMethodArg; public class HandlerExecution { private Method method; @@ -16,6 +22,22 @@ public HandlerExecution(Method method, Object object) { } public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { + if (isParameterMappingMethod()) { + return (ModelAndView) method.invoke(object, convertToMethodArg(method, request.getRequestURI(), request.getParameterMap())); + } return (ModelAndView) method.invoke(object, request, response); } + + private boolean isParameterMappingMethod() { + Parameter[] parameters = method.getParameters(); + if (parameters.length != 2) { + return false; + } + + if (!parameters[0].getType().equals(HttpServletRequest.class)) { + return false; + } + + return parameters[0].getType().equals(HttpServletResponse.class); + } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/ParameterConverter.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/ParameterConverter.java new file mode 100644 index 00000000..ef046b9a --- /dev/null +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/ParameterConverter.java @@ -0,0 +1,163 @@ +package com.interface21.webmvc.servlet.mvc.tobe.support; + +import com.interface21.web.bind.annotation.PathVariable; +import com.interface21.web.bind.annotation.RequestMapping; + +import java.lang.reflect.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class ParameterConverter { + public static Object[] convertToMethodArg( + Method method, + String uri, + Map parameterMap + ) { + if (isDtoParam(method.getParameters())) { + return convertToDto(method.getParameters(), parameterMap); + } + + return convertToPrimitive(method, uri, parameterMap); + } + + private static boolean isDtoParam(Parameter[] parameters) { + // dto는 하나만 사용할 수 있게 처리 + if (parameters.length > 1) { + return false; + } + + // 만약 primitive 타입이거나 wrapper class, 배열이 아닐 경우, 필드 매핑을 해줘야하기때문에 dto인지 판단 + return !parameters[0].getClass().isArray() + && !(isPrimitiveOrPrimitiveWrapperOrString(parameters[0].getType())); + } + + // 출처 https://stackoverflow.com/questions/25039080/java-how-to-determine-if-type-is-any-of-primitive-wrapper-string-or-something + private static boolean isPrimitiveOrPrimitiveWrapperOrString(Class type) { + return (type.isPrimitive() && type != void.class) || + type == Double.class || type == Float.class || type == Long.class || + type == Integer.class || type == Short.class || type == Character.class || + type == Byte.class || type == Boolean.class || type == String.class; + } + + private static Object[] convertToDto(Parameter[] parameters, Map parameterMap) { + Parameter parameter = parameters[0]; + Class clazz = parameter.getType(); + Object[] args = new Object[1]; + Map convertedFieldByName = new HashMap<>(); + Map> fieldTypeByName = new HashMap<>(); + + for (int i = 0; i < clazz.getDeclaredFields().length; i++) { + Field field = clazz.getDeclaredFields()[i]; + convertedFieldByName.put(field.getName(), convertVariable(field.getName(), field.getType(), parameterMap)); + fieldTypeByName.put(field.getName(), field.getType()); + } + + args[0] = getNewInstance(convertedFieldByName, fieldTypeByName, clazz); + + return args; + } + + private static Object getNewInstance( + Map convertedFieldByName, + Map> fieldTypeByName, + Class clazz + ) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + for (Constructor constructor : constructors) { + Parameter[] parameters = constructor.getParameters(); + boolean isPossibleToGetNewInstance = true; + Object[] args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + if (!convertedFieldByName.containsKey(parameter.getName())) { + isPossibleToGetNewInstance = false; + break; + } + + if (!fieldTypeByName.containsKey(parameter.getName())) { + isPossibleToGetNewInstance = false; + break; + } + + if (!fieldTypeByName.get(parameter.getName()).equals(parameter.getType())) { + isPossibleToGetNewInstance = false; + break; + } + + args[i] = convertedFieldByName.get(parameter.getName()); + } + + if (isPossibleToGetNewInstance) { + try { + return constructor.newInstance(args); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } + } + + throw new RuntimeException("해당하는 생성자가 없습니다"); + } + + private static Object[] convertToPrimitive(Method method, String uri, Map parameterMap) { + Parameter[] parameters = method.getParameters(); + Object[] args = new Object[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + Parameter parameter = parameters[i]; + PathVariable pathVariable = parameter.getAnnotation(PathVariable.class); + if (pathVariable != null) { + args[i] = convertPathVariable(method, uri, parameter); + } else if (parameter.getType().isArray()) { + args[i] = convertArray(parameter.getName(), parameter.getType().getComponentType(), parameterMap); + } else { + args[i] = convertVariable(parameter.getName(), parameter.getType(), parameterMap); + } + } + return args; + } + + private static Object convertPathVariable(Method method, String uri, Parameter parameter) { + String uriPattern = method.getAnnotation(RequestMapping.class).value(); + return convertValue( + parameter.getType(), + PathPatternUtil.getUriValue(uriPattern, uri, parameter.getName()) + ); + } + + private static Object[] convertArray( + String parameterName, + Class compositionType, + Map parameterMap + ) { + String[] values = parameterMap.get(parameterName); + return Arrays.stream(values) + .map(v -> convertValue(compositionType, v)) + .toArray(); + } + + private static Object convertVariable( + String parameterName, + Class clazz, + Map parameterMap + ) { + String value = parameterMap.get(parameterName)[0]; + return convertValue(clazz, value); + } + + private static Object convertValue(Class clazz, String value) { + if( Boolean.class == clazz || Boolean.TYPE == clazz) return Boolean.parseBoolean( value ); + if( Byte.class == clazz || Byte.TYPE == clazz) return Byte.parseByte( value ); + if( Short.class == clazz || Short.TYPE == clazz) return Short.parseShort( value ); + if( Integer.class == clazz || Integer.TYPE == clazz) return Integer.parseInt( value ); + if( Long.class == clazz || Long.TYPE == clazz) return Long.parseLong( value ); + if( Float.class == clazz || Float.TYPE == clazz) return Float.parseFloat( value ); + if( Double.class == clazz || Double.TYPE == clazz) return Double.parseDouble( value ); + return value; + } +} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternParser.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternParser.java index f8a204c5..46e2dbd7 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternParser.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternParser.java @@ -1,8 +1,7 @@ package com.interface21.webmvc.servlet.mvc.tobe.support; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.regex.Matcher; import java.util.regex.Pattern; public class PathPatternParser { @@ -16,14 +15,29 @@ public PathPatternParser(String pattern) { } private String buildRegex(String pattern) { - return null; + Matcher matcher = Pattern.compile("\\{([^}]+)}").matcher(pattern); + StringBuffer buffer = new StringBuffer(); + while (matcher.find()) { + variableNames.add(matcher.group(1)); + matcher.appendReplacement(buffer, "([^/]+)"); + } + matcher.appendTail(buffer); + return buffer.toString(); } public boolean matches(String path) { - return false; + return pattern.matcher(path).matches(); } public Map extractUriVariables(String path) { - return null; + Matcher matcher = pattern.matcher(path); + if (!matcher.matches()) { + return Collections.emptyMap(); + } + Map variables = new HashMap<>(); + for (int i = 0; i < variableNames.size(); i++) { + variables.put(variableNames.get(i), matcher.group(i + 1)); + } + return variables; } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternUtil.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternUtil.java index fcac0170..e62f9030 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternUtil.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternUtil.java @@ -5,14 +5,20 @@ public class PathPatternUtil { public static String getUriValue(String pattern, String path, String key) { - return null; + final Map uriVariables = getUriVariables(pattern, path); + return uriVariables.get(key); } public static Map getUriVariables(String pattern, String path) { - return null; + if (!isUrlMatch(pattern, path)) { + return Map.of(); + } + final PathPatternParser parser = new PathPatternParser(pattern); + return parser.extractUriVariables(path); } public static boolean isUrlMatch(String pattern, String path) { - return false; + final PathPatternParser parser = new PathPatternParser(pattern); + return parser.matches(path); } } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/ParametersTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/ParametersTest.java new file mode 100644 index 00000000..3298e02b --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/ParametersTest.java @@ -0,0 +1,37 @@ +package com.interface21.webmvc.servlet.mvc.tobe; + +import com.interface21.web.bind.annotation.RequestMapping; +import org.junit.jupiter.api.Test; +import samples.TestUserController; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class ParametersTest { + @Test + void 파라미터를_추출해서_저장() throws NoSuchMethodException { + Class classObj = TestUserController.class; + Method method = classObj.getDeclaredMethod("test", List.class); + + Parameter[] parameters = method.getParameters(); + + List test = List.of("a"); + System.out.println(parameters[0].getParameterizedType().equals(test.getClass())); + System.out.println(); + } + + @Test + void 파라미터를_추출해서_저장2() throws NoSuchMethodException { + Class classObj = TestUserController.class; + Method method = classObj.getDeclaredMethod("create_string", String.class, String.class); + + Parameter[] parameters = method.getParameters(); + + List test = List.of("a"); + System.out.println(); + } +} \ No newline at end of file diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/support/ParameterConverterTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/support/ParameterConverterTest.java new file mode 100644 index 00000000..a2b8eeea --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/support/ParameterConverterTest.java @@ -0,0 +1,58 @@ +package com.interface21.webmvc.servlet.mvc.tobe.support; + +import org.junit.jupiter.api.Test; +import samples.TestUser; +import samples.TestUserController; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ParameterConverterTest { + @Test + void 파라미터_매핑이_성공적으로_동작() throws NoSuchMethodException { + // given + Class clazz = TestUserController.class; + Method method = clazz.getMethod("create_javabean", TestUser.class); + Map parameterMap = new HashMap<>(); + parameterMap.put("userId", new String[]{"test"}); + parameterMap.put("password", new String[]{"pw"}); + parameterMap.put("age", new String[]{"12"}); + + // when + Object[] result = ParameterConverter.convertToMethodArg(method, "test", parameterMap); + + assertEquals(1, result.length); + assertTrue(result[0] instanceof TestUser); + } + + @Test + void 단일원시타입_파라미터_매핑이_성공적으로_동작() throws NoSuchMethodException { + Class clazz = TestUserController.class; + Method method = clazz.getMethod("create_int_long", long.class, int.class); + Map parameterMap = new HashMap<>(); + parameterMap.put("id", new String[]{"1"}); + parameterMap.put("age", new String[]{"12"}); + + // when + Object[] result = ParameterConverter.convertToMethodArg(method, "test", parameterMap); + + assertEquals(2, result.length); + } + + @Test + void 경로변수_추출_성공() throws NoSuchMethodException { + Class clazz = TestUserController.class; + Method method = clazz.getMethod("show_pathvariable", long.class); + Map parameterMap = new HashMap<>(); + + // when + Object[] result = ParameterConverter.convertToMethodArg(method, "/users/1", parameterMap); + + assertEquals(1, result.length); + assertEquals(1L, result[0]); + } +} \ No newline at end of file