/* * Copyright (C) 2010-2015 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; import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.servlet.http.HttpServletRequest; import org.akvo.flow.domain.DataUtils; import org.apache.commons.codec.binary.Base64; import org.json.JSONObject; import org.waterforpeople.mapping.analytics.dao.AccessPointStatusSummaryDao; import org.waterforpeople.mapping.analytics.dao.SurveyQuestionSummaryDao; import org.waterforpeople.mapping.analytics.domain.AccessPointStatusSummary; import org.waterforpeople.mapping.analytics.domain.SurveyQuestionSummary; import org.waterforpeople.mapping.app.gwt.client.surveyinstance.QuestionAnswerStoreDto; import org.waterforpeople.mapping.app.util.DtoMarshaller; import org.waterforpeople.mapping.app.web.dto.DataBackoutRequest; import org.waterforpeople.mapping.app.web.dto.QuestionAnswerResponse; import org.waterforpeople.mapping.dao.AccessPointDao; import org.waterforpeople.mapping.dao.SurveyInstanceDAO; import org.waterforpeople.mapping.domain.AccessPoint; import org.waterforpeople.mapping.domain.QuestionAnswerStore; import org.waterforpeople.mapping.domain.SurveyInstance; import com.gallatinsystems.framework.dao.BaseDAO; import com.gallatinsystems.framework.rest.AbstractRestApiServlet; import com.gallatinsystems.framework.rest.RestRequest; import com.gallatinsystems.framework.rest.RestResponse; import com.gallatinsystems.survey.dao.QuestionDao; import com.gallatinsystems.survey.domain.Question; import com.gallatinsystems.surveyal.dao.SurveyedLocaleDao; import com.gallatinsystems.surveyal.domain.SurveyalValue; import com.gallatinsystems.surveyal.domain.SurveyedLocale; import com.google.appengine.api.datastore.Entity; /** * servlet for backing out survey response data (and corresponding summarizations) * * @author Christopher Fagiani */ public class DataBackoutServlet extends AbstractRestApiServlet { private static final long serialVersionUID = 4608959174864994769L; private QuestionDao qDao; private SurveyQuestionSummaryDao questionSummaryDao; private SurveyInstanceDAO instanceDao; private AccessPointDao accessPointDao; private SurveyedLocaleDao localeDao; private AccessPointStatusSummaryDao apSummaryDao; private static final ThreadLocal<DateFormat> OUT_FMT = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("dd-MM-yyyy HH:mm:ss z"); }; }; public DataBackoutServlet() { setMode(PLAINTEXT_MODE); qDao = new QuestionDao(); localeDao = new SurveyedLocaleDao(); questionSummaryDao = new SurveyQuestionSummaryDao(); instanceDao = new SurveyInstanceDAO(); accessPointDao = new AccessPointDao(); apSummaryDao = new AccessPointStatusSummaryDao(); } @Override protected RestRequest convertRequest() throws Exception { HttpServletRequest req = getRequest(); RestRequest restRequest = new DataBackoutRequest(); restRequest.populateFromHttpRequest(req); return restRequest; } @Override protected RestResponse handleRequest(RestRequest req) throws Exception { DataBackoutRequest boReq = (DataBackoutRequest) req; RestResponse response = new RestResponse(); if (DataBackoutRequest.GET_QUESTION_ACTION.equals(boReq.getAction())) { response.setMessage(listQuestionIds(boReq.getSurveyId())); } else if (DataBackoutRequest.DELETE_QUESTION_SUMMARY_ACTION .equals(boReq.getAction())) { deleteQuestionSummary(boReq.getQuestionId()); } else if (DataBackoutRequest.LIST_INSTANCE_ACTION.equals(boReq .getAction())) { response.setMessage(listSurveyInstance(boReq.getSurveyId(), boReq.includeDate(), boReq.getLastCollection(), boReq.getFromDate(), boReq.getToDate(), boReq.getLimit())); } else if (DataBackoutRequest.DELETE_SURVEY_INSTANCE_ACTION .equals(boReq.getAction())) { deleteSurveyInstance(boReq.getSurveyInstanceId()); } else if (DataBackoutRequest.DELETE_ACCESS_POINT_ACTION.equals(boReq .getAction())) { response.setMessage("" + deleteAccessPoint(boReq.getCountryCode(), boReq.getToDate())); } else if (DataBackoutRequest.DELETE_AP_SUMMARY_ACTION.equals(boReq .getAction())) { response.setMessage("" + deleteAccessPointSummary(boReq.getCountryCode(), boReq.getToDate())); } else if (DataBackoutRequest.LIST_INSTANCE_RESPONSE_ACTION .equals(boReq.getAction())) { response.setMessage(listResponses(boReq.getSurveyInstanceId())); } else if (DataBackoutRequest.LIST_QUESTION_RESPONSE_ACTION .equals(boReq.getAction())) { response = listQuestionResponse(boReq.getQuestionId(), boReq.getCursor()); } return response; } /** * lists all responses for a single question * * * @param surveyId * @param questionId * @return */ private QuestionAnswerResponse listQuestionResponse(Long questionId, String cursor) { List<QuestionAnswerStore> answers = instanceDao .listQuestionAnswerStoreForQuestion(questionId.toString(), cursor); return convertToAnswerResponse(answers, SurveyInstanceDAO.getCursor(answers)); } /** * lists all questionAnswerStore records for a given instance... in a csv like format TODO: We * should probably quote the values somehow, otherwise, what happens if a response contains \n? * * @param surveyInstanceId * @return */ private String listResponses(Long surveyInstanceId) { StringBuilder result = new StringBuilder(); if (surveyInstanceId != null) { List<QuestionAnswerStore> qasList = instanceDao .listQuestionAnswerStore(surveyInstanceId, null); if (qasList != null) { boolean isFirst = true; for (QuestionAnswerStore qas : qasList) { if (!isFirst) { result.append("\n"); } else { isFirst = false; } String questionId = qas.getQuestionID(); Integer iteration = qas.getIteration(); iteration = iteration == null ? 0 : iteration; String value = qas.getValue(); // strip image data that will not be used in the excel export if (Question.Type.SIGNATURE.toString().equals(qas.getType())) { value = DataUtils.parseSignatory(value); } value = value == null ? "" : value; result.append(questionId) .append(",") .append(iteration) .append(",") .append(Base64.encodeBase64URLSafeString(value .getBytes(StandardCharsets.UTF_8))); } } } return result.toString(); } /** * deletes all access point status summary objects for the country specified with a creation * date on or after the date passed in. This method will delete 20 records at a time. If there * are more remaining, it will return true, otherwise it will return false * * @param country * @param creationDate * @return */ private boolean deleteAccessPointSummary(String country, Date creationDate) { boolean hasMore = false; List<AccessPointStatusSummary> apList = apSummaryDao .listByCountryAndCreationDate(country, creationDate, null); if (apList != null) { if (apList.size() == BaseDAO.DEFAULT_RESULT_COUNT) { hasMore = true; } accessPointDao.delete(apList); } return hasMore; } /** * deletes all access points in the country specified with a collection date on or after the * date passed in. This method will delete 20 records at a time. If there are more remaining, it * will return true, otherwise it will return false * * @param country * @param collectionDateFrom * @return */ private boolean deleteAccessPoint(String country, Date collectionDateFrom) { boolean hasMore = false; List<AccessPoint> apList = accessPointDao.searchAccessPoints(country, null, collectionDateFrom, null, null, null, null, null, null, null, null, null); if (apList != null) { if (apList.size() == BaseDAO.DEFAULT_RESULT_COUNT) { hasMore = true; } accessPointDao.delete(apList); } return hasMore; } /** * returns a comma separated list of survyeInstanceIds for the survey passed in * * @param surveyId * @return */ private String listSurveyInstance(Long surveyId, boolean includeDate, boolean lastCollection, Date fromDate, Date toDate, Integer limit) { boolean keysOnly = true; if (includeDate || lastCollection) { keysOnly = false; } Iterable<Entity> instances = instanceDao.listRawEntity(keysOnly, fromDate, toDate, limit, surveyId); StringBuilder buffer = new StringBuilder(); List<Long> processed = new ArrayList<Long>(); if (instances != null) { boolean isFirst = true; for (Entity result : instances) { if (lastCollection && processed.contains((Long) result .getProperty("surveyedLocaleId"))) { continue; // skip } if (!isFirst) { buffer.append(","); } else { isFirst = false; } buffer.append(result.getKey().getId()); if (includeDate && result.getProperty("collectionDate") != null) { buffer.append("|").append( OUT_FMT.get().format( result.getProperty("collectionDate"))); } if (lastCollection) { processed.add((Long) result.getProperty("surveyedLocaleId")); } } } return buffer.toString(); } /** * deletes a survey instance and it's associated questionAnswerStore objects * * @param surveyInstanceId */ private void deleteSurveyInstance(Long surveyInstanceId) { List<QuestionAnswerStore> questions = instanceDao .listQuestionAnswerStore(surveyInstanceId, null); if (questions != null) { instanceDao.delete(questions); } SurveyInstance instance = instanceDao.getByKey(surveyInstanceId); if (instance != null) { instanceDao.delete(instance); } List<SurveyalValue> vals = localeDao .listSurveyalValuesByInstance(surveyInstanceId); if (vals != null && vals.size() > 0) { Long localeId = vals.get(0).getSurveyedLocaleId(); localeDao.delete(vals); // now see if there are any other values for the same locale List<SurveyalValue> otherVals = localeDao .listValuesByLocale(localeId); if (otherVals == null || otherVals.size() == 0) { // if there are no other values, delete the locale SurveyedLocale l = localeDao.getByKey(localeId); localeDao.delete(l); } } } /** * deletes all the SurveyQuestionSummary objects for a specific questionId * * @param questionId */ private void deleteQuestionSummary(Long questionId) { List<SurveyQuestionSummary> summaries = questionSummaryDao .listByQuestion(questionId.toString()); if (summaries != null) { questionSummaryDao.delete(summaries); } } /** * returns a comma separated list of question IDs contained in the survey passed in * * @param surveyId * @return */ private String listQuestionIds(Long surveyId) { List<Question> questions = qDao.listQuestionsBySurvey(surveyId); StringBuilder buffer = new StringBuilder(); if (questions != null) { boolean isFirst = true; for (Question q : questions) { if (!isFirst) { buffer.append(","); } else { isFirst = false; } buffer.append(q.getKey().getId()); } } return buffer.toString(); } @Override protected void writeOkResponse(RestResponse resp) throws Exception { getResponse().setStatus(200); if (resp instanceof QuestionAnswerResponse) { QuestionAnswerResponse ansResponse = (QuestionAnswerResponse) resp; JSONObject result = new JSONObject(ansResponse); getResponse().getWriter().println(result.toString()); } else { getResponse().getWriter().println(resp.getMessage()); } } /** * converts the domain objects to dtos and then installs them in an QuestionAnswerResponse * object */ protected QuestionAnswerResponse convertToAnswerResponse( List<QuestionAnswerStore> answerList, String cursor) { QuestionAnswerResponse resp = new QuestionAnswerResponse(); if (answerList != null) { List<QuestionAnswerStoreDto> dtoList = new ArrayList<QuestionAnswerStoreDto>(); for (QuestionAnswerStore ans : answerList) { QuestionAnswerStoreDto qasDto = new QuestionAnswerStoreDto(); DtoMarshaller.copyToDto(ans, qasDto); dtoList.add(qasDto); } resp.setAnswers(dtoList); } resp.setCursor(cursor); return resp; } }