/*
 * Copyright (C) 2016 Red Hat, inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package org.jboss.as.jaxrs;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.NAME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUBSYSTEM;
import static org.wildfly.extension.undertow.DeploymentDefinition.CONTEXT_ROOT;
import static org.wildfly.extension.undertow.DeploymentDefinition.SERVER;
import static org.wildfly.extension.undertow.DeploymentDefinition.VIRTUAL_HOST;

import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.api.ThreadSetupHandler;
import io.undertow.servlet.handlers.ServletHandler;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.MatrixParam;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

import org.jboss.as.controller.AttributeDefinition;
import org.jboss.as.controller.ObjectListAttributeDefinition;
import org.jboss.as.controller.ObjectTypeAttributeDefinition;
import org.jboss.as.controller.OperationContext;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
import org.jboss.as.controller.SimpleListAttributeDefinition;
import org.jboss.as.controller.SimpleResourceDefinition;
import org.jboss.as.controller.OperationContext.AttachmentKey;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.registry.ManagementResourceRegistration;
import org.jboss.as.jaxrs.logging.JaxrsLogger;
import org.jboss.as.server.Services;
import org.jboss.as.server.moduleservice.ServiceModuleLoader;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.logmanager.Level;
import org.jboss.modules.Module;
import org.jboss.msc.service.ServiceController;
import org.jboss.resteasy.spi.ResourceInvoker;
import org.jboss.resteasy.core.ResourceLocatorInvoker;
import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.jboss.resteasy.core.ResourceMethodRegistry;
import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher;
import org.jboss.resteasy.spi.metadata.ResourceBuilder;
import org.jboss.resteasy.spi.metadata.ResourceClass;
import org.jboss.resteasy.spi.metadata.ResourceLocator;
import org.jboss.resteasy.spi.metadata.ResourceMethod;
import org.wildfly.extension.undertow.UndertowExtension;
import org.wildfly.extension.undertow.UndertowService;
import org.wildfly.extension.undertow.deployment.UndertowDeploymentService;

/**
 * @author <a href="mailto:lgao@redhat.com">Lin Gao</a>
 */
@SuppressWarnings("deprecation")
public class DeploymentRestResourcesDefintion extends SimpleResourceDefinition {

    public static final DeploymentRestResourcesDefintion INSTANCE = new DeploymentRestResourcesDefintion();

    public static final String REST_RESOURCE_NAME = "rest-resource";

    public static final AttributeDefinition RESOURCE_CLASS = new SimpleAttributeDefinitionBuilder("resource-class",
            ModelType.STRING, true).setStorageRuntime().build();

    public static final AttributeDefinition RESOURCE_PATH = new SimpleAttributeDefinitionBuilder("resource-path", ModelType.STRING, true)
            .setStorageRuntime().build();

    public static final AttributeDefinition RESOURCE_METHOD = new SimpleAttributeDefinitionBuilder("resource-method",
            ModelType.STRING, false).setStorageRuntime().build();

    public static final AttributeDefinition RESOURCE_METHODS = new SimpleListAttributeDefinition.Builder("resource-methods", RESOURCE_METHOD)
            .setStorageRuntime().build();

    public static final AttributeDefinition CONSUME = new SimpleAttributeDefinitionBuilder("consume", ModelType.STRING, true)
            .setStorageRuntime().build();

    public static final AttributeDefinition CONSUMES = new SimpleListAttributeDefinition.Builder("consumes", CONSUME)
            .setStorageRuntime().build();

    public static final AttributeDefinition PRODUCE = new SimpleAttributeDefinitionBuilder("produce", ModelType.STRING, true)
            .setStorageRuntime().build();

    public static final AttributeDefinition PRODUCES = new SimpleListAttributeDefinition.Builder("produces", PRODUCE)
            .setStorageRuntime().build();

    public static final AttributeDefinition JAVA_METHOD = new SimpleAttributeDefinitionBuilder("java-method", ModelType.STRING,
            true).setStorageRuntime().build();

    public static final ObjectTypeAttributeDefinition RESOURCE_PATH_GRP = new ObjectTypeAttributeDefinition.Builder(
            "rest-resource-path-group", RESOURCE_PATH, CONSUMES, PRODUCES, JAVA_METHOD, RESOURCE_METHODS).build();

    public static final ObjectListAttributeDefinition RESOURCE_PATHS = new ObjectListAttributeDefinition.Builder(
            "rest-resource-paths", RESOURCE_PATH_GRP).build();

