HandlerMethodScanner.java

package sprout.mvc.mapping;

import sprout.beans.annotation.Component;
import sprout.beans.annotation.Controller;
import sprout.context.BeanFactory;
import sprout.mvc.annotation.*;
import sprout.mvc.http.HttpMethod;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;

@Component
public class HandlerMethodScanner {
    private final RequestMappingRegistry requestMappingRegistry;
    private final PathPatternResolver pathPatternResolver;

    public HandlerMethodScanner(RequestMappingRegistry requestMappingRegistry, PathPatternResolver pathPatternResolver) {
        this.requestMappingRegistry = requestMappingRegistry;
        this.pathPatternResolver = pathPatternResolver;
    }

    public void scanControllers(BeanFactory context) {
        Collection<Object> beans = context.getAllBeans();
        System.out.println(beans.size() + " beans found");
        for (Object bean : beans) {
            Class<?> beanClass = bean.getClass();
            if (beanClass.isAnnotationPresent(Controller.class)) {
                System.out.println("found controller: " + beanClass.getName());
                String classLevelBasePath = extractBasePath(beanClass);
                for (Method method : beanClass.getMethods()) {
                    RequestMappingInfoExtractor requestMappingInfoExtractor = findRequestMappingInfoExtractor(method);
                    if (requestMappingInfoExtractor != null) {
                        String methodPath = requestMappingInfoExtractor.getPath();
                        HttpMethod[] httpMethods = requestMappingInfoExtractor.getHttpMethods();

                        String finalPathString = combinePaths(classLevelBasePath, methodPath);
                        PathPattern pathPattern = pathPatternResolver.resolve(finalPathString);

                        for (HttpMethod httpMethod : httpMethods) {
                            requestMappingRegistry.register(pathPattern, httpMethod, bean, method);
                        }
                    }
                }
            }
        }
    }

    String extractBasePath(Class<?> clazz) {
        RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
        if (requestMapping != null) {
            if (requestMapping.path().length > 0) {
                return requestMapping.path()[0];
            }
            if (requestMapping.value().length > 0) {
                return requestMapping.value()[0];
            }
        }
        return "";
    }

    public RequestMappingInfoExtractor findRequestMappingInfoExtractor(Method method) {
        for (Annotation ann : method.getDeclaredAnnotations()) {

            // 직접 선언된 @RequestMapping
            RequestMapping rm = ann instanceof RequestMapping
                    ? (RequestMapping) ann
                    : ann.annotationType().getAnnotation(RequestMapping.class);

            if (rm == null) continue;

            if (ann instanceof RequestMapping) {
                int cnt = rm.method().length;
                if (cnt != 1) { // 0 개(생략) -or- 2 개 이상이면 무시
                    System.out.printf(
                            "[WARN] %s.%s() - skipped: ambiguous @RequestMapping (method=%s)%n",
                            method.getDeclaringClass().getSimpleName(), method.getName(),
                            Arrays.toString(rm.method())
                    );
                    return null;
                }
            }

            // value() 가 비어 있으면 path() → value() 순으로 탐색
            String[] paths = extractPaths(ann, rm);

            HttpMethod[] methods = rm.method();
            if (methods.length == 0) methods = new HttpMethod[] { HttpMethod.GET };

            String path = (paths.length > 0 && !paths[0].isBlank()) ? paths[0] : "/";
            return new RequestMappingInfoExtractor(path, methods);
        }
        return null;
    }

    private String[] extractPaths(Annotation ann, RequestMapping fallback) {
        String[] p = getAttribute(ann, "value");
        if (p.length == 0 || p[0].isBlank()) p = getAttribute(ann, "path");
        if (p.length == 0 || p[0].isBlank()) p = fallback.path();
        if (p.length == 0 || p[0].isBlank()) p = fallback.value();
        return p;
    }

    private String[] getAttribute(Annotation ann, String attr) {
        try {
            Method m = ann.annotationType().getDeclaredMethod(attr);
            if (m.getReturnType().isArray()
                    && m.getReturnType().getComponentType() == String.class) {
                return (String[]) m.invoke(ann);
            }
        } catch (ReflectiveOperationException ignored) { }
        return new String[0];
    }


    public String combinePaths(String basePath, String methodPath) {
        if (basePath.isEmpty() || basePath.equals("/")) {
            return methodPath.startsWith("/") ? methodPath : "/" + methodPath;
        }
        if (methodPath.isEmpty() || methodPath.equals("/")) {
            return basePath.startsWith("/") ? basePath : "/" + basePath;
        }
        // 기본 경로가 /로 끝나지 않고 메서드 경로가 /로 시작하지 않는 경우 처리
        String normalizedBasePath = basePath.endsWith("/") ? basePath.substring(0, basePath.length() - 1) : basePath;
        String normalizedMethodPath = methodPath.startsWith("/") ? methodPath : "/" + methodPath;
        return normalizedBasePath + normalizedMethodPath;
    }

}