package org.webpieces.router.impl.ctx; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import org.webpieces.ctx.api.Current; import org.webpieces.ctx.api.HttpMethod; import org.webpieces.ctx.api.RequestContext; import org.webpieces.ctx.api.RouterRequest; import org.webpieces.router.api.ResponseStreamer; import org.webpieces.router.api.actions.RenderContent; import org.webpieces.router.api.dto.RedirectResponse; import org.webpieces.router.api.dto.RenderContentResponse; import org.webpieces.router.api.dto.RenderResponse; import org.webpieces.router.api.dto.RenderStaticResponse; import org.webpieces.router.api.dto.RouteType; import org.webpieces.router.api.dto.View; import org.webpieces.router.api.exceptions.IllegalReturnValueException; import org.webpieces.router.api.routing.RouteId; import org.webpieces.router.impl.ReverseRoutes; import org.webpieces.router.impl.Route; import org.webpieces.router.impl.RouteMeta; 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.params.ObjectToParamTranslator; public class ResponseProcessor { private ReverseRoutes reverseRoutes; private RouteMeta matchedMeta; private ObjectToParamTranslator reverseTranslator; private RequestContext ctx; private ResponseStreamer responseCb; private boolean responseSent = false; public ResponseProcessor(RequestContext ctx, ReverseRoutes reverseRoutes, ObjectToParamTranslator reverseTranslator, RouteMeta meta, ResponseStreamer responseCb) { this.ctx = ctx; this.reverseRoutes = reverseRoutes; this.reverseTranslator = reverseTranslator; this.matchedMeta = meta; this.responseCb = responseCb; } public void createRawRedirect(RawRedirect controllerResponse) { String url = controllerResponse.getUrl(); if(url.startsWith("http")) { wrapFunctionInContext(s -> responseCb.sendRedirect(new RedirectResponse(url))); return; } RouterRequest request = ctx.getRequest(); RedirectResponse redirectResponse = new RedirectResponse(false, request.isHttps, request.domain, request.port, url); wrapFunctionInContext(s -> responseCb.sendRedirect(redirectResponse)); } public void createAjaxRedirect(AjaxRedirectImpl action) { RouteId id = action.getId(); Map<String, Object> args = action.getArgs(); createRedirect(id, args, true); } public void createFullRedirect(RedirectImpl action) { RouteId id = action.getId(); Map<String, Object> args = action.getArgs(); createRedirect(id, args, false); } private void createRedirect(RouteId id, Map<String, Object> args, boolean isAjaxRedirect) { if(responseSent) throw new IllegalStateException("You already sent a response. do not call Actions.redirect or Actions.render more than once"); responseSent = true; RouterRequest request = ctx.getRequest(); Method method = matchedMeta.getMethod(); RouteMeta nextRequestMeta = reverseRoutes.get(id); if(nextRequestMeta == null) throw new IllegalReturnValueException("Route="+id+" returned from method='"+method+"' was not added in the RouterModules"); else if(!nextRequestMeta.getRoute().matchesMethod(HttpMethod.GET)) throw new IllegalReturnValueException("method='"+method+"' is trying to redirect to routeid="+id+" but that route is not a GET method route and must be"); Route route = nextRequestMeta.getRoute(); Map<String, String> keysToValues = reverseTranslator.formMap(method, route.getPathParamNames(), args); Set<String> keySet = keysToValues.keySet(); List<String> argNames = route.getPathParamNames(); if(keySet.size() != argNames.size()) { throw new IllegalReturnValueException("Method='"+method+"' returns a Redirect action with wrong number of arguments. args="+keySet.size()+" when it should be size="+argNames.size()); } String path = route.getFullPath(); for(String name : argNames) { String value = keysToValues.get(name); if(value == null) throw new IllegalArgumentException("Method='"+method+"' returns a Redirect that is missing argument key="+name+" to form the url on the redirect"); path = path.replace("{"+name+"}", value); } RedirectResponse redirectResponse = new RedirectResponse(isAjaxRedirect, request.isHttps, request.domain, request.port, path); wrapFunctionInContext(s -> responseCb.sendRedirect(redirectResponse)); } public void createRenderResponse(RenderImpl controllerResponse) { if(responseSent) throw new IllegalStateException("You already sent a response. do not call Actions.redirect or Actions.render more than once"); responseSent = true; RouterRequest request = ctx.getRequest(); Method method = matchedMeta.getMethod(); //in the case where the POST route was found, the controller canNOT be returning RenderHtml and should follow PRG //If the POST route was not found, just render the notFound page that controller sends us violating the //PRG pattern in this one specific case for now (until we test it with the browser to make sure back button is //not broken) if(matchedMeta.getRoute().getRouteType() == RouteType.HTML && HttpMethod.POST == request.method) { throw new IllegalReturnValueException("Controller method='"+method+"' MUST follow the PRG " + "pattern(https://en.wikipedia.org/wiki/Post/Redirect/Get) so " + "users don't have a poor experience using your website with the browser back button. " + "This means on a POST request, you cannot return RenderHtml object and must return Redirects"); } String controllerName = matchedMeta.getControllerInstance().getClass().getName(); String methodName = matchedMeta.getMethod().getName(); String relativeOrAbsolutePath = controllerResponse.getRelativeOrAbsolutePath(); if(relativeOrAbsolutePath == null) { relativeOrAbsolutePath = methodName+".html"; } Map<String, Object> pageArgs = controllerResponse.getPageArgs(); // Add context as a page arg: pageArgs.put("_context", ctx); pageArgs.put("_session", ctx.getSession()); pageArgs.put("_flash", ctx.getFlash()); View view = new View(controllerName, methodName, relativeOrAbsolutePath); RenderResponse resp = new RenderResponse(view, pageArgs, matchedMeta.getRoute().getRouteType()); wrapFunctionInContext(s -> responseCb.sendRenderHtml(resp)); } private void wrapFunctionInContext(Consumer<Void> function) { boolean wasSet = Current.isContextSet(); if(!wasSet) Current.setContext(ctx); //Allow html tags to use the contexts try { function.accept(null); } finally { if(!wasSet) //then reset Current.setContext(null); } } public void failureRenderingInternalServerErrorPage(Throwable e) { wrapFunctionInContext(s -> responseCb.failureRenderingInternalServerErrorPage(e)); } public CompletableFuture<Void> renderStaticResponse(RenderStaticResponse renderStatic) { boolean wasSet = Current.isContextSet(); if(!wasSet) Current.setContext(ctx); //Allow html tags to use the contexts try { return responseCb.sendRenderStatic(renderStatic); } finally { if(!wasSet) //then reset Current.setContext(null); } } public void createContentResponse(RenderContent r) { if(responseSent) throw new IllegalStateException("You already sent a response. do not call Actions.redirect or Actions.render more than once"); RenderContentResponse resp = new RenderContentResponse(r.getContent(), r.getStatusCode(), r.getMimeType()); wrapFunctionInContext(s -> responseCb.sendRenterContent(resp)); } }