Java tutorial
/* * Copyright (c) <2013> <Jim Johnson jimj@jimj.net> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package net.jimj.automaton.commands; import com.mongodb.BasicDBObject; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.DBObject; import net.jimj.automaton.events.DataStoredEvent; import net.jimj.automaton.events.ReplyEvent; import net.jimj.automaton.model.User; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; public class QuoteCommand extends Command { protected static final String QUOTE_NICK = "nick"; protected static final String QUOTE_QUOTE = "quote"; protected static final String QUOTE_NETWORK = "network"; private static final Pattern TIMESTAMP = Pattern.compile(".*([0-9]{1,2}:[0-9][0-9]).*"); private static final String NO_QUOTES = "No quotes found."; private DBCollection quotes; private static final Logger LOGGER = LoggerFactory.getLogger(QuoteCommand.class); public QuoteCommand(DBCollection quotes) { this.quotes = quotes; } @Override public String getCommandName() { return "quote"; } @Override public void execute(User user, String args) { if (args == null) { args = ""; } //Is this a search? //e.g. quote foo /search string/ //to find a quote by 'foo' containing 'search string' int searchStart = args.indexOf("/"); int searchEnd = args.lastIndexOf("/"); String[] argParts = ArgUtil.split(args); //If there's more than 1 argument, and no search term. if (argParts.length > 1 && searchStart == searchEnd) { LOGGER.debug(String.format("%d > 1 && %d == %d", argParts.length, searchStart, searchEnd)); if (storeQuote(args, argParts)) { addEvent(new DataStoredEvent(user, "quote")); } else { addEvent(new ReplyEvent(user, "I couldn't parse the quote correctly.")); } } else { addEvent(new ReplyEvent(user, getQuote(args))); } } protected boolean storeQuote(String args, String[] argParts) { boolean stored = false; //Try to determine the start of the quote based on the assumption that //pasted quotes will contain nicknames surrounded by some sort of 'special' character. int quoteStart = findQuoteStart(argParts); if (quoteStart == -1) { LOGGER.warn("Couldn't find nick in quote " + args); } else { Set<String> nicks = findNickCandidates(argParts); //Quote defaults to entire string String quote = args; if (quoteStart > 0) { //Add on any 'passed in' nicks for the quote as well //i.e. .quote foo <foobar> my quote //would get the nicks ["foo", "foobar"] associated w/ it. for (int i = 0; i < quoteStart; i++) { nicks.add(argParts[i]); } //Cut out the passed in nicks from the actual quote. quote = ArgUtil.squash(argParts, quoteStart); } BasicDBObject quoteObj = new BasicDBObject(QUOTE_NICK, nicks); quoteObj.append(QUOTE_QUOTE, quote); quoteObj.append(QUOTE_NETWORK, "slashnet"); quoteObj.append("QUOTE_VERSION", "1"); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Storing quote: " + quoteObj); } quotes.save(quoteObj); stored = true; } return stored; } protected int findQuoteStart(String[] argParts) { for (int i = 0; i < argParts.length; i++) { String argPart = argParts[i]; //Does the argPart start w/ a legal char? If not, probably a nickname. if (isMetaWord(argPart)) { return i; } } return -1; } protected HashSet<String> findNickCandidates(String[] argParts) { HashSet<String> candidates = new HashSet<>(); for (int i = 0; i < argParts.length; i++) { String argPart = argParts[i]; //String splitting happens on a space //argParts could show up as ["<", "nick>"] due to irc client formatting. //If this is the case, append the next argPart onto the current string if (argPart.length() == 1) { argPart += argParts[i + 1]; } if (isMetaWord(argPart)) { String candidate = getNormalizedNick(argPart); if (!looksLikeTimestamp(candidate)) { candidates.add(candidate); } } } return candidates; } protected boolean looksLikeTimestamp(String str) { return TIMESTAMP.matcher(str).matches(); } protected String getNormalizedNick(String nick) { int startNick = 0; int endNick = nick.length(); int j = nick.length(); for (int i = 0; i < nick.length(); i++) { if (i >= j) { break; } if (!legalChar(nick.charAt(i))) { startNick = i + 1; } j--; if (!legalChar(nick.charAt(j))) { endNick = j; } } return nick.substring(startNick, endNick).toLowerCase(); } protected boolean isMetaWord(String word) { if (StringUtils.isBlank(word)) { return false; } return !(Character.isAlphabetic(word.codePointAt(0)) || Character.isDigit(word.codePointAt(0))); } protected boolean legalChar(char c) { switch (c) { case '<': case '@': case '+': case '>': case ' ': return false; default: return true; } } protected String getQuote(String arg) { BasicDBObject query = buildQuoteSearch(arg); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Searching for quotes that match " + query); } BasicDBObject keysWanted = new BasicDBObject("_id", "1"); List<DBObject> quoteIds = quotes.find(query, keysWanted).toArray(); return getRandomQuote(quoteIds); } protected BasicDBObject buildQuoteSearch(String arg) { //todo: replace w/ real Network concept. BasicDBObject query = new BasicDBObject(QUOTE_NETWORK, "slashnet"); if (arg != null) { //Regex search int searchStart = arg.indexOf("/"); int searchEnd = arg.lastIndexOf("/"); if (searchStart != -1 && searchStart < searchEnd) { //cut out the included / characters. String quoteSearch = arg.substring(searchStart + 1, searchEnd); if (quoteSearch.length() > 0) { query.append(QUOTE_QUOTE, Pattern.compile(quoteSearch, Pattern.CASE_INSENSITIVE)); } } String nick = arg; if (searchStart != -1) { nick = arg.substring(0, searchStart).trim(); } if (!StringUtils.isBlank(nick)) { query.append(QUOTE_NICK, nick); } } LOGGER.debug(query.toString()); return query; } protected String getRandomQuote(List<DBObject> quoteIds) { if (quoteIds == null) { return NO_QUOTES; } int totalQuotes = quoteIds.size(); if (totalQuotes < 1) { return NO_QUOTES; } else { int quoteNum = RANDOM.nextInt(totalQuotes); Object quoteId = quoteIds.get(quoteNum).get("_id"); DBCursor quoteCur = quotes.find(new BasicDBObject("_id", quoteId), new BasicDBObject(QUOTE_QUOTE, 1)); DBObject quoteObj = quoteCur.next(); return (String) quoteObj.get(QUOTE_QUOTE); } } @Override public void help(User user) { addEvent(new ReplyEvent(user, "quote [tag] <quote> - stores a quote.")); addEvent(new ReplyEvent(user, "quote [tag] [/search text/] - displays a random quote.")); addEvent(new ReplyEvent(user, "To store a quote, you must include the nickname wrapped in angle brackets as " + "part of the quote. Each space delimited text included before the first angle bracket " + "(excluding timestamps) will be included as a tag. All nicknames wrapped in angle brackets will also " + "be included as tags.")); addEvent(new ReplyEvent(user, "If a tag is included when getting a random quote, quotes considered for " + "display will be restricted to those with the tag. This option can be combined with search text.")); addEvent(new ReplyEvent(user, "If search text is included when getting a random quote, quotes considered for " + "display will be restricted to those that contain the search text. Search text is denoted by the " + "surrounded slashes. This option can be combined with a tag.")); } }