org.openxdata.server.sms.FormSmsParser.java Source code

Java tutorial

Introduction

Here is the source code for org.openxdata.server.sms.FormSmsParser.java

Source

/*
 *  Licensed to the OpenXdata Foundation (OXDF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The OXDF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with the License. 
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, 
 *  software distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and limitations under the License.
 *
 *  Copyright 2010 http://www.openxdata.org.
 */
package org.openxdata.server.sms;

import java.io.IOException;
import java.io.StringReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.openxdata.model.Condition;
import org.openxdata.model.OpenXdataConstants;
import org.openxdata.model.FormDef;
import org.openxdata.model.OptionData;
import org.openxdata.model.OptionDef;
import org.openxdata.model.PageData;
import org.openxdata.model.QuestionData;
import org.openxdata.model.QuestionDef;
import org.openxdata.model.SkipRule;
import org.openxdata.model.ValidationRule;
import org.openxdata.server.OpenXDataConstants;
import org.openxdata.server.admin.model.FormData;
import org.openxdata.server.admin.model.User;
import org.openxdata.server.admin.model.exception.OpenXDataParsingException;
import org.openxdata.server.admin.model.exception.OpenXDataSecurityException;
import org.openxdata.server.admin.model.exception.OpenXDataValidationException;
import org.openxdata.server.Context;
import org.openxdata.server.serializer.KxmlSerializerUtil;
import org.openxdata.server.service.AuthenticationService;
import org.openxdata.server.service.FormDownloadService;
import org.openxdata.server.service.SettingService;
import org.openxdata.server.service.UserService;
import org.openxdata.server.util.XmlUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * Responsible for parsing form data submitted as text sms.
 * 
 * @author daniel
 *
 */
@Component
public class FormSmsParser {

    /** The separator for different fields in the sms text. */
    private String FIELD_SEP_CHAR = "=";

    /** Determines if each sms is expected to contain a user name and password. */
    private boolean smsValidateNamePassword = true;

    /** Determines if we should accept only those phone numbers attached to user accounts. */
    private boolean smsValidatePhoneNo = false;

    private HashMap<String, String> formXml;

    private HashMap<String, FormDef> formDefs;

    @Autowired
    private FormDownloadService formDownloadService;

    @Autowired
    private UserService userService;

    @Autowired
    private SettingService settingService;

    private static final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

    org.openxdata.model.FormData formData;

    @Autowired
    private AuthenticationService authenticationService;

    //Response messages
    private String MSG_SMS_SHOULD_NOT_BE_EMPTY = "sms should not be empty";
    private String MSG_MISSING_SPACE_AFTER_USERNAME = "Expected space after username";
    private String MSG_MISSING_SPACE_AFTER_PASSWORD = "Expected space after password";
    private String MSG_ACCESS_DENIED = "Access denied for user";
    private String MSG_NUMBER_NOT_ATTACHED_TO_ANY_USER = "This phone number is not attached to any user account";
    private String MSG_MISSING_SPACE_AFTER_FORMID = "Expected space after form identifier";
    private String MSG_NO_FORM_WITH_IDENTIFIER = "No form found with identifier";
    private String MSG_NO_QUESTION_AT_POSITION = "Form has not question at position";
    private String MSG_IS_OUT_OF_RANGE_FOR = "is out of range for";
    private String MSG_FORM_HAS_NO_QUESTIONS = "Form has no questions";
    private String MSG_ANSWER_REQUIRED_FOR = "An answer is required for";
    private String MSG_SHOULD_BE_DATE = "should be a date";
    private String MSG_SHOULD_BE_IN_LIST = "should be in list {1,2,y,n,yes,no}";
    private String MSG_SHOULD_BE_DATE_TIME = "should be a date and time";
    private String MSG_SHOULD_BE_TIME = "should be time";
    private String MSG_SHOULD_BE_NUMBER = "should be a number";
    private String MSG_NO_ANSWER_EXPECTED_FOR = "no answer is expected for";
    private String MSG_DUE_TO_ANSWER_FOR = "Due to the answer for";
    private String MSG_USER_NOT_REGISTERED_FOR_NUMBER = "is not registered for this phone number";
    private static final String ERROR_UNABLE_TO_SET_FORM_DATA_DESCRIPTION = "Unable to set form data description in form xml";
    private static final String ERROR_PARSING_SMS_INTO_XFORM_MODEL = "Error parsing sms into xform model.";

