/** * Helios, OpenSource Monitoring * Brought to you by the Helios Development Group * * Copyright 2007, Helios Development Group and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package org.helios.apmrouter.server.unification.pipeline.http; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.helios.apmrouter.server.ServerComponentBean; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelEvent; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelUpstreamHandler; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.frame.TooLongFrameException; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; import org.jboss.netty.util.CharsetUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEvent; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.jmx.export.annotation.ManagedAttribute; /** * <p>Title: HttpRequestRouter</p> * <p>Description: Service to route incoming {@link HttpRequest}s to a {@link HttpRequestHandler} by the request URI/</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.apmrouter.server.unification.pipeline.http.HttpRequestRouter</code></p> */ public class HttpRequestRouter extends ServerComponentBean implements ChannelUpstreamHandler { /** A map of {@link HttpRequestHandler}s keyed by their bean name */ protected Map<String, HttpRequestHandler> handlers = new ConcurrentHashMap<String, HttpRequestHandler>(); /** A map of {@link HttpRequestHandler}s keyed by the URI pattern they respond to */ protected ConcurrentHashMap<String, HttpRequestHandler> uriRoutes = new ConcurrentHashMap<String, HttpRequestHandler>(); /** A map of {@link HttpRequestHandler}s keyed by the wildcarded URI pattern they respond to */ protected ConcurrentHashMap<String, HttpRequestHandler> uriWildcardRoutes = new ConcurrentHashMap<String, HttpRequestHandler>(); /** The websocket handler to be inserted into the pipeline if a request comes in with a URI suffix of {@link #WS_URI_SUFFIX} */ protected WebSocketServiceHandler webSocketHandler = null; /** Default page URI */ public static final String DEFAULT_URI = "index.html"; /** Root URI */ public static final String ROOT_URI = "/"; /** WebSocket URI Suffix */ public static final String WS_URI_SUFFIX = "/ws"; /** * Creates a new HttpRequestRouter and adds {@link HttpRequestHandlerStarted} and {@link HttpRequestHandlerStopped} to * the supported application event set. */ public HttpRequestRouter() { super(); supportedEventTypes.add(HttpRequestHandlerStarted.class); supportedEventTypes.add(HttpRequestHandlerStopped.class); } /** * {@inheritDoc} * @see org.helios.apmrouter.server.ServerComponentBean#doStart() */ @Override protected void doStart() { applicationContext.addApplicationListener(this); } /** * On start, searches the app context for {@link HttpRequestHandler}s not already registered. * @param event The app context refresh event */ @Override public void onApplicationContextRefresh(ContextRefreshedEvent event) { Map<String, HttpRequestHandler> inits = event.getApplicationContext().getBeansOfType(HttpRequestHandler.class); if(!inits.isEmpty()) { for(Map.Entry<String, HttpRequestHandler> entry: inits.entrySet()) { if(!handlers.containsKey(entry.getKey())) { handlers.put(entry.getKey(), entry.getValue()); int added = 0; for(String key: entry.getValue().getUriPatterns()) { key = key.trim().toLowerCase(); if(uriRoutes.putIfAbsent(key, entry.getValue())==null) { added++; } else { warn("HttpRequestHandler [", entry.getKey(), "] attempted to register with URI [", key, "] which was already registered"); } } info("Adding Discovered HttpRequestHandler [", entry.getKey(), "] with [", added, "] URI keys" ); } } } } /** * Called when a {@link HttpRequestHandler} starts * @param handlerStarted The {@link HttpRequestHandler} start event */ public void onApplicationEvent(HttpRequestHandlerStarted handlerStarted) { HttpRequestHandler rh = handlerStarted.getHandler(); if(!handlers.containsKey(rh.getBeanName())) { handlers.put(rh.getBeanName(), rh); int added = 0; int wadded = 0; for(String key: rh.getUriPatterns()) { key = key.trim().toLowerCase(); if(key.endsWith("*")) { if(uriWildcardRoutes.putIfAbsent(key.substring(0, key.length()-1), rh)==null) { wadded++; } else { warn("HttpRequestHandler [", rh.getBeanName() , "] attempted to register with wildcard URI [", key, "] which was already registered"); } } else { if(uriRoutes.putIfAbsent(key, rh)==null) { added++; } else { warn("HttpRequestHandler [", rh.getBeanName() , "] attempted to register with URI [", key, "] which was already registered"); } } } info("Adding Discovered HttpRequestHandler [", rh.getBeanName(), "] with [", added, "] URI keys and [", wadded, "] wildcard keys" ); } } /** * <p>Delegates to the typed application event handler. * Required since the origination of events from hot-deployed contexts does not trigger the typed handlers.</p> * {@inheritDoc} * @see org.helios.apmrouter.server.ServerComponentBean#onApplicationEvent(org.springframework.context.ApplicationEvent) */ @Override public void onApplicationEvent(ApplicationEvent appEvent) { if(appEvent instanceof HttpRequestHandlerStopped) { onApplicationEvent((HttpRequestHandlerStopped)appEvent); } else if(appEvent instanceof HttpRequestHandlerStarted) { onApplicationEvent((HttpRequestHandlerStarted)appEvent); } } /** * Called when a {@link HttpRequestHandler} stops * @param handlerStopped The {@link HttpRequestHandler} stop event */ public void onApplicationEvent(HttpRequestHandlerStopped handlerStopped) { HttpRequestHandler rh = handlerStopped.getHandler(); if(handlers.remove(rh.getBeanName())!=null) { int removed = 0; int wremoved = 0; for(String key: rh.getUriPatterns()) { key = key.trim().toLowerCase(); if(uriRoutes.remove(key)!=null) removed++; if(uriWildcardRoutes.remove(key)!=null) wremoved++; } info("Removed Stopped HttpRequestHandler [", rh.getBeanName(), "] with [", removed, "] URI keys and [", wremoved, "] wildcard keys" ); } } /** * {@inheritDoc} * @see org.helios.apmrouter.server.ServerComponentBean#supportsEventType(java.lang.Class) */ @Override public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) { boolean supports = super.supportsEventType(eventType); return supports; } /** * {@inheritDoc} * @see org.helios.apmrouter.server.ServerComponentBean#supportsSourceType(java.lang.Class) */ @Override public boolean supportsSourceType(Class<?> sourceType) { return true; } /** * Returns an unmodifiable set of URI patterns that this modifier is activated for * @return an unmodifiable set of URI patterns that this modifier is activated for */ @ManagedAttribute(description="URI patterns that this modifier is activated for") public Map<String, String> getUriMappings() { Map<String, String> map = new HashMap<String, String>(handlers.size()); for(Map.Entry<String, HttpRequestHandler> entry: uriRoutes.entrySet()) { map.put(entry.getKey(), entry.getValue().getClass().getName()); } return map; } /** * Returns an unmodifiable set of wildcard URI patterns that this modifier is activated for * @return an unmodifiable set of wildcard URI patterns that this modifier is activated for */ @ManagedAttribute(description="Wildcard URI patterns that this modifier is activated for") public Map<String, String> getWildcardUriMappings() { Map<String, String> map = new HashMap<String, String>(handlers.size()); for(Map.Entry<String, HttpRequestHandler> entry: uriWildcardRoutes.entrySet()) { map.put(entry.getKey(), entry.getValue().getClass().getName()); } return map; } /** * {@inheritDoc} * @see org.jboss.netty.channel.ChannelUpstreamHandler#handleUpstream(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.ChannelEvent) */ @Override public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { HttpRequest request = null; if(!(e instanceof MessageEvent)) { if(e instanceof ExceptionEvent) { error("Http Routing Exception ", ((ExceptionEvent)e).getCause()); sendError(ctx, INTERNAL_SERVER_ERROR); return; } ctx.sendUpstream(e); return; } MessageEvent me = (MessageEvent)e; Object message = me.getMessage(); if(message instanceof WebSocketFrame) { ctx.sendUpstream(e); return; } try { request = (HttpRequest)((MessageEvent)e).getMessage(); if(request==null) { throw new Exception("HttpRequest was null", new Throwable()); } } catch (Exception ex) { throw new Exception("Failed to extract a message event for assumed type HttpRequest", ex); } // now we have a request... String uri = request.getUri(); int qindex = uri.indexOf("?"); if(qindex!=-1) { uri = uri.substring(0, qindex); } if(uri.endsWith(WS_URI_SUFFIX)) { ctx.getPipeline().addLast(webSocketHandler.getBeanName(), webSocketHandler); ctx.sendUpstream(e); return; } if(uri.isEmpty() || ROOT_URI.equals(uri)) { uri = "index.html"; } debug("Processing HTTP Request for URI [", uri, "]"); // ====================================================================== // ====================================================================== // The handler lookup // ====================================================================== // ====================================================================== HttpRequestHandler handler = uriRoutes.get(uri); if(handler==null) { handler = findWildcardMatch(uri); } if(handler==null) { handler = uriRoutes.get(""); // the default handler is the file server } // ====================================================================== if(handler==null) { sendError(ctx, NOT_FOUND); } else { try { handler.handle(ctx, (MessageEvent)e, request, uri); } catch (Exception ex) { Channel ch = e.getChannel(); error("Uncaught exception", ex); if (ex instanceof TooLongFrameException) { sendError(ctx, BAD_REQUEST); return; } if (ch.isConnected()) { sendError(ctx, INTERNAL_SERVER_ERROR); } } } } /** * Attempts to match the passed URI to a wildcarded handler * @param uri The uri to match * @return the matched handler or null if one was not found */ protected HttpRequestHandler findWildcardMatch(String uri) { for(Map.Entry<String, HttpRequestHandler> wc : uriWildcardRoutes.entrySet()) { if(uri.startsWith(wc.getKey())) { return wc.getValue(); } } return null; } /** * Handles uncaught exceptions * @param ctx The channel handler context * @param e The exception event * @throws Exception the uncaught exception handling exception */ public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { Channel ch = e.getChannel(); Throwable cause = e.getCause(); if(cause != null && cause instanceof TooLongFrameException) { } error("Uncaught exception", cause); if (cause instanceof TooLongFrameException) { sendError(ctx, BAD_REQUEST); return; } if (ch.isConnected()) { sendError(ctx, INTERNAL_SERVER_ERROR); } } /** * Returns an HTTP error back to the caller * @param ctx The channel handler context * @param status The HTTP Status to send */ private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8"); response.setContent(ChannelBuffers.copiedBuffer( "Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8)); // Close the connection as soon as the error message is sent. if(ctx.getChannel().isOpen()) { ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE); } } /** * Sets the websocket handler to be inserted into the pipeline if a request comes in with a URI suffix of {@link #WS_URI_SUFFIX} * @param webSocketHandler the webSocketHandler to set */ @Autowired(required=true) public void setWebSocketHandler(WebSocketServiceHandler webSocketHandler) { this.webSocketHandler = webSocketHandler; } }