/*
* Dijjer - A Peer to Peer HTTP Cache
* Copyright (C) 2004,2005 Change.Tv, Inc
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package freenet.io.comm;
import java.util.ArrayList;
import java.util.List;
import freenet.node.PrioRunnable;
import freenet.support.Executor;
import freenet.support.Logger;
/**
* @author ian
*
* To change the template for this generated type comment go to Window - Preferences - Java - Code Generation - Code and
* Comments
*/
public final class MessageFilter {
private static volatile boolean logMINOR;
static {
Logger.registerClass(MessageFilter.class);
}
public static final String VERSION = "$Id: MessageFilter.java,v 1.7 2005/08/25 17:28:19 amphibian Exp $";
private boolean _matched;
private PeerContext _droppedConnection;
private MessageType _type;
private final List<Object> _fields = new ArrayList<Object>();
private final List<String> _fieldNames = new ArrayList<String>();
private PeerContext _source;
private long _timeout;
/** If true, timeouts are relative to the start of waiting, if false, they are relative to
* the time of calling setTimeout() */
private boolean _timeoutFromWait;
private long _initialTimeout;
private MessageFilter _or;
private Message _message;
private long _oldBootID;
private AsyncMessageFilterCallback _callback;
private ByteCounter _ctr;
private boolean _setTimeout = false;
private MessageFilter() {
_timeoutFromWait = true;
}
public static MessageFilter create() {
return new MessageFilter();
}
void onStartWaiting(boolean waitFor) {
synchronized(this) {
/* We cannot wait on a MessageFilter with a callback, because onMatched() calls clearMatched()
* if we have a callback. The solution would be to:
* - Set a flag indicating we are waitFor()ing a filter here.
* - On matching a message (setMessage), call the callback immediately if not waitFor()ing.
* - If we are waitFor()ing, call the callback when we exit waitFor() (onStopWaiting()???).
*/
if(waitFor && _callback != null)
throw new IllegalStateException("Cannot wait on a MessageFilter with a callback!");
if(!_setTimeout)
throw new IllegalStateException("No timeout set on filter " + this + "; cannot wait.");
if(_initialTimeout > 0 && _timeoutFromWait)
_timeout = System.currentTimeMillis() + _initialTimeout;
}
if(_or != null)
_or.onStartWaiting(waitFor);
}
/**
* Set whether the timeout is relative to the creation of the filter, or the start of
* waitFor().
* @param b If true, the timeout is relative to the time at which setTimeout() was called,
* if false, it's relative to the start of waitFor().
*/
public MessageFilter setTimeoutRelativeToCreation(boolean b) {
_timeoutFromWait = !b;
return this;
}
/**
* This filter will expire after the specificed amount of time. Note also that where two or more filters match the
* same message, the one with the nearer expiry time will get priority
*
* @param timeout The time before this filter expires in ms
* @return This message filter
*/
public MessageFilter setTimeout(long timeout) {
_setTimeout = true;
_initialTimeout = timeout;
_timeout = System.currentTimeMillis() + timeout;
return this;
}
public MessageFilter setNoTimeout() {
_setTimeout = true;
_timeout = Long.MAX_VALUE;
_initialTimeout = 0;
return this;
}
public MessageFilter setType(MessageType type) {
_type = type;
return this;
}
public MessageFilter setSource(PeerContext source) {
_source = source;
if(source != null)
_oldBootID = source.getBootID();
return this;
}
/**
Returns the source that this filter (or chain) matches
*/
public PeerContext getSource() {
return _source;
}
public MessageFilter setField(String fieldName, boolean value) {
return setField(fieldName, Boolean.valueOf(value));
}
public MessageFilter setField(String fieldName, byte value) {
return setField(fieldName, Byte.valueOf(value));
}
public MessageFilter setField(String fieldName, short value) {
return setField(fieldName, Short.valueOf(value));
}
public MessageFilter setField(String fieldName, int value) {
return setField(fieldName, Integer.valueOf(value));
}
public MessageFilter setField(String fieldName, long value) {
return setField(fieldName, Long.valueOf(value));
}
public MessageFilter setField(String fieldName, Object fieldValue) {
if ((_type != null) && (!_type.checkType(fieldName, fieldValue))) {
throw new IncorrectTypeException("Got " + fieldValue.getClass() + ", expected " + _type.typeOf(fieldName) + " for " + _type.getName());
}
synchronized (_fields) {
final int i = _fieldNames.indexOf(fieldName);
if (i >= 0) {
_fields.set(i, fieldValue);
} else {
_fieldNames.add(fieldName);
_fields.add(fieldValue);
}
}
return this;
}
/**
* Modifies the filter so that it returns true if either it or the filter in the argument returns true.
* Multiple combinations must be nested: such as filter1.or(filter2.or(filter3))).
* @return reference to this, the modified filter.
*/
public MessageFilter or(MessageFilter or) {
if((or != null) && (_or != null) && or != _or) {
throw new IllegalStateException("Setting a second .or() on the same filter will replace the " +
"existing one, not add another. " + _or + " would be replaced by " + or + ".");
}
if(or._initialTimeout != _initialTimeout) {
Logger.error(this, "Message filters being or()ed have different timeouts! This is very dangerous! This is "+this+" or is "+or);
// FIXME throw new IllegalArgumentException()
}
_or = or;
return this;
}
public MessageFilter setAsyncCallback(AsyncMessageFilterCallback cb, ByteCounter ctr) {
_callback = cb;
_ctr = ctr;
return this;
}
enum MATCHED {
MATCHED,
TIMED_OUT,
TIMED_OUT_AND_MATCHED,
NONE
}
public MATCHED match(Message m, long now) {
return match(m, false, now);
}
public MATCHED match(Message m, boolean noTimeout, long now) {
if(_or != null) {
MATCHED matched = _or.match(m, noTimeout, now);
if(matched != MATCHED.NONE)
return matched; // Filter is matched once only. That includes timeouts.
}
final MATCHED resultNoMatch = _timeout < now ? MATCHED.TIMED_OUT : MATCHED.NONE;
if ((_type != null && !_type.equals(m.getSpec())) ||
(_source != null && !_source.equals(m.getSource()))) {
// Timeout immediately, but don't check the callback, so we still need the periodic check.
return resultNoMatch;
}
synchronized (_fields) {
for (int i = 0; i < _fieldNames.size(); i++) {
final String fieldName = _fieldNames.get(i);
if (!m.isSet(fieldName)) {
return resultNoMatch;
}
final Object fieldValue = _fields.get(i);
if (!fieldValue.equals(m.getFromPayload(fieldName))) {
return resultNoMatch;
}
}
}
if((!noTimeout) && reallyTimedOut(now)) {
if(logMINOR) Logger.minor(this, "Matched but timed out: "+this);
return MATCHED.TIMED_OUT_AND_MATCHED;
}
return MATCHED.MATCHED;
}
public boolean matched() {
return _matched;
}
/**
* Which connection dropped or was restarted?
*/
public PeerContext droppedConnection() {
return _droppedConnection;
}
boolean reallyTimedOut(long time) {
if(_callback != null && _callback.shouldTimeout())
_timeout = -1; // timeout immediately
return _timeout < time;
}
/**
* @param time The current time in milliseconds.
* @return True if the filter has timed out, or if it has been matched already. Caller will
* remove the filter from _filters if we return true.
*/
boolean timedOut(long time) {
if(_matched) {
Logger.error(this, "Impossible: filter already matched in timedOut(): "+this, new Exception("error"));
return true; // Remove it.
}
return reallyTimedOut(time);
}
public Message getMessage() {
return _message;
}
public synchronized void setMessage(Message message) {
//Logger.debug(this, "setMessage("+message+") on "+this, new Exception("debug"));
_message = message;
// Avoid race conditions where it is removed from the filter list because of a timeout but not woken up.
_matched = true;
notifyAll();
}
public long getInitialTimeout() {
return _initialTimeout;
}
public long getTimeout() {
return _timeout;
}
@Override
public String toString() {
return super.toString()+":"+_type.getName();
}
public void clearMatched() {
// If the filter matched in an _or, and it is re-used, then
// we need to clear all the _or's.
MessageFilter or;
synchronized(this) {
_matched = false;
_message = null;
or = _or;
}
if(or != null)
or.clearMatched();
}
public void clearOr() {
_or = null;
}
public boolean matchesDroppedConnection(PeerContext ctx) {
if(_source == ctx) return true;
if(_or != null) return _or.matchesDroppedConnection(ctx);
return false;
}
public boolean matchesRestartedConnection(PeerContext ctx) {
if(_source == ctx) return true;
if(_or != null) return _or.matchesRestartedConnection(ctx);
return false;
}
/**
* Notify because of a dropped connection.
* Caller must verify _matchesDroppedConnection and _source.
* @param ctx
*/
public void onDroppedConnection(final PeerContext ctx, Executor executor) {
final AsyncMessageFilterCallback cb;
synchronized(this) {
cb = _callback;
_droppedConnection = ctx;
notifyAll();
_ctr = null;
}
if(cb != null) {
if(cb instanceof SlowAsyncMessageFilterCallback) {
executor.execute(new PrioRunnable() {
@Override
public void run() {
cb.onDisconnect(ctx);
}
@Override
public int getPriority() {
return ((SlowAsyncMessageFilterCallback)cb).getPriority();
}
});
} else {
cb.onDisconnect(ctx);
}
}
}
/**
* Notify because of a restarted connection.
* Caller must verify _matchesDroppedConnection and _source.
* @param ctx
*/
public void onRestartedConnection(final PeerContext ctx, Executor executor) {
final AsyncMessageFilterCallback cb;
synchronized(this) {
_droppedConnection = ctx;
cb = _callback;
notifyAll();
_ctr = null;
}
if(cb != null) {
if(cb instanceof SlowAsyncMessageFilterCallback) {
executor.execute(new PrioRunnable() {
@Override
public void run() {
cb.onRestarted(ctx);
}
@Override
public int getPriority() {
return ((SlowAsyncMessageFilterCallback)cb).getPriority();
}
});
} else {
cb.onRestarted(ctx);
}
}
}
/**
* Notify waiters that we have been matched.
* Hopefully no locks will be held at this point by the caller.
*/
public void onMatched(Executor executor) {
final Message msg;
final AsyncMessageFilterCallback cb;
ByteCounter ctr;
synchronized(this) {
msg = _message;
cb = _callback;
ctr = _ctr;
// Clear matched before calling callback in case we are re-added.
if(_callback != null)
clearMatched();
}
if(cb != null) {
if(cb instanceof SlowAsyncMessageFilterCallback)
executor.execute(new PrioRunnable() {
@Override
public void run() {
cb.onMatched(msg);
}
@Override
public int getPriority() {
return ((SlowAsyncMessageFilterCallback)cb).getPriority();
}
}, "Slow callback for "+cb);
else
cb.onMatched(msg);
if(ctr != null)
ctr.receivedBytes(msg._receivedByteCount);
}
}
/**
* Notify waiters that we have timed out.
*/
public void onTimedOut(Executor executor) {
final AsyncMessageFilterCallback cb;
synchronized(this) {
notifyAll();
cb = _callback;
}
if(cb != null) {
if(cb instanceof SlowAsyncMessageFilterCallback) {
executor.execute(new PrioRunnable() {
@Override
public void run() {
cb.onTimeout();
}
@Override
public int getPriority() {
return ((SlowAsyncMessageFilterCallback)cb).getPriority();
}
});
} else
cb.onTimeout();
}
}
/**
* Returns true if a connection related to this filter has been dropped or restarted.
*/
public boolean anyConnectionsDropped() {
if(_matched) return false;
if(_source != null) {
if(!_source.isConnected()) {
return true;
} else if(_source.getBootID() != _oldBootID) {
return true; // Counts as a disconnect.
}
}
if(_or != null)
return _or.anyConnectionsDropped();
return false;
}
public synchronized boolean hasCallback() {
return _callback != null;
}
}