    public FormSmsParser() {
    }

    void setAuthenticationService(AuthenticationService authenticationService) {
        this.authenticationService = authenticationService;
    }

    void setSettingService(SettingService settingService) {
        this.settingService = settingService;
    }

    void setUserService(UserService userService) {
        this.userService = userService;
    }

    public void setFormDownloadService(FormDownloadService formDownloadService) {
        this.formDownloadService = formDownloadService;
    }

    public void init() {
        formDefs = new HashMap<String, FormDef>();
        formXml = new HashMap<String, String>();

        //Prefetch a list of all form definitions from the server such that we do
        //not have to do it at each sms received, hence making a performance boost in
        //sms processing. The trade of is that if someone creates a new form after
        //the sms server has already been started, they have to restart it in order
        //to pick up the new form definitions.

        String val = settingService.getSetting("smsFieldSepChar");
        if (val != null)
            FIELD_SEP_CHAR = val;

        val = settingService.getSetting("smsValidateNamePassword");
        if ("false".equalsIgnoreCase(val))
            smsValidateNamePassword = false;

        val = settingService.getSetting("smsValidatePhoneNo");
        if ("true".equalsIgnoreCase(val))
            smsValidatePhoneNo = true;

        //We some how need to get the user who tries to submit data and hence we need to at least have one of these properties.
        if (smsValidateNamePassword == false && smsValidatePhoneNo == false)
            smsValidateNamePassword = true;

        loadCustomErrorMessages(settingService);

        //TODO Need to use proper locale
        List<String> forms = formDownloadService.getFormsDefaultVersionXml(null, "en");

        for (String xml : forms) {
            try {
                FormDef formDef = KxmlSerializerUtil.fromXform2FormDef(new StringReader(xml));
                formDefs.put(formDef.getVariableName(), formDef);
                formXml.put(formDef.getVariableName(), xml);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * Creates a form data item from a text sms.
     * sample sms= guyzb daniel123 newform 1=Daniel Kayiwa 2=67.8 3=m 4=1,3,4
     * 
     * @param sender
     * @param text
     * @return
     * @throws OpenXDataParsingException 
     * @throws OpenXDataValidationException 
     * @throws IOException 
     * @throws SAXException 
     * @throws ParserConfigurationException 
     */
    public FormData sms2FormData(String sender, String text)
            throws OpenXDataParsingException, OpenXDataValidationException {

        if (text == null || text.trim().length() == 0)
            throw new OpenXDataParsingException(MSG_SMS_SHOULD_NOT_BE_EMPTY);

        //Turned on for now to prevent requiring a restart of the sms server after a change in
        //settings or form definition. May introduce an unnecessary performance penalty.
        init();

        //First get off closing spaces if any.
        text = text.trim();

        //Authenticate the user

        //this returns the string position of the username and password, this is the position
        //passed to the initFormData object. the substring
        text = authenticateUser(sender, text);

        //Create an epihandy form data object.
        text = initFormData(text);

        //Zero or more spaces, followed by one or more digits, followed by zero or more spaces, followed by equal sign
        String[] values = text.split("\\s*\\d+\\s*" + getRegexFriendlyFieldSep());

        //Set the values from the sms
        List<String> errors = new ArrayList<String>();
        int pos, startindex = 0;
        for (int index = 1; index < values.length; index++) {
            String value = values[index];
            pos = text.indexOf(value, text.indexOf(FIELD_SEP_CHAR, startindex) + 1);
            String key = text.substring(startindex, pos); //eg 1=,2=,3=,4=
            startindex = pos + value.length();

            QuestionData questionData = getQuestion(key, formData, errors);
            if (questionData != null)
                setQuestionAnswer(questionData, formData, value.trim(), errors);
        }

        //Start with skip logic because it can make some fields mandatory
        //hence giving validation rules a chance to also catch these
        //fields whose mandatority is conditional.
        Vector<QuestionData> ruleRequiredQtns = new Vector<QuestionData>();
        String errorMsgs = getSkipErrorMsg(formData, ruleRequiredQtns);

        errorMsgs = addErrorMsg(errorMsgs, getValidationErrorMsg(formData, ruleRequiredQtns));

        for (String error : errors)
            errorMsgs = addErrorMsg(errorMsgs, error);

        if (errorMsgs != null)
            throw new OpenXDataValidationException(errorMsgs);

        //Get the xform model xml that is filled with data.
        String xml;
        try {
            String variableName = formData.getDef().getVariableName();
            StringReader stringReader = new StringReader(formXml.get(variableName));
            org.kxml2.kdom.Document document = KxmlSerializerUtil.getDocument(stringReader);
            xml = KxmlSerializerUtil.updateXformModel(document, formData);
        } catch (Exception e) {
            throw new OpenXDataParsingException(ERROR_PARSING_SMS_INTO_XFORM_MODEL);
        }

        //Create an openxdata form data object and fill values before returning to the caller.
        FormData data = new FormData();
        data.setData(xml);
        data.setFormDefVersionId(formData.getDef().getId());
        setFormDataDescription(xml, data);
        data.setDateCreated(new Date());
        data.setCreator(userService.getLoggedInUser());

        return data;
    }

    private String getRegexFriendlyFieldSep() {
        String regSep = FIELD_SEP_CHAR;

        regSep = regSep.replace("\\", "\\\\");
        regSep = regSep.replace("^", "\\^");
        regSep = regSep.replace("*", "\\*");
        regSep = regSep.replace("+", "\\+");
        regSep = regSep.replace("$", "\\$");

        regSep = regSep.replace("?", "\\?");
        regSep = regSep.replace(".", "\\.");
        regSep = regSep.replace("(", "\\(");
        regSep = regSep.replace(")", "\\)");
        regSep = regSep.replace("{", "\\{");
        regSep = regSep.replace("}", "\\}");

        regSep = regSep.replace("[", "\\[");
        regSep = regSep.replace("]", "\\]");
        regSep = regSep.replace("|", "\\|");

        return regSep;
    }

    private void setFormDataDescription(String xml, FormData formData) throws OpenXDataParsingException {
        try {
            DocumentBuilder db = dbf.newDocumentBuilder();
            Document doc = db.parse(IOUtils.toInputStream(xml, "UTF-8"));
            String descTemplate = doc.getDocumentElement()
                    .getAttribute(OpenXDataConstants.ATTRIBUTE_NAME_DESCRIPTION_TEMPLATE);

            formData.setDescription(XmlUtil.getDescriptionTemplate(doc.getDocumentElement(), descTemplate));
        } catch (ParserConfigurationException e) {
            handleFormDataException();
        } catch (SAXException e) {
            handleFormDataException();
        } catch (IOException e) {
            handleFormDataException();
        }
    }

    private void handleFormDataException() throws OpenXDataParsingException {
        throw new OpenXDataParsingException(ERROR_UNABLE_TO_SET_FORM_DATA_DESCRIPTION);
    }

    private String authenticateUser(String sender, String text) throws OpenXDataValidationException {

        int pos = 0;

        if (smsValidateNamePassword) {
            pos = text.indexOf(' ');
            if (pos < 0)
                throw new OpenXDataValidationException(MSG_MISSING_SPACE_AFTER_USERNAME);

            String username = text.substring(0, pos).trim();

            text = text.substring(pos).trim();
            pos = text.indexOf(' ');
            if (pos < 0)
                throw new OpenXDataValidationException(MSG_MISSING_SPACE_AFTER_PASSWORD);
            String password = text.substring(0, pos).trim();

            User user = null;
            user = authenticationService.authenticate(username, password);

            if (user == null)
                throw new OpenXDataSecurityException(MSG_ACCESS_DENIED + " " + username);
            if (smsValidatePhoneNo && !sender.equals(user.getPhoneNo()))
                throw new OpenXDataSecurityException("User " + username + " " + MSG_USER_NOT_REGISTERED_FOR_NUMBER);
        } else {
            assert (smsValidatePhoneNo);// Both smsValidateNamePassword and smsValidatePhoneNo cant be false

            User user = formDownloadService.getUserByPhoneNo(sender);
            if (user == null)
                throw new OpenXDataSecurityException(MSG_NUMBER_NOT_ATTACHED_TO_ANY_USER);

            Context.setAuthenticatedUser(user);
        }

        return text.substring(pos).trim();
    }

    private String initFormData(String text) throws OpenXDataValidationException {

        //Get the form identifier.
        int pos = text.indexOf(' ');
        if (pos < 0)
            throw new OpenXDataValidationException(MSG_MISSING_SPACE_AFTER_FORMID);

        String formid = text.substring(0, pos).trim();
        FormDef formDef = formDefs.get(formid);
        if (formDef == null) {
            Integer id = getFormId(formid);
            Collection<FormDef> forms = formDefs.values();
            for (FormDef form : forms) {
                if (form.getId() == id.intValue()) {
                    formDef = form;
                    break;
                }
            }
        }

        if (formDef == null)
            throw new OpenXDataValidationException(MSG_NO_FORM_WITH_IDENTIFIER + "=" + formid);

        formData = new org.openxdata.model.FormData(formDef);

        return text.substring(pos);
    }

    private QuestionData getQuestion(String idtext, org.openxdata.model.FormData formData, List<String> errors) {

        String text = idtext.substring(0, idtext.indexOf(FIELD_SEP_CHAR)).trim();
        int id = Integer.parseInt(text);
        QuestionData questionData = formData.getQuestion((byte) id);
        if (questionData == null)
            errors.add(MSG_NO_QUESTION_AT_POSITION + " " + id);
        return questionData;
    }

    private void setQuestionAnswer(QuestionData questionData, org.openxdata.model.FormData formData, String answer,
            List<String> errors) {
        //TODO May need to handle dynamic optiondef
        QuestionDef questionDef = questionData.getDef();
        if (questionDef.getType() == QuestionDef.QTN_TYPE_LIST_EXCLUSIVE
                || questionDef.getType() == QuestionDef.QTN_TYPE_LIST_EXCLUSIVE_DYNAMIC) {
            questionData.setAnswer(getOptionData(questionDef, answer, errors));
            formData.updateDynamicOptions(questionData, false);
        } else if (questionDef.getType() == QuestionDef.QTN_TYPE_LIST_MULTIPLE) {
            Vector<OptionData> optionAnswers = new Vector<OptionData>();
            String values[] = answer.split(" ");
            for (int index = 0; index < values.length; index++)
                optionAnswers.addElement(getOptionData(questionDef, values[index], errors));
            questionData.setAnswer(optionAnswers);
        } else if (questionDef.getType() == QuestionDef.QTN_TYPE_BOOLEAN)
            questionData.setAnswer(answer);
        else
            questionData.setTextAnswer(answer);
    }

    private OptionData getOptionData(QuestionDef questionDef, String answer, List<String> errors) {
        OptionDef optionDef = questionDef.getOptionWithValue(answer);
        if (optionDef == null) {
            try {
                int val = Integer.parseInt(answer) - 1;
                if (val < questionDef.getOptions().size() && val >= 0)
                    optionDef = (OptionDef) questionDef.getOptions().elementAt(val);
            } catch (Exception ex) {
            }
        }

        if (optionDef == null) {
            errors.add(answer + " " + MSG_IS_OUT_OF_RANGE_FOR + " " + questionDef.getText());

            //Since we have an out of range error message,we do not need to also report the 
            //required error, hence making the error report sms as small as possible.
            //This will not result into bugs only on condition that on each sms received,
            //a new formdef is constructed.
            questionDef.setMandatory(false);

            return null;
        }

        return new OptionData(optionDef);
    }

    /**
     * Get the validation error messages in a filled form.
     * 
     * @param formData the form data to validate.
     * @param ruleRequiredQtns a list of questions which become required after a firing of some rules.
     * @return a comma separated list of validation error messages if any, else null.
     */
    @SuppressWarnings("unchecked")
    private String getValidationErrorMsg(org.openxdata.model.FormData formData,
            Vector<QuestionData> ruleRequiredQtns) {
        String sErrors = null;

        //Check if form has any questions.
        Vector<PageData> pages = formData.getPages();
        if (pages == null || pages.size() == 0) {
            sErrors = MSG_FORM_HAS_NO_QUESTIONS;
            return sErrors;
        }

        //First get error messages for required fields which have not been answered
        //and answers not allowed for the data type.
        for (byte i = 0; i < pages.size(); i++) {
            PageData page = (PageData) pages.elementAt(i);
            for (byte j = 0; j < page.getQuestions().size(); j++) {
                QuestionData qtn = (QuestionData) page.getQuestions().elementAt(j);
                if (!ruleRequiredQtns.contains(qtn) && !qtn.isValid())
                    sErrors = addErrorMsg(sErrors, MSG_ANSWER_REQUIRED_FOR + " " + qtn.getDef().getText());

                //Do data type validation
                String msg = getTypeErrorMsg(qtn);
                if (msg != null)
                    sErrors = addErrorMsg(sErrors, msg);
            }
        }

        //Check if form has any validation rules.
        Vector<ValidationRule> rules = formData.getDef().getValidationRules();
        if (rules == null)
            return sErrors;

        //Deal with the user supplied validation rules
        for (int index = 0; index < rules.size(); index++) {
            ValidationRule rule = (ValidationRule) rules.elementAt(index);
            rule.setFormData(formData);
            if (!rule.isValid())
                sErrors = addErrorMsg(sErrors, rule.getErrorMessage());
        }

        return sErrors;
    }

    private String addErrorMsg(String errorMsgs, String msg) {
        if (msg != null) {
            if (errorMsgs == null)
                errorMsgs = "";
            else
                errorMsgs += ", ";
            errorMsgs += msg;
        }

        return errorMsgs;
    }

    private String getTypeErrorMsg(QuestionData questionData) {
        if (questionData.getAnswer() == null)
            return null;

        QuestionDef questionDef = questionData.getDef();
        switch (questionDef.getType()) {
        case QuestionDef.QTN_TYPE_BOOLEAN:
            if (questionData.getAnswer() == null)
                return null;
            else if ("1".equals(questionData.getAnswer()) || "yes".equals(questionData.getAnswer())
                    || "y".equals(questionData.getAnswer())) {
                questionData.setTextAnswer(QuestionData.TRUE_VALUE);
                return null;
            } else if ("2".equals(questionData.getAnswer()) || "no".equals(questionData.getAnswer())
                    || "n".equals(questionData.getAnswer())) {
                questionData.setTextAnswer(QuestionData.FALSE_VALUE);
                return null;
            } else
                return questionData.getAnswer() + " " + questionDef.getText() + " " + MSG_SHOULD_BE_IN_LIST;
        case QuestionDef.QTN_TYPE_TEXT:
            return null;
        case QuestionDef.QTN_TYPE_DATE:
            try {
                Date date = null;
                if (questionData.isDateFunction(questionData.getAnswer()))
                    date = new Date();
                else
                    date = fromDisplayString2Date(questionData.getAnswer().toString());
                questionData.setTextAnswer(fromDate2SubmitString(date));
                return null;
            } catch (Exception ex) {
                return questionData.getAnswer() + " " + questionDef.getText() + " " + MSG_SHOULD_BE_DATE;
            }
        case QuestionDef.QTN_TYPE_DATE_TIME:
            try {
                Date date = null;
                if (questionData.isDateFunction(questionData.getAnswer()))
                    date = new Date();
                else
                    date = fromDisplayString2DateTime(questionData.getAnswer().toString());
                questionData.setTextAnswer(fromDateTime2SubmitString(date));
                return null;
            } catch (Exception ex) {
                return questionData.getAnswer() + " " + questionDef.getText() + " " + MSG_SHOULD_BE_DATE_TIME;
            }
        case QuestionDef.QTN_TYPE_TIME:
            try {
                Date date = null;
                if (questionData.isDateFunction(questionData.getAnswer()))
                    date = new Date();
                else
                    date = fromDisplayString2Time(questionData.getAnswer().toString());
                questionData.setTextAnswer(fromTime2SubmitString(date));
                return null;
            } catch (Exception ex) {
                return questionData.getAnswer() + " " + questionDef.getText() + " " + MSG_SHOULD_BE_TIME;
            }
        case QuestionDef.QTN_TYPE_LIST_EXCLUSIVE:
        case QuestionDef.QTN_TYPE_LIST_EXCLUSIVE_DYNAMIC:
            return null;
        case QuestionDef.QTN_TYPE_LIST_MULTIPLE:
            return null;
        case QuestionDef.QTN_TYPE_DECIMAL:
        case QuestionDef.QTN_TYPE_NUMERIC:
            try {
                Double.parseDouble(questionData.getAnswer().toString());
                return null;
            } catch (Exception ex) {
                return questionData.getAnswer() + " " + questionDef.getText() + " " + MSG_SHOULD_BE_NUMBER;
            }
        }

        return null;
    }

    /**
     * Converts a date and time display string to a date object.
     * 
     * @param dateTime the date and time display string.
     * @return the date object.
     */
    private Date fromDisplayString2DateTime(String dateTime) throws ParseException {
        SimpleDateFormat dateFormat = new SimpleDateFormat(
                settingService.getSetting(OpenXDataConstants.SETTING_NAME_DISPLAY_DATETIME_FORMAT,
                        OpenXDataConstants.DEFAULT_DATETIME_DISPLAY_FORMAT));
        return dateFormat.parse(dateTime);
    }

    /**
     * Converts a date display string to a date object.
     * 
     * @param date the date display string.
     * @return the date object.
     */
    private Date fromDisplayString2Date(String date) throws ParseException {
        SimpleDateFormat dateFormat = new SimpleDateFormat(
                settingService.getSetting(OpenXDataConstants.SETTING_NAME_DISPLAY_DATE_FORMAT,
                        OpenXDataConstants.DEFAULT_DATE_DISPLAY_FORMAT));
        return dateFormat.parse(date);
    }

    /**
     * Converts time to its xml text representation.
     * 
     * @param time time object to convert.
     * @return the xml text representation of the time.
     */
    private String fromTime2SubmitString(Date time) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(settingService.getSetting(
                OpenXDataConstants.SETTING_NAME_SUBMIT_TIME_FORMAT, OpenXDataConstants.DEFAULT_TIME_SUBMIT_FORMAT));
        return dateFormat.format(time);
    }

    /**
     * Converts a date to its xml text representation.
     * 
     * @param date the date to convert.
     * @return the xml text representation of the date.
     */
    private String fromDate2SubmitString(Date date) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(settingService.getSetting(
                OpenXDataConstants.SETTING_NAME_SUBMIT_DATE_FORMAT, OpenXDataConstants.DEFAULT_DATE_SUBMIT_FORMAT));
        return dateFormat.format(date);
    }

    /**
     * Converts a time display string to a date object.
     * 
     * @param time the time display string.
     * @return the date object.
     */
    private Date fromDisplayString2Time(String time) throws ParseException {
        SimpleDateFormat dateFormat = new SimpleDateFormat(
                settingService.getSetting(OpenXDataConstants.SETTING_NAME_DISPLAY_TIME_FORMAT,
                        OpenXDataConstants.DEFAULT_TIME_DISPLAY_FORMAT));
        return dateFormat.parse(time);
    }

    /**
     * Converts a date and time object to its xml text representation.
     * 
     * @param dateTime the date and time object to convert.
     * @return the xml text representation of the date and time.
     */
    private String fromDateTime2SubmitString(Date dateTime) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(
                settingService.getSetting(OpenXDataConstants.SETTING_NAME_SUBMIT_DATETIME_FORMAT,
                        OpenXDataConstants.DEFAULT_DATETIME_SUBMIT_FORMAT));
        return dateFormat.format(dateTime);
    }

