diff --git a/README.md b/README.md index e391b2aa..7aedb00e 100644 --- a/README.md +++ b/README.md @@ -39,4 +39,13 @@ ### [STEP3] JSON View 구현하기 #### TODO -- [ ] JsonView render 구현 후 UserController가 정상동작하는지 테스트 \ No newline at end of file +- [X] JsonView render 구현 후 UserController가 정상동작하는지 테스트 + +### [STEP4] 4단계 - Controller 메서드 인자 매핑 +#### TODO +- [X] method의 parameter types를 통해 parsing할 수 있는 parameter parser + - [X] query param parser -> 메소드 파라미터의 type, name 보고 request.getParameter 에서 추출 + - [X] path variable parser -> @PathVariable이 있을경우 메소드 파라미터의 type, name 보고 /{}/{} path에 특정한 양식 ({})으로 추출 + - [X] parameter typed parser -> 특정 타입을 deserialization. request.getParameter에서 추출 +- [X] handlerExecution에서 invoke 시점에 parser로 parsing +- [X] testusercontroller로 확인 diff --git a/app/build.gradle b/app/build.gradle index 0d35baea..51e70713 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,3 +45,7 @@ sourceSets { java.destinationDirectory.set(file('src/main/webapp/WEB-INF/classes')) } } + +compileJava { + options.compilerArgs << '-parameters' +} \ No newline at end of file diff --git a/app/src/main/java/camp/nextstep/controller/support/DefaultExceptionHandlers.java b/app/src/main/java/camp/nextstep/controller/support/DefaultExceptionHandlers.java index 3bb99768..b438dc7d 100644 --- a/app/src/main/java/camp/nextstep/controller/support/DefaultExceptionHandlers.java +++ b/app/src/main/java/camp/nextstep/controller/support/DefaultExceptionHandlers.java @@ -10,6 +10,11 @@ @ControllerAdvice public class DefaultExceptionHandlers { + @ExceptionHandler(IllegalArgumentException.class) + public void handle(IllegalArgumentException ex, HttpServletRequest request, HttpServletResponse response) { + response.setStatus(400); + } + @ExceptionHandler(NoHandlerFoundException.class) public void handle(NoHandlerFoundException ex, HttpServletRequest request, HttpServletResponse response) { response.setStatus(404); diff --git a/mvc/build.gradle b/mvc/build.gradle index 142ca323..402de788 100644 --- a/mvc/build.gradle +++ b/mvc/build.gradle @@ -35,3 +35,7 @@ dependencies { test { useJUnitPlatform() } + +tasks.withType(JavaCompile) { + options.compilerArgs.add("-parameters") +} \ No newline at end of file diff --git a/mvc/src/main/java/com/interface21/web/parameter/ParameterParser.java b/mvc/src/main/java/com/interface21/web/parameter/ParameterParser.java new file mode 100644 index 00000000..510b7658 --- /dev/null +++ b/mvc/src/main/java/com/interface21/web/parameter/ParameterParser.java @@ -0,0 +1,11 @@ +package com.interface21.web.parameter; + +import jakarta.servlet.http.HttpServletRequest; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +public interface ParameterParser { + Object parse(final Method method, final Parameter parameter, final HttpServletRequest request); + +} diff --git a/mvc/src/main/java/com/interface21/web/parameter/ParameterParsers.java b/mvc/src/main/java/com/interface21/web/parameter/ParameterParsers.java new file mode 100644 index 00000000..bcca4b5c --- /dev/null +++ b/mvc/src/main/java/com/interface21/web/parameter/ParameterParsers.java @@ -0,0 +1,48 @@ +package com.interface21.web.parameter; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class ParameterParsers { + + private final List parsers = new ArrayList<>(Arrays.asList(new PathVariableParser(), new QueryParamParser())); + + public ParameterParsers() { + + } + + public ParameterParsers(ParameterParser... parsers) { + this.parsers.addAll(Arrays.asList(parsers)); + } + + public Object[] parse(final Method method, final HttpServletRequest request, final HttpServletResponse response) { + var parameters = method.getParameters(); + var params = new Object[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + var parsedParam = parse(method, parameters[i], request, response); + params[i] = parsedParam; + } + return params; + } + + private Object parse(final Method method, final Parameter parameter, final HttpServletRequest request, final HttpServletResponse response) { + if (parameter.getType().equals(HttpServletRequest.class)) { + return request; + } + if (parameter.getType().equals(HttpServletResponse.class)) { + return response; + } + + return parsers.stream().map(parser -> parser.parse(method, parameter, request)).filter(Objects::nonNull).findFirst() + .orElseThrow(IllegalArgumentException::new); + } + +} diff --git a/mvc/src/main/java/com/interface21/web/parameter/ParameterTypedParser.java b/mvc/src/main/java/com/interface21/web/parameter/ParameterTypedParser.java new file mode 100644 index 00000000..c31ab5bb --- /dev/null +++ b/mvc/src/main/java/com/interface21/web/parameter/ParameterTypedParser.java @@ -0,0 +1,58 @@ +package com.interface21.web.parameter; + +import com.interface21.web.support.TypedParsers; +import jakarta.servlet.http.HttpServletRequest; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Arrays; + +public class ParameterTypedParser implements ParameterParser { + private final Class type; + + public ParameterTypedParser(Class type) { + this.type = type; + } + + @Override + public Object parse(Method method, Parameter parameter, HttpServletRequest request) { + if (!parameter.getType().equals(type)) { + return null; + } + var constructors = type.getDeclaredConstructors(); + var fullyFieldConstructor = Arrays.stream(constructors).filter(constructor -> constructor.getParameterCount() == type.getDeclaredFields().length).findFirst(); + if (fullyFieldConstructor.isPresent()) { + return parseFully(fullyFieldConstructor.get(), request); + } + var defaultConstructor = Arrays.stream(constructors).filter(constructor -> constructor.getParameterCount() == 0).findFirst(); + return defaultConstructor.map(constructor -> parseDefault(constructor, request)).orElse(null); + } + + private Object parseFully(Constructor constructor, HttpServletRequest request) { + var parameters = constructor.getParameters(); + var args = new Object[parameters.length]; + for (int i = 0; i < parameters.length; i++) { + args[i] = TypedParsers.parse(parameters[i].getType(), request.getParameter(parameters[i].getName())); + } + try { + return constructor.newInstance(args); + } catch (Exception e) { + throw new IllegalArgumentException(); + } + } + + private Object parseDefault(Constructor constructor, HttpServletRequest request) { + try { + var instance = constructor.newInstance(); + for (Field field : type.getDeclaredFields()) { + field.setAccessible(true); + field.set(instance, TypedParsers.parse(field.getType(), request.getParameter(field.getName()))); + } + return instance; + } catch (Exception e) { + throw new IllegalArgumentException(); + } + } +} diff --git a/mvc/src/main/java/com/interface21/web/parameter/PathVariableParser.java b/mvc/src/main/java/com/interface21/web/parameter/PathVariableParser.java new file mode 100644 index 00000000..e80f0e37 --- /dev/null +++ b/mvc/src/main/java/com/interface21/web/parameter/PathVariableParser.java @@ -0,0 +1,33 @@ +package com.interface21.web.parameter; + + +import com.interface21.web.bind.annotation.PathVariable; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.support.PathPatternUtil; +import com.interface21.web.support.TypedParsers; +import jakarta.servlet.http.HttpServletRequest; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +public class PathVariableParser implements ParameterParser { + + @Override + public Object parse(Method method, Parameter parameter, HttpServletRequest request) { + var variable = parameter.getAnnotation(PathVariable.class); + var requestMapping = method.getAnnotation(RequestMapping.class); + if (variable == null || requestMapping == null) { + return null; + } + var name = variable.name(); + if (name.isBlank()) { + name = parameter.getName(); + } + var param = PathPatternUtil.getUriValue(requestMapping.value(), request.getRequestURI(), name); + if (param == null) { + return null; + } + return TypedParsers.parse(parameter.getType(), param); + } + +} diff --git a/mvc/src/main/java/com/interface21/web/parameter/QueryParamParser.java b/mvc/src/main/java/com/interface21/web/parameter/QueryParamParser.java new file mode 100644 index 00000000..4768b448 --- /dev/null +++ b/mvc/src/main/java/com/interface21/web/parameter/QueryParamParser.java @@ -0,0 +1,21 @@ +package com.interface21.web.parameter; + + +import com.interface21.web.support.TypedParsers; +import jakarta.servlet.http.HttpServletRequest; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +public class QueryParamParser implements ParameterParser { + + @Override + public Object parse(Method method, Parameter parameter, HttpServletRequest request) { + var param = request.getParameter(parameter.getName()); + if (param == null) { + return null; + } + return TypedParsers.parse(parameter.getType(), param); + } + +} diff --git a/mvc/src/main/java/com/interface21/web/support/PathPatternParser.java b/mvc/src/main/java/com/interface21/web/support/PathPatternParser.java new file mode 100644 index 00000000..54f7f802 --- /dev/null +++ b/mvc/src/main/java/com/interface21/web/support/PathPatternParser.java @@ -0,0 +1,42 @@ +package com.interface21.web.support; + +import java.util.*; +import java.util.regex.Pattern; + +public class PathPatternParser { + + private final Pattern pattern; + private final List variableNames = new ArrayList<>(); + + public PathPatternParser(String pattern) { + String regex = buildRegex(pattern); + this.pattern = Pattern.compile(regex); + } + + private String buildRegex(String pattern) { + var 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 pattern.matcher(path).matches(); + } + + public Map extractUriVariables(String path) { + var 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/web/support/PathPatternUtil.java b/mvc/src/main/java/com/interface21/web/support/PathPatternUtil.java new file mode 100644 index 00000000..d66b61cf --- /dev/null +++ b/mvc/src/main/java/com/interface21/web/support/PathPatternUtil.java @@ -0,0 +1,24 @@ +package com.interface21.web.support; + +import java.util.Map; + +public class PathPatternUtil { + + public static String getUriValue(String pattern, String path, String key) { + final Map uriVariables = getUriVariables(pattern, path); + return uriVariables.get(key); + } + + public static Map getUriVariables(String pattern, String path) { + 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) { + final PathPatternParser parser = new PathPatternParser(pattern); + return parser.matches(path); + } +} diff --git a/mvc/src/main/java/com/interface21/web/support/TypedParser.java b/mvc/src/main/java/com/interface21/web/support/TypedParser.java new file mode 100644 index 00000000..a00ac6f3 --- /dev/null +++ b/mvc/src/main/java/com/interface21/web/support/TypedParser.java @@ -0,0 +1,40 @@ +package com.interface21.web.support; + + +import java.util.function.Function; + +public class TypedParser { + protected static final TypedParser STRING = new TypedParser(String.class, (s) -> s); + protected static final TypedParser INT = new TypedParser(int.class, Integer::parseInt); + protected static final TypedParser WRAPPER_INTEGER = new TypedParser(Integer.class, Integer::parseInt); + protected static final TypedParser LONG = new TypedParser(long.class, Long::parseLong); + protected static final TypedParser WRAPPER_LONG = new TypedParser(Long.class, Integer::parseInt); + protected static final TypedParser BOOLEAN = new TypedParser(boolean.class, Boolean::parseBoolean); + protected static final TypedParser WRAPPER_BOOLEAN = new TypedParser(Boolean.class, Boolean::parseBoolean); + protected static final TypedParser SHORT = new TypedParser(short.class, Short::parseShort); + protected static final TypedParser WRAPPER_SHORT = new TypedParser(Short.class, Short::parseShort); + protected static final TypedParser FLOAT = new TypedParser(float.class, Float::parseFloat); + protected static final TypedParser WRAPPER_FLOAT = new TypedParser(Float.class, Float::parseFloat); + protected static final TypedParser DOUBLE = new TypedParser(double.class, Double::parseDouble); + protected static final TypedParser WRAPPER_DOUBLE = new TypedParser(Double.class, Double::parseDouble); + protected static final TypedParser CHAR = new TypedParser(char.class, s -> s.charAt(0)); + protected static final TypedParser WRAPPER_CHAR = new TypedParser(Character.class, s -> s.charAt(0)); + + private final Class type; + private final Function parser; + + public TypedParser(Class type, Function parser) { + this.type = type; + this.parser = parser; + } + + protected boolean canParse(Class type) { + return type.equals(this.type); + } + + protected Object parse(String value) { + return parser.apply(value); + } + + +} \ No newline at end of file diff --git a/mvc/src/main/java/com/interface21/web/support/TypedParsers.java b/mvc/src/main/java/com/interface21/web/support/TypedParsers.java new file mode 100644 index 00000000..66c4788e --- /dev/null +++ b/mvc/src/main/java/com/interface21/web/support/TypedParsers.java @@ -0,0 +1,19 @@ +package com.interface21.web.support; + +import java.util.ArrayList; +import java.util.List; + +public class TypedParsers { + private static List parserList = new ArrayList<>(List.of(TypedParser.STRING, TypedParser.INT, TypedParser.WRAPPER_INTEGER, TypedParser.LONG, TypedParser.WRAPPER_LONG, TypedParser.BOOLEAN, TypedParser.WRAPPER_BOOLEAN, TypedParser.SHORT, TypedParser.WRAPPER_SHORT, TypedParser.FLOAT, TypedParser.WRAPPER_FLOAT, TypedParser.DOUBLE, TypedParser.WRAPPER_DOUBLE, TypedParser.CHAR, TypedParser.WRAPPER_CHAR)); + + + public static void add(TypedParser... parsers) { + parserList.addAll(List.of(parsers)); + } + + public static Object parse(final Class type, final String value) { + return parserList.stream().filter(parser -> parser.canParse(type)).findFirst() + .orElseThrow(IllegalArgumentException::new) + .parse(value); + } +} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/ControllerAdviceHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/ControllerAdviceHandlerMapping.java index 542d3ccf..cee4a069 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/ControllerAdviceHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/ControllerAdviceHandlerMapping.java @@ -2,15 +2,18 @@ import com.interface21.context.stereotype.ControllerAdvice; import com.interface21.web.bind.annotation.ExceptionHandler; +import com.interface21.web.parameter.ParameterParsers; import java.lang.reflect.Method; import java.util.Arrays; public class ControllerAdviceHandlerMapping implements HandlerMapping { + private final ParameterParsers parsers; private final Object[] basePackage; - public ControllerAdviceHandlerMapping(final Object... basePackage) { + public ControllerAdviceHandlerMapping(ParameterParsers parsers, final Object... basePackage) { + this.parsers = parsers; this.basePackage = basePackage; } @@ -28,7 +31,7 @@ private void mappingHandler(final ExceptionHandlers handlers, final Object contr return; } for (Class exceptionClass : handler.value()) { - var execution = new HandlerExecution(controllerInstance, method); + var execution = new HandlerExecution(controllerInstance, method, parsers); handlers.add(exceptionClass, execution); } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/ControllerHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/ControllerHandlerMapping.java index 674e1fdd..ab2408b2 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/ControllerHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/ControllerHandlerMapping.java @@ -3,15 +3,18 @@ import com.interface21.context.stereotype.Controller; import com.interface21.web.bind.annotation.RequestMapping; import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.web.parameter.ParameterParsers; import java.lang.reflect.Method; import java.util.Arrays; public class ControllerHandlerMapping implements HandlerMapping { + private final ParameterParsers parsers; private final Object[] basePackage; - public ControllerHandlerMapping(final Object... basePackage) { + public ControllerHandlerMapping(final ParameterParsers parsers, final Object... basePackage) { + this.parsers = parsers; this.basePackage = basePackage; } @@ -29,7 +32,7 @@ private void mappingHandler(final HttpRequestHandlers handlers, final Object con } for (RequestMethod requestMethod : handler.method()) { var key = new HandlerKey(handler.value(), requestMethod); - var execution = new HandlerExecution(controllerInstance, method); + var execution = new HandlerExecution(controllerInstance, method, parsers); handlers.add(key, execution); } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/DispatcherServlet.java b/mvc/src/main/java/com/interface21/webmvc/servlet/DispatcherServlet.java index ec2e83fd..a25c72cc 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/DispatcherServlet.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/DispatcherServlet.java @@ -1,5 +1,8 @@ package com.interface21.webmvc.servlet; +import com.interface21.web.parameter.ParameterParsers; +import com.interface21.web.parameter.PathVariableParser; +import com.interface21.web.parameter.QueryParamParser; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -18,8 +21,9 @@ public class DispatcherServlet extends HttpServlet { private final HandlerAdapterRegistry handlerAdapters = new HandlerAdapterRegistry(); public DispatcherServlet() { - requestHandlers.addHandlerMapping(new ControllerHandlerMapping(BASE_PACKAGE)); - exceptionHandlers.addHandlerMapping(new ControllerAdviceHandlerMapping(BASE_PACKAGE)); + var parameterParsers = new ParameterParsers(new PathVariableParser(), new QueryParamParser()); + requestHandlers.addHandlerMapping(new ControllerHandlerMapping(parameterParsers, BASE_PACKAGE)); + exceptionHandlers.addHandlerMapping(new ControllerAdviceHandlerMapping(parameterParsers, BASE_PACKAGE)); handlerAdapters.addAdapter(new RequestHandlerAdapter()); } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerExecution.java index 83036789..459cbc8a 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerExecution.java @@ -1,5 +1,6 @@ package com.interface21.webmvc.servlet; +import com.interface21.web.parameter.ParameterParsers; import com.interface21.webmvc.servlet.view.JspView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -9,14 +10,16 @@ public class HandlerExecution { private final Object controllerInstance; private final Method method; + private final ParameterParsers parsers; - public HandlerExecution(Object controllerInstance, Method method) { + public HandlerExecution(Object controllerInstance, Method method, ParameterParsers parsers) { this.controllerInstance = controllerInstance; this.method = method; + this.parsers = parsers; } public ModelAndView handle(HttpServletRequest request, HttpServletResponse response) throws Exception { - var result = method.invoke(controllerInstance, request, response); + var result = method.invoke(controllerInstance, parsers.parse(method, request, response)); if (result instanceof ModelAndView) { return (ModelAndView) result; } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerKey.java b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerKey.java index 60e83bec..2c96cc20 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerKey.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/HandlerKey.java @@ -1,6 +1,7 @@ package com.interface21.webmvc.servlet; import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.web.support.PathPatternUtil; import jakarta.servlet.http.HttpServletRequest; import java.util.Objects; @@ -16,7 +17,7 @@ public HandlerKey(final String url, final RequestMethod requestMethod) { } public boolean matchUrl(final HttpServletRequest request) { - return request.getRequestURI().equals(url); + return request.getRequestURI().equals(url) || PathPatternUtil.isUrlMatch(url, request.getRequestURI()); } public boolean matchMethod(final HttpServletRequest request) { 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 deleted file mode 100644 index f8a204c5..00000000 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternParser.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.interface21.webmvc.servlet.mvc.tobe.support; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; - -public class PathPatternParser { - - private final Pattern pattern; - private final List variableNames = new ArrayList<>(); - - public PathPatternParser(String pattern) { - String regex = buildRegex(pattern); - this.pattern = Pattern.compile(regex); - } - - private String buildRegex(String pattern) { - return null; - } - - public boolean matches(String path) { - return false; - } - - public Map extractUriVariables(String path) { - return null; - } -} 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 deleted file mode 100644 index fcac0170..00000000 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternUtil.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.interface21.webmvc.servlet.mvc.tobe.support; - -import java.util.Map; - -public class PathPatternUtil { - - public static String getUriValue(String pattern, String path, String key) { - return null; - } - - public static Map getUriVariables(String pattern, String path) { - return null; - } - - public static boolean isUrlMatch(String pattern, String path) { - return false; - } -} diff --git a/mvc/src/test/java/com/interface21/web/parameter/ParameterTypedParserTest.java b/mvc/src/test/java/com/interface21/web/parameter/ParameterTypedParserTest.java new file mode 100644 index 00000000..1eba9062 --- /dev/null +++ b/mvc/src/test/java/com/interface21/web/parameter/ParameterTypedParserTest.java @@ -0,0 +1,52 @@ +package com.interface21.web.parameter; + +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; +import samples.TestUser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +class ParameterTypedParserTest { + + private final ParameterTypedParser parser = new ParameterTypedParser(TestUser.class); + + + @Test + void cannot_parse_nomatch_type() throws NoSuchMethodException { + var parameter = TestTypedParamClass.class.getDeclaredMethod("cannot_parse_nomatch_type", String.class).getParameters()[0]; + + assertThat(parser.parse(null, parameter, null)).isNull(); + } + + @Test + void parse_by_requestParam() throws NoSuchMethodException { + var parameter = TestTypedParamClass.class.getDeclaredMethod("parse_by_requestParam", TestUser.class).getParameters()[0]; + var request = mock(HttpServletRequest.class); + when(request.getParameter("userId")).thenReturn("woo-yu"); + when(request.getParameter("password")).thenReturn("pass"); + when(request.getParameter("age")).thenReturn("1234"); + + var result = (TestUser) parser.parse(null, parameter, request); + + assertThat(result.getUserId()).isEqualTo("woo-yu"); + assertThat(result.getPassword()).isEqualTo("pass"); + assertThat(result.getAge()).isEqualTo(1234); + + } + + +} + +class TestTypedParamClass { + + public void cannot_parse_nomatch_type(String name) { + + } + + public void parse_by_requestParam(TestUser user) { + + } +} \ No newline at end of file diff --git a/mvc/src/test/java/com/interface21/web/parameter/PathVariableParserTest.java b/mvc/src/test/java/com/interface21/web/parameter/PathVariableParserTest.java new file mode 100644 index 00000000..3b850623 --- /dev/null +++ b/mvc/src/test/java/com/interface21/web/parameter/PathVariableParserTest.java @@ -0,0 +1,67 @@ +package com.interface21.web.parameter; + +import com.interface21.web.bind.annotation.PathVariable; +import com.interface21.web.bind.annotation.RequestMapping; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Parameter; +import java.net.http.HttpRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class PathVariableParserTest { + + private final PathVariableParser parser = new PathVariableParser(); + + @Test + void cannot_parse_without_pathVariableParameter() throws NoSuchMethodException { + var method = TestPathVariableClass.class.getDeclaredMethod("cannot_parse_without_pathVariableParameter", String.class); + var parameter = method.getParameters()[0]; + + var result = parser.parse(method, parameter, null); + + assertThat(result).isNull(); + } + + @Test + void cannot_parse_without_RequestMapping() throws NoSuchMethodException { + var method = TestPathVariableClass.class.getDeclaredMethod("cannot_parse_without_RequestMapping", String.class); + var parameter = method.getParameters()[0]; + + var result = parser.parse(method, parameter, null); + + assertThat(result).isNull(); + } + + @Test + void can_parse_by_matched() throws NoSuchMethodException { + var request = mock(HttpServletRequest.class); + when(request.getRequestURI()).thenReturn("/1/2"); + var method = TestPathVariableClass.class.getDeclaredMethod("can_parse_by_matched", long.class, int.class); + + var parameter1 = method.getParameters()[0]; + var parameter2 = method.getParameters()[1]; + assertThat(parser.parse(method, parameter1, request)).isEqualTo(1L); + assertThat(parser.parse(method, parameter2, request)).isEqualTo(2); + } +} + +class TestPathVariableClass { + + public void cannot_parse_without_pathVariableParameter(String id) { + + } + + public void cannot_parse_without_RequestMapping(@PathVariable String id) { + + } + + @RequestMapping(value = "/{a}/{b}") + public void can_parse_by_matched(@PathVariable long a, @PathVariable int b) { + + } +} \ No newline at end of file diff --git a/mvc/src/test/java/com/interface21/web/parameter/QueryParamParserTest.java b/mvc/src/test/java/com/interface21/web/parameter/QueryParamParserTest.java new file mode 100644 index 00000000..75750baa --- /dev/null +++ b/mvc/src/test/java/com/interface21/web/parameter/QueryParamParserTest.java @@ -0,0 +1,43 @@ +package com.interface21.web.parameter; + +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +class QueryParamParserTest { + + private final QueryParamParser parser = new QueryParamParser(); + + @Test + void cannot_parse_nomatch_name() throws NoSuchMethodException { + var parameter = TestQueryParamClass.class.getDeclaredMethod("cannot_parse_nomatch_name", String.class).getParameters()[0]; + final HttpServletRequest request = mock(HttpServletRequest.class); + doReturn("1").when(request).getParameter("wrong"); + + assertThat(parser.parse(null, parameter, request)).isNull(); + } + + + @Test + void parsed_by_param_name() throws NoSuchMethodException { + var parameter = TestQueryParamClass.class.getDeclaredMethod("parsed_by_param_name", long.class).getParameters()[0]; + final HttpServletRequest request = mock(HttpServletRequest.class); + doReturn("1").when(request).getParameter("count"); + + assertThat(parser.parse(null, parameter, request)).isEqualTo(1L); + } +} + +class TestQueryParamClass { + + public void cannot_parse_nomatch_name(String name) { + + } + + public void parsed_by_param_name(long count) { + + } +} \ No newline at end of file diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternUtilTest.java b/mvc/src/test/java/com/interface21/web/support/PathPatternUtilTest.java similarity index 90% rename from mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternUtilTest.java rename to mvc/src/test/java/com/interface21/web/support/PathPatternUtilTest.java index 5171f513..7b842737 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/support/PathPatternUtilTest.java +++ b/mvc/src/test/java/com/interface21/web/support/PathPatternUtilTest.java @@ -1,4 +1,4 @@ -package com.interface21.webmvc.servlet.mvc.tobe.support; +package com.interface21.web.support; import org.junit.jupiter.api.Test; diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/ComponentScannerTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/ComponentScannerTest.java index 57758e68..bd883434 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/ComponentScannerTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/ComponentScannerTest.java @@ -1,18 +1,15 @@ package com.interface21.webmvc.servlet; import com.interface21.context.stereotype.Controller; -import com.interface21.webmvc.servlet.ComponentScanner; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; -import samples.TestController; class ComponentScannerTest { @Test void test() { var components = ComponentScanner.scan(Controller.class, "samples"); - Assertions.assertThat(components).hasSize(1); - Assertions.assertThat(components.get(0).methods).hasSize(2); + Assertions.assertThat(components).hasSize(2); } } \ No newline at end of file diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/ControllerHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/ControllerHandlerMappingTest.java index c41c8c91..2dc642f2 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/ControllerHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/ControllerHandlerMappingTest.java @@ -1,5 +1,6 @@ package com.interface21.webmvc.servlet; +import com.interface21.web.parameter.ParameterParsers; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; @@ -13,10 +14,11 @@ class ControllerHandlerMappingTest { private ControllerHandlerMapping handlerMapping; private HttpRequestHandlers handlers = new HttpRequestHandlers(); + private ParameterParsers parsers = new ParameterParsers(); @BeforeEach void setUp() { - handlerMapping = new ControllerHandlerMapping("samples"); + handlerMapping = new ControllerHandlerMapping(parsers, "samples"); handlerMapping.initialize(handlers); } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/TestUserControllerTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/TestUserControllerTest.java new file mode 100644 index 00000000..ad4d168d --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/TestUserControllerTest.java @@ -0,0 +1,101 @@ +package com.interface21.webmvc.servlet; + +import com.interface21.web.parameter.ParameterParser; +import com.interface21.web.parameter.ParameterParsers; +import com.interface21.web.parameter.ParameterTypedParser; +import com.interface21.web.support.TypedParser; +import com.interface21.web.support.TypedParsers; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import samples.TestUser; + +import java.lang.reflect.*; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TestUserControllerTest { + + private ControllerHandlerMapping handlerMapping; + private HttpRequestHandlers handlers = new HttpRequestHandlers(); + private ParameterParsers parsers = new ParameterParsers(new ParameterTypedParser(TestUser.class)); + + @BeforeEach + void setUp() { + handlerMapping = new ControllerHandlerMapping(parsers, "samples"); + handlerMapping.initialize(handlers); + } + + @Test + void users_string() throws Exception { + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + + when(request.getParameter("userId")).thenReturn("gugu"); + when(request.getParameter("password")).thenReturn("password"); + when(request.getRequestURI()).thenReturn("/users/string"); + when(request.getMethod()).thenReturn("POST"); + + final var handlerExecution = (HandlerExecution) handlers.getHandler(request); + final var modelAndView = handlerExecution.handle(request, response); + + assertThat(modelAndView.getObject("userId")).isEqualTo("gugu"); + assertThat(modelAndView.getObject("password")).isEqualTo("password"); + } + + + @Test + void users_number() throws Exception { + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + + when(request.getParameter("id")).thenReturn("123"); + when(request.getParameter("age")).thenReturn("22"); + when(request.getRequestURI()).thenReturn("/users/number"); + when(request.getMethod()).thenReturn("POST"); + + final var handlerExecution = (HandlerExecution) handlers.getHandler(request); + final var modelAndView = handlerExecution.handle(request, response); + + assertThat(modelAndView.getObject("id")).isEqualTo(123L); + assertThat(modelAndView.getObject("age")).isEqualTo(22); + } + + @Test + void users_bean() throws Exception { + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + + when(request.getParameter("userId")).thenReturn("woo-yu"); + when(request.getParameter("password")).thenReturn("pass"); + when(request.getParameter("age")).thenReturn("123"); + when(request.getRequestURI()).thenReturn("/users/bean"); + when(request.getMethod()).thenReturn("POST"); + + final var handlerExecution = (HandlerExecution) handlers.getHandler(request); + final var modelAndView = handlerExecution.handle(request, response); + final var result = (TestUser) modelAndView.getObject("testUser"); + + assertThat(result.getUserId()).isEqualTo("woo-yu"); + assertThat(result.getPassword()).isEqualTo("pass"); + assertThat(result.getAge()).isEqualTo(123); + } + + @Test + void users_pathVariable() throws Exception { + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + + when(request.getRequestURI()).thenReturn("/users/123"); + when(request.getMethod()).thenReturn("GET"); + + final var handlerExecution = (HandlerExecution) handlers.getHandler(request); + final var modelAndView = handlerExecution.handle(request, response); + + assertThat(modelAndView.getObject("id")).isEqualTo(123L); + } +} \ No newline at end of file diff --git a/mvc/src/test/java/samples/TestUserController.java b/mvc/src/test/java/samples/TestUserController.java index a669cb42..e4526f7e 100644 --- a/mvc/src/test/java/samples/TestUserController.java +++ b/mvc/src/test/java/samples/TestUserController.java @@ -1,5 +1,6 @@ package samples; +import com.interface21.context.stereotype.Controller; import com.interface21.web.bind.annotation.PathVariable; import com.interface21.web.bind.annotation.RequestMapping; import com.interface21.web.bind.annotation.RequestMethod; @@ -8,11 +9,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@Controller public class TestUserController { private static final Logger log = LoggerFactory.getLogger(TestUserController.class); - @RequestMapping(value = "/users", method = RequestMethod.POST) + @RequestMapping(value = "/users/string", method = RequestMethod.POST) public ModelAndView create_string(String userId, String password) { log.debug("userId: {}, password: {}", userId, password); ModelAndView mav = new ModelAndView(new JsonView()); @@ -21,7 +23,7 @@ public ModelAndView create_string(String userId, String password) { return mav; } - @RequestMapping(value = "/users", method = RequestMethod.POST) + @RequestMapping(value = "/users/number", method = RequestMethod.POST) public ModelAndView create_int_long(long id, int age) { log.debug("id: {}, age: {}", id, age); ModelAndView mav = new ModelAndView(new JsonView()); @@ -30,7 +32,7 @@ public ModelAndView create_int_long(long id, int age) { return mav; } - @RequestMapping(value = "/users", method = RequestMethod.POST) + @RequestMapping(value = "/users/bean", method = RequestMethod.POST) public ModelAndView create_javabean(TestUser testUser) { log.debug("testUser: {}", testUser); ModelAndView mav = new ModelAndView(new JsonView());