package com.mousetech.gourmetj; import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import javax.annotation.PostConstruct; import javax.faces.event.AjaxBehaviorEvent; import javax.faces.model.DataModel; import javax.faces.model.ListDataModel; import javax.faces.view.ViewScoped; import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.Part; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mousetech.gourmetj.IngredientUI; import com.mousetech.gourmetj.UserSession; import com.mousetech.gourmetj.persistence.model.Category; import com.mousetech.gourmetj.persistence.model.Ingredient; import com.mousetech.gourmetj.persistence.model.Recipe; import com.mousetech.gourmetj.persistence.model.Shopcat; import com.mousetech.gourmetj.persistence.service.RecipeService; import com.mousetech.gourmetj.springweb.PictureController; import com.mousetech.gourmetj.IngredientDigester; /** * Backing bean for display/edit recipe detail * * @author timh * @since Jun 28, 2012 TODO: Cross-reference ingredients to * keylookup TODO: Cross-reference shopcats */ @Named @ViewScoped public class RecipeDetailBean implements Serializable { private static final long serialVersionUID = 1L; /* Logger */ private static final Logger log = LoggerFactory.getLogger(RecipeDetailBean.class); /** * Default Constructor. */ public RecipeDetailBean() { log.warn("Constructing RecipeDetail " + this); } /** * Persistency service for Recipes */ @Inject transient RecipeService recipeService; public void setRecipeService(RecipeService service) { log.debug("INJECT RECIPESERVICE===" + service); this.recipeService = service; } private String instructions = null; @Inject UserSession userSession; /** * @return the userSession */ public UserSession getUserSession() { return userSession; } /** * @param userSession the userSession to set */ public void setUserSession(UserSession userSession) { this.userSession = userSession; } /** * @param instructions the instructions to set * @see #getInstructions() */ public void setInstructions(String instructions) { this.instructions = instructions; } /** * @param ingredients the ingredients to set */ public void setIngredients( DataModel ingredients) { this.ingredients = ingredients; } // ** private String ingredientText = ""; /** * @return the ingredientText */ public String getIngredientText() { return ingredientText; } /** * @param ingredientText the ingredientText to set */ public void setIngredientText(String ingredientText) { this.ingredientText = ingredientText; } // ================================== private Recipe recipe = new Recipe(); public Recipe getRecipe() { if (this.recipe.getId() == null) { Long recipeId = userSession.getLastEdit(); if (recipeId != null) { loadRecipe(recipeId); } } return this.recipe; } /** * @param recipe the recipe to set Required by JSF 2.2, but * DON'T USE! */ public void setRecipe(Recipe recipe) { this.recipe = recipe; } public String getTitle() { return "ATITLE"; } /**/ transient DataModel ingredients; /**/ private String category = ""; /** * @return the category as a comma-separated list. */ public String getCategory() { return category; } /** * @param category the category (list) to set * @see #getCategory() */ public void setCategory(String category) { this.category = category; this.setDirty(); } private boolean dirty; private String modifications; /** * @return the dirty */ public boolean isDirty() { return dirty; } /** * @param dirty the dirty to set */ public void setDirty(boolean dirty) { this.dirty = dirty; } public void setDirty() { setDirty(true); } /** * @return the ingredients */ public DataModel getIngredients() { if (ingredients == null) { ingredients = new ListDataModel<>(); ingredients .setWrappedData(new ArrayList(1)); } return ingredients; } /** * After construction and injection, we obtain the recipe ID * passed to us, if any and load the recipe. It's also stored * in @see UserSession for the detail editor and * * @see PictureController. */ @PostConstruct private void init() { this.recipe = userSession.getRecipe(); /** * For "create new, this recipe is a blank constructed * and passed from main page. For Detail display, it's * null and ID come in via Flash scope. For Detail Edit, * it's passed from detail display or "create new". */ if (this.recipe == null) { Long rid = (Long) JSFUtils.flashScope().get("recipeID"); if (rid != null) { this.recipe = loadRecipe(rid); } else { // alternative (and probably dead) version of "new // recipe". this.recipe = new Recipe(); return; } } userSession.setRecipe(this.recipe); getIngredients().setWrappedData( buildIngredientFacade(recipe.getIngredientHash())); } /** * Load recipe from database. @see RecipeDetailBean#init() * * @param recipeId ID of recipe to load * @return loaded recipe or null, if not found. */ private Recipe loadRecipe(Long recipeId) { Recipe recipe = recipeService.findDetails(recipeId); if ( recipe == null ) { return null; } Set cList = recipe.getCategories(); StringBuffer sb = new StringBuffer(35); boolean first = true; for (Category cat : cList) { if (first) { first = false; } else { sb.append(", "); } sb.append(cat.getCategory()); } this.category = sb.toString(); return recipe; } /** * Wrap a list of Ingredients in a decorative facade for * better displayability. * * Ingredient group is actually part of Ingredient. Add dummy * "ingredients" to make them separate lines. * * @param ingredients * @return Wrapped list */ private List buildIngredientFacade( List ingredients) { final List list = new ArrayList<>(ingredients.size()); String currentIngGroup = null; for (Ingredient ing : ingredients) { String ingGroup = ing.getInggroup(); if (ingGroup != null && ingGroup.isBlank()) { ingGroup = null; } if (!Objects.equals(ingGroup, currentIngGroup)) { Ingredient dummy = new Ingredient(); IngredientUI groupIng = new IngredientUI(dummy); groupIng.setItem(ingGroup); groupIng.setIngGroup(true); list.add(groupIng); currentIngGroup = ingGroup; } IngredientUI ingUi = new IngredientUI(ing); list.add(ingUi); // Shopcat is an eager fetch on Ingredient Shopcat shopCat = ing.getShopCat(); if (shopCat != null) { ingUi.setShopCat(shopCat.getShopcategory()); } } return list; } /** * @return the instructions (cached) */ public String getInstructions() { if (instructions == null) { instructions = formatInstructions( getRecipe().getInstructions()); } return instructions; } /** * @return the instructions (cached) */ public String getModifications() { if (this.modifications == null) { this.modifications = formatInstructions( getRecipe().getModifications()); } return this.modifications; } /** * Convert instruction plain-text to HTML form * * @param instructions * @return */ private String formatInstructions(String instructions) { if (instructions == null) { return ""; } String s = instructions.replace("\n\n", "

"); s = s.replace("\n", "
"); return s; } /** * Action for "Add Ingredient" button * * @return null - stay on page. The Ingredient list table * will update. */ public String doAddIngredient() { this.addIngredientList(this.getIngredientText()); setIngredientText(""); // clear for next entry return null; } // ===== /** * Handle entry of a single ingredient line into the input * form. * * @param event Unused??? */ public void ajaxAddIngredient(AjaxBehaviorEvent event) { doAddIngredient(); } public void ajaxMoveUp(AjaxBehaviorEvent event) { if (!isMoveUpAble()) { JSFUtils.addErrorMessage("Cannot move up."); return; } final List rows = getWrappedIngredients(); final int ingSize = rows.size(); for (int i = 1; i < ingSize; i++) { IngredientUI r = rows.get(i); if (r.isSelected()) { // swap with preceding row. rows.remove(i); rows.add(i - 1, r); } } this.setDirty(); } /** * Move selected rows down. * * @param eventUnused */ public void ajaxMoveDown(AjaxBehaviorEvent event) { if (!isMoveDownAble()) { JSFUtils.addErrorMessage("Cannot move down."); return; } final List rows = getWrappedIngredients(); final int ingSize = rows.size() - 1; for (int i = ingSize; i > 0; i--) { IngredientUI r = rows.get(i - 1); if (r.isSelected()) { // swap with following row. rows.remove(i - 1); rows.add(i, r); } } } public void ajaxDeleteItems(AjaxBehaviorEvent event) { final List rows = getWrappedIngredients(); List selectedRows = new ArrayList(); for (IngredientUI row : rows) { if (row.isSelected()) { this.dirty = true; // Delete row from list. selectedRows.add(row); } } // 2nd stage to avoid ConcurrentModificationException for (IngredientUI row : selectedRows) { rows.remove(row); } } /** * Bulk add for ingredients. Looks for long input and if * found, tries to split it up and add as multiple * ingredients. * * @param ingredientText2 * @see #addIngredient(String) */ private void addIngredientList(String ingredientTextLines) { if (ingredientTextLines.length() < 40) { addIngredient(ingredientTextLines); return; } // Otherwise, try for split. String[] lineArray = ingredientTextLines.split(" "); for (String line : lineArray) { if (line.isBlank()) { continue; // actually should discard any above // this } if (line.toLowerCase().contains("ingredients")) { continue; // actually should discard any above // this } addIngredient(line); } } /** * Add ingredient text line to recipe * * @param ingredientText * @see #doAddIngredient(), bulk loader * @see #addIngredientList(String) */ public void addIngredient(String ingredientText) { log.info("Ingredient line: \"" + ingredientText + "\""); Ingredient ing = IngredientDigester.digest(ingredientText); // get ing list size, set ing position, append List ingredients = getWrappedIngredients(); int lsize = ingredients.size(); ing.setPosition(lsize + 1); ingredients.add(new IngredientUI(ing)); } public boolean isSelectionActive() { List rows = getWrappedIngredients(); for (IngredientUI row : rows) { if (row.isSelected()) { return true; } } return false; } public void setSelectionActive(boolean value) { // This is required by JBoss JSF, but the property is // read-only. } public void setMoveUpAble() { } public void setMoveDownAble() { } public boolean isMoveUpAble() { if (isSelectionActive()) { List rows = getWrappedIngredients(); return !rows.get(0).isSelected(); } return false; } public boolean isMoveDownAble() { return true; } // === /** * Convenience method to get ingredientUI list without * constant whining about unchecked casting. * * @return */ @SuppressWarnings("unchecked") private List getWrappedIngredients() { return (List) this.getIngredients() .getWrappedData(); } /** * Save the recipe. * * @return Main page if OK, re-display with error if cannot * save ingredients. */ public String doSave() { if (!saveIngredients()) { return null; } updateRecipeCategories(recipe, category); // Rebuild ingredients list with groups applied updateRecipeGroups(getWrappedIngredients()); recipeService.save(recipe); userSession.setRecipe(null); setDirty(false); return "main"; } /** * Delete the recipe. This will cause ingredients to be * deleted by cascade. * * @return Main page */ public String doDelete() { recipeService.delete(this.recipe); this.userSession.setLastEdit(null); // Don't point to me! return "main"; } /** * Apply ingredient group IDs (optional) to individual * ingredients. * * @param wrappedIngredients The wrapped ingredient facade. */ private void updateRecipeGroups( List wrappedIngredients) { String ingGroup = null; for (IngredientUI ingUI : wrappedIngredients) { if (ingUI.isIngGroup()) { ingGroup = ingUI.getItem(); } else { ingUI.getIngredient().setInggroup(ingGroup); } } } /** * Update ingredients and shopcat from UI model * * @return false if a category has no ingredient key to link * it. */ private boolean saveIngredients() { List saveIng = getWrappedIngredients(); List iList = recipe.getIngredientHash(); iList.clear(); for (IngredientUI iui : saveIng) { Ingredient ing = iui.getIngredient(); ing.setRecipe(recipe); String ingKey = iui.getIngkey(); String shopCatName = iui.getShopCat(); Shopcat scat = ing.getShopCat(); if (scat == null) { if ((ingKey != null) && !ingKey.isBlank()) { scat = new Shopcat(); scat.setIngkey(ingKey); scat.setShopcategory(shopCatName); ing.setShopCat(scat); } else { JSFUtils.addErrorMessage( "Shopping Category requires an Ingredient Key"); return false; } } else { if ((ingKey == null) || ingKey.isBlank()) { ing.setShopCat(null); } else { ing.getShopCat() .setShopcategory(shopCatName); } } iList.add(ing); } return true; } /** * Parse out the comma-separated category text control and * post the results as children of the recipe * * @param recipe2 * @param category2 */ private void updateRecipeCategories(Recipe recipe2, String category2) { final Set oldList = recipe2.getCategories(); List newList = new ArrayList(); String[] cats = this.category.split(","); for (String s : cats) { s = s.trim(); if (!s.isEmpty()) { Category newCat = new Category(); newCat.setRecipe(recipe2); newCat.setCategory(s); newList.add(newCat); } } for (Category cat : newList) { // For existing Categories, use existing ID. Category ocat = searchCategory(oldList, cat); if (ocat != null) { cat.setId(ocat.getId()); } } recipe.setCategories(new HashSet(newList)); } private Category searchCategory(Set oldList, Category cat) { String catName = cat.getCategory(); for (Category c : oldList) { if (catName.equals(c.getCategory())) { return c; } } return null; } /** * Partial input sent via AJAX when suggestion box is keyed. * called each time a character is entered or removed, * subject (I presume) to the minimum character limit. * * Use this to assemble the suggestion list. */ private String cuisinePartial; /** * @return the cuisinePartial */ public String getCuisinePartial() { return cuisinePartial; } /** * @param cuisinePartial the cuisinePartial to set */ public void setCuisinePartial(String cuisinePartial) { this.cuisinePartial = cuisinePartial; this.cuisineList = null; // trigger construction of new // list. } private List masterCuisineList = null; private List cuisineList = null; /** * Load the Cuisine list, which is assembled by scanning * cuisine fields from all recipes in the database at the * time we are called. */ private List loadCuisineList() { List cuisines = recipeService.findCuisines(); return cuisines; } /** * @return the master cuisineList */ public List getMasterCuisineList() { if (this.masterCuisineList == null) { this.masterCuisineList = loadCuisineList(); } return this.masterCuisineList; } /** * @return the cuisineList built by matching the master list * agaist the partial cuisine names. */ public List getCuisineList() { if (this.cuisineList == null) { this.cuisineList = buildCuisineList(); } return this.cuisineList; } /** * Using the cuisinePartial, build a subset of the master * cuisine list. * * @return */ private List buildCuisineList() { List list = new ArrayList(); String partial = this.cuisinePartial; // Handle cases where we aren't set up, or the partial // input is blank. if (partial == null) { return list; } partial = partial.trim(); if (partial.isEmpty()) { return list; } List masterList = this.getMasterCuisineList(); for (String s : masterList) { if (s.contains(this.cuisinePartial)) { list.add(s); } } return list; } /** * @param cuisineList the cuisineList to set */ public void setCuisineList(List cuisineList) { this.cuisineList = cuisineList; } // *** private String shopcatPartial; /** * @return the shopcatPartial */ public String getShopcatPartial() { return shopcatPartial; } /** * @param shopcatPartial the shopcatPartial to set */ public void setShopcatPartial(String shopcatPartial) { this.shopcatPartial = shopcatPartial; } private List shopcatList; public List getShopcatList() { if (shopcatList == null) { shopcatList = recipeService.findShoppingCategories(); } return shopcatList; } // *** public String editDescription() { this.setDetailTab(0); return "detailEdit"; } public String editIngredients() { this.setDetailTab(1); return "detailEdit"; } public String editInstructions() { this.setDetailTab(2); return "detailEdit"; } public String editNotes() { this.setDetailTab(3); return "detailEdit"; } private void setDetailTab(int i) { this.userSession.setDetailTab(i); } // *** // *** Category suggestions private String catToAdd = ""; private List suggestCategory = null; /** * @return the catToAdd */ public String getCatToAdd() { return catToAdd; } /** * @param catToAdd the catToAdd to set */ public void setCatToAdd(String catToAdd) { this.catToAdd = catToAdd; } /** * @return the suggestCategory */ public List getSuggestCategory() { if (suggestCategory == null) { suggestCategory = loadCategories(); } return suggestCategory; } private List loadCategories() { List catList = this.recipeService.findCategories(); return catList; } /** * @param suggestCategory the suggestCategory to set */ public void setSuggestCategory( List suggestCategory) { this.suggestCategory = suggestCategory; } public void ajaxSuggestCategory(AjaxBehaviorEvent event) { if (!this.category.isBlank()) { this.category += ", "; } this.category += catToAdd; catToAdd = ""; } // *** Part imageFile = null; /** * @return the imageFile set by the image upload control */ public Part getImageFile() { return imageFile; } /** * @param imageFile the imageFile to set */ public void setImageFile(Part imageFile) { this.imageFile = imageFile; } /** * Load/replace images. Computes thumbnail. * @param event Notused */ public void ajaxUploadImage(AjaxBehaviorEvent event) { // String fileType = imageFile.getContentType(); PictureController.importImage(recipe, imageFile); } /** * Remove images from recipe * @param event Notused */ public void ajaxDeleteImage(AjaxBehaviorEvent event) { this.recipe.setImage(null); this.recipe.setThumb(null); } /** * Return marker for image. Unlike normal JSF, I don't * care if it gets multiple times and returns different values. * * @return "random" string */ public String getCurrentTime() { long now = new java.util.Date().getTime(); return String.valueOf(now); } }