    private Integer getFormId(String id) {
        try {
            return Integer.parseInt(id);
        } catch (Exception ex) {
        }

        return null;
    }

    /**
     * Get the skip logic error messages in a filled form. (eg pregnant males)
     * 
     * @param formData the form data to validate.
     * @param ruleRequiredQtns a list of questions which become required after rule firing.
     * @return a comma separated list of validation error messages if any, else null.
     */
    @SuppressWarnings("unchecked")
    private String getSkipErrorMsg(org.openxdata.model.FormData formData, Vector<QuestionData> ruleRequiredQtns) {
        String sErrors = null;

        //Check if form has any questions.
        Vector<PageData> pages = formData.getPages();
        if (pages == null || pages.size() == 0) {
            sErrors = MSG_FORM_HAS_NO_QUESTIONS;
            return sErrors;
        }

        //Check if form has any skip rules.
        Vector<SkipRule> rules = formData.getDef().getSkipRules();
        if (rules == null)
            return sErrors;

        //Deal with the user supplied validation rules
        for (int index = 0; index < rules.size(); index++) {
            SkipRule rule = (SkipRule) rules.elementAt(index);
            Vector<QuestionData> answeredQtns = getAnsweredQuestions(formData, rule.getActionTargets());

            rule.fire(formData);

            //Get the question text of the first condition. This could be improved with a user supplied skip logic error message, in future.
            String qtnText = formData.getQuestion(((Condition) rule.getConditions().elementAt(0)).getQuestionId())
                    .getText();

            boolean mandatoryRule = (rule.getAction() & OpenXdataConstants.ACTION_MAKE_MANDATORY) != 0;

            Vector<Byte> ids = rule.getActionTargets();
            for (byte i = 0; i < ids.size(); i++) {
                QuestionData questionData = formData.getQuestion(Byte.parseByte(ids.elementAt(i).toString()));

                //Check if the user answered a question they were supposed to skip.
                if (!questionData.isAnswered() && answeredQtns.contains(questionData))
                    sErrors = addErrorMsg(sErrors, MSG_DUE_TO_ANSWER_FOR + " " + qtnText + ", "
                            + MSG_NO_ANSWER_EXPECTED_FOR + " " + questionData.getDef().getText());

                //Check is the user has not answered a question which has become required after an answer to some other question.
                if (mandatoryRule && questionData.getDef().isMandatory() && !questionData.isAnswered())
                    sErrors = addErrorMsg(sErrors, MSG_DUE_TO_ANSWER_FOR + " " + qtnText + ", "
                            + MSG_NO_ANSWER_EXPECTED_FOR + " " + questionData.getDef().getText());

                if (mandatoryRule)
                    ruleRequiredQtns.add(questionData);
            }
        }

        return sErrors;
    }

