/* * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jboss.errai.enterprise.rebind; import static org.jboss.errai.codegen.util.Stmt.if_; import static org.jboss.errai.codegen.util.Stmt.load; import static org.jboss.errai.codegen.util.Stmt.loadVariable; import static org.jboss.errai.codegen.util.Stmt.nestedCall; import static org.jboss.errai.enterprise.rebind.TypeMarshaller.demarshal; import static org.jboss.errai.enterprise.rebind.TypeMarshaller.marshal; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.ws.rs.QueryParam; import org.jboss.errai.codegen.BlockStatement; import org.jboss.errai.codegen.BooleanOperator; import org.jboss.errai.codegen.DefParameters; import org.jboss.errai.codegen.Parameter; import org.jboss.errai.codegen.Statement; import org.jboss.errai.codegen.StringStatement; import org.jboss.errai.codegen.Variable; import org.jboss.errai.codegen.builder.BlockBuilder; import org.jboss.errai.codegen.builder.ClassStructureBuilder; import org.jboss.errai.codegen.builder.ContextualStatementBuilder; import org.jboss.errai.codegen.builder.impl.ObjectBuilder; import org.jboss.errai.codegen.exception.GenerationException; import org.jboss.errai.codegen.meta.MetaClass; import org.jboss.errai.codegen.meta.MetaClassFactory; import org.jboss.errai.codegen.meta.MetaParameterizedType; import org.jboss.errai.codegen.meta.MetaType; import org.jboss.errai.codegen.util.Bool; import org.jboss.errai.codegen.util.If; import org.jboss.errai.codegen.util.ProxyUtil; import org.jboss.errai.codegen.util.ProxyUtil.InterceptorProvider; import org.jboss.errai.codegen.util.Stmt; import org.jboss.errai.common.client.framework.CallContextStatus; import org.jboss.errai.enterprise.client.jaxrs.ResponseDemarshallingCallback; import org.jboss.errai.enterprise.client.jaxrs.api.RestClient; import org.jboss.errai.enterprise.client.jaxrs.api.interceptor.RestCallContext; import org.jboss.errai.enterprise.rebind.TypeMarshaller.PrimitiveTypeMarshaller; import com.google.gwt.core.ext.GeneratorContext; import com.google.gwt.http.client.RequestBuilder; import com.google.gwt.http.client.Response; import com.google.gwt.http.client.URL; /** * Generates a JAX-RS remote proxy method. * * @author Christian Sadilek <csadilek@redhat.com> */ public class JaxrsProxyMethodGenerator { private static final String APPEND = "append"; private final MetaClass remote; private final MetaClass declaringClass; private final JaxrsResourceMethod resourceMethod; private final BlockBuilder<?> methodBlock; private final List<Statement> parameters; private final InterceptorProvider interceptorProvider; private final GeneratorContext context; public JaxrsProxyMethodGenerator(final MetaClass remote, final ClassStructureBuilder<?> classBuilder, final JaxrsResourceMethod resourceMethod, final InterceptorProvider interceptorProvider, final GeneratorContext context) { this.remote = remote; this.declaringClass = classBuilder.getClassDefinition(); this.resourceMethod = resourceMethod; this.interceptorProvider = interceptorProvider; this.context = context; final Parameter[] parms = DefParameters.from(resourceMethod.getMethod()).getParameters().toArray(new Parameter[0]); final Parameter[] finalParms = new Parameter[parms.length]; parameters = new ArrayList<>(); for (int i = 0; i < parms.length; i++) { finalParms[i] = Parameter.of(parms[i].getType(), parms[i].getName(), true); parameters.add(Stmt.loadVariable(parms[i].getName())); } this.methodBlock = classBuilder.publicMethod(resourceMethod.getMethod().getReturnType(), resourceMethod.getMethod().getName(), finalParms); } public void generate() { if (resourceMethod.getHttpMethod() != null) { final JaxrsResourceMethodParameters jaxrsParams = resourceMethod.getParameters(); methodBlock.append(generateUrl(jaxrsParams)); methodBlock.append(generateRequestBuilder()); methodBlock.append(generateHeaders(jaxrsParams)); final List<Class<?>> interceptors = interceptorProvider.getInterceptors(remote, resourceMethod.getMethod()); if (!interceptors.isEmpty()) { methodBlock.append(generateInterceptorLogic(interceptors)); } else { methodBlock.append(generateRequest()); } } generateReturnStatement(); } /** * Generates a {@link StringBuilder} constructing the request URL based on the method parameters * annotated with JAX-RS annotations. * * @param params * the resource method's parameters * @return the URL statement */ private Statement generateUrl(final JaxrsResourceMethodParameters params) { final BlockStatement block = new BlockStatement(); block.addStatement(Stmt.declareVariable("url", StringBuilder.class, Stmt.newObject(StringBuilder.class, new StringStatement("getBaseUrl()")))); // construct path using @PathParams and @MatrixParams final String path = resourceMethod.getPath(); ContextualStatementBuilder pathValue = Stmt.loadLiteral(path); for (final String pathParamExpr : JaxrsResourceMethodParameters.getPathParameterExpressions(path)) { String pathParamId = pathParamExpr; if (pathParamExpr.contains(":")) { pathParamId = pathParamExpr.split(":")[0]; } Statement pathParam = marshal(params.getPathParameter(pathParamId)); if (params.needsEncoding(pathParamId)) { pathParam = encodePath(pathParam); } pathValue = pathValue.invoke("replace", "{" + pathParamExpr + "}", pathParam); } if (params.getMatrixParameters() != null) { for (final String matrixParamName : params.getMatrixParameters().keySet()) { pathValue = pathValue.invoke("concat", ";" + matrixParamName + "=") .invoke("concat", encodePath(marshal(params.getMatrixParameter(matrixParamName)))); } } ContextualStatementBuilder urlBuilder = Stmt.loadVariable("url").invoke(APPEND, pathValue); block.addStatement(urlBuilder); // construct query using @QueryParams if (params.getQueryParameters() != null) { urlBuilder = urlBuilder.invoke(APPEND, "?"); int i = 0; for (final String queryParamName : params.getQueryParameters().keySet()) { for (final Statement queryParam : params.getQueryParameters(queryParamName)) { final MetaClass queryParamType = queryParam.getType(); if (isListOrSet(queryParamType)) { final MetaClass paramType = assertValidCollectionParam(queryParamType, queryParamName, QueryParam.class); final ContextualStatementBuilder listParam = (queryParam instanceof Parameter) ? Stmt.loadVariable(((Parameter) queryParam).getName()) : Stmt.nestedCall(queryParam); block.addStatement(listParam.foreachIfNotNull("p") .append(If.not(Stmt.loadVariable("url").invoke("toString").invoke("endsWith", "?")) .append(Stmt.loadVariable("url").invoke(APPEND, "&")).finish()) .append(Stmt.loadVariable("url").invoke(APPEND, queryParamName).invoke(APPEND, "=") .invoke(APPEND, encodeQuery(marshal(paramType, Stmt.loadVariable("p"))))) .finish() ); } else { if (i++ > 0) { urlBuilder = urlBuilder.invoke(APPEND, "&"); } urlBuilder = urlBuilder.invoke(APPEND, queryParamName).invoke(APPEND, "=") .invoke(APPEND, encodeQuery(marshal(queryParam))); } } } } return block; } /** * Checks if the provided type is a {@link List} or {@link Set}. * * @param paramType * the type to check * @return true if the type can be assigned to a List or Set, otherwise false. */ private boolean isListOrSet(final MetaClass paramType) { return paramType.isAssignableTo(List.class) || paramType.isAssignableTo(Set.class); } /** * Asserts that the provided type is a valid collection type for JAX-RS resource parameters. * * @param paramType * the provided type. * @param paramName * the name of the resource parameter for error reporting. * @param jaxrsParamType * the JAX-RS resource parameter type for error reporting. * * @return the element type. * @throws GenerationException * if the type parameters of the collection type are invalid for JAX-RS resource * parameters. */ private MetaClass assertValidCollectionParam(final MetaClass paramType, final String paramName, final Class<?> jaxrsParamType) { final MetaParameterizedType queryParamPType = paramType.getParameterizedType(); final MetaType[] typeParams = (queryParamPType != null) ? queryParamPType.getTypeParameters() : null; if (typeParams != null && typeParams.length == 1 && typeParams[0] instanceof MetaClass && PrimitiveTypeMarshaller.canHandle((MetaClass) typeParams[0], "text/plain")) { return (MetaClass) typeParams[0]; } else { throw new GenerationException( "Unsupported type parameter found on " + jaxrsParamType.getSimpleName() + " with name " + paramName + " in method " + resourceMethod.getMethod() + " (check the JavaDocs of " + jaxrsParamType.getName() + " for details!)"); } } /** * Generates the declaration for a new {@link RequestBuilder} instance, initialized with the * generated URL {@link #generateUrl(JaxrsResourceMethodParameters)} * * @return the RequestBuilder statement */ private Statement generateRequestBuilder() { final Statement requestBuilder = Stmt.declareVariable("requestBuilder", RequestBuilder.class, Stmt.newObject(RequestBuilder.class) .withParameters(resourceMethod.getHttpMethod(), Stmt.loadVariable("url").invoke("toString"))); return requestBuilder; } /** * Generates calls to set the appropriate headers on the generated {@link RequestBuilder} (see * {@link #generateRequestBuilder()}) based on the method parameters annotated with JAX-RS * annotations. * * @param params * the resource method's parameters * @return a block statement with the corresponding calls to * {@link RequestBuilder#setHeader(String, String)} */ private Statement generateHeaders(final JaxrsResourceMethodParameters params) { final BlockStatement block = new BlockStatement(); // set headers based on method and class for (final String key : resourceMethod.getHeaders().keySet()) { block.addStatement(Stmt.loadVariable("requestBuilder").invoke("setHeader", key, resourceMethod.getHeaders().get(key))); } // set headers based on @HeaderParams if (params.getHeaderParameters() != null) { for (final String headerParamName : params.getHeaderParameters().keySet()) { ContextualStatementBuilder headerValueBuilder = Stmt.nestedCall(Stmt.newObject(StringBuilder.class)); int i = 0; for (final Statement headerParam : params.getHeaderParameters(headerParamName)) { if (i++ > 0) { headerValueBuilder = headerValueBuilder.invoke(APPEND, ","); } headerValueBuilder = headerValueBuilder.invoke(APPEND, marshal(headerParam)); } block.addStatement(Stmt.loadVariable("requestBuilder").invoke("setHeader", headerParamName, headerValueBuilder.invoke("toString"))); } } // set cookies based on @CookieParams if (params.getCookieParameters() != null) { for (final String cookieName : params.getCookieParameters().keySet()) { final Statement cookieParam = params.getCookieParameters().get(cookieName).get(0); final ContextualStatementBuilder setCookie = (cookieParam instanceof Parameter) ? Stmt.loadVariable(((Parameter) cookieParam).getName()) : Stmt.nestedCall(cookieParam); setCookie.if_(BooleanOperator.NotEquals, null) .append(Stmt.invokeStatic(RestClient.class, "setCookie", cookieName, marshal(cookieParam))) .finish(); block.addStatement(setCookie); } } return block.isEmpty() ? null : block; } /** * Generates the logic required to make this proxy method interceptable. * * @param interceptors * @return statement representing the interceptor logic. */ private Statement generateInterceptorLogic(final List<Class<?>> interceptors) { final JaxrsResourceMethodParameters jaxrsParams = JaxrsResourceMethodParameters.fromMethod(resourceMethod.getMethod(), "parameters"); final Statement callContext = ProxyUtil.generateProxyMethodCallContext(context, RestCallContext.class, declaringClass, resourceMethod.getMethod(), generateInterceptedRequest(), interceptors) .publicOverridesMethod("setParameters", Parameter.of(Object[].class, "parameters")) .append(new StringStatement("super.setParameters(parameters)")) .append(generateUrl(jaxrsParams)) .append(generateRequestBuilder()) .append(generateHeaders(jaxrsParams)) .append(new StringStatement("setRequestBuilder(requestBuilder)")) .finish() .finish(); return Stmt.try_() .append( Stmt.declareVariable(CallContextStatus.class).asFinal().named("status").initializeWith( Stmt.newObject(CallContextStatus.class).withParameters(interceptors.toArray()))) .append( Stmt.declareVariable(RestCallContext.class).asFinal().named("callContext") .initializeWith(callContext)) .append( Stmt.loadVariable("callContext").invoke("setRequestBuilder", Variable.get("requestBuilder"))) .append( Stmt.loadVariable("callContext").invoke("setParameters", Stmt.newArray(Object.class).initialize(parameters.toArray()))) .append( Stmt.loadVariable("callContext").invoke("proceed")) .finish() .catch_(Throwable.class, "throwable") .append( Stmt.loadStatic(declaringClass, "this").invoke("handleError", Variable.get("throwable"), null, null)) .finish(); } /** * Generates the call to * {@link RequestBuilder#sendRequest(String, com.google.gwt.http.client.RequestCallback)} for * interceptable methods. * * @return statement representing the request */ private Statement generateInterceptedRequest() { return generateRequest( Stmt.nestedCall(new StringStatement("getRequestBuilder()", MetaClassFactory.get(RequestBuilder.class))), Stmt.loadStatic(declaringClass, "this")); } /** * Generates the call to * {@link RequestBuilder#sendRequest(String, com.google.gwt.http.client.RequestCallback)} for * non-interceptable methods. * * @return statement representing the request */ private Statement generateRequest() { return generateRequest(Stmt.loadVariable("requestBuilder"), Stmt.loadVariable("this")); } /** * Generates the call to * {@link RequestBuilder#sendRequest(String, com.google.gwt.http.client.RequestCallback)} for * proxy methods. * * @return statement representing the request */ private Statement generateRequest(final ContextualStatementBuilder requestBuilder, final ContextualStatementBuilder proxy) { Statement sendRequest = null; if (resourceMethod.getParameters().getEntityParameter() == null) { sendRequest = proxy.invoke("sendRequest", requestBuilder, null, responseDemarshallingCallback()); } else { final Statement body = marshal(resourceMethod.getParameters().getEntityParameter(), resourceMethod.getContentTypeHeader()); sendRequest = proxy.invoke("sendRequest", requestBuilder, body, responseDemarshallingCallback()); } return sendRequest; } /** * Generates an anonymous implementation/instance of {@link ResponseDemarshallingCallback} that * will handle the demarshalling of the HTTP response. * * @return statement representing the {@link ResponseDemarshallingCallback}. */ private Statement responseDemarshallingCallback() { final MetaClass methodReturnType = resourceMethod.getMethod().getReturnType(); final Statement result; if (methodReturnType.equals(MetaClassFactory.get(void.class))) { result = load(null).returnValue(); } else if (methodReturnType.isAssignableTo(javax.ws.rs.core.Response.class)) { result = loadVariable("response").returnValue(); } else { final Statement demarshalStmt = demarshal( methodReturnType, loadVariable("response").invoke("getText"), resourceMethod.getAcceptHeader()); result = if_(Bool.equals(loadVariable("response").invoke("getStatusCode"), 204)) .append(load(null).returnValue()) .finish() .else_() .append(nestedCall(demarshalStmt).returnValue()) .finish(); } return ObjectBuilder .newInstanceOf(ResponseDemarshallingCallback.class) .extend() .publicOverridesMethod("demarshallResponse", Parameter.of(Response.class, "response")) .append(result) .finish() .finish(); } /** * Adds path encoding to the provided statement * * @param s * the statement representing an HTTP path that should be encoded * @return statement with path encoding */ private Statement encodePath(final Statement s) { return Stmt.invokeStatic(URL.class, "encodePathSegment", s); } /** * Adds query parameter encoding to the provided statement * * @param s * the statement representing an HTTP query that should be encoded * @return statement with query param encoding */ private Statement encodeQuery(final Statement s) { return Stmt.invokeStatic(URL.class, "encodeQueryString", s); } /** * Generates the return statement of this proxy method if required. If the proxy method returns * void, it will just finish the method block. */ private void generateReturnStatement() { final Statement returnStatement = ProxyUtil.generateProxyMethodReturnStatement(resourceMethod.getMethod()); if (returnStatement != null) { methodBlock.append(returnStatement); } methodBlock.finish(); } }