com.healthcit.cacure.businessdelegates.GeneratedModuleDataManager.java Source code

Java tutorial

Introduction

Here is the source code for com.healthcit.cacure.businessdelegates.GeneratedModuleDataManager.java

Source

/*L
 * Copyright HealthCare IT, Inc.
 *
 * Distributed under the OSI-approved BSD 3-Clause License.
 * See http://ncip.github.com/edct-formbuilder/LICENSE.txt for details.
 */

package com.healthcit.cacure.businessdelegates;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;

import com.healthcit.cacure.dao.CouchDBDao;
import com.healthcit.cacure.dao.FormDao;
import com.healthcit.cacure.dao.ModuleDao;
import com.healthcit.cacure.model.Answer;
import com.healthcit.cacure.model.AnswerValue;
import com.healthcit.cacure.model.BaseForm;
import com.healthcit.cacure.model.BaseModule;
import com.healthcit.cacure.model.BaseQuestion;
import com.healthcit.cacure.model.FormElement;
import com.healthcit.cacure.model.LinkElement;
import com.healthcit.cacure.model.Question;
import com.healthcit.cacure.model.QuestionnaireForm;
import com.healthcit.cacure.model.TableQuestion;
import com.healthcit.cacure.model.admin.GeneratedModuleDataDetail;
import com.healthcit.cacure.utils.ConcurrentUtils;
import com.healthcit.cacure.utils.DateUtils;
import com.healthcit.cacure.utils.PropertyUtils;
import com.healthcit.cacure.utils.RandomGeneratorUtils;
import com.healthcit.cacure.utils.RandomGeneratorUtils.Algorithm;

/**
 * Business delegate class used for preparing sample form data and saving it to CouchDB.
 * @author Oawofolu
 *
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * NOTES on UNIQUENESS:
 * "uniquePerAllModules", "uniquePerEntity" and "uniquePerEntityModules"
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * When generating sample data, users should be able to choose
 * whether or not their generated modules should have any uniqueness constraints.
 * 
 * The following uniqueness constraints have been defined:
 * 
 * 1) Unique Per All Modules
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * In Relational DB terms, a field with this constraint
 * is part of a primary key whose target is the module.
 * On the front-end, users will select "Module Unique" to enable this constraint. 
 * 
 * 2) Unique Per Entity
 * ~~~~~~~~~~~~~~~~~~~~
 * In Relational DB terms, a field with this constraint
 * is part of a primary key whose target is the entity.
 * On the front-end, users will select "Entity Specific" to enable this constraint.
 * 
 * 3) Unique Per Entity Modules
 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * In Relational DB terms, a field with this constraint
 * is part of a unique key  whose target is the module and 
 * which includes this field,
 * any other fields with this constraint,
 * and the entity.
 * On the front-end, users will select "Entity Unique" to enable this constraint.  
 * 
 */

public class GeneratedModuleDataManager {
    private static Logger log = Logger.getLogger(GeneratedModuleDataManager.class);
    private static String FORM_ID = "formId";
    private static String QUESTIONS = "questions";
    private static String FORM_NAME = "formName";
    private static String MODULE_NAME = "moduleName";
    private static String ENTITY_ID = "ownerId";
    private static String MODULE_ID = "moduleId";
    private static String UPDATED_DATE = "updatedDate";
    private static String TEXT = "text";
    private static String UUID_VALUE = "uuid";
    private static String DATATYPE = "datatype";
    private static String STATIC = "static";
    private static String LOWERBOUND = "lowerbound";
    private static String UPPERBOUND = "upperbound";
    private static String LIST = "list";
    private static String SELECTED = "selected";
    private static String DYNAMIC = "dynamic";
    private static String SHORTNAME = "shortname";
    private static String QUESTION_ID = "questionId";
    private static String LINK_ID = "linkId";
    private static String QUESTION_SN = "questionSn";
    private static String QUESTION_TEXT = "questionText";
    private static String TABLE_QUESTION_TEXT = "tableQuestionText";
    private static String TABLE_QUESTION_ID = "tableQuestionId";
    private static String TABLE_QUESTION_SN = "tableQuestionSn";
    private static String TABLE_QUESTION_FIRST_COLUMN = "tableQuestionFirstColumn";
    private static String TABLE_QUESTION_IDENTIFYING_COLUMN = "tableQuestionIdentifyingColumn";
    private static String ANSWERVALUE_ID = "ansId";
    private static String ANSWERVALUE_SN = "ansSn";
    private static String ANSWERVALUE_TEXT = "ansText";
    private static String ANSWERVALUE_VALUE = "ansValue";
    private static String ANSWERVALUES = "answerValues";
    private static String UNIQUE_PER_ENTITY = "uniquePerEntity";
    private static String UNIQUE_PER_ALL_MODULES = "uniquePerAllModules";
    private static String UNIQUE_PER_ENTITY_MODULES = "uniquePerEntityModules";

    @Autowired
    private FormDao formDao;

    @Autowired
    private ModuleDao moduleDao;

    @Autowired
    private CouchDBDao couchDbDao;