    /**
     * Gets a list of questions which have been answered.
     * 
     * @param formData the form data.
     * @param ids
     * @return list of answered questions
     */
    private Vector<QuestionData> getAnsweredQuestions(org.openxdata.model.FormData formData, Vector<Byte> ids) {
        Vector<QuestionData> qtns = new Vector<QuestionData>();

        for (byte i = 0; i < ids.size(); i++) {
            QuestionData questionData = formData.getQuestion(Byte.parseByte(ids.elementAt(i).toString()));
            if (questionData.isAnswered())
                qtns.add(questionData);
        }

        return qtns;
    }

    private void loadCustomErrorMessages(SettingService service) {
        String val = service.getSetting("MSG_MISSING_SPACE_AFTER_USERNAME");
        if (val != null && val.trim().length() > 0)
            MSG_MISSING_SPACE_AFTER_USERNAME = val;

        val = service.getSetting("MSG_MISSING_SPACE_AFTER_PASSWORD");
        if (val != null && val.trim().length() > 0)
            MSG_MISSING_SPACE_AFTER_PASSWORD = val;

        val = service.getSetting("MSG_ACCESS_DENIED");
        if (val != null && val.trim().length() > 0)
            MSG_ACCESS_DENIED = val;

        val = service.getSetting("MSG_NUMBER_NOT_ATTACHED_TO_ANY_USER");
        if (val != null && val.trim().length() > 0)
            MSG_NUMBER_NOT_ATTACHED_TO_ANY_USER = val;

        val = service.getSetting("MSG_MISSING_SPACE_AFTER_FORMID");
        if (val != null && val.trim().length() > 0)
            MSG_MISSING_SPACE_AFTER_FORMID = val;

        val = service.getSetting("MSG_NO_FORM_WITH_IDENTIFIER");
        if (val != null && val.trim().length() > 0)
            MSG_NO_FORM_WITH_IDENTIFIER = val;

        val = service.getSetting("MSG_NO_QUESTION_AT_POSITION");
        if (val != null && val.trim().length() > 0)
            MSG_NO_QUESTION_AT_POSITION = val;

        val = service.getSetting("MSG_IS_OUT_OF_RANGE_FOR");
        if (val != null && val.trim().length() > 0)
            MSG_IS_OUT_OF_RANGE_FOR = val;

        val = service.getSetting("MSG_FORM_HAS_NO_QUESTIONS");
        if (val != null && val.trim().length() > 0)
            MSG_FORM_HAS_NO_QUESTIONS = val;

        val = service.getSetting("MSG_ANSWER_REQUIRED_FOR");
        if (val != null && val.trim().length() > 0)
            MSG_ANSWER_REQUIRED_FOR = val;

        val = service.getSetting("MSG_SHOULD_BE_DATE");
        if (val != null && val.trim().length() > 0)
            MSG_SHOULD_BE_DATE = val;

        val = service.getSetting("MSG_SHOULD_BE_IN_LIST");
        if (val != null && val.trim().length() > 0)
            MSG_SHOULD_BE_IN_LIST = val;

        val = service.getSetting("MSG_SHOULD_BE_DATE_TIME");
        if (val != null && val.trim().length() > 0)
            MSG_SHOULD_BE_DATE_TIME = val;

        val = service.getSetting("MSG_SHOULD_BE_TIME");
        if (val != null && val.trim().length() > 0)
            MSG_SHOULD_BE_TIME = val;

        val = service.getSetting("MSG_SHOULD_BE_NUMBER");
        if (val != null && val.trim().length() > 0)
            MSG_SHOULD_BE_NUMBER = val;

        val = service.getSetting("MSG_NO_ANSWER_EXPECTED_FOR");
        if (val != null && val.trim().length() > 0)
            MSG_NO_ANSWER_EXPECTED_FOR = val;

        val = service.getSetting("MSG_DUE_TO_ANSWER_FOR");
        if (val != null && val.trim().length() > 0)
            MSG_DUE_TO_ANSWER_FOR = val;

        val = service.getSetting("MSG_USER_NOT_REGISTERED_FOR_NUMBER");
        if (val != null && val.trim().length() > 0)
            MSG_USER_NOT_REGISTERED_FOR_NUMBER = val;

        val = service.getSetting("MSG_SMS_SHOULD_NOT_BE_EMPTY");
        if (val != null && val.trim().length() > 0)
            MSG_SMS_SHOULD_NOT_BE_EMPTY = val;
    }
}