package org.commcare.print; import android.os.AsyncTask; import android.os.Bundle; import org.commcare.google.services.analytics.GoogleAnalyticsFields; import org.commcare.google.services.analytics.GoogleAnalyticsUtils; import org.commcare.utils.PrintValidationException; import org.commcare.utils.TemplatePrinterUtils; import java.io.File; import java.io.IOException; import java.io.Serializable; /** * Asynchronous task for populating an html document with data. * * @author Richard Lu * @author amstone */ public class TemplatePrinterTask extends AsyncTask<Void, Void, TemplatePrinterTask.PrintTaskResult> { public enum PrintTaskResult { SUCCESS, IO_ERROR, VALIDATION_ERROR_MUSTACHE, VALIDATION_ERROR_CHEVRON } /** * Used to track the region in the template file where a validation error was encountered */ private String problemString; /** * The mapping from keywords to case property values to be used in populating the template */ private final Bundle templatePopulationMapping; private final File templateFile; private final String populatedFilepath; private final PopulateListener listener; public TemplatePrinterTask(File input, String outputPath, Bundle values, PopulateListener listener) { this.templateFile = input; this.populatedFilepath = outputPath; this.templatePopulationMapping = values; this.listener = listener; } /** * Attempts to perform population of the template file */ @Override protected PrintTaskResult doInBackground(Void... params) { GoogleAnalyticsUtils.reportFeatureUsage(GoogleAnalyticsFields.ACTION_PRINT); try { populateAndSaveHtml(templateFile, templatePopulationMapping, populatedFilepath); return PrintTaskResult.SUCCESS; } catch (IOException e) { return PrintTaskResult.IO_ERROR; } catch (PrintValidationException e) { problemString = e.getMessage(); return e.getErrorType(); } } /** * Receives the return value from doInBackground and proceeds accordingly */ @Override protected void onPostExecute(PrintTaskResult result) { listener.onPopulationFinished(result, problemString); } /** * Populates an html print template based on the given set of key-value pairings * and save the newly-populated template to a temp location * * @param input the html print template * @param mapping the mapping of keywords to case property values * @throws IOException, PrintValidationException */ private static void populateAndSaveHtml(File input, Bundle mapping, String outputPath) throws IOException, PrintValidationException { // Read from input file String fileText = TemplatePrinterUtils.docToString(input); // Check if <body></body> section of html string is properly formed int startBodyIndex = fileText.toLowerCase().indexOf("<body"); String beforeBodySection = fileText.substring(0, startBodyIndex); String bodySection = fileText.substring(startBodyIndex); validateString(bodySection); // Swap out place-holder keywords for case property values within <body></body> section bodySection = replace(bodySection, mapping); // Write the new HTML to the desired temp file location TemplatePrinterUtils.writeStringToFile(beforeBodySection + bodySection, outputPath); } /** * Populate an input string with attribute keys formatted as {{ attr_key }} with attribute * values. * * @param input String input * @param mapping Bundle of key-value mappings with which to complete replacements * @return The populated String */ private static String replace(String input, Bundle mapping) { // Split input into tokens bounded by {{ and }} String[] tokens = TemplatePrinterUtils.splitKeepDelimiter(input, "\\{{2}", "\\}{2}"); for (int i = 0; i < tokens.length; i++) { String token = tokens[i]; // Every 2nd token is a attribute enclosed in {{ }} if (i % 2 == 1) { // Split token into tokenSplits bounded by < and > String[] tokenSplits = TemplatePrinterUtils.splitKeepDelimiter(token, "<|(\\}{2})", ">|(\\{{2})"); // First and last tokenSplits are {{ and }} for (int j = 1; j < tokenSplits.length - 1; j++) { String tokenSplit = tokenSplits[j]; // tokenSplit is key or whitespace if (!tokenSplit.startsWith("<")) { replaceKeyWithValue(mapping, tokenSplit, tokenSplits, j); } } // Remove {{ and }} tokenSplits[0] = ""; tokenSplits[tokenSplits.length - 1] = ""; // Reconstruct token tokens[i] = TemplatePrinterUtils.join(tokenSplits); } } // Reconstruct input return TemplatePrinterUtils.join(tokens); } private static void replaceKeyWithValue(Bundle mapping, String tokenSplit, String[] tokenSplits, int index) { // Remove whitespace from key String key = TemplatePrinterUtils.remove(tokenSplit, " "); String valToPopulate = ""; if (mapping.containsKey(key) && mapping.get(key) != null) { Serializable passedValue = mapping.getSerializable(key); if (passedValue instanceof PrintableDetailField) { // If we are printing from a detail, the passed values will be of type // PrintableDetailField PrintableDetailField printableField = ((PrintableDetailField)passedValue); if (printableField.isPrintSupported()) { valToPopulate = printableField.getFormattedValueString(); } else { // Empty if printing is not supported for this type of detail field valToPopulate = ""; } } else { // If we are printing from a form, the passed values will just be strings valToPopulate = (String)passedValue; } } tokenSplits[index] = valToPopulate; } /** * Validates the input string for well-formed {{ }} and < > pairs. * If malformed, throws an exception that will be caught by * doInBackground(), and trigger the appropriate result code to be * sent back to the attached PopulateListener * * @param input String to validate */ private static void validateString(String input) throws PrintValidationException { boolean isBetweenMustaches = false; boolean isBetweenChevrons = false; StringBuilder recentString = new StringBuilder(); for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); recentString.append(c); if (recentString.length() > 40) { recentString.deleteCharAt(0); } if (c == '{') { if (isBetweenMustaches) { throw new PrintValidationException(recentString.toString(), PrintTaskResult.VALIDATION_ERROR_MUSTACHE); } else { i++; c = input.charAt(i); if (c == '{') { isBetweenMustaches = true; recentString.append(c); } else { isBetweenMustaches = false; } } } else if (c == '}') { if (isBetweenMustaches) { i++; c = input.charAt(i); if (c != '}') { recentString.append(c); throw new PrintValidationException(recentString.toString(), PrintTaskResult.VALIDATION_ERROR_MUSTACHE); } else { isBetweenMustaches = false; } } } else if (c == '<') { if (isBetweenChevrons) { throw new PrintValidationException(recentString.toString(), PrintTaskResult.VALIDATION_ERROR_CHEVRON); } else { isBetweenChevrons = true; } } else if (c == '>') { if (isBetweenChevrons) { isBetweenChevrons = false; } else { throw new PrintValidationException(recentString.toString(), PrintTaskResult.VALIDATION_ERROR_CHEVRON); } } } // If we reach the end of the string and are in between either type, should also throw error if (isBetweenChevrons) { throw new PrintValidationException(recentString.toString(), PrintTaskResult.VALIDATION_ERROR_CHEVRON); } else if (isBetweenMustaches) { throw new PrintValidationException(recentString.toString(), PrintTaskResult.VALIDATION_ERROR_MUSTACHE); } } /** * A listener for this task, implemented by TemplatePrinterActivity */ public interface PopulateListener { void onPopulationFinished(PrintTaskResult result, String problemString); } }