org.smap.sdal.managers.PDFSurveyManager.java Source code

Java tutorial

Introduction

Here is the source code for org.smap.sdal.managers.PDFSurveyManager.java

Source

package org.smap.sdal.managers;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.sql.Connection;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.ResourceBundle;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletResponse;

import org.codehaus.jettison.json.JSONObject;
import org.smap.sdal.Utilities.GeneralUtilityMethods;
import org.smap.sdal.Utilities.PdfPageSizer;
import org.smap.sdal.Utilities.PdfUtilities;
import org.smap.sdal.model.DisplayItem;
import org.smap.sdal.model.Form;
import org.smap.sdal.model.Label;
import org.smap.sdal.model.Option;
import org.smap.sdal.model.OptionList;
import org.smap.sdal.model.Question;
import org.smap.sdal.model.Result;
import org.smap.sdal.model.Row;
import org.smap.sdal.model.ServerData;
import org.smap.sdal.model.Survey;
import org.smap.sdal.model.User;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.itextpdf.text.Anchor;
import com.itextpdf.text.BadElementException;
import com.itextpdf.text.BaseColor;
import com.itextpdf.text.Chunk;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Element;
import com.itextpdf.text.Font;
import com.itextpdf.text.FontFactory;
import com.itextpdf.text.Image;
import com.itextpdf.text.List;
import com.itextpdf.text.ListItem;
import com.itextpdf.text.PageSize;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.pdf.AcroFields;
import com.itextpdf.text.pdf.BarcodeQRCode;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.PushbuttonField;
import com.itextpdf.tool.xml.ElementList;
import com.itextpdf.tool.xml.XMLWorker;
import com.itextpdf.tool.xml.XMLWorkerFontProvider;
import com.itextpdf.tool.xml.XMLWorkerHelper;
import com.itextpdf.tool.xml.css.CssFile;
import com.itextpdf.tool.xml.css.StyleAttrCSSResolver;
import com.itextpdf.tool.xml.html.CssAppliers;
import com.itextpdf.tool.xml.html.CssAppliersImpl;
import com.itextpdf.tool.xml.html.Tags;
import com.itextpdf.tool.xml.parser.XMLParser;
import com.itextpdf.tool.xml.pipeline.css.CSSResolver;
import com.itextpdf.tool.xml.pipeline.css.CssResolverPipeline;
import com.itextpdf.tool.xml.pipeline.end.ElementHandlerPipeline;
import com.itextpdf.tool.xml.pipeline.html.HtmlPipeline;
import com.itextpdf.tool.xml.pipeline.html.HtmlPipelineContext;

/*****************************************************************************
    
This file is part of SMAP.
    
SMAP is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
SMAP is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with SMAP.  If not, see <http://www.gnu.org/licenses/>.
    
 ******************************************************************************/

public class PDFSurveyManager {

    private static Logger log = Logger.getLogger(PDFSurveyManager.class.getName());

    LogManager lm = new LogManager(); // Application log

    public static Font Symbols = null;
    public static Font defaultFont = null;
    public static Font defaultFontBold = null;
    public static Font defaultFontLink = null;
    public static Font arabicFont = null;
    public static Font bengaliFont = null;
    public static Font bengaliFontBold = null;
    private static final String DEFAULT_CSS = "/smap_bin/resources/css/default_pdf.css";
    private static int NUMBER_TABLE_COLS = 10;
    private static int NUMBER_QUESTION_COLS = 10;

    // Global values set in constructor
    private ResourceBundle localisation;
    private ChoiceManager choiceManager = null;
    private Survey survey;
    private Connection sd;
    private Connection cResults;
    private String user;
    private String tz;

    // Other global values
    int languageIdx = 0;

    private class Parser {
        XMLParser xmlParser = null;
        ElementList elements = null;
    }

    int marginLeft = 50;
    int marginRight = 50;
    int marginTop_1 = 130;
    int marginBottom_1 = 100;
    int marginTop_2 = 50;
    int marginBottom_2 = 100;

    boolean mExcludeEmpty = false;

    private class GlobalVariables { // Level descended in form hierarchy
        //HashMap<String, Integer> count = new HashMap<String, Integer> ();      // Record number at a location given by depth_length as a string
        int[] cols = { NUMBER_QUESTION_COLS }; // Current Array of columns
        boolean hasAppendix = false;
        String mapbox_key;

        // Map of questions that need to have the results of another question appended to their results in a pdf report
        HashMap<String, ArrayList<String>> addToList = new HashMap<String, ArrayList<String>>();
    }

    public PDFSurveyManager(ResourceBundle l, Connection sd, Connection cResults, Survey s, String u, String tz) {
        localisation = l;
        choiceManager = new ChoiceManager(l, tz);
        this.sd = sd;
        this.cResults = cResults;
        survey = s;
        user = u;
        if (tz == null) {
            tz = "UTC";
        }
        this.tz = tz;
    }

