1135 lines
26 KiB
Java
1135 lines
26 KiB
Java
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 jakarta.annotation.PostConstruct;
|
|
|
|
import jakarta.faces.model.DataModel;
|
|
import jakarta.faces.model.ListDataModel;
|
|
import jakarta.faces.view.ViewScoped;
|
|
import jakarta.inject.Inject;
|
|
import jakarta.inject.Named;
|
|
import jakarta.faces.event.AjaxBehaviorEvent;
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.primefaces.event.FileUploadEvent;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.orm.jpa.JpaSystemException;
|
|
|
|
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.utils.IngredientDigester;
|
|
|
|
/**
|
|
* Backing bean for display/edit recipe detail
|
|
*
|
|
* @author timh
|
|
*/
|
|
|
|
@Named
|
|
@ViewScoped
|
|
public class RecipeDetailBean implements Serializable {
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
/* Logger */
|
|
|
|
private static final Logger log =
|
|
LoggerFactory.getLogger(RecipeDetailBean.class);
|
|
|
|
// Split lines at 2 or more spaces OR at line terminators.
|
|
private static final String RE_INGSPLIT = "\\s\\s+|\\r?+\\n";
|
|
|
|
/**
|
|
* Default Constructor.
|
|
*/
|
|
public RecipeDetailBean() {
|
|
log.debug("Constructing RecipeDetail " + this);
|
|
}
|
|
|
|
/**
|
|
* Persistency service for Recipes, Shopcats and Categories
|
|
*/
|
|
|
|
@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.
|
|
* @see #stringifyCategories(Recipe)
|
|
*/
|
|
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.
|
|
*
|
|
* @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.getFlash("recipeID");
|
|
userSession.getLastEdit();
|
|
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()));
|
|
stringifyCategories(recipe);
|
|
|
|
this.shop = this.getUserSession().getShoppingList()
|
|
.contains(recipe);
|
|
|
|
log.debug("Set recipe: " + this.recipe);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
stringifyCategories(recipe);
|
|
return recipe;
|
|
}
|
|
|
|
/**
|
|
* Categories are a Set attached to recipe. Build a
|
|
* displayable comma-separated list of them.
|
|
*
|
|
* @param recipe Recipe to get categories from.
|
|
*
|
|
* @see #getCategory()
|
|
*/
|
|
private void stringifyCategories(Recipe recipe) {
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
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("\r\n", "<p/>")
|
|
.replace("\n\n", "<p/>")
|
|
.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
|
|
updateSelectionStatus();
|
|
return null;
|
|
}
|
|
|
|
// =====
|
|
/**
|
|
* Handle entry of ingredient line(s) into text control on
|
|
* the input form.
|
|
*
|
|
* Note: In the original Tobago port of this app, the input
|
|
* was an inputText. In PrimeFaces, this did not preserve
|
|
* line separation characters, so an inputTextArea was used
|
|
* instead.
|
|
*
|
|
* @param event Unused
|
|
*/
|
|
public void ajaxAddIngredient(jakarta.faces.event.AjaxBehaviorEvent event) {
|
|
doAddIngredient();
|
|
}
|
|
|
|
// ===
|
|
/**
|
|
* Listen to the SELECT checkboxes on Ingredients and update
|
|
* the action button statuses.
|
|
*
|
|
* @param event notused
|
|
*/
|
|
public void ajaxSelectionListener(AjaxBehaviorEvent event) {
|
|
updateSelectionStatus();
|
|
}
|
|
|
|
/**
|
|
* Manage the ability buttons based on selections.
|
|
*/
|
|
private void updateSelectionStatus() {
|
|
List<IngredientUI> ingList = getWrappedIngredients();
|
|
final int ingCount = ingList.size();
|
|
|
|
boolean moveUpable = true;
|
|
boolean moveDownable = true;
|
|
boolean selectable = false;
|
|
|
|
for (int i = 0; i < ingCount; i++) {
|
|
boolean selected = ingList.get(i).isSelected();
|
|
if ((i == 0) && selected) {
|
|
moveUpable = false;
|
|
}
|
|
if ((i == (ingCount - 1)) && selected) {
|
|
moveDownable = false;
|
|
}
|
|
selectable |= selected;
|
|
}
|
|
this.setMoveUpAble(moveUpable && selectable);
|
|
this.setMoveDownAble(moveDownable && selectable);
|
|
this.setSelectable(selectable);
|
|
|
|
auditRows(ingList);
|
|
}
|
|
|
|
// ---
|
|
public void setMoveUpAble(boolean moveUpable) {
|
|
this.moveUpable = moveUpable;
|
|
}
|
|
|
|
public void setMoveDownAble(boolean moveDownable) {
|
|
this.moveDownable = moveDownable;
|
|
}
|
|
|
|
public void setSelectable(boolean selectable) {
|
|
this.selectable = selectable;
|
|
}
|
|
|
|
public boolean isMoveUpAble() {
|
|
return this.moveUpable;
|
|
}
|
|
|
|
public boolean isMoveDownAble() {
|
|
return this.moveDownable;
|
|
}
|
|
|
|
public boolean isSelectable() {
|
|
return this.selectable;
|
|
}
|
|
|
|
// ---
|
|
public void ajaxMoveUp() {
|
|
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();
|
|
updateSelectionStatus();
|
|
}
|
|
|
|
private void auditRows(List<IngredientUI> rows) {
|
|
// log.info("=== AUDIT ROWS ===");
|
|
// for ( IngredientUI row : rows ) {
|
|
// log.info((row.isSelected() ? "[X]" : "[ ]" ) +" ROW="+row);
|
|
// }
|
|
// log.info("=== DONE ===");
|
|
}
|
|
|
|
/**
|
|
* Move selected rows down.
|
|
*
|
|
* @param eventUnused
|
|
*/
|
|
public void ajaxMoveDown() {
|
|
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);
|
|
}
|
|
}
|
|
updateSelectionStatus();
|
|
}
|
|
|
|
public void ajaxDeleteItems() {
|
|
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);
|
|
}
|
|
updateSelectionStatus();
|
|
}
|
|
|
|
// =====
|
|
@Inject
|
|
EditShopcatBean editShopcatBean;
|
|
|
|
private boolean moveUpable;
|
|
|
|
private boolean moveDownable;
|
|
|
|
private boolean selectable;
|
|
|
|
/**
|
|
* Invoked when the "E"(dit" button for Ingkey shopping
|
|
* category has been clicked.
|
|
*
|
|
* @param item The item whose ingredient key will have its
|
|
* shopping category edited. Resets the dialog
|
|
* backing bean internal state.
|
|
*/
|
|
public void ajaxEditShopcat(IngredientUI item) {
|
|
editShopcatBean.beginEdit(item.getIngkey(),
|
|
item.getShopCat());
|
|
}
|
|
|
|
/**
|
|
* On "OK" for edit shopcat where shopcat has changed, update
|
|
* the shopcat Entity and the ingredients.
|
|
*/
|
|
public void doUpdateShopcat() {
|
|
|
|
final String key = editShopcatBean.getIngkey();
|
|
if (StringUtils.isBlank(key)) {
|
|
return; // Do not set category if no ingKey
|
|
}
|
|
|
|
final String catname = editShopcatBean.getShopcatName();
|
|
|
|
Shopcat sc = this.recipeService
|
|
.findShopcatForIngredientKey(key);
|
|
if (sc == null) {
|
|
sc = new Shopcat();
|
|
sc.setIngkey(key);
|
|
} else {
|
|
if (StringUtils.equals(sc.getShopcategory(),
|
|
catname)) {
|
|
return; // No change
|
|
}
|
|
}
|
|
sc.setShopcategory(catname);
|
|
|
|
/*
|
|
* Because the database does not have a UNIQUE constraint
|
|
* on ingkeys, we must delete old shopcat(s) for this key
|
|
* before adding (updating) the new shopcat.
|
|
*/
|
|
this.recipeService.deleteShopcatByIngKey(key);
|
|
if (!StringUtils.isBlank(catname)) {
|
|
this.recipeService.saveShopcat(sc);
|
|
}
|
|
updateDisplayedShopcats(key, sc);
|
|
}
|
|
|
|
/**
|
|
* When Shopcat name changes, update the Ingredients. In
|
|
* detailEdit, an AJAX "render" will then update the display.
|
|
* In recipeDetails, nothing actually shows.
|
|
*/
|
|
private void updateDisplayedShopcats(String key,
|
|
Shopcat sc) {
|
|
List<IngredientUI> ingList =
|
|
this.getWrappedIngredients();
|
|
for (IngredientUI ingUI : ingList) {
|
|
if (key.equals(ingUI.getIngkey())) {
|
|
ingUI.setShopCat(sc);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(RE_INGSPLIT);
|
|
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);
|
|
}
|
|
updateSelectionStatus();
|
|
}
|
|
|
|
/**
|
|
* Add ingredient text line to recipe
|
|
*
|
|
* @param ingredientText
|
|
* @see #doAddIngredient(), bulk loader
|
|
* @see #addIngredientList(String)
|
|
*/
|
|
public void addIngredient(String ingredientText) {
|
|
log.debug("Ingredient line: \"" + ingredientText + "\"");
|
|
Ingredient ing =
|
|
IngredientDigester.digest(ingredientText);
|
|
|
|
String ingkey = ing.getIngkey();
|
|
if (!StringUtils.isEmpty(ingkey)) {
|
|
try {
|
|
Shopcat scat = this.recipeService
|
|
.findShopcatForIngredientKey(ingkey);
|
|
ing.setShopCat(scat);
|
|
} catch (JpaSystemException ex) {
|
|
String msg = String.format(
|
|
"Database Error: Unable to fetch info on \"%s\".",
|
|
ingkey);
|
|
log.error(msg);
|
|
JSFUtils.addErrorMessage(msg);
|
|
|
|
}
|
|
}
|
|
// 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));
|
|
}
|
|
|
|
// ===
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
private boolean shop = false;
|
|
|
|
public boolean isShop() {
|
|
return shop;
|
|
}
|
|
|
|
/**
|
|
* Add/remove recipe to shopping list (toggle)
|
|
*/
|
|
public void doShop() {
|
|
shop = !shop;
|
|
List<Recipe> shoppingList =
|
|
userSession.getShoppingList();
|
|
if (shop) {
|
|
shoppingList.add(recipe);
|
|
} else {
|
|
shoppingList.remove(recipe);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save the recipe.
|
|
*
|
|
* @return Main page if OK, re-display with error if cannot
|
|
* save ingredients.
|
|
*/
|
|
public String doSave() {
|
|
// if ! isDirty()...
|
|
// Update recipe object based on UI:
|
|
|
|
if (!saveIngredients()) {
|
|
JSFUtils
|
|
.addErrorMessage("Could not save Ingredients");
|
|
return null;
|
|
}
|
|
|
|
if (recipeService.save(this.getRecipe())) {
|
|
return "recipeDetails?faces-redirect=true";
|
|
} else {
|
|
JSFUtils.addErrorMessage("Save recipe failed");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.jsf";
|
|
}
|
|
|
|
/**
|
|
* Update ingredients and shopcat from UI model
|
|
*
|
|
* @return false if a category has no ingredient key to link
|
|
* it.
|
|
*/
|
|
private boolean saveIngredients() {
|
|
if (!updateIngredientList()) {
|
|
return false;
|
|
}
|
|
updateRecipeCategories(recipe, category);
|
|
// Rebuild ingredients list with groups applied
|
|
updateRecipeGroups(getWrappedIngredients());
|
|
return true;
|
|
}
|
|
|
|
private boolean updateIngredientList() {
|
|
List<IngredientUI> saveIng = getWrappedIngredients();
|
|
List<Ingredient> iList = recipe.getIngredientHash();
|
|
iList.clear();
|
|
for (IngredientUI iui : saveIng) {
|
|
if (iui.isIngGroup()) {
|
|
// Ing group is an attribute of ingredients.
|
|
continue;
|
|
}
|
|
Ingredient ing = iui.getIngredient();
|
|
ing.setRecipe(recipe);
|
|
if (!updateShopcat(iui)) { // Obsolete???
|
|
log.info("Shopcat has not been updated" + iui);
|
|
return false;
|
|
}
|
|
iList.add(ing);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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 shopcat for Ingredient.
|
|
*
|
|
* @param ing Ingredient to update
|
|
* @return true if update succeeded.
|
|
*/
|
|
private boolean updateShopcat(IngredientUI ingUI) {
|
|
final Ingredient ing = ingUI.getIngredient();
|
|
final String ingKey = ing.getIngkey();
|
|
if ((ingKey == null) || (ingKey.isBlank())) {
|
|
ing.setIngkey(null);
|
|
ing.setShopCat(null);
|
|
return true;
|
|
}
|
|
Shopcat scat = recipeService
|
|
.findShopcatForIngredientKey(ingKey);
|
|
if (scat == null) {
|
|
log.debug(
|
|
"No Shopping Category is defined for Ingredient Key "
|
|
+ ingKey);
|
|
// return false;
|
|
}
|
|
ing.setShopCat(scat);
|
|
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<Category>(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;
|
|
}
|
|
|
|
public List<String> cuisineSuggestions(String query) {
|
|
if (!query.equals(cuisinePartial)) {
|
|
setCuisinePartial(query);
|
|
}
|
|
return getCuisineList();
|
|
}
|
|
|
|
/**
|
|
* Set query for eligibility, force list to rebuild.
|
|
*
|
|
* @param cuisinePartial the cuisinePartial to set
|
|
*/
|
|
public void setCuisinePartial(String cuisinePartial) {
|
|
this.cuisinePartial = cuisinePartial;
|
|
// trigger construction of new list.
|
|
this.cuisineList = null;
|
|
}
|
|
|
|
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
|
|
* against 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;
|
|
}
|
|
|
|
// ***
|
|
// Shopcat for IngredientUI
|
|
private List<String> shopcatList;
|
|
|
|
public List<String> shopcatList(String query) {
|
|
if (shopcatList == null) {
|
|
shopcatList = recipeService.findShoppingCategories();
|
|
}
|
|
return shopcatList;
|
|
}
|
|
|
|
public void ajaxUpdateShopcat(IngredientUI item) {
|
|
log.debug("SHOPCAT2 ");
|
|
updateShopcat(item);
|
|
}
|
|
|
|
// ***
|
|
// Editor pages are secured (redirect=true)
|
|
|
|
public String editDescription() {
|
|
this.setDetailTab(0);
|
|
return "detailEdit.xhtml?faces-redirect=true";
|
|
}
|
|
|
|
public String editIngredients() {
|
|
this.setDetailTab(1);
|
|
return "detailEdit?faces-redirect=true";
|
|
}
|
|
|
|
public String editInstructions() {
|
|
this.setDetailTab(2);
|
|
return "detailEdit?faces-redirect=true";
|
|
}
|
|
|
|
public String editNotes() {
|
|
this.setDetailTab(3);
|
|
return "detailEdit?faces-redirect=true";
|
|
}
|
|
|
|
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 List
|
|
*/
|
|
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 = "";
|
|
}
|
|
|
|
public void getAjaxSuggestCategory() {
|
|
if (!this.category.isBlank()) {
|
|
this.category += ", ";
|
|
}
|
|
this.category += catToAdd;
|
|
catToAdd = "";
|
|
}
|
|
|
|
// ***
|
|
|
|
/**
|
|
* Load/replace images. Computes thumbnail.
|
|
*
|
|
* @param event PrimeFaces file upload event object
|
|
*/
|
|
public void ajaxUploadImage(FileUploadEvent event) {
|
|
PictureController.importImage(recipe,
|
|
event.getFile().getContent());
|
|
}
|
|
|
|
/**
|
|
* Remove images from recipe
|
|
*/
|
|
public void ajaxDeleteImage() {
|
|
log.info("Deleting current recipe image");
|
|
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);
|
|
}
|
|
|
|
// *** Add Group
|
|
|
|
private String newGroupName;
|
|
|
|
/**
|
|
* @return the newGroupName
|
|
*/
|
|
public String getNewGroupName() {
|
|
return newGroupName;
|
|
}
|
|
|
|
/**
|
|
* @param newGroupName the newGroupName to set
|
|
*/
|
|
public void setNewGroupName(String newGroupName) {
|
|
this.newGroupName = newGroupName;
|
|
}
|
|
|
|
/**
|
|
* Add new group to bottom of model as AJAX operation.
|
|
*
|
|
* @return null
|
|
*/
|
|
public void doAddGroup() {
|
|
IngredientUI iui = new IngredientUI(null);
|
|
iui.setIngGroup(true);
|
|
iui.setItem(this.getNewGroupName());
|
|
List<IngredientUI> ingUIList =
|
|
this.getWrappedIngredients();
|
|
ingUIList.add(iui);
|
|
this.setNewGroupName(""); // Clear for next time!
|
|
}
|
|
}
|