package com.elibom.jogger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import com.elibom.jogger.exception.NotFoundException;
import com.elibom.jogger.http.Request;
import com.elibom.jogger.http.Response;
import com.elibom.jogger.http.servlet.ServletRequest;
import com.elibom.jogger.http.servlet.ServletResponse;
import com.elibom.jogger.template.FreemarkerTemplateEngine;
import com.elibom.jogger.template.TemplateEngine;
import com.elibom.jogger.util.Preconditions;
/**
* A server that handles HTTP requests using the provided middleware list.
*
* @author German Escobar
*/
public class Jogger {
private static final int DEFAULT_PORT = 5000;
/**
* The Jetty server instance.
*/
private Server server;
/**
* The port in which the server will respond.
*/
private int port = DEFAULT_PORT;
/**
* The factory used to create the middleware list.
*/
private MiddlewaresFactory middlewareFactory;
/**
* The cached version of the middlewares
*/
private Middleware[] middlewares;
private TemplateEngine templateEngine = new FreemarkerTemplateEngine();
private ExceptionHandler exceptionHandler = new DefaultExceptionHandler();
/**
* Constructor. Initializes a new instance without middlewares.
*/
public Jogger() {
this(new MiddlewaresFactory() {
@Override
public Middleware[] create() {
return new Middleware[0];
}
});
}
/**
* Constructor. Initializes a new instance with the supplied middleware list. The order is important because they will be
* called in that same order.
*
* @param middlewares an array of middlewares that will be executed on each request.
*/
public Jogger(final Middleware...middlewares) {
Preconditions.notNull(middlewares, "no middlewares provided.");
this.middlewareFactory = new MiddlewaresFactory() {
@Override
public Middleware[] create() {
return middlewares;
}
};
this.middlewares = middlewares;
}
/**
* Constructor. Initializes a new instance with the supplied {@link MiddlewaresFactory}.
*
* @param middlewareFactory the factory from which we are going to retrieve the array of middlewares.
*/
public Jogger(final MiddlewaresFactory middlewareFactory) {
Preconditions.notNull(middlewareFactory, "no middlewareFactory provided.");
this.middlewareFactory = middlewareFactory;
try {
this.middlewares = middlewareFactory.create();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Handles an HTTP request by delgating the call to the middlewares.
*
* @param request the Jogger HTTP request.
* @param response the Jogger HTTP response.
* @throws Exception
*/
public void handle(Request request, Response response) throws Exception {
if (Environment.isDevelopment()) {
this.middlewares = this.middlewareFactory.create();
}
try {
handle(request, response, new ArrayList<Middleware>(Arrays.asList(middlewares)));
} catch (Exception e) {
if (exceptionHandler != null) {
exceptionHandler.handle(e, request, response);
} else {
throw e;
}
}
}
/**
* Helper method. Creates a {@link MiddlewareChain} implementation to recursively call the middlewares.
*
* @param request
* @param response
* @param middlewares
* @throws Exception
*/
private void handle(final Request request, final Response response, final List<Middleware> middlewares) throws Exception {
if (middlewares.isEmpty()) {
throw new NotFoundException();
}
Middleware current = middlewares.remove(0);
current.handle(request, response, new MiddlewareChain() {
@Override
public void next() throws Exception {
// recursive call
handle(request, response, middlewares);
}
});
}
/**
* Starts the HTTP server listening in the configured <code>port</code> attribute.
*
* @return itself for method chaining.
*/
public Jogger listen() {
return listen(port);
}
/**
* Starts the HTTP server listening in the specified <code>port</code>
*
* @param port the port in which the HTTP server will listen.
*
* @return itself for method chaining.
*/
public Jogger listen(int port) {
this.port = port;
// configure the Jetty server
server = new Server(port);
server.setHandler(new JoggerHandler());
// start the Jetty server
try {
server.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
return this;
}
/**
* Joins to the server thread preventing the program to finish.
*
* @return itself for method chaining.
* @throws InterruptedException if the thread is interrupted.
*/
public Jogger join() throws InterruptedException {
server.join();
return this;
}
/**
* Stops the HTTP server.
*
* @return itself for method chaining.
*/
public Jogger stop() {
try {
server.stop();
} catch (Exception e) {
throw new RuntimeException(e);
}
return this;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public Middleware[] getMiddlewareList() {
return middlewares;
}
public TemplateEngine getTemplateEngine() {
return templateEngine;
}
public void setTemplateEngine(TemplateEngine templateEngine) {
Preconditions.notNull(templateEngine, "no templateEngine provided");
this.templateEngine = templateEngine;
}
public void setExceptionHandler(ExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
/**
* The Jetty handler that will handle HTTP requests.
*
* @author German Escobar
*/
private class JoggerHandler extends AbstractHandler {
@Override
public void handle(String target, org.eclipse.jetty.server.Request baseRequest, HttpServletRequest servletRequest,
HttpServletResponse servletResponse) throws IOException, ServletException {
try {
// wrap Jetty's request and response in Jogger objects
Request request = new ServletRequest(servletRequest);
Response response = new ServletResponse(servletResponse, templateEngine);
Jogger.this.handle(request, response);
} catch (Exception e) {
throw new ServletException(e);
} finally {
baseRequest.setHandled(true);
}
}
}
}