    /*
     * Call this function to create a PDF
     * Return a suggested name for the PDF file derived from the results
     */
    public String createPdf(OutputStream outputStream, String basePath, String serverRoot, String remoteUser,
            String language, boolean generateBlank, String filename, boolean landscape, // Set true if landscape
            HttpServletResponse response) throws Exception {

        if (language != null) {
            language = language.replace("'", "''"); // Escape apostrophes
        } else {
            language = "none";
        }

        mExcludeEmpty = survey.exclude_empty;

        User user = null;

        ServerManager serverManager = new ServerManager();
        ServerData serverData = serverManager.getServer(sd, localisation);

        UserManager um = new UserManager(localisation);
        int[] repIndexes = new int[20]; // Assume repeats don't go deeper than 20 levels

        Document document = null;
        PdfWriter writer = null;
        PdfReader reader = null;
        PdfStamper stamper = null;

        try {

            // Get fonts and embed them
            String os = System.getProperty("os.name");
            log.info("Operating System:" + os);

            if (os.startsWith("Mac")) {
                FontFactory.register("/Library/Fonts/fontawesome-webfont.ttf", "Symbols");
                //FontFactory.register("/Library/Fonts/Arial Unicode.ttf", "default");
                FontFactory.register("/Library/Fonts/NotoNaskhArabic-Regular.ttf", "arabic");
                FontFactory.register("/Library/Fonts/NotoSans-Regular.ttf", "notosans");
                FontFactory.register("/Library/Fonts/NotoSans-Bold.ttf", "notosansbold");
                FontFactory.register("/Library/Fonts/NotoSansBengali-Regular.ttf", "bengali");
                FontFactory.register("/Library/Fonts/NotoSansBengali-Bold.ttf", "bengalibold");
            } else if (os.indexOf("nix") >= 0 || os.indexOf("nux") >= 0 || os.indexOf("aix") > 0) {
                // Linux / Unix
                FontFactory.register("/usr/share/fonts/truetype/fontawesome-webfont.ttf", "Symbols");
                FontFactory.register("/usr/share/fonts/truetype/NotoNaskhArabic-Regular.ttf", "arabic");
                FontFactory.register("/usr/share/fonts/truetype/NotoSans-Regular.ttf", "notosans");
                FontFactory.register("/usr/share/fonts/truetype/NotoSans-Bold.ttf", "notosansbold");
                FontFactory.register("/usr/share/fonts/truetype/NotoSansBengali-Regular.ttf", "bengali");
                FontFactory.register("/usr/share/fonts/truetype/NotoSansBengali-Bold.ttf", "bengalibold");
            }

            Symbols = FontFactory.getFont("Symbols", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, 12);
            defaultFontLink = FontFactory.getFont("Symbols", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, 12);
            defaultFont = FontFactory.getFont("notosans", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, 10);
            defaultFontBold = FontFactory.getFont("notosansbold", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, 10);
            arabicFont = FontFactory.getFont("arabic", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, 10);
            bengaliFont = FontFactory.getFont("bengali", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, 10);
            bengaliFontBold = FontFactory.getFont("bengalibold", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, 10);

            defaultFontLink.setColor(BaseColor.BLUE);

            /*
             * Get the results and details of the user that submitted the survey
             */

            log.info("User Ident who submitted the survey: " + survey.instance.user);
            String userName = survey.instance.user;
            if (userName == null) {
                userName = remoteUser;
            }
            if (userName != null) {
                user = um.getByIdent(sd, userName);
            }

            // If a filename was not specified then get one from the survey data
            // This filename is returned to the calling program so that it can be used as a permanent name for the temporary file created here
            // If the PDF is to be returned in an http response then the header is set now before writing to the output stream
            log.info("Filename passed to createPDF is: " + filename);
            if (filename == null) {
                filename = survey.getInstanceName() + ".pdf";
            } else {
                if (!filename.endsWith(".pdf")) {
                    filename += ".pdf";
                }
            }

            // If the PDF is to be returned in an http response then set the file name now
            if (response != null) {
                log.info("Setting filename to: " + filename);
                GeneralUtilityMethods.setFilenameInResponse(filename, response);
            }

            /*
             * Get a template for the PDF report if it exists
             * The template name will be the same as the XLS form name but with an extension of pdf
             */
            File templateFile = GeneralUtilityMethods.getPdfTemplate(basePath, survey.displayName, survey.p_id);

            /*
             * Get dependencies between Display Items, for example if a question result should be added to another
             *  question's results
             */
            GlobalVariables gv = new GlobalVariables();
            if (!generateBlank) {
                for (int i = 0; i < survey.instance.results.size(); i++) {
                    getDependencies(gv, survey.instance.results.get(i), survey, i);
                }
            }
            gv.mapbox_key = serverData.mapbox_default;
            int oId = GeneralUtilityMethods.getOrganisationId(sd, remoteUser);

            languageIdx = GeneralUtilityMethods.getLanguageIdx(survey, language);
            if (templateFile.exists()) {

                log.info("PDF Template Exists");
                String templateName = templateFile.getAbsolutePath();

                reader = new PdfReader(templateName);
                stamper = new PdfStamper(reader, outputStream);

                for (int i = 0; i < survey.instance.results.size(); i++) {
                    fillTemplate(gv, stamper.getAcroFields(), survey.instance.results.get(i), basePath, null, i,
                            serverRoot, stamper, oId);
                }
                if (user != null) {
                    fillTemplateUserDetails(stamper.getAcroFields(), user, basePath);
                }
                stamper.setFormFlattening(true);

            } else {
                log.info("++++No template exists creating a pdf file programmatically");

                /*
                 * Create a PDF without the stationary
                 * If we need to add a letter head then create document in two passes, the second pass adds the letter head
                 * Else just create the document directly in a single pass
                 */
                Parser parser = getXMLParser();

                // Step 1 - Create the underlying document as a byte array
                if (landscape) {
                    document = new Document(PageSize.A4.rotate());
                } else {
                    document = new Document(PageSize.A4);
                }
                document.setMargins(marginLeft, marginRight, marginTop_1, marginBottom_1);
                writer = PdfWriter.getInstance(document, outputStream);

                writer.setInitialLeading(12);

                writer.setPageEvent(new PdfPageSizer(survey.displayName, survey.projectName, user, basePath, null,
                        marginLeft, marginRight, marginTop_2, marginBottom_2));
                document.open();

                // If this form has data maintain a list of parent records to lookup ${values}
                ArrayList<ArrayList<Result>> parentRecords = null;
                if (!generateBlank) {
                    parentRecords = new ArrayList<ArrayList<Result>>();
                }

                for (int i = 0; i < survey.instance.results.size(); i++) {
                    processForm(parser, document, survey.instance.results.get(i), basePath, serverRoot,
                            generateBlank, 0, i, repIndexes, gv, false, parentRecords, remoteUser, oId);
                }

                fillNonTemplateUserDetails(document, user, basePath);

                // Add appendix
                if (gv.hasAppendix) {
                    document.newPage();
                    document.add(new Paragraph("Appendix", defaultFontBold));

                    for (int i = 0; i < survey.instance.results.size(); i++) {
                        processForm(parser, document, survey.instance.results.get(i), basePath, serverRoot,
                                generateBlank, 0, i, repIndexes, gv, true, parentRecords, remoteUser, oId);
                    }
                }

            }

        } finally {
            if (document != null)
                try {
                    document.close();
                } catch (Exception e) {
                }
            ;
            if (writer != null)
                try {
                    writer.close();
                } catch (Exception e) {
                }
            ;
            if (stamper != null)
                try {
                    stamper.close();
                } catch (Exception e) {
                }
            ;
            if (reader != null)
                try {
                    reader.close();
                } catch (Exception e) {
                }
            ;
        }

        return filename;

    }

    /*
     * Get dependencies between question
     */
    private void getDependencies(GlobalVariables gv, ArrayList<Result> record, org.smap.sdal.model.Survey survey,
            int recNumber) {

        for (int j = 0; j < record.size(); j++) {
            Result r = record.get(j);
            if (r.type.equals("form")) {
                for (int k = 0; k < r.subForm.size(); k++) {
                    getDependencies(gv, r.subForm.get(k), survey, k);
                }
            } else {

                if (r.appearance != null && r.appearance.contains("pdfaddto")) {
                    String name = getReferencedQuestion(r.appearance);
                    if (name != null) {
                        String refKey = r.fIdx + "_" + recNumber + "_" + name;
                        ArrayList<String> deps = gv.addToList.get(refKey);

                        if (deps == null) {
                            deps = new ArrayList<String>();
                            gv.addToList.put(refKey, deps);
                        }
                        deps.add(r.value);
                    }
                }
            }
        }
    }

    private String getReferencedQuestion(String app) {
        String name = null;

        String[] appValues = app.split(" ");
        for (int i = 0; i < appValues.length; i++) {
            if (appValues[i].startsWith("pdfaddto")) {
                int idx = appValues[i].indexOf('_');
                if (idx > -1) {
                    name = appValues[i].substring(idx + 1);
                }
                break;
            }
        }

        return name;
    }