    /**
     * 
     * @param moduleDetail
     * @throws IOException
     * @throws URISyntaxException
     */
    public void generateSampleDataInCouchDB(GeneratedModuleDataDetail moduleDetail)
            throws IOException, URISyntaxException {
        log.debug("In generateSampleDataInCouchDB method...");

        // Generate the list of modules to be saved
        JSONArray modules = generateModuleObjects(moduleDetail);

        // From the list of modules, generate the actual documents that 
        // will be saved to CouchDB
        JSONArray couchDbDocuments = generateCouchDbReadyDocuments(modules, moduleDetail);

        // Save the document to CouchDB
        saveDocuments(couchDbDocuments, moduleDetail);

        log.debug("Sample data generated successfully.");
    }

    /**
     * Saves a list of modules to the appropriate CouchDB database
     * TODO: Currently each document represents all the questions and answers
     * associated with a module.
     * Instead of saving each wholesale module,
     * the documents should be broken down into actual forms
     * which could then be saved individually to CouchDb.
     * @throws URISyntaxException 
     * @throws IOException 
     */
    private void saveDocuments(JSONArray modules, GeneratedModuleDataDetail moduleDetail)
            throws IOException, URISyntaxException {
        log.debug("Saving randomly generated documents...");
        log.debug("...............................");
        log.debug("...............................");

        // Get the database host
        String couchDbHost = moduleDetail.getCouchDbHost();

        // Get the database port
        int couchDbPort = moduleDetail.getCouchDbPort();

        // Get the database name
        String couchDbName = moduleDetail.getCouchDbName();

        // save the documents
        couchDbDao.bulkWriteToDb(modules, couchDbHost, couchDbPort, couchDbName);
    }

    /**
     * From a list of modules, generates the corresponding form documents 
     * which will be saved to CouchDb
     */
    private JSONArray generateCouchDbReadyDocuments(JSONArray modules, GeneratedModuleDataDetail moduleDetail) {
        return generateFormDocuments(modules, moduleDetail);
    }

    private JSONArray generateFormDocuments(JSONArray modules, GeneratedModuleDataDetail moduleDetail) {
        log.debug("In generateFormDocuments method...");
        log.debug("..................................");
        log.debug("..................................");

        // metadata associated with the module
        Map<String, String> metadata = getMetadataForModule(moduleDetail.getModuleId(), false);

        // number of documents to be generated per module
        int numFormsPerModule = moduleDetail.getActualNumberOfCouchDbDocuments()
                / moduleDetail.getActualNumberOfModules();

        JSONObject[] documentArray = new JSONObject[moduleDetail.getActualNumberOfCouchDbDocuments()];

        JSONArray documentJSONArray = new JSONArray();

        log.debug("Number of modules generated: " + modules.size());
        log.debug(
                "Number of CouchDB documents to be generated: " + moduleDetail.getActualNumberOfCouchDbDocuments());

        // Generate the documents in a multithreaded fashion
        List<Callable<Object>> tasks = new ArrayList<Callable<Object>>();

        for (int index = 0; index < modules.size(); ++index) {
            Callable<Object> task = new GenerateCouchDbDocumentCommand(modules, index, numFormsPerModule,
                    documentArray, metadata);

            tasks.add(task);
        }

        ConcurrentUtils.invokeBulkActions(tasks);

        // remove any null entries from the documentArray
        while (ArrayUtils.contains(documentArray, null)) {
            documentArray = (JSONObject[]) ArrayUtils.removeElement(documentArray, null);
        }

        // add the documents to the JSON array
        documentJSONArray.addAll(Arrays.asList(documentArray));

        documentArray = null;

        //debugging
        log.debug("Number of documents actually generated and ready to save: " + documentJSONArray.size());
        if (documentJSONArray.size() > 0)
            log.debug("First document to be saved: " + documentJSONArray.getJSONObject(0));

        // return the JSON array
        return documentJSONArray;
    }

    /**
     * Generates a list of modules to be saved to CouchDB
     */
    public JSONArray generateModuleObjects(GeneratedModuleDataDetail formDetail) {
        log.debug("Generating list of modules...");
        log.debug("...............................");
        log.debug("...............................");

        // list of generated modules
        JSONArray list = new JSONArray();

        // actual number of modules to be generated
        int actualNumberOfModules = formDetail.getActualNumberOfModules();

        // actual number of entities to be generated
        int actualNumberOfEntities = formDetail.getActualNumberOfEntities();

        // actual number of CouchDb documents to be generated
        int numberOfDocumentsPerModule = formDao.getNumberOfFormsForModuleId(new Long(formDetail.getModuleId()))
                .intValue();

        int actualNumberOfDocuments = numberOfDocumentsPerModule * actualNumberOfModules;

        formDetail.setActualNumberOfCouchDbDocuments(actualNumberOfDocuments);

        // maximum number of modules per entity
        int numberOfModulesPerEntity = (int) Math.floor(actualNumberOfModules / actualNumberOfEntities);

        // debugging
        log.debug("Number of modules to be generated: " + actualNumberOfModules);
        log.debug("Number of entities to be generated: " + actualNumberOfEntities);

        // entity Id
        String entityId = null;

        // keep track of the last generated unique key
        Map<String, JSONObject> lastUniqueKey = new HashMap<String, JSONObject>();

        for (int index = 0; index < actualNumberOfModules; ++index) {
            // Get an "entity-module" id
            int entityModuleId = index % numberOfModulesPerEntity;

            // Get a new entity Id, whenever appropriate
            if (entityModuleId == 0)
                entityId = UUID.randomUUID().toString();

            // Set up the unique keys for this module
            Map<String, JSONObject> uniqueKey = generateUniqueKey(formDetail, lastUniqueKey, entityId, index,
                    entityModuleId);

            JSONObject module = generateModuleData(formDetail, uniqueKey, lastUniqueKey, entityId);

            // track this unique key for later
            lastUniqueKey.clear();
            lastUniqueKey.putAll(uniqueKey);

            list.add(module);
        }

        return list;
    }

