package org.webpieces.router.impl;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.webpieces.ctx.api.Current;
import org.webpieces.ctx.api.FlashSub;
import org.webpieces.ctx.api.Messages;
import org.webpieces.ctx.api.RequestContext;
import org.webpieces.ctx.api.RouterRequest;
import org.webpieces.router.api.ResponseStreamer;
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.dto.RenderStaticResponse;
import org.webpieces.router.api.dto.RouteType;
import org.webpieces.router.api.exceptions.NotFoundException;
import org.webpieces.router.impl.actions.AjaxRedirectImpl;
import org.webpieces.router.impl.actions.RawRedirect;
import org.webpieces.router.impl.actions.RedirectImpl;
import org.webpieces.router.impl.actions.RenderImpl;
import org.webpieces.router.impl.ctx.RequestLocalCtx;
import org.webpieces.router.impl.ctx.ResponseProcessor;
import org.webpieces.router.impl.model.MatchResult;
import org.webpieces.router.impl.params.ObjectToParamTranslator;
import org.webpieces.util.filters.Service;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import org.webpieces.util.logging.SupressedExceptionLog;
@Singleton
public class RouteInvoker {
private static final Logger log = LoggerFactory.getLogger(RouteInvoker.class);
//initialized in init() method and re-initialized in dev mode from that same method..
private ReverseRoutes reverseRoutes;
private ObjectToParamTranslator reverseTranslator;
private RouterConfig config;
@Inject
public RouteInvoker(
ObjectToParamTranslator reverseTranslator,
RouterConfig config
) {
this.reverseTranslator = reverseTranslator;
this.config = config;
}
public CompletableFuture<Void> invoke(MatchResult result, RequestContext requestCtx, ResponseStreamer responseCb, ErrorRoutes errorRoutes) {
CompletableFuture<Void> future;
try{
future = invoke2(result, requestCtx, responseCb, errorRoutes);
} catch(Throwable e) {
future = new CompletableFuture<Void>();
future.completeExceptionally(e);
}
return future.handle((r, t) -> {
if(t instanceof NotFoundException)
return processNotFound(responseCb, requestCtx, (NotFoundException) t, errorRoutes, null);
else if(t != null) {
CompletableFuture<Void> failFuture = new CompletableFuture<>();
failFuture.completeExceptionally(t);
return failFuture;
}
return CompletableFuture.completedFuture(r);
}).thenCompose(Function.identity());
}
public CompletableFuture<Void> invoke2(MatchResult result, RequestContext requestCtx, ResponseStreamer responseCb, ErrorRoutes errorRoutes) {
//This makes us consistent with other NotFoundExceptions and without the cost of
//throwing an exception and filling in stack trace...
//We could convert the exc. to FastException and override method so stack is not filled in but that
//can get very annoying
RouteMeta meta = result.getMeta();
Route route = meta.getRoute();
RouteType routeType = route.getRouteType();
if(routeType == RouteType.NOT_FOUND) {
CompletableFuture<Void> future = new CompletableFuture<Void>();
future.completeExceptionally(new NotFoundException("route not found"));
return future;
}
return invokeImpl(result, meta.getService222(), requestCtx, responseCb);
}
public CompletableFuture<Void> processNotFound(ResponseStreamer responseCb, RequestContext requestCtx, NotFoundException e, ErrorRoutes errorRoutes, Object meta) {
NotFoundException exc = (NotFoundException) e;
NotFoundInfo notFoundInfo = errorRoutes.fetchNotfoundRoute(exc);
RouteMeta notFoundResult = notFoundInfo.getResult();
RouterRequest overridenRequest = notFoundInfo.getReq();
RequestContext overridenCtx = new RequestContext(requestCtx.getValidation(), (FlashSub) requestCtx.getFlash(), requestCtx.getSession(), overridenRequest);
//http 404...(unless an exception happens in calling this code and that then goes to 500)
return notFound(notFoundResult, notFoundInfo.getService(), exc, overridenCtx, responseCb);
}
public Void processException(ResponseStreamer responseCb, RequestContext requestCtx, Throwable e, ErrorRoutes errorRoutes, Object meta) {
//If this fails, then the users 5xx page is messed up and we then render our own 5xx page
CompletableFuture<Void> future = internalServerError(errorRoutes, e, requestCtx, responseCb, meta);
future.exceptionally(finalExc -> finalFailure(responseCb, finalExc, requestCtx));
return null;
}
public Void finalFailure(ResponseStreamer responseCb, Throwable e, RequestContext requestCtx) {
log.error("This is a final(secondary failure) trying to render the Internal Server Error Route", e);
ResponseProcessor processor = new ResponseProcessor(requestCtx, reverseRoutes, reverseTranslator, null, responseCb);
processor.failureRenderingInternalServerErrorPage(e);
return null;
}
private CompletableFuture<Void> notFound(RouteMeta notFoundResult, Service<MethodMeta, Action> service, NotFoundException exc, RequestContext requestCtx, ResponseStreamer responseCb) {
try {
MatchResult notFoundRes = new MatchResult(notFoundResult);
return invokeImpl(notFoundRes, service, requestCtx, responseCb);
} catch(Throwable e) {
//http 500...
//return a completed future with the exception inside...
CompletableFuture<Void> futExc = new CompletableFuture<Void>();
futExc.completeExceptionally(new RuntimeException("NotFound Route had an exception", e));
return futExc;
}
}
private CompletableFuture<Void> internalServerError(
ErrorRoutes errorRoutes, Throwable exc, RequestContext requestCtx, ResponseStreamer responseCb, Object failedRoute) {
try {
log.error("There is three parts to this error message... request, route found, and the exception "
+ "message. You should\nread the exception message below as well as the RouterRequest and RouteMeta.\n\n"
+requestCtx.getRequest()+"\n\n"+failedRoute+". \n\nNext, server will try to render apps 5xx page\n\n", exc);
SupressedExceptionLog.log(exc);
RouteMeta meta = errorRoutes.fetchInternalServerErrorRoute();
MatchResult res = new MatchResult(meta);
return invokeImpl(res, meta.getService222(), requestCtx, responseCb);
} catch(Throwable e) {
//http 500...
//return a completed future with the exception inside...
CompletableFuture<Void> futExc = new CompletableFuture<Void>();
futExc.completeExceptionally(e);
return futExc;
}
}
public CompletableFuture<Void> invokeImpl(MatchResult result, Service<MethodMeta, Action> service, RequestContext requestCtx, ResponseStreamer responseCb) {
RouteMeta meta = result.getMeta();
ResponseProcessor processor = new ResponseProcessor(requestCtx, reverseRoutes, reverseTranslator, meta, responseCb);
if(meta.getRoute().getRouteType() == RouteType.STATIC) {
StaticRoute route = (StaticRoute) meta.getRoute();
boolean isOnClassPath = route.getIsOnClassPath();
RenderStaticResponse resp = new RenderStaticResponse(route.getTargetCacheLocation(), isOnClassPath);
if(route.isFile()) {
resp.setFilePath(route.getFileSystemPath());
} else {
String relativeUrl = result.getPathParams().get("resource");
resp.setRelativeFile(route.getFileSystemPath(), relativeUrl);
}
return processor.renderStaticResponse(resp);
}
Object obj = meta.getControllerInstance();
if(obj == null)
throw new IllegalStateException("Someone screwed up, as controllerInstance should not be null at this point, bug");
Method method = meta.getMethod();
if(service == null)
throw new IllegalStateException("Bug, service should never be null at this point");
Messages messages = new Messages(meta.getI18nBundleName(), "webpieces");
requestCtx.setMessages(messages);
RequestLocalCtx.set(processor);
Current.setContext(requestCtx);
CompletableFuture<Action> response;
try {
response = invokeMethod(service, obj, method, meta);
} finally {
RequestLocalCtx.set(null);
Current.setContext(null);
}
CompletableFuture<Void> future = response.thenApply(resp -> continueProcessing(processor, resp, responseCb));
return future;
}
public Void continueProcessing(ResponseProcessor processor, Action controllerResponse, ResponseStreamer responseCb) {
if(controllerResponse instanceof RedirectImpl) {
processor.createFullRedirect((RedirectImpl)controllerResponse);
} else if(controllerResponse instanceof AjaxRedirectImpl) {
processor.createAjaxRedirect((AjaxRedirectImpl)controllerResponse);
} else if(controllerResponse instanceof RenderImpl) {
processor.createRenderResponse((RenderImpl)controllerResponse);
} else if(controllerResponse instanceof RawRedirect) {
processor.createRawRedirect((RawRedirect)controllerResponse);
} else if(controllerResponse instanceof RenderContent) {
processor.createContentResponse((RenderContent)controllerResponse);
} else {
throw new UnsupportedOperationException("Bug, a webpieces developer must have missed some code to write");
}
return null;
}
private CompletableFuture<Action> invokeMethod(Service<MethodMeta, Action> service, Object obj, Method m, RouteMeta meta) {
MethodMeta methodMeta = new MethodMeta(obj, m, Current.getContext(), meta.getRoute(), meta.getBodyContentBinder());
return service.invoke(methodMeta);
}
public void init(ReverseRoutes reverseRoutes) {
this.reverseRoutes = reverseRoutes;
}
public String convertToUrl(String routeId, Map<String, String> args, boolean isValidating) {
return reverseRoutes.convertToUrl(routeId, args, isValidating);
}
}