    /*
     * Fill the template with data from the survey
     */
    private void fillTemplate(GlobalVariables gv, AcroFields pdfForm, ArrayList<Result> record, String basePath,
            String formName, int repeatIndex, String serverRoot, PdfStamper stamper, int oId)
            throws IOException, DocumentException {
        try {

            for (Result r : record) {

                String value = "";
                boolean hideLabel = false;
                String fieldName = getFieldName(formName, repeatIndex, r.name);
                String fieldNameQR = getFieldName(formName, repeatIndex, r.name + "_qr");

                DisplayItem di = new DisplayItem();
                try {
                    Form form = survey.forms.get(r.fIdx);
                    Question question = getQuestionFromResult(sd, r, form);
                    setQuestionFormats(question.appearance, di);
                } catch (Exception e) {
                    // If we can't get the question details for this data then that is ok
                }

                /*
                 * Set the value based on the result
                 * Process subforms if this is a repeating group
                 */
                if (r.type.equals("form")) {
                    for (int k = 0; k < r.subForm.size(); k++) {
                        fillTemplate(gv, pdfForm, r.subForm.get(k), basePath, fieldName, k, serverRoot, stamper,
                                oId);
                    }
                } else if (r.type.equals("select1")) {

                    Form form = survey.forms.get(r.fIdx);
                    Question question = form.questions.get(r.qIdx);

                    ArrayList<String> matches = new ArrayList<String>();
                    matches.add(r.value);
                    value = choiceManager.getLabel(sd, cResults, user, oId, survey.id, question.id, question.l_id,
                            question.external_choices, question.external_table,
                            survey.languages.get(languageIdx).name, languageIdx, matches, survey.ident);

                } else if (r.type.equals("select")) {

                    String nameValue = r.value;
                    if (nameValue != null) {
                        String vArray[] = nameValue.split(" ");
                        ArrayList<String> matches = new ArrayList<String>();
                        if (vArray != null) {
                            for (String v : vArray) {
                                matches.add(v);
                            }
                        }
                        Form form = survey.forms.get(r.fIdx);
                        Question question = form.questions.get(r.qIdx);
                        value = choiceManager.getLabel(sd, cResults, user, oId, survey.id, question.id,
                                question.l_id, question.external_choices, question.external_table,
                                survey.languages.get(languageIdx).name, languageIdx, matches, survey.ident);
                    }

                } else if (r.type.equals("dateTime") || r.type.equals("timestamp")) {

                    value = null;
                    if (r.value != null) {
                        // Convert date to local time
                        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                        df.setTimeZone(TimeZone.getTimeZone("UTC"));
                        Date date = df.parse(r.value);
                        df.setTimeZone(TimeZone.getTimeZone(tz));
                        value = df.format(date);
                        log.info("Convert date to local time (template): " + r.name + " : " + r.value + " : "
                                + " : " + value + " : " + r.type + " : " + tz);
                    }

                } else {
                    value = r.value;
                }

                /*
                 * Add the value to the form
                 * Alternatively remove the fieldName if the value is empty.
                 */
                if (value == null || value.trim().equals("")) {
                    try {
                        pdfForm.removeField(fieldName);
                    } catch (Exception e) {
                        log.info("Error removing field: " + fieldName + ": " + e.getMessage());
                    }

                } else if (r.type.equals("geopoint") || r.type.equals("geoshape") || r.type.equals("geotrace")
                        || r.type.startsWith("geopolygon_") || r.type.startsWith("geolinestring_")) {

                    PushbuttonField ad = pdfForm.getNewPushbuttonFromField(fieldName);
                    if (ad != null) {
                        Image img = PdfUtilities.getMapImage(sd, di.map, r.value, di.location, di.zoom,
                                gv.mapbox_key, survey.id, user, di.markerColor);
                        PdfUtilities.addMapImageTemplate(pdfForm, ad, fieldName, img);
                    } else {
                        log.info("No field for image (Mapbox not called: " + fieldName);
                    }

                } else if (r.type.equals("image") || r.type.equals("video") || r.type.equals("audio")) {
                    PdfUtilities.addImageTemplate(pdfForm, fieldName, basePath, value, serverRoot, stamper,
                            defaultFontLink);

                } else {
                    if (hideLabel) {
                        try {
                            pdfForm.removeField(fieldName);
                        } catch (Exception e) {
                            log.info("Error removing field: " + fieldName + ": " + e.getMessage());
                        }
                    } else {
                        if (di.isBarcode) {
                            PushbuttonField ad = pdfForm.getNewPushbuttonFromField(fieldName);
                            if (ad != null) {
                                BarcodeQRCode qrcode = new BarcodeQRCode(value.trim(), 1, 1, null);
                                Image qrcodeImage = qrcode.getImage();
                                qrcodeImage.setAbsolutePosition(10, 500);
                                qrcodeImage.scalePercent(200);
                                PdfUtilities.addMapImageTemplate(pdfForm, ad, fieldName, qrcodeImage);
                            }
                        } else {
                            pdfForm.setField(fieldName, value);

                        }
                    }
                }

                /*
                 * Add any QR code values to fields that have been identified using the QR suffix
                 */
                if (fieldNameQR != null && value != null && value.trim().length() > 0) {
                    PushbuttonField ad = pdfForm.getNewPushbuttonFromField(fieldName);
                    if (ad != null) {
                        BarcodeQRCode qrcode = new BarcodeQRCode(value.trim(), 1, 1, null);
                        Image qrcodeImage = qrcode.getImage();
                        qrcodeImage.setAbsolutePosition(10, 500);
                        qrcodeImage.scalePercent(200);
                        PdfUtilities.addMapImageTemplate(pdfForm, ad, fieldNameQR, qrcodeImage);
                    }
                }

            }
        } catch (Exception e) {
            log.log(Level.SEVERE, "Error filling template", e);
        }
    }

    private String getFieldName(String formName, int index, String qName) {
        String name = null;

        if (formName == null || formName.equals("")) {
            name = qName;
        } else {
            name = formName + "[" + index + "]." + qName;
        }
        return name;
    }

    private class UserSettings {
        String title;
        String license;
    }

    /*
     * Fill the template with data from the survey
     */
    private static void fillTemplateUserDetails(AcroFields pdfForm, User user, String basePath)
            throws IOException, DocumentException {
        try {

            pdfForm.setField("user_name", user.name);
            pdfForm.setField("user_company", user.company_name);

            /*
             * User configurable data TODO This should be an array of key value pairs
             * As interim use a hard coded class to hold the data
             */
            String settings = user.settings;
            Type type = new TypeToken<UserSettings>() {
            }.getType();
            Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create();
            UserSettings us = gson.fromJson(settings, type);

            if (us != null) {
                pdfForm.setField("user_title", us.title);
                pdfForm.setField("user_license", us.license);

                PushbuttonField ad = pdfForm.getNewPushbuttonFromField("user_signature");
                if (ad != null) {
                    ad.setLayout(PushbuttonField.LAYOUT_ICON_ONLY);
                    ad.setProportionalIcon(true);
                    String filename = null;
                    try {
                        filename = basePath + "/media/users/" + user.id + "/sig/" + user.signature;
                        ad.setImage(Image.getInstance(filename));
                    } catch (Exception e) {
                        log.info("Error: Failed to add signature " + filename + " to pdf");
                    }
                    pdfForm.replacePushbuttonField("user_signature", ad.getField());
                } else {
                    //log.info("Picture field: user_signature not found");
                }
            }

        } catch (Exception e) {
            log.log(Level.SEVERE, "Error filling template", e);
        }
    }

