package uc.protocols; import helpers.GH; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ProtocolException; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.io.IOException; import logger.LoggerFactory; import org.apache.log4j.Logger; import org.eclipse.core.runtime.Platform; /** * Base class for all Connection Protocols. Used to make implementation of * plain text protocols easier * * Does most of the hard work specially provides a nice interface a connection can call. * * Clients should extend this to present the protocol unspecific entity. like a hub or a client * * most protocol depending stuff could go into implementations of IProtocolCommand objects.. * * * */ public abstract class ConnectionProtocol implements ReadWriteLock { private static Logger logger = LoggerFactory.make(); private final ReentrantReadWriteLock rwLock; protected volatile IConnection connection; //Needs to be set soon... bad thing.. really.. protected int defaultPort; private final Object charsetSynch = new Object(); private Charset charset; private static final int SOCKET_TIMEOUT = 40000; private static final Map<InetAddress,IConnectionDebugger> AUTO_ATTACH= Collections.synchronizedMap(new HashMap<InetAddress,IConnectionDebugger>()); public static void addNotifyAttachable(InetAddress ia,IConnectionDebugger debug) { AUTO_ATTACH.put(ia, debug); } public static void removeNotifyAttachable(InetAddress ia) { AUTO_ATTACH.remove(ia); } private final CopyOnWriteArraySet<IConnectionDebugger> debuggers = new CopyOnWriteArraySet<IConnectionDebugger>(); private volatile boolean loginDone = false; private volatile boolean pendingDestroyed = false; /** * maps prefixes of the commands to the commands */ protected Map<String,IProtocolCommand<? extends ConnectionProtocol>> commands = new HashMap<String,IProtocolCommand<? extends ConnectionProtocol>>(); /** * default command used when no prefix can be determined.. */ protected IProtocolCommand<? extends ConnectionProtocol> defaultCommand; /** * a pattern that has the prefix of a command as first capture * if it not matches.. some default command is called.. * default value is NMDC */ protected volatile Pattern prefix = Pattern.compile("(\\$\\S+)[^|]*"); private final Object csSynch = new Object(); /** * the current state of the protocol */ private ConnectionState state = ConnectionState.CONNECTING ; private volatile long lastLogin = 0; private final int[] performancePrefs; /** * store for unfinished commands.. */ private final StringBuffer stringbuffer = new StringBuffer(); public ConnectionProtocol(AbstractConnection a) { this(a,null); } /** * @param a which connectionProtocol is used with this connection.. * @param performancePrefs @see {@link Socket#setPerformancePreferences(int, int, int)} * the array must be of length 3 and specify performance preferences * as the Socket does... */ public ConnectionProtocol(AbstractConnection a,int[] performancePrefs) { this(performancePrefs); this.connection = a; } /** * * @param performancePrefs - @see {@link Socket#setPerformancePreferences(int, int, int)} * the array must be of length 3 and specify performance preferences * as the Socket does... */ public ConnectionProtocol(int[] performancePrefs){ if (performancePrefs != null && performancePrefs.length != 3) { throw new IllegalArgumentException(); } this.performancePrefs = performancePrefs; this.rwLock = new ReentrantReadWriteLock(); } public ConnectionProtocol() { this((int[])null); } /** * be called at the beginning.. when the protocol starts * */ public void start() { //mt.registerCP(this); } public void beforeConnect() { clearCommands(); stringbuffer.delete(0, stringbuffer.length()); //clear loginDone = false; setState(ConnectionState.CONNECTING); } public void keyPrintFailed() { setState(ConnectionState.KPFAILED); } /** * retrieves a pattern needed to get capture a command * first capture group must be the part of the command * that is provided to receivedCommand * while the whole pattern should match the whole command * @see{NMDCProtocol.java} for the implementation of this */ public abstract Pattern getCommandRegexPattern(); /** * * @return the byte after which command processing shall be stopped.. */ public abstract int getCommandStopByte(); // These 3 Methods have to be overridden by each subclass of protocol /** * OnConnect is Called once by the connection after Socket is connected.. * to the other side * overwriting methods must call super.onConnect() */ public void onConnect() throws IOException { setState(ConnectionState.CONNECTED); InetAddress ia = connection.getInetSocketAddress().getAddress(); IConnectionDebugger debug = AUTO_ATTACH.get(ia); if (debug != null) { debug.notifyAttachable(ia, this); } } /** * Function is called by the protocol!! when login is done.. * this sets the login done flag so future commands go to commandReceived() * instead of commandReceivedDuringLogin() */ public void onLogIn() throws IOException { synchronized(csSynch) { //because sometimes the connection gets closed during login -> check if (state != ConnectionState.CONNECTED) { if (Platform.inDevelopmentMode() && ConnectionState.DESTROYED != state && ConnectionState.CLOSED != state) { logger.warn("Bad state: "+state+" "+toString(),new Throwable()); } throw new IOException("can not login when not in connected state: "+state); } loginDone = true; lastLogin = System.currentTimeMillis(); setState(ConnectionState.LOGGEDIN); } } void receivedCommand(byte[] command) throws IOException, ProtocolException { String s = getCharset().decode(ByteBuffer.wrap(command)).toString(); stringbuffer.append(s); Matcher m = null; while ((m = getCommandRegexPattern().matcher(stringbuffer)).find()) { String found = m.group(1); // logger.debug("command: "+found); try { if (!GH.isNullOrEmpty(found)) { stringbuffer.delete(0, m.end()); receivedCommand(found); } else { stringbuffer.deleteCharAt(0); break; } } catch(RuntimeException re) { logger.warn("Caused by: "+found+ " in "+toString()); throw re; } } } /** * called for each command that is received.. * * @param command - the command as provided by the CP's set Regexp * @throws IOException - especially needed when writing back .. so this can be caught * by the Connection * @throws ProtocolException - * */ @SuppressWarnings("unchecked") public void receivedCommand(String command) throws IOException, ProtocolException { Matcher m = prefix.matcher(command); IProtocolCommand<? extends ConnectionProtocol> com = null; if (m.matches()) { String prefix = m.group(1); com = commands.get(prefix); } else if (defaultCommand != null){ com = defaultCommand; } boolean matches = false; if (com != null ) { matches = com.matches(command); } for (IConnectionDebugger debugger:debuggers) { debugger.receivedCommand(com, matches, command); } if (com != null) { // logger.debug("command found "+com.getPrefix()); if (matches) { ((IProtocolCommand<ConnectionProtocol>)com).handle(this,command); } else { onMalformedCommandReceived(command); } } else { onUnexpectedCommandReceived(command); } } protected void onUnexpectedCommandReceived(String command) { logger.debug("Unexpected command received: "+command+" in "+connection.getInetSocketAddress()); } protected void onMalformedCommandReceived(String command) { logger.debug("Malformed Command received: "+command+" in "+connection.getInetSocketAddress()); } /** * Called when Socket is closed * overwriting methods must make sure to * to call super.onDisconnect() * * @throws IOException */ public void onDisconnect() throws IOException { //mt.deregisterCP(this); setState(ConnectionState.CLOSED); } /** * a timer for every protocol * it is called every second by a global timer */ public final void timer() { } /** * sends a raw without doing any tinkering * like formatting by context.. * @param mes - the message to be sent.. */ protected void sendRaw(String mes) { for (IConnectionDebugger debugger:debuggers) { debugger.sentCommand(mes); } ByteBuffer b = getCharset().encode(mes); try { connection.send(b); // logger.debug("sent raw: "+mes); } catch(IOException ioe) { logger.warn(ioe,ioe); } } protected void sendRaw(byte[] mes) { for (IConnectionDebugger debugger:debuggers) { debugger.sentCommand(getCharset().decode(ByteBuffer.wrap(mes)).toString()); } try { connection.send(ByteBuffer.wrap(mes)); // logger.debug("sent raw: "+new String(mes)); } catch(IOException ioe) { logger.warn(ioe,ioe); } } /** * @param addy - takes a ip:port string and gives a socketpair back.. * @param defaultport - used if no : is found in the addy string */ public static InetSocketAddress inetFromString(String addy, int defaultport) { int i = addy.lastIndexOf(':'); int port = defaultport; String onlyaddy = addy; if (i != -1) { String subs = addy.substring(i+1); port = subs.isEmpty()? defaultport: Integer.parseInt(subs); onlyaddy= addy.substring(0, i); } return new InetSocketAddress(onlyaddy,port); } private final CopyOnWriteArrayList<IProtocolStatusChangedListener> cscl = new CopyOnWriteArrayList<IProtocolStatusChangedListener>(); public void registerProtocolStatusListener(IProtocolStatusChangedListener listener) { if (listener == null && Platform.inDevelopmentMode()) { logger.warn("registered null", new Throwable()); } cscl.addIfAbsent(listener); } /** * adds an element to the beginning of the list .. so it is notified before others.. * @param listener - a high priority listener. */ protected void registerListenerFirst(IProtocolStatusChangedListener listener) { if (listener == null) { logger.warn("registered null", new Throwable()); } cscl.remove(listener); cscl.add(0, listener); } public void unregisterProtocolStatusListener(IProtocolStatusChangedListener listener){ cscl.remove(listener); } protected void setState(ConnectionState state) { synchronized(csSynch) { if (this.state == ConnectionState.DESTROYED) { throw new IllegalStateException("State was already destroyed "+this); } this.state = state; for (IProtocolStatusChangedListener listener: cscl) { if (listener == null) { logger.warn("found Listener null", new Throwable()); } else { listener.statusChanged(state, this); } } if (pendingDestroyed && this.state == ConnectionState.CLOSED) { setState(ConnectionState.DESTROYED); } } } /** * sets state of the connection Destroyed.. * kind of like dispose() in swt.. this is needed so timer is no longer * called and the Connection Protocol can be garbage Collected.. * but does nothing.. * * this method will immediately post an Destroyed event to setState() * if the Connection is closed.. * or send it as soon as the connection is Closed.. */ public void end() { synchronized (csSynch) { if (state == ConnectionState.CLOSED) { setState(ConnectionState.DESTROYED); } else { pendingDestroyed = true; } } } /** * * @return the current state of the protocol.. */ public ConnectionState getState() { synchronized (csSynch) { return state; } } public void clearCommands() { commands.clear(); } public void addCommand(IProtocolCommand<? extends ConnectionProtocol> com) { // for (IProtocolCommand<? extends ConnectionProtocol> com: command ) { commands.put(com.getPrefix(), com); // } } public void removeCommand(IProtocolCommand<?> command) { commands.remove(command.getPrefix()); } protected void setPrefix(Pattern prefix) { this.prefix = prefix; } public Charset getCharset() { synchronized (charsetSynch) { return charset; } } public void setCharset(Charset cs) { synchronized (charsetSynch) { this.charset = cs; } } public int getSocketTimeout() { return SOCKET_TIMEOUT; } /** * whether this ClientProtocol is encrypted.. * @return */ public boolean isEncrypted() { return connection.usesEncryption(); } /** * * @return true if KEYP is in use for this connection */ public boolean isFingerPrintUsed() { return connection.isFingerPrintUsed(); } int[] getPerformancePrefs() { return performancePrefs; } /** * * @return the connection this ConnectionProtocol is associated with */ public IConnection getConnection() { return connection; } public boolean isLoginDone() { return loginDone; } public long getLastLogin() { return lastLogin; } /** * @param conDebug - register Debugger that then gets notified of connections */ public void registerDebugger(IConnectionDebugger conDebug) { debuggers.add(conDebug); registerProtocolStatusListener(conDebug); } /** * @param conDebug - unregister Debugger that then gets notified of connections */ public void unregisterDebugger(IConnectionDebugger conDebug) { debuggers.remove(conDebug); unregisterProtocolStatusListener(conDebug); } public WriteLock writeLock() { return rwLock.writeLock(); } public ReadLock readLock() { return rwLock.readLock(); } public boolean isIPv6() { return connection.getInetSocketAddress().getAddress() instanceof Inet6Address; } public boolean isIPv4() { return connection.getInetSocketAddress().getAddress() instanceof Inet4Address; } }