diff --git a/src/main/java/shef/data/Ingredient.java b/src/main/java/shef/data/Ingredient.java new file mode 100644 index 0000000..406b851 --- /dev/null +++ b/src/main/java/shef/data/Ingredient.java @@ -0,0 +1,63 @@ +// Copyright 2019 Google LLC +// +// 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 +// +// https://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 shef.data; + +import com.google.appengine.api.datastore.EmbeddedEntity; + +public class Ingredient { + + private double amount; + private String unit; + private String name; + + public Ingredient(double amount, String unit, String name) { + this.amount = amount; + this.unit = unit; + this.name = name; + } + + public Ingredient(EmbeddedEntity entity) { + this.amount = (double) entity.getProperty("amount"); + this.unit = (String) entity.getProperty("unit"); + this.name = (String) entity.getProperty("name"); + } + + public double amount() { + return amount; + } + + public String unit() { + return unit; + } + + public String name() { + return name; + } + + @Override + public boolean equals(Object other) { + return other instanceof Ingredient && this.amount == ((Ingredient) other).amount() && this.unit.equals(((Ingredient) other).unit()) && this.name.equals(((Ingredient) other).name()); + } + + @Override + public String toString() { + return "[Ingredient: " + amount + ", " + unit + ", " + name + "]"; + } + + @Override + public int hashCode() { + return (int) amount >> 2 + name.length(); + } +} diff --git a/src/main/java/shef/data/Recipe.java b/src/main/java/shef/data/Recipe.java index 2e61060..8653542 100644 --- a/src/main/java/shef/data/Recipe.java +++ b/src/main/java/shef/data/Recipe.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Set; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.logging.*; import java.util.Iterator; import com.google.appengine.api.datastore.Entity; @@ -32,8 +33,12 @@ public class Recipe { private String key; private String name; private String description; + private double time; + private double servings; + private String imageKey; private Set tags; - private Set ingredients; + private Set ingredients; + private Set equipment; private List steps; private Set spinOffs; private long timestamp; @@ -52,7 +57,7 @@ public Recipe(Recipe recipe) { } /** Default constructor called when creating a new recipe. */ - public Recipe(String name, String description, Set tags, Set ingredients, List steps, long timestamp) { + public Recipe(String name, String description, Set tags, Set ingredients, List steps, long timestamp) { this.name = name; this.description = description; this.tags = tags; @@ -62,18 +67,25 @@ public Recipe(String name, String description, Set tags, Set ing this.timestamp = timestamp; } - /** Creates a Recipe from a Datastore entity. */ + /** + * Creates a Recipe from a Datastore entity. + * This constructor is used to easily convert an Entity into an object, which can be sent as JSON. + */ public Recipe(Entity recipeEntity) { this.name = (String) recipeEntity.getProperty("name"); this.description = (String) recipeEntity.getProperty("description"); + this.time = (double) recipeEntity.getProperty("time"); + this.servings = (double) recipeEntity.getProperty("servings"); + this.imageKey = (String) recipeEntity.getProperty("imageKey"); this.tags = getTagsFromEntity((Collection) recipeEntity.getProperty("tags")); this.ingredients = getIngredientsFromEntity((Collection) recipeEntity.getProperty("ingredients")); + this.equipment = getEquipmentFromEntity((Collection) recipeEntity.getProperty("equipment")); this.steps = getStepsFromEntity((Collection) recipeEntity.getProperty("steps")); this.timestamp = (long) recipeEntity.getProperty("timestamp"); } /** Constructor called when creating a recipe to display on the recipe feed. */ - public Recipe(String key, String name, String description, Set tags, Set ingredients, List steps, long timestamp) { + public Recipe(String key, String name, String description, Set tags, Set ingredients, List steps, long timestamp) { this.key = key; this.name = name; this.tags = tags; @@ -112,7 +124,7 @@ public Set getTags() { } /** Gets the recipe's ingredients. */ - public Set getIngredients() { + public Set getIngredients() { return ingredients; } @@ -127,7 +139,7 @@ public void addTag(String tag) { } /** Adds an ingredient to the recipe. */ - public void addIngredient(String ingredient) { + public void addIngredient(Ingredient ingredient) { ingredients.add(ingredient); } @@ -142,7 +154,7 @@ public void removeTag(String tag) { } /** Removes an ingredient from the recipe. */ - public void removeIngredient(String ingredient) { + public void removeIngredient(Ingredient ingredient) { ingredients.remove(ingredient); } @@ -244,14 +256,23 @@ private Set getTagsFromEntity(Collection entityTags) { } /** Returns the ingredients of an EmbeddedEntity as a Set. */ - private Set getIngredientsFromEntity(Collection entityIngredients) { - Set ingredientsSet = new HashSet<>(); + private Set getIngredientsFromEntity(Collection entityIngredients) { + Set ingredientsSet = new HashSet<>(); for (EmbeddedEntity ingredient : entityIngredients) { - ingredientsSet.add((String) ingredient.getProperty("ingredient")); + ingredientsSet.add(new Ingredient(ingredient)); } return ingredientsSet; } + /** Returns the equipment of an EmbeddedEntity as a Set. */ + private Set getEquipmentFromEntity(Collection entityEquipment) { + Set equipmentSet = new HashSet<>(); + for (EmbeddedEntity equipment : entityEquipment) { + equipmentSet.add((String) equipment.getProperty("equipment")); + } + return equipmentSet; + } + /** Returns the steps of an EmbeddedEntity as a List. */ private List getStepsFromEntity(Collection entitySteps) { List stepsList = new LinkedList<>(); diff --git a/src/main/java/shef/servlets/BlobServlet.java b/src/main/java/shef/servlets/BlobServlet.java index 32167f0..c5c4ff2 100644 --- a/src/main/java/shef/servlets/BlobServlet.java +++ b/src/main/java/shef/servlets/BlobServlet.java @@ -17,11 +17,15 @@ import com.google.appengine.api.blobstore.BlobKey; import com.google.appengine.api.blobstore.BlobstoreService; import com.google.appengine.api.blobstore.BlobstoreServiceFactory; +import com.google.appengine.api.blobstore.BlobInfo; +import com.google.appengine.api.blobstore.BlobInfoFactory; import java.io.IOException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; /** Servlet that serves blobstore files. */ @WebServlet("/blob") @@ -33,4 +37,30 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro BlobKey blobKey = new BlobKey(request.getParameter("blob-key")); blobstoreService.serve(blobKey, response); } + + /* + * Returns the String representation of the BlobKey of the uploaded file, or null if the user didn't upload a file. + * Used in alternative to getting the blob's direct URL, which causes crashes on the live server. + */ + public static String getUploadedFileBlobKey(HttpServletRequest request, String formInputElementName) { + BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); + Map> blobs = blobstoreService.getUploads(request); + List blobKeys = blobs.get(formInputElementName); + + // User submitted form without selecting a file, so we can't get a URL. (dev server) + if (blobKeys == null || blobKeys.isEmpty()) { + return null; + } + + // Our form only contains a single file input, so get the first index. + BlobKey blobKey = blobKeys.get(0); + + // User submitted form without selecting a file, so we can't get a URL. (live server) + BlobInfo blobInfo = new BlobInfoFactory().loadBlobInfo(blobKey); + if (blobInfo.getSize() == 0) { + blobstoreService.delete(blobKey); + return null; + } + return blobKey.getKeyString(); + } } \ No newline at end of file diff --git a/src/main/java/shef/servlets/DisplayRecipesServlet.java b/src/main/java/shef/servlets/DisplayRecipesServlet.java index c4b2bf3..24aca87 100644 --- a/src/main/java/shef/servlets/DisplayRecipesServlet.java +++ b/src/main/java/shef/servlets/DisplayRecipesServlet.java @@ -53,7 +53,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro List recipes = new ArrayList<>(); for (Entity entity : results.asIterable()) { - recipes.add(entityToRecipe(entity)); + recipes.add(new Recipe(entity)); } Gson gson = new Gson(); @@ -61,26 +61,4 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro response.setContentType("application/json;"); response.getWriter().println(gson.toJson(recipes)); } - - /** Converts a Datastore entity into a Recipe. */ - public Recipe entityToRecipe(Entity recipeEntity) { - String key = KeyFactory.keyToString(recipeEntity.getKey()); - String name = (String) recipeEntity.getProperty("name"); - String description = (String) recipeEntity.getProperty("description"); - LinkedHashSet tags = new LinkedHashSet<>((LinkedList) (LinkedList) getDataAsList(recipeEntity.getProperty("tags"), "tag")); - LinkedHashSet ingredients = new LinkedHashSet<>((LinkedList) (LinkedList) getDataAsList(recipeEntity.getProperty("ingredients"), "ingredient")); - LinkedList steps = (LinkedList) (LinkedList) getDataAsList(recipeEntity.getProperty("steps"), "step"); - long timestamp = (long) recipeEntity.getProperty("timestamp"); - return new Recipe(key, name, description, tags, ingredients, steps, timestamp); - } - - /** Gets a list of Recipe parameters from a Datastore property. */ - public Collection getDataAsList(Object propertiesObject, String field) { - Collection properties = (Collection) propertiesObject; - Collection dataAsList = new LinkedList<>(); - for (EmbeddedEntity property : properties) { - dataAsList.add(property.getProperty(field)); - } - return dataAsList; - } } diff --git a/src/main/java/shef/servlets/NewRecipeServlet.java b/src/main/java/shef/servlets/NewRecipeServlet.java index ee43642..95b60fd 100644 --- a/src/main/java/shef/servlets/NewRecipeServlet.java +++ b/src/main/java/shef/servlets/NewRecipeServlet.java @@ -42,6 +42,7 @@ public class NewRecipeServlet extends HttpServlet { private DatastoreService datastore; private final String TAG = "tag"; private final String INGREDIENT = "ingredient"; + private final String EQUIPMENT = "equipment"; private final String STEP = "step"; @Override @@ -62,7 +63,7 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) thro } Recipe original = new Recipe(recipeEntity); response.setContentType("application/json;"); - response.getWriter().println(convertToJsonUsingGson(original)); + response.getWriter().println(new Gson().toJson(original)); } /** Posts a new recipe to the servlet. */ @@ -71,30 +72,33 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr Collection searchStrings = new HashSet<>(); String name = request.getParameter("name"); searchStrings.add(name.toUpperCase()); + double time = request.getParameter("time").equals("") ? 0 : Double.parseDouble(request.getParameter("time")); + double servings = request.getParameter("servings").equals("") ? 0 : Double.parseDouble(request.getParameter("servings")); + String imageKey = BlobServlet.getUploadedFileBlobKey(request, "image"); String description = request.getParameter("description"); Collection tags = getParameters(request, TAG, searchStrings); - Collection ingredients = getParameters(request, INGREDIENT, searchStrings); + Collection ingredients = getIngredients(request, searchStrings); + Collection equipment = getParameters(request, EQUIPMENT, null); Collection steps = getParameters(request, STEP, null); - int likes; - try { - likes = Integer.parseInt(request.getParameter("likes")); - } catch (NumberFormatException e) { - likes = 0; - } + int likes = request.getParameter("likes") == null ? 0 : Integer.parseInt(request.getParameter("likes")); long timestamp = System.currentTimeMillis(); Entity recipe = new Entity("Recipe"); recipe.setProperty("name", name); + recipe.setProperty("time", time); + recipe.setProperty("servings", servings); + recipe.setProperty("imageKey", imageKey); recipe.setProperty("description", description); recipe.setProperty("tags", tags); recipe.setProperty("ingredients", ingredients); + recipe.setProperty("equipment", equipment); recipe.setProperty("steps", steps); recipe.setProperty("search-strings", new ArrayList(searchStrings)); recipe.setProperty("timestamp", timestamp); recipe.setProperty("likes", likes); datastore.put(recipe); - response.sendRedirect("/recipe.html"); + response.sendRedirect("/recipe.html?key=" + KeyFactory.keyToString(recipe.getKey())); } /** @@ -122,6 +126,28 @@ private Collection getParameters(HttpServletRequest request, Str return parameters; } + private Collection getIngredients(HttpServletRequest request, Collection searchStrings) { + Collection parameters = new LinkedList<>(); + int parameterNum = 0; + String parameterName = "ingredient" + parameterNum; + String[] parameterValues = request.getParameterValues(parameterName); + + // In the HTML form, parameters are named as [field name][index], ie step0. + // This loop increments the index of the parameter's name, exiting once it reaches an index for which there is no parameter. + while (parameterValues != null) { + addToSearchStrings(searchStrings, parameterValues[2]); + EmbeddedEntity parameterEntity = new EmbeddedEntity(); + parameterEntity.setProperty("amount", parameterValues[0].equals("") ? 0 : Double.parseDouble(parameterValues[0])); + parameterEntity.setProperty("unit", parameterValues[1]); + parameterEntity.setProperty("name", parameterValues[2]); + parameters.add(parameterEntity); + + parameterName = "ingredient" + (++parameterNum); + parameterValues = request.getParameterValues(parameterName); + } + return parameters; + } + /** Adds a formatted search string to the set of search strings. */ private void addToSearchStrings(Collection searchStrings, String stringToAdd) { if (searchStrings == null) { @@ -129,9 +155,4 @@ private void addToSearchStrings(Collection searchStrings, String stringT } searchStrings.add(stringToAdd.toUpperCase()); } - - private String convertToJsonUsingGson(Recipe recipe) { - Gson gson = new Gson(); - return gson.toJson(recipe); - } } diff --git a/src/main/java/shef/servlets/RecipeImageUploadUrlServlet.java b/src/main/java/shef/servlets/RecipeImageUploadUrlServlet.java new file mode 100644 index 0000000..57daba7 --- /dev/null +++ b/src/main/java/shef/servlets/RecipeImageUploadUrlServlet.java @@ -0,0 +1,45 @@ +// +// 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 +// +// https://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 shef.servlets; + +import com.google.appengine.api.blobstore.BlobInfo; +import com.google.appengine.api.blobstore.BlobInfoFactory; +import com.google.appengine.api.blobstore.BlobKey; +import com.google.appengine.api.blobstore.BlobstoreService; +import com.google.appengine.api.blobstore.BlobstoreServiceFactory; +import com.google.appengine.api.images.ImagesService; +import com.google.appengine.api.images.ImagesServiceFactory; +import com.google.appengine.api.images.ServingUrlOptions; +import java.net.MalformedURLException; +import java.net.URL; +import java.io.IOException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; +import java.util.List; + +/** Gets the Blobstore URL to send the recipe creation form to. */ +@WebServlet("/recipe-image-upload-url") +public class RecipeImageUploadUrlServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); + String uploadUrl = blobstoreService.createUploadUrl("/new-recipe"); + response.setContentType("text/html"); + response.getWriter().println(uploadUrl); + } +} diff --git a/src/main/java/shef/servlets/TestDisplayRecipesServlet.java b/src/main/java/shef/servlets/TestDisplayRecipesServlet.java deleted file mode 100644 index fcb76b9..0000000 --- a/src/main/java/shef/servlets/TestDisplayRecipesServlet.java +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2019 Google LLC -// -// 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 -// -// https://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 shef.servlets; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import shef.data.TestRecipe; -import com.google.appengine.api.datastore.DatastoreService; -import com.google.appengine.api.datastore.DatastoreServiceFactory; -import com.google.appengine.api.datastore.Entity; -import com.google.appengine.api.datastore.PreparedQuery; -import com.google.appengine.api.datastore.Query; -import com.google.appengine.api.datastore.Query.SortDirection; -import com.google.gson.Gson; -import java.io.IOException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** Servlet responsible for displaying recipes. */ -@WebServlet("/display-recipes") -public class TestDisplayRecipesServlet extends HttpServlet { - - @Override - public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { - Query query = new Query("Recipe").addSort("timestamp", SortDirection.DESCENDING); - - DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); - PreparedQuery results = datastore.prepare(query); - - List testRecipes = new LinkedList<>(); - for (Entity entity : results.asIterable()) { - long id = entity.getKey().getId(); - ArrayList searchStrings = (ArrayList) entity.getProperty("search-strings"); - long timestamp = (long) entity.getProperty("timestamp"); - - TestRecipe testRecipe = new TestRecipe(id, searchStrings, timestamp); - testRecipes.add(testRecipe); - } - - Gson gson = new Gson(); - - response.setContentType("application/json;"); - response.getWriter().println(gson.toJson(testRecipes)); - } -} diff --git a/src/main/java/shef/servlets/UserServlet.java b/src/main/java/shef/servlets/UserServlet.java index b40c44d..86a16d9 100644 --- a/src/main/java/shef/servlets/UserServlet.java +++ b/src/main/java/shef/servlets/UserServlet.java @@ -92,7 +92,7 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr // Get properties that don't come from the request. String id = userService.getCurrentUser().getUserId(); String email = userService.getCurrentUser().getEmail(); - String profilePicKey = getUploadedFileBlobKey(request, "profile-pic"); + String profilePicKey = BlobServlet.getUploadedFileBlobKey(request, "profile-pic"); // Get properties from the request. Map parameterMap = request.getParameterMap(); @@ -130,28 +130,4 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) thr datastore.put(finalUser); response.sendRedirect(redirectUrl); } - - // Returns the String representation of the BlobKey of the uploaded file, or null if the user didn't upload a file. - private String getUploadedFileBlobKey(HttpServletRequest request, String formInputElementName) { - BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService(); - Map> blobs = blobstoreService.getUploads(request); - List blobKeys = blobs.get(formInputElementName); - - // User submitted form without selecting a file, so we can't get a URL. (dev server) - if (blobKeys == null || blobKeys.isEmpty()) { - return null; - } - - // Our form only contains a single file input, so get the first index. - BlobKey blobKey = blobKeys.get(0); - - // User submitted form without selecting a file, so we can't get a URL. (live server) - BlobInfo blobInfo = new BlobInfoFactory().loadBlobInfo(blobKey); - if (blobInfo.getSize() == 0) { - blobstoreService.delete(blobKey); - return null; - } - - return blobKey.getKeyString(); - } } diff --git a/src/main/webapp/create-recipe-blank.html b/src/main/webapp/create-recipe-blank.html index 4de8237..4396dad 100644 --- a/src/main/webapp/create-recipe-blank.html +++ b/src/main/webapp/create-recipe-blank.html @@ -36,7 +36,7 @@ shef - +