package org.webpieces.router.impl.loader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.List; import java.util.concurrent.CompletableFuture; import org.webpieces.ctx.api.RequestContext; import org.webpieces.ctx.api.RouterRequest; import org.webpieces.data.api.DataWrapper; import org.webpieces.router.api.BodyContentBinder; import org.webpieces.router.api.RouterConfig; import org.webpieces.router.api.actions.Action; import org.webpieces.router.api.actions.RenderContent; import org.webpieces.router.api.dto.MethodMeta; import org.webpieces.router.api.exceptions.BadRequestException; import org.webpieces.router.api.exceptions.NotFoundException; import org.webpieces.router.impl.Route; import org.webpieces.router.impl.body.BodyParser; import org.webpieces.router.impl.body.BodyParsers; import org.webpieces.router.impl.ctx.SessionImpl; import org.webpieces.router.impl.params.ParamToObjectTranslatorImpl; import org.webpieces.util.filters.Service; import org.webpieces.util.logging.Logger; import org.webpieces.util.logging.LoggerFactory; public class ServiceProxy implements Service<MethodMeta, Action> { private final static Logger log = LoggerFactory.getLogger(ServiceProxy.class); private ParamToObjectTranslatorImpl translator; private BodyParsers requestBodyParsers; private RouterConfig config; public ServiceProxy(ParamToObjectTranslatorImpl translator, BodyParsers requestBodyParsers, RouterConfig config) { this.translator = translator; this.requestBodyParsers = requestBodyParsers; this.config = config; } @Override public CompletableFuture<Action> invoke(MethodMeta meta) { try { return invokeMethod(meta); } catch(InvocationTargetException e) { Throwable cause = e.getCause(); if(cause instanceof NotFoundException) { return createNotFound((NotFoundException) cause); } return createRuntimeFuture(cause); } catch(NotFoundException e) { return createNotFound(e); } catch(Throwable e) { return createRuntimeFuture(e); } } private CompletableFuture<Action> createRuntimeFuture(Throwable e) { CompletableFuture<Action> future = new CompletableFuture<Action>(); future.completeExceptionally(e); return future; } private CompletableFuture<Action> createNotFound(NotFoundException e) { CompletableFuture<Action> future = new CompletableFuture<Action>(); future.completeExceptionally(e); return future; } @SuppressWarnings("unchecked") private CompletableFuture<Action> invokeMethod(MethodMeta meta) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { RouterRequest req = meta.getCtx().getRequest(); log.info("Incoming content length was specified, but no contentType was(We will not parse the body). req="+req); parseBodyFromContentType(meta.getRoute(), meta.getCtx(), meta.getBodyContentBinder()); Method m = meta.getMethod(); Object obj = meta.getControllerInstance(); //We chose to do this here so any filters ESPECIALLY API filters //can catch and translate api errors and send customers a logical response //On top of that ORM plugins can have a transaction filter and then in this //createArgs can look up the bean before applying values since it is in //the transaction filter List<Object> argsResult = translator.createArgs(m, meta.getCtx(), meta.getBodyContentBinder()); Object retVal = m.invoke(obj, argsResult.toArray()); if(meta.getBodyContentBinder() != null) return unwrapResult(m, retVal, meta.getBodyContentBinder()); if(retVal == null) throw new IllegalStateException("Your controller method returned null which is not allowed. offending method="+m); if(retVal instanceof CompletableFuture) { return (CompletableFuture<Action>) retVal; } else { Action action = (Action) retVal; return CompletableFuture.completedFuture(action); } } private void parseBodyFromContentType(Route route, RequestContext ctx, BodyContentBinder bodyContentBinder) { RouterRequest req = ctx.getRequest(); if(bodyContentBinder != null) return; //A route that defines the content gets to override the content type header so just return if(req.contentLengthHeaderValue == null) return; if(req.contentTypeHeaderValue == null) { log.info("Incoming content length was specified, but no contentType was(We will not parse the body). req="+req); return; } BodyParser parser = requestBodyParsers.lookup(req.contentTypeHeaderValue); if(parser == null) { log.error("Incoming content length was specified but content type was not 'application/x-www-form-urlencoded'(We will not parse body). req="+req); return; } DataWrapper body = req.body; Charset encoding = config.getDefaultFormAcceptEncoding(); parser.parse(body, req, encoding); if(config.isTokenCheckOn() && route.isCheckSecureToken()) { String token = ctx.getSession().get(SessionImpl.SECURE_TOKEN_KEY); List<String> formToken = req.multiPartFields.get(RequestContext.SECURE_TOKEN_FORM_NAME); if(formToken == null) throw new BadRequestException("missing form token(or route added without setting checkToken variable to false)" + "...someone posting form without getting it first(hacker or otherwise) OR " + "you are not using the #{form}# tag or the #{secureToken}# tag to secure your forms"); else if(!token.equals(formToken.get(0))) throw new BadRequestException("bad form token...someone posting form with invalid token(hacker or otherwise)"); } } @SuppressWarnings("unchecked") private CompletableFuture<Action> unwrapResult(Method method, Object retVal, BodyContentBinder binder) { Class<?> returnType = method.getReturnType(); if(CompletableFuture.class.isAssignableFrom(returnType)) { if(retVal == null) throw new IllegalStateException("Your method returned a null CompletableFuture which it not allowed. method="+method); CompletableFuture<Object> future = (CompletableFuture<Object>) retVal; return future.thenApply((bean) -> binder.marshal(bean)); } else { RenderContent content = binder.marshal(retVal); return CompletableFuture.completedFuture(content); } } }