gourmetj-springboot/src/main/java/com/mousetech/gourmetj/RecipeDetailBean.java

899 lines
20 KiB
Java
Raw Normal View History

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<IngredientUI> 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<IngredientUI> 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<IngredientUI> getIngredients() {
if (ingredients == null) {
ingredients = new ListDataModel<>();
ingredients
.setWrappedData(new ArrayList<IngredientUI>(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<Category> 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<IngredientUI> buildIngredientFacade(
List<Ingredient> ingredients) {
final List<IngredientUI> 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", "<p/>");
s = s.replace("\n", "<br/>");
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<IngredientUI> 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<IngredientUI> 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<IngredientUI> rows = getWrappedIngredients();
List<IngredientUI> selectedRows =
new ArrayList<IngredientUI>();
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<IngredientUI> ingredients = getWrappedIngredients();
int lsize = ingredients.size();
ing.setPosition(lsize + 1);
ingredients.add(new IngredientUI(ing));
}
public boolean isSelectionActive() {
List<IngredientUI> 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<IngredientUI> 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<IngredientUI> getWrappedIngredients() {
return (List<IngredientUI>) 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<IngredientUI> 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<IngredientUI> saveIng = getWrappedIngredients();
List<Ingredient> 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<Category> oldList = recipe2.getCategories();
List<Category> newList = new ArrayList<Category>();
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<Category> 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<String> masterCuisineList = null;
private List<String> 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<String> loadCuisineList() {
List<String> cuisines = recipeService.findCuisines();
return cuisines;
}
/**
* @return the master cuisineList
*/
public List<String> 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<String> 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<String> buildCuisineList() {
List<String> list = new ArrayList<String>();
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<String> 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<String> 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<String> shopcatList;
public List<String> 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<String> 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<String> getSuggestCategory() {
if (suggestCategory == null) {
suggestCategory = loadCategories();
}
return suggestCategory;
}
private List<String> loadCategories() {
List<String> catList =
this.recipeService.findCategories();
return catList;
}
/**
* @param suggestCategory the suggestCategory to set
*/
public void setSuggestCategory(
List<String> 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);
}
}