package io.mangoo.routing.handlers;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import com.google.inject.Inject;
import freemarker.template.TemplateException;
import io.mangoo.annotations.FilterWith;
import io.mangoo.core.Application;
import io.mangoo.enums.Binding;
import io.mangoo.enums.Default;
import io.mangoo.enums.Required;
import io.mangoo.exceptions.MangooTemplateEngineException;
import io.mangoo.helpers.RequestHelper;
import io.mangoo.interfaces.MangooRequestFilter;
import io.mangoo.routing.Attachment;
import io.mangoo.routing.Response;
import io.mangoo.routing.bindings.Request;
import io.mangoo.utils.JsonUtils;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
/**
* Main class that handles all controller requests
*
* @author skubiak
*
*/
public class RequestHandler implements HttpHandler {
private Attachment attachment;
private final RequestHelper requestHelper;
@Inject
public RequestHandler(RequestHelper requestHelper) {
this.requestHelper = Objects.requireNonNull(requestHelper, Required.REQUEST_HELPER.toString());
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
this.attachment = exchange.getAttachment(RequestHelper.ATTACHMENT_KEY);
this.attachment.setBody(getRequestBody(exchange));
this.attachment.setRequest(getRequest(exchange));
final Response response = getResponse(exchange);
response.getCookies().forEach(exchange::setResponseCookie);
this.attachment.setResponse(response);
exchange.putAttachment(RequestHelper.ATTACHMENT_KEY, this.attachment);
nextHandler(exchange);
}
/**
* Creates a new request object containing the current request data
*
* @param exchange The Undertow HttpServerExchange
*/
protected Request getRequest(HttpServerExchange exchange) {
final String authenticity = Optional.ofNullable(this.attachment.getRequestParameter()
.get(Default.AUTHENTICITY.toString()))
.orElse(this.attachment.getForm().get(Default.AUTHENTICITY.toString()));
return new Request(exchange)
.withSession(this.attachment.getSession())
.withAuthenticity(authenticity)
.withAuthentication(this.attachment.getAuthentication())
.withParameter(this.attachment.getRequestParameter())
.withBody(this.attachment.getBody());
}
/**
* Execute filters if exists in the following order:
* RequestFilter, ControllerFilter, MethodFilter
*
* @param exchange The Undertow HttpServerExchange
* @return A Response object that will be merged to the final response
*
* @throws NoSuchMethodException
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws TemplateException
* @throws IOException
* @throws MangooTemplateEngineException
*/
protected Response getResponse(HttpServerExchange exchange) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, MangooTemplateEngineException {
//execute global request filter
Response response = Response.withOk();
if (this.attachment.hasRequestFilter()) {
final MangooRequestFilter mangooRequestFilter = Application.getInstance(MangooRequestFilter.class);
response = mangooRequestFilter.execute(this.attachment.getRequest(), response);
}
if (response.isEndResponse()) {
return response;
}
//execute controller filters
response = executeFilter(this.attachment.getClassAnnotations(), response);
if (response.isEndResponse()) {
return response;
}
//execute method filters
response = executeFilter(this.attachment.getMethodAnnotations(), response);
if (response.isEndResponse()) {
return response;
}
return invokeController(exchange, response);
}
/**
* Invokes the controller methods and retrieves the response which
* is later send to the client
*
* @param exchange The Undertow HttpServerExchange
* @return A response object
*
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws IOException
* @throws TemplateException
* @throws MangooTemplateEngineException
*/
protected Response invokeController(HttpServerExchange exchange, Response response) throws IllegalAccessException, InvocationTargetException, MangooTemplateEngineException {
Response invokedResponse;
if (this.attachment.getMethodParameters().isEmpty()) {
invokedResponse = (Response) this.attachment.getMethod().invoke(this.attachment.getControllerInstance());
} else {
final Object [] convertedParameters = getConvertedParameters(exchange);
invokedResponse = (Response) this.attachment.getMethod().invoke(this.attachment.getControllerInstance(), convertedParameters);
}
invokedResponse.andContent(response.getContent());
invokedResponse.andHeaders(response.getHeaders());
if (!invokedResponse.isRendered()) {
invokedResponse.andBody(this.attachment.getTemplateEngine().render(
this.attachment.getFlash(),
this.attachment.getSession(),
this.attachment.getForm(),
this.attachment.getMessages(),
this.attachment.getSubject(),
getTemplatePath(invokedResponse),
invokedResponse.getContent(),
this.attachment.getControllerAndMethod(),
this.attachment.getLocale()));
}
return invokedResponse;
}
/**
* Returns the complete path to the template based on the
* controller and method name
*
* @param response The current response
*
* @return A case-sensitive template path, e.g. /ApplicationController/index.ftl
*/
protected String getTemplatePath(Response response) {
return StringUtils.isBlank(response.getTemplate()) ? (this.attachment.getControllerClassName() + "/" + this.attachment.getTemplateEngine().getTemplateName(this.attachment.getControllerMethodName())) : response.getTemplate();
}
/**
* Creates an array with the request controller method parameter and sets the appropriate values
*
* @param exchange The Undertow HttpServerExchange
* @return an array with the request controller method parameter and sets the appropriate values
*
* @throws IOException
*/
protected Object[] getConvertedParameters(HttpServerExchange exchange) {
final Object [] convertedParameters = new Object[this.attachment.getMethodParametersCount()];
int index = 0;
for (final Map.Entry<String, Class<?>> entry : this.attachment.getMethodParameters().entrySet()) {
final String key = entry.getKey();
final Class<?> clazz = entry.getValue();
final Binding binding = Optional.ofNullable(Binding.fromString(clazz.getName())).orElse(Binding.UNDEFINED);
switch (binding) {
case FORM:
convertedParameters[index] = this.attachment.getForm();
break;
case AUTHENTICATION:
convertedParameters[index] = this.attachment.getAuthentication();
break;
case SESSION:
convertedParameters[index] = this.attachment.getSession();
break;
case FLASH:
convertedParameters[index] = this.attachment.getFlash();
break;
case REQUEST:
convertedParameters[index] = this.attachment.getRequest();
break;
case LOCALDATE:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? null : LocalDate.parse(this.attachment.getRequestParameter().get(key));
break;
case LOCALDATETIME:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? null : LocalDateTime.parse(this.attachment.getRequestParameter().get(key));
break;
case STRING:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? null : this.attachment.getRequestParameter().get(key);
break;
case INT_PRIMITIVE:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? 0 : Integer.parseInt(this.attachment.getRequestParameter().get(key));
break;
case INTEGER:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? null : Integer.valueOf(this.attachment.getRequestParameter().get(key));
break;
case DOUBLE_PRIMITIVE:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? 0 : Double.parseDouble(this.attachment.getRequestParameter().get(key));
break;
case DOUBLE:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? null : Double.valueOf(this.attachment.getRequestParameter().get(key));
break;
case FLOAT_PRIMITIVE:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? 0 : Float.parseFloat(this.attachment.getRequestParameter().get(key));
break;
case FLOAT:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? null : Float.valueOf(this.attachment.getRequestParameter().get(key));
break;
case LONG_PRIMITIVE:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? 0 : Long.parseLong(this.attachment.getRequestParameter().get(key));
break;
case LONG:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? null : Long.valueOf(this.attachment.getRequestParameter().get(key));
break;
case OPTIONAL:
convertedParameters[index] = StringUtils.isBlank(this.attachment.getRequestParameter().get(key)) ? Optional.empty() : Optional.of(this.attachment.getRequestParameter().get(key));
break;
case UNDEFINED:
convertedParameters[index] = this.requestHelper.isJsonRequest(exchange) ? JsonUtils.fromJson(this.attachment.getBody(), clazz) : null;
break;
default:
convertedParameters[index] = null;
break;
}
index++;
}
return convertedParameters;
}
/**
* Executes all filters on controller and method level
*
* @param annotations An array of @FilterWith annotated classes and methods
* @param response
* @return True if the request should continue after filter execution, false otherwise
*
* @throws NoSuchMethodException
* @throws IllegalAccessException
* @throws InvocationTargetException
*/
protected Response executeFilter(List<Annotation> annotations, Response response) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
for (final Annotation annotation : annotations) {
final FilterWith filterWith = (FilterWith) annotation;
for (final Class<?> clazz : filterWith.value()) {
if (response.isEndResponse()) {
return response;
} else {
final Method classMethod = clazz.getMethod(Default.FILTER_METHOD.toString(), Request.class, Response.class);
response = (Response) classMethod.invoke(Application.getInstance(clazz), this.attachment.getRequest(), response);
}
}
}
return response;
}
/**
* Retrieves the complete request body from the request
*
* @param exchange The Undertow HttpServerExchange
* @return A body object containing the request body
*
* @throws IOException
*/
protected String getRequestBody(HttpServerExchange exchange) throws IOException {
String body = "";
if (this.requestHelper.isPostPutPatch(exchange)) {
exchange.startBlocking();
body = IOUtils.toString(exchange.getInputStream(), Default.ENCODING.toString());
}
return body;
}
/**
* Handles the next request in the handler chain
*
* @param exchange The HttpServerExchange
* @throws Exception Thrown when an exception occurs
*/
@SuppressWarnings("all")
protected void nextHandler(HttpServerExchange exchange) throws Exception {
Application.getInstance(OutboundCookiesHandler.class).handleRequest(exchange);
}
}