package de.skuzzle.polly.core.internal.commands;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.log4j.Logger;
import de.skuzzle.jeve.EventProvider;
import de.skuzzle.polly.core.parser.Evaluator;
import de.skuzzle.polly.core.parser.InputScanner;
import de.skuzzle.polly.core.parser.ParseException;
import de.skuzzle.polly.core.parser.ParserProperties;
import de.skuzzle.polly.core.parser.Position;
import de.skuzzle.polly.core.parser.Token;
import de.skuzzle.polly.core.parser.TokenType;
import de.skuzzle.polly.core.parser.ast.Identifier;
import de.skuzzle.polly.core.parser.ast.Root;
import de.skuzzle.polly.core.parser.ast.declarations.Declaration;
import de.skuzzle.polly.core.parser.ast.declarations.Namespace;
import de.skuzzle.polly.core.parser.ast.declarations.types.Type;
import de.skuzzle.polly.core.parser.ast.directives.DelayDirective;
import de.skuzzle.polly.core.parser.ast.expressions.Expression;
import de.skuzzle.polly.core.parser.ast.expressions.literals.ChannelLiteral;
import de.skuzzle.polly.core.parser.ast.expressions.literals.DateLiteral;
import de.skuzzle.polly.core.parser.ast.expressions.literals.ListLiteral;
import de.skuzzle.polly.core.parser.ast.expressions.literals.Literal;
import de.skuzzle.polly.core.parser.ast.expressions.literals.UserLiteral;
import de.skuzzle.polly.core.parser.ast.visitor.ASTTraversalException;
import de.skuzzle.polly.core.parser.problems.SimpleProblemReporter;
import de.skuzzle.polly.core.util.MillisecondStopwatch;
import de.skuzzle.polly.core.util.Stopwatch;
import de.skuzzle.polly.core.util.TypeMapper;
import de.skuzzle.polly.sdk.AbstractDisposable;
import de.skuzzle.polly.sdk.Command;
import de.skuzzle.polly.sdk.CommandHistoryEntry;
import de.skuzzle.polly.sdk.CommandManager;
import de.skuzzle.polly.sdk.Configuration;
import de.skuzzle.polly.sdk.IrcManager;
import de.skuzzle.polly.sdk.Signature;
import de.skuzzle.polly.sdk.Types;
import de.skuzzle.polly.sdk.User;
import de.skuzzle.polly.sdk.eventlistener.MessageSendListener;
import de.skuzzle.polly.sdk.eventlistener.OwnMessageEvent;
import de.skuzzle.polly.sdk.exceptions.CommandException;
import de.skuzzle.polly.sdk.exceptions.DisposingException;
import de.skuzzle.polly.sdk.exceptions.DuplicatedSignatureException;
import de.skuzzle.polly.sdk.exceptions.InsufficientRightsException;
import de.skuzzle.polly.sdk.exceptions.UnknownCommandException;
import de.skuzzle.polly.sdk.exceptions.UnknownSignatureException;
import de.skuzzle.polly.sdk.time.DateUtils;
public class CommandManagerImpl extends AbstractDisposable
implements CommandManager {
private class HistoryEntryImpl implements CommandHistoryEntry {
private Command command;
private Signature Signature;
private String name;
public HistoryEntryImpl(Command command, Signature signature, String name) {
this.command = command;
this.Signature = signature;
this.name = name;
}
@Override
public Command getCommand() {
return this.command;
}
@Override
public Signature getSignature() {
return this.Signature;
}
@Override
public String getExecuterName() {
return this.name;
}
}
private class ReinterpretListener implements MessageSendListener {
private final User executor;
private final Object source;
private boolean done = false;
public ReinterpretListener(Object source, User executor) {
this.source = source;
this.executor = executor;
}
@Override
public boolean workDone(EventProvider parent) {
return this.done;
}
@Override
public void messageSent(OwnMessageEvent e) {
if (e.getMessageSource() != this.source) {
return;
}
try {
executeString(e.getMessage(), e.getChannel(), e.inQuery(),
this.executor, e.getSource());
this.done = true;
} catch (UnsupportedEncodingException | UnknownSignatureException
| InsufficientRightsException | CommandException
| UnknownCommandException e1) {
e.getSource().sendMessage(e.getChannel(), e.getMessage(), this);
}
}
}
private final static String[] DAYS = {"montag", "dienstag", "mittwoch",
"donnerstag", "freitag", "samstag", "sonntag"};
private static Logger logger = Logger.getLogger(CommandManagerImpl.class.getName());
private Map<String, Command> commands;
private Set<String> ignoredCommands;
private String encodingName;
private final Timer delayService;
/**
* Command history. Key: channel, value: the last command executed on that channel
*/
private Map<String, CommandHistoryEntry> cmdHistory;
public CommandManagerImpl(String encoding, Configuration config) {
this.encodingName = encoding;
this.commands = new HashMap<String, Command>();
this.ignoredCommands = new HashSet<String>(
config.readStringList(Configuration.IGNORED_COMMANDS));
this.cmdHistory = new HashMap<String, CommandHistoryEntry>();
this.delayService = new Timer(true);
}
@Override
public synchronized void registerCommand(Command cmd)
throws DuplicatedSignatureException {
this.registerCommand(cmd.getCommandName(), cmd);
}
@Override
public synchronized void registerCommand(String as, Command cmd)
throws DuplicatedSignatureException {
if (as.length() < ParserProperties.getInt(ParserProperties.COMMAND_MIN_LENGTH)) {
throw new IllegalArgumentException(
"Too short commandname: " + as);
}
if (this.ignoredCommands.contains(as)) {
logger.warn("Ignoring command '" + as + "'.");
return;
}
if (this.isRegistered(as)) {
throw new DuplicatedSignatureException(as);
}
this.commands.put(as.toLowerCase(), cmd);
logger.debug("Command '" + as + "' with " +
cmd.getSignatures().size() + " signatures successfuly registered");
}
@Override
public synchronized void unregisterCommand(Command command) {
Command cmd;
try {
cmd = this.getCommand(command.getCommandName());
this.commands.remove(cmd.getCommandName().toLowerCase());
logger.debug("Unregistered command: " + command.getCommandName());
} catch (UnknownCommandException e) {
logger.debug("Tried to unregister nonexistent command", e);
}
}
@Override
public List<Command> getRegisteredCommands() {
return Collections.unmodifiableList(
new ArrayList<Command>(this.commands.values()));
}
@Override
public boolean isRegistered(Command cmd) {
return this.isRegistered(cmd.getCommandName());
}
@Override
public boolean isRegistered(String name) {
return this.commands.containsKey(name.toLowerCase());
}
@Override
public synchronized Command getCommand(Signature signature)
throws UnknownSignatureException, UnknownCommandException {
logger.debug("Looking for '" + signature.toString() + "'.");
Command cmd = this.getCommand(signature.getName());
boolean found = false;
if (signature.equals(cmd.getHelpSignature0()) || signature.equals(cmd.getHelpSignature1())) {
found = true;
} else {
for (Signature formal : cmd.getSignatures()) {
if (formal.equals(signature)) {
found = true;
signature.setId(formal.getId());
logger.debug("Signature found. Formal id is " + signature.getId());
break;
}
}
}
if (!found) {
throw new UnknownSignatureException(signature);
}
assert cmd != null;
return cmd;
}
@Override
public Command getCommand(String name) throws UnknownCommandException {
Command cmd = this.commands.get(name.toLowerCase());
if (cmd == null) {
throw new UnknownCommandException(name);
}
return cmd;
}
@Override
public boolean executeString(String input, final String channel, final boolean inQuery,
final User executor, final IrcManager ircManager)
throws UnsupportedEncodingException,
UnknownSignatureException, InsufficientRightsException,
CommandException, UnknownCommandException {
Stopwatch watch = new MillisecondStopwatch();
watch.start();
Root root = null;
try {
Map<String, Types> constants = this.getCommandConstants(input);
final Namespace rootNs = Namespace.forName(executor.getName());
final Namespace workingNs = rootNs.enter();
this.createContext(channel, executor, ircManager, constants, workingNs);
root = this.parseMessage(input, rootNs, workingNs);
} catch (ParseException e) {
// HACK: wrap exception into command exception, as ParseException is not
// available in the sdk
throw new CommandException(e.getMessage(), e);
} catch (ASTTraversalException e) {
// HACK: dito
throw new CommandException(e.getMessage(), e);
}
if (root == null || root.hasProblems()) {
return false;
}
final Signature sig = this.createSignature(root);
final Command cmd = this.getCommand(sig);
try {
final boolean reinterpret = root.getDirectives().containsKey(
TokenType.REINTERPRET);
if (reinterpret) {
final ReinterpretListener l = new ReinterpretListener(cmd, executor);
ircManager.addMessageSendListener(l);
}
if (root.getDirectives().containsKey(TokenType.DELAY)) {
final DelayDirective delay = (DelayDirective)
root.getDirectives().get(TokenType.DELAY);
final Date target = delay.getResult().getValue();
logger.debug("Delaying execution of '" + cmd + "' until " + target);
this.delayService.schedule(new TimerTask() {
@Override
public void run() {
logger.debug("Executing '" + cmd + "' on channel " +
channel);
try {
cmd.doExecute(executor, channel, inQuery, sig);
synchronized (cmdHistory) {
if (cmd.trackInHistory()) {
cmdHistory.put(channel, new HistoryEntryImpl(cmd, sig,
executor.getCurrentNickName()));
}
}
} catch (InsufficientRightsException | CommandException e) {
ircManager.sendMessage(channel, e.getMessage(), this);
}
}
}, target);
} else {
logger.debug("Executing '" + cmd + "' on channel " +
channel);
cmd.doExecute(executor, channel, inQuery, sig);
synchronized (cmdHistory) {
if (cmd.trackInHistory()) {
cmdHistory.put(channel, new HistoryEntryImpl(cmd, sig,
executor.getCurrentNickName()));
}
}
}
} finally {
watch.stop();
logger.trace("Execution time: " + watch.getDifference() + "ms");
}
return true;
}
@Override
public CommandHistoryEntry getLastCommand(String channel) {
synchronized (this.cmdHistory) {
return this.cmdHistory.get(channel);
}
}
private Root parseMessage(String message, Namespace rootNs, Namespace workingNs)
throws UnsupportedEncodingException, ASTTraversalException {
final Stopwatch watch = new MillisecondStopwatch();
watch.start();
final Evaluator eval = new Evaluator(message.trim(), this.encodingName,
new SimpleProblemReporter());
eval.evaluate(rootNs, workingNs);
watch.stop();
if (eval.getRoot() != null) {
logger.trace("Parsing time: " + watch.getDifference() + "ms");
}
final Root root = eval.getRoot();
if (eval.errorOccurred()) {
throw eval.getLastError();
}
return root;
}
private void createContext(String channel, User user, IrcManager ircManager,
Map<String, Types> constants, Namespace d) throws ASTTraversalException {
List<Expression> channels = new ArrayList<Expression>();
for (String chan : ircManager.getChannels()) {
channels.add(new ChannelLiteral(Position.NONE, chan));
}
// ISSUE: 0000008
List<Expression> users = new ArrayList<Expression>();
for (String u : ircManager.getChannelUser(channel)) {
users.add(new UserLiteral(Position.NONE, u));
}
d.declare(new Declaration(Position.NONE, new Identifier("me"),
new UserLiteral(Position.NONE, user.getCurrentNickName())));
d.declare(new Declaration(Position.NONE, new Identifier("here"),
new ChannelLiteral(Position.NONE, channel)));
//d.declare(new Declaration(Position.NONE, new Identifier("all"),
// new ListLiteral(Position.NONE, channels, Type.CHANNEL)));
d.declare(new Declaration(Position.NONE, new Identifier("each"),
new ListLiteral(Position.NONE, users, Type.USER)));
int m = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
d.declare(new Declaration(Position.NONE, new Identifier("morgen"),
new DateLiteral(Position.NONE, DateUtils.getDayDate(m + 1))));
d.declare(new Declaration(Position.NONE, new Identifier("übermorgen"),
new DateLiteral(Position.NONE, DateUtils.getDayDate(m + 2))));
int start = Calendar.MONDAY;
for (String day : DAYS) {
d.declare(new Declaration(Position.NONE, new Identifier(day),
new DateLiteral(Position.NONE, DateUtils.getDayDate(start++))));
}
/*logger.trace(" me := " + user.getCurrentNickName());
logger.trace(" here := " + channel);
logger.trace(" all := " + channels.toString());
logger.trace(" each := " + users);
logger.trace(" morgen := " + tmp.getTime());*/
if (constants != null && !constants.isEmpty()) {
logger.trace("Command-specific constant names:");
for (Entry<String, Types> e : constants.entrySet()) {
Literal l = TypeMapper.typesToLiteral(e.getValue());
d.declare(new Declaration(Position.NONE,
new Identifier(e.getKey()), l));
logger.trace(" " + e.getKey() + " := " + l.toString());
}
}
}
private Signature createSignature(Root root) throws UnknownSignatureException {
List<Types> parameters = new ArrayList<Types>(root.getResults().size());
for (Literal lit : root.getResults()) {
parameters.add(TypeMapper.literalToTypes(lit));
}
return new Signature(root.getCommand().getId(), -1, parameters);
}
private Map<String, Types> getCommandConstants(String input) {
try {
InputScanner s = new InputScanner(input);
if (!s.match(TokenType.COLON)) {
return null;
}
Token id = s.lookAhead();
if (!s.match(TokenType.IDENTIFIER)) {
return null;
}
Command cmd = this.getCommand(id.getStringValue());
logger.trace("Renewing command-specific constants");
final Map<String, Types> constants = new HashMap<>();
cmd.renewConstants(constants);
return constants;
} catch (Exception e) {
return null;
}
}
@Override
protected void actualDispose() throws DisposingException {
this.delayService.cancel();
}
}