    /*
     * Get an XML Parser
     */
    private Parser getXMLParser() {

        Parser parser = new Parser();

        // CSS
        CSSResolver cssResolver = new StyleAttrCSSResolver();
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(DEFAULT_CSS);
            CssFile cssFile = XMLWorkerHelper.getCSS(fis);
            cssResolver.addCss(cssFile);
        } catch (Exception e) {
            log.log(Level.SEVERE, "Failed to get CSS file", e);
            cssResolver = XMLWorkerHelper.getInstance().getDefaultCssResolver(true);
        } finally {
            try {
                fis.close();
            } catch (Exception e) {
            }
        }

        // Pipelines
        parser.elements = new ElementList();
        ElementHandlerPipeline end = new ElementHandlerPipeline(parser.elements, null);

        String os = System.getProperty("os.name");
        log.info("Operating System:" + os);

        XMLWorkerFontProvider fontProvider = new XMLWorkerFontProvider();

        if (os.startsWith("Mac")) {
            fontProvider.register("/Library/Fonts/NotoSansBengali-Regular.ttf", BaseFont.IDENTITY_H);
            fontProvider.register("/Library/Fonts/NotoNaskhArabic-Regular.ttf", BaseFont.IDENTITY_H);
            fontProvider.register("/Library/Fonts/NotoSansBengali-Bold.ttf", BaseFont.IDENTITY_H);
            fontProvider.register("/Library/Fonts/NotoSans-Regular.ttf", BaseFont.IDENTITY_H);
            fontProvider.register("/Library/Fonts/NotoSans-Bold.ttf", BaseFont.IDENTITY_H);

        } else if (os.indexOf("nix") >= 0 || os.indexOf("nux") >= 0 || os.indexOf("aix") > 0) {
            // Linux / Unix
            fontProvider.register("/usr/share/fonts/truetype/NotoSansBengali-Regular.ttf", BaseFont.IDENTITY_H);
            fontProvider.register("/usr/share/fonts/truetype/NotoNaskhArabic-Regular.ttf", BaseFont.IDENTITY_H);
            fontProvider.register("/usr/share/fonts/truetype/NotoNaskhArabic-Regular.ttf", BaseFont.IDENTITY_H);
            fontProvider.register("/usr/share/fonts/truetype/NotoSans-Regular.ttf", BaseFont.IDENTITY_H);
            fontProvider.register("/usr/share/fonts/truetype/NotoSans-Bold.ttf", BaseFont.IDENTITY_H);
        }

        /*
         System.out.println("Fonts present in " + fontProvider.getClass().getName());
             Set<String> registeredFonts = fontProvider.getRegisteredFonts();
             for (String font : registeredFonts)
           System.out.println(font);
         */
        CssAppliers cssAppliers = new CssAppliersImpl(fontProvider);

        // HTML
        HtmlPipelineContext htmlContext = new HtmlPipelineContext(cssAppliers);
        htmlContext.setTagFactory(Tags.getHtmlTagProcessorFactory());
        htmlContext.autoBookmark(false);
        HtmlPipeline html = new HtmlPipeline(htmlContext, end);
        CssResolverPipeline css = new CssResolverPipeline(cssResolver, html);

        XMLWorker worker = new XMLWorker(css, true);
        parser.xmlParser = new XMLParser(worker);

