/**
*
*/
package com.trendrr.strest.server;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import com.trendrr.oss.DynMapFactory;
import com.trendrr.oss.Reflection;
import com.trendrr.strest.StrestException;
import com.trendrr.strest.StrestHttpException;
import com.trendrr.strest.StrestUtil;
import com.trendrr.strest.annotations.AnnotationHelper;
import com.trendrr.strest.annotations.Async;
import com.trendrr.strest.server.connections.StrestNettyConnectionChannel;
import com.trendrr.strest.server.routing.UriMapping;
import com.trendrr.strest.server.v2.models.*;
import com.trendrr.strest.server.v2.models.StrestHeader.TxnAccept;
import com.trendrr.strest.server.v2.models.StrestHeader.TxnStatus;
import com.trendrr.strest.server.v2.models.json.StrestJsonResponse;
/**
* Handles creating and routing to the controllers.
* There is one instance of this class per server, so everything
* must be extremely threadsafe.
*
*
* @author Dustin Norlander
* @created Jan 12, 2011
*
*/
public class StrestRouter {
protected Log log = LogFactory.getLog(StrestRouter.class);
protected RouteLookup routeLookup = new RouteLookup();
// protected ConcurrentHashMap<Channel, StrestNettyConnectionChannel> connections = new ConcurrentHashMap<Channel, StrestNettyConnectionChannel>();
protected HashMap<String, List<StrestControllerFilter>> defaultFilters = new HashMap<String,List<StrestControllerFilter>> ();
protected ConcurrentHashMap<Class, StrestControllerFilter> filtersByClass = new ConcurrentHashMap<Class,StrestControllerFilter>();
//the server that this router belongs to .
protected StrestServer server = null;
public StrestServer getServer() {
return server;
}
public void setServer(StrestServer server) {
this.server = server;
}
/**
* will search this package (and all subpackages) for controllers.
* will also work for fully qualified classnames
*
*
* @param packageName
*/
public void addControllerPackage(String packageName) {
List<StrestController> controllers = new ArrayList<StrestController>();
if (!packageName.toLowerCase().equals(packageName)) {
//see if this is a classname
StrestController controller;
try {
controller = Reflection.defaultInstance(StrestController.class, packageName);
if (controller != null)
controllers.add(controller);
} catch (Exception e) {
//do nothing.
}
}
List<StrestController> ctrls = Reflection.instances(StrestController.class, packageName, true);
if (ctrls != null) {
controllers.addAll(ctrls);
}
if (controllers.isEmpty()) {
log.warn("No controllers found in package: " + packageName);
return;
}
for (StrestController c : controllers) {
String[] routes = c.routes();
if (routes == null) {
this.log.warn("Controller: " + c.getClass().getCanonicalName() + " has no routes. Skipping");
continue;
}
for (String route : c.routes()) {
this.addRoute(route, c.getClass());
}
}
}
/**
* gets all the filters for this controller. Will return an empty list, never null.
* @param controller
* @return
*/
protected List<StrestControllerFilter> getFilters(StrestController controller) {
//TODO: implement a local cache of this? or overkill?
List<StrestControllerFilter> retList = new ArrayList<StrestControllerFilter>();
if (controller == null)
return retList;
Class[] filters = controller.filters();
if (filters != null) {
for (Class f : filters) {
StrestControllerFilter filter = this.getFilter(f);
retList.add(filter);
}
}
retList.addAll(this.getNamespaceFilters(controller.getControllerNamespace()));
return retList;
}
/**
* sets the default filters that will be run on all controllers.
*
* List should be full class names of the filters.
*
* TODO: this is not currently threadsafe. it is meant to be executed before starting up, but even so, should be safe.
*
* @param defaultFilters
*/
public void setFilters(String namespace, List<String> defaultFilters) {
if (namespace == null) {
namespace = "default";
}
List<StrestControllerFilter> filters = new ArrayList<StrestControllerFilter> ();
for (String d : defaultFilters) {
try {
StrestControllerFilter f = Reflection.defaultInstance(StrestControllerFilter.class, d);
filters.add(f);
} catch (Exception x) {
log.warn("Unable to load filter: " + d, x);
}
}
this.defaultFilters.put(namespace, filters);
}
/**
* returns the list of filters for the given namespace or empty list
* @param namespace
* @return
*/
protected List<StrestControllerFilter> getNamespaceFilters(String namespace) {
List<StrestControllerFilter> filters = this.defaultFilters.get(namespace);
if (filters == null)
filters = new ArrayList<StrestControllerFilter>();
return filters;
}
public void addRoute(String route, Class cls) {
this.getRouteLookup().addRoute(route, cls);
}
/**
* gets the # of connections.
*
* @return
*/
public int getNumConnections() {
//TODO: this is a hacky
//need to support other connection types
return StrestNettyConnectionChannel.size();
}
public RouteLookup getRouteLookup() {
return routeLookup;
}
public void setRouteLookup(RouteLookup lookup) {
this.routeLookup = lookup;
}
/**
* removes any state associated with this channel.
* does not do anything to the channel itself.
* @param channel
*/
// public void removeChannel(Channel channel) {
// StrestNettyConnectionChannel con = this.connections.remove(channel);
// if (con == null)
// return;
// con.cleanup();
// }
private StrestControllerFilter getFilter(Class cls) {
StrestControllerFilter f = this.filtersByClass.get(cls);
if (f != null)
return f;
try {
f = (StrestControllerFilter)Reflection.defaultInstance(cls);
this.filtersByClass.putIfAbsent(cls, f);
} catch (Exception x) {
log.warn("Unable to load filter: " + cls, x);
}
return f;
}
private boolean isAsync(StrestController controller, StrestHeader.Method method) {
return AnnotationHelper.hasMethodAnnotation(Async.class, controller, "handle" + method.toString());
}
public void incoming(StrestRequest request) {
boolean isStrest = StrestUtil.isStrest(request);
// Build the response object.
//throw an illegal exception here?
//TODO:throws a problem if the packet is invalid..
try {
StrestUtil.validateRequest(request);
} catch (StrestException x) {
//TODO: send exception response..
log.error("caught", x);
try {
//SEND ERROR REsponse
StrestJsonResponse error = new StrestJsonResponse();
error.setStatus(StrestHttpException.BAD_REQUEST().getCode(), x.getMessage());
request.getConnectionChannel().sendMessage(error);
} catch (Exception x2) {
log.error("caught", x2);
}
if (request.getConnectionChannel() != null) {
request.getConnectionChannel().cleanup();
}
return;
}
ResponseBuilder response = new ResponseBuilder(request);
// this.connections.putIfAbsent(channel, new StrestNettyConnectionChannel(channel));
// StrestNettyConnectionChannel con = this.connections.get(channel);
// request.setConnectionChannel(con);
String txnId = request.getTxnId();
request.getConnectionChannel().incoming(request);
StrestController controller = null;
try {
try {
controller = this.getRouteLookup().find(request.getUri());
if (controller == null) {
throw StrestHttpException.NOT_FOUND();
}
controller.setRouter(this);
controller.setStrest(isStrest);
if (isStrest) {
controller.setStrestTxnId(txnId);
}
//parse the get string params
//TODO: this should be handled by the request objects
controller.setParamsGET(DynMapFactory.instanceFromURL(request.getUri()));
controller.getParams().putAll(request.getParams());
//parse any post params
//TODO: don't think this is really correct.
String contentType = request.getHeader(CONTENT_TYPE);
if(contentType != null){
String pms = request.getContent().toString();
if(pms != null){
if (contentType.contains("form-urlencoded")) {
controller.setParamsPOST(DynMapFactory.instanceFromURLEncoded(pms));
}else if (contentType.contains("json")){
controller.setParamsPOST(DynMapFactory.instanceFromJSON(pms));
}
}
}
controller.getParams().putAll(controller.getParamsGET());
controller.getParams().putAll(controller.getParamsPOST());
controller.setRequest(request);
controller.setResponse(response.getResponse());
//before filters
for (StrestControllerFilter f : this.getFilters(controller)) {
f.before(controller);
}
//now execution the appropriate action.
if (!controller.isSkipExecution()) {
if (request.getMethod() == StrestHeader.Method.GET) {
controller.handleGET(controller.getParams());
} else if (request.getMethod() == StrestHeader.Method.POST) {
controller.handlePOST(controller.getParams());
} else if (request.getMethod() == StrestHeader.Method.PUT) {
controller.handlePUT(controller.getParams());
} else if (request.getMethod() == StrestHeader.Method.DELETE) {
controller.handleDELETE(controller.getParams());
} else {
throw StrestHttpException.METHOD_NOT_ALLOWED();
}
if (this.isAsync(controller, request.getMethod())) {
//user is responsable to complete the request.
// return;
}
}
} catch (StrestHttpException e) {
throw e;
} catch (Exception x) {
StrestHttpException e = StrestHttpException.INTERNAL_SERVER_ERROR();
e.setCause(x);
log.error("Caught", x);
throw e;
}
} catch (StrestHttpException e) {
response.status(e.getCode(), e.getMessage());
response.txnStatus(TxnStatus.COMPLETED);
//run the error filters
if (controller != null) {
for (StrestControllerFilter f : this.getFilters(controller)) {
f.error(controller, response.getResponse(), e);
}
}
try {
this.sendResponse(request, response);
} catch (Exception e1) {
log.error("Caught", e);
}
return;
}
this.finishResponse(controller, response);
}
/**
* actually does the send, and handles the txn.
* @param controller
* @param response
*/
protected void sendResponse(StrestRequest request, ResponseBuilder response) throws Exception{
StrestHeader.TxnStatus txnStatus = response.getResponse().getTxnStatus();
if (txnStatus == null) {
txnStatus = StrestHeader.TxnStatus.COMPLETED;
}
//client only accepts single transactions.
if (request.getTxnAccept() == TxnAccept.SINGLE) {
txnStatus = StrestHeader.TxnStatus.COMPLETED;
}
//now set the status
response.txnStatus(txnStatus);
// Write the response.
// System.out.println(response.getResponse());
// System.out.println("*****");
// System.out.println(response.getResponse().getContent().toString());
Object future = request.getConnectionChannel().sendMessage(response);
// Close the non-keep-alive connection after the write operation is done.
if (!StrestUtil.isStrest(request) && future instanceof ChannelFuture) {
// log.info("CLOSING NON STREST CONNECTION");
((ChannelFuture)future).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
((ChannelFuture)future).addListener(ChannelFutureListener.CLOSE);
}
}
/**
* Returns all the controllers registered controllers.
*
* This copies and instantiates all registered controllers into a new list
* so if there are many this could be a heavy operation.
*
*/
public List<StrestController> getAllControllers() {
return this.routeLookup.getAllControllers();
}
/**
* completes a response.
* @param controller
* @param response
*/
public void finishResponse(StrestController controller, ResponseBuilder response) {
try {
try {
//execute final filters
for (StrestControllerFilter f : this.getFilters(controller)) {
f.after(controller);
}
response.setResponse(controller.getResponse());
if (!controller.isSendResponse()) {
return;
}
} catch (StrestHttpException e) {
throw e;
} catch (Exception x) {
StrestHttpException e = StrestHttpException.INTERNAL_SERVER_ERROR();
e.setCause(x);
log.error("Caught", x);
throw e;
}
} catch (StrestHttpException e) {
response.status(e.getCode(), e.getMessage());
response.getResponse().setTxnStatus(TxnStatus.COMPLETED);
//run the error filters
if (controller != null) {
for (StrestControllerFilter f : this.getFilters(controller)) {
f.error(controller, response.getResponse(), e);
}
}
}
try {
this.sendResponse(controller.getRequest(), response);
} catch (Exception e) {
log.error("Caught", e);
}
}
}