    public static final ObjectTypeAttributeDefinition SUB_RESOURCE_LOCATOR = new ObjectTypeAttributeDefinition.Builder(
            "sub-resource-locator-group", RESOURCE_CLASS, RESOURCE_PATH, CONSUMES, PRODUCES, JAVA_METHOD, RESOURCE_METHODS).build();

    public static final ObjectListAttributeDefinition SUB_RESOURCE_LOCATORS = new ObjectListAttributeDefinition.Builder(
            "sub-resource-locators", SUB_RESOURCE_LOCATOR).build();

    private AttachmentKey<Map<PathAddress, UndertowDeploymentService>> undertowDeployServiceKey = AttachmentKey.create(Map.class);
    private AttachmentKey<Map<PathAddress, Module>> deploymentModuleKey = AttachmentKey.create(Map.class);
    private AttachmentKey<Map<String, ResourceMeta>> resourceMetaKey = AttachmentKey.create(Map.class);

    private DeploymentRestResourcesDefintion() {
        super(PathElement.pathElement(REST_RESOURCE_NAME), JaxrsExtension.getResolver("deployment"));
    }

    @Override
    public void registerAttributes(ManagementResourceRegistration resourceRegistration) {
        resourceRegistration.registerMetric(RESOURCE_CLASS, new AbstractRestResReadHandler() {
            @Override
            void handleAttribute(String className, List<JaxrsResourceMethodDescription> methodInvokers,
                    List<JaxrsResourceLocatorDescription> locatorIncokers, Collection<String> servletMappings,
                    ModelNode response) {
                response.set(className);
            }
        });
        resourceRegistration.registerMetric(RESOURCE_PATHS, new AbstractRestResReadHandler() {
            @Override
            void handleAttribute(String className, List<JaxrsResourceMethodDescription> methodInvokers,
                    List<JaxrsResourceLocatorDescription> locatorIncokers, Collection<String> servletMappings,
                    ModelNode response) {
                for (JaxrsResourceMethodDescription methodDesc: methodInvokers) {
                    response.add(methodDesc.toModelNode());
                }
            }
        });

        resourceRegistration.registerMetric(SUB_RESOURCE_LOCATORS, new AbstractRestResReadHandler() {
            @Override
            void handleAttribute(String className, List<JaxrsResourceMethodDescription> methodInvokers,
                    List<JaxrsResourceLocatorDescription> locatorIncokers, Collection<String> servletMappings,
                    ModelNode response) {
                for (JaxrsResourceLocatorDescription methodDesc: locatorIncokers) {
                    response.add(methodDesc.toModelNode());
                }
            }
        });
    }

    abstract class AbstractRestResReadHandler implements OperationStepHandler {

        @Override
        public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
            PathAddress address = context.getCurrentAddress();
            String clsName = address.getLastElement().getValue();
            final PathAddress parentAddress = address.getParent();
            final ModelNode subModel = context.readResourceFromRoot(parentAddress.subAddress(0, parentAddress.size() - 1).append(SUBSYSTEM, UndertowExtension.SUBSYSTEM_NAME), false).getModel();
            final String host = VIRTUAL_HOST.resolveModelAttribute(context, subModel).asString();
            final String contextPath = CONTEXT_ROOT.resolveModelAttribute(context, subModel).asString();
            final String server = SERVER.resolveModelAttribute(context, subModel).asString();

            Map<PathAddress, UndertowDeploymentService> deploymentServiceMap = context.getAttachment(undertowDeployServiceKey);
            if (deploymentServiceMap == null) {
                deploymentServiceMap = Collections.synchronizedMap(new HashMap<PathAddress, UndertowDeploymentService>());
                context.attach(undertowDeployServiceKey, deploymentServiceMap);
            }
            UndertowDeploymentService undertowDeploymentService = deploymentServiceMap.get(parentAddress);
            if (undertowDeploymentService == null) {
                final ServiceController<?> controller = context.getServiceRegistry(false).getService(UndertowService.deploymentServiceName(server, host, contextPath));
                undertowDeploymentService = (UndertowDeploymentService) controller.getService();
                deploymentServiceMap.put(parentAddress, undertowDeploymentService);
            }
            final UndertowDeploymentService deploymentService = undertowDeploymentService;

