/*
* 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.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import org.cruxframework.crux.core.client.rest.RestProxy.TargetEndPoint;
import org.cruxframework.crux.core.client.rest.RestProxy.TargetRestService;
import org.cruxframework.crux.core.client.utils.EscapeUtils;
import org.cruxframework.crux.core.config.ConfigurationFactory;
import org.cruxframework.crux.core.rebind.CruxGeneratorException;
import org.cruxframework.crux.core.rebind.context.RebindContext;
import org.cruxframework.crux.core.server.rest.core.registry.RestServiceFactoryInitializer;
import org.cruxframework.crux.core.server.rest.util.HttpMethodHelper;
import org.cruxframework.crux.core.server.rest.util.InvalidRestMethod;
import org.cruxframework.crux.core.shared.rest.RestException;
import org.cruxframework.crux.core.shared.rest.annotation.GET;
import org.cruxframework.crux.core.shared.rest.annotation.Path;
import org.cruxframework.crux.core.shared.rest.annotation.StateValidationModel;
import org.cruxframework.crux.core.utils.EncryptUtils;
import org.cruxframework.crux.core.utils.JClassUtils;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JEnumConstant;
import com.google.gwt.core.ext.typeinfo.JEnumType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
/**
* This class creates a client proxy for calling rest services
*
* @author Thiago da Rosa de Bustamante
*
*/
public class CruxRestProxyCreatorFromServerMetadata extends CruxRestProxyCreator
{
private Class<?> restImplementationClass;
public CruxRestProxyCreatorFromServerMetadata(RebindContext context, JClassType baseIntf)
{
super(context, baseIntf);
}
@Override
protected void generateHostPathInitialization(SourcePrinter srcWriter)
{
TargetEndPoint targetEndPoint = baseIntf.getAnnotation(TargetEndPoint.class);
if (targetEndPoint != null)
{
String basePath = targetEndPoint.value();
if (basePath.endsWith("/"))
{
basePath = basePath.substring(0, basePath.length()-1);
}
srcWriter.println("__hostPath = \""+basePath+"\";");
}
else
{
if(Boolean.parseBoolean(ConfigurationFactory.getConfigurations().enableRestHostPageBaseURL()))
{
srcWriter.println("__hostPath = com.google.gwt.core.client.GWT.getHostPageBaseURL();");
} else
{
srcWriter.println("__hostPath = com.google.gwt.core.client.GWT.getModuleBaseURL();");
}
srcWriter.println("__hostPath = __hostPath.substring(0, __hostPath.lastIndexOf(com.google.gwt.core.client.GWT.getModuleName()));");
}
}
@Override
protected String getServiceBasePath(RebindContext context)
{
restImplementationClass = getRestImplementationClass(baseIntf);
String basePath;
try
{
basePath = context.getGeneratorContext().getPropertyOracle().getConfigurationProperty("crux.rest.base.path").getValues().get(0);
if (basePath.endsWith("/"))
{
basePath = basePath.substring(0, basePath.length()-1);
}
}
catch (Exception e)
{
basePath = "rest";
}
String value = restImplementationClass.getAnnotation(Path.class).value();
if (value == null)
{
value = "";
}
else if (value.startsWith("/"))
{
value = value.substring(1);
}
value = PathUtils.getSegmentParameter(value);
return basePath+"/"+value;
}
@Override
protected RestMethodInfo getRestMethodInfo(JMethod method) throws InvalidRestMethod
{
Method implementationMethod = getImplementationMethod(method);
Annotation[][] parameterAnnotations = implementationMethod.getParameterAnnotations();
String methodURI = getRestURI(method, parameterAnnotations, implementationMethod.getAnnotation(Path.class));
StateValidationModel validationModel = HttpMethodHelper.getStateValidationModel(implementationMethod);
String httpMethod = HttpMethodHelper.getHttpMethod(implementationMethod.getAnnotations(), false);
boolean isReadMethod = implementationMethod.getAnnotation(GET.class) != null;
RestMethodInfo methodInfo = new RestMethodInfo(method, parameterAnnotations, methodURI, httpMethod, validationModel, isReadMethod);
return methodInfo;
}
@Override
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("}");
Class<?>[] restExceptionTypes = getRestExceptionTypes(getImplementationMethod(methodInfo.method));
if (restExceptionTypes != null && restExceptionTypes.length > 0)
{
srcWriter.println("JSONValue exId = jsonObject.get(\"exId\");");
srcWriter.println("if (exId == null){");
srcWriter.println("JSONValue jsonErrorMsg = jsonObject.get(\"message\");");
srcWriter.println("String stringJsonErrorMsg = (jsonErrorMsg != null && jsonErrorMsg.isString() != null) ? jsonErrorMsg.isString().stringValue() : \"\";");
srcWriter.println(callbackParameterName+".onError(new RestError("+responseVariable+".getStatusCode(), stringJsonErrorMsg));");
srcWriter.println("} else {");
srcWriter.println("String hash = exId.isString().stringValue();");
boolean first = true;
for (Class<?> restException : restExceptionTypes)
{
JClassType exceptionType = context.getGeneratorContext().getTypeOracle().findType(restException.getCanonicalName());
if (exceptionType == null)
{
throw new CruxGeneratorException("Exception type ["+restException.getCanonicalName()+"] can not be used on client code. Add this exeption to a GWT client package.");
}
if (!first)
{
srcWriter.print("else ");
}
first = false;
srcWriter.println("if (StringUtils.unsafeEquals(hash,"+EscapeUtils.quote(EncryptUtils.hash(exceptionType.getParameterizedQualifiedSourceName()))+")){");
String serializerName = new JSonSerializerProxyCreator(context, exceptionType).create();
srcWriter.println("Exception ex = new "+serializerName+"().decode(jsonObject.get(\"exData\"));");
srcWriter.println(callbackParameterName+".onError(ex);");
srcWriter.println("}");
}
srcWriter.println("else {");
srcWriter.println(callbackParameterName+".onError(new RestError("+responseVariable+".getStatusCode(), jsonObject.get(\"exData\").toString()));");
srcWriter.println("}");
srcWriter.println("}");
}
else
{
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);
}
}
private Class<?> getRestImplementationClass(JClassType baseIntf)
{
TargetRestService restService = baseIntf.getAnnotation(TargetRestService.class);
if (restService == null)
{
throw new CruxGeneratorException("Can not create the rest proxy. Use @RestProxy.TargetRestService annotation to inform the target of current proxy.");
}
String serviceName = restService.value();
try
{
return RestServiceFactoryInitializer.getServiceFactory().getServiceClass(serviceName);
}
catch (Exception e)
{
throw new CruxGeneratorException("Can not create the rest proxy. Can not found the implementationClass for service name ["+serviceName+"].");
}
}
private Class<?>[] getRestExceptionTypes(Method method)
{
List<Class<?>> result = new ArrayList<Class<?>>();
Class<?>[] types = method.getExceptionTypes();
for (Class<?> exceptionClass : types)
{
if (RestException.class.isAssignableFrom(exceptionClass))
{
result.add(exceptionClass);
}
}
return result.toArray(new Class[result.size()]);
}
private Method getImplementationMethod(JMethod method)
{
Method implementationMethod = getImplementationMethod(method, restImplementationClass);
validateImplementationMethod(method, implementationMethod);
return implementationMethod;
}
private Method getImplementationMethod(JMethod method, Class<?> clazz)
{
Method[] allMethods = clazz.getMethods();
for (Method m: allMethods)
{
if (!m.isSynthetic() && m.getName().equals(method.getName()))
{
return m;
}
}
Class<?> superClass = clazz.getSuperclass();
if (superClass != null)
{
return getImplementationMethod(method, superClass);
}
return null;
}
private void validateImplementationMethod(JMethod method, Method implementationMethod)
{
if (implementationMethod == null)
{
throw new CruxGeneratorException("Invalid signature for rest proxy method. Can not found the implementation method: "+
method.getName()+", on class: "+restImplementationClass.getCanonicalName());
}
if (!Modifier.isPublic(implementationMethod.getModifiers()))
{
throw new CruxGeneratorException("Invalid signature for rest proxy method. Implementation method: "+
method.getName()+", on class: "+restImplementationClass.getCanonicalName()+", is not public.");
}
Class<?>[] implTypes = implementationMethod.getParameterTypes();
JType[] proxyTypes = method.getParameterTypes();
if ((proxyTypes.length -1)!= implTypes.length)
{
throw new CruxGeneratorException("Invalid signature for rest proxy method. The implementation method: "+
method.getName()+", on class: "+restImplementationClass.getCanonicalName() + " does not match the parameters list.");
}
for (int i=0; i<implTypes.length; i++)
{
if (!isTypesCompatiblesForSerialization(implTypes[i], proxyTypes[i]))
{
throw new CruxGeneratorException("Invalid signature for rest proxy method. Incompatible parameters on method["+method.getReadableDeclaration()+"]");
}
}
JClassType lastParameterType = proxyTypes[proxyTypes.length - 1].isClassOrInterface();
if (!isTypesCompatiblesForSerialization(implementationMethod.getReturnType(), JClassUtils.getTypeArgForGenericType(lastParameterType)))
{
throw new CruxGeneratorException("Invalid signature for rest proxy method. Return type of implementation method is not compatible with Callback's type. Method["+method.getReadableDeclaration()+"]");
}
}
private boolean isTypesCompatiblesForSerialization(Class<?> class1, JType jType)
{
if (jType.isEnum() != null)
{
return isEnumTypesCompatibles(class1, jType.isEnum());
}
else if (JClassUtils.isSimpleType(jType))
{
return (getAllowedType(jType).contains(class1));
}
else
{
JClassType classOrInterface = jType.isClassOrInterface();
if (classOrInterface != null)
{
if (javascriptObjectType.isAssignableFrom(classOrInterface))
{
if (classOrInterface.getQualifiedSourceName().equals(JsArray.class.getCanonicalName()))
{
boolean validArray = false;
if (class1.isArray())
{
Class<?> componentType = class1.getComponentType();
JClassType jClassType = jType.isClassOrInterface();
validArray = jClassType != null && isTypesCompatiblesForSerialization(componentType, JClassUtils.getTypeArgForGenericType(jClassType));
}
return validArray || (List.class.isAssignableFrom(class1)) || (Set.class.isAssignableFrom(class1));
}
else
{
return true;
}
}
}
}
//Use a jsonEncorer implicitly
return true;
}
private boolean isEnumTypesCompatibles(Class<?> class1, JEnumType jType)
{
if (class1.isEnum())
{
Object[] values1 = class1.getEnumConstants();
JEnumConstant[] values2 = jType.getEnumConstants();
if (values1.length != values2.length)
{
return false;
}
for (JEnumConstant jEnumConstant : values2)
{
String name = jEnumConstant.getName();
boolean found = false;
for (Object enumConstant : values1)
{
if (name.equals(enumConstant.toString()))
{
found = true;
break;
}
}
if (!found)
{
return false;
}
}
return true;
}
else if (String.class.isAssignableFrom(class1))
{
return true;
}
return false;
}
private static List<Class<?>> getAllowedType(JType jType)
{
List<Class<?>> result = new ArrayList<Class<?>>();
JPrimitiveType primitiveType = jType.isPrimitive();
if (primitiveType == JPrimitiveType.INT || jType.getQualifiedSourceName().equals(Integer.class.getCanonicalName()))
{
result.add(Integer.TYPE);
result.add(Integer.class);
}
else if (primitiveType == JPrimitiveType.SHORT || jType.getQualifiedSourceName().equals(Short.class.getCanonicalName()))
{
result.add(Short.TYPE);
result.add(Short.class);
}
else if (primitiveType == JPrimitiveType.LONG || jType.getQualifiedSourceName().equals(Long.class.getCanonicalName()))
{
result.add(Long.TYPE);
result.add(Long.class);
}
else if (primitiveType == JPrimitiveType.BYTE || jType.getQualifiedSourceName().equals(Byte.class.getCanonicalName()))
{
result.add(Byte.TYPE);
result.add(Byte.class);
}
else if (primitiveType == JPrimitiveType.FLOAT || jType.getQualifiedSourceName().equals(Float.class.getCanonicalName()))
{
result.add(Float.TYPE);
result.add(Float.class);
}
else if (primitiveType == JPrimitiveType.DOUBLE || jType.getQualifiedSourceName().equals(Double.class.getCanonicalName()))
{
result.add(Double.TYPE);
result.add(Double.class);
}
else if (primitiveType == JPrimitiveType.BOOLEAN || jType.getQualifiedSourceName().equals(Boolean.class.getCanonicalName()))
{
result.add(Boolean.TYPE);
result.add(Boolean.class);
}
else if (primitiveType == JPrimitiveType.CHAR || jType.getQualifiedSourceName().equals(Character.class.getCanonicalName()))
{
result.add(Character.TYPE);
result.add(Character.class);
}
else if (jType.getQualifiedSourceName().equals(String.class.getCanonicalName()))
{
result.add(String.class);
}
else if (jType.getQualifiedSourceName().equals(Date.class.getCanonicalName()))
{
result.add(Date.class);
}
else if (jType.getQualifiedSourceName().equals(java.sql.Date.class.getCanonicalName()))
{
result.add(java.sql.Date.class);
}
else if (jType.getQualifiedSourceName().equals(BigInteger.class.getCanonicalName()))
{
result.add(BigInteger.class);
}
else if (jType.getQualifiedSourceName().equals(BigDecimal.class.getCanonicalName()))
{
result.add(BigDecimal.class);
}
return result;
}
}