From df6567ad1ca03e39148adf5b43542a7e2bf3827f Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Mon, 8 Apr 2019 01:40:28 -0700 Subject: [PATCH] Switched to Alpha Avantage API, created test. --- .../erik/mobibot/modules/StockQuote.java | 141 +++++++++++------- .../erik/mobibot/modules/StockQuoteTest.java | 81 ++++++++++ 2 files changed, 172 insertions(+), 50 deletions(-) create mode 100644 src/test/java/net/thauvin/erik/mobibot/modules/StockQuoteTest.java diff --git a/src/main/java/net/thauvin/erik/mobibot/modules/StockQuote.java b/src/main/java/net/thauvin/erik/mobibot/modules/StockQuote.java index 55d9ab5..0eb67f0 100644 --- a/src/main/java/net/thauvin/erik/mobibot/modules/StockQuote.java +++ b/src/main/java/net/thauvin/erik/mobibot/modules/StockQuote.java @@ -31,13 +31,20 @@ */ package net.thauvin.erik.mobibot.modules; -import com.Ostermiller.util.CSVParser; import net.thauvin.erik.mobibot.Mobibot; +import net.thauvin.erik.mobibot.Utils; +import net.thauvin.erik.mobibot.msg.ErrorMessage; +import net.thauvin.erik.mobibot.msg.Message; +import net.thauvin.erik.mobibot.msg.PrivateMessage; +import net.thauvin.erik.mobibot.msg.PublicMessage; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import org.json.JSONException; +import org.json.JSONObject; import java.io.IOException; +import java.util.ArrayList; /** * The StockQuote module. @@ -47,19 +54,86 @@ import java.io.IOException; * @since 1.0 */ public final class StockQuote extends AbstractModule { + + static final String ALPHAVANTAGE_API_KEY_PROP = "alphavantage-api-key"; + + // The Alpha Advantage URL. + private static final String ALAPHADVANTAGE_URL = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE"; + /** * The quote command. */ - public static final String STOCK_CMD = "stock"; - - // The Yahoo! stock quote URL. - private static final String YAHOO_URL = "http://finance.yahoo.com/d/quotes.csv?&f=snl1d1t1c1oghv&e=.csv&s="; + private static final String STOCK_CMD = "stock"; /** * Creates a new {@link StockQuote} instance. */ public StockQuote() { commands.add(STOCK_CMD); + properties.put(ALPHAVANTAGE_API_KEY_PROP, ""); + } + + + /** + * Get a stock quote. + * + * @param symbol The stock symbol. + * @return The stock quote. + * @throws ModuleException If an errors occurs. + */ + static ArrayList getQuote(String symbol, String apiKey) throws ModuleException { + final String debugMessage = "getQuote(" + symbol + ')'; + final ArrayList messages = new ArrayList<>(); + final OkHttpClient client = new OkHttpClient(); + final Request request = + new Request.Builder().url(ALAPHADVANTAGE_URL + "&symbol=" + symbol + "&apikey=" + apiKey).build(); + + try { + final Response response = client.newCall(request).execute(); + final JSONObject json = new JSONObject(response.body().string()); + + try { + final String info = json.getString("Information"); + if (!info.isEmpty()) { + throw new ModuleException(debugMessage, Utils.unescapeXml(info)); + } + } catch (JSONException ignore) { + // Do nothing. + } + + try { + final String error = json.getString("Error Message"); + if (!error.isEmpty()) { + throw new ModuleException(debugMessage, Utils.unescapeXml(error)); + } + } catch (JSONException ignore) { + // Do nothing. + } + + final JSONObject quote = json.getJSONObject("Global Quote"); + + if (quote.isEmpty()) { + messages.add(new ErrorMessage("Invalid symbol.")); + return messages; + } + + messages.add( + new PublicMessage("Symbol: " + Utils.unescapeXml(quote.getString("01. symbol")))); + messages.add(new PublicMessage(" Price: " + Utils.unescapeXml(quote.getString("05. price")))); + messages.add(new PublicMessage(" Previous: " + + Utils.unescapeXml(quote.getString("08. previous close")))); + messages.add(new PrivateMessage(" Open: " + Utils.unescapeXml(quote.getString("02. open")))); + messages.add(new PrivateMessage(" High: " + Utils.unescapeXml(quote.getString("03. high")))); + messages.add(new PrivateMessage(" Low: " + Utils.unescapeXml(quote.getString("04. low")))); + messages.add(new PrivateMessage(" Volume: " + Utils.unescapeXml(quote.getString("06. volume")))); + messages.add(new PrivateMessage(" Latest: " + + Utils.unescapeXml(quote.getString("07. latest trading day")))); + messages.add(new PrivateMessage(" Change: " + Utils.unescapeXml(quote.getString("09. change")) + + " [" + Utils.unescapeXml(quote.getString("10. change percent")) + ']')); + } catch (IOException e) { + throw new ModuleException(debugMessage, "An error has occurred retrieving a stock quote.", e); + } + return messages; } /** @@ -80,60 +154,27 @@ public final class StockQuote extends AbstractModule { @Override public void helpResponse(final Mobibot bot, final String sender, final String args, final boolean isPrivate) { bot.send(sender, "To retrieve a stock quote:"); - bot.send(sender, bot.helpIndent(bot.getNick() + ": " + STOCK_CMD + " ")); + bot.send(sender, bot.helpIndent(bot.getNick() + ": " + STOCK_CMD + " ")); } /** - * Returns the specified stock quote from Yahoo! + * Returns the specified stock quote from Alpha Advantage. */ private void run(final Mobibot bot, final String sender, final String symbol) { try { - final OkHttpClient client = new OkHttpClient(); - final Request request = new Request.Builder().url(YAHOO_URL + symbol.toUpperCase()).build(); - final Response response = client.newCall(request).execute(); - - final String[][] lines = CSVParser.parse(response.body().string()); - - if (lines.length > 0) { - final String[] quote = lines[0]; - - if (quote.length > 0) { - if ((quote.length > 3) && (!"N/A".equalsIgnoreCase(quote[3]))) { - bot.send(bot.getChannel(), "Symbol: " + quote[0] + " [" + quote[1] + ']'); - - if (quote.length > 5) { - bot.send(bot.getChannel(), "Last Trade: " + quote[2] + " (" + quote[5] + ')'); - } else { - bot.send(bot.getChannel(), "Last Trade: " + quote[2]); - } - - if (quote.length > 4) { - bot.send(sender, "Time: " + quote[3] + ' ' + quote[4]); - } - - if (quote.length > 6 && !"N/A".equalsIgnoreCase(quote[6])) { - bot.send(sender, "Open: " + quote[6]); - } - - if (quote.length > 7 && !"N/A".equalsIgnoreCase(quote[7]) && !"N/A".equalsIgnoreCase(quote[8])) { - bot.send(sender, "Day's Range: " + quote[7] + " - " + quote[8]); - } - - if (quote.length > 9 && !"0".equalsIgnoreCase(quote[9])) { - bot.send(sender, "Volume: " + quote[9]); - } - } else { - bot.send(sender, "Invalid ticker symbol."); - } + final ArrayList messages = + getQuote(symbol, properties.get(ALPHAVANTAGE_API_KEY_PROP)); + for (Message msg : messages) { + if (msg.isPrivate() || msg.isError()) { + bot.send(sender, msg.getMessage()); } else { - bot.send(sender, "No values returned."); + bot.send(bot.getChannel(), msg.getMessage()); } - } else { - bot.send(sender, "No data returned."); } - } catch (NullPointerException | IOException e) { - bot.getLogger().debug("Unable to retrieve stock quote for: " + symbol, e); - bot.send(sender, "An error has occurred retrieving a stock quote: " + e.getMessage()); + + } catch (ModuleException e) { + bot.getLogger().warn(e.getDebugMessage(), e); + bot.send(sender, "An error has occurred retrieving a stock quote."); } } } diff --git a/src/test/java/net/thauvin/erik/mobibot/modules/StockQuoteTest.java b/src/test/java/net/thauvin/erik/mobibot/modules/StockQuoteTest.java new file mode 100644 index 0000000..d3c9a69 --- /dev/null +++ b/src/test/java/net/thauvin/erik/mobibot/modules/StockQuoteTest.java @@ -0,0 +1,81 @@ +/* + * StockQuoteTest.java + * + * Copyright (c) 2004-2019, Erik C. Thauvin (erik@thauvin.net) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules; + +import net.thauvin.erik.mobibot.msg.Message; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The StockQuoteTest class. + * + * @author Erik C. Thauvin + * @created 2019-04-07 + * @since 1.0 + */ +public class StockQuoteTest { + @Test + public void testGetQuote() throws ModuleException, IOException { + String apiKey = System.getenv("ALPHA_ADVANTAGE"); + if (apiKey == null) { + final Path localProps = Paths.get("local.properties"); + if (Files.exists(localProps)) { + try (final InputStream stream = Files.newInputStream(localProps)) { + final Properties p = new Properties(); + p.load(stream); + apiKey = p.getProperty(StockQuote.ALPHAVANTAGE_API_KEY_PROP); + } + } + } + + ArrayList messages = StockQuote.getQuote("AAPL", apiKey); + assertThat(messages).as("response not empty").isNotEmpty(); + assertThat(messages.get(0).getMessage()).as("same stock symbol").contains("AAPL"); + + messages = StockQuote.getQuote("012", apiKey); + assertThat(messages.get(0).isError()).as("invalid symbol error").isTrue(); + } + + @Test + public void testStockQuoteImpl() { + AbstractModuleTest.testAbstractModule(new StockQuote()); + } +}