com.seleritycorp.context.ContextApiDemoMain.java Source code

Java tutorial

Introduction

Here is the source code for com.seleritycorp.context.ContextApiDemoMain.java

Source

/*
 * Copyright (C) 2017 Selerity, Inc. (support@seleritycorp.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.seleritycorp.context;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.logging.LogManager;

/**
 * Main entry point for demo of the Selerity Context API.
 */
public class ContextApiDemoMain {
    /**
     * Upper bound for how many entities a DDS query should return.
     */
    private static final int MAX_ENTITIES = 20;

    /**
     * Email address to which send questions. 
     */
    private static final String SUPPORT_EMAIL_ADDRESS = "support@selerityinc.com";

    @Option(name = "-help", usage = "Prints this help page", aliases = { "--help", "-h", "-?" })
    boolean showHelp = false;

    @Option(name = "-apiserver", metaVar = "URL", usage = "The Context API server to connect to")
    String apiServerRootUrl = "context-api-test.seleritycorp.com";

    @Option(name = "-apikey", metaVar = "API_KEY", usage = "The key used for the API connections")
    String apiKey = null;

    @Option(name = "-sessionid", metaVar = "SESSION_ID", usage = "The session id to use for requests")
    String sessionId = "<automatic>";

    @Option(name = "-sources", usage = "Output the entitled sources. In this mode, no query for "
            + "content is made.")
    boolean requestedSourcesQuery = false;

    @Option(name = "-query", metaVar = "QUERY", usage = "Query only for content items of the given "
            + "entity (E.g.: AAPL, Google)")
    String query = "";

    @Option(name = "-exact", usage = "When matching entities, consider only exact matches, instead of"
            + "also partial matches")
    boolean exactMatching = false;

    @Option(name = "-live", usage = "Avoid pausing between updates to get fresh content immediately "
            + "at the cost of increased network traffic and increased API request rate")
    boolean live = false;

    @Option(name = "-v", usage = "Increases the verbosity. Supply multiple times to increase "
            + "verbosity further and further.")
    boolean[] verboseCollector = {};

    @Option(name = "-querytype", metaVar = "TYPE", usage = "The type of query to make. One of\n"
            + "  - FEED            <- use to keep on top of latest\n" + "                       breaking news\n"
            + "  - RECOMMENDATION  <- use to get 'up to speed'\n" + "                       quickly\n"
            + "  - SURVEY\n" + "  - SEARCH\n" + "  - DISCOVERY\n"
            + "(No AUTHOR query, as this demo does not allow to specify authors)")
    String queryType = "FEED";

    @Option(name = "-contributions", metaVar = "MODE", usage = "Allows to expose which factors "
            + "contributed to the score of a found content item. Can be one of:\n"
            + "  - NONE     <- does not show contributions\n" + "  - DIRECT   <- shows direct contributions\n"
            + "  - ALL      <- shows all contributions\n")
    String contributions = "NONE";

    @Option(name = "-pause", metaVar = "SECS", hidden = true, usage = "Pause in seconds between "
            + "content update queries")
    long pauseSeconds = 30;

    private QueryUtils queryUtils;
    private PrintUtils printUtils;

    /**
     * Handles argument parsing.
     *
     * @param args arguments to use as command line arguments.
     */
    @SuppressFBWarnings(value = "DM_EXIT", justification = "Hard exiting on invalid arguments is wanted and desired")
    private void parseArgs(final String[] args) {
        CmdLineParser parser = new CmdLineParser(this);
        try {
            parser.parseArgument(args);
        } catch (CmdLineException e) {
            // Parsing the command line arguments failed. So we print a help screen
            System.err.println(e.getMessage());
            System.err.println();
            printUsage(parser, System.err);
            System.exit(1);
        }

        if (showHelp) {
            printUsage(parser, System.out);
            System.exit(1);
        }

        updateLogging();

        // Making sure we to avoid obviously wrong api keys.
        if (apiKey == null || apiKey.isEmpty()) {
            System.err.println("No usable api key given. Please run the demo command with\n" + "\n"
                    + "  -apikey INSERT-YOUR-API-KEY-HERE\n" + "\n"
                    + "If you have not yet gotten an API key, get in touch with us at " + SUPPORT_EMAIL_ADDRESS);
            System.exit(1);
        }

        // Fixing up apiServerRootUrl to be a proper URL.
        if (!apiServerRootUrl.contains("://")) {
            apiServerRootUrl = "https://" + apiServerRootUrl;
        }

        // Making sure, we're on a good session
        if (sessionId == null || sessionId.isEmpty() || "<automatic>".equals(sessionId)) {
            sessionId = UUID.randomUUID().toString();
        }

        // Fixing up queryType
        switch (queryType) {
        case "FEED":
        case "RECOMMENDATION":
        case "SURVEY":
        case "SEARCH":
        case "DISCOVERY":
            break;
        default:
            System.err.println("Unknown query type " + queryType + ". Switching to FEED.");
            queryType = "FEED";
        }
    }

