com.ibm.watson.movieapp.dialog.rest.WDSBlueMixProxyResource.java Source code

Java tutorial

Introduction

Here is the source code for com.ibm.watson.movieapp.dialog.rest.WDSBlueMixProxyResource.java

Source

/* Copyright IBM Corp. 2015
 *
 * 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.ibm.watson.movieapp.dialog.rest;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpException;
import org.apache.http.client.ClientProtocolException;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.ibm.watson.developer_cloud.dialog.v1.DialogService;
import com.ibm.watson.developer_cloud.dialog.v1.model.Conversation;
import com.ibm.watson.developer_cloud.natural_language_classifier.v1.NaturalLanguageClassifier;
import com.ibm.watson.developer_cloud.natural_language_classifier.v1.model.Classification;
import com.ibm.watson.developer_cloud.natural_language_classifier.v1.model.ClassifiedClass;
import com.ibm.watson.movieapp.dialog.exception.WatsonTheatersException;
import com.ibm.watson.movieapp.dialog.payload.MoviePayload;
import com.ibm.watson.movieapp.dialog.payload.ServerErrorPayload;
import com.ibm.watson.movieapp.dialog.payload.WDSConversationPayload;

/**
 * <p>
 * Proxy class to communicate with Watson Dialog Service
 * (WDS) to generate chat responses to the user input.
 * </p>
 * <p>
 * There are multiple JAX-RS entry points to this class, depending on the task to be performed. eg.: /postConversation to post the user input to the WDS
 * service and get a response.
 * </p>
 * <p>
 * In addition, there are various helper methods to parse response text, etc.
 * </p>
 */

@Path("/bluemix")
public class WDSBlueMixProxyResource {
    private static DialogService dialogService = new DialogService();
    private static NaturalLanguageClassifier nlcService = new NaturalLanguageClassifier();
    private static String dialog_id;
    private static String classifier_id;
    private static String personalized_prompt_movie_selected = "USER CLICKS BOX"; //$NON-NLS-1$
    private static String personalized_prompt_movies_returned = "UPDATE NUM_MOVIES"; //$NON-NLS-1$
    private static String personalized_prompt_current_index = "UPDATE CURRENT_INDEX"; //$NON-NLS-1$

    static {
        dialog_id = System.getenv("DIALOG_ID");
        classifier_id = System.getenv("CLASSIFIER_ID");
    }

    /**
     * Checks and extracts movie parameters sent by WDS
     * <p>
     * This will extract movie parameters sent by WDS (in the response text) when they're sent.
     * </p>
     * 
     * @param wdsResponseText the textual part of the response sent by WDS
     * @return the JsonObject containing the response from WDS as well as the parameters and their values sent by WDS.
     */
    public JsonObject matchSearchNowPattern(String wdsResponseText) {
        JsonObject result = new JsonObject();
        // If WDS wants us to search themoviedb then it will return a JSON
        // payload within the response. Quickly check the response for a specific token
        int idx = wdsResponseText.toLowerCase().indexOf("{search_now:"); //$NON-NLS-1$
        if (idx != -1) {
            // token exists, parse out some extra chars from dialog.
            String json = wdsResponseText.substring(idx).trim();
            wdsResponseText = wdsResponseText.substring(0, idx - 1).trim();
            if (json.startsWith("\"")) { //$NON-NLS-1$
                json = json.substring(0);
            }
            if (json.endsWith("\"")) { //$NON-NLS-1$
                json = json.substring(0, json.length() - 1);
            }
            JsonElement element = new JsonParser().parse(json);
            result.add("Params", element.getAsJsonObject()); //$NON-NLS-1$
        }
        result.addProperty("WDSMessage", wdsResponseText); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        return result;
    }