            Map<String, ResourceMeta> resourceMetaMap = context.getAttachment(resourceMetaKey);
            if (resourceMetaMap == null) {
                resourceMetaMap = Collections.synchronizedMap(new HashMap<String, ResourceMeta>());
                context.attach(resourceMetaKey, resourceMetaMap);
            }
            ResourceMeta resMeta = resourceMetaMap.get(clsName);
            if (resMeta == null) {
                resMeta = new ResourceMeta();
                resourceMetaMap.put(clsName, resMeta);
            }
            final ResourceMeta resourceMeta = resMeta;
            try {
                if(deploymentService.getDeployment() == null) {
                    return;
                }
                deploymentService.getDeployment().createThreadSetupAction(new ThreadSetupHandler.Action<Object, Object>() {
                    @Override
                    public Object call(HttpServerExchange exchange, Object ctxObject) throws Exception {
                        List<HttpServletDispatcher> resteasyServlets = new ArrayList<>();
                        for (Map.Entry<String, ServletHandler> servletHandler : deploymentService.getDeployment().getServlets().getServletHandlers().entrySet()) {
                            if (HttpServletDispatcher.class.isAssignableFrom(servletHandler.getValue().getManagedServlet().getServletInfo().getServletClass())) {
                                resteasyServlets.add((HttpServletDispatcher) servletHandler.getValue().getManagedServlet().getServlet().getInstance());
                            }
                        }
                        if (!resteasyServlets.isEmpty()) {
                            context.addStep(new OperationStepHandler() {
                                @Override
                                public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
                                    final ModelNode response = new ModelNode();
                                    List<JaxrsResourceMethodDescription> resMethodInvokers = resourceMeta.methodInvokers;
                                    List<JaxrsResourceLocatorDescription> resLocatorInvokers = resourceMeta.resLocatorInvokers;
                                    for (HttpServletDispatcher resteasyServlet: resteasyServlets) {
                                        final Collection<String> servletMappings = resteasyServlet.getServletConfig().getServletContext().getServletRegistration(resteasyServlet.getServletConfig().getServletName()).getMappings();
                                        if (!resourceMeta.metaComplete) {
                                            resourceMeta.metaComplete = true;
                                            final ResourceMethodRegistry registry = (ResourceMethodRegistry) resteasyServlet.getDispatcher().getRegistry();
                                            for (Map.Entry<String, List<ResourceInvoker>> resource : registry.getBounded().entrySet()) {
                                                String mapping = resource.getKey();
                                                List<ResourceInvoker> resouceInvokers = resource.getValue();
                                                for (ResourceInvoker resourceInvoker : resouceInvokers) {
                                                    if (ResourceMethodInvoker.class.isAssignableFrom(resourceInvoker.getClass())) {
                                                        ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) resourceInvoker;
                                                        Class<?> resClass = methodInvoker.getResourceClass();
                                                        if (resClass.getCanonicalName().equals(clsName)) {
                                                            JaxrsResourceMethodDescription resMethodDesc = resMethodDescription(methodInvoker, contextPath, mapping, servletMappings, clsName);
                                                            resMethodInvokers.add(resMethodDesc);
                                                        } else if (resClass.isInterface()){
                                                            Class<?> resClsInModel = getResourceClassInModel(clsName, context);
                                                            if (resClass.isAssignableFrom(resClsInModel)) {
                                                                JaxrsResourceMethodDescription resMethodDesc = resMethodDescription(methodInvoker, contextPath, mapping, servletMappings, clsName);
                                                                resMethodInvokers.add(resMethodDesc);
                                                            }
                                                        }
                                                    } else if (ResourceLocatorInvoker.class.isAssignableFrom(resourceInvoker.getClass())) {
                                                        ResourceLocatorInvoker locatorInvoker = (ResourceLocatorInvoker) resourceInvoker;
                                                        Class<?> resLocatorClass = locatorInvoker.getMethod().getDeclaringClass();
                                                        if (clsName.equals(resLocatorClass.getCanonicalName())) {
                                                            ResourceClass resClass = ResourceBuilder.locatorFromAnnotations(locatorInvoker.getMethod().getReturnType());
                                                            JaxrsResourceLocatorDescription resLocatorDesc = resLocatorDescription(resClass, contextPath, mapping, servletMappings, new ArrayList<Class<?>>());
                                                            resLocatorInvokers.add(resLocatorDesc);
                                                        } else if (resLocatorClass.isInterface()) {
                                                            Class<?> resClsInModel = getResourceClassInModel(clsName, context);
                                                            if (resLocatorClass.isAssignableFrom(resClsInModel)) {
                                                                ResourceClass resClass = ResourceBuilder.locatorFromAnnotations(locatorInvoker.getMethod().getReturnType());
                                                                JaxrsResourceLocatorDescription resLocatorDesc = resLocatorDescription(resClass, contextPath, mapping, servletMappings, new ArrayList<Class<?>>());
                                                                resLocatorInvokers.add(resLocatorDesc);
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                            Collections.sort(resMethodInvokers);
                                            Collections.sort(resLocatorInvokers);
                                        }
                                        handleAttribute(clsName, resMethodInvokers, resLocatorInvokers, servletMappings, response);
                                    }
                                    context.getResult().set(response);
                                }

                                private Class<?> getResourceClassInModel(String clsName, OperationContext context) throws OperationFailedException{
                                    try {
                                        Map<PathAddress, Module> deploymentModuleMap = context.getAttachment(deploymentModuleKey);
                                        if (deploymentModuleMap == null) {
                                            deploymentModuleMap = Collections.synchronizedMap(new HashMap<PathAddress, Module>());
                                            context.attach(deploymentModuleKey, deploymentModuleMap);
                                        }
                                        Module deployModule = deploymentModuleMap.get(parentAddress);
                                        if (deployModule == null) {
                                            final StringBuilder sb = new StringBuilder(ModelDescriptionConstants.DEPLOYMENT);
                                            sb.append(".");
                                            String deployRuntimeName = address.getElement(0).getValue();
                                            final ModelNode deployModel = context.readResourceFromRoot(address.subAddress(0, 1)).getModel();
                                            if (deployModel.isDefined() && deployModel.hasDefined(ModelDescriptionConstants.RUNTIME_NAME)) {
                                                deployRuntimeName = deployModel.get(ModelDescriptionConstants.RUNTIME_NAME).asString();
                                            }
                                            sb.append(deployRuntimeName);
                                            if (address.size() > 1 && address.getElement(1).getKey().equals(ModelDescriptionConstants.SUBDEPLOYMENT)) {
                                                sb.append(".");
                                                sb.append(address.getElement(1).getValue());
                                            }
                                            String moduleName = sb.toString();
                                            ServiceModuleLoader srvModuleLoader = (ServiceModuleLoader) context.getServiceRegistry(false).getRequiredService(Services.JBOSS_SERVICE_MODULE_LOADER).getValue();
                                            deployModule = srvModuleLoader.loadModule(moduleName);
                                            deploymentModuleMap.put(parentAddress, deployModule);
                                        }
                                        return Class.forName(clsName, false, deployModule.getClassLoader());
                                    } catch (Exception e) {
                                        throw new OperationFailedException(e);
                                    }
                                }

                            }, OperationContext.Stage.RUNTIME);
                        }
                        return null;
                    }
                }).call(null, null);
            } catch (Exception ex) {
                //WFLY-10222 we don't want a failure to read the attribute to break everything
                JaxrsLogger.JAXRS_LOGGER.failedToReadAttribute(ex, address, operation.get(NAME));
                context.addResponseWarning(Level.WARN, ex.getMessage());
            }
        }

        abstract void handleAttribute(String className, List<JaxrsResourceMethodDescription> methodInvokers,
                List<JaxrsResourceLocatorDescription> locatorIncokers, Collection<String> servletMappings, ModelNode response);
    }

    private class ResourceMeta {
        private List<JaxrsResourceMethodDescription> methodInvokers;
        private List<JaxrsResourceLocatorDescription> resLocatorInvokers;
        private boolean metaComplete = false;
        private ResourceMeta() {
            this.methodInvokers = new ArrayList<>();
            this.resLocatorInvokers = new ArrayList<>();
        }
    }

    private JaxrsResourceLocatorDescription resLocatorDescription(ResourceClass resClass, String contextPath, String mapping,
            Collection<String> servletMappings, List<Class<?>> resolvedCls) {
        JaxrsResourceLocatorDescription locatorRes = new JaxrsResourceLocatorDescription();
        locatorRes.resourceClass = resClass.getClazz();
        resolvedCls.add(resClass.getClazz());
        for (ResourceMethod resMethod : resClass.getResourceMethods()) {
            JaxrsResourceMethodDescription jaxrsRes = new JaxrsResourceMethodDescription();
            jaxrsRes.consumeTypes = resMethod.getConsumes();
            jaxrsRes.contextPath = contextPath;
            jaxrsRes.httpMethods = resMethod.getHttpMethods();
            jaxrsRes.method = resMethod.getMethod();
            jaxrsRes.produceTypes = resMethod.getProduces();
            jaxrsRes.resourceClass = resClass.getClazz().getCanonicalName();
            String resPath = new StringBuilder(mapping).append("/").append(resMethod.getFullpath()).toString().replace("//", "/");
            jaxrsRes.resourcePath = resPath;
            jaxrsRes.servletMappings = servletMappings;
            addMethodParameters(jaxrsRes, resMethod.getMethod());
            locatorRes.methodsDescriptions.add(jaxrsRes);
        }
        for (ResourceLocator resLocator: resClass.getResourceLocators()) {
            Class<?> clz = resLocator.getReturnType();
            if (clz.equals(resClass.getClazz())) {
                break;
            }
            if (resolvedCls.contains(clz)) {
                break;
            } else {
                resolvedCls.add(clz);
            }
            ResourceClass subResClass = ResourceBuilder.locatorFromAnnotations(clz);
            String subMapping = new StringBuilder(mapping).append("/").append(resLocator.getFullpath()).toString().replace("//", "/");
            JaxrsResourceLocatorDescription inner = resLocatorDescription(subResClass, contextPath, subMapping, servletMappings, resolvedCls);
            if (inner.containsMethodResources()) {
                locatorRes.subLocatorDescriptions.add(inner);
            }
        }
        return locatorRes;
    }

    private JaxrsResourceMethodDescription resMethodDescription(ResourceMethodInvoker methodInvoker, String contextPath,
            String mapping, Collection<String> servletMappings, String clsName) {
        JaxrsResourceMethodDescription jaxrsRes = new JaxrsResourceMethodDescription();
        jaxrsRes.consumeTypes = methodInvoker.getConsumes();
        jaxrsRes.contextPath = contextPath;
        jaxrsRes.httpMethods = methodInvoker.getHttpMethods();
        jaxrsRes.method = methodInvoker.getMethod();
        jaxrsRes.produceTypes = methodInvoker.getProduces();
        jaxrsRes.resourceClass = clsName;
        jaxrsRes.resourcePath = mapping;
        jaxrsRes.servletMappings = servletMappings;
        addMethodParameters(jaxrsRes, methodInvoker.getMethod());
        return jaxrsRes;
    }

    private void addMethodParameters(JaxrsResourceMethodDescription jaxrsRes, Method method) {
        for (Parameter param : method.getParameters()) {
            ParamInfo paramInfo = new ParamInfo();
            paramInfo.cls = param.getType();
            paramInfo.defaultValue = null;
            paramInfo.name = null;
            paramInfo.type = null;
            Annotation annotation;
            if ((annotation = param.getAnnotation(PathParam.class)) != null) {
                PathParam pathParam = (PathParam) annotation;
                paramInfo.name = pathParam.value();
                paramInfo.type = "@" + PathParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(QueryParam.class)) != null) {
                QueryParam queryParam = (QueryParam) annotation;
                paramInfo.name = queryParam.value();
                paramInfo.type = "@" + QueryParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(HeaderParam.class)) != null) {
                HeaderParam headerParam = (HeaderParam) annotation;
                paramInfo.name = headerParam.value();
                paramInfo.type = "@" + HeaderParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(CookieParam.class)) != null) {
                CookieParam cookieParam = (CookieParam) annotation;
                paramInfo.name = cookieParam.value();
                paramInfo.type = "@" + CookieParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(MatrixParam.class)) != null) {
                MatrixParam matrixParam = (MatrixParam) annotation;
                paramInfo.name = matrixParam.value();
                paramInfo.type = "@" + MatrixParam.class.getSimpleName();
            } else if ((annotation = param.getAnnotation(FormParam.class)) != null) {
                FormParam formParam = (FormParam) annotation;
                paramInfo.name = formParam.value();
                paramInfo.type = "@" + FormParam.class.getSimpleName();
            }
            if (paramInfo.name == null) {
                paramInfo.name = param.getName();
            }
            if ((annotation = param.getAnnotation(DefaultValue.class)) != null) {
                DefaultValue defaultValue = (DefaultValue) annotation;
                paramInfo.defaultValue = defaultValue.value();
            }
            jaxrsRes.parameters.add(paramInfo);
        }
    }

    private static class JaxrsResourceLocatorDescription implements Comparable<JaxrsResourceLocatorDescription> {

        private Class<?> resourceClass;
        private List<JaxrsResourceMethodDescription> methodsDescriptions = new ArrayList<>();
        private List<JaxrsResourceLocatorDescription> subLocatorDescriptions = new ArrayList<>();

        @Override
        public int compareTo(JaxrsResourceLocatorDescription o) {
            return resourceClass.getCanonicalName().compareTo(o.resourceClass.getCanonicalName());
        }

        public ModelNode toModelNode() {
            ModelNode node = new ModelNode();
            node.get(RESOURCE_CLASS.getName()).set(resourceClass.getCanonicalName());
            ModelNode resPathNode = node.get(RESOURCE_PATHS.getName());
            Collections.sort(methodsDescriptions);
            for (JaxrsResourceMethodDescription methodRes : methodsDescriptions) {
                resPathNode.add(methodRes.toModelNode());
            }
            ModelNode subResNode = node.get(SUB_RESOURCE_LOCATORS.getName());
            Collections.sort(subLocatorDescriptions);
            for (JaxrsResourceLocatorDescription subLocator : subLocatorDescriptions) {
                subResNode.add(subLocator.toModelNode());
            }
            return node;
        }

        private boolean containsMethodResources() {
            if (!this.methodsDescriptions.isEmpty()) {
                return true;
            }
            for (JaxrsResourceLocatorDescription p : this.subLocatorDescriptions) {
                if (p.containsMethodResources()) {
                    return true;
                }
            }
            return false;
        }
    }

    private static class JaxrsResourceMethodDescription implements Comparable<JaxrsResourceMethodDescription> {

        private String resourceClass;
        private String resourcePath;
        private Method method;
        private List<ParamInfo> parameters = new ArrayList<>();
        private Set<String> httpMethods = Collections.emptySet();
        private MediaType[] consumeTypes;
        private MediaType[] produceTypes;

        private Collection<String> servletMappings = Collections.emptyList();
        private String contextPath;

        @Override
        public int compareTo(JaxrsResourceMethodDescription other) {
            int result = this.resourcePath.compareTo(other.resourcePath);
            if (result == 0) {
                result = this.resourceClass.compareTo(other.resourceClass);
            }
            if (result == 0) {
                result = this.method.getName().compareTo(other.method.getName());
            }
            return result;
        }

        ModelNode toModelNode() {
            ModelNode node = new ModelNode();
            node.get(RESOURCE_PATH.getName()).set(resourcePath);
            ModelNode consumeNode = node.get(CONSUMES.getName());
            if (consumeTypes != null && consumeTypes.length > 0) {
                for (MediaType consume : consumeTypes) {
                    consumeNode.add(consume.toString());
                }
            }
            ModelNode produceNode = node.get(PRODUCES.getName());
            if (produceTypes != null && produceTypes.length > 0) {
                for (MediaType produce : produceTypes) {
                    produceNode.add(produce.toString());
                }
            }
            node.get(JAVA_METHOD.getName()).set(formatJavaMethod());
            for (final String servletMapping : servletMappings) {
                for (final String httpMethod : httpMethods) {
                    node.get(RESOURCE_METHODS.getName()).add(httpMethod + " " + formatPath(servletMapping, contextPath, resourcePath));
                }
            }
            return node;
        }

        private String formatPath(String servletMapping, String ctxPath, String resPath) {
            StringBuilder sb = new StringBuilder();
            String servletPath = servletMapping.replaceAll("\\*", "");
            if (servletPath.charAt(0) == '/') {
                servletPath = servletPath.substring(1);
            }
            sb.append(ctxPath).append('/').append(servletPath).append(resPath);
            return sb.toString().replace("//", "/");
        }

        private String formatJavaMethod() {
            StringBuilder sb = new StringBuilder();
            sb.append(method.getReturnType().getCanonicalName()).append(" ").append(resourceClass)
                    .append(".").append(method.getName()).append('(');
            int i = 0;
            for (ParamInfo param : this.parameters) {
                if (param.type != null) {
                    sb.append(param.type).append(" ");
                }
                sb.append(param.cls.getCanonicalName()).append(" ").append(param.name);
                if (param.defaultValue != null) {
                    sb.append(" = '");
                    sb.append(param.defaultValue);
                    sb.append("'");
                }
                if (++i < this.parameters.size()) {
                    sb.append(", ");
                }
            }
            sb.append(")");
            return sb.toString();
        }
    }

    private static class ParamInfo {
        private String name;
        private Class<?> cls;
        private String type; // @PathParam, or @CookieParam, or @QueryParam, etc
        private String defaultValue;
    }
}