    /**
     * Updates logging configuration to reflect verbosity settings.  
     */
    private void updateLogging() {
        int verbosity = (verboseCollector == null) ? 0 : verboseCollector.length;

        if (verbosity > 0) {
            LogManager logManager = LogManager.getLogManager();
            String globalLevel = (verbosity <= 2) ? "INFO" : "FINEST";
            String comSeleritycorpLevel = (verbosity <= 1) ? "FINE" : "FINEST";
            String configString = "\n" + ".level=" + globalLevel + "\n" + "com.seleritycorp.level="
                    + comSeleritycorpLevel + "\n" + "handlers=java.util.logging.ConsoleHandler\n"
                    + "java.util.logging.ConsoleHandler.level = FINEST\n"
                    + "java.util.logging.SimpleFormatter.format=%1$tFT%1$tT.%1$tL %4$s %2$s - %5$s%6$s%n";
            InputStream configStream = new ByteArrayInputStream(configString.getBytes(StandardCharsets.UTF_8));

            try {
                logManager.readConfiguration(configStream);
            } catch (SecurityException | IOException e) {
                System.err.print("Failed to update logging config. " + e.toString());
            }
        }
    }

    /**
     * Prints usage information to a given stream.
     * 
     * @param parser The parser for command line options
     * @param printer The stream to print usage information to
     */
    private void printUsage(CmdLineParser parser, PrintStream printer) {
        printer.println("\nUsage:\n");
        printer.print("  run-demo.sh ");
        parser.printSingleLineUsage(printer);
        printer.println();

        printer.println();

        printer.println("The following options are available:");
        printer.println();

        parser.printUsage(printer);

        printer.println("\n" + "\n" + "Examples:\n" + "\n" + "  ./run-demo.sh -apikey REPLACE_WITH_YOUR_API_KEY\n"
                + "\n" + "The above command will query and show the latest breaking news.\n" + "\n" + "\n"
                + "  ./run-demo.sh -apikey REPLACE_WITH_YOUR_API_KEY -querytype RECOMMENDATION\n" + "\n"
                + "The above command will query and show the most important recent content.\n" + "\n" + "\n"
                + "  ./run-demo.sh -apikey REPLACE_WITH_YOUR_API_KEY -query Google\n" + "\n"
                + "The above command will query and show the latest breaking news for items relating\n"
                + "to Google. Even partial matches like the Twitter username @googleventures are\n"
                + "considered.\n" + "\n" + "\n"
                + "  ./run-demo.sh -apikey REPLACE_WITH_YOUR_API_KEY -query Google -exact\n" + "\n"
                + "The above command will query and show the latest breaking news for items relating\n"
                + "to entities that exactly match Google (i.e.: The company itself and the Google\n"
                + "twitter account). It will not consider content that is only relevant to entities\n"
                + "partially matching Google. So for example the Twitter username @googleventures is\n"
                + "not considered.\n" + "\n" + "\n"
                + "  ./run-demo.sh -apikey REPLACE_WITH_YOUR_API_KEY -query Google -contributions ALL\n" + "\n"
                + "The above command will query and show the latest breaking news for items relating\n"
                + "to Google (also considering partial matches), and gives details on which aspects\n"
                + "influenced the scoring.\n" + "\n" + "\n"
                + "  ./run-demo.sh -apikey REPLACE_WITH_YOUR_API_KEY -query Google -live\n" + "\n"
                + "The above command will query and show the latest breaking news for items relating to"
                + "Google (including partial matches) while skipping pauses between updates. That way,"
                + "new items are received right away.\n" + "\n" + "\n"
                + "  ./run-demo.sh -apikey REPLACE_WITH_YOUR_API_KEY -sources\n" + "\n"
                + "The above command will query and show the sources that your API key is entitled for.\n" + "\n"
                + "\n" + "\n" + "Questions/Support:\n" + "\n"
                + "If you run into issues or have questions, please let us know at\n" + SUPPORT_EMAIL_ADDRESS);
    }

    /**
     * Queries for and prints entitled sources.
     *
     * @throws Exception, if any error occurs.
     */
    private void printEntitledSources() throws Exception {
        JsonArray sources = queryUtils.queryEntitledSources();

        if (sources.size() == 0) {
            printUtils.println("API key is not entitled for any source.");
        } else {
            printUtils.println("API key is entitled for the following sources:");
            for (JsonElement source : sources) {
                printUtils.println("* " + source.getAsString());
            }
        }
    }

