/*
* 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.server.rest.core.dispatch;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.codehaus.jackson.map.ObjectWriter;
import org.cruxframework.crux.core.server.rest.annotation.RestService.CorsSupport;
import org.cruxframework.crux.core.server.rest.annotation.RestService.JsonPSupport;
import org.cruxframework.crux.core.server.rest.core.EntityTag;
import org.cruxframework.crux.core.server.rest.core.HttpRequestAware;
import org.cruxframework.crux.core.server.rest.core.HttpResponseAware;
import org.cruxframework.crux.core.server.rest.core.registry.RestServiceFactoryInitializer;
import org.cruxframework.crux.core.server.rest.spi.HttpRequest;
import org.cruxframework.crux.core.server.rest.spi.HttpResponse;
import org.cruxframework.crux.core.server.rest.spi.HttpServletResponseHeaders;
import org.cruxframework.crux.core.server.rest.spi.InternalServerErrorException;
import org.cruxframework.crux.core.server.rest.spi.RestFailure;
import org.cruxframework.crux.core.server.rest.state.ResourceStateConfig;
import org.cruxframework.crux.core.server.rest.util.HttpHeaderNames;
import org.cruxframework.crux.core.server.rest.util.HttpMethodHelper;
import org.cruxframework.crux.core.server.rest.util.JsonUtil;
import org.cruxframework.crux.core.utils.ClassUtils;
import org.cruxframework.crux.core.utils.EncryptUtils;
/**
*
* @author Thiago da Rosa de Bustamante
*
*/
public class ResourceMethod
{
private static final Lock lock = new ReentrantLock();
private static final Lock exceptionlock = new ReentrantLock();
protected String httpMethod;
protected Method method;
protected Class<?> resourceClass;
protected Type genericReturnType;
protected MethodInvoker methodInvoker;
protected ObjectWriter writer;
protected Map<String, ObjectWriter> exceptionWriters = new HashMap<String, ObjectWriter>();
protected Map<String, String> exceptionIds = new HashMap<String, String>();
protected CacheInfo cacheInfo;
protected boolean hasReturnType;
protected JsonPData jsonPData;
protected CorsData corsData;
private boolean etagGenerationEnabled = false;
private boolean isRequestAware;
private boolean isResponseAware;;
public ResourceMethod(Class<?> clazz, Method method, String httpMethod)
{
this.httpMethod = httpMethod;
this.resourceClass = clazz;
this.isRequestAware = HttpRequestAware.class.isAssignableFrom(resourceClass);
this.isResponseAware = HttpResponseAware.class.isAssignableFrom(resourceClass);
this.method = method;
this.genericReturnType = ClassUtils.getGenericReturnTypeOfGenericInterfaceMethod(clazz, method);
this.hasReturnType = genericReturnType != null && !genericReturnType.equals(Void.class) && !genericReturnType.equals(Void.TYPE);
if (!hasReturnType && httpMethod.equals("GET"))
{
throw new InternalServerErrorException("Invalid rest method: " + method.toString() + ". @GET methods " +
"can not be void.", "Can not execute requested service");
}
this.methodInvoker = new MethodInvoker(resourceClass, method, httpMethod);
this.cacheInfo = HttpMethodHelper.getCacheInfoForGET(method);
CorsSupport corsSupport = method.getAnnotation(CorsSupport.class);
if (corsSupport == null)
{
corsSupport = resourceClass.getAnnotation(CorsSupport.class);
}
this.corsData = CorsData.parseCorsData(corsSupport);
JsonPSupport jsonPSupport = method.getAnnotation(JsonPSupport.class);
if (jsonPSupport == null)
{
jsonPSupport = resourceClass.getAnnotation(JsonPSupport.class);
}
this.jsonPData = JsonPData.parseJsonPData(jsonPSupport);
}
public boolean supportsCors()
{
return corsData != null;
}
public boolean supportsJsonP()
{
return jsonPData != null;
}
public void setCorsAllowedMethods(List<String> methods)
{
if (supportsCors())
{
for (String method : methods)
{
corsData.addAllowMethod(method);
}
}
}
public Type getGenericReturnType()
{
return genericReturnType;
}
public Class<?> getResourceClass()
{
return resourceClass;
}
public Annotation[] getMethodAnnotations()
{
return method.getAnnotations();
}
public Method getMethod()
{
return method;
}
public void forceEtagGeneration()
{
etagGenerationEnabled = true;
}
public boolean isEtagGenerationEnabled()
{
return etagGenerationEnabled || (cacheInfo != null && cacheInfo.isCacheEnabled());
}
public MethodReturn invoke(HttpRequest request, HttpResponse response)
{
try
{
if (ResourceStateConfig.isResourceStateCacheEnabled())
{
StateHandler stateHandler = new StateHandler(this, request, response);
MethodReturn ret = stateHandler.handledByCache();
return ret;
}
else
{
Object target = createTarget(request, response);
return invoke(request, response, target);
}
}
catch (RestFailure e)
{
throw e;
}
catch (Exception e)
{
throw new InternalServerErrorException("Error invoking rest service endpoint", "Error processing requested service", e);
}
}
public String getHttpMethod()
{
return httpMethod;
}
public boolean checkCorsPermissions(HttpRequest request, HttpResponse response, boolean preflightRequest)
{
boolean allowed = true;
if (supportsCors())
{
String origin = request.getHttpHeaders().getHeaderString(HttpHeaderNames.ORIGIN);
if (corsData.isOriginAllowed(origin))
{
HttpServletResponseHeaders outputHeaders = response.getOutputHeaders();
outputHeaders.putSingle(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, corsData.isAllOriginsAllowed()?"*":origin);
outputHeaders.add(HttpHeaderNames.VARY, HttpHeaderNames.ORIGIN);// Needed to make proxy caches works
if (corsData.isAllowCredentials())
{
outputHeaders.putSingle(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}
Iterator<String> exposeHeaders = corsData.getExposeHeaders();
while (exposeHeaders.hasNext())
{
outputHeaders.add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, exposeHeaders.next());
}
if (preflightRequest)
{
Iterator<String> allowMethods = corsData.getAllowMethods();
while (allowMethods.hasNext())
{
outputHeaders.add(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, allowMethods.next());
}
if (corsData.getMaxAge() >= 0)
{
outputHeaders.putSingle(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, corsData.getMaxAge());
}
if (!corsData.isAllowMethod(request.getHttpMethod()))
{
allowed = false;
}
}
}
else
{
allowed = (origin == null); // Same origin requests can send no origin header.
}
}
return allowed;
}
protected MethodReturn doInvoke(HttpRequest request, HttpResponse response) throws InstantiationException, IllegalAccessException
{
MethodReturn ret;
Object target = createTarget(request, response);
ret = invoke(request, response, target);
return ret;
}
private Object createTarget(HttpRequest request, HttpResponse response) throws InstantiationException, IllegalAccessException
{
Object target = RestServiceFactoryInitializer.getServiceFactory().getService(resourceClass);
if (isRequestAware)
{
((HttpRequestAware)target).setRequest(request);
}
if (isResponseAware)
{
((HttpResponseAware)target).setResponse(response);
}
return target;
}
private MethodReturn invoke(HttpRequest request, HttpResponse response, Object target)
{
Object rtn = methodInvoker.invoke(request, response, target);
String retVal = null;
String exeptionData = null;
try
{
if (rtn != null && rtn instanceof Exception)
{
exeptionData = getReturnedValue(request, getExceptionData((Exception) rtn));
}
else if (hasReturnType && rtn != null)
{
retVal = getReturnedValue(request, getReturnWriter().writeValueAsString(rtn));
}
}
catch (Exception e)
{
throw new InternalServerErrorException("Error serializing rest service return", "Error processing requested service", e);
}
return new MethodReturn(hasReturnType, retVal, exeptionData, cacheInfo, null, isEtagGenerationEnabled());
}
private String getReturnedValue(HttpRequest request, String value)
{
if (supportsJsonP())
{
String callbackParam = request.getUri().getQueryParameters().getFirst(jsonPData.getCallbackParameter());
if (callbackParam != null && callbackParam.length() > 0)
{
value = callbackParam+"("+value+");";
}
}
return value;
}
private String getExceptionData(Exception e) throws IOException
{
return "{\"exId\": \"" + getExceptionId(e) + "\", \"exData\": " + getExceptionWriter(e).writeValueAsString(e) + "}";
}
private String getExceptionId(Exception e)
{
Class<?> clazz = e.getClass();
String className = clazz.getCanonicalName();
String exceptionId = exceptionIds.get(className);
if (exceptionId == null)
{
initializeExceptionObjects(clazz, className);
}
return exceptionIds.get(className);
}
private ObjectWriter getExceptionWriter(Exception e)
{
Class<?> clazz = e.getClass();
String className = clazz.getCanonicalName();
ObjectWriter objectWriter = exceptionWriters.get(className);
if (objectWriter == null)
{
initializeExceptionObjects(clazz, className);
}
return exceptionWriters.get(className);
}
private void initializeExceptionObjects(Class<?> clazz, String className)
{
exceptionlock.lock();
try
{
ObjectWriter objectWriter = exceptionWriters.get(className);
if (objectWriter == null)
{
Class<?> jsonSubTypesSuperClass = JsonUtil.getJsonSubTypesSuperClass(clazz, clazz);
if(jsonSubTypesSuperClass == null)
{
objectWriter = JsonUtil.createWriter(clazz);
exceptionWriters.put(className, objectWriter);
exceptionIds.put(className, hash(className));
}
else
{
objectWriter = JsonUtil.createWriter(jsonSubTypesSuperClass);
exceptionWriters.put(className, objectWriter);
exceptionIds.put(className, hash(jsonSubTypesSuperClass.getCanonicalName()));
}
}
}
finally
{
exceptionlock.unlock();
}
}
private String hash(String s)
{
try
{
return EncryptUtils.hash(s);
}
catch (NoSuchAlgorithmException ns)
{
throw new InternalServerErrorException("Error generating MD5 hash for String["+s+"]", "Error processing requested service", ns);
}
}
private ObjectWriter getReturnWriter()
{
if (writer == null)
{
lock.lock();
try
{
if (writer == null)
{
Type returnType = ClassUtils.resolveGenericTypeOnMethod(genericReturnType, resourceClass, method);
writer = JsonUtil.createWriter(returnType);
}
}
finally
{
lock.unlock();
}
}
return writer;
}
public static class MethodReturn
{
protected final boolean hasReturnType;
protected final String ret;
private final CacheInfo cacheInfo;
private final ConditionalResponse conditionalResponse;
protected EntityTag etag;
protected long dateModified;
protected final boolean etagGenerationEnabled;
protected String checkedExceptionData;
protected MethodReturn(boolean hasReturnType, String ret, String exceptionData, CacheInfo cacheInfo, ConditionalResponse conditionalResponse,
boolean etagGenerationEnabled)
{
this.hasReturnType = hasReturnType;
this.ret = ret;
this.checkedExceptionData = exceptionData;
this.cacheInfo = cacheInfo;
this.conditionalResponse = conditionalResponse;
this.etagGenerationEnabled = etagGenerationEnabled;
}
public boolean hasReturnType()
{
return hasReturnType;
}
public String getReturn()
{
return ret;
}
public CacheInfo getCacheInfo()
{
return cacheInfo;
}
public ConditionalResponse getConditionalResponse()
{
return conditionalResponse;
}
public EntityTag getEtag()
{
return etag;
}
public void setEtag(EntityTag etag)
{
this.etag = etag;
}
public long getDateModified()
{
return dateModified;
}
public void setDateModified(long dateModified)
{
this.dateModified = dateModified;
}
public boolean isEtagGenerationEnabled()
{
return etagGenerationEnabled;
}
public String getCheckedExceptionData()
{
return checkedExceptionData;
}
public void setCheckedExceptionData(String checkedExceptionData)
{
this.checkedExceptionData = checkedExceptionData;
}
}
}