/**
* 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.protocol;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.helios.apmrouter.server.ServerComponentBean;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferFactory;
import org.jboss.netty.buffer.DirectChannelBufferFactory;
import org.jboss.netty.buffer.DynamicChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelLocal;
import org.jboss.netty.channel.ChannelUpstreamHandler;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.UpstreamMessageEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.jmx.export.annotation.ManagedAttribute;
/**
* <p>Title: ProtocolSwitch</p>
* <p>Description: An upfront channel handler to determine if the incoming is HTTP or plain socket text for submissions</p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.server.unification.protocol.ProtocolSwitch</code></p>
*/
public class ProtocolSwitch extends ServerComponentBean implements ChannelUpstreamHandler {
/** A map of {@link ProtocolInitiator}s keyed by their bean name */
protected final Map<String, ProtocolInitiator> initiators = new ConcurrentHashMap<String, ProtocolInitiator>();
// /** A map of {@link ProtocolInitiator}s keyed by a magic long know to be a match */
// protected final Map<Long, ProtocolInitiator> cachedMatchInitiators = new ConcurrentHashMap<Long, ProtocolInitiator>();
/** A channel local to accumulate unclassified buffers */
protected final ChannelLocal<DynamicChannelBuffer> preSwitchedBuffer = new ChannelLocal<DynamicChannelBuffer>(true);
/** The channel buffer factory */
protected final ChannelBufferFactory chanelBufferFactory = new DirectChannelBufferFactory(ByteOrder.nativeOrder(), 1024);
/**
* Creates a new ProtocolSwitch and adds {@link ProtocolInitiatorStarted} and {@link ProtocolInitiatorStopped} to
* the supported application event set.
*/
public ProtocolSwitch() {
super();
supportedEventTypes.add(ProtocolInitiatorStarted.class);
supportedEventTypes.add(ProtocolInitiatorStopped.class);
}
/**
* On start, searches the app context for {@link ProtocolInitiator}s not already registered.
* @param event The app context refresh event
*/
@Override
public void onApplicationContextRefresh(ContextRefreshedEvent event) {
Map<String, ProtocolInitiator> inits = event.getApplicationContext().getBeansOfType(ProtocolInitiator.class);
if(!inits.isEmpty()) {
for(Map.Entry<String, ProtocolInitiator> entry: inits.entrySet()) {
if(!initiators.containsKey(entry.getKey())) {
initiators.put(entry.getKey(), entry.getValue());
info("Adding Discovered ProtocolInitiator [", entry.getKey(), "]" );
}
}
}
}
/**
* Called when a {@link ProtocolInitiator} starts
* @param protocolInitiatorStarted The {@link ProtocolInitiator} start event
*/
public void onApplicationEvent(ProtocolInitiatorStarted protocolInitiatorStarted) {
ProtocolInitiator pi = protocolInitiatorStarted.getInitiator();
if(!initiators.containsKey(pi.getBeanName())) {
initiators.put(pi.getBeanName(), pi);
info("Adding Started ProtocolInitiator [", pi.getBeanName(), "]" );
}
}
/**
* Called when a {@link ProtocolInitiator} stops
* @param protocolInitiatorStopped The {@link ProtocolInitiator} stop event
*/
public void onApplicationEvent(ProtocolInitiatorStopped protocolInitiatorStopped) {
ProtocolInitiator pi = protocolInitiatorStopped.getInitiator();
if(initiators.remove(pi.getBeanName())!=null) {
info("Removing stopped ProtocolInitiator [", pi.getBeanName(), "]" );
}
}
/**
* {@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 {
if(e instanceof MessageEvent) {
MessageEvent me = (MessageEvent) e;
if(me.getMessage() instanceof ChannelBuffer) {
ChannelBuffer postDetectBuffer = protocolSwitch(ctx, e.getChannel(), (ChannelBuffer)me.getMessage(), e);
if(postDetectBuffer!=null) {
ctx.getPipeline().remove(this);
ctx.sendUpstream(new UpstreamMessageEvent(e.getChannel(), postDetectBuffer, ((MessageEvent) e).getRemoteAddress()));
}
}
} else {
ctx.sendUpstream(e);
}
}
/**
* Examines the channel buffer and attempts to match the protocol of the request and invoke the matching {@link ProtocolInitiator}.
* @param ctx The channel handler context
* @param channel The channel
* @param bufferx The message buffer
* @param e The channel event
* @return The channel buffer to send upstream, or null if we need more bytes
*/
protected ChannelBuffer protocolSwitch(ChannelHandlerContext ctx, Channel channel, ChannelBuffer bufferx, ChannelEvent e) {
ChannelBuffer cb = preSwitchedBuffer.get(channel);
if(cb!=null) {
cb.writeBytes(bufferx);
cb.resetReaderIndex();
} else {
cb = bufferx;
}
// this guy will be set with a matching initiator
ProtocolInitiator selectedInitiator = null;
// this guy will be set to false if at least 1 initiator had insufficient bytes to match
boolean sufficientBytes = true;
// ths guy has the total bytes available in the buffer
final int bytesAvailable = cb.readableBytes();
for(ProtocolInitiator pi : initiators.values()) {
if(pi.requiredBytes() < bytesAvailable) {
sufficientBytes = false;
} else {
if(pi.match(cb)) {
selectedInitiator = pi;
break;
}
}
}
if(selectedInitiator==null) {
// we did not get a match
if(!sufficientBytes) {
// ok, we did not have enough bytes
DynamicChannelBuffer dcb = preSwitchedBuffer.get(channel);
if(dcb == null) {
dcb = new DynamicChannelBuffer(cb.order(), 1024, chanelBufferFactory);
preSwitchedBuffer.set(channel, dcb);
}
dcb.writeBytes(cb);
dcb.resetReaderIndex();
return null;
}
// darn, we have enough bytes for any of the inits,
// but none matched
throw new RuntimeException("Failed to match any protocol initiator");
}
preSwitchedBuffer.remove(channel);
selectedInitiator.modifyPipeline(ctx, channel, cb);
return cb;
// if we get here, it means we did not find a protocol match
// so pass to the default protocol initiator.
}
/**
* Returns a map of {@link ProtocolInitiator} bean names keyed by protocol name they implement
* @return a map of {@link ProtocolInitiator} bean names keyed by protocol name they implement
*/
@ManagedAttribute(description="The supported protocols")
public Map<String, String> getProtocols() {
Map<String, String> map = new HashMap<String, String>();
for(ProtocolInitiator pi: initiators.values()) {
map.put(pi.getProtocol(), pi.getBeanName());
}
return map;
}
}