        return parser;

    }

    /*
     * Process the form
     * Attempt to follow the standard set by enketo for the layout of forms so that the same layout directives
     *  can be applied to showing the form on the screen and generating the PDF
     */
    private void processForm(Parser parser, Document document, ArrayList<Result> record, String basePath,
            String serverRoot, boolean generateBlank, int depth, int length, int[] repIndexes, GlobalVariables gv,
            boolean appendix, ArrayList<ArrayList<Result>> parentRecords, String remoteUser, int oId)
            throws DocumentException, IOException, SQLException {

        // Check that the depth of repeats hasn't exceeded the maximum
        if (depth > repIndexes.length - 1) {
            depth = repIndexes.length - 1;
        }

        boolean firstQuestion = true;
        for (int j = 0; j < record.size(); j++) {
            Result r = record.get(j);
            if (r.type.equals("form")) {

                firstQuestion = true; // Make sure there is a gap when we return from the sub form
                // If this is a blank template check to see the number of times we should repeat this sub form
                if (generateBlank) {
                    int blankRepeats = getBlankRepeats(r.appearance);
                    for (int k = 0; k < blankRepeats; k++) {
                        repIndexes[depth] = k;
                        processForm(parser, document, r.subForm.get(0), basePath, serverRoot, generateBlank,
                                depth + 1, k, repIndexes, gv, appendix, null, remoteUser, oId);
                    }
                } else {
                    for (int k = 0; k < r.subForm.size(); k++) {
                        // Maintain array list of parent records in order to look up ${values}
                        parentRecords.add(0, record); // Push this record in at the beginning of the list as we want to search most recent first
                        repIndexes[depth] = k;
                        processForm(parser, document, r.subForm.get(k), basePath, serverRoot, generateBlank,
                                depth + 1, k, repIndexes, gv, appendix, parentRecords, remoteUser, oId);
                    }
                }
            } else {
                // Process the question

                Form form = survey.forms.get(r.fIdx);
                Question question = getQuestionFromResult(sd, r, form);

                if (question != null) {

                    if (includeResult(r, question, appendix, gv, generateBlank)) {
                        if (question.type.equals("begin group")) {
                            if (question.isNewPage()) {
                                document.newPage();
                            }
                        } else if (question.type.equals("end group")) {
                            //ignore
                        } else {

                            Row row = prepareRow(record, survey, j, gv, length, appendix, parentRecords,
                                    generateBlank);
                            PdfPTable newTable = processRow(parser, row, basePath, serverRoot, generateBlank, depth,
                                    repIndexes, gv, remoteUser, oId);

                            newTable.setWidthPercentage(100);
                            newTable.setKeepTogether(true);

                            // Add a gap if this is the first question of the record
                            // or the previous row was at a different depth
                            if (firstQuestion) {
                                newTable.setSpacingBefore(5);
                            } else {
                                newTable.setSpacingBefore(row.spaceBefore());
                            }
                            firstQuestion = false;

                            // Start a new page if the first question needs to be on a new page
                            if (row.items.get(0).isNewPage) {
                                document.newPage();
                            }
                            document.add(newTable);
                            j += row.items.size() - 1; // Jump over multiple questions if more than one was added to the row
                        }
                    }
                } else {
                    log.info("Question Idx not found: " + r.qIdx);
                }

            }
        }

        return;
    }

    private Question getQuestionFromResult(Connection sd, Result r, Form form) throws SQLException {

        Question question = null;
        if (r.qIdx >= 0) {
            question = form.questions.get(r.qIdx);
        }
        if (r.qIdx <= -1000) {
            question = GeneralUtilityMethods.getPreloadAsQuestion(sd, survey.id, r.qIdx); // A preload
        } else if (r.qIdx == -1) {
            question = new Question(); // Server generated
            question.name = r.name;
            question.type = r.type;
        }
        return question;
    }

    /*
     * Make a decision as to whether this result should be included in the PDF
     */
    private boolean includeResult(Result r, org.smap.sdal.model.Question question, boolean appendix,
            GlobalVariables gv, boolean generateBlank) {

        boolean include = true;
        boolean inMeta = question.inMeta;

        // Don't include the question if it has been marked as not to be included
        if (!generateBlank && mExcludeEmpty && isSkipped(question, r)) {
            include = false;
        } else if (question.appearance != null) {
            if (question.appearance.contains("pdfno")) {
                include = false;
            } else {
                boolean appendixQuestion = question.appearance.contains("pdfapp");
                if (appendixQuestion) {
                    gv.hasAppendix = true;
                }
                if (appendix && !appendixQuestion || (!appendix && appendixQuestion)) {
                    include = false;
                }
            }
        } else {
            // Questions without appearance should not appear in the appendix
            if (appendix) {
                include = false;
            }
        }

        // Check appendix status

        if (include) {
            if (r.name == null) {
                include = false;
            } else if (r.name.startsWith("meta") && r.type.equals("begin group")) {
                include = false;
            } else if (inMeta) {
                include = false;
            } else if (r.name.startsWith("meta_group")) {
                include = false;
            } else if (r.name.startsWith("_")) {
                // Don't include questions that start with "_",  these are only added to the letter head
                //include = false;
            }
        }

        return include;
    }

    /*
     * Add the table row to the document
     */
    private PdfPTable processRow(Parser parser, Row row, String basePath, String serverRoot, boolean generateBlank,
            int depth, int[] repIndexes, GlobalVariables gv, String remoteUser, int oId)
            throws BadElementException, MalformedURLException, IOException {

        PdfPTable table = new PdfPTable(depth + NUMBER_TABLE_COLS); // Add a column for each level of repeats so that the repeat number can be shown

        // Add the cells to record repeat indexes
        for (int i = 0; i < depth; i++) {
            PdfPCell c = new PdfPCell();
            c.addElement(new Paragraph(String.valueOf(repIndexes[i] + 1), defaultFont));
            c.setBackgroundColor(BaseColor.LIGHT_GRAY);
            table.addCell(c);

        }

        int spanCount = NUMBER_TABLE_COLS;
        int numberItems = row.items.size();
        for (DisplayItem di : row.items) {

            PdfPCell cell = new PdfPCell(
                    addDisplayItem(parser, di, basePath, serverRoot, generateBlank, gv, remoteUser, oId));
            cell.setBorderColor(BaseColor.LIGHT_GRAY);

            // Make sure the last cell extends to the end of the table
            if (numberItems == 1) {
                di.width = spanCount;
            }
            cell.setColspan(di.width);
            table.addCell(cell);

            numberItems--;
            spanCount -= di.width;
        }
        return table;
    }

    /*
     * Return true if an answer has not been supplied to a question
     */
    private boolean isSkipped(org.smap.sdal.model.Question q, Result r) {
        boolean skipped = false;
        boolean choiceSet = false;

        /*
        if(r.choices != null) {
           for(Result c : r.choices) {
        if(c.isSet) {
           choiceSet = true;
           break;
        }
           }
        }
        */
        if (!q.type.equals("note")) {
            skipped = ((r.value == null || r.value.trim().length() == 0) && !choiceSet);
        }
        return skipped;
    }

    /*
     * Add a row of questions
     * Each row is created as a table
     * converts questions and results to display items
     * As many display items are added as will fit in the current groupWidth
     * If the total width of the display items does not add up to the groupWidth then the last item
     *  will be extended so that the total is equal to the group width
     */
    private Row prepareRow(ArrayList<Result> record, org.smap.sdal.model.Survey survey, int offset,
            GlobalVariables gv, int recNumber, boolean appendix, ArrayList<ArrayList<Result>> parentRecords,
            boolean generateBlank) throws SQLException {

        Row row = new Row();
        row.groupWidth = gv.cols.length;

        for (int i = offset; i < record.size(); i++) {
            Result r = record.get(i);

            Form form = survey.forms.get(r.fIdx);
            Question question = getQuestionFromResult(sd, r, form);

            Label label = null;
            if (question.display_name != null && question.display_name.trim().length() > 0) {
                // Use display name in preference to labels if it exists
                label = new Label();
                label.text = question.display_name;
            } else {
                // Use labels as this is the old way
                if (question.labels.size() > 0) {
                    label = question.labels.get(languageIdx);
                } else {
                    label = new Label();
                    log.info("Error: No label found for question: " + question.name);
                }
            }

            boolean isNewPage = question.isNewPage();

            if (i == offset) {
                // First question of row - update the number of columns
                int[] updateCols = question.updateCols(gv.cols);
                if (updateCols != null) {
                    gv.cols = updateCols; // Can only update the number of columns with the first question of the row
                }

                includeQuestion(row.items, gv, i, label, question, offset, survey, r, isNewPage, recNumber, record,
                        parentRecords);
            } else if (i - offset < gv.cols.length) {
                // 2nd or later questions in the row
                int[] updateCols = question.updateCols(gv.cols); // Returns null if the number of columns has not changed

                if (updateCols == null || isNewPage) {
                    if (includeResult(r, question, appendix, gv, generateBlank)) {
                        includeQuestion(row.items, gv, i, label, question, offset, survey, r, isNewPage, recNumber,
                                record, parentRecords);
                    }
                } else {
                    // If the question updated the number of columns then we will need to start a new row
                    break;
                }

            } else {
                break;
            }

        }
        return row;
    }

    /*
     * Include question in the row
     */
    private void includeQuestion(ArrayList<DisplayItem> items, GlobalVariables gv, int colIdx, Label label,
            org.smap.sdal.model.Question question, int offset, org.smap.sdal.model.Survey survey, Result r,
            boolean isNewPage, int recNumber, ArrayList<Result> record,
            ArrayList<ArrayList<Result>> parentRecords) {

        int[] cols = gv.cols;
        DisplayItem di = new DisplayItem();
        di.width = cols[colIdx - offset];
        if (question.type != null && question.type.equals("calculate")) {
            di.text = ""; // Hack to remove labels from calculate questions
        } else {
            di.text = label.text == null ? "" : label.text;
        }
        di.text = lookupReferenceValue(di.text, record, parentRecords);

        di.hint = label.hint == null ? "" : label.hint;
        di.hint = lookupReferenceValue(di.hint, record, parentRecords);

        di.type = question.type;
        di.name = question.name;
        di.value = r.value;
        di.isNewPage = isNewPage;

        setQuestionFormats(question.appearance, di);
        di.fIdx = r.fIdx;
        di.qIdx = r.qIdx;
        di.rec_number = recNumber;

        items.add(di);
    }

    /*
     * Where a label includes a reference value such as ${name} then these need to be converted to the actual value
     */
    public String lookupReferenceValue(String input, ArrayList<Result> record,
            ArrayList<ArrayList<Result>> parentRecords) {

        StringBuffer newValue = new StringBuffer("");
        String v;

        // Return if we are generating a blank template
        if (parentRecords == null) {
            return input;
        }

        Pattern pattern = Pattern.compile("\\$\\{.+?\\}");
        java.util.regex.Matcher matcher = pattern.matcher(input);
        int start = 0;
        while (matcher.find()) {

            String matched = matcher.group();
            String qname = matched.substring(2, matched.length() - 1);

            // Add any text before the match
            int startOfGroup = matcher.start();
            newValue.append(input.substring(start, startOfGroup));

            // Add the matched value after lookup
            // First check in the current record
            v = lookupInRecord(qname, record);

            // If not found try each of the parent records starting from the closest
            if (v == null) {
                for (ArrayList<Result> p : parentRecords) {
                    v = lookupInRecord(qname, p);
                    if (v != null) {
                        break;
                    }
                }
            }

            // Still null!  well maybe this ${..} pattern was just meant to be
            if (v == null) {
                v = matcher.group();
            }
            newValue.append(v);

            // Reset the start
            start = matcher.end();

        }

        // Get the remainder of the string
        if (start < input.length()) {
            newValue.append(input.substring(start));
        }

        return newValue.toString();
    }

    /*
     * Lookup the value of a question in a record
     */
    private String lookupInRecord(String name, ArrayList<Result> record) {
        String value = null;

        for (Result r : record) {
            if (r.name.equals(name)) {
                value = r.value;
                break;
            }
        }

        return value;

    }

    /*
     * Get the number of blank repeats to generate
     */
    int getBlankRepeats(String appearance) {
        int repeats = 1;

        if (appearance != null) {
            String[] appValues = appearance.split(" ");
            if (appearance != null) {
                for (int i = 0; i < appValues.length; i++) {
                    if (appValues[i].startsWith("pdfrepeat")) {
                        String[] parts = appValues[i].split("_");
                        if (parts.length >= 2) {
                            repeats = Integer.valueOf(parts[1]);
                        }
                        break;
                    }
                }
            }
        }

        return repeats;
    }

    /*
     * Set the attributes for this question from keys set in the appearance column
     */
    void setQuestionFormats(String appearance, DisplayItem di) {

        if (appearance != null) {
            String[] appValues = appearance.split(" ");
            if (appearance != null) {
                for (int i = 0; i < appValues.length; i++) {
                    String app = appValues[i].trim().toLowerCase();
                    if (app.startsWith("pdflabelbg")) {
                        setColor(app, di, true);
                    } else if (app.startsWith("pdfvaluebg")) {
                        setColor(app, di, false);
                    } else if (app.startsWith("pdfmarkercolor")) {
                        di.markerColor = getRGBColor(app);
                    } else if (app.startsWith("pdflabelw")) {
                        setWidths(app, di);
                    } else if (app.startsWith("pdfheight")) {
                        setHeight(app, di);
                    } else if (app.startsWith("pdfspace")) {
                        setSpace(app, di);
                    } else if (app.equals("pdflabelcaps")) {
                        di.labelcaps = true;
                    } else if (app.equals("pdflabelbold")) {
                        di.labelbold = true;
                    } else if (app.startsWith("pdfmap")) { // mapbox map id
                        di.map = getAppValue(app);
                    } else if (app.startsWith("pdflocation")) {
                        di.location = getAppValue(app); // lon,lat,zoom
                    } else if (app.startsWith("pdfbarcode")) {
                        di.isBarcode = true;
                    } else if (app.startsWith("pdfzoom")) {
                        di.zoom = getAppValue(app);
                    } else if (app.startsWith("pdfhyperlink")) {
                        di.isHyperlink = true;
                    } else if (app.equals("signature")) {
                        di.isSignature = true;
                    }
                }
            }
        }
    }

    /*
     * Get the color values for a single appearance value
     * Format is:  xxxx_0Xrr_0Xgg_0xbb
     */
    void setColor(String aValue, DisplayItem di, boolean isLabel) {

        BaseColor color = null;

        String[] parts = aValue.split("_");
        if (parts.length >= 4) {
            if (parts[1].startsWith("0x")) {
                color = new BaseColor(Integer.decode(parts[1]), Integer.decode(parts[2]), Integer.decode(parts[3]));
            } else {
                color = new BaseColor(Integer.decode("0x" + parts[1]), Integer.decode("0x" + parts[2]),
                        Integer.decode("0x" + parts[3]));
            }
        }

        if (isLabel) {
            di.labelbg = color;
        } else {
            di.valuebg = color;
        }

    }

    /*
     * Get the color values for a single appearance value
     * Output is just the RGB value
     * Format is:  xxxx_0Xrr_0Xgg_0xbb
     */
    String getRGBColor(String aValue) {

        String rgbValue = "";

        String[] parts = aValue.split("_");
        if (parts.length >= 4) {
            rgbValue = parts[1] + parts[2] + parts[3];
        }
        return rgbValue;

    }

    /*
     * Set the widths of the label and the value
     * Appearance is:  pdflabelw_## where ## is a number from 0 to 10
     */
    void setWidths(String aValue, DisplayItem di) {

        String[] parts = aValue.split("_");
        if (parts.length >= 2) {
            di.widthLabel = Integer.valueOf(parts[1]);
        }

        // Do bounds checking
        if (di.widthLabel < 0 || di.widthLabel > 10) {
            di.widthLabel = 5;
        }

    }

    /*
     * Set the height of the value
     * Appearance is:  pdfheight_## where ## is the height
     */
    void setHeight(String aValue, DisplayItem di) {

        String[] parts = aValue.split("_");
        if (parts.length >= 2) {
            di.valueHeight = Double.valueOf(parts[1]);
        }

    }

    /*
     * Set space before this item
     * Appearance is:  pdfheight_## where ## is the height
     */
    void setSpace(String aValue, DisplayItem di) {

        String[] parts = aValue.split("_");
        if (parts.length >= 2) {
            di.space = Integer.valueOf(parts[1]);
        }

    }

    String getAppValue(String aValue) {
        String[] parts = aValue.split("_");
        if (parts.length >= 2) {
            return parts[1];
        } else
            return null;
    }

    /*
     * Add the question label, hint, and any media
     */
    private PdfPTable addDisplayItem(Parser parser, DisplayItem di, String basePath, String serverRoot,
            boolean generateBlank, GlobalVariables gv, String remoteUser, int oId)
            throws BadElementException, MalformedURLException, IOException {

        PdfPCell labelCell = new PdfPCell();
        PdfPCell valueCell = new PdfPCell();
        labelCell.setBorderColor(BaseColor.LIGHT_GRAY);
        valueCell.setBorderColor(BaseColor.LIGHT_GRAY);

        PdfPTable tItem = null;

        // Add label
        StringBuffer html = new StringBuffer();
        html.append("<span class='label ");
        if (di.labelbold) {
            html.append(" lbold");
        }

        // Get text value
        String textValue = "";
        if (di.text != null && di.text.trim().length() > 0) {
            textValue = di.text;
        } else {
            textValue = di.name;
        }
        textValue = textValue.trim();
        if (textValue.charAt(textValue.length() - 1) != ':') {
            textValue += ":";
        }
        if (di.labelcaps) {
            textValue = textValue.toUpperCase();
        }

        // Add language class
        html.append(GeneralUtilityMethods.getLanguage(textValue));
        html.append("'>");

        // Add text value
        html.append(GeneralUtilityMethods.unesc(textValue));
        html.append("</span>");

        // Only include hints if we are generating a blank template
        if (generateBlank) {
            html.append("<span class='hint ");
            if (di.hint != null) {
                html.append(GeneralUtilityMethods.getLanguage(di.hint));
                html.append("'>");
                html.append(GeneralUtilityMethods.unesc(di.hint));
            }
            html.append("</span>");
        }

        parser.elements.clear();
        try {
            parser.xmlParser.parse(new StringReader(html.toString()));

            for (Element element : parser.elements) {
                if (textValue != null && textValue.length() > 0) {
                    if (GeneralUtilityMethods.isRtlLanguage(textValue)) {
                        labelCell.setRunDirection(PdfWriter.RUN_DIRECTION_RTL);
                    }
                } else if (di.hint != null && di.hint.length() > 0) {
                    if (GeneralUtilityMethods.isRtlLanguage(textValue)) {
                        labelCell.setRunDirection(PdfWriter.RUN_DIRECTION_RTL);
                    }
                }
                labelCell.addElement(element);
            }
        } catch (Exception e) {
            log.info("Error parsing: " + html.toString() + " : " + e.getMessage());
            lm.writeLog(sd, survey.getId(), remoteUser, LogManager.ERROR,
                    e.getMessage() + " for: " + html.toString());
            labelCell.addElement(getPara(html.toString(), di, gv, null, null));
        }

        // Set the content of the value cell
        try {
            updateValueCell(parser, remoteUser, valueCell, di, generateBlank, basePath, serverRoot, gv, oId);
        } catch (Exception e) {
            log.info("Error updating value cell, continuing: " + basePath + " : " + di.value);
            log.log(Level.SEVERE, "Exception", e);
        }

        int widthValue = 5;
        if (di.widthLabel == 10) {
            widthValue = 1; // Label and value in 1 column
            di.widthLabel = 1;
            tItem = new PdfPTable(1);
        } else {
            // Label and value in 2 columns
            widthValue = 10 - di.widthLabel;
            tItem = new PdfPTable(10);
        }
        // Format label cell
        labelCell.setColspan(di.widthLabel);
        if (di.labelbg != null) {
            labelCell.setBackgroundColor(di.labelbg);
        }

        // Format value cell
        valueCell.setColspan(widthValue);
        if (di.valueHeight > -1.0) {
            valueCell.setMinimumHeight((float) di.valueHeight);
        }
        if (di.valuebg != null) {
            valueCell.setBackgroundColor(di.valuebg);
        }

        tItem.addCell(labelCell);
        tItem.addCell(valueCell);
        return tItem;
    }

    /*
     * Set the contents of the value cell
     */
    private void updateValueCell(Parser parser, String remoteUser, PdfPCell valueCell, DisplayItem di,
            boolean generateBlank, String basePath, String serverRoot, GlobalVariables gv, int oId)
            throws Exception {

        // Questions that append their values to this question
        ArrayList<String> deps = gv.addToList.get(di.fIdx + "_" + di.rec_number + "_" + di.name);

        if (di.type.startsWith("select")) {
            processSelect(parser, remoteUser, valueCell, di, generateBlank, gv, oId);
        } else if (di.type.equals("image")) {
            if (di.value != null && !di.value.trim().equals("") && !di.value.trim().equals("Unknown")) {
                if (di.isHyperlink) {
                    Anchor anchor = new Anchor(serverRoot + di.value);
                    anchor.setReference(serverRoot + di.value);

                    valueCell.addElement(getPara("", di, gv, deps, anchor));
                } else {
                    try {
                        Image img = Image.getInstance(basePath + "/" + di.value);
                        valueCell.addElement(img);
                    } catch (Exception e) {
                        log.info("Error: image " + basePath + "/" + di.value + " not added: " + e.getMessage());
                        log.log(Level.SEVERE, "Adding image to pdf", e);
                    }
                }

            } else {
                // TODO add empty image
            }

        } else if (di.type.equals("video") || di.type.equals("audio")) {
            if (di.value != null && !di.value.trim().equals("") && !di.value.trim().equals("Unknown")) {
                Anchor anchor = new Anchor(serverRoot + di.value);
                anchor.setReference(serverRoot + di.value);

                valueCell.addElement(getPara("", di, gv, deps, anchor));

            } else {
                // TODO add empty image
            }

        } else if (di.type.equals("geopoint") || di.type.equals("geoshape") || di.type.equals("geotrace")
                || di.type.startsWith("geopolygon_") || di.type.startsWith("geolinestring_")) {

            Image img = PdfUtilities.getMapImage(sd, di.map, di.value, di.location, di.zoom, gv.mapbox_key,
                    survey.id, user, di.markerColor);

            if (img != null) {
                valueCell.addElement(img);
            } else {
                valueCell.addElement(getPara(" ", di, gv, deps, null));
            }

        } else if (di.isBarcode) {
            BarcodeQRCode qrcode = new BarcodeQRCode(di.value.trim(), 1, 1, null);
            Image qrcodeImage = qrcode.getImage();
            qrcodeImage.setAbsolutePosition(10, 500);
            qrcodeImage.scalePercent(200);

            valueCell.addElement((qrcodeImage));

        } else {
            // Todo process other question types
            String value = null;

            if (di.value == null || di.value.trim().length() == 0) {
                value = " "; // Need a space to show a blank row
            } else {
                if (di.value != null && di.value.length() > 0) {
                    if (GeneralUtilityMethods.isRtlLanguage(di.value)) {
                        valueCell.setRunDirection(PdfWriter.RUN_DIRECTION_RTL);
                    }
                }

                if (di.type.equals("dateTime") || di.type.equals("timestamp")) { // Set date time to local time

                    DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    df.setTimeZone(TimeZone.getTimeZone("UTC"));
                    Date date = df.parse(di.value);
                    df.setTimeZone(TimeZone.getTimeZone(tz));
                    value = df.format(date);
                    log.info("Convert date to local time: " + di.name + " : " + di.value + " : " + " : " + value
                            + " : " + di.type + " : " + tz);
                } else {
                    value = di.value;
                }

            }
            valueCell.addElement(getPara(value, di, gv, deps, null));
        }
    }

    private Paragraph getPara(String value, DisplayItem di, GlobalVariables gv, ArrayList<String> deps,
            Anchor anchor) {

        boolean hasContent = false;
        Font f = null;
        boolean isRtl = false;
        String lang = "";

        Paragraph para = new Paragraph("", defaultFont);

        if (value != null && value.trim().length() > 0) {
            lang = GeneralUtilityMethods.getLanguage(value);
            f = getFont(lang);
            isRtl = isRtl(lang);
            para.add(new Chunk(GeneralUtilityMethods.unesc(value), f));
            hasContent = true;
        }

        // Add dependencies

        if (deps != null) {
            for (String n : deps) {
                if (n != null && n.trim().length() > 0) {
                    if (hasContent) {
                        para.add(new Chunk(",", defaultFont));
                    }

                    lang = GeneralUtilityMethods.getLanguage(n);
                    f = getFont(lang);
                    if (!isRtl) { // Don't override RTL if it has already been set
                        isRtl = isRtl(lang);
                    }
                    para.add(new Chunk(n, f));
                }

            }
        }
        if (anchor != null) {
            para.setFont(defaultFontLink);
            para.add(anchor);
            para.setFont(defaultFontLink);
        }
        return para;
    }

    /*
     * HTML equivalent of getPara
     */
    private String getHtml(String value, DisplayItem di, GlobalVariables gv, ArrayList<String> deps) {

        boolean hasContent = false;

        StringBuffer html = new StringBuffer();
        html.append("<span>");
        if (value != null && value.trim().length() > 0) {
            html.append(value);
            hasContent = true;
        }

        // Add dependencies

        if (deps != null) {
            for (String n : deps) {
                if (n != null && n.trim().length() > 0) {
                    if (hasContent) {
                        html.append(",");
                    }
                    html.append(n);
                }

            }
        }
        html.append("</span>");

        return html.toString();
    }

    private Font getFont(String lang) {
        Font f = defaultFont;

        if (lang.length() > 0) {
            if (lang.equals("arabic")) {
                f = arabicFont;
            } else if (lang.equals("bengali")) {
                f = bengaliFont;
            }
        }

        return f;
    }

    private boolean isRtl(String lang) {
        boolean isRtl = false;

        if (lang.length() > 0) {
            if (lang.equals("arabic")) {
                isRtl = true;
            }
        }
        return isRtl;
    }

    private void processSelect(Parser parser, String remoteUser, PdfPCell cell, DisplayItem di,
            boolean generateBlank, GlobalVariables gv, int oId) throws Exception {

        Font f = null;
        boolean isRtl = false;

        // If generating blank template
        List list = new List();
        list.setAutoindent(false);
        list.setSymbolIndent(24);

        String lang;

        boolean isSelectMultiple = di.type.equals("select") ? true : false;

        // Questions that append their values to this question
        ArrayList<String> deps = gv.addToList.get(di.fIdx + "_" + di.rec_number + "_" + di.name);

        /*
         * Add the value of this question unless
         *   The form is not blank and the value is "other" and their are 1 or more dependent questions
         *   In this case we assume that its only the values of the dependent questions that are needed
         */

        if (generateBlank) {
            // TODO get real choices using choice manager
            Form form = survey.forms.get(di.fIdx);
            Question question = form.questions.get(di.qIdx);
            OptionList ol = survey.optionLists.get(question.list_name);
            for (Option o : ol.options) {

                String text = null;
                if (o.display_name != null && o.display_name.trim().length() > 0) {
                    text = o.display_name;
                } else {
                    text = o.labels.get(languageIdx).text;
                }
                lang = GeneralUtilityMethods.getLanguage(text);
                f = getFont(lang);
                isRtl = isRtl(lang);

                ListItem item = new ListItem(GeneralUtilityMethods.unesc(text), f);

                if (isSelectMultiple) {
                    item.setListSymbol(new Chunk("\uf096", Symbols));
                    list.add(item);
                } else {
                    item.setListSymbol(new Chunk("\uf10c", Symbols));
                    list.add(item);
                }
            }

            if (isRtl) {
                cell.setRunDirection(PdfWriter.RUN_DIRECTION_RTL);
            }
            cell.addElement(list);

        } else {
            if (deps == null || (di.value != null && !di.value.trim().toLowerCase().equals("other"))) {

                String value = di.value;
                if (di.type.equals("select1")) {

                    Form form = survey.forms.get(di.fIdx);
                    Question question = form.questions.get(di.qIdx);

                    ArrayList<String> matches = new ArrayList<String>();
                    matches.add(di.value);
                    value = choiceManager.getLabel(sd, cResults, user, oId, survey.id, question.id, question.l_id,
                            question.external_choices, question.external_table,
                            survey.languages.get(languageIdx).name, languageIdx, matches, survey.ident);
                } else if (di.type.equals("select")) {
                    String nameValue = value;
                    if (nameValue != null) {
                        String vArray[] = nameValue.split(" ");
                        ArrayList<String> matches = new ArrayList<String>();
                        if (vArray != null) {
                            for (String v : vArray) {
                                matches.add(v);
                            }
                        }
                        Form form = survey.forms.get(di.fIdx);
                        Question question = form.questions.get(di.qIdx);
                        value = choiceManager.getLabel(sd, cResults, user, oId, survey.id, question.id,
                                question.l_id, question.external_choices, question.external_table,
                                survey.languages.get(languageIdx).name, languageIdx, matches, survey.ident);
                    }
                }

                if (GeneralUtilityMethods.isRtlLanguage(di.value)) {
                    cell.setRunDirection(PdfWriter.RUN_DIRECTION_RTL);
                }
                parser.elements.clear();
                String html = getHtml(value, di, gv, deps);
                try {
                    parser.xmlParser.parse(new StringReader(html));
                } catch (Exception e) {
                    log.info("Error parsing: " + html.toString() + " : " + e.getMessage());
                    lm.writeLog(sd, survey.getId(), remoteUser, LogManager.ERROR,
                            e.getMessage() + " for: " + html.toString());
                    cell.addElement(getPara(html.toString(), di, gv, null, null));
                }
                for (Element element : parser.elements) {
                    cell.addElement(element);
                }
                //cell.addElement(getPara(value, di, gv, deps, null));
            }

        }

    }

    /*
     * Get the value of a select question
     *
    String getSelectValue(boolean isSelectMultiple, DisplayItem di, ArrayList<String> deps) {
       StringBuffer sb = new StringBuffer("");
        
       for(DisplayItem aChoice : di.choices) {
        
     if(isSelectMultiple) {
        if(aChoice.isSet) {
        
           if(deps == null || (aChoice.name != null && !aChoice.name.trim().toLowerCase().equals("other"))) {
              if(sb.length() > 0) {
                 sb.append(", ");
              }
              sb.append(aChoice.text);
           }
        
        } 
     } else {
        if(aChoice.isSet) {
        
           if(deps == null || (aChoice.name != null && !aChoice.name.trim().toLowerCase().equals("other"))) {
              if(sb.length() > 0) {
                 sb.append(", ");
              }
              sb.append(aChoice.text);
           }
        
        }
     }
        
        
       }
        
       return sb.toString();
        
    }
    */

    /*
     * Fill in user details for the output when their is no template
     */
    private void fillNonTemplateUserDetails(Document document, User user, String basePath)
            throws IOException, DocumentException {

        String settings = user.settings;
        Type type = new TypeToken<UserSettings>() {
        }.getType();
        Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd").create();
        UserSettings us = gson.fromJson(settings, type);

        float indent = (float) 20.0;
        addValue(document, "Completed by:", (float) 0.0);
        if (user.signature != null && user.signature.trim().length() > 0) {
            String fileName = null;
            try {
                //fileName = basePath + user.signature;

                fileName = basePath + "/media/users/" + user.id + "/sig/" + user.signature;

                Image img = Image.getInstance(fileName);
                img.scaleToFit(200, 50);
                img.setIndentationLeft(indent);

                document.add(img);

            } catch (Exception e) {
                log.info(
                        "Error: Failed to add signature (non template) " + fileName + " to pdf: " + e.getMessage());
            }
        }
        addValue(document, user.name, indent);
        addValue(document, user.company_name, indent);
        if (us != null) {
            addValue(document, us.title, indent);
            addValue(document, us.license, indent);
        }

    }

    /*
     * Format a single value into a paragraph
     */
    private void addValue(Document document, String value, float indent) throws DocumentException {

        Font f = null;
        String lang;

        if (value != null && value.trim().length() > 0) {
            lang = GeneralUtilityMethods.getLanguage(value);
            f = getFont(lang);

            Paragraph para = new Paragraph("", f);
            para.setIndentationLeft(indent);
            para.add(new Chunk(GeneralUtilityMethods.unesc(value), f));
            document.add(para);
        }
    }

}