content: /*
   *  Copyright (C) 2010-2019 Stichting Akvo (Akvo Foundation)
   *
   *  This file is part of Akvo FLOW.
   *
   *  Akvo FLOW is free software: you can redistribute it and modify it under the terms of
   *  the GNU Affero General Public License (AGPL) as published by the Free Software Foundation,
   *  either version 3 of the License or any later version.
   *
   *  Akvo FLOW 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 Affero General Public License included below for more details.
   *
   *  The full license text can also be seen at <http://www.gnu.org/licenses/agpl.html>.
   */
  
  package com.gallatinsystems.survey.dao;
  
  import static com.gallatinsystems.common.util.MemCacheUtils.containsKey;
  import static com.gallatinsystems.common.util.MemCacheUtils.initCache;
  import static com.gallatinsystems.common.util.MemCacheUtils.putObjects;
  
  import java.lang.reflect.Field;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.SortedMap;
  import java.util.TreeMap;
  import java.util.logging.Level;
  
  import javax.jdo.PersistenceManager;
  import javax.jdo.annotations.NotPersistent;
  
  import net.sf.jsr107cache.Cache;
  import net.sf.jsr107cache.CacheException;
  
  import org.waterforpeople.mapping.dao.QuestionAnswerStoreDao;
  
  import com.gallatinsystems.framework.dao.BaseDAO;
  import com.gallatinsystems.framework.exceptions.IllegalDeletionException;
  import com.gallatinsystems.framework.servlet.PersistenceFilter;
  import com.gallatinsystems.survey.domain.CascadeResource;
  import com.gallatinsystems.survey.domain.Question;
  import com.gallatinsystems.survey.domain.QuestionGroup;
  import com.gallatinsystems.survey.domain.QuestionHelpMedia;
  import com.gallatinsystems.survey.domain.QuestionOption;
  import com.gallatinsystems.survey.domain.Translation;
  import com.gallatinsystems.survey.domain.Translation.ParentType;
  import com.google.appengine.api.datastore.DatastoreService;
  import com.google.appengine.api.datastore.DatastoreServiceFactory;
  import com.google.appengine.api.datastore.Entity;
  import com.google.appengine.api.datastore.Key;
  import com.google.appengine.api.datastore.Transaction;
  
  /**
   * saves/finds question objects
   */
  public class QuestionDao extends BaseDAO<Question> {
  
      private QuestionOptionDao optionDao;
      private QuestionHelpMediaDao helpDao;
      private TranslationDao translationDao;
      private ScoringRuleDao scoringRuleDao;
      private Cache cache;
  
      public QuestionDao() {
          super(Question.class);
          optionDao = new QuestionOptionDao();
          helpDao = new QuestionHelpMediaDao();
          translationDao = new TranslationDao();
          scoringRuleDao = new ScoringRuleDao();
          cache = initCache(4 * 60 * 60); // cache questions list for 4 hours
      }
  
  
      /**
       * loads the Question object but NOT any associated options
       *
       * @param id
       * @return
       */
      public Question getQuestionHeader(Long id) {
          return getByKey(id);
  
      }
  
      /**
       * lists minimal question information by surveyId
       *
       * @param surveyId
       * @return
       */
      public List<Question> listQuestionsBySurvey(Long surveyId) {
          List<Question> questionsList = listByProperty("surveyId", surveyId, "Long", "order", "asc");
  
          if (questionsList == null) {
              return Collections.emptyList();
          }
  
          cache(questionsList);
  
          return questionsList;
      }
  
      /**
       * Delete a list of questions
       *
       * @param qList
       */
      public void delete(List<Question> qList) {
          uncache(qList);
          super.delete(qList);
      }
  
      /**
       * Delete question from data store.
       *
       * @param question
       */
      public void delete(Question question) throws IllegalDeletionException {
          delete(question, Boolean.FALSE);
      }
  
      /**
       * Delete a question and adjust the question order for the remaining questions if specified
       *
       * @param question
       * @param adjustQuestionOrder
       * @throws IllegalDeletionException
       */
      public void delete(Question question, Boolean adjustQuestionOrder)
              throws IllegalDeletionException {
          QuestionAnswerStoreDao qasDao = new QuestionAnswerStoreDao();
          if (qasDao.listByQuestion(question.getKey().getId()).size() > 0) {
              throw new IllegalDeletionException(
                      "Cannot delete question with id "
                              + question.getKey().getId()
                              + " ("
                              + question.getText()
                              + ") because there are already survey responses stored for this question."
                              + " Please delete all survey responses first.");
          }
  
          helpDao.deleteHelpMediaForQuestion(question.getKey().getId());
  
          optionDao.deleteOptionsForQuestion(question.getKey().getId());
  
          translationDao.deleteTranslationsForParent(question.getKey().getId(),
                  Translation.ParentType.QUESTION_TEXT);
  
          // to use later when adjust question order
          Long deletedQuestionGroupId = question.getQuestionGroupId();
          Integer deletedQuestionOrder = question.getOrder();
  
          uncache(Arrays.asList(question)); // clear from cached first
  
          // only delete after extracting group ID and order
          super.delete(question);
  
          if (adjustQuestionOrder != null && adjustQuestionOrder) {
              // update question order
              TreeMap<Integer, Question> groupQs = listQuestionsByQuestionGroup(
                      deletedQuestionGroupId, false);
              if (groupQs != null) {
                  for (Question gq : groupQs.values()) {
                      if (gq.getOrder() >= deletedQuestionOrder) {
                          gq.setOrder(gq.getOrder() - 1);
                      }
                  }
              }
          }
      }
  
      /**
       * Delete all the questions in a group
       *
       * @param questionGroupId
       * @throws IllegalDeletionException
       */
      public void deleteQuestionsForGroup(Long questionGroupId) throws IllegalDeletionException {
          for (Question q : listQuestionsByQuestionGroup(questionGroupId, Boolean.TRUE).values()) {
              delete(q, Boolean.FALSE);
          }
      }
  
      /**
       * lists all questions in a group and orders them by their sortOrder
       *
       * @param groupId
       * @return
       */
      public List<Question> listQuestionsInOrderForGroup(Long groupId) {
          return listByProperty("questionGroupId", groupId, "Long", "order", "asc");
      }
  
  
      /**
       * list questions in order, doing our own sorting to avoid many datastore calls. Optionally filtered by type
       *
       * @param surveyId
       * @return
       */
      public List<Question> listQuestionsInOrder(Long surveyId, Question.Type type) {
          List<Question> orderedQuestionList = new ArrayList<Question>();
          //We must know the order of the question groups
          QuestionGroupDao qgDao = new QuestionGroupDao();
          List<QuestionGroup> orderedGroupList = qgDao.listQuestionGroupBySurvey(surveyId);
  
          Map<Long, List<Question>> idMap = new HashMap<>();
          for (QuestionGroup group : orderedGroupList) {
              List<Question> questions = new ArrayList<>();
              idMap.put(group.getKey().getId(), questions);
          }
          List<Question> unorderedQuestions;
          if (type == null) {
              unorderedQuestions = listByProperty("surveyId", surveyId, "Long");
          } else {
              unorderedQuestions = getBySurveyAndType(surveyId, type);
          }
  
          // Sort them into their respective lists
          for (Question q:unorderedQuestions) {
              List<Question> myList = idMap.get(q.getQuestionGroupId());
              if (myList != null) { //in case db is inconsistent
                  myList.add(q);
              }
          }
          // Lists complete, now we can sort them and copy each in order
          for (QuestionGroup group : orderedGroupList) {
              List<Question> questions = idMap.get(group.getKey().getId());
              Collections.sort(questions, new Comparator<Question>() {
                  @Override
                  public int compare(Question o1, Question o2) {
                      //order should never be null, but accidents happen...
                      int v1 = o1.getOrder() != null ? o1.getOrder() : 0;
                      int v2 = o2.getOrder() != null ? o2.getOrder() : 0;
                      return v1 - v2;
                  }
              });
              orderedQuestionList.addAll(questions);
          }
  
          return orderedQuestionList;
      }
  
      /**
       * Lists questions by surveyId and type
       *
       * @param surveyId
       * @param type
       * @return
       */
      @SuppressWarnings("unchecked")
      private List<Question> getBySurveyAndType(long surveyId, Question.Type type) {
          PersistenceManager pm = PersistenceFilter.getManager();
          javax.jdo.Query query = pm.newQuery(Question.class);
          query.setFilter(" surveyId == surveyIdParam && type == questionTypeParam");
          query.declareParameters("Long surveyIdParam, String questionTypeParam");
          List<Question> results = (List<Question>) query.execute(surveyId, type.toString());
          if (results != null && results.size() > 0) {
              return results;
          } else {
              return Collections.emptyList();
          }
      }
  
      /**
       * saves a question object in a transaction
       *
       * @param q
       * @return
       */
      public Question saveTransactional(Question q) {
          DatastoreService datastore = DatastoreServiceFactory
                  .getDatastoreService();
          Transaction txn = datastore.beginTransaction();
          Entity question = null;
          try {
              if (q.getKey() != null) {
                  try {
                      question = datastore.get(q.getKey());
                  } catch (Exception e) {
                      log.log(Level.WARNING, "Key is set but not found. Assuming this is an import");
                      question = new Entity(q.getKey());
                  }
              } else {
                  question = new Entity("Question");
              }
  
              Field[] f = Question.class.getDeclaredFields();
              for (int i = 0; i < f.length; i++) {
                  if (!"key".equals(f[i].getName())
                          && f[i].getAnnotation(NotPersistent.class) == null
                          && !"type".equals(f[i].getName())
                          && !f[i].getName().startsWith("jdo")
                          && !f[i].getName().equals("serialVersionUID")) {
                      f[i].setAccessible(true);
                      question.setProperty(f[i].getName(), f[i].get(q));
                  }
              }
              // now set the type
              question.setProperty("type", q.getType().toString());
              // Ensure that createdDateTime and lastUpdateDateTime properties are set
              Date date = new Date();
              if (question.getProperty("createdDateTime") == null) {
                  question.setProperty("createdDateTime", date);
              }
              if (question.getProperty("lastUpdateDateTime") == null) {
                  question.setProperty("lastUpdateDateTime", date);
              }
          } catch (Exception e) {
              log.log(Level.SEVERE, "Could not set entity fields", e);
          }
  
          Key key = datastore.put(question);
          q.setKey(key);
          cache(Arrays.asList(q));
          txn.commit();
  
          return q;
      }
  
      /**
       * saves a question, including its question options, translations, and help media (if any).
       *
       * @param question
       * @param questionGroupId
       * @return
       */
      public Question save(Question question, Long questionGroupId) {
          Long surveyId = null;
          if (questionGroupId != null) {
              question.setQuestionGroupId(questionGroupId);
              QuestionGroup group = getByKey(questionGroupId, QuestionGroup.class);
              if (group != null) {
                  surveyId = group.getSurveyId();
                  question.setSurveyId(surveyId);
              }
          }
          question = saveTransactional(question);
          // delete existing options
  
          QuestionOptionDao qoDao = new QuestionOptionDao();
          TreeMap<Integer, QuestionOption> qoMap = qoDao
                  .listOptionByQuestion(question.getKey().getId());
          if (qoMap != null) {
              for (Map.Entry<Integer, QuestionOption> entry : qoMap.entrySet()) {
                  qoDao.delete(entry.getValue());
              }
          }
          if (question.getQuestionOptionMap() != null) {
              for (QuestionOption opt : question.getQuestionOptionMap().values()) {
                  opt.setQuestionId(question.getKey().getId());
                  if (opt.getText() != null && opt.getText().contains(",")) {
                      opt.setText(opt.getText().replaceAll(",", "-"));
                      if (opt.getCode() != null) {
                          opt.setCode(opt.getCode().replaceAll(",", "-"));
                      }
                  }
                  save(opt);
                  HashMap<String, Translation> optionsTranslationMap = opt.getTranslationMap();
                  if (optionsTranslationMap != null) {
                      for (Translation t : optionsTranslationMap.values()) {
                          t.setParentId(opt.getKey().getId());
                          t.setSurveyId(surveyId);
                          t.setQuestionGroupId(questionGroupId);
                      }
                      super.save(optionsTranslationMap.values());
                  }
              }
          }
          List<Translation> translations = question.getTranslations();
          if (translations != null) {
              for (Translation t : translations) {
                  t.setParentId(question.getKey().getId());
                  t.setSurveyId(surveyId);
                  t.setQuestionGroupId(questionGroupId);
              }
              super.save(translations);
          }
  
          if (question.getQuestionHelpMediaMap() != null) {
              for (QuestionHelpMedia help : question.getQuestionHelpMediaMap().values()) {
                  //help is only used to transmit question tip translations
                  if (help.getTranslationMap() != null) {
                      for (Translation t : help.getTranslationMap().values()) {
                          t.setParentId(question.getKey().getId());
                          t.setSurveyId(surveyId);
                          t.setQuestionGroupId(questionGroupId);
                      }
                      super.save(help.getTranslationMap().values());
                  }
              }
          }
          return question;
      }
  
      /**
       * Saves question and update cache
       *
       * @param question
       */
      public Question save(Question question) {
          // first save and get Id
          Question savedQuestion = super.save(question);
          cache(Arrays.asList(savedQuestion));
          return savedQuestion;
      }
  
      /**
       * Save a collection of questions and cache
       *
       * @param qList
       * @return
       */
      public List<Question> save(List<Question> qList) {
          List<Question> savedQuestions = (List<Question>) super.save(qList);
          cache(savedQuestions);
          return savedQuestions;
      }
  
      /**
       * Add a collection of Question objects to the cache. If the object already exists in the cached
       * questions list, they are replaced by the ones passed in through this list
       *
       * @param qList
       */
      private void cache(List<Question> qList) {
          if (qList == null || qList.isEmpty()) {
              return;
          }
  
          Map<Object, Object> cacheMap = new HashMap<Object, Object>();
          for (Question qn : qList) {
              if (qn == null) {
                  continue;
              }
              String cacheKey = null;
              try {
                  cacheKey = getCacheKey(qn);
                  cacheMap.put(cacheKey, qn);
              } catch (CacheException e) {
                  log.log(Level.WARNING, e.getMessage());
              }
          }
  
          putObjects(cache, cacheMap);
      }
  
      /**
       * Remove a collection of questions from the cache
       *
       * @param qList
       */
      private void uncache(List<Question> qList) {
          if (qList == null || qList.isEmpty()) {
              return;
          }
  
          for (Question qn : qList) {
              if (qn == null) {
                  continue;
              }
              String cacheKey;
              try {
                  cacheKey = getCacheKey(qn);
                  if (containsKey(cache, cacheKey)) {
                      cache.remove(cacheKey);
                  }
              } catch (CacheException e) {
                  log.log(Level.WARNING, e.getMessage());
              }
          }
      }
  
      /**
       * finds a question by its reference id
       *
       * @param refid
       * @return
       * @deprecated
       */
      @Deprecated
      public Question findByReferenceId(String refid) {
          Question q = findByProperty("referenceIndex", refid, "String");
          return q;
      }
  
      /**
       * finds a question by its id. If needDetails is true, all child objects (options, help,
       * translations) will also be loaded.
       *
       * @param id
       * @param needDetails
       * @return
       */
      public Question getByKey(Long id, boolean needDetails) {
          Question q = getByKey(id);
          if (needDetails) {
              q.setQuestionHelpMediaMap(helpDao.listHelpByQuestion(q.getKey().getId()));
              if (Question.Type.OPTION == q.getType()) {
                  q.setQuestionOptionMap(optionDao.listOptionByQuestion(q.getKey().getId()));
              }
              q.setTranslations(translationDao.findTranslations(q.getKey().getId(), ParentType.QUESTION_TEXT, ParentType.QUESTION_TIP));
              // only load scoring rules for types that support scoring
              if (Question.Type.OPTION == q.getType()
                      || Question.Type.FREE_TEXT == q.getType()
                      || Question.Type.NUMBER == q.getType()) {
                  q.setScoringRules(scoringRuleDao.listRulesByQuestion(q.getKey().getId()));
              }
          }
          return q;
      }
  
      /**
       * finds the base question (no child objects) by id
       */
      @Override
      public Question getByKey(Key key) {
          return super.getByKey(key);
      }
  
      /**
       * Find a question based on the id in string form
       */
      @Override
      public Question getByKey(Long questionId) {
          Question question = null;
          String cacheKey = null;
  
          // retrieve from cache
          try {
              cacheKey = getCacheKey(questionId.toString());
              if (containsKey(cache, cacheKey)) {
                  return (Question) cache.get(cacheKey);
              }
          } catch (CacheException e) {
              log.log(Level.WARNING, e.getMessage());
          }
  
          // else from datastore and attempt to cache
          question = super.getByKey(questionId);
          cache(Arrays.asList(question));
  
          return question;
      }
  
      /**
       * lists questions within a group ordered by creation date
       *
       * @param questionGroupId
       * @return
       */
      public List<Question> listQuestionsByQuestionGroupOrderByCreatedDateTime(Long questionGroupId) {
          return listByProperty("questionGroupId", questionGroupId, "Long", "createdDateTime", "asc");
      }
  
      /**
       * lists questions within a group. If needDetails flag is true, the child objects will be loaded
       * for each question. Due to processing constraints on GAE, needDetails should only be true when
       * calling this method from a task.
       *
       * @param questionGroupId
       * @param needDetails
       * @return
       */
      public TreeMap<Integer, Question> listQuestionsByQuestionGroup(
              Long questionGroupId, boolean needDetails) {
          return listQuestionsByQuestionGroup(questionGroupId, needDetails, true);
      }
  
  
      /**
       * lists all the questions in a form, optionally loading details.
       * Could be unrolled further for datastore performance gains.
       *
       * @param formId
       * @param needDetails
       * @return
       */
      public List<Question> listQuestionsByForm(Long formId, boolean needDetails) {
          List<Question> qList = listByProperty("surveyId", formId, "Long", "order", "asc");
          TreeMap<Integer, Question> map = new TreeMap<Integer, Question>();
          if (qList != null) {
              for (Question q : qList) {
  
                  if (needDetails) {
                      if (Question.Type.OPTION == q.getType()) {
                          q.setQuestionOptionMap(optionDao.listOptionByQuestion(q.getKey().getId()));
                      }
                      q.setTranslations(translationDao.findTranslations(
                              q.getKey().getId(), ParentType.QUESTION_TEXT, ParentType.QUESTION_TIP));
                      //Cascade level names
                      if (q.getType().equals(Question.Type.CASCADE) && q.getCascadeResourceId() != null) {
                          CascadeResource cr = new CascadeResourceDao().getByKey(q.getCascadeResourceId());
                          if (cr != null) {
                              q.setCascadeResource(cr.getResourceId());
                              q.setLevelNames(cr.getLevelNames());
                          }
                      }
                  }
  
                  if (q.getOrder() == null) {
                      q.setOrder(qList.size() + 1); //Anything, temporarily, to make a useable list
                  }
                  map.put(q.getOrder(), q);
              }
          }
          return qList;
      }
  
      /**
       * lists all the questions in a group, optionally loading details. If allowSideEffects is true,
       * it will attempt to reorder any duplicated question orderings on retrieval. New users of this
       * method should ALWAYS call this with allowSideEffects = false
       *
       * @param questionGroupId
       * @param needDetails
       * @param allowSideEffects
       * @return
       */
      public TreeMap<Integer, Question> listQuestionsByQuestionGroup(
              Long questionGroupId, boolean needDetails, boolean allowSideEffects) {
          List<Question> qList =
                  listByProperty("questionGroupId", questionGroupId, "Long", "order", "asc");
          TreeMap<Integer, Question> map = new TreeMap<Integer, Question>();
          if (qList != null) {
              for (Question q : qList) {
  
                  if (needDetails) {
                      q.setQuestionHelpMediaMap(helpDao.listHelpByQuestion(q.getKey().getId()));
                      if (Question.Type.OPTION == q.getType()
                              || Question.Type.STRENGTH == q.getType()) {
                          q.setQuestionOptionMap(optionDao.listOptionByQuestion(q.getKey().getId()));
                      }
                      q.setTranslations(translationDao.findTranslations(q.getKey().getId(),  ParentType.QUESTION_TEXT));
                      //Cascade level names
                      if (q.getType().equals(Question.Type.CASCADE) && q.getCascadeResourceId() != null) {
                          CascadeResource cr = new CascadeResourceDao().getByKey(q.getCascadeResourceId());
                          if (cr != null) {
                              q.setLevelNames(cr.getLevelNames());
                          }
                      }
                  }
  
                  if (q.getOrder() == null) {
                      q.setOrder(qList.size() + 1); //Anything, temporarily, to make a useable list
                  } else if (allowSideEffects) {
                      if (q.getOrder() != map.size() + 1) { //Not next in sequence
                          log.log(Level.WARNING, "Reordering question in group " + questionGroupId
                                  + " from " + q.getOrder() + " to " + (map.size() + 1));
                          q.setOrder(map.size() + 1);
                          super.save(q);
                      } else if (map.size() == 0) {
                          super.save(q);
                      }
                  }
                  map.put(q.getOrder(), q);
              }
          }
          return map;
      }
  
      /**
       * finds q question by its path and order. Path is defined as the name of the
       * "surveyGroupName/surveyName/QuestionGroupName"
       *
       * @param order
       * @param path
       * @return
       */
      @SuppressWarnings("unchecked")
      public Question getByPath(Integer order, String path) {
          PersistenceManager pm = PersistenceFilter.getManager();
          javax.jdo.Query query = pm.newQuery(Question.class);
          query.setFilter(" path == pathParam && order == orderParam");
          query.declareParameters("String pathParam, String orderParam");
          List<Question> results = (List<Question>) query.execute(path, order);
          if (results != null && results.size() > 0) {
              return results.get(0);
          } else {
              return null;
          }
      }
  
      /**
       * finds a question within a group by matching on the questionText passed in
       *
       * @param questionGroupId
       * @param questionText
       * @return
       */
      @SuppressWarnings("unchecked")
      public Question getByQuestionGroupId(Long questionGroupId,
              String questionText) {
          PersistenceManager pm = PersistenceFilter.getManager();
          javax.jdo.Query query = pm.newQuery(Question.class);
          query.setFilter(" questionGroupId == questionGroupIdParam && text == questionTextParam");
          query.declareParameters("Long questionGroupIdParam, String questionTextParam");
          List<Question> results = (List<Question>) query.execute(questionGroupId, questionText);
          if (results != null && results.size() > 0) {
              return results.get(0);
          } else {
              return null;
          }
      }
  
      /**
       * finds a question by groupId and order. If there are questions with duplicated orders, the
       * first is returned.
       *
       * @param questionGroupId
       * @param order
       * @return
       */
      @SuppressWarnings("unchecked")
      public Question getByGroupIdAndOrder(Long questionGroupId, Integer order) {
          PersistenceManager pm = PersistenceFilter.getManager();
          javax.jdo.Query query = pm.newQuery(Question.class);
          query.setFilter(" questionGroupId == questionGroupIdParam && order == orderParam");
          query.declareParameters("Long questionGroupIdParam, Integer orderParam");
          List<Question> results = (List<Question>) query.execute(questionGroupId, order);
          if (results != null && results.size() > 0) {
              return results.get(0);
          } else {
              return null;
          }
      }
  
      /**
       * updates ONLY the order field within the question object for the questions passed in. All
       * questions must exist in the datastore
       *
       * @param questionList
       */
      public void updateQuestionOrder(List<Question> questionList) {
          if (questionList != null) {
              for (Question q : questionList) {
                  Question persistentQuestion = getByKey(q.getKey());
                  persistentQuestion.setOrder(q.getOrder());
                  // since the object is still attached, we don't need to call
                  // save. It will be saved on flush of the Persistent session
              }
          }
      }
  
      /**
       * lists all questions that depend on the id passed in
       *
       * @param questionId
       * @return
       */
      public List<Question> listQuestionsByDependency(Long questionId) {
          return listByProperty("dependentQuestionId", questionId, "Long");
      }
  
      /**
       * Returns a list of questions whose responses will be shown as the display name of a data
       * point. The returned list of questions is ordered by question group order and then by question
       * order within a group
       *
       * @param surveyId
       * @return
       */
      @SuppressWarnings("unchecked")
      public List<Question> listDisplayNameQuestionsBySurveyId(Long surveyId) {
          QuestionGroupDao qgDao = new QuestionGroupDao();
          PersistenceManager pm = PersistenceFilter.getManager();
          javax.jdo.Query query = pm.newQuery(Question.class);
          query.setFilter("surveyId == surveyIdParam && localeNameFlag == true");
          query.declareParameters("Long surveyIdParam");
          query.setOrdering("order asc");
          List<Question> results = (List<Question>) query.execute(
                  surveyId);
          if (results != null && results.size() > 0) {
              SortedMap<Integer, Question> orderedQuestionMap = new TreeMap<Integer, Question>();
              for (Question question : results) {
                  int orderIndex = 0;
                  QuestionGroup qg = qgDao.getByKey(question.getQuestionGroupId());
                  if (qg != null) {
                      orderIndex = (qg.getOrder() != null ? qg.getOrder() * 1000 : 0);
                      orderIndex += (question.getOrder() != null ? question.getOrder() : 0);
                  }
                  orderedQuestionMap.put(orderIndex, question);
              }
              return new ArrayList<Question>(orderedQuestionMap.values());
          } else {
              return Collections.emptyList();
          }
      }
  
      public List<Question> listByCascadeResourceId(Long cascadeResourceId) {
          return listByProperty("cascadeResourceId", cascadeResourceId, "Long");
      }
  
      /**
       * Returns a list of questions whose sourceQuestionId parameter is included in the list of ids
       * passed in as parameter
       *
       * @param sourceQuestionIds
       * @return
       */
      @SuppressWarnings("unchecked")
      public List<Question> listBySourceQuestionId(List<Long> sourceQuestionIds) {
          PersistenceManager pm = PersistenceFilter.getManager();
          javax.jdo.Query query = pm.newQuery(Question.class, ":p1.contains(sourceQuestionId)");
          List<Question> results = (List<Question>) query.execute(sourceQuestionIds);
          if (results == null) {
              return Collections.emptyList();
          } else {
              return results;
          }
      }
  }
  