package de.skuzzle.polly.core.internal.conversations;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.log4j.Logger;
import de.skuzzle.polly.sdk.AbstractDisposable;
import de.skuzzle.polly.sdk.Conversation;
import de.skuzzle.polly.sdk.ConversationManager;
import de.skuzzle.polly.sdk.IrcManager;
import de.skuzzle.polly.sdk.User;
import de.skuzzle.polly.sdk.eventlistener.MessageEvent;
import de.skuzzle.polly.sdk.eventlistener.MessageListener;
import de.skuzzle.polly.sdk.exceptions.ConversationException;
import de.skuzzle.polly.sdk.exceptions.DisposingException;
import de.skuzzle.polly.sdk.time.Time;
import de.skuzzle.polly.tools.concurrent.ThreadFactoryBuilder;
public class ConversationManagerImpl extends AbstractDisposable implements ConversationManager {
private static Logger logger = Logger.getLogger(
ConversationManagerImpl.class.getName());
private class ConversationImpl extends AbstractDisposable
implements Conversation, MessageListener {
private List<MessageEvent> history;
private BlockingQueue<MessageEvent> readQueue;
protected String channel;
private User user;
private IrcManager ircManager;
private Thread readThread;
private long lastInput;
private int idleTimeout;
public ConversationImpl(IrcManager ircManager, User user, String channel,
int idleTimeout) {
this.ircManager = ircManager;
this.user = user;
this.channel = channel;
this.readQueue = new LinkedBlockingQueue<MessageEvent>();
this.history = new ArrayList<MessageEvent>();
/*
* This is a constructor. The following text therefore is totally stupid o_O
*
* Important:
* Set lastInput before setting idelTimeout. Otherwise it may happen
* that the timeout thread checks for idling right before idleTimout
* has been set. That would cause #isIdle() to return true although
* this conversation hasn't even started!
*/
this.lastInput = Time.currentTimeMillis();
this.idleTimeout = idleTimeout;
}
private void checkClosed() {
if (this.isDisposed()) {
throw new IllegalStateException("ConversationImpl closed"); //$NON-NLS-1$
}
}
@Override
public String readStringLine() throws IOException, InterruptedException {
return this.readLine().getMessage();
}
@Override
public MessageEvent readLine() throws IOException, InterruptedException {
this.checkClosed();
// HACK: use the history list to synchronize on to save an extra attribute
// to lock on.
synchronized (this.history) {
if (this.readThread == null) {
this.readThread = Thread.currentThread();
} else if (!this.readThread.equals(Thread.currentThread())) {
throw new IOException("invalid cross thread read"); //$NON-NLS-1$
}
}
MessageEvent msg = this.readQueue.take();
this.lastInput = Time.currentTimeMillis();
return msg;
}
@Override
public void writeLine(String line) {
this.checkClosed();
this.ircManager.sendMessage(this.channel, line, this);
}
@Override
public List<MessageEvent> getHistory() {
return this.history;
}
public boolean isIdle() {
return Time.currentTimeMillis() - this.lastInput > this.idleTimeout;
}
private synchronized void onMessage(final MessageEvent e) {
assert !this.isDisposed() : "Listener should have been removed before closing"; //$NON-NLS-1$
if (!e.getChannel().equals(this.channel) ||
!e.getUser().getNickName().equals(this.user.getCurrentNickName())) {
return;
}
this.history.add(e);
ConversationManagerImpl.this.convExecutor.execute(new Runnable() {
@Override
public void run() {
try {
ConversationImpl.this.readQueue.put(e);
} catch (InterruptedException e1) {
logger.warn("Interrupted while reading", e1); //$NON-NLS-1$
}
}
});
}
@Override
public void publicMessage(MessageEvent e) {
this.onMessage(e);
}
@Override
public void privateMessage(MessageEvent e) {
this.onMessage(e);
}
@Override
public void noticeMessage(MessageEvent e) {
this.onMessage(e);
}
@Override
public void actionMessage(MessageEvent e) {}
@Override
protected void actualDispose() throws DisposingException {
synchronized (crossMutex) {
this.ircManager.removeMessageListener(this);
ConversationManagerImpl.this.cache.remove(this);
if (this.readThread != null) {
this.readThread.interrupt();
}
this.history.clear();
this.readQueue.clear();
}
}
@Override
public void close() {
try {
if (!this.isDisposed()) {
this.dispose();
}
} catch (DisposingException e) {
logger.error("Error while disposing", e); //$NON-NLS-1$
}
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ConversationImpl other = (ConversationImpl) obj;
if (channel == null) {
if (other.channel != null)
return false;
} else if (!channel.equals(other.channel))
return false;
if (user == null) {
if (other.user != null)
return false;
} else if (!user.getCurrentNickName().equals(other.user.getCurrentNickName()))
return false;
return true;
}
@Override
public String toString() {
return "CONV " + this.channel + ": " + this.user.getCurrentNickName(); //$NON-NLS-1$ //$NON-NLS-2$
}
}
// Object to synchronize on when closing or creating conversations
private static Object crossMutex = new Object();
private ExecutorService convExecutor;
private ScheduledExecutorService timeoutSched;
private List<Conversation> cache;
public ConversationManagerImpl() {
this.convExecutor = Executors.newCachedThreadPool(
new ThreadFactoryBuilder("CONVERSATION_%n%")); //$NON-NLS-1$
this.timeoutSched = Executors.newScheduledThreadPool(1,
new ThreadFactoryBuilder("CONVERSATION_TIMEOUT")); //$NON-NLS-1$
this.cache = Collections.synchronizedList(new LinkedList<Conversation>());
/*
* Schedule a Runnable that iterates through all conversations and checks
* whether they are idle. If so, they are closed.
*/
this.timeoutSched.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
// XXX: This is an unsafe check for emptiness. This avoids unneeded
// synchronizations.
if (cache.isEmpty()) {
return;
}
synchronized (crossMutex) {
for (Conversation conv : cache) {
if (conv.isIdle()) {
logger.warn("Auto closing idling conversation: " + conv); //$NON-NLS-1$
conv.close();
}
}
}
}
}, 1000, 1000, TimeUnit.MILLISECONDS);
}
@Override
public Conversation create(IrcManager ircManager, User user, String channel)
throws ConversationException {
return this.create(ircManager, user, channel, 60);
}
@Override
public Conversation create(IrcManager ircManager, User user, String channel,
int idleTimeout) throws ConversationException {
if (this.checkExisting(user, channel) != null) {
throw new ConversationException("Conversation already active"); //$NON-NLS-1$
}
return this.createInternal(ircManager, user, channel, idleTimeout);
}
private Conversation checkExisting(User user, String channel) {
synchronized (crossMutex) {
Conversation key = new ConversationImpl(null, user, channel, 0);
logger.info("Checking for existing conversation"); //$NON-NLS-1$
int index = this.cache.indexOf(key);
if (index != -1) {
this.cache.get(index);
}
return null;
}
}
private Conversation createInternal(IrcManager ircManager, User user, String channel,
int idleTimeout) {
synchronized (crossMutex) {
ConversationImpl c = new ConversationImpl(
ircManager, user, channel, idleTimeout * 1000); // calc timeout in seconds
ircManager.addMessageListener(c);
this.cache.add(c);
logger.debug("Created new conversation with " + user.getCurrentNickName() + //$NON-NLS-1$
" on channel " + channel); //$NON-NLS-1$
return c;
}
}
@Override
protected void actualDispose() throws DisposingException {
try {
for (Conversation conv : this.cache) {
conv.dispose();
}
this.convExecutor.shutdown();
this.timeoutSched.shutdown();
this.cache.clear();
} catch (Exception e) {
throw new DisposingException(e);
}
}
}