/*
* Copyright (c) 2005-2011 Grameen Foundation USA
* All rights reserved.
*
* Licensed 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.
*
* See also http://www.apache.org/licenses/LICENSE-2.0.html for an
* explanation of the license and how it is applied.
*/
package org.mifos.platform.questionnaire.validators;
import org.apache.commons.lang.StringUtils;
import org.mifos.framework.exceptions.SystemException;
import org.mifos.platform.validations.ValidationException;
import org.mifos.platform.questionnaire.domain.AnswerType;
import org.mifos.platform.questionnaire.domain.QuestionChoiceEntity;
import org.mifos.platform.questionnaire.domain.QuestionEntity;
import org.mifos.platform.questionnaire.exceptions.BadNumericResponseException;
import org.mifos.platform.questionnaire.exceptions.MandatoryAnswerNotFoundException;
import org.mifos.platform.questionnaire.persistence.EventSourceDao;
import org.mifos.platform.questionnaire.persistence.QuestionDao;
import org.mifos.platform.questionnaire.persistence.QuestionGroupDao;
import org.mifos.platform.questionnaire.service.QuestionDetail;
import org.mifos.platform.questionnaire.service.QuestionGroupDetail;
import org.mifos.platform.questionnaire.service.QuestionType;
import org.mifos.platform.questionnaire.service.SectionDetail;
import org.mifos.platform.questionnaire.service.SectionQuestionDetail;
import org.mifos.platform.questionnaire.service.dtos.ChoiceDto;
import org.mifos.platform.questionnaire.service.dtos.EventSourceDto;
import org.mifos.platform.questionnaire.service.dtos.QuestionDto;
import org.mifos.platform.questionnaire.service.dtos.QuestionGroupDto;
import org.mifos.platform.questionnaire.service.dtos.SectionDto;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.math.BigInteger;
import static org.mifos.platform.questionnaire.QuestionnaireConstants.*; //NOPMD
import static org.mifos.platform.util.CollectionUtils.isEmpty;
import static org.mifos.platform.util.CollectionUtils.isNotEmpty;
@SuppressWarnings("PMD")
public class QuestionnaireValidatorImpl implements QuestionnaireValidator {
@Autowired
private EventSourceDao eventSourceDao;
@Autowired
private QuestionDao questionDao;
@SuppressWarnings({"UnusedDeclaration"})
private QuestionnaireValidatorImpl() {
}
public QuestionnaireValidatorImpl(EventSourceDao eventSourceDao, QuestionGroupDao questionGroupDao, QuestionDao questionDao) {
this.eventSourceDao = eventSourceDao;
this.questionDao = questionDao;
}
@Override
public void validateForDefineQuestion(QuestionDetail questionDetail) throws SystemException {
validateQuestionText(questionDetail);
validateQuestionType(questionDetail);
}
@Override
public void validateForDefineQuestionGroup(QuestionGroupDetail questionGroupDetail) throws SystemException {
validateQuestionGroupTitle(questionGroupDetail);
validateQuestionGroupSections(questionGroupDetail.getSectionDetails());
List<EventSourceDto> eventSourceDtos = questionGroupDetail.getEventSources();
if (eventSourceDtos == null || eventSourceDtos.size() == 0) {
throw new SystemException(INVALID_EVENT_SOURCE);
}
else {
for (EventSourceDto eventSourceDto : eventSourceDtos) {
validateForEventSource(eventSourceDto);
}
}
}
@Override
public void validateForEventSource(EventSourceDto eventSourceDto) throws SystemException {
if (eventSourceDto == null || StringUtils.isEmpty(eventSourceDto.getSource()) || StringUtils.isEmpty(eventSourceDto.getEvent())) {
throw new SystemException(INVALID_EVENT_SOURCE);
}
validateEventSource(eventSourceDto);
}
@Override
public void validateForQuestionGroupResponses(List<QuestionGroupDetail> questionGroupDetails) {
if (isEmpty(questionGroupDetails)) {
throw new SystemException(NO_ANSWERS_PROVIDED);
}
ValidationException validationException = new ValidationException(GENERIC_VALIDATION);
for (QuestionGroupDetail questionGroupDetail : questionGroupDetails) {
validateResponsesInQuestionGroup(questionGroupDetail, validationException);
}
if (validationException.hasChildExceptions()) {
throw validationException;
}
}
@Override
public void validateForDefineQuestionGroup(QuestionGroupDto questionGroupDto) {
validateForDefineQuestionGroup(questionGroupDto, true);
}
@Override
public void validateForDefineQuestionGroup(QuestionGroupDto questionGroupDto,
boolean withDuplicateQuestionTextCheck) {
ValidationException parentException = new ValidationException(GENERIC_VALIDATION);
validateQuestionGroupTitle(questionGroupDto, parentException);
List<EventSourceDto> eventSourceDtos = questionGroupDto.getEventSourceDtos();
if (eventSourceDtos == null || eventSourceDtos.size() == 0) {
throw new SystemException(INVALID_EVENT_SOURCE);
}
else {
for (EventSourceDto eventSourceDto : eventSourceDtos) {
validateEventSource(eventSourceDto, parentException);
}
}
validateSections(questionGroupDto.getSections(), parentException, withDuplicateQuestionTextCheck);
if (parentException.hasChildExceptions()) {
throw parentException;
}
}
private void validateSections(List<SectionDto> sections, ValidationException parentException,
boolean withDuplicateQuestionTextCheck) {
if (isEmpty(sections)) {
parentException.addChildException(new ValidationException(QUESTION_GROUP_SECTION_NOT_PROVIDED));
} else {
if (!sectionsHaveInvalidNames(sections, parentException) && !sectionsHaveInvalidOrders(sections, parentException)) {
for (SectionDto section : sections) {
validateSection(section, parentException, withDuplicateQuestionTextCheck);
}
}
}
}
private void validateSection(SectionDto section, ValidationException parentException,
boolean withDuplicateQuestionTextCheck) {
validateSectionName(section, parentException);
validateQuestions(section.getQuestions(), parentException, withDuplicateQuestionTextCheck);
}
private void validateSectionName(SectionDto section, ValidationException parentException) {
String name = section.getName().trim();
if (name.length() >= MAX_LENGTH_FOR_TITILE) {
parentException.addChildException(new ValidationException(SECTION_NAME_TOO_BIG));
}
}
private void validateQuestions(List<QuestionDto> questions, ValidationException parentException,
boolean withDuplicateQuestionTextCheck) {
if (isEmpty(questions)) {
parentException.addChildException(new ValidationException(NO_QUESTIONS_FOUND_IN_SECTION));
} else {
if (!questionsHaveInvalidNames(questions, parentException, withDuplicateQuestionTextCheck) && !questionsHaveInvalidOrders(questions, parentException)) {
for (QuestionDto question : questions) {
validateQuestion(question, parentException, true, withDuplicateQuestionTextCheck);
}
}
}
}
@Override
public void validateForDefineQuestion(QuestionDto questionDto) {
ValidationException parentException = new ValidationException(GENERIC_VALIDATION);
validateQuestion(questionDto, parentException, false, true);
if (parentException.hasChildExceptions()) {
throw parentException;
}
}
private void validateQuestion(QuestionDto question, ValidationException parentException,
boolean withDuplicateQuestionTypeCheck, boolean withDuplicateQuestionTextCheck) {
if (StringUtils.isEmpty(question.getText())) {
parentException.addChildException(new ValidationException(QUESTION_TEXT_NOT_PROVIDED));
} else if (question.getText().length() >= MAX_LENGTH_FOR_QUESTION_TEXT) {
parentException.addChildException(new ValidationException(QUESTION_TITLE_TOO_BIG));
} else if (withDuplicateQuestionTextCheck && questionHasDuplicateTitle(question, withDuplicateQuestionTypeCheck)) {
parentException.addChildException(new ValidationException(QUESTION_TITILE_MATCHES_EXISTING_QUESTION));
} else {
if (QuestionType.INVALID == question.getType()) {
parentException.addChildException(new ValidationException(ANSWER_TYPE_NOT_PROVIDED));
} else if (question.isTypeWithChoices()) {
validateChoices(question, parentException);
} else if (QuestionType.NUMERIC == question.getType()) {
validateNumericBounds(question, parentException);
}
}
}
private void validateNumericBounds(QuestionDto question, ValidationException parentException) {
if (areInValidNumericBounds(question.getMinValue(), question.getMaxValue())) {
parentException.addChildException(new ValidationException(INVALID_NUMERIC_BOUNDS));
}
}
private void validateChoices(QuestionDto question, ValidationException parentException) {
List<ChoiceDto> choices = question.getChoices();
if (isEmpty(choices) || choices.size() < MAX_CHOICES_FOR_QUESTION) {
parentException.addChildException(new ValidationException(QUESTION_CHOICES_INSUFFICIENT));
} else if (choicesHaveInvalidValues(choices) || choicesHaveInvalidOrders(choices)) {
parentException.addChildException(new ValidationException(QUESTION_CHOICES_INVALID));
}
}
private boolean choicesHaveInvalidValues(List<ChoiceDto> choiceDtos) {
return !allChoicesHaveValues(choiceDtos) || !allChoicesHaveUniqueValues(choiceDtos);
}
private boolean choicesHaveInvalidOrders(List<ChoiceDto> choiceDtos) {
return !allChoicesHaveOrders(choiceDtos) || !allChoicesHaveUniqueOrders(choiceDtos);
}
private boolean allChoicesHaveUniqueOrders(List<ChoiceDto> choiceDtos) {
boolean result = true;
Set<Integer> choiceOrders = new HashSet<Integer>();
for (ChoiceDto choiceDto : choiceDtos) {
Integer order = choiceDto.getOrder();
if (choiceOrders.contains(order)) {
result = false;
break;
} else {
choiceOrders.add(order);
}
}
return result;
}
private boolean allChoicesHaveOrders(List<ChoiceDto> choiceDtos) {
boolean result = true;
for (ChoiceDto choiceDto : choiceDtos) {
if (null == choiceDto.getOrder()) {
result = false;
break;
}
}
return result;
}
private boolean allChoicesHaveValues(List<ChoiceDto> choiceDtos) {
boolean result = true;
for (ChoiceDto choiceDto : choiceDtos) {
choiceDto.trimValue();
if (StringUtils.isEmpty(choiceDto.getValue())) {
result = false;
break;
}
}
return result;
}
private boolean allChoicesHaveUniqueValues(List<ChoiceDto> choiceDtos) {
boolean result = true;
Set<String> choiceValues = new HashSet<String>();
for (ChoiceDto choiceDto : choiceDtos) {
String value = choiceDto.getValue().toLowerCase(Locale.getDefault());
if (choiceValues.contains(value)) {
result = false;
break;
} else {
choiceValues.add(value);
}
}
return result;
}
private boolean questionHasDuplicateTitle(QuestionDto question, boolean withQuestionTypeCheck) {
List<QuestionEntity> questions = questionDao.retrieveByText(question.getText());
boolean result = false;
if (isNotEmpty(questions)) {
result = true;
if (withQuestionTypeCheck) {
QuestionEntity questionEntity = questions.get(0);
result = !areSameQuestionTypes(question.getType(), questionEntity.getAnswerTypeAsEnum()) || haveIncompatibleChoices(question, questionEntity);
}
}
return result;
}
private boolean haveIncompatibleChoices(QuestionDto question, QuestionEntity questionEntity) {
List<ChoiceDto> choiceDtos = question.getChoices();
List<QuestionChoiceEntity> choiceEntities = questionEntity.getChoices();
boolean result = false;
if (choiceDtos != null && choiceEntities != null) {
result = choiceDtos.size() != choiceEntities.size();
for (int i = 0, choiceDetailsSize = choiceDtos.size(); i < choiceDetailsSize && !result; i++) {
String choiceValue = choiceDtos.get(i).getValue();
result = isUniqueChoice(choiceEntities, choiceValue);
}
}
return result;
}
private boolean isUniqueChoice(List<QuestionChoiceEntity> choiceEntities, String choiceValue) {
boolean uniqueChoice = true;
for (QuestionChoiceEntity choiceEntity : choiceEntities) {
if (StringUtils.equalsIgnoreCase(choiceValue, choiceEntity.getChoiceText())) {
uniqueChoice = false;
break;
}
}
return uniqueChoice;
}
private boolean areSameQuestionTypes(QuestionType type, AnswerType answerType) {
boolean result;
switch(type) {
case FREETEXT:
result = AnswerType.FREETEXT == answerType;
break;
case SMART_SELECT:
result = AnswerType.SMARTSELECT == answerType;
break;
case SINGLE_SELECT:
result = AnswerType.CHOICE == answerType || AnswerType.SINGLESELECT == answerType;
break;
case DATE:
result = AnswerType.DATE == answerType;
break;
case NUMERIC:
result = AnswerType.NUMBER == answerType;
break;
case MULTI_SELECT:
result = AnswerType.MULTISELECT == answerType;
break;
default:
result = false;
}
return result;
}
private boolean questionsHaveInvalidOrders(List<QuestionDto> questions, ValidationException parentException) {
boolean invalid = false;
if (!allQuestionsHaveOrders(questions)) {
parentException.addChildException(new ValidationException(QUESTION_ORDER_NOT_PROVIDED));
invalid = true;
} else if(!allQuestionsHaveUniqueOrders(questions)) {
parentException.addChildException(new ValidationException(QUESTION_ORDER_DUPLICATE));
invalid = true;
}
return invalid;
}
private boolean questionsHaveInvalidNames(List<QuestionDto> questions, ValidationException parentException,
boolean withDuplicateQuestionTextCheck) {
boolean invalid = false;
if (!allQuestionsHaveNames(questions)) {
parentException.addChildException(new ValidationException(QUESTION_TEXT_NOT_PROVIDED));
invalid = true;
} else if(withDuplicateQuestionTextCheck && !allQuestionsHaveUniqueNames(questions)) {
parentException.addChildException(new ValidationException(QUESTION_TITLE_DUPLICATE));
invalid = true;
}
return invalid;
}
private boolean allQuestionsHaveUniqueNames(List<QuestionDto> questions) {
boolean result = true;
Set<String> questionNames = new HashSet<String>();
for (QuestionDto question : questions) {
String name = question.getText().toLowerCase(Locale.getDefault());
if (questionNames.contains(name)) {
result = false;
break;
} else {
questionNames.add(name);
}
}
return result;
}
private boolean sectionsHaveInvalidOrders(List<SectionDto> sections, ValidationException parentException) {
boolean invalid = false;
if (!allSectionsHaveOrders(sections)) {
parentException.addChildException(new ValidationException(SECTION_ORDER_NOT_PROVIDED));
invalid = true;
} else if(!allSectionsHaveUniqueOrders(sections)) {
parentException.addChildException(new ValidationException(SECTION_ORDER_DUPLICATE));
invalid = true;
}
return invalid;
}
private boolean allSectionsHaveUniqueOrders(List<SectionDto> sections) {
boolean result = true;
Set<Integer> sectionOrders = new HashSet<Integer>();
for (SectionDto section : sections) {
Integer order = section.getOrder();
if (sectionOrders.contains(order)) {
result = false;
break;
} else {
sectionOrders.add(order);
}
}
return result;
}
private boolean allQuestionsHaveUniqueOrders(List<QuestionDto> questions) {
boolean result = true;
Set<Integer> sectionOrders = new HashSet<Integer>();
for (QuestionDto question : questions) {
Integer order = question.getOrder();
if (sectionOrders.contains(order)) {
result = false;
break;
} else {
sectionOrders.add(order);
}
}
return result;
}
private boolean allSectionsHaveOrders(List<SectionDto> sections) {
boolean result = true;
for (SectionDto section : sections) {
if (null == section.getOrder()) {
result = false;
break;
}
}
return result;
}
private boolean allQuestionsHaveOrders(List<QuestionDto> questions) {
boolean result = true;
for (QuestionDto question : questions) {
if (null == question.getOrder()) {
result = false;
break;
}
}
return result;
}
private boolean sectionsHaveInvalidNames(List<SectionDto> sections, ValidationException parentException) {
boolean invalid = false;
if (!allSectionsHaveNames(sections)) {
parentException.addChildException(new ValidationException(SECTION_TITLE_NOT_PROVIDED));
invalid = true;
} else if(!allSectionsHaveUniqueNames(sections)) {
parentException.addChildException(new ValidationException(SECTION_TITLE_DUPLICATE));
invalid = true;
}
return invalid;
}
private boolean allSectionsHaveUniqueNames(List<SectionDto> sections) {
boolean result = true;
Set<String> sectionNames = new HashSet<String>();
for (SectionDto section : sections) {
String name = section.getName().toLowerCase(Locale.getDefault());
if (sectionNames.contains(name)) {
result = false;
break;
} else {
sectionNames.add(name);
}
}
return result;
}
private boolean allSectionsHaveNames(List<SectionDto> sections) {
boolean result = true;
for (SectionDto section : sections) {
section.trimName();
if (StringUtils.isEmpty(section.getName())) {
result = false;
break;
}
}
return result;
}
private boolean allQuestionsHaveNames(List<QuestionDto> questions) {
boolean result = true;
for (QuestionDto questionDto : questions) {
questionDto.trimTitle();
if (StringUtils.isEmpty(questionDto.getText())) {
result = false;
break;
}
}
return result;
}
private void validateEventSource(EventSourceDto eventSourceDto, ValidationException parentException) {
if (eventSourceDto == null || eventSourceDto.getEvent() == null || eventSourceDto.getSource() == null) {
parentException.addChildException(new ValidationException(INVALID_EVENT_SOURCE));
} else {
try {
validateEventSource(eventSourceDto);
} catch (SystemException e) {
parentException.addChildException(new ValidationException(e.getKey()));
}
}
}
private void validateQuestionGroupTitle(QuestionGroupDto questionGroupDto, ValidationException parentException) {
String title = questionGroupDto.getTitle();
if (StringUtils.isEmpty(title)) {
parentException.addChildException(new ValidationException(QUESTION_GROUP_TITLE_NOT_PROVIDED));
} else {
title = title.trim();
if (title.length() >= MAX_LENGTH_FOR_TITILE) {
parentException.addChildException(new ValidationException(QUESTION_GROUP_TITLE_TOO_BIG));
}
}
}
private void validateResponsesInQuestionGroup(QuestionGroupDetail questionGroupDetail, ValidationException validationException) {
for (SectionDetail sectionDetail : questionGroupDetail.getSectionDetails()) {
for (SectionQuestionDetail sectionQuestionDetail : sectionDetail.getQuestions()) {
validateSectionQuestionDetail(validationException, sectionQuestionDetail);
}
}
}
@SuppressWarnings({"ThrowableInstanceNeverThrown"})
private void validateSectionQuestionDetail(ValidationException validationException, SectionQuestionDetail sectionQuestionDetail) {
// TODO: When there are more such validations, use a chain of validators
String questionTitle = sectionQuestionDetail.getText();
if (sectionQuestionDetail.isMandatory() && sectionQuestionDetail.hasNoAnswer()) {
validationException.addChildException(new MandatoryAnswerNotFoundException(questionTitle));
} else if (sectionQuestionDetail.hasAnswer() && sectionQuestionDetail.isNumeric()) {
Integer allowedMinValue = sectionQuestionDetail.getNumericMin();
Integer allowedMaxValue = sectionQuestionDetail.getNumericMax();
if (invalidNumericAnswer(sectionQuestionDetail.getValue(), allowedMinValue, allowedMaxValue)) {
validationException.addChildException(new BadNumericResponseException(questionTitle, allowedMinValue, allowedMaxValue));
}
}
}
private boolean invalidNumericAnswer(String answer, Integer allowedMin, Integer allowedMax) {
boolean result;
try {
BigInteger answerAsInt = new BigInteger(answer, 10);
result = (allowedMin != null && answerAsInt.compareTo(new BigInteger(allowedMin.toString())) < 0) ||
(allowedMax != null && answerAsInt.compareTo(new BigInteger(allowedMax.toString())) > 0);
} catch (NumberFormatException e) {
result = true;
}
return result;
}
private void validateEventSource(EventSourceDto eventSourceDto) throws SystemException {
List<Long> result = eventSourceDao.retrieveCountByEventAndSource(eventSourceDto.getEvent(), eventSourceDto.getSource());
if (isEmpty(result) || result.get(0) == 0) {
throw new SystemException(INVALID_EVENT_SOURCE);
}
}
private void validateQuestionGroupSections(List<SectionDetail> sectionDetails) throws SystemException {
if(isEmpty(sectionDetails)) {
throw new SystemException(QUESTION_GROUP_SECTION_NOT_PROVIDED);
}
validateSectionDefinitions(sectionDetails);
}
private void validateSectionDefinitions(List<SectionDetail> sectionDetails) throws SystemException {
for (SectionDetail sectionDetail : sectionDetails) {
validateSectionDefinition(sectionDetail);
}
}
private void validateSectionDefinition(SectionDetail sectionDetail) throws SystemException {
if (isEmpty(sectionDetail.getQuestions())) {
throw new SystemException(NO_QUESTIONS_FOUND_IN_SECTION);
}
}
private void validateQuestionGroupTitle(QuestionGroupDetail questionGroupDetail) throws SystemException {
if (StringUtils.isEmpty(questionGroupDetail.getTitle())) {
throw new SystemException(QUESTION_GROUP_TITLE_NOT_PROVIDED);
}
}
private void validateQuestionType(QuestionDetail questionDetail) throws SystemException {
if (QuestionType.INVALID == questionDetail.getType()) {
throw new SystemException(ANSWER_TYPE_NOT_PROVIDED);
}
if (QuestionType.NUMERIC == questionDetail.getType()) {
validateForNumericQuestionType(questionDetail.getNumericMin(), questionDetail.getNumericMax());
}
}
private void validateForNumericQuestionType(Integer min, Integer max) {
if (areInValidNumericBounds(min, max)) {
throw new SystemException(INVALID_NUMERIC_BOUNDS);
}
}
private boolean areInValidNumericBounds(Integer min, Integer max) {
return min != null && max != null && min > max;
}
private void validateQuestionText(QuestionDetail questionDefinition) throws SystemException {
if (StringUtils.isEmpty(questionDefinition.getText())) {
throw new SystemException(QUESTION_TEXT_NOT_PROVIDED);
}
}
}