/* * Copyright 2013 cruxframework.org. * * 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.cruxframework.crux.core.rebind.rest; import java.lang.annotation.Annotation; import java.math.BigInteger; import java.security.SecureRandom; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.cruxframework.crux.core.client.Crux; import org.cruxframework.crux.core.client.collection.FastMap; import org.cruxframework.crux.core.client.rest.Callback; import org.cruxframework.crux.core.client.rest.RestError; import org.cruxframework.crux.core.client.rest.RestProxy; import org.cruxframework.crux.core.client.rest.RestProxy.UseJsonP; import org.cruxframework.crux.core.client.rpc.CruxRpcRequestBuilder; import org.cruxframework.crux.core.client.screen.Screen; import org.cruxframework.crux.core.client.screen.views.View; import org.cruxframework.crux.core.client.screen.views.ViewAware; import org.cruxframework.crux.core.client.screen.views.ViewBindable; import org.cruxframework.crux.core.client.utils.EscapeUtils; import org.cruxframework.crux.core.client.utils.StringUtils; import org.cruxframework.crux.core.config.ConfigurationFactory; import org.cruxframework.crux.core.rebind.AbstractInterfaceWrapperProxyCreator; import org.cruxframework.crux.core.rebind.CruxGeneratorException; import org.cruxframework.crux.core.rebind.context.RebindContext; import org.cruxframework.crux.core.server.rest.util.Encode; import org.cruxframework.crux.core.server.rest.util.HttpHeaderNames; import org.cruxframework.crux.core.server.rest.util.InvalidRestMethod; import org.cruxframework.crux.core.shared.rest.annotation.Path; import org.cruxframework.crux.core.shared.rest.annotation.StateValidationModel; import org.cruxframework.crux.core.utils.JClassUtils; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsonUtils; import com.google.gwt.core.ext.typeinfo.JClassType; import com.google.gwt.core.ext.typeinfo.JMethod; import com.google.gwt.core.ext.typeinfo.JParameter; import com.google.gwt.core.ext.typeinfo.JPrimitiveType; import com.google.gwt.core.ext.typeinfo.JType; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.RequestBuilder; import com.google.gwt.http.client.RequestCallback; import com.google.gwt.http.client.Response; import com.google.gwt.http.client.URL; import com.google.gwt.json.client.JSONNumber; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONParser; import com.google.gwt.json.client.JSONString; import com.google.gwt.json.client.JSONValue; import com.google.gwt.jsonp.client.JsonpRequestBuilder; import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.user.client.rpc.AsyncCallback; /** * This class creates a client proxy for calling rest services * * @author Thiago da Rosa de Bustamante * */ public abstract class CruxRestProxyCreator extends AbstractInterfaceWrapperProxyCreator { private static final String RANDOM_TOKEN = new BigInteger(130, new SecureRandom()).toString(32).substring(0, 5); protected JClassType callbackType; protected JClassType javascriptObjectType; protected String serviceBasePath; protected Set<String> readMethods = new HashSet<String>(); protected Set<String> updateMethods = new HashSet<String>(); protected Set<RestMethodInfo> restMethods = new HashSet<RestMethodInfo>(); protected boolean mustGenerateStateControlMethods; protected QueryParameterHandler queryParameterHandler; protected BodyParameterHandler bodyParameterHandler; protected JClassType restProxyType; protected boolean useJsonP; protected String jsonPCallbackParam; protected String jsonPFailureCallbackParam; protected JsonPRestCreatorHelper jsonPRestCreatorHelper; private JClassType viewBindableType; private JClassType viewAwareType; public CruxRestProxyCreator(RebindContext context, JClassType baseIntf) { super(context, baseIntf, true); callbackType = context.getGeneratorContext().getTypeOracle().findType(Callback.class.getCanonicalName()); restProxyType = context.getGeneratorContext().getTypeOracle().findType(RestProxy.class.getCanonicalName()); javascriptObjectType = context.getGeneratorContext().getTypeOracle().findType(JavaScriptObject.class.getCanonicalName()); viewBindableType = context.getGeneratorContext().getTypeOracle().findType(ViewBindable.class.getCanonicalName()); viewAwareType = context.getGeneratorContext().getTypeOracle().findType(ViewAware.class.getCanonicalName()); UseJsonP jsonP = baseIntf.getAnnotation(UseJsonP.class); useJsonP = jsonP != null; if (useJsonP) { jsonPRestCreatorHelper = new JsonPRestCreatorHelper(context); jsonPCallbackParam = jsonP.callbackParam(); jsonPFailureCallbackParam = jsonP.failureCallbackParam(); } queryParameterHandler = new QueryParameterHandler(context); bodyParameterHandler = new BodyParameterHandler(context); serviceBasePath = getServiceBasePath(context); initializeRestMethods(); } protected abstract String getServiceBasePath(RebindContext context); protected abstract void generateHostPathInitialization(SourcePrinter srcWriter); protected abstract RestMethodInfo getRestMethodInfo(JMethod method) throws InvalidRestMethod; @Override protected void generateProxyContructor(SourcePrinter srcWriter) throws CruxGeneratorException { srcWriter.println("public "+getProxySimpleName()+"(){"); generateHostPathInitialization(srcWriter); srcWriter.println("}"); } @Override protected void generateProxyFields(SourcePrinter srcWriter) throws CruxGeneratorException { if (mustGenerateStateControlMethods) { srcWriter.println(FastMap.class.getCanonicalName()+"<String> __currentEtags = new "+FastMap.class.getCanonicalName()+"<String>();"); } srcWriter.println("private String __hostPath;"); srcWriter.println("private static Logger __log = Logger.getLogger("+getProxyQualifiedName()+".class.getName());"); srcWriter.println("private String __view;"); } @Override protected void generateProxyMethods(SourcePrinter srcWriter) throws CruxGeneratorException { if (mustGenerateStateControlMethods) { generateStateControlMethods(srcWriter); } for (RestMethodInfo methodInfo : restMethods) { generateWrapperMethod(methodInfo, srcWriter); } generateSetEndpointMethod(srcWriter); generateViewBindableMethods(srcWriter); } protected void generateViewBindableMethods(SourcePrinter sourceWriter) { sourceWriter.println("public String getBoundCruxViewId(){"); sourceWriter.println("return this.__view;"); sourceWriter.println("}"); sourceWriter.println(); sourceWriter.println("public "+View.class.getCanonicalName()+" getBoundCruxView(){"); sourceWriter.println("return (this.__view!=null?"+View.class.getCanonicalName()+".getView(this.__view):null);"); sourceWriter.println("}"); sourceWriter.println(); sourceWriter.println("public void bindCruxView(String view){"); sourceWriter.println("this.__view = view;"); sourceWriter.println("}"); sourceWriter.println(); } protected void generateSetEndpointMethod(SourcePrinter srcWriter) { srcWriter.println("public void setEndpoint(String address){"); srcWriter.println("this.__hostPath = address;"); srcWriter.println("if (__hostPath.endsWith(\"/\")){"); srcWriter.println("__hostPath = __hostPath.substring(0, __hostPath.length()-1);"); srcWriter.println("}"); srcWriter.println("}"); } protected void generateStateControlMethods(SourcePrinter srcWriter) { srcWriter.println("public boolean __readCurrentEtag(String uri, RequestBuilder builder, boolean required){"); srcWriter.println("String etag = __currentEtags.get(uri);"); srcWriter.println("if (required && etag == null){"); srcWriter.println("return false;"); srcWriter.println("}"); srcWriter.println("if (etag != null){"); srcWriter.println("builder.setHeader("+EscapeUtils.quote(HttpHeaderNames.IF_MATCH)+", etag);"); srcWriter.println("}"); srcWriter.println("return true;"); srcWriter.println("}"); srcWriter.println("public void __saveCurrentEtag(String uri, Response response){"); srcWriter.println("String etag = response.getHeader("+EscapeUtils.quote(HttpHeaderNames.ETAG)+");"); srcWriter.println("__currentEtags.put(uri, etag);"); srcWriter.println("}"); } protected void initializeRestMethods() { JMethod[] methods = baseIntf.getOverridableMethods(); for (JMethod method : methods) { try { if ((!restProxyType.equals(method.getEnclosingType())) && (!viewAwareType.equals(method.getEnclosingType())) && (!viewBindableType.equals(method.getEnclosingType()))) { validateProxyMethod(method); RestMethodInfo methodInfo = getRestMethodInfo(method); if (useJsonP) { if (methodInfo.isReadMethod) { readMethods.add(methodInfo.methodURI); } else if (methodInfo.validationModel != null && !methodInfo.validationModel.equals(StateValidationModel.NO_VALIDATE)) { updateMethods.add(methodInfo.methodURI); } } restMethods.add(methodInfo); } } catch (InvalidRestMethod e) { throw new CruxGeneratorException("Invalid Method: " + method.getEnclosingType().getName() + "." + method.getName() + "().", e); } } mustGenerateStateControlMethods = false; for (String methodURI : updateMethods) { if (!readMethods.contains(methodURI)) { throw new CruxGeneratorException("Can not create the rest proxy. Can not found the " + "GET method for state dependent write method ["+methodURI+"]."); } mustGenerateStateControlMethods = true; } } protected void generateWrapperMethod(RestMethodInfo methodInfo, SourcePrinter srcWriter) { List<JParameter> parameters = generateProxyWrapperMethodDeclaration(srcWriter, methodInfo.method); JParameter callbackParameter = parameters.get(parameters.size()-1); String callbackResultTypeName = getCallbackResultTypeName(callbackParameter.getType().isClassOrInterface()); String callbackParameterName = callbackParameter.getName(); srcWriter.println("String baseURIPath = " + EscapeUtils.quote(methodInfo.methodURI.startsWith("/")?methodInfo.methodURI:"/"+methodInfo.methodURI) + ";"); queryParameterHandler.generateMethodParamToURICode(srcWriter, methodInfo, "baseURIPath"); srcWriter.println("final String restURI = __hostPath + baseURIPath;"); if (useJsonP) { jsonPRestCreatorHelper.generateJSONPInvocation(methodInfo, srcWriter, callbackParameter, callbackResultTypeName, callbackParameterName, "restURI", jsonPCallbackParam, jsonPFailureCallbackParam); } else { generateAJAXInvocation(methodInfo, srcWriter, callbackParameter, callbackResultTypeName, callbackParameterName, "restURI"); } srcWriter.println("}"); } protected void generateAJAXInvocation(RestMethodInfo methodInfo, SourcePrinter srcWriter, JParameter callbackParameter, String callbackResultTypeName, String callbackParameterName, String restURIParam) { srcWriter.println("RequestBuilder builder = " + getRequestBuilderInitialization(methodInfo, restURIParam) + ";"); setLocaleInfo(srcWriter, "builder"); if (ConfigurationFactory.getConfigurations().sendCruxViewNameOnClientRequests().equals("true")) { srcWriter.println("builder.setHeader("+EscapeUtils.quote(CruxRpcRequestBuilder.VIEW_INFO_HEADER)+", __view);"); } srcWriter.println("builder.setCallback(new RequestCallback(){"); String responseVariable = getNonConflictedVarName("response", callbackParameter.getName()); srcWriter.println("public void onResponseReceived(Request request, Response "+responseVariable+"){"); srcWriter.println("int s = ("+responseVariable+".getStatusCode()-200);"); srcWriter.println("if (s >= 0 && s < 10){"); generateSuccessCallHandlingCode(methodInfo, srcWriter, callbackParameter, callbackResultTypeName, callbackParameterName, restURIParam); srcWriter.println("}else{ "); generateExceptionCallHandlingCode(methodInfo, srcWriter, callbackParameterName, responseVariable); srcWriter.println("}"); srcWriter.println("}"); srcWriter.println("public void onError(Request request, Throwable exception){"); srcWriter.println(callbackParameterName+".onError(new RestError(-1, Crux.getMessages().restServiceUnexpectedError(exception.getMessage())));"); srcWriter.println("}"); srcWriter.println("});"); srcWriter.println("try{"); bodyParameterHandler.generateMethodParamToBodyCode(srcWriter, methodInfo, "builder", methodInfo.httpMethod); generateValidateStateBlock(srcWriter, methodInfo.validationModel, "builder", restURIParam, methodInfo.methodURI, callbackParameterName); generateXSRFHeaderProtectionForWrites(methodInfo.httpMethod, "builder", srcWriter); srcWriter.println("builder.send();"); srcWriter.println("}catch (Exception e){"); generateLogHandlingCode(srcWriter, "Level.SEVERE", "e"); srcWriter.println(callbackParameterName+".onError(new RestError(-1, Crux.getMessages().restServiceUnexpectedError(e.getMessage())));"); srcWriter.println("}"); } protected String getRequestBuilderInitialization(RestMethodInfo methodInfo, String restURIParam) { return "new RequestBuilder(RequestBuilder."+methodInfo.httpMethod+", "+restURIParam+")"; } protected static void generateLogHandlingCode(SourcePrinter srcWriter, String logLevel, String e) { srcWriter.println("if (LogConfiguration.loggingIsEnabled()){"); srcWriter.println("__log.log("+logLevel+", "+e+".getMessage(), e);"); srcWriter.println("}"); } protected void generateExceptionCallHandlingCode(RestMethodInfo methodInfo, SourcePrinter srcWriter, String callbackParameterName, String responseVariable) { try { srcWriter.println("if (LogConfiguration.loggingIsEnabled()){"); srcWriter.println("__log.log(Level.SEVERE, \"Error received from service: \"+"+responseVariable+".getText());"); srcWriter.println("}"); //try to parse response object srcWriter.println("JSONObject jsonObject = null;"); srcWriter.println("try {"); srcWriter.println("jsonObject = JSONParser.parseStrict("+responseVariable+".getText()).isObject();"); //For instance if we have 400-404 server response, the object is not a json value. This will make JSON throws an Exception srcWriter.println("} catch (Exception exception) {"); srcWriter.println(callbackParameterName+".onError(new RestError("+responseVariable+".getStatusCode(), "+responseVariable+".getText()));"); srcWriter.println("return;"); srcWriter.println("}"); srcWriter.println(callbackParameterName+".onError(new RestError("+responseVariable+".getStatusCode(), (jsonObject.get(\"message\") != null && jsonObject.get(\"message\").isString() != null) ? jsonObject.get(\"message\").isString().stringValue() : \"\"));"); } catch (Exception e) { throw new CruxGeneratorException("Error generatirng exception handlers for type ["+baseIntf.getParameterizedQualifiedSourceName()+"].", e); } } //TODO: put this in a DeferredBindingUtils class protected static String getNonConflictedVarName(String originalVar, String possibleConflictedVar) { if (possibleConflictedVar.equals(originalVar)) { return originalVar + "_" + RANDOM_TOKEN; } //return the same variable to improve code legibility return originalVar; } protected void generateSuccessCallHandlingCode(RestMethodInfo methodInfo, SourcePrinter srcWriter, JParameter callbackParameter, String callbackResultTypeName, String callbackParameterName, String restURIParam) { String resultVariable = getNonConflictedVarName("result", callbackParameter.getName()); String responseVariable = getNonConflictedVarName("response", callbackParameter.getName()); if (!callbackResultTypeName.equalsIgnoreCase("void")) { JClassType callbackResultType = JClassUtils.getTypeArgForGenericType(callbackParameter.getType().isClassOrInterface()); srcWriter.println("String jsonText = "+responseVariable+".getText();"); srcWriter.println("if (Response.SC_NO_CONTENT != "+responseVariable+".getStatusCode() && !"+StringUtils.class.getCanonicalName()+".isEmpty(jsonText)){"); srcWriter.println("try{"); if (callbackResultType != null && callbackResultType.isAssignableTo(javascriptObjectType)) { srcWriter.println(callbackResultTypeName+" "+resultVariable+" = "+JsonUtils.class.getCanonicalName()+".safeEval(jsonText);"); } else { srcWriter.println("JSONValue jsonValue = JSONParser.parseStrict(jsonText);"); String serializerName = new JSonSerializerProxyCreator(context, callbackResultType).create(); srcWriter.println(callbackResultTypeName+" "+resultVariable+" = new "+serializerName+"().decode(jsonValue);"); } generateSaveStateBlock(srcWriter, methodInfo.isReadMethod, responseVariable, restURIParam, methodInfo.methodURI); srcWriter.println(callbackParameterName+".onSuccess("+resultVariable+");"); srcWriter.println("}catch (Exception e){"); generateLogHandlingCode(srcWriter, "Level.SEVERE", "e"); srcWriter.println("}"); srcWriter.println("}else {"); generateSaveStateBlock(srcWriter, methodInfo.isReadMethod, responseVariable, restURIParam, methodInfo.methodURI); srcWriter.println(callbackParameterName+".onSuccess(null);"); srcWriter.println("}"); } else { generateSaveStateBlock(srcWriter, methodInfo.isReadMethod, responseVariable, restURIParam, methodInfo.methodURI); srcWriter.println(callbackParameterName+".onSuccess(null);"); } } protected void setLocaleInfo(SourcePrinter srcWriter, String builderVariable) { srcWriter.println("String _locale = "+Screen.class.getCanonicalName()+".getLocale();"); srcWriter.println("if (_locale != null && !"+StringUtils.class.getCanonicalName()+".unsafeEquals(_locale, \"default\")){"); srcWriter.println(builderVariable+".setHeader(\""+HttpHeaderNames.ACCEPT_LANGUAGE+"\", _locale.replace('_', '-'));");// pt-BR,pt;q=0.8,en-US;q=0.5,en;q=0.3 srcWriter.println("}"); } protected void generateXSRFHeaderProtectionForWrites(String httpMethod, String builderVar, SourcePrinter srcWriter) { if (!httpMethod.equals("GET")) { srcWriter.println(builderVar+".setHeader("+EscapeUtils.quote(HttpHeaderNames.XSRF_PROTECTION_HEADER)+", \"1\");"); } } protected void generateSaveStateBlock(SourcePrinter srcWriter, boolean isReadMethod, String responseVar, String uriVar, String uri) { if (mustGenerateStateControlMethods && readMethods.contains(uri) && updateMethods.contains(uri)) { if (isReadMethod) { srcWriter.println("__saveCurrentEtag("+uriVar+", "+responseVar+");"); } } } protected void generateValidateStateBlock(SourcePrinter srcWriter, StateValidationModel validationModel, String builderVar, String uriVar, String uri, String callbackParameterName) { if (mustGenerateStateControlMethods && readMethods.contains(uri) && updateMethods.contains(uri)) { if (validationModel != null) { srcWriter.println("if (!__readCurrentEtag("+uriVar+", "+builderVar+","+validationModel.equals(StateValidationModel.ENSURE_STATE_MATCHES)+")){"); srcWriter.println(callbackParameterName+".onError(new RestError(-1, Crux.getMessages().restServiceMissingStateEtag("+uriVar+")));"); srcWriter.println("return;"); srcWriter.println("}"); } } } protected String getCallbackResultTypeName(JClassType callbackParameter) { JClassType jClassType = JClassUtils.getTypeArgForGenericType(callbackParameter); if (jClassType.isPrimitive() != null) { return jClassType.isPrimitive().getQualifiedBoxedSourceName(); } return jClassType.getParameterizedQualifiedSourceName(); } protected String getRestURI(JMethod method, Annotation[][] parameterAnnotations, Path path) { String methodPath = paths(serviceBasePath); if (path != null) { methodPath = paths(methodPath, path.value()); } String queryString = queryParameterHandler.getQueryString(method, parameterAnnotations); if (queryString.length() > 0) { return methodPath+"?"+queryString; } return methodPath; } protected String paths(String basePath, String... segments) { String path = basePath; if (path == null) { path = ""; } for (String segment : segments) { if ("".equals(segment)) { continue; } if (path.endsWith("/")) { if (segment.startsWith("/")) { segment = segment.substring(1); if ("".equals(segment)) { continue; } } segment = Encode.encodePath(PathUtils.getSegmentParameter(segment)); path += segment; } else { segment = Encode.encodePath(PathUtils.getSegmentParameter(segment)); if ("".equals(path)) { path = segment; } else if (segment.startsWith("/")) { path += segment; } else { path += "/" + segment; } } } return path; } @Override protected String[] getImports() { return new String[] { Level.class.getCanonicalName(), Logger.class.getCanonicalName(), LogConfiguration.class.getCanonicalName(), RequestBuilder.class.getCanonicalName(), RequestCallback.class.getCanonicalName(), Request.class.getCanonicalName(), Response.class.getCanonicalName(), JsonUtils.class.getCanonicalName(), JSONValue.class.getCanonicalName(), JSONObject.class.getCanonicalName(), JSONNumber.class.getCanonicalName(), JSONString.class.getCanonicalName(), JSONParser.class.getCanonicalName(), URL.class.getCanonicalName(), Crux.class.getCanonicalName(), Callback.class.getCanonicalName(), StringUtils.class.getCanonicalName(), RestError.class.getCanonicalName(), JsonpRequestBuilder.class.getCanonicalName(), AsyncCallback.class.getCanonicalName() }; } protected void validateProxyMethod(JMethod method) { if (method.getReturnType() != JPrimitiveType.VOID) { throw new CruxGeneratorException("Invalid signature for rest proxy method <"+method.getName()+">. Any method must be void"); } JType[] parameterTypes = method.getParameterTypes(); if (parameterTypes == null || parameterTypes.length < 1) { throw new CruxGeneratorException("Invalid signature for rest proxy method <"+method.getName()+">. Any method must have a last parameter of type Callback"); } JClassType lastParameterType = parameterTypes[parameterTypes.length - 1].isClassOrInterface(); if (lastParameterType == null || !callbackType.isAssignableFrom(lastParameterType)) { throw new CruxGeneratorException("Invalid signature for rest proxy method <"+method.getName()+">. Any method must have a last parameter of type Callback"); } } }