/*
* Copyright (C) 2012-2016 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 org.waterforpeople.mapping.app.web.rest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import org.akvo.flow.domain.DataUtils;
import org.apache.commons.lang.StringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.waterforpeople.mapping.analytics.dao.SurveyQuestionSummaryDao;
import org.waterforpeople.mapping.app.gwt.client.surveyinstance.QuestionAnswerStoreDto;
import org.waterforpeople.mapping.app.util.DtoMarshaller;
import org.waterforpeople.mapping.app.web.DataProcessorRestServlet;
import org.waterforpeople.mapping.app.web.rest.dto.QuestionAnswerStorePayload;
import org.waterforpeople.mapping.app.web.rest.dto.RestStatusDto;
import org.waterforpeople.mapping.dao.QuestionAnswerStoreDao;
import org.waterforpeople.mapping.dao.SurveyInstanceDAO;
import org.waterforpeople.mapping.domain.QuestionAnswerStore;
import org.waterforpeople.mapping.serialization.response.MediaResponse;
import com.gallatinsystems.common.Constants;
import com.gallatinsystems.survey.dao.CascadeNodeDao;
import com.gallatinsystems.survey.dao.QuestionDao;
import com.gallatinsystems.survey.dao.SurveyUtils;
import com.gallatinsystems.survey.domain.CascadeNode;
import com.gallatinsystems.survey.domain.Question;
import com.gallatinsystems.surveyal.dao.SurveyedLocaleDao;
import com.gallatinsystems.surveyal.domain.SurveyalValue;
@Controller
@RequestMapping("/question_answers")
public class QuestionAnswerRestService {
@Inject
private QuestionAnswerStoreDao questionAnswerStoreDao;
@Inject
private QuestionDao questionDao;
@Inject
private CascadeNodeDao cascadeNodeDao;
// list questionAnswerStores by id
@RequestMapping(method = RequestMethod.GET, value = "")
@ResponseBody
public Map<String, List<QuestionAnswerStoreDto>> listQABySurveyInstanceId(
HttpServletRequest httpRequest,
@RequestParam(value = "surveyInstanceId", defaultValue = "") Long surveyInstanceId) {
final Map<String, List<QuestionAnswerStoreDto>> response = new HashMap<String, List<QuestionAnswerStoreDto>>();
List<QuestionAnswerStoreDto> results = new ArrayList<QuestionAnswerStoreDto>();
List<QuestionAnswerStore> questionAnswerStores = null;
SurveyInstanceDAO siDao = new SurveyInstanceDAO();
QuestionDao qDao = new QuestionDao();
// get list of question-answers
if (surveyInstanceId != null) {
questionAnswerStores = siDao.listQuestionAnswerStore(
surveyInstanceId, null);
if (questionAnswerStores != null && questionAnswerStores.size() > 0) {
// get the list of questions belonging to this surveyInstance in
// the right order
List<Question> qList = qDao
.listQuestionsInOrder(questionAnswerStores.get(0)
.getSurveyId(), null);
results = new ArrayList<>();
// sort the questionAnswers in the order of the questions
int notFoundCount = 0;
if (qList != null) {
for (QuestionAnswerStore qas : questionAnswerStores) {
QuestionAnswerStoreDto qasDto = new QuestionAnswerStoreDto();
DtoMarshaller.copyToDto(qas, qasDto);
int idx = -1;
for (int i = 0; i < qList.size(); i++) {
if (Long.parseLong(qas.getQuestionID()) == qList
.get(i).getKey().getId()) {
qasDto.setQuestionText(qList.get(i).getText());
qasDto.setTextualQuestionId(qList.get(i).getQuestionId());
idx = i;
break;
}
}
// Store not found items at the beginning
if (idx < 0) {
results.add(notFoundCount++, qasDto);
continue;
}
idx += notFoundCount;
while (results.size() < idx + 1) {
// Make sure we have enough room for the item
results.add(null);
}
processApiResponse(qasDto, httpRequest);
results.add(idx, qasDto);
}
}
}
}
// FIXME: use a better solution for removing null items...
while (results.remove(null))
;
response.put("question_answers", results);
return response;
}
/**
* Process the response returned to take into account formats for the API versions
*/
private void processApiResponse(QuestionAnswerStoreDto response,
HttpServletRequest httpRequest) {
if (httpRequest.getRequestURI().startsWith(Constants.API_V1_PREFIX)) {
// V1 API
formatResponseAPIV1(response);
} else {
// Latest API
formatResponseLatestAPI(response);
}
}
/**
* Format Question response according to API v1
*/
private void formatResponseAPIV1(QuestionAnswerStoreDto response) {
String value = response.getValue();
String type = response.getType();
if (StringUtils.isEmpty(value)) {
return;
}
switch (type) {
case "OPTION":
case "OTHER":
if (value.startsWith("[")) {
response.setValue(DataUtils.jsonResponsesToPipeSeparated(value));
}
break;
case "IMAGE":
case "VIDEO":
response.setValue(MediaResponse.format(value, MediaResponse.VERSION_STRING));
break;
default:
break;
}
}
/**
* Format Question response according to the most up-to-date API format
*/
private void formatResponseLatestAPI(QuestionAnswerStoreDto response) {
String value = response.getValue();
String type = response.getType();
if (StringUtils.isEmpty(value)) {
return;
}
switch (type) {
case "IMAGE":
case "VIDEO":
value = MediaResponse.format(value, MediaResponse.VERSION_GEOTAGGING);
response.setValue(value);
break;
default:
break;
}
}
// find a single questionAnswerStore by the questionAnswerStoreId
// TODO include question text in dto
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
@ResponseBody
public Map<String, QuestionAnswerStoreDto> findQuestionAnswerStore(
@PathVariable("id") Long id) {
final Map<String, QuestionAnswerStoreDto> response = new HashMap<String, QuestionAnswerStoreDto>();
QuestionAnswerStoreDao qaDao = new QuestionAnswerStoreDao();
QuestionAnswerStore s = qaDao.getByKey(id);
QuestionAnswerStoreDto dto = null;
if (s != null) {
dto = new QuestionAnswerStoreDto();
DtoMarshaller.copyToDto(s, dto);
// This endpoint is only used in the FLOW dashboard.
// Latest API format can be safely used.
formatResponseLatestAPI(dto);
}
response.put("question_answer", dto);
return response;
}
// update existing questionAnswerStore
@RequestMapping(method = RequestMethod.PUT, value = "/{id}")
@ResponseBody
public Map<String, Object> saveExistingQuestionAnswerStore(
@RequestBody QuestionAnswerStorePayload payLoad) {
final QuestionAnswerStoreDto requestDto = payLoad
.getQuestion_answer();
final RestStatusDto statusDto = new RestStatusDto();
statusDto.setStatus("failed");
final Map<String, Object> response = new HashMap<String, Object>();
response.put("meta", statusDto);
final QuestionAnswerStoreDto responseDto = new QuestionAnswerStoreDto();
// if the POST data contains a valid questionAnswerStoreDto, continue.
// Otherwise,
// server will respond with 400 Bad Request
if (requestDto != null) {
Long keyId = requestDto.getKeyId();
Question q = questionDao.getByKey(Long.parseLong(requestDto.getQuestionID()));
QuestionAnswerStore qa;
// if the questionAnswerStoreDto has a key, try to get the
// questionAnswerStore.
if (keyId != null && q != null) {
qa = questionAnswerStoreDao.getByKey(keyId);
// if we find the questionAnswerStore, update it's properties
if (qa != null) {
// Before updating the properties, fix the questionAnswerSummary counts if it is
// an OPTION question
if (Question.Type.OPTION.equals(q.getType())) {
// decrease count of current item
SurveyQuestionSummaryDao.incrementCount(qa, -1);
// increase count of new item
String newVal = requestDto.getValue();
if (newVal != null && newVal.trim().length() > 0) {
SurveyQuestionSummaryDao.incrementCount(
constructQAS(qa.getQuestionID(), newVal), 1);
}
} else if (Question.Type.CASCADE.equals(q.getType())) {
JSONArray cascadeResponse = null;
boolean isValidJson = true;
boolean isValidResponse = true;
try {
cascadeResponse = new JSONArray(requestDto.getValue());
isValidResponse = isValidCascadeResponse(q, cascadeResponse);
} catch (JSONException e) {
isValidJson = false;
}
// validate individual nodes
if (!isValidJson || !isValidResponse) {
statusDto.setMessage("_invalid_cascade_response");
return response;
}
}
// copy the properties, except the createdDateTime property,
// because it is set in the Dao.
BeanUtils.copyProperties(requestDto, qa,
new String[] {
"createdDateTime", "status",
"version", "lastUpdateDateTime",
"displayName", "questionGroupList", "questionText"
});
qa = questionAnswerStoreDao.save(qa);
// next, update the corresponding surveyalValue object
// find surveyalValue based on surveyInstanceId and questionId
Long surveyInstanceId = qa.getSurveyInstanceId();
String questionId = qa.getQuestionID();
SurveyedLocaleDao slDao = new SurveyedLocaleDao();
List<SurveyalValue> svals = slDao.listSVByQuestionAndSurveyInstance(
surveyInstanceId, Long.parseLong(questionId));
Long surveyedLocaleId = null;
if (svals != null && svals.size() > 0) {
SurveyalValue sval = svals.get(0);
sval.setStringValue(qa.getValue());
slDao.save(sval);
// Populate locale id from the only entity containing this attribute
surveyedLocaleId = sval.getSurveyedLocaleId();
}
// Update datapoint names for this survey, if applies
if (q.getLocaleNameFlag() && surveyedLocaleId != null) {
DataProcessorRestServlet.scheduleDatapointNameAssembly(
null, surveyedLocaleId, true);
}
// return result to the Dashboard
DtoMarshaller.copyToDto(qa, responseDto);
// give back the question text as we received it
responseDto.setQuestionText(requestDto.getQuestionText());
statusDto.setStatus("ok");
try {
// A PUT is done when editing a QuestionAnswerStore, we
// need to invalidate a cached report
List<Long> surveyIds = new ArrayList<Long>();
surveyIds.add(requestDto.getSurveyId());
SurveyUtils
.notifyReportService(surveyIds, "invalidate");
} catch (Exception e) {
// no-op
}
}
}
}
response.put("question_answer", responseDto);
return response;
}
/**
* Compare submitted cascade response with nodes from the datastore to determine cascade
* response validity
*
* @param question
* @param response
* @return
* @throws JSONException
*/
private boolean isValidCascadeResponse(Question question, JSONArray response)
throws JSONException {
boolean valid = false;
List<String> responseNodeNames = new ArrayList<String>();
for (int i = 0; i < response.length(); i++) {
responseNodeNames.add(response.getJSONObject(i).getString("name"));
}
List<CascadeNode> nodes = cascadeNodeDao.listByName(question.getCascadeResourceId(),
responseNodeNames);
List<List<CascadeNode>> cascadePathsList = createCascadeNodePaths(nodes);
for (List<CascadeNode> path : cascadePathsList) {
if (path.size() != response.length()) {
continue;
}
List<String> pathNodeNames = new ArrayList<String>();
for (int i = 0; i < path.size(); i++) {
pathNodeNames.add(path.get(i).getName());
}
if (responseNodeNames.equals(pathNodeNames)) {
valid = true;
break;
}
}
return valid;
}
/**
* Given a list of cascade nodes, split it up into a number of lists containing the cascade
* paths within that list of nodes
*
* @param nodes
* @return
*/
private List<List<CascadeNode>> createCascadeNodePaths(List<CascadeNode> nodes) {
List<List<CascadeNode>> pathsList = new ArrayList<List<CascadeNode>>();
Map<Long, CascadeNode> nodesMap = new HashMap<Long, CascadeNode>();
for (CascadeNode node : nodes) {
nodesMap.put(node.getKey().getId(), node);
}
for (CascadeNode node : nodes) {
CascadeNode currentNode = node;
List<CascadeNode> path = new ArrayList<CascadeNode>();
if (currentNode.getParentNodeId().equals(0L)) {
path.add(currentNode);
} else {
while (currentNode != null && !currentNode.getParentNodeId().equals(0L)) {
path.add(currentNode);
currentNode = nodesMap.get(currentNode.getParentNodeId());
}
if (currentNode != null) {
path.add(currentNode); // add first element in path
}
}
Collections.reverse(path);
pathsList.add(path);
}
return pathsList;
}
/**
* helper method to create a new QuestionAnswerStore object using the values passed in. Same
* method as in SurveyQuestionSummaryUpdater
*
* @param id
* @param value
* @return
*/
private QuestionAnswerStore constructQAS(String id, String value) {
QuestionAnswerStore qas = new QuestionAnswerStore();
qas.setQuestionID(id);
qas.setValue(value);
return qas;
}
}