package com.brew.brewshop.xml;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
import com.brew.brewshop.R;
import com.brew.brewshop.fragments.RecipeListFragment;
import com.brew.brewshop.storage.recipes.BeerStyle;
import com.brew.brewshop.storage.recipes.Hop;
import com.brew.brewshop.storage.recipes.HopAddition;
import com.brew.brewshop.storage.recipes.HopUsage;
import com.brew.brewshop.storage.recipes.Malt;
import com.brew.brewshop.storage.recipes.MaltAddition;
import com.brew.brewshop.storage.recipes.Quantity;
import com.brew.brewshop.storage.recipes.Recipe;
import com.brew.brewshop.storage.recipes.Weight;
import com.brew.brewshop.storage.recipes.Yeast;
import com.brew.brewshop.storage.style.BjcpCategory;
import com.brew.brewshop.storage.style.BjcpCategoryList;
import com.brew.brewshop.storage.style.BjcpCategoryStorage;
import com.brew.brewshop.storage.style.BjcpSubcategory;
import com.brew.brewshop.storage.style.VitalStatistics;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathException;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
public class BeerXMLReader extends AsyncTask<InputStream, Integer, Recipe[]> {
private static final String TAG = BeerXMLReader.class.getName();
private ProgressDialog dialog;
private Context mContext = null;
private RecipeListFragment parentFragment;
private BjcpCategoryList mBjcpCategoryList;
public BeerXMLReader(RecipeListFragment parentFragment) {
this.parentFragment = parentFragment;
mContext = parentFragment.getActivity();
dialog = new ProgressDialog(mContext);
mBjcpCategoryList = new BjcpCategoryStorage(mContext).getStyles();
}
public BeerXMLReader(Context context) {
this.mContext = context;
mBjcpCategoryList = new BjcpCategoryStorage(context).getStyles();
}
@Override
protected void onPreExecute() {
dialog.setMessage(mContext.getString(R.string.open_recipe_progress));
dialog.show();
}
@Override
protected Recipe[] doInBackground(InputStream... inputStreams) {
return readInputStream(inputStreams[0]);
}
@Override
protected void onProgressUpdate(Integer... progress) {
int current = progress[0] + 1;
int total = progress[1];
String message;
if (total > 1) {
message = String.format(
mContext.getString(R.string.open_recipes_progress),
current, total);
} else {
message = mContext.getString(R.string.open_recipe_progress);
}
if (dialog != null) {
dialog.setMessage(message);
} else {
Log.i(TAG, message);
}
}
@Override
protected void onPostExecute(final Recipe[] recipes) {
parentFragment.addRecipes(recipes);
if (dialog.isShowing()) {
dialog.dismiss();
}
}
private Recipe[] readInputStream(InputStream inputStream) {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = null;
try {
dBuilder = dbFactory.newDocumentBuilder();
} catch (ParserConfigurationException e1) {
Log.e(TAG, "Couldn't create DocumentBuilderFactory", e1);
return null;
}
Document recipeDocument = null;
XPath xp = null;
try {
recipeDocument = dBuilder.parse(inputStream);
} catch (Exception e) {
Log.e(TAG, "Couldn't read XML File", e);
return null;
}
return readDocument(recipeDocument);
}
public Recipe[] readFile(File beerXMLFile) {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = null;
try {
dBuilder = dbFactory.newDocumentBuilder();
} catch (ParserConfigurationException e1) {
Log.e(TAG, "Couldn't create DocumentBuilderFactory", e1);
return null;
}
Document recipeDocument = null;
try {
recipeDocument = dBuilder.parse(beerXMLFile);
} catch (Exception e) {
Log.e(TAG, beerXMLFile + " isn't an XML File");
return null;
}
return readDocument(recipeDocument);
}
private Recipe[] readDocument(Document recipeDocument) {
XPath xp = null;
try {
xp = XPathFactory.newInstance().newXPath();
NodeList recipeList =
(NodeList) xp.evaluate(
"/RECIPES/RECIPE", recipeDocument, XPathConstants.NODESET);
if (recipeList.getLength() == 0) {
Log.i("BrewShop", "No Recipes found in file");
return null;
}
return readRecipe(recipeDocument, null);
} catch (XPathException xpe) {
Log.e(TAG, "Couldn't run XPATH", xpe);
return null;
}
}
private Recipe[] readRecipe(Document beerDocument, String name) throws XPathException {
String recipeSelector = "";
if (name != null) {
recipeSelector = "[NAME[text()=\"" + name + "\"]]";
}
XPath xp = XPathFactory.newInstance().newXPath();
NodeList recipeData =
(NodeList) xp.evaluate(
"/RECIPES/RECIPE" + recipeSelector, beerDocument, XPathConstants.NODESET);
ArrayList<Recipe> recipeList = new ArrayList<Recipe>();
for (int i = 0; i < recipeData.getLength(); i++) {
publishProgress(i, recipeData.getLength());
try {
recipeList.add(readSingleRecipe(recipeData.item(i)));
} catch (XPathException xpe) {
Log.e(TAG, "Couldn't read the recipe at index " + i, xpe);
} catch (NumberFormatException nfe) {
Log.e(TAG, "Couldn't read the recipe at index " + i + " due to a bad number", nfe);
}
}
return recipeList.toArray(new Recipe[recipeList.size()]);
}
private Recipe readSingleRecipe(Node recipeNode) throws XPathException, NumberFormatException {
XPath xp = XPathFactory.newInstance().newXPath();
Recipe recipe = new Recipe();
// otherwise get the details from the recipe
String recipeName = (String) xp.evaluate("NAME/text()", recipeNode, XPathConstants.STRING);
String brewerName = (String) xp.evaluate("BREWER/text()", recipeNode, XPathConstants.STRING);
String notes = (String) xp.evaluate("NOTES/text()", recipeNode, XPathConstants.STRING);
double efficiency = getDouble(recipeNode, "EFFICIENCY", xp);
double batchSize = getDouble(recipeNode, "BATCH_SIZE", xp);
double boilSize = getDouble(recipeNode, "BOIL_SIZE", xp);
double boilTime = getDouble(recipeNode, "BOIL_TIME", xp);
recipe.setName(recipeName);
recipe.setBrewerName(brewerName);
recipe.setNotes(notes);
recipe.setBatchVolume(Quantity.convertUnit("litres", "gallons US", batchSize));
recipe.setBoilVolume(Quantity.convertUnit("litres", "gallons US", boilSize));
recipe.setBoilTime(boilTime);
recipe.setEfficiency(efficiency);
NodeList hopsList = (NodeList) xp.evaluate("HOPS", recipeNode, XPathConstants.NODESET);
parseHops(recipe, hopsList);
NodeList maltList = (NodeList) xp.evaluate("FERMENTABLES", recipeNode, XPathConstants.NODESET);
parseMalts(recipe, maltList);
NodeList yeastList = (NodeList) xp.evaluate("YEASTS", recipeNode, XPathConstants.NODESET);
parseYeasts(recipe, yeastList);
Node styleList = (Node) xp.evaluate("STYLE", recipeNode, XPathConstants.NODE);
parseStyle(recipe, styleList);
return recipe;
}
private void parseHops(Recipe recipe, NodeList hops) throws XPathException, NumberFormatException {
if (hops == null || hops.getLength() == 0) {
return;
}
XPath xp = XPathFactory.newInstance().newXPath();
NodeList hopList = (NodeList) xp.evaluate("HOP", hops.item(0), XPathConstants.NODESET);
for (int i = 0; i < hopList.getLength(); i++) {
Node hop = hopList.item(i);
// Get the values
String name = (String) xp.evaluate("NAME", hop, XPathConstants.STRING);
String temp = (String) xp.evaluate("AMOUNT", hop, XPathConstants.STRING);
double amount = Double.parseDouble(temp);
temp = (String) xp.evaluate("ALPHA", hop, XPathConstants.STRING);
double alpha = Double.parseDouble(temp);
temp = (String) xp.evaluate("TIME", hop, XPathConstants.STRING);
int time = (int) Math.round(Double.parseDouble(temp));
String use = (String) xp.evaluate("USE", hop, XPathConstants.STRING);
Hop hopObject = new Hop();
hopObject.setName(name);
hopObject.setPercentAlpha(alpha);
HopAddition hopAddition = new HopAddition();
hopAddition.setHop(hopObject);
hopAddition.setWeight(Weight.fromKg(amount));
// Not all of these are used by beerxml 1.0, but we can change as and when
if (use.equalsIgnoreCase("boil")) {
hopAddition.setUsage(HopUsage.BOIL);
hopAddition.setBoilTime(time);
} else if (use.equalsIgnoreCase("dry hop")) {
hopAddition.setUsage(HopUsage.DRY_HOP);
int days = (time / 60) / 24;
hopAddition.setDryHopDays(days);
} else if (use.equalsIgnoreCase("mash")) {
hopAddition.setUsage(HopUsage.MASH);
hopAddition.setBoilTime(time);
} else if (use.equalsIgnoreCase("first wort")) {
hopAddition.setUsage(HopUsage.FIRST_WORT);
hopAddition.setBoilTime(time);
} else if (use.equalsIgnoreCase("aroma") || use.equalsIgnoreCase("whirlpool")) {
hopAddition.setUsage(HopUsage.WHIRLPOOL);
hopAddition.setBoilTime(time);
}
// Everything is OK here, so add it in.
recipe.addHop(hopAddition);
}
}
private void parseMalts(Recipe recipe, NodeList malts) throws XPathException, NumberFormatException {
if (malts == null || malts.getLength() == 0) {
return;
}
XPath xp = XPathFactory.newInstance().newXPath();
NodeList fermentableList = (NodeList) xp.evaluate("FERMENTABLE", malts.item(0), XPathConstants.NODESET);
for (int i = 0; i < fermentableList.getLength(); i++) {
try {
Node fermentable = fermentableList.item(i);
// Get the values
String name = (String) xp.evaluate("NAME", fermentable, XPathConstants.STRING);
String type = (String) xp.evaluate("TYPE", fermentable, XPathConstants.STRING);
type = type.toLowerCase();
boolean mashed = type.contains("malt") || type.contains("grain");
double amount = getDouble(fermentable, "AMOUNT", xp);
double color = getDouble(fermentable, "COLOR", xp);
double yield = getDouble(fermentable, "YIELD", xp);
Malt malt = new Malt();
malt.setName(name);
malt.setGravity(1 + yield * .01 * (BeerXMLCommon.SUCROSE_GRAVITY - 1));
malt.setColor(color);
malt.setMashed(mashed);
MaltAddition maltAddition = new MaltAddition();
maltAddition.setWeight(Weight.fromKg(amount));
maltAddition.setMalt(malt);
recipe.addFermentable(maltAddition);
} catch (NumberFormatException nfe) {
Log.e(TAG, "Couldn't parse a number", nfe);
} catch (Exception e) {
if (e instanceof XPathException) {
throw (XPathException) e;
} else {
Log.e(TAG, "Couldn't read the weight for a malt", e);
}
}
}
}
private void parseYeasts(Recipe recipe, NodeList yeasts) throws XPathException, NumberFormatException {
if (yeasts == null || yeasts.getLength() == 0) {
return;
}
XPath xp = XPathFactory.newInstance().newXPath();
NodeList yeastList = (NodeList) xp.evaluate("YEAST", yeasts.item(0), XPathConstants.NODESET);
for (int i = 0; i < yeastList.getLength(); i++) {
try {
Node yeastItem = yeastList.item(i);
String name = (String) xp.evaluate("NAME", yeastItem, XPathConstants.STRING);
String type = (String) xp.evaluate("TYPE", yeastItem, XPathConstants.STRING);
String form = (String) xp.evaluate("FORM", yeastItem, XPathConstants.STRING);
String attenuation = (String) xp.evaluate("ATTENUATION", yeastItem, XPathConstants.STRING);
Yeast yeast = new Yeast();
yeast.setName(name);
yeast.setAttenuation(Double.parseDouble(attenuation));
recipe.addYeast(yeast);
} catch (NumberFormatException nfe) {
Log.e(TAG, "Couldn't parse a number", nfe);
} catch (Exception e) {
if (e instanceof XPathException) {
throw (XPathException) e;
} else {
Log.e("BrewShop", "Couldn't read the weight for a malt", e);
}
}
}
}
private void parseStyle(Recipe recipe, Node style) throws XPathExpressionException {
if (style == null) {
return;
}
XPath xp = XPathFactory.newInstance().newXPath();
String name = (String) xp.evaluate("NAME", style, XPathConstants.STRING);
String notes = (String) xp.evaluate("NOTES", style, XPathConstants.STRING);
int categoryNumber = getInteger(style, "CATEGORY_NUMBER", xp);
String styleLetter = (String) xp.evaluate("STYLE_LETTER", style, XPathConstants.STRING);
String styleGuide = (String) xp.evaluate("STYLE_GUIDE", style, XPathConstants.STRING);
String type = (String) xp.evaluate("TYPE", style, XPathConstants.STRING);
double ogMin = getDouble(style, "OG_MIN", xp);
double ogMax = getDouble(style, "OG_MAX", xp);
double fgMin = getDouble(style, "FG_MIN", xp);
double fgMax = getDouble(style, "FG_MAX", xp);
double ibuMin = getDouble(style, "IBU_MIN", xp);
double ibuMax = getDouble(style, "IBU_MAX", xp);
double colorMin = getDouble(style, "COLOR_MIN", xp);
double colorMax = getDouble(style, "COLOR_MAX", xp);
double abvMin = getDouble(style, "ABV_MIN", xp);
double abvMax = getDouble(style, "ABV_MAX", xp);
// Check to see if we have this style
BjcpCategory bjcpCategory = mBjcpCategoryList.findByName(name);
if (bjcpCategory == null && name.contains("&")) {
bjcpCategory = mBjcpCategoryList.findByName(name.replace("&", "and"));
}
if (bjcpCategory == null) {
return;
}
BjcpSubcategory bjcpSubcategory = bjcpCategory.findSubcategoryByLetter(styleLetter);
VitalStatistics vitalStatistics = null;
if (bjcpCategory.getSubcategories() == null || bjcpCategory.getSubcategories().isEmpty()) {
vitalStatistics = bjcpCategory.getGuidelines().getVitalStatistics();
} else if (bjcpSubcategory != null) {
vitalStatistics = bjcpSubcategory.getGuidelines().getVitalStatistics();
}
BeerStyle beerStyle = new BeerStyle();
beerStyle.setStyleName(bjcpCategory.getName());
if (bjcpSubcategory != null) {
beerStyle.setSubstyleName(bjcpSubcategory.getName());
}
beerStyle.setStyleGuide(styleGuide);
beerStyle.setDescription(notes);
beerStyle.setType(type);
if (vitalStatistics == null) {
if (abvMax == abvMin || abvMax == 0.0 || abvMin == 0.0) {
abvMax = (76.08 * (ogMax - fgMin) / (1.775 - ogMax)) * (fgMin / 0.794);
abvMin = (76.08 * (ogMin - fgMax) / (1.775 - ogMin)) * (fgMax / 0.794);
}
beerStyle.setAbvMax(abvMax);
beerStyle.setAbvMin(abvMin);
beerStyle.setFgMax(fgMax);
beerStyle.setFgMin(fgMin);
beerStyle.setOgMax(ogMax);
beerStyle.setOgMin(ogMin);
beerStyle.setSrmMax(colorMax);
beerStyle.setSrmMin(colorMin);
beerStyle.setIbuMax(ibuMax);
beerStyle.setIbuMin(ibuMin);
} else {
beerStyle.setOgMin(vitalStatistics.getOgMin());
beerStyle.setOgMax(vitalStatistics.getOgMax());
beerStyle.setFgMin(vitalStatistics.getFgMin());
beerStyle.setFgMax(vitalStatistics.getFgMax());
beerStyle.setIbuMin(vitalStatistics.getIbuMin());
beerStyle.setIbuMax(vitalStatistics.getIbuMax());
beerStyle.setSrmMin(vitalStatistics.getSrmMin());
beerStyle.setSrmMax(vitalStatistics.getSrmMax());
beerStyle.setAbvMin(vitalStatistics.getAbvMin());
beerStyle.setAbvMax(vitalStatistics.getAbvMax());
}
recipe.setStyle(beerStyle);
}
private double getDouble(NodeList element, String name, XPath xp) {
try {
String temp = (String) xp.evaluate(name.toUpperCase(), element, XPathConstants.STRING);
return Double.parseDouble(temp);
} catch (XPathException xpe) {
Log.e(TAG, "Failed to run XPATH", xpe);
return 0.0;
} catch (NumberFormatException nfe) {
Log.e(TAG, "Failed to parse string", nfe);
return 0.0;
}
}
private double getDouble(Node element, String name, XPath xp) {
try {
String temp = (String) xp.evaluate(name.toUpperCase(), element, XPathConstants.STRING);
return Double.parseDouble(temp);
} catch (XPathException xpe) {
Log.e(TAG, "Failed to run XPATH", xpe);
return 0.0;
} catch (NumberFormatException nfe) {
Log.e(TAG, "Failed to parse string", nfe);
return 0.0;
}
}
private int getInteger(Node element, String name, XPath xp) {
try {
String temp = (String) xp.evaluate(name.toUpperCase(), element, XPathConstants.STRING);
return Integer.parseInt(temp);
} catch (XPathException xpe) {
Log.e(TAG, "Failed to run XPATH", xpe);
return 0;
} catch (NumberFormatException nfe) {
Log.e(TAG, "Failed to parse string", nfe);
return 0;
}
}
}