    /**
     * Resolves the query to entities, dumps information about them, and yields the entity ids. 
     * 
     * <p/>Queries DDS for entities that match the query command line argument. If the command line
     * arguments requested exact matching, exact matching is requested from DDS, otherwise partial
     * matching is performed.
     * 
     * </p>Only up to {@link #MAX_ENTITIES} are fetched from DDS. 
     *
     * @return the entity ids of the entities for the query
     * @throws Exception, if any error occurs
     */
    private Iterable<String> resolveQueryEntityIds() throws Exception {
        List<String> entityIds = new LinkedList<>();

        if (query != null && !query.isEmpty()) {
            String entityQueryMode = exactMatching ? "EXACT_MATCH" : "PARTIAL_MATCH";
            JsonArray results = queryUtils.queryEntities(query, entityQueryMode, MAX_ENTITIES);

            printUtils.println("Query for '" + query + "' will look for those entities:");
            for (JsonElement resultElement : results) {
                JsonObject result = resultElement.getAsJsonObject();
                String id = result.getAsJsonPrimitive("entityID").getAsString();

                printUtils.print("* " + id);
                printUtils.printEntityDetails(result);

                entityIds.add(id);
            }
        }

        return entityIds;
    }

    /**
     * Waits a bit before asking API for updates.
     * 
     * @throws InterrutedException upon Thread interruption.
     */
    private void pauseBeforeUpdate() throws InterruptedException {
        try {
            printUtils.println(
                    "Sleeping for " + pauseSeconds + " seconds before asking for updated " + "content items");
            Thread.sleep(pauseSeconds * 1000);
        } catch (InterruptedException e) {
            throw e;
        }
    }

    /**
     * Performs intitial query, performs update endlessly, and prints new content items.
     * 
     * @throws Exception, if any errors occur
     */
    private void queryWithUpdates() throws Exception {
        // First, we resolve the query to entity ids.
        Iterable<String> entityIds = resolveQueryEntityIds();

        int batchSize = 10; // requesting only up to 10 items per query
        Deque<String> seenContentIds = new LinkedList<>(); // Used to filter seen items from updates
        JsonArray recommendations; // Will hold the recommended content items of the last query
        boolean isInitial = true; // Whether or not to perform an INITIAL or UPDATE query.
        while (true) {
            // Perform the query
            recommendations = queryUtils.queryRecommendations(queryType, isInitial, batchSize, contributions,
                    entityIds);
            isInitial = false; // From now on, all queries are UPDATES

            // Filter down to unseen recommendations    
            List<JsonObject> unseenRecommendations = new ArrayList<>(recommendations.size());
            for (JsonElement recommendationElement : recommendations) {
                JsonObject recommendation = recommendationElement.getAsJsonObject();
                String contentId = recommendation.getAsJsonPrimitive("contentID").getAsString();
                if (contentId != null) {
                    if (!seenContentIds.contains(contentId)) {
                        // Recommendation has not yet been seen
                        unseenRecommendations.add(recommendation);

                        // And we remember that we saw that content item.
                        seenContentIds.addFirst(contentId);

                        // To avoid keeping too many items in memory, we prune old items. 
                        if (seenContentIds.size() > 2 * batchSize) {
                            seenContentIds.removeLast();
                        }
                    }
                }
            }

            printUtils.println("Received " + recommendations.size() + " recommendations. "
                    + unseenRecommendations.size() + " of those have not yet been seen.");

            // Printing unseen recommendations
            for (JsonObject recommendation : unseenRecommendations) {
                printUtils.printRecommendation(recommendation);
            }

            if (!live) {
                // Backing-off a bit before the next query to avoid hammering servers.
                pauseBeforeUpdate();
            }
        }
    }

    /**
     * Runs Context API demo.
     *
     * @param args arguments to use as command line arguments.
     */
    public void doMain(final String[] args) {
        parseArgs(args);

        // Setting up the endpoint config
        RequestUtils requestUtils = new RequestUtils(apiServerRootUrl);

        // Setting up query helpers for the endpoint
        queryUtils = new QueryUtils(apiKey, sessionId, requestUtils);

        // Finally, setting the print helpers
        printUtils = new PrintUtils(queryUtils);

        printUtils.println("Using Selerity Context API server at " + apiServerRootUrl);

        // Now that setup is complete, start the queries. 
        try {
            if (requestedSourcesQuery) {
                printEntitledSources();
            } else {
                queryWithUpdates();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Spawns a demo object and runs it.
     * 
     * @param args arguments to use as command line arguments.
     */
    public static void main(final String[] args) {
        (new ContextApiDemoMain()).doMain(args);
    }
}