/*
* Copyright 2011 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.rpc;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.cruxframework.crux.core.client.Crux;
import org.cruxframework.crux.core.client.screen.Screen;
import org.cruxframework.crux.core.shared.rpc.st.CruxSynchronizerTokenService;
import org.cruxframework.crux.core.shared.rpc.st.CruxSynchronizerTokenServiceAsync;
import org.cruxframework.crux.core.shared.rpc.st.SensitiveMethodAlreadyBeingProcessedException;
import org.cruxframework.crux.core.shared.rpc.st.UseSynchronizerToken;
import org.cruxframework.crux.core.utils.JClassUtils;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.RebindMode;
import com.google.gwt.core.ext.RebindResult;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
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.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.rebind.rpc.ProxyCreator;
/**
* This class overrides the GWT Proxy Creator to add a wrapper class around the original generated class.
*
* <p>
* The wrapper has two goals:<br>
* - Point all requests that does not inform an endPoint to the Crux FrontController Servlet.<br>
* - Handle security issues like SynchronizationToken for sensitive methods.
* </p>
*
*
* @author Thiago da Rosa de Bustamante
*
*/
public class CruxProxyCreator extends ProxyCreator
{
private static final String WRAPPER_SUFFIX = "_Wrapper";
private boolean hasSyncTokenMethod = false;
private TreeLogger logger;
/**
* @param type
*/
public CruxProxyCreator(JClassType type)
{
super(type);
this.hasSyncTokenMethod = hasSyncTokenMethod(type);
}
/**
* @see com.google.gwt.user.rebind.rpc.ProxyCreator#create(com.google.gwt.core.ext.TreeLogger, com.google.gwt.core.ext.GeneratorContext)
*/
@Override
public RebindResult create(TreeLogger logger, GeneratorContext context)
throws UnableToCompleteException
{
this.logger = logger;
RebindResult result = super.create(logger, context);
String asyncServiceTypeName = result.getResultTypeName();
return createAsyncWrapper(context, asyncServiceTypeName);
}
/**
* @param fullClassName
* @return
*/
protected String[] getPackageAndClassName(String fullClassName)
{
String className = fullClassName;
String packageName = "";
int index = -1;
if ((index = className.lastIndexOf('.')) >= 0)
{
packageName = className.substring(0, index);
className = className.substring(index + 1, className.length());
}
return new String[] {packageName, className};
}
/**
* @see com.google.gwt.user.rebind.rpc.ProxyCreator#getRemoteServiceRelativePath()
*/
@Override
protected String getRemoteServiceRelativePath()
{
String ret = super.getRemoteServiceRelativePath();
if (ret == null)
{
ret = "\"crux.rpc\"";
}
return ret;
}
private boolean checkAlreadyGenerated(GeneratorContext context, String className)
{
return context.getTypeOracle().findType(className) != null;
}
/**
* @param logger
* @param context
* @param asyncServiceTypeName
* @return
* @throws UnableToCompleteException
*/
private RebindResult createAsyncWrapper(GeneratorContext context, String asyncServiceTypeName) throws UnableToCompleteException
{
JClassType serviceAsync = context.getTypeOracle().findType(serviceIntf.getQualifiedSourceName() + "Async");
String asyncWrapperName = getProxyWrapperQualifiedName();
if (checkAlreadyGenerated(context, asyncWrapperName))
{
return new RebindResult(RebindMode.USE_EXISTING, asyncWrapperName);
}
SourceWriter srcWriter = getSourceWriter(logger, context, asyncServiceTypeName, asyncWrapperName);
if (srcWriter == null)
{
return new RebindResult(RebindMode.USE_EXISTING, asyncWrapperName);
}
generateWrapperProxyFields(srcWriter, asyncServiceTypeName);
generateWrapperProxyContructor(srcWriter);
generateProxyWrapperMethods(srcWriter, serviceAsync);
if (this.hasSyncTokenMethod)
{
generateProxyWrapperStartMethod(srcWriter);
generateProxyWrapperEndMethod(srcWriter);
generateProxyWrapperUpdateTokenMethod(srcWriter);
generateSetServiceEntryPointMethod(srcWriter);
}
srcWriter.commit(logger);
return new RebindResult(RebindMode.USE_ALL_NEW_WITH_NO_CACHING, asyncWrapperName);
}
/**
*
* @param parameter
* @param methodDescVar
* @param blocksScreen
*/
private void generateAsyncCallbackForSyncTokenMethod(SourceWriter srcWriter, JParameter parameter, String methodDescVar, boolean blocksScreen)
{
JParameterizedType parameterizedType = parameter.getType().isParameterized();
String typeSourceName = parameterizedType.getParameterizedQualifiedSourceName();
JClassType[] typeArgs = parameterizedType.getTypeArgs();
String typeParameterSourceName = typeArgs[0].getParameterizedQualifiedSourceName();
srcWriter.println("new "+typeSourceName+"(){");
srcWriter.indent();
srcWriter.println("public void onSuccess("+typeParameterSourceName+" result){");
srcWriter.indent();
srcWriter.println("try{");
srcWriter.println(parameter.getName()+".onSuccess(result);");
srcWriter.println("}finally{");
srcWriter.println("__endMethodCall("+methodDescVar+", "+blocksScreen+");");
srcWriter.println("}");
srcWriter.outdent();
srcWriter.println("}");
srcWriter.println("public void onFailure(Throwable caught){");
srcWriter.indent();
srcWriter.println("try{");
srcWriter.println(parameter.getName()+".onFailure(caught);");
srcWriter.println("}finally{");
srcWriter.println("__endMethodCall("+methodDescVar+", "+blocksScreen+");");
srcWriter.println("}");
srcWriter.outdent();
srcWriter.println("}");
srcWriter.outdent();
srcWriter.print("}");
}
/**
* @param srcWriter
* @param asyncMethod
* @param parameters
* @param methodDescVar
*/
private void generateProxyMethodCall(SourceWriter srcWriter, JMethod asyncMethod,
List<JParameter> parameters, String methodDescVar, boolean blocksScreen)
{
srcWriter.print(getProxyWrapperQualifiedName()+".super."+asyncMethod.getName() + "(");
boolean needsComma = false;
for (int i = 0; i < parameters.size(); ++i)
{
JParameter parameter = parameters.get(i);
if (needsComma)
{
srcWriter.print(", ");
}
needsComma = true;
if (i < (parameters.size()-1))
{
srcWriter.print(parameter.getName());
}
else
{
generateAsyncCallbackForSyncTokenMethod(srcWriter, parameter, methodDescVar, blocksScreen);
}
}
srcWriter.println(");");
}
/**
* @param srcWriter
*/
private void generateProxyWrapperEndMethod(SourceWriter srcWriter)
{
srcWriter.println();
srcWriter.println("private void __endMethodCall(String methodDesc, boolean unblocksScreen){");
srcWriter.indent();
srcWriter.println("Boolean isProcessing = __syncProcessingMethods.remove(methodDesc);");
srcWriter.println("if (isProcessing != null && !isProcessing){");
srcWriter.indent();
srcWriter.println("if (unblocksScreen) Screen.unblockToUser();");
srcWriter.println("setServiceEntryPoint(__baseEntrypoint);");
srcWriter.outdent();
srcWriter.println("}");
srcWriter.outdent();
srcWriter.println("}");
}
/**
* @param srcWriter
* @param asyncMethod
* @throws UnableToCompleteException
*/
private void generateProxyWrapperMethod(SourceWriter srcWriter, JMethod asyncMethod) throws UnableToCompleteException
{
try
{
JMethod syncMethod = getSyncMethodFromAsync(asyncMethod);
if (syncMethod.getAnnotation(UseSynchronizerToken.class) != null)
{
JType asyncReturnType = asyncMethod.getReturnType().getErasedType();
List<JParameter> parameters = generateProxyWrapperMethodDeclaration(srcWriter, asyncMethod, asyncReturnType);
generateProxyWrapperMethodCall(srcWriter, syncMethod, asyncMethod, asyncReturnType, parameters);
srcWriter.outdent();
srcWriter.println("}");
}
}
catch (NotFoundException e)
{
logger.log(TreeLogger.ERROR, "No method found on service interface that matches the async method ["+asyncMethod.getName()+"].");
}
}
/**
* @param srcWriter
* @param asyncMethod
* @param asyncReturnType
* @param parameters
* @throws UnableToCompleteException
*/
private void generateProxyWrapperMethodCall(SourceWriter srcWriter, JMethod syncMethod,
JMethod asyncMethod, JType asyncReturnType, List<JParameter> parameters) throws UnableToCompleteException
{
if (asyncReturnType != JPrimitiveType.VOID)
{
logger.log(TreeLogger.ERROR, "UseSynchronizer Token only can be used with void return type on Async interface.");
throw new UnableToCompleteException();
}
UseSynchronizerToken synchronizerTokenAnnot = syncMethod.getAnnotation(UseSynchronizerToken.class);
boolean blocksScreen = synchronizerTokenAnnot.blocksUserInteraction();
JParameter parameter = parameters.get(parameters.size()-1);
srcWriter.println("final String methodDesc = \""+JClassUtils.getMethodDescription(syncMethod)+"\";");
srcWriter.println("if (__startMethodCall(methodDesc, "+blocksScreen+")){");
srcWriter.indent();
srcWriter.println("__syncTokenService.getSynchronizerToken(methodDesc,");
srcWriter.println("new AsyncCallback<String>(){");
srcWriter.indent();
srcWriter.println("public void onSuccess(String result){");
srcWriter.indent();
srcWriter.println("__updateMethodToken(methodDesc, result);");
generateProxyMethodCall(srcWriter, asyncMethod, parameters, "methodDesc", blocksScreen);
srcWriter.outdent();
srcWriter.println("}");
srcWriter.println("public void onFailure(Throwable caught){");
srcWriter.indent();
srcWriter.println("try{");
srcWriter.println(parameter.getName()+".onFailure(caught);");
srcWriter.println("}finally{");
srcWriter.println("__endMethodCall(methodDesc, "+blocksScreen+");");
srcWriter.println("}");
srcWriter.outdent();
srcWriter.println("}");
srcWriter.outdent();
srcWriter.println("});");
srcWriter.outdent();
srcWriter.println("}");
if (synchronizerTokenAnnot.notifyCallsWhenProcessing())
{
srcWriter.println("else{");
srcWriter.indent();
String sensitiveErrMsg = Crux.class.getName() + ".getMessages().methodIsAlreadyBeingProcessed()";
srcWriter.println(Crux.class.getName()+".getErrorHandler().handleError("
+ sensitiveErrMsg
+ ", new " + SensitiveMethodAlreadyBeingProcessedException.class.getName() + "(" + sensitiveErrMsg + ")" +
");");
srcWriter.outdent();
srcWriter.println("}");
}
}
/**
* @param srcWriter
* @param asyncMethod
* @param asyncReturnType
* @return
*/
private List<JParameter> generateProxyWrapperMethodDeclaration(SourceWriter srcWriter,
JMethod asyncMethod, JType asyncReturnType)
{
srcWriter.println();
srcWriter.print("public ");
srcWriter.print(asyncReturnType.getQualifiedSourceName());
srcWriter.print(" ");
srcWriter.print(asyncMethod.getName() + "(");
boolean needsComma = false;
List<JParameter> parameters = new ArrayList<JParameter>();
JParameter[] asyncParams = asyncMethod.getParameters();
for (int i = 0; i < asyncParams.length; ++i)
{
JParameter param = asyncParams[i];
if (needsComma)
{
srcWriter.print(", ");
}
else
{
needsComma = true;
}
JType paramType = param.getType();
if (i == (asyncParams.length-1))
{
srcWriter.print("final ");
}
srcWriter.print(paramType.getQualifiedSourceName());
srcWriter.print(" ");
String paramName = param.getName();
parameters.add(param);
srcWriter.print(paramName);
}
srcWriter.println(") {");
srcWriter.indent();
return parameters;
}
/**
* @param srcWriter
* @param serviceAsync
* @throws UnableToCompleteException
*/
private void generateProxyWrapperMethods(SourceWriter srcWriter, JClassType serviceAsync) throws UnableToCompleteException
{
JMethod[] asyncMethods = serviceAsync.getOverridableMethods();
for (JMethod asyncMethod : asyncMethods)
{
JClassType enclosingType = asyncMethod.getEnclosingType();
JParameterizedType isParameterizedType = enclosingType.isParameterized();
if (isParameterizedType != null)
{
JMethod[] methods = isParameterizedType.getMethods();
for (int i = 0; i < methods.length; ++i)
{
if (methods[i] == asyncMethod)
{
asyncMethod = isParameterizedType.getBaseType().getMethods()[i];
}
}
}
generateProxyWrapperMethod(srcWriter, asyncMethod);
}
}
/**
* @param srcWriter
*/
private void generateProxyWrapperStartMethod(SourceWriter srcWriter)
{
srcWriter.println();
srcWriter.println("private boolean __startMethodCall(String methodDesc, boolean blocksScreen){");
srcWriter.indent();
srcWriter.println("boolean ret = !__syncProcessingMethods.containsKey(methodDesc);");
srcWriter.println("if (ret){");
srcWriter.indent();
srcWriter.println("__syncProcessingMethods.put(methodDesc, true);");
srcWriter.println("if (blocksScreen) Screen.blockToUser();");
srcWriter.outdent();
srcWriter.println("}");
srcWriter.println("return ret;");
srcWriter.outdent();
srcWriter.println("}");
}
/**
* @param srcWriter
*/
private void generateProxyWrapperUpdateTokenMethod(SourceWriter srcWriter)
{
srcWriter.println();
srcWriter.println("private void __updateMethodToken(String methodDesc, String token){");
srcWriter.indent();
srcWriter.println("__syncProcessingMethods.put(methodDesc, false);");
srcWriter.println("if (this.__hasParameters){");
srcWriter.indent();
srcWriter.println("super.setServiceEntryPoint(__baseEntrypoint + \"&"+CruxSynchronizerTokenService.CRUX_SYNC_TOKEN_PARAM+"=\" + token);");
srcWriter.outdent();
srcWriter.println("}else{");
srcWriter.indent();
srcWriter.println("super.setServiceEntryPoint(__baseEntrypoint + \"?"+CruxSynchronizerTokenService.CRUX_SYNC_TOKEN_PARAM+"=\" + token);");
srcWriter.outdent();
srcWriter.println("}");
srcWriter.outdent();
srcWriter.println("}");
}
/**
* @param srcWriter
*/
private void generateSetServiceEntryPointMethod(SourceWriter srcWriter)
{
srcWriter.println();
srcWriter.println("public void setServiceEntryPoint(String entryPoint){");
srcWriter.indent();
srcWriter.println("__baseEntrypoint = entryPoint;");
srcWriter.println("super.setServiceEntryPoint(entryPoint);");
srcWriter.outdent();
srcWriter.println("}");
}
/**
* @param srcWriter
*/
private void generateWrapperProxyContructor(SourceWriter srcWriter)
{
srcWriter.println("public " + getProxyWrapperSimpleName() + "() {");
srcWriter.indent();
srcWriter.println("super();");
srcWriter.println("this.__hasParameters = (getServiceEntryPoint()!=null?getServiceEntryPoint().indexOf('?')>0:false);");
if (this.hasSyncTokenMethod)
{
srcWriter.println("this.__baseEntrypoint = getServiceEntryPoint();");
srcWriter.println("this.__syncTokenService = (CruxSynchronizerTokenServiceAsync)GWT.create(CruxSynchronizerTokenService.class);");
}
srcWriter.println("String locale = Screen.getLocale();");
srcWriter.println("if (locale != null && locale.trim().length() > 0){");
srcWriter.indent();
srcWriter.println("if (this.__hasParameters){");
srcWriter.indent();
srcWriter.println("setServiceEntryPoint(getServiceEntryPoint() + \"&locale=\" + locale);");
srcWriter.outdent();
srcWriter.println("}else{");
srcWriter.indent();
srcWriter.println("setServiceEntryPoint(getServiceEntryPoint() + \"?locale=\" + locale);");
srcWriter.println("this.__hasParameters = true;");
srcWriter.outdent();
srcWriter.println("}");
srcWriter.outdent();
srcWriter.println("}");
srcWriter.outdent();
srcWriter.println("}");
}
/**
* @param srcWriter
* @param asyncServiceInterfaceName
*/
private void generateWrapperProxyFields(SourceWriter srcWriter, String asyncServiceInterfaceName)
{
srcWriter.println("private boolean __hasParameters = false;");
if (this.hasSyncTokenMethod)
{
srcWriter.println("private Map<String, Boolean> __syncProcessingMethods = new HashMap<String, Boolean>();");
srcWriter.println("private CruxSynchronizerTokenServiceAsync __syncTokenService;");
srcWriter.println("private String __baseEntrypoint;");
}
}
/**
* @return
*/
private String getProxyWrapperQualifiedName()
{
return serviceIntf.getQualifiedSourceName()+ WRAPPER_SUFFIX;
}
/**
* @return
*/
private String getProxyWrapperSimpleName()
{
return serviceIntf.getSimpleSourceName()+WRAPPER_SUFFIX;
}
/**
* @param logger
* @param ctx
* @param asyncServiceName
* @return
*/
private SourceWriter getSourceWriter(TreeLogger logger, GeneratorContext ctx, String asyncServiceName, String asyncWrapperName)
{
String name[] = getPackageAndClassName(asyncWrapperName);
String packageName = name[0];
String className = name[1];
PrintWriter printWriter = ctx.tryCreate(logger, packageName, className);
if (printWriter == null)
{
return null;
}
ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
packageName, className);
composerFactory.addImport(Screen.class.getName());
composerFactory.addImport(Map.class.getName());
composerFactory.addImport(HashMap.class.getName());
composerFactory.addImport(AsyncCallback.class.getName());
if (this.hasSyncTokenMethod)
{
composerFactory.addImport(CruxSynchronizerTokenService.class.getName());
composerFactory.addImport(CruxSynchronizerTokenServiceAsync.class.getName());
composerFactory.addImport(GWT.class.getName());
}
composerFactory.setSuperclass(asyncServiceName);
return composerFactory.createSourceWriter(ctx, printWriter);
}
/**
* @param asyncMethod
* @return
* @throws NotFoundException
*/
private JMethod getSyncMethodFromAsync(JMethod asyncMethod) throws NotFoundException
{
JParameter[] parameters = asyncMethod.getParameters();
List<JType> syncParamTypes = new ArrayList<JType>();
if (parameters != null && parameters.length > 1)
{
for (int i=0; i<parameters.length-1; i++)
{
JParameter jParameter = parameters[i];
syncParamTypes.add(jParameter.getType());
}
}
return serviceIntf.getMethod(asyncMethod.getName(), syncParamTypes.toArray(new JType[syncParamTypes.size()]));
}
/**
* @param type
* @return
*/
private boolean hasSyncTokenMethod(JClassType type)
{
JMethod[] methods = type.getOverridableMethods();
for (JMethod jMethod : methods)
{
if (jMethod.getAnnotation(UseSynchronizerToken.class) != null)
{
return true;
}
}
return false;
}
}