    /**
     * Takes a GeneratedFormDataDetail object and uses it to generate random CouchDb data
     * (a JSON object) for a module.
     */
    @SuppressWarnings("unchecked")
    private JSONObject generateModuleData(GeneratedModuleDataDetail form, Map<String, JSONObject> uniqueKey,
            Map<String, JSONObject> lastUniqueKey, String entityId) {
        JSONObject jsonForm = new JSONObject();

        // set up entity ID
        jsonForm.put(ENTITY_ID, entityId);

        // set up module ID (random string)
        jsonForm.put(MODULE_ID, UUID.randomUUID().toString());

        // set up the updatedDate (current timestamp)
        jsonForm.put(UPDATED_DATE, DateUtils.formatDateUTC(DateUtils.now()));

        // set up questions
        JSONObject questions = new JSONObject();

        for (Map<String, Object> obj : form.getQuestionFields()) {
            JSONObject jsonQuestion = new JSONObject();

            if (GeneratedModuleDataDetail.isSelected(obj)) {
                Map<String, Object> selectedQuestion = obj;

                // debugging
                JSONObject debugObj = new JSONObject();
                debugObj.putAll(selectedQuestion);
                log.debug("Selected question..." + debugObj);

                // question UUID
                String questionUUID = (String) selectedQuestion.get(UUID_VALUE);

                // set "questionId"
                jsonQuestion.put(QUESTION_ID, questionUUID);

                // set "linkId"
                jsonQuestion.put(LINK_ID, selectedQuestion.get(LINK_ID));

                // set "questionSn"
                jsonQuestion.put(QUESTION_SN, selectedQuestion.get(SHORTNAME));

                // set "questionText"
                jsonQuestion.put(QUESTION_TEXT, selectedQuestion.get(TEXT));

                // set "answer values"
                jsonQuestion.put(ANSWERVALUES, selectedQuestion.get(ANSWERVALUES));

                // set "form name" - temporary value which will be used to generate CouchDB-ready documents later
                jsonQuestion.put(FORM_NAME, selectedQuestion.get(FORM_NAME));

                // set "module name" - temporary value which will be used to generate CouchDB-ready documents later
                jsonQuestion.put(MODULE_NAME, selectedQuestion.get(MODULE_NAME));

                // check if this is one of the unique key questions
                boolean isUniqueKeyQuestion = uniqueKey.containsKey(questionUUID);

                // set up answer values
                // NOTE: Currently we will only generate one answer value for each question
                // 
                JSONArray jsonAnswerValuesArray = new JSONArray();

                JSONArray randomQuestionAnswerValues = new JSONArray();

                // If this question is one of the "unique-per-entity", "unique-per-entity-modules" or "unique-per-all-modules" questions,
                // then get the answer value from the uniqueKey map;
                // else, generate a random answer value object.
                Object lastRandomQuestionAnswerValue = lastUniqueKey.get(questionUUID) == null ? null
                        : getAnswerValue(lastUniqueKey.get(questionUUID));

                Map randomQuestionAnswerValue = isUniqueKeyQuestion ? uniqueKey.get(questionUUID)
                        : generateRandomAnswerValue(selectedQuestion, lastRandomQuestionAnswerValue,
                                Algorithm.PSEUDORANDOM);

                randomQuestionAnswerValues.add(randomQuestionAnswerValue);

                for (Object obj2 : randomQuestionAnswerValues) {

                    JSONObject answerValue = (JSONObject) obj2;

                    JSONObject jsonAnswer = new JSONObject();

                    // get answer value text 
                    String answerValueText = (String) answerValue.get(ANSWERVALUE_TEXT);

                    // get answer value value
                    String answerValueValue = answerValue.get(ANSWERVALUE_VALUE).toString();

                    // answer value id
                    jsonAnswer.put(ANSWERVALUE_ID, answerValue.get(ANSWERVALUE_ID));

                    // answer value shortname
                    jsonAnswer.put(ANSWERVALUE_SN, answerValue.get(ANSWERVALUE_SN));

                    // answer value text
                    jsonAnswer.put(ANSWERVALUE_TEXT, answerValueText);

                    // answer value value
                    jsonAnswer.put(ANSWERVALUE_VALUE, answerValueValue);

                    jsonAnswerValuesArray.add(jsonAnswer);

                    // track this answer for future frequency analysis
                    form.trackQuestionAndAnswer(questionUUID,
                            StringUtils.defaultIfEmpty(answerValueText, answerValueValue));
                }

                jsonQuestion.put(ANSWERVALUES, jsonAnswerValuesArray);

                questions.put(questionUUID, jsonQuestion);
            }
        }
        jsonForm.put(QUESTIONS, questions);

        return jsonForm;
    }