    /**
     * Makes chat conversation with WDS
     * <p>
     * This makes chat conversation with WDS for the provided client id and conversation id, against the user input provided.
     * </p>
     * <p>
     * When WDS has collected all the required movie preferences, it sends a bunch of movie parameters embedded in the text response and signals to discover
     * movies from themoviedb.org. There may be the following kinds of discover movie calls:
     * <ul>
     * <li>New search: First time searching for the given set of parameters
     * <li>Repeat search: Repeat the search with the same parameters (just re-display the results)
     * <li>Previous search: Display the results on the previous page
     * <li>Next search: Display the results on the next page
     * </ul>
     * Depending on the kind of call, profile variables are set in WDS and personalized prompts are retrieved to be sent back to the UI in the payload.
     * </p>
     * 
     * @param conversationId the conversation id for the client id specified
     * @param clientId the client id for the session
     * @param input the user's input
     * @return a response containing either of these two entities- {@code WDSConversationPayload} or {@code ServerErrorPayload}
     */
    @GET
    @Path("/postConversation")
    @Produces(MediaType.APPLICATION_JSON)
    public Response postConversation(@QueryParam("conversationId") String conversationId,
            @QueryParam("clientId") String clientId, @QueryParam("input") String input) {
        long lStartTime = System.nanoTime();
        long lEndTime, difference;
        String errorMessage = null, issue = null;
        String wdsMessage = null;
        JsonObject processedText = null;
        if (input == null || input.trim().isEmpty()) {
            errorMessage = Messages.getString("WDSBlueMixProxyResource.SPECIFY_INPUT"); //$NON-NLS-1$
            issue = Messages.getString("WDSBlueMixProxyResource.EMPTY_QUESTION"); //$NON-NLS-1$
            UtilityFunctions.logger.error(issue);
            return Response.serverError().entity(new ServerErrorPayload(errorMessage, issue)).build();
        }
        try {

            // 1.Get all the class info from NLC and set appropriate profile variables.
            List<ClassifiedClass> classInfo = null;
            if (nlcService != null) {
                if (UtilityFunctions.logger.isTraceEnabled()) {
                    UtilityFunctions.logger.trace(Messages.getString("WDSBlueMixProxyResource.NLC_SERVICE")); //$NON-NLS-1$
                }
                // Send utterance to NLC to get user intent
                Classification classification = nlcService.classify(classifier_id, input);
                classInfo = classification.getClasses();
                // Set classification profile variables for WDS.
                Map<String, String> profile = new HashMap<>();
                profile.put("Class1", classInfo.get(0).getName());
                profile.put("Class1_Confidence", Double.toString(classInfo.get(0).getConfidence()));
                profile.put("Class2", classInfo.get(1).getName());
                profile.put("Class2_Confidence", Double.toString(classInfo.get(1).getConfidence()));
                dialogService.updateProfile(dialog_id, new Integer(clientId), profile);
            }

            // 2. Send original utterance to WDS
            Map<String, Object> converseParams = new HashMap<String, Object>();
            converseParams.put("dialog_id", dialog_id);
            converseParams.put("client_id", Integer.parseInt(clientId));
            converseParams.put("conversation_id", Integer.parseInt(conversationId));
            converseParams.put("input", input);
            Conversation conversation = dialogService.converse(converseParams);
            wdsMessage = StringUtils.join(conversation.getResponse(), " ");
            processedText = matchSearchNowPattern(wdsMessage);
            WDSConversationPayload conversationPayload = new WDSConversationPayload();
            if (!processedText.has("Params")) { //$NON-NLS-1$
                // We do not have enough info to search the movie db, go back to the user for more info.
                conversationPayload.setClientId(clientId); //$NON-NLS-1$
                conversationPayload.setConversationId(clientId); //$NON-NLS-1$
                conversationPayload.setInput(input); //$NON-NLS-1$
                conversationPayload.setWdsResponse(processedText.get("WDSMessage").getAsString()); //$NON-NLS-1$
                if (UtilityFunctions.logger.isTraceEnabled()) {
                    // Log the execution time.
                    lEndTime = System.nanoTime();
                    difference = lEndTime - lStartTime;
                    UtilityFunctions.logger.trace("Throughput: " + difference / 1000000 + "ms.");
                }
                return Response.ok(conversationPayload, MediaType.APPLICATION_JSON_TYPE).build();
            } else {
                // Dialog says we have enough info to proceed with a search of themoviedb..
                // Find out search variables.
                JsonObject paramsObj = processedText.getAsJsonObject("Params"); //$NON-NLS-1$
                boolean newSearch = false, prevSearch = false, nextSearch = false, repeatSearch = false;
                String page = paramsObj.get("Page").getAsString(); //$NON-NLS-1$
                switch (page) {
                case "new": //$NON-NLS-1$
                    newSearch = true;
                    break;
                case "next": //$NON-NLS-1$
                    nextSearch = true;
                    break;
                case "previous": //$NON-NLS-1$
                    prevSearch = true;
                    break;
                case "repeat": //$NON-NLS-1$
                    repeatSearch = true;
                    break;
                default:
                    errorMessage = Messages.getString("WDSBlueMixProxyResource.DIALOG_UNDERSTAND_FAIL"); //$NON-NLS-1$
                    issue = Messages.getString("WDSBlueMixProxyResource.PAGE_TYPE_NOT_UNDERSTOOD"); //$NON-NLS-1$
                    UtilityFunctions.logger.error(issue);
                }

                if (UtilityFunctions.logger.isTraceEnabled()) {
                    UtilityFunctions.logger
                            .trace(Messages.getString("WDSBlueMixProxyResource.WDS_RESPONSE") + paramsObj); //$NON-NLS-1$
                }
                String prompt;
                Integer currentIndex = Integer.parseInt(paramsObj.get("Index").getAsString()); //$NON-NLS-1$
                Integer numMovies = 0;
                Integer totalPages = 0;
                boolean tmdbCallNeeded = true;
                Map<String, String> profile;
                if (paramsObj.has("Total_Movies")) {
                    numMovies = Integer.parseInt(paramsObj.get("Total_Movies").getAsString());
                    totalPages = Integer.parseInt(paramsObj.get("Total_Pages").getAsString());
                    // If the user wishes to "go back" when the first set of results is displayed or
                    // "show more" results when all results have been displayed already---> do not need to make a call to themoviedb.org
                    tmdbCallNeeded = !((currentIndex <= 10 && prevSearch)
                            || (currentIndex == numMovies && nextSearch));
                }
                if (tmdbCallNeeded) {
                    // Need to make a call to TMDB.
                    int pageNum = (int) Math.ceil((float) currentIndex / 20);// round up.. 10/20 = .5 == page# 1
                    if ((nextSearch || newSearch) && (currentIndex % 20) == 0) {
                        pageNum++;
                    }

                    // Decrement page num. eg.: currentIndex = 30, 23, etc. Do not decrement page num for currentIndex = 20, 36, etc.
                    if (prevSearch && (currentIndex % 20 <= 10 && (currentIndex % 20 != 0))) {
                        pageNum--;
                    }

                    int currentDisplayCount = (currentIndex % 10 == 0) ? 10 : currentIndex % 10;
                    SearchTheMovieDbProxyResource tmdb = new SearchTheMovieDbProxyResource();
                    conversationPayload = tmdb.discoverMovies(UtilityFunctions.getPropValue(paramsObj, "Genre"), //$NON-NLS-1$
                            UtilityFunctions.getPropValue(paramsObj, "Rating"), //$NON-NLS-1$
                            UtilityFunctions.getPropValue(paramsObj, "Recency"), //$NON-NLS-1$
                            currentIndex, pageNum, nextSearch || newSearch);
                    int size = conversationPayload.getMovies().size();
                    if (prevSearch) {
                        currentIndex -= currentDisplayCount;
                    } else if (nextSearch || newSearch) {
                        currentIndex += size;
                    }

                    profile = new HashMap<>();
                    // Save the number of movies displayed till now.
                    profile.put("Current_Index", currentIndex.toString()); //$NON-NLS-1$
                    // Save the total number of pages in a profile variable.
                    profile.put("Total_Pages", conversationPayload.getTotalPages().toString()); //$NON-NLS-1$
                    // Save the total number of movies in Num_Movies.
                    profile.put("Num_Movies", conversationPayload.getNumMovies().toString()); //$NON-NLS-1$
                    // Set the profile variables.
                    dialogService.updateProfile(dialog_id, new Integer(clientId), profile);
                }
                if (!tmdbCallNeeded) {
                    // Set the value of the Index_Updated profile variable to No so that WDS knows that no indices were updated.
                    profile = new HashMap<>();
                    profile.put("Index_Updated", "No");
                    dialogService.updateProfile(dialog_id, new Integer(clientId), profile);
                    // Set some values in the ConversationPayload which are needed by the UI.
                    List<MoviePayload> movies = new ArrayList<MoviePayload>();
                    conversationPayload.setMovies(movies);
                    conversationPayload.setNumMovies(numMovies);
                    conversationPayload.setTotalPages(totalPages);
                }
                // If first time, get personalized prompt based on Num_Movies
                prompt = personalized_prompt_current_index;
                if (newSearch || repeatSearch) {
                    prompt = personalized_prompt_movies_returned;
                }
                // Get the personalized prompt.
                converseParams = new HashMap<String, Object>();
                converseParams.put("dialog_id", dialog_id);
                converseParams.put("client_id", Integer.parseInt(clientId));
                converseParams.put("conversation_id", Integer.parseInt(conversationId));
                converseParams.put("input", prompt);
                conversation = dialogService.converse(converseParams);
                wdsMessage = StringUtils.join(conversation.getResponse(), " ");

                // Build the moviePayload.
                conversationPayload.setWdsResponse(wdsMessage);
                conversationPayload.setClientId(clientId); //$NON-NLS-1$
                conversationPayload.setConversationId(clientId); //$NON-NLS-1$
                conversationPayload.setInput(input); //$NON-NLS-1$

                if (UtilityFunctions.logger.isTraceEnabled()) {
                    // Log the execution time.
                    lEndTime = System.nanoTime();
                    difference = lEndTime - lStartTime;
                    UtilityFunctions.logger.trace("Throughput: " + difference / 1000000 + "ms.");
                }
                // Return to UI.
                return Response.ok(conversationPayload, MediaType.APPLICATION_JSON_TYPE).build();
            }
        } catch (ClientProtocolException e) {
            errorMessage = Messages.getString("WDSBlueMixProxyResource.API_CALL_NOT_EXECUTED"); //$NON-NLS-1$
            issue = Messages.getString("WDSBlueMixProxyResource.CLIENT_EXCEPTION_IN_GET_RESPONSE"); //$NON-NLS-1$
            UtilityFunctions.logger.error(issue, e);
        } catch (IllegalStateException e) {
            errorMessage = Messages.getString("WDSBlueMixProxyResource.API_CALL_NOT_EXECUTED"); //$NON-NLS-1$
            issue = Messages.getString("WDSBlueMixProxyResource.ILLEGAL_STATE_GET_RESPONSE"); //$NON-NLS-1$
            UtilityFunctions.logger.error(issue, e);
        } catch (IOException e) {
            errorMessage = Messages.getString("WDSBlueMixProxyResource.API_CALL_NOT_EXECUTED"); //$NON-NLS-1$
            issue = Messages.getString("WDSBlueMixProxyResource.IO_EXCEPTION_GET_RESPONSE"); //$NON-NLS-1$
            UtilityFunctions.logger.error(issue, e);
        } catch (HttpException e) {
            errorMessage = Messages.getString("WDSBlueMixProxyResource.TMDB_API_CALL_NOT_EXECUTED"); //$NON-NLS-1$
            issue = Messages.getString("WDSBlueMixProxyResource.HTTP_EXCEPTION_GET_RESPONSE"); //$NON-NLS-1$
            UtilityFunctions.logger.error(issue, e);
        } catch (WatsonTheatersException e) {
            errorMessage = e.getErrorMessage();
            issue = e.getIssue();
            UtilityFunctions.logger.error(issue, e);
        } catch (URISyntaxException e) {
            errorMessage = Messages.getString("WDSBlueMixProxyResource.TMDB_URL_INCORRECT"); //$NON-NLS-1$
            issue = Messages.getString("WDSBlueMixProxyResource.URI_EXCEPTION_IN_DISOVERMOVIE"); //$NON-NLS-1$
            UtilityFunctions.logger.error(issue, e);
        } catch (ParseException e) {
            errorMessage = Messages.getString("WDSBlueMixProxyResource.TMDB_RESPONSE_PARSE_FAIL"); //$NON-NLS-1$
            issue = Messages.getString("WDSBlueMixProxyResource.PARSE_EXCEPTION_TMDB_GET"); //$NON-NLS-1$
            UtilityFunctions.logger.error(issue, e);
        }
        return Response.serverError().entity(new ServerErrorPayload(errorMessage, issue)).build();
    }

    /**
     * Returns selected movie details
     * <p>
     * This extracts the details of the movie specified. It uses themoviedb.org API to populate movie details in {@link MoviePayload}.
     * </p>
     * 
     * @param clientId the client id for the session
     * @param conversationId the conversation id for the client id specified
     * @param movieName the movie name
     * @param movieId the movie id
     * @return a response containing either of these two entities- {@code WDSConversationPayload} or {@code ServerErrorPayload}
     */
    @GET
    @Path("/getSelectedMovieDetails")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getSelectedMovieDetails(@QueryParam("clientId") String clientId,
            @QueryParam("conversationId") String conversationId, @QueryParam("movieName") String movieName,
            @QueryParam("movieId") String movieId) throws IOException, HttpException, WatsonTheatersException {

        String errorMessage = Messages.getString("WDSBlueMixProxyResource.WDS_API_CALL_NOT_EXECUTED"); //$NON-NLS-1$
        String issue = null;
        WDSConversationPayload conversationPayload = new WDSConversationPayload();
        try {
            // Get movie info from TMDB.
            SearchTheMovieDbProxyResource tmdb = new SearchTheMovieDbProxyResource();
            Response tmdbResponse = tmdb.getMovieDetails(movieId, movieName);
            MoviePayload movie = (MoviePayload) tmdbResponse.getEntity();

            // Set the profile variable for WDS.
            Map<String, String> profile = new HashMap<>();
            profile.put("Selected_Movie", URLEncoder.encode(movieName, "UTF-8")); //$NON-NLS-1$ //$NON-NLS-2$
            profile.put("Popularity_Score", movie.getPopularity().toString()); //$NON-NLS-1$
            dialogService.updateProfile(dialog_id, new Integer(clientId), profile);

            // Get the personalized prompt.
            Map<String, Object> converseParams = new HashMap<String, Object>();
            converseParams.put("dialog_id", dialog_id);
            converseParams.put("client_id", Integer.parseInt(clientId));
            converseParams.put("conversation_id", Integer.parseInt(conversationId));
            converseParams.put("input", personalized_prompt_movie_selected);
            Conversation conversation = dialogService.converse(converseParams);
            String wdsMessage = StringUtils.join(conversation.getResponse(), " ");

            // Add the wds personalized prompt to the MoviesPayload and return.
            List<MoviePayload> movieList = new ArrayList<MoviePayload>();
            movieList.add(movie);
            conversationPayload.setMovies(movieList);
            conversationPayload.setWdsResponse(wdsMessage);
            if (UtilityFunctions.logger.isTraceEnabled()) {
                UtilityFunctions.logger.trace(Messages.getString("WDSBlueMixProxyResource.MOVIE_NAME") + movieName //$NON-NLS-1$
                        + Messages.getString("WDSBlueMixProxyResource.POPULARITY") //$NON-NLS-1$
                        + movie.getPopularity().toString());
                UtilityFunctions.logger.trace(
                        Messages.getString("WDSBlueMixProxyResource.WDS_PROMPT_SELECTED_MOVIE") + wdsMessage); //$NON-NLS-1$
            }
            return Response.ok(conversationPayload, MediaType.APPLICATION_JSON_TYPE).build();

        } catch (IllegalStateException e) {
            issue = Messages.getString("WDSBlueMixProxyResource.ILLEGAL_STATE_EXCEPTION_GET_RESPONSE"); //$NON-NLS-1$
            UtilityFunctions.logger.error(issue, e);
        }
        return Response.serverError().entity(new ServerErrorPayload(errorMessage, issue)).build();
    }

    /**
     * Initializes chat with WDS This initiates the chat with WDS by requesting for a client id and conversation id(to be used in subsequent API calls) and a
     * response message to be displayed to the user. If it's a returning user, it sets the First_Time profile variable to "No" so that the user is not taken
     * through the hand-holding process.
     * 
     * @param firstTimeUser specifies if it's a new user or a returning user(true/false). If it is a returning user WDS is notified via profile var.
     * 
     * @return a response containing either of these two entities- {@code WDSConversationPayload} or {@code ServerErrorPayload}
     */
    @GET
    @Path("/initChat")
    @Produces(MediaType.APPLICATION_JSON)
    public Response startConversation(@QueryParam("firstTimeUser") boolean firstTimeUser) {
        Conversation conversation = dialogService.createConversation(dialog_id);
        if (!firstTimeUser) {
            Map<String, String> profile = new HashMap<>();
            profile.put("First_Time", "No");
            dialogService.updateProfile(dialog_id, conversation.getClientId(), profile);
        }
        WDSConversationPayload conversationPayload = new WDSConversationPayload();
        conversationPayload.setClientId(Integer.toString(conversation.getClientId())); //$NON-NLS-1$
        conversationPayload.setConversationId(Integer.toString(conversation.getId())); //$NON-NLS-1$
        conversationPayload.setInput(conversation.getInput()); //$NON-NLS-1$
        conversationPayload.setWdsResponse(StringUtils.join(conversation.getResponse(), " "));
        return Response.ok(conversationPayload, MediaType.APPLICATION_JSON_TYPE).build();
    }
}