@Override
  public List<String> validateResponseAttributes(
      List<FeedbackResponseAttributes> responses, int numRecipients) {
    if (responses.isEmpty()) {
      return new ArrayList<String>();
    }

    if (areDuplicatesAllowed) {
      return new ArrayList<String>();
    } else {
      List<String> errors = new ArrayList<>();

      for (FeedbackResponseAttributes response : responses) {
        FeedbackRankOptionsResponseDetails frd =
            (FeedbackRankOptionsResponseDetails) response.getResponseDetails();
        Set<Integer> responseRank = new HashSet<>();

        for (int answer : frd.getFilteredSortedAnswerList()) {
          if (responseRank.contains(answer)) {
            errors.add("Duplicate rank " + answer);
          }
          responseRank.add(answer);
        }
      }

      return errors;
    }
  }
  /**
   * From the feedback responses, generate a mapping of the option to a list of ranks received for
   * that option. The key of the map returned is the option name. The values of the map are list of
   * ranks received by the key.
   *
   * @param responses a list of responses
   */
  private Map<String, List<Integer>> generateOptionRanksMapping(
      List<FeedbackResponseAttributes> responses) {
    Map<String, List<Integer>> optionRanks = new HashMap<>();
    for (FeedbackResponseAttributes response : responses) {
      FeedbackRankOptionsResponseDetails frd =
          (FeedbackRankOptionsResponseDetails) response.getResponseDetails();

      List<Integer> answers = frd.getAnswerList();
      Map<String, Integer> mapOfOptionToRank = new HashMap<>();

      Assumption.assertEquals(answers.size(), options.size());

      for (int i = 0; i < options.size(); i++) {
        int rankReceived = answers.get(i);
        mapOfOptionToRank.put(options.get(i), rankReceived);
      }

      Map<String, Integer> normalisedRankForOption =
          obtainMappingToNormalisedRanksForRanking(mapOfOptionToRank, options);

      for (int i = 0; i < options.size(); i++) {
        String optionReceivingRanks = options.get(i);
        int rankReceived = normalisedRankForOption.get(optionReceivingRanks);

        if (rankReceived != Const.POINTS_NOT_SUBMITTED) {
          updateOptionRanksMapping(optionRanks, optionReceivingRanks, rankReceived);
        }
      }
    }
    return optionRanks;
  }
  /**
   * Produce a FeedbackResponseCommentSearchResultBundle from the Results<ScoredDocument>
   * collection. The list of InstructorAttributes is used to filter out the search result.
   */
  public FeedbackResponseCommentSearchResultBundle fromResults(
      Results<ScoredDocument> results, List<InstructorAttributes> instructors) {
    if (results == null) {
      return this;
    }

    // get instructor's information
    instructorEmails = new HashSet<String>();
    instructorCourseIdList = new HashSet<String>();
    for (InstructorAttributes ins : instructors) {
      instructorEmails.add(ins.email);
      instructorCourseIdList.add(ins.courseId);
    }

    cursor = results.getCursor();
    List<ScoredDocument> filteredResults = filterOutCourseId(results, instructors);
    for (ScoredDocument doc : filteredResults) {
      // get FeedbackResponseComment from results
      FeedbackResponseCommentAttributes comment =
          JsonUtils.fromJson(
              doc.getOnlyField(Const.SearchDocumentField.FEEDBACK_RESPONSE_COMMENT_ATTRIBUTE)
                  .getText(),
              FeedbackResponseCommentAttributes.class);
      if (frcLogic.getFeedbackResponseComment(comment.getId()) == null) {
        frcLogic.deleteDocument(comment);
        continue;
      }
      comment.sendingState = CommentSendingState.SENT;
      List<FeedbackResponseCommentAttributes> commentList =
          comments.get(comment.feedbackResponseId);
      if (commentList == null) {
        commentList = new ArrayList<FeedbackResponseCommentAttributes>();
        comments.put(comment.feedbackResponseId, commentList);
      }
      commentList.add(comment);

      // get related response from results
      FeedbackResponseAttributes response =
          JsonUtils.fromJson(
              doc.getOnlyField(Const.SearchDocumentField.FEEDBACK_RESPONSE_ATTRIBUTE).getText(),
              FeedbackResponseAttributes.class);
      if (frLogic.getFeedbackResponse(response.getId()) == null) {
        frcLogic.deleteDocument(comment);
        continue;
      }
      List<FeedbackResponseAttributes> responseList = responses.get(response.feedbackQuestionId);
      if (responseList == null) {
        responseList = new ArrayList<FeedbackResponseAttributes>();
        responses.put(response.feedbackQuestionId, responseList);
      }
      if (!isAdded.contains(response.getId())) {
        isAdded.add(response.getId());
        responseList.add(response);
      }

      // get related question from results
      FeedbackQuestionAttributes question =
          JsonUtils.fromJson(
              doc.getOnlyField(Const.SearchDocumentField.FEEDBACK_QUESTION_ATTRIBUTE).getText(),
              FeedbackQuestionAttributes.class);
      if (fqLogic.getFeedbackQuestion(question.getId()) == null) {
        frcLogic.deleteDocument(comment);
        continue;
      }
      List<FeedbackQuestionAttributes> questionList = questions.get(question.feedbackSessionName);
      if (questionList == null) {
        questionList = new ArrayList<FeedbackQuestionAttributes>();
        questions.put(question.feedbackSessionName, questionList);
      }
      if (!isAdded.contains(question.getId())) {
        isAdded.add(question.getId());
        questionList.add(question);
      }

      // get related session from results
      FeedbackSessionAttributes session =
          JsonUtils.fromJson(
              doc.getOnlyField(Const.SearchDocumentField.FEEDBACK_SESSION_ATTRIBUTE).getText(),
              FeedbackSessionAttributes.class);
      if (fsLogic.getFeedbackSession(session.getSessionName(), session.getCourseId()) == null) {
        frcLogic.deleteDocument(comment);
        continue;
      }
      if (!isAdded.contains(session.getFeedbackSessionName())) {
        isAdded.add(session.getFeedbackSessionName());
        sessions.put(session.getSessionName(), session);
      }

      // get giver and recipient names
      String responseGiverName =
          extractContentFromQuotedString(
              doc.getOnlyField(Const.SearchDocumentField.FEEDBACK_RESPONSE_GIVER_NAME).getText());
      responseGiverTable.put(response.getId(), getFilteredGiverName(response, responseGiverName));

      String responseRecipientName =
          extractContentFromQuotedString(
              doc.getOnlyField(Const.SearchDocumentField.FEEDBACK_RESPONSE_RECEIVER_NAME)
                  .getText());
      responseRecipientTable.put(
          response.getId(), getFilteredRecipientName(response, responseRecipientName));

      String commentGiverName =
          extractContentFromQuotedString(
              doc.getOnlyField(Const.SearchDocumentField.FEEDBACK_RESPONSE_COMMENT_GIVER_NAME)
                  .getText());
      commentGiverTable.put(
          comment.getId().toString(),
          getFilteredCommentGiverName(response, comment, commentGiverName));
      numberOfCommentFound++;
    }

    for (List<FeedbackQuestionAttributes> questions : this.questions.values()) {
      Collections.sort(questions);
    }

    for (List<FeedbackResponseAttributes> responses : this.responses.values()) {
      FeedbackResponseAttributes.sortFeedbackResponses(responses);
    }

    for (List<FeedbackResponseCommentAttributes> responseComments : this.comments.values()) {
      FeedbackResponseCommentAttributes.sortFeedbackResponseCommentsByCreationTime(
          responseComments);
    }

    return this;
  }