    /**
     * This method generates the "questionFields" collection in the GeneratedFormDataDetail entity
     * from the given form UUID.
     */
    @SuppressWarnings("unchecked")
    public List<Map<String, Object>> generateQuestionFields(QuestionnaireForm form) {
        log.debug("In generateQuestionFields method...");

        // Initialize a JSONArray
        List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();

        // Get the form's questions
        List<FormElement> formElements = form.getElements();

        for (FormElement formElement : formElements) {
            Object questionsObject = PropertyUtils.readProperty(formElement, "questions");

            if (questionsObject instanceof Collection) {
                Collection<? extends BaseQuestion> questions = (Collection<? extends BaseQuestion>) questionsObject;

                for (BaseQuestion baseQuestion : questions) {
                    if (baseQuestion instanceof Question || baseQuestion instanceof TableQuestion) // do not add ExternalQuestions
                    {
                        // Add a questionField to the array
                        Map questionField = getQuestionField(formElement, baseQuestion, form);

                        // Add to the array if it's not null
                        if (questionField != null)
                            list.add(questionField);
                    }
                }
            }
        }

        return list;
    }

    /**
     * This method generates a "questionField" from a Question object.
     * The structure is as follows:
     *  // question UUID, text, Shortname, type, etc
     *  { questionId: ..., questionText: ..., questionSn: ..., 
     *    list:COMMA DELIMITED LIST,lowerbound:...,upperbound:...,
     *  // static answer values  are answer values that can be selected (not free text) 
     *        {answervalues : { static : [{ansId:...,ansVal:...,ansText:...,ansSn:...}],
     *  // dynamic answer values are the free text answer values
     *                        dynamic : [{ansId:...,ansVal:...,ansText:...,ansSn:...}]}}}
     */
    @SuppressWarnings("unchecked")
    private Map getQuestionField(FormElement formElement, BaseQuestion q, QuestionnaireForm form) {
        // If this is a Table Question then initialize an object casting it as a TableQuestion entity;
        // else,  initialize an object casting it as a Question entity
        TableQuestion tableQuestion = null;

        Question question = null;

        if (q instanceof TableQuestion) {
            tableQuestion = (TableQuestion) q;
        }

        if (q instanceof Question) {
            question = (Question) q;
        }

        // If this is not a Table Question then initialize an object casting it as a Question entity

        // Get the answer element associated with the question
        Answer a = q.getAnswer();

        // Get the question UUID (use the FormElement uuid)
        String uuid = formElement.getUuid();

        // Get the link ID, if it exists
        String linkId = (formElement instanceof LinkElement) ? ((LinkElement) formElement).getSourceId() : "";

        // Get the question text
        String text = (tableQuestion != null) ? tableQuestion.getDescription()
                : question.getQuestionElement().getDescription();

        // Get the question shortname
        String shortname = q.getShortName();

        // Get the question type 
        String datatype = (a != null) ? a.getType().name() : null;

        // Get the question's form name
        String formname = form.getName();

        // Get the question's associated module name
        String modulename = form.getModule().getDescription();

        // Get the table question's text (if it is a table question)
        String tableQuestionText = null;

        // Get the table question's ID (if it is a table question)
        String tableQuestionId = null;

        // Get the table question's shortname (if it is a table question)
        String tableQuestionSn = null;

        // Get the first question associated with this table question (if it is a table question)
        String tableQuestionFirstColumn = null;

        // Get the identifying column associated with this table question (if it is a table question)
        String tableQuestionIdentifyingColumn = null;

        // Set up various properties associated with this question (if it is a table question)
        if (tableQuestion != null) {
            tableQuestionText = tableQuestion.getTable().getDescription();

            tableQuestionId = tableQuestion.getTable().getUuid();

            tableQuestionSn = tableQuestion.getTable().getTableShortName();

            tableQuestionFirstColumn = tableQuestion.getTable().getFirstQuestion().getUuid();

            TableQuestion identifyingColumn = tableQuestion.getTable().getIdentifyingQuestion();

            tableQuestionIdentifyingColumn = (identifyingColumn == null ? null : identifyingColumn.getUuid());
        }

        // Set up the Map representing the question      
        Map questionMap = new HashMap();

        questionMap.put(UUID_VALUE, uuid);

        questionMap.put(LINK_ID, linkId);

        questionMap.put(TEXT, text);

        questionMap.put(SHORTNAME, shortname);

        questionMap.put(FORM_NAME, formname);

        questionMap.put(MODULE_NAME, modulename);

        questionMap.put(SELECTED, null);

        questionMap.put(LIST, null);

        questionMap.put(UPPERBOUND, null);

        questionMap.put(LOWERBOUND, null);

        questionMap.put(UNIQUE_PER_ENTITY, null);

        questionMap.put(UNIQUE_PER_ALL_MODULES, null);

        questionMap.put(UNIQUE_PER_ENTITY_MODULES, null);

        questionMap.put(TABLE_QUESTION_TEXT, tableQuestionText);

        questionMap.put(TABLE_QUESTION_ID, tableQuestionId);

        questionMap.put(TABLE_QUESTION_SN, tableQuestionSn);

        questionMap.put(TABLE_QUESTION_FIRST_COLUMN, tableQuestionFirstColumn);

        questionMap.put(TABLE_QUESTION_IDENTIFYING_COLUMN, tableQuestionIdentifyingColumn);

        if (datatype != null)
            questionMap.put(DATATYPE, datatype);

        // set up the answer values
        Map<String, List> answerValues = new HashMap<String, List>();

        if (a != null) {
            // set up the array of static answer values, if any
            List staticAvArray = new ArrayList();

            for (AnswerValue av : a.getAnswerValues()) {
                Map o = new HashMap();

                // set the answer value id
                if (av.getId() != null)
                    o.put(ANSWERVALUE_ID, av.getId());

                // set the answer value value
                if (av.getValue() != null)
                    o.put(ANSWERVALUE_VALUE, av.getValue());

                // set the answer value text, if it exists
                if (av.getDescription() != null)
                    o.put(ANSWERVALUE_TEXT, av.getDescription());

                // set the answer value shortname
                if (av.getName() != null)
                    o.put(ANSWERVALUE_SN, av.getName());

                // set whether or not this answer value is selected by the user
                o.put(SELECTED, null);

                // add to the array of static answer values
                staticAvArray.add(o);
            }

            answerValues.put(STATIC, staticAvArray);

            // now, set up the set of dynamic (free text) answer values (contains ONE answer value for now).
            // Originally the answer value's value is null;
            // it will get populated once the user submits the form IF this question permits free text.
            List dynamicAvArray = new ArrayList();

            Map o = new HashMap();

            o.put(ANSWERVALUE_ID, a.getId());

            o.put(SELECTED, null);

            dynamicAvArray.add(o);

            answerValues.put(DYNAMIC, dynamicAvArray);

        }

        questionMap.put(ANSWERVALUES, answerValues);

        return questionMap;
    }

    @SuppressWarnings({ "unchecked" })
    private JSONObject generateRandomAnswerValueForUniqueKey(Map question, Map uniqueKey,
            Map<String, JSONObject> lastUniqueKey, Map<Object, List<Object>> previousAnswers, Object groupId) {
        JSONObject randomAnswerValueObject;

        // Get the question's UUID
        String questionUUID = (String) question.get(UUID_VALUE);

        // If a random answer has already been generated for this question
        // (example, if this is a unique-per-all-modules question that is also
        // a unique-per-entity question, and the unique-per-entity question was processed first),
        // then simply copy the previously generated random answer for this unique key

        if (uniqueKey.containsKey(questionUUID)) {
            randomAnswerValueObject = (JSONObject) uniqueKey.get(questionUUID);

            return randomAnswerValueObject;
        }

        // else:
        else {
            // Get the previously generated unique keys as an ordered list
            LinkedList<Map.Entry> previousUniqueKeys = new LinkedList(((LinkedHashMap) previousAnswers).entrySet());

            // Get the value of the field which identifies the last unique group which was processed
            Map.Entry last = (previousUniqueKeys.isEmpty() ? null : previousUniqueKeys.getLast());

            List list = (last == null ? null : (List) last.getValue());

            Object lastGroupId = list == null ? null : Collections.synchronizedList(list).get(list.size() - 1);

            // If the current groupId matches the lastGroupId,
            // then it means that the same answer value should be used for the current unique key
            // ( since uniqueness is determined by the group )
            if (groupId.equals(lastGroupId)) {
                randomAnswerValueObject = new JSONObject();

                randomAnswerValueObject.putAll(lastUniqueKey.get(questionUUID));

                return randomAnswerValueObject;
            }

            // Otherwise, a new group is being processed, so generate a new random answer value
            else {
                // Determine the last generated random value for this question
                JSONObject lastRandomAnswerValueObject = lastUniqueKey.get(questionUUID);

                Object lastRandomAnswerValue = lastRandomAnswerValueObject == null ? null
                        : getAnswerValue(lastRandomAnswerValueObject);

                return generateRandomAnswerValue(question, lastRandomAnswerValue,
                        (lastRandomAnswerValue == null ? Algorithm.PSEUDORANDOM : Algorithm.EVEN));
            }
        }
    }

    /**
     * This method generates a JSONObject which represents a possible answervalue
     * for the given question.
     * The answer value may be generated one of 2 ways:
     * 1) Static answer values: 
     *       - randomly select one of the static answer value objects
     * 2) Dynamic answer values (free text):
     *        - if the "list" field is not null, then
     *         split the comma-delimited list into tokens and randomly select one of the tokens
     *       - else (if the "list" field is null), then
     *         use the lower/upper bounds to generate a value based on the datatype,
     *         or if no lower/upper bounds exist then simply generate a random value.
     * @param question
     * @return
     */
    @SuppressWarnings("unchecked")
    private JSONObject generateRandomAnswerValue(Map question, Object lastRandomlyGeneratedValue,
            Algorithm algorithm) {
        List dynamicAnsValues = (List) ((Map) question.get(ANSWERVALUES)).get(DYNAMIC);

        Object randomAnswerValue = null;

        JSONObject randomAnswerValueObject = new JSONObject();

        if (GeneratedModuleDataDetail.isFreeTextQuestion(question)) {
            String commaDelimitedList = (String) question.get(LIST);

            String lowerBound = (String) question.get(LOWERBOUND);

            String upperBound = (String) question.get(UPPERBOUND);

            if (StringUtils.isNotBlank(commaDelimitedList)) {
                // This means there is a comma-delimited list of tokens
                String[] tokens = StringUtils.split(commaDelimitedList, ",");

                randomAnswerValue = RandomGeneratorUtils.selectRandomElement(tokens, lastRandomlyGeneratedValue,
                        algorithm);
            }

            else if (StringUtils.isNotBlank(lowerBound) || StringUtils.isNotBlank(upperBound)) {
                // This means the random value should be selected from a range of values
                randomAnswerValue = RandomGeneratorUtils.selectRandomElementFromRange(lowerBound, upperBound,
                        (String) lastRandomlyGeneratedValue, algorithm);
            }

            else {
                //  else, generate a random string
                // TODO: Generate a different random answer value based on the data type of the question
                randomAnswerValue = RandomGeneratorUtils.generateRandomString();
            }

            // Update the JSON object representing the random answer value
            randomAnswerValueObject = new JSONObject();

            randomAnswerValueObject.putAll((Map) dynamicAnsValues.get(0));

            randomAnswerValueObject.put(ANSWERVALUE_VALUE, randomAnswerValue);
        }

        // Otherwise this is NOT a free-text question, 
        // i.e. there is a predefined set of answers for this question.
        else {
            // Generate the list of answer values that were selected for this question
            List allAnswerValues = (List) ((Map) question.get(ANSWERVALUES)).get(STATIC);
            List selectedAnswerValues = new ArrayList();
            Map<String, Object> lastSelectedAnswer = new HashMap<String, Object>();

            for (Object selectedAnswerValue : allAnswerValues) {
                if (GeneratedModuleDataDetail.isSelected((Map) selectedAnswerValue)) {
                    selectedAnswerValues.add(selectedAnswerValue);

                    if (lastSelectedAnswer.isEmpty() && lastRandomlyGeneratedValue != null
                            && StringUtils.equals(lastRandomlyGeneratedValue.toString(),
                                    ((Map) selectedAnswerValue).get(ANSWERVALUE_VALUE).toString())) {
                        lastSelectedAnswer.putAll((Map) selectedAnswerValue);
                    }
                }
            }

            // Randomly pick one of the selected answer values
            randomAnswerValueObject = new JSONObject();

            randomAnswerValueObject.putAll((Map) RandomGeneratorUtils
                    .selectRandomElement(selectedAnswerValues.toArray(), lastSelectedAnswer, algorithm));
        }

        return randomAnswerValueObject;
    }

    /**
     * Updates the GeneratedFormDataDetail object with 
     * randomly generated unique key field values.
     */
    private Map<String, JSONObject> generateUniqueKey(GeneratedModuleDataDetail form,
            Map<String, JSONObject> lastUniqueKey, String entityId, int moduleId, int entityModuleId) {
        log.debug("Generating unique key..............");
        log.debug("==========================");
        // Get the list of "unique-per-entity" fields
        List<Map<String, Object>> uniquePerEntityQuestions = form.retrieveUniquePerEntityQuestions();

        // Get the list of "unique-per-all-modules" fields
        List<Map<String, Object>> uniquePerAllModulesQuestions = form.retrieveUniquePerAllModulesQuestions();

        // Get the list of "unique-per-entity-modules" fields
        List<Map<String, Object>> uniquePerEntityModulesQuestions = form.retrieveUniquePerEntityModulesQuestions();

        // Generate the answer values: first for "unique-per-entity", then "unique-per-entity-modules", then "unique-per-all-modules"
        Map<String, JSONObject> uniqueKey = new HashMap<String, JSONObject>();

        // Get the map of answer-value to "unique-per-entity" question fields
        Map<Object, List<Object>> uniquePerEntityCombinations = form.getUniquePerEntityQuestionCombinations();

        // Get the map of answer-value to "unique-per-all-modules" question fields
        Map<Object, List<Object>> uniquePerAllModulesCombinations = form
                .getUniquePerAllModuleQuestionCombinations();

        // Get the map of answer-value to "unique-per-entity-modules" question fields
        Map<Object, List<Object>> uniquePerEntityModulesCombinations = form
                .getUniquePerEntityModuleQuestionCombinations();

        // Generate the answer values: first for "unique-per-entity", then "unique-per-entity-modules", then "unique-per-all-modules"
        uniqueKey.clear();

        // First, generate the unique answer values for "unique-per-entity" fields
        buildNewKey(uniqueKey, lastUniqueKey, uniquePerEntityQuestions, uniquePerEntityCombinations, entityId);

        // Then, generate the unique answer values for "unique-per-entity-modules" fields
        buildNewKey(uniqueKey, lastUniqueKey, uniquePerEntityModulesQuestions, uniquePerEntityModulesCombinations,
                entityModuleId);

        // Then, generate the unique answer values for "unique-per-all-modules" fields
        buildNewKey(uniqueKey, lastUniqueKey, uniquePerAllModulesQuestions, uniquePerAllModulesCombinations,
                moduleId);

        // Debugging
        log.debug("Generated unique key fields: " + (uniqueKey.isEmpty() ? "NONE" : ""));
        for (Map.Entry<String, JSONObject> entry : uniqueKey.entrySet()) {
            log.debug("==========Key: " + entry.getKey());
            log.debug("==========Text:"
                    + StringUtils.defaultIfEmpty((String) entry.getValue().get(ANSWERVALUE_TEXT), "")
                    + "==========Value:" + entry.getValue().get(ANSWERVALUE_VALUE).toString());
        }

        return uniqueKey;
    }

    /**
     * Generates a random unique key for a given module.
     * Returns whether or not this key is actually a duplicate 
     * within the appropriate scope (per-module, per-entity or per-entitymodule).
     */
    private void buildNewKey(Map<String, JSONObject> uniqueKey, Map<String, JSONObject> lastUniqueKey,
            List<Map<String, Object>> keyQuestions, Map<Object, List<Object>> keyQuestionCombinations,
            Object uniqueGroupId) {
        for (int i = 0; i < keyQuestions.size(); ++i) {
            // Get the unique key question
            Map<String, Object> uniquePerEntityOrModuleQuestion = keyQuestions.get(i);

            // Get the question UUID
            String questionUUID = (String) uniquePerEntityOrModuleQuestion.get(UUID_VALUE);

            // Generate a random answer value for this question
            JSONObject randomAnswerValue = generateRandomAnswerValueForUniqueKey(uniquePerEntityOrModuleQuestion,
                    uniqueKey, lastUniqueKey, keyQuestionCombinations, uniqueGroupId);

            uniqueKey.put(questionUUID, randomAnswerValue);

            // Track the newly generated key in the questionCombinations collection
            if (i == keyQuestions.size() - 1) {
                for (Map.Entry<String, JSONObject> entry : uniqueKey.entrySet()) {

                    String key = GeneratedModuleDataDetail.getTwoPartMapKey(entry.getKey(),
                            entry.getValue().get(ANSWERVALUE_VALUE).toString());

                    List<Object> list = keyQuestionCombinations.get(key);

                    if (list == null) {
                        list = Collections.synchronizedList(new ArrayList<Object>());
                    }

                    if (!list.contains(uniqueGroupId))
                        list.add(uniqueGroupId);

                    keyQuestionCombinations.put(key, list);
                }
            }
        }
    }

    /**
     * Gets a Map of FormNames => FormIDs and ModuleNames => ModuleIDs.
     * This represents the metadata associated with a given module.
     * 
     */
    public Map<String, String> getMetadataForModule(String moduleId, boolean useExistingIds) {
        // The map which will store the metadata for this module
        Map<String, String> metadata = new HashMap<String, String>();

        // The module associated with this "moduleId"
        BaseModule module = moduleDao.getById(new Long(moduleId));

        // Generate a map of name-to-id pairings for each form in this module as applicable
        for (BaseForm form : module.getForms()) {
            if (form instanceof QuestionnaireForm) {
                metadata.put(form.getName(), (useExistingIds ? form.getUuid() : UUID.randomUUID().toString()));
            }
        }

        // Add the moduleName-to-moduleId pairing to the map
        metadata.put(module.getDescription(), (useExistingIds ? moduleId : UUID.randomUUID().toString()));

        return metadata;
    }

    /**
     * Method which retrieves the answer value from a given JSON answer value object.
     * 
     */
    private String getAnswerValue(JSONObject answerValueObject) {
        return answerValueObject.get(ANSWERVALUE_VALUE).toString();
    }

    public void setFormDao(FormDao formDao) {
        this.formDao = formDao;
    }

    public void setCouchDbDao(CouchDBDao couchDbDao) {
        this.couchDbDao = couchDbDao;
    }

    public void setModuleDao(ModuleDao moduleDao) {
        this.moduleDao = moduleDao;
    }

    /**
     * Used to handle the generation of CouchDb-ready form documents 
     * in a multithreaded fashion.
     */
    private class GenerateCouchDbDocumentCommand implements Callable<Object> {
        /**
         * The list of modules to be converted into CouchDb-ready form documents
         */
        private JSONArray modules;

        /**
         * The index of the module that this runnable will be converting (in the list)
         */
        private int moduleIndex;

        /**
         * The number of forms associated with a module
         */
        private int numberOfFormsPerModule;

        /**
         * The metadata associated with a module
         */
        private Map<String, String> moduleMetadata;

        /**
         * The array of JSONObjects to which this runnable will be adding
         * newly generated CouchDb-ready documents
         */
        private JSONObject[] documentArray;

        private GenerateCouchDbDocumentCommand(JSONArray modules, int index, int numberOfFormsPerModule,
                JSONObject[] documentArray, Map<String, String> moduleMetadata) {
            this.modules = modules;

            this.moduleIndex = index;

            this.numberOfFormsPerModule = numberOfFormsPerModule;

            this.documentArray = documentArray;

            this.moduleMetadata = moduleMetadata;
        }

        @Override
        public Object call() {
            // Get the module that this thread will be working on
            JSONObject module = (JSONObject) modules.get(moduleIndex);

            // Get the questions associated with this module
            JSONObject questions = module.getJSONObject(QUESTIONS);

            // we can set up a Map to keep track of the generated documents
            Map<String, JSONObject> generatedDocuments = new HashMap<String, JSONObject>();

            // Iterate through all the questions, re-associating each question with 
            // a brand new document which will be saved to CouchDb   
            for (Object question : questions.values()) {
                // The current question
                JSONObject jsonQuestion = (JSONObject) question;

                // The current question ID
                String jsonQuestionId = jsonQuestion.getString(QUESTION_ID);

                // The name of the form associated with the current question
                // (since this attribute was only added as a temporary store,
                // remove it from the CouchDb-ready version of the question)
                String formName = (String) jsonQuestion.remove(FORM_NAME);

                // The name of the module associated with the current question
                // (since this attribute was only added as a temporary store,
                // remove it from the CouchDb-ready version of the question)
                String moduleName = (String) jsonQuestion.remove(MODULE_NAME);

                // The link ID associated with this question
                // (since this attribute was only added as a temporary store,
                // remove it from the CouchDb-ready version of the question)
                String linkId = (String) jsonQuestion.remove(LINK_ID);

                // If the linkId is not blank, then
                // the questionId should be reset to be equal to the link
                if (StringUtils.isNotBlank(linkId)) {
                    jsonQuestionId = linkId;
                    jsonQuestion.put(QUESTION_ID, jsonQuestionId);
                }

                // Create the new CouchDB document.
                // It will clone most of the properties of the original module document, 
                // while maintaining form-specific properties as well
                JSONObject couchDbDoc = generatedDocuments.get(formName);

                if (couchDbDoc == null) {
                    couchDbDoc = new JSONObject();

                    // set the module name
                    couchDbDoc.put(MODULE_NAME, moduleName);

                    // set the module ID
                    couchDbDoc.put(MODULE_ID, moduleMetadata.get(moduleName));

                    // set the form name
                    couchDbDoc.put(FORM_NAME, formName);

                    // set the form ID
                    couchDbDoc.put(FORM_ID, moduleMetadata.get(formName));

                    // set the entity ID
                    couchDbDoc.put(ENTITY_ID, module.get(ENTITY_ID));

                    // set the updated Date
                    couchDbDoc.put(UPDATED_DATE, module.get(UPDATED_DATE));

                    // set the questions
                    couchDbDoc.put(QUESTIONS, new JSONObject());

                    // set the module ID
                    String moduleId = (String) moduleMetadata.get(moduleName);

                    if (StringUtils.isNotEmpty(moduleId)) {
                        couchDbDoc.put(MODULE_ID, moduleId);
                    }

                    // set the form ID
                    String formId = (String) moduleMetadata.get(formName);

                    if (StringUtils.isNotEmpty(formId)) {
                        couchDbDoc.put(FORM_ID, formId);
                    }
                }

                // Add this question to the document
                couchDbDoc.getJSONObject(QUESTIONS).put(jsonQuestionId, question);

                // Add this document to the generatedDocuments map
                generatedDocuments.put(formName, couchDbDoc);
            }

            // Add the newly generated documents to the documentArray

            int startIndex = moduleIndex * numberOfFormsPerModule; // start of the loop to add documents

            int endIndex = startIndex + generatedDocuments.size(); // end of the loop to add documents

            Iterator<JSONObject> generatedDocumentIterator = generatedDocuments.values().iterator();

            for (int i = startIndex; i < endIndex; ++i) {
                if (generatedDocumentIterator.hasNext()) {
                    documentArray[i] = generatedDocumentIterator.next();

                    // debugging (just views the first 5 documents)
                    if (i < 5)
                        log.debug("Document added at index " + i + ": " + documentArray[i]);
                }
            }

            // Clear unused collections
            generatedDocuments.clear();

            return null;
        }
    }
}