/*
* 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.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.servlet.api.ThreadSetupAction.Handle;
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.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.ws.rs.CookieParam;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.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.registry.ManagementResourceRegistration;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.msc.service.ServiceController;
import org.jboss.resteasy.core.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 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 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();
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();
final ServiceController<?> controller = context.getServiceRegistry(false).getService(
UndertowService.deploymentServiceName(server, host, contextPath));
final UndertowDeploymentService deploymentService = (UndertowDeploymentService) controller.getService();
Servlet resteasyServlet = null;
Handle handle = deploymentService.getDeployment().getThreadSetupAction().setup(null);
try {
for (Map.Entry<String, ServletHandler> servletHandler : deploymentService.getDeployment().getServlets()
.getServletHandlers().entrySet()) {
if (HttpServletDispatcher.class.isAssignableFrom(servletHandler.getValue().getManagedServlet()
.getServletInfo().getServletClass())) {
resteasyServlet = (Servlet) servletHandler.getValue().getManagedServlet().getServlet().getInstance();
break;
}
}
if (resteasyServlet != null) {
final Collection<String> servletMappings = resteasyServlet.getServletConfig().getServletContext()
.getServletRegistration(resteasyServlet.getServletConfig().getServletName()).getMappings();
final ResourceMethodRegistry registry = (ResourceMethodRegistry) ((HttpServletDispatcher) resteasyServlet)
.getDispatcher().getRegistry();
context.addStep(new OperationStepHandler() {
@Override
public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
final ModelNode response = new ModelNode();
List<JaxrsResourceMethodDescription> resMethodInvokers = new ArrayList<>();
List<JaxrsResourceLocatorDescription> resLocatorInvokers = new ArrayList<>();
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;
if (methodInvoker.getResourceClass().getCanonicalName().equals(clsName)) {
JaxrsResourceMethodDescription resMethodDesc = resMethodDescription(methodInvoker, contextPath, mapping, servletMappings);
resMethodInvokers.add(resMethodDesc);
}
} else if (ResourceLocatorInvoker.class.isAssignableFrom(resourceInvoker.getClass())) {
ResourceLocatorInvoker locatorInvoker = (ResourceLocatorInvoker) resourceInvoker;
if (clsName.equals(locatorInvoker.getMethod().getDeclaringClass().getCanonicalName())) {
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);
}
}, OperationContext.Stage.RUNTIME);
}
} catch (ServletException ex) {
throw new RuntimeException(ex);
} finally {
handle.tearDown();
}
}
abstract void handleAttribute(String className, List<JaxrsResourceMethodDescription> methodInvokers,
List<JaxrsResourceLocatorDescription> locatorIncokers, Collection<String> servletMappings, ModelNode response);
}
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();
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) {
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 = methodInvoker.getResourceClass();
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.size() > 0) {
return true;
}
return this.subLocatorDescriptions.stream().anyMatch(p -> p.containsMethodResources());
}
}
private static class JaxrsResourceMethodDescription implements Comparable<JaxrsResourceMethodDescription> {
private Class<?> 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.getCanonicalName().compareTo(other.resourceClass.getCanonicalName());
}
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.getCanonicalName())
.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;
}
}