Java tutorial
/* 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.JsonArray; 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.dialog.v1.model.NameValue; 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 This class acts as a proxy on the server-side to communicate with the 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> * * @author Ashima Arora */ @Path("/bluemix") public class WDSBlueMixProxyResource { private static String wds_base_url; private static DialogService dialogService = new DialogService(); private static String dialog_id; private static String username = null; private static String password = null; 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 { loadStaticBluemixProperties(); createDialogServiceInstance(); } /** * */ private static void loadStaticBluemixProperties() { String envServices = System.getenv("VCAP_SERVICES"); //$NON-NLS-1$ if (envServices != null) { UtilityFunctions.logger.info(Messages.getString("WDSBlueMixProxyResource.VCAP_SERVICES_ENV_VAR_FOUND")); //$NON-NLS-1$ JsonObject services = new JsonParser().parse(envServices).getAsJsonObject(); UtilityFunctions.logger .info(Messages.getString("WDSBlueMixProxyResource.VCAP_SERVICES_JSONOBJECT_SUCCESS")); //$NON-NLS-1$ JsonArray arr = (JsonArray) services.get("dialog"); //$NON-NLS-1$ if (arr.size() > 0) { services = arr.get(0).getAsJsonObject(); JsonObject credentials = services.get("credentials").getAsJsonObject(); //$NON-NLS-1$ wds_base_url = credentials.get("url").getAsString(); //$NON-NLS-1$ if (credentials.get("username") != null && !credentials.get("username").isJsonNull()) { //$NON-NLS-1$ //$NON-NLS-2$ username = credentials.get("username").getAsString(); //$NON-NLS-1$ UtilityFunctions.logger.info(Messages.getString("WDSBlueMixProxyResource.FOUND_USERNAME")); //$NON-NLS-1$ } if (credentials.get("password") != null && !credentials.get("password").isJsonNull()) { //$NON-NLS-1$ //$NON-NLS-2$ password = credentials.get("password").getAsString(); //$NON-NLS-1$ UtilityFunctions.logger.info(Messages.getString("WDSBlueMixProxyResource.FOUND_PASSWORD")); //$NON-NLS-1$ } } } else { UtilityFunctions.logger.error(Messages.getString("WDSBlueMixProxyResource.VCAP_SERVICES_CANNOT_LOAD")); //$NON-NLS-1$ } envServices = System.getenv("DIALOG_ID"); //$NON-NLS-1$ if (envServices != null) { dialog_id = envServices; UtilityFunctions.logger.info(Messages.getString("WDSBlueMixProxyResource.DIALOG_ACCOUNT_ID_SUCCESS")); //$NON-NLS-1$ } else { UtilityFunctions.logger.error(Messages.getString("WDSBlueMixProxyResource.DIALOG_ACCOUNT_ID_FAIL")); //$NON-NLS-1$ } } public static void createDialogServiceInstance() { if (username == null || password == null) { UtilityFunctions.logger.error(Messages.getString("WDSBlueMixProxyResource.CREATE_CONVERSATION_FAIL")); } dialogService.setUsernameAndPassword(username, password); dialogService.setEndPoint(wds_base_url); } /** * 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) { String errorMessage = null, issue = null; String wdsMessage = 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 { 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(), " "); JsonObject 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$ 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$ } Integer currentIndex = Integer.parseInt(paramsObj.get("Index").getAsString()); //$NON-NLS-1$ 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 = 0; if (conversationPayload.getMovies() != null) { size = conversationPayload.getMovies().size(); } if (prevSearch) { currentIndex -= currentDisplayCount; } else if (nextSearch || newSearch) { currentIndex += size; } List<NameValue> nameValues = new ArrayList<NameValue>(); // Save the number of movies displayed till now. nameValues.add(new NameValue("Current_Index", currentIndex.toString())); //$NON-NLS-1$ // Save the total number of pages in a profile variable. nameValues.add(new NameValue("Total_Pages", conversationPayload.getTotalPages().toString())); //$NON-NLS-1$ // Save the total number of movies in Num_Movies. nameValues.add(new NameValue("Num_Movies", conversationPayload.getNumMovies().toString())); //$NON-NLS-1$ // If first time, get personalized prompt based on Num_Movies String prompt = personalized_prompt_current_index; if (newSearch || repeatSearch) { prompt = personalized_prompt_movies_returned; } // Set the profile variables. dialogService.updateProfile(dialog_id, Integer.parseInt(clientId), nameValues); // 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$ // 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. List<NameValue> nameValues = new ArrayList<NameValue>(); nameValues.add(new NameValue("Selected_Movie", URLEncoder.encode(movieName, "UTF-8"))); //$NON-NLS-1$ //$NON-NLS-2$ nameValues.add(new NameValue("Popularity_Score", movie.getPopularity().toString())); //$NON-NLS-1$ dialogService.updateProfile(dialog_id, Integer.parseInt(clientId), nameValues); // 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) { List<NameValue> nameValues = new ArrayList<NameValue>(); nameValues.add(new NameValue("First_Time", "No")); dialogService.updateProfile(dialog_id, conversation.getClientId(), nameValues); } 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(); } }