/* * 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); } } }