Java tutorial
package ca.ualberta.cmput301w13t11.FoodBook.model; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.HttpParams; import android.os.StrictMode; /** * Communicates with the server to perform searches, upload recipes and upload photos to recipes. * Implements the singleton design pattern. * * Code largely based on: * ESDemo with HTTP Client and GSON * * LICENSE: * * CC0 http://creativecommons.org/choose/zero/ * * To the extent possible under law, Abram Hindle and Chenlei Zhang has waived all copyright and related or neighboring rights to this work. This work is published from: Canada. * * * getThreadSafeClient() method cribbed from * http://tech.chitgoks.com/2011/05/05/fixing-the-invalid-use-of-singleclientconnmanager-connection-still-allocated-problem/ * (last accessed March 10, 2013) * * @author Abram Hindle, Chenlei Zhang, Marko Babic */ public class ServerClient { private static ServerClient instance = null; private static ResultsDbManager dbManager = null; static private final Logger logger = Logger.getLogger(ServerClient.class.getName()); static private final long TIMEOUT_PERIOD = 30; static private final long UPLOAD_PHOTO_GRACE_PERIOD = 8; static private String test_server_string = "http://cmput301.softwareprocess.es:8080/testing/cmput301w13t11/"; static private String serverString = "http://cmput301.softwareprocess.es:8080/cmput301w13t11/newrecipes/"; private static HttpClient httpclient = null; private static ClientHelper helper = null; private ArrayList<Recipe> results; /** * Return codes possible for a networking task. * @author mbabic */ public static enum ReturnCode { SUCCESS, ALREADY_EXISTS, NO_RESULTS, NOT_FOUND, ERROR, BUSY; } /** * Empty constructor. */ private ServerClient() { } /** * Returns the instance of ServerClient, or generates it if has not yet be instantiated. * @return The instance of the ServerClient */ public static ServerClient getInstance() { if (instance == null) { /* Allow networking on main thread. Will be changed later so networking tasks are asynchronous. */ StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); StrictMode.setThreadPolicy(policy); instance = new ServerClient(); dbManager = ResultsDbManager.getInstance(); httpclient = ServerClient.getThreadSafeClient(); helper = new ClientHelper(); } return instance; } /** * Returns true if a connection can be established to the given url. * Else, returns false * @return True if connection is established, false otherwise. */ public boolean hasConnection(String url) { try { HttpGet get = new HttpGet(url); HttpResponse response = httpclient.execute(get); int ret = response.getStatusLine().getStatusCode(); logger.log(Level.INFO, "Connection test return status: " + Integer.toString(ret)); return true; } catch (Exception e) { logger.log(Level.SEVERE, "Exception occured when testing connection:" + e.getMessage()); return false; } } /** * Gets a thread safe client - that is, a client which can be used in a multithreaded * program and protects against certain errors that exist even in single threaded programs. * @return A Object of type DefaultHttpClient which will */ public static DefaultHttpClient getThreadSafeClient() { DefaultHttpClient client = new DefaultHttpClient(); ClientConnectionManager manager = client.getConnectionManager(); HttpParams params = client.getParams(); client = new DefaultHttpClient(new ThreadSafeClientConnManager(params, manager.getSchemeRegistry()), params); return client; } /** * Writes the results of the latest successful search to the ResultsDb. */ public void writeResultsToDb() { if (results == null) return; if (results.get(0).getPhotos().isEmpty()) { logger.log(Level.SEVERE, "Result's 0 photos are null!"); } dbManager = ResultsDbManager.getInstance(); if (dbManager == null) { logger.log(Level.SEVERE, "ResultsDbManager null!!!"); } dbManager.storeRecipes(results); DbManager dbm = DbManager.getInstance(); dbm.notifyViews(); logger.log(Level.SEVERE, "GOT RESULTS"); logger.log(Level.SEVERE, "First result: " + results.get(0).getTitle()); } /** * Query the server for the Recipe with the given uri and return a ReturnCode based on the * server response. * @param uri The uri of the Recipe for which we wish to search. * @return SUCCESS if recipe was found, NOT_FOUND if we received a 404, * ERROR if a problem occurred during the query. */ private ReturnCode checkForRecipe(long uri) { httpclient = getThreadSafeClient(); HttpResponse response = null; int retcode = -1; try { HttpGet get = new HttpGet(serverString + uri); get.addHeader("Accept", "application/json"); response = httpclient.execute(get); retcode = response.getStatusLine().getStatusCode(); logger.log(Level.INFO, "HttpGet server response: " + response.getStatusLine().toString()); } catch (Exception e) { return ReturnCode.ERROR; } if (retcode == HttpStatus.SC_NOT_FOUND) { logger.log(Level.SEVERE, "Recipe not on server. Response code: " + retcode); return ReturnCode.NOT_FOUND; } if (retcode != HttpStatus.SC_OK) { logger.log(Level.SEVERE, "Recipe to upload photo to could not be found. Response code: " + retcode); return ReturnCode.ERROR; } /* Else the recipe was found and we return success. */ return ReturnCode.SUCCESS; } /****************************************************************************************************************/ /***************************************<Upload Recipe>**********************************************************/ /****************************************************************************************************************/ /** * The task which implements the code necessary to upload a Recipe to the server -- * it is implemented as a Callable object such that the executing thread can be cancelled * should the operation be taking too long (ie. the network is down, spotty connection, etc.) * @author mbabic * */ private class UploadRecipeTask implements Callable<ReturnCode> { private Recipe recipe; public UploadRecipeTask(Recipe recipe) { this.recipe = recipe; } @Override public ReturnCode call() { httpclient = getThreadSafeClient(); ReturnCode checkForRecipe = checkForRecipe(recipe.getUri()); if (checkForRecipe == ReturnCode.SUCCESS) { return ReturnCode.ALREADY_EXISTS; } /* We are using the Recipe's URI as its _id on the server */ HttpResponse response = null; HttpPost httpPost = new HttpPost(serverString + recipe.getUri()); StringEntity se = null; se = helper.recipeToJSON(recipe); httpPost.setHeader("Accept", "application/json"); httpPost.setEntity(se); try { response = httpclient.execute(httpPost); } catch (ClientProtocolException cpe) { logger.log(Level.SEVERE, cpe.getMessage()); cpe.printStackTrace(); return ReturnCode.ERROR; } catch (IOException ioe) { logger.log(Level.SEVERE, ioe.getMessage()); ioe.printStackTrace(); return ReturnCode.ERROR; } String status = response.getEntity().toString(); int retcode = response.getStatusLine().getStatusCode(); logger.log(Level.INFO, "upload request server response: " + response.getStatusLine().toString()); logger.log(Level.INFO, "upload request server response: " + response.toString()); if (retcode == HttpStatus.SC_CREATED) return ReturnCode.SUCCESS; else return ReturnCode.ALREADY_EXISTS; } } /** * Uploads the given recipe to the server. * @param recipe The recipe to be uploaded. * ReturnCode.ERROR if anything goes wrong, ReturnCode.ALREADY_EXISTS if a recipe * by that name already exists on the server (this will eventually be modified to check * against URI instead of Recipe title), ReturnCode.SUCCESS if the recipe was successfully * uploaded, or ReturnCode.BUSY if the network is not responding or the operation is * taking too long. */ public ReturnCode uploadRecipe(Recipe recipe) throws IllegalStateException, IOException { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<ReturnCode> future = executor.submit(new UploadRecipeTask(recipe)); ReturnCode ret = ReturnCode.ERROR; try { ret = future.get(TIMEOUT_PERIOD, TimeUnit.SECONDS); } catch (TimeoutException te) { logger.log(Level.SEVERE, "Upload Recipe operation timed out."); return ReturnCode.BUSY; } catch (Exception e) { logger.log(Level.SEVERE, "Exception during upload recipe operation."); return ReturnCode.ERROR; } /* Got here so the operation finished. */ executor.shutdownNow(); return ret; } /**************************************</Upload Recipe>**********************************************************/ /****************************************************************************************************************/ /**************************************<Search by Keywords>******************************************************/ /****************************************************************************************************************/ /** * The task which implements the code necessary to query the server using keywords to the server -- * it is implemented as a Callable object such that the executing thread can be cancelled * should the operation be taking too long (ie. the network is down, spotty connection, etc.) * @author mbabic */ private class SearchByKeywordsTask implements Callable<ReturnCode> { private String str; public SearchByKeywordsTask(String str) { this.str = str; } @Override public ReturnCode call() { httpclient = getThreadSafeClient(); ArrayList<Recipe> search_results = new ArrayList<Recipe>(); HttpResponse response = null; logger.log(Level.SEVERE, "Search string passed:" + str); try { HttpGet search_request = new HttpGet( serverString + "_search?q=" + java.net.URLEncoder.encode(str, "UTF-8")); search_request.setHeader("Accept", "application/json"); response = httpclient.execute(search_request); String status = response.getStatusLine().toString(); logger.log(Level.INFO, "search response: " + status); String json = helper.responseToString(response); search_results = helper.toRecipeList(json); results = search_results; } catch (IllegalArgumentException iae) { logger.log(Level.SEVERE, "HttpGet failed: " + iae.getMessage()); return ReturnCode.ERROR; } catch (ClientProtocolException cpe) { logger.log(Level.SEVERE, cpe.getMessage()); return ReturnCode.ERROR; } catch (IOException ioe) { logger.log(Level.SEVERE, "execute failed" + ioe.getMessage()); return ReturnCode.ERROR; } /* * If no results were found, inform the caller by setting ReturnCode to * NO_RESULTS -- do NOT attempt to clear/write these results to ServerDb. */ if (search_results.isEmpty()) { logger.log(Level.SEVERE, "Search by keywords \"" + str + "\" yielded no results."); return ReturnCode.NO_RESULTS; } return ReturnCode.SUCCESS; } } /** * Performs a search of online recipes by keywords. * @param str The string of keywords we wish to search by. * @return ReturnCode.ERROR if anything goes wrong, ReturnCode.NO_RESULTS if * the search returned no results, ReturnCode.SUCCESS if the search was successful, * in which case the results are written to the database and the observing views * are notified, ReturnCode.BUSY if the server was busy or the operation took * longer than TIME_PERIOD seconds. */ public ReturnCode searchByKeywords(String str) throws ClientProtocolException, IOException { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<ReturnCode> future = executor.submit(new SearchByKeywordsTask(str)); ReturnCode ret = ReturnCode.ERROR; try { ret = future.get(TIMEOUT_PERIOD, TimeUnit.SECONDS); } catch (TimeoutException te) { logger.log(Level.SEVERE, "Search by Keywords operation timed out."); return ReturnCode.BUSY; } catch (Exception e) { logger.log(Level.SEVERE, "Exception during Search by Keywords operation."); return ReturnCode.ERROR; } /* Got here so the operation finished. */ executor.shutdownNow(); return ret; } /*************************************</Search by Keywords>****************************************************/ /****************************************************************************************************************/ /**************************************<Search by Ingredients>***************************************************/ /****************************************************************************************************************/ /** * The task which implements the code necessary to query the server by a list of Ingredients -- * it is implemented as a Callable object such that the executing thread can be cancelled * should the operation be taking too long (ie. the network is down, spotty connection, etc.) * @author mbabic * */ private class SearchByIngredientsTask implements Callable<ReturnCode> { private ArrayList<Ingredient> ingredients; public SearchByIngredientsTask(ArrayList<Ingredient> ingredients) { this.ingredients = ingredients; } @Override public ReturnCode call() { httpclient = getThreadSafeClient(); ArrayList<Recipe> search_results = null; String ingredients_str = ingredientsToString(ingredients); /* We next form the HTTP query string itself. */ HttpPost searchRequest = new HttpPost(serverString + "_search?pretty=1"); String query = "{\"query\" : {\"query_string\" : {\"default_field\" : \"ingredients.name\", \"query\" : \"" + ingredients_str + "\"}}}"; logger.log(Level.INFO, "query string = " + query); try { StringEntity str_entity = new StringEntity(query); searchRequest.setHeader("Accept", "application/json"); searchRequest.setEntity(str_entity); HttpResponse response = httpclient.execute(searchRequest); logger.log(Level.SEVERE, "Http response (searchByIngredients search attempt): " + response.getStatusLine().toString()); String response_str = helper.responseToString(response); search_results = helper.toRecipeList(response_str); } catch (UnsupportedEncodingException uee) { logger.log(Level.SEVERE, "Failed to create StringEntity from query : " + uee.getMessage()); uee.printStackTrace(); return ReturnCode.ERROR; } catch (ClientProtocolException cpe) { logger.log(Level.SEVERE, "ClientProtocolException in execution of search request: " + cpe.getMessage()); cpe.printStackTrace(); return ReturnCode.ERROR; } catch (IOException ioe) { logger.log(Level.SEVERE, "IOException in execution of search request: " + ioe.getMessage()); ioe.printStackTrace(); return ReturnCode.ERROR; } /* * If our search returned no results, we do not write anything to the Results Db * and we return the code no_results. */ if (search_results.isEmpty()) { return ReturnCode.NO_RESULTS; } /* Else, our search returned results; we update "results" and return success code. */ results = search_results; return ReturnCode.SUCCESS; } } /** * Converts the given list of ingredients to a string appropriate for use in a server query. * @param ingredients The list of ingredients to convert to a search appropriate string. * @return A string consisting of the string names with " OR " inserted between. */ private String ingredientsToString(ArrayList<Ingredient> ingredients) { String ingredients_str = ""; /* The list of ingredients with the logical OR between each item. */ for (int i = 0; i < ingredients.size(); i++) { if (i != 0) { /* * If it is not our first go around, we do not need to * concatenate "OR" to the string */ ingredients_str += " OR "; } ingredients_str += ingredients.get(i).getName(); } logger.log(Level.INFO, "ingredients_str = " + ingredients_str); return ingredients_str; } /** * Query the server for Recipes which contains at subset of the given ingredients list. * @param ingredients The list of ingredients by which to search. * @return ReturnCode.ERROR if anything goes wrong, ReturnCode.NO_RESULTS if * the search returned no results, ReturnCode.SUCCESS if the search was successful, * in which case the results are written to the database and the observing views * are notified, ReturnCode.BUSY if the server was busy or the operation took * longer than TIME_PERIOD seconds. */ public ReturnCode searchByIngredients(ArrayList<Ingredient> ingredients) { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<ReturnCode> future = executor.submit(new SearchByIngredientsTask(ingredients)); ReturnCode ret = ReturnCode.ERROR; try { ret = future.get(TIMEOUT_PERIOD, TimeUnit.SECONDS); } catch (TimeoutException te) { logger.log(Level.SEVERE, "Search by Ingredients operation timed out."); return ReturnCode.BUSY; } catch (Exception e) { logger.log(Level.SEVERE, "Exception during Search by Ingredients operation."); return ReturnCode.ERROR; } /* Got here so the operation finished. */ executor.shutdownNow(); return ret; } /*************************************</Search by Ingredients>***************************************************/ /****************************************************************************************************************/ /**************************************<Upload Photo>************************************************************/ /****************************************************************************************************************/ /** * The task which implements the code necessary to upload a photo to the Server -- * it is implemented as a Callable object such that the executing thread can be cancelled * should the operation be taking too long (ie. the network is down, spotty connection, etc.) * @author mbabic * */ private class UploadPhotoTask implements Callable<ReturnCode> { private Photo photo; private long uri; public UploadPhotoTask(Photo photo, long uri) { this.photo = photo; this.uri = uri; } @Override public ReturnCode call() { int maxTries = 10; int tries = 0; ReturnCode searchForRecipe = checkForRecipe(uri); if (searchForRecipe != ReturnCode.SUCCESS) { /* * We couldn't find the recipe online or we encountered an error, so we cannot upload the recipe * at this time. */ return searchForRecipe; } /* Else, the Recipe exists online and we try to upload the given photo to it. */ int retcode = HttpStatus.SC_INTERNAL_SERVER_ERROR; HttpResponse response = null; while (tries < maxTries && retcode == HttpStatus.SC_INTERNAL_SERVER_ERROR) { tries++; try { httpclient = getThreadSafeClient(); /* We first must convert the given Photo to a ServerPhoto. */ ServerPhoto serverPhoto = new ServerPhoto(photo); /* Now we must construct a suitable JSON style string for the ServerPhoto. */ String sp_str = helper.serverPhotoToJSON(serverPhoto); logger.log(Level.INFO, "serverPhotoToJson() result: " + sp_str); HttpPost updateRequest = new HttpPost(serverString + uri + "/_update"); String query = "{\"script\":\"ctx._source.photos += photo\", \"params\" : " + "{ \"photo\" : " + sp_str + "}}"; logger.log(Level.INFO, "stringQuery = " + query); StringEntity stringentity = new StringEntity(query); updateRequest.setHeader("Accept", "application/json"); updateRequest.setEntity(stringentity); response = httpclient.execute(updateRequest); retcode = response.getStatusLine().getStatusCode(); logger.log(Level.SEVERE, "Upload request return code: " + response.getStatusLine().toString()); if (retcode == HttpStatus.SC_OK) break; } catch (ClientProtocolException cpe) { logger.log(Level.SEVERE, "ClientProtocolException when executing HttpGet : " + cpe.getMessage()); cpe.printStackTrace(); return ReturnCode.ERROR; } catch (IOException ioe) { logger.log(Level.SEVERE, "IOException when executing HttpGet : " + ioe.getMessage()); ioe.printStackTrace(); return ReturnCode.ERROR; } } if (tries < maxTries) return ReturnCode.SUCCESS; else return ReturnCode.ERROR; } } /** * Upload the given Photo to the appropriate Recipe. * @param (Photo) photo The photo to be added to the server-side version of the Recipe. * @param (long) uri The uri of the Recipe to be updated. * @return NOT_FOUND if the Recipe cannot be found on the server, * ERROR on any other error occurred while attempting to upload, * SUCCESS on successful upload. */ public ReturnCode uploadPhotoToRecipe(Photo photo, long uri) { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<ReturnCode> future = executor.submit(new UploadPhotoTask(photo, uri)); ReturnCode ret = ReturnCode.ERROR; try { ret = future.get(TIMEOUT_PERIOD + UPLOAD_PHOTO_GRACE_PERIOD, TimeUnit.SECONDS); } catch (TimeoutException te) { logger.log(Level.SEVERE, "Upload photo operation timed out."); return ReturnCode.BUSY; } catch (Exception e) { logger.log(Level.SEVERE, "Exception during upload photo operation."); return ReturnCode.ERROR; } /* Got here so the operation finished. */ executor.shutdownNow(); return ret; } /** * @uml.property name="resultsDbManager" * @uml.associationEnd inverse="serverClient:ca.ualberta.cmput301w13t11.FoodBook.model.ResultsDbManager" * @uml.association name="writes results for" */ private ResultsDbManager resultsDbManager; /** * Getter of the property <tt>resultsDbManager</tt> * @return Returns the resultsDbManager. * @uml.property name="resultsDbManager" */ public ResultsDbManager getResultsDbManager() { return resultsDbManager; } /** * Setter of the property <tt>resultsDbManager</tt> * @param resultsDbManager The resultsDbManager to set. * @uml.property name="resultsDbManager" */ public void setResultsDbManager(ResultsDbManager resultsDbManager) { this.resultsDbManager = resultsDbManager; } /**************************************</Upload Photo>************************************************************/ }