/* * 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.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.logging.Level; import java.util.logging.Logger; import javax.inject.Inject; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Controller; 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.SurveyInstanceSummaryDao; import org.waterforpeople.mapping.analytics.domain.SurveyInstanceSummary; import org.waterforpeople.mapping.app.gwt.client.survey.SurveyDto; import org.waterforpeople.mapping.app.gwt.server.survey.SurveyServiceImpl; import org.waterforpeople.mapping.app.web.dto.BootstrapGeneratorRequest; import org.waterforpeople.mapping.app.web.dto.DataProcessorRequest; import org.waterforpeople.mapping.app.web.rest.dto.RestStatusDto; import org.waterforpeople.mapping.dao.DeviceApplicationDao; import org.waterforpeople.mapping.dao.SurveyInstanceDAO; import org.waterforpeople.mapping.domain.DeviceApplication; import org.waterforpeople.mapping.domain.QuestionAnswerStore; import org.waterforpeople.mapping.domain.SurveyInstance; import com.gallatinsystems.common.Constants; import com.gallatinsystems.survey.dao.QuestionDao; import com.gallatinsystems.survey.dao.SurveyDAO; import com.gallatinsystems.survey.dao.SurveyGroupDAO; import com.gallatinsystems.survey.dao.SurveyUtils; import com.gallatinsystems.survey.domain.Question; import com.gallatinsystems.survey.domain.Survey; import com.gallatinsystems.survey.domain.SurveyGroup; import com.gallatinsystems.surveyal.app.web.SurveyalRestRequest; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.taskqueue.Queue; import com.google.appengine.api.taskqueue.QueueFactory; import com.google.appengine.api.taskqueue.TaskOptions; import com.google.appengine.api.utils.SystemProperty; @Controller @RequestMapping("/actions") public class ActionRestService { private static final Logger logger = Logger.getLogger(ActionRestService.class.getName()); @Inject private SurveyDAO surveyDao; @Inject private SurveyGroupDAO surveyGroupDao; @Inject private QuestionDao questionDao; @RequestMapping(method = RequestMethod.GET, value = "") @ResponseBody public Map<String, Object> doAction( @RequestParam(value = "action", defaultValue = "") String action, @RequestParam(value = "surveyId", defaultValue = "") Long surveyId, @RequestParam(value = "cascadeResourceId", defaultValue = "") Long cascadeResourceId, @RequestParam(value = "surveyIds[]", defaultValue = "") Long[] surveyIds, @RequestParam(value = "email", defaultValue = "") String email, @RequestParam(value = "version", defaultValue = "") String version, @RequestParam(value = "dbInstructions", defaultValue = "") String dbInstructions, @RequestParam(value = "targetId", defaultValue = "") Long targetId, @RequestParam(value = "folderId", defaultValue = "") Long folderId) { String status = "failed"; String message = ""; final Map<String, Object> response = new HashMap<String, Object>(); RestStatusDto statusDto = new RestStatusDto(); // perform the required action if ("recomputeSurveyInstanceSummaries".equals(action)) { status = recomputeSurveyInstanceSummaries(); } else if ("publishSurvey".equals(action) && surveyId != null) { status = publishSurvey(surveyId); } else if ("generateBootstrapFile".equals(action) && surveyIds != null && email != null) { message = generateBootstrapFile(surveyIds, dbInstructions, email); status = "ok"; statusDto.setMessage(message); } else if ("removeZeroValues".equals(action)) { status = removeZeroMinMaxValues(); } else if ("fixOptions2Values".equals(action)) { status = fixOptions2Values(); } else if ("newApkVersion".equals(action)) { String path = newApkVersion(version); if (path.length() > 0) { status = "success"; statusDto.setMessage("Created entry for " + path); } } else if ("populateGeocellsForLocale".equals(action)) { status = computeGeocellsForLocales(); } else if ("createTestLocales".equals(action)) { status = createTestLocales(); } else if ("publishCascade".equals(action)) { status = SurveyUtils.publishCascade(cascadeResourceId); } else if ("copyProject".equals(action)) { status = copyProject(targetId, folderId); } statusDto.setStatus(status); response.put("actions", "[]"); response.put("meta", statusDto); return response; } /** * Used to create test locales. The only field populated is surveyId, which is set to 1. To be * used only to test clustering during development in order to speed this up, it is advisable to * comment out the code in SurveyalRestServlet which computes the geoplace while running this * method. **/ private String createTestLocales() { double latc; double lonc; double lat; double lon; SurveyInstanceDAO sDao = new SurveyInstanceDAO(); Random generator = new Random(); // create random points, in clusters. for (int i = 0; i < 1; i++) { latc = generator.nextDouble() * 120 - 60; lonc = generator.nextDouble() * 360 - 180; for (int j = 0; j < 100; j++) { SurveyInstance newSI = new SurveyInstance(); newSI.setSurveyId(1L); newSI.setCollectionDate(new Date()); newSI = sDao.save(newSI); QuestionAnswerStore newQAS = new QuestionAnswerStore(); newQAS.setSurveyInstanceId(newSI.getKey().getId()); newQAS.setType("GEO"); newQAS.setCollectionDate(new Date()); lat = latc + generator.nextDouble() * 10 - 5; lon = lonc + generator.nextDouble() * 10 - 5; String geoloc = lat + "|" + lon + "|" + 0 + "|" + "aaaaaa"; newQAS.setValue(geoloc); newQAS = sDao.save(newQAS); Queue queue = QueueFactory.getDefaultQueue(); queue.add(TaskOptions.Builder .withUrl("/app_worker/surveyalservlet") .param(SurveyalRestRequest.ACTION_PARAM, SurveyalRestRequest.INGEST_INSTANCE_ACTION) .param(SurveyalRestRequest.SURVEY_INSTANCE_PARAM, newSI.getKey().getId() + "")); } } return "ok"; } /** * runs over all surveydLocale objects, and populates: the Geocells field based on the latitude * and longitude. New surveyedLocales will have these fields populated automatically, this * method is to update legacy data. This method is invoked as a URL request: * http://..../rest/actions?action=populateGeocellsForLocale Clusters are not automatically * computed.This is done by 1) deleting all the cluster objects by hand 2) running * recomputeLocaleClusters in the dataProcessorRestServlet. **/ private String computeGeocellsForLocales() { Queue queue = QueueFactory.getDefaultQueue(); queue.add(TaskOptions.Builder.withUrl("/app_worker/surveyalservlet") .param(SurveyalRestRequest.ACTION_PARAM, SurveyalRestRequest.POP_GEOCELLS_FOR_LOCALE_ACTION) .param("cursor", "")); return "Done"; } // remove zero minVal and maxVal values // that were the result of a previous bug private String removeZeroMinMaxValues() { List<Question> questions = questionDao.list(Constants.ALL_RESULTS); int counter = 0; if (questions != null) { Double epsilon = 0.000001; for (Question q : questions) { if (q.getMinVal() != null && q.getMaxVal() != null) { if (Math.abs(q.getMinVal()) < epsilon && Math.abs(q.getMaxVal()) < epsilon) { q.setMinVal(null); q.setMaxVal(null); questionDao.save(q); counter += 1; } } } } return "updated " + counter + " questions"; } @SuppressWarnings("unused") private String recomputeSurveyInstanceSummaries() { List<Survey> surveys = surveyDao.list(Constants.ALL_RESULTS); String status = "failed"; if (surveys != null) { SurveyInstanceSummary sis = null; SurveyInstanceSummaryDao sisDao = new SurveyInstanceSummaryDao(); for (Survey s : surveys) { // need to do it per page Iterable<Entity> siList = null; SurveyInstanceDAO dao = new SurveyInstanceDAO(); siList = dao.listSurveyInstanceKeysBySurveyId(s.getKey() .getId()); Long count = 0L; for (Entity si : siList) { count++; } sis = sisDao.findBySurveyId(s.getKey().getId()); if (sis == null) { sis = new SurveyInstanceSummary(); sis.setCount(count); sis.setSurveyId(s.getKey().getId()); } else { sis.setCount(count); } sisDao.save(sis); } status = "success"; } return status; } private String publishSurvey(Long surveyId) { SurveyServiceImpl surveyService = new SurveyServiceImpl(); surveyService.publishSurveyAsync(surveyId); return "publishing requested"; } private String fixOptions2Values() { Queue queue = QueueFactory.getDefaultQueue(); TaskOptions options = TaskOptions.Builder .withUrl("/app_worker/dataprocessor") .param(DataProcessorRequest.ACTION_PARAM, DataProcessorRequest.FIX_OPTIONS2VALUES_ACTION); queue.add(options); return "fixing opions to values in surveyInstances requested"; } private String generateBootstrapFile(Long[] surveyIdList, String dbInstructions, String notificationEmail) { StringBuilder buf = new StringBuilder(); if (surveyIdList != null && surveyIdList[0] != null) { for (int i = 0; i < surveyIdList.length; i++) { if (i > 0) { buf.append(BootstrapGeneratorRequest.DELMITER); } buf.append(String.valueOf(surveyIdList[i])); } } Queue queue = QueueFactory.getQueue("background-processing"); queue.add(TaskOptions.Builder .withUrl("/app_worker/bootstrapgen") .param(BootstrapGeneratorRequest.ACTION_PARAM, BootstrapGeneratorRequest.GEN_ACTION) .param(BootstrapGeneratorRequest.SURVEY_ID_LIST_PARAM, buf.toString()) .param(BootstrapGeneratorRequest.EMAIL_PARAM, notificationEmail) .param(BootstrapGeneratorRequest.DB_PARAM, dbInstructions != null ? dbInstructions : "")); return "_request_submitted_email_will_be_sent"; } /** * Create datastore entry for new apk version object called as: * http://host/rest/actions?action=newApkVersion&version=x.y.z appCode and deviceType properties * are defaults. * * @Param version */ private String newApkVersion(String version) { Properties props = System.getProperties(); String apkS3Path = props.getProperty("apkS3Path"); // apkS3Path property in appengine-web.xml has a trailing slash apkS3Path += SystemProperty.applicationId.get() + "/"; if (version != null && version.length() > 0 && apkS3Path != null && apkS3Path.length() > 0) { DeviceApplicationDao daDao = new DeviceApplicationDao(); DeviceApplication da = new DeviceApplication(); da.setAppCode("fieldSurvey"); da.setDeviceType("androidPhone"); da.setVersion(version); da.setFileName(apkS3Path + "fieldsurvey-" + version + ".apk"); daDao.save(da); return da.getFileName(); } else { return ""; } } private String copyProject(Long targetId, Long folderId) { SurveyGroup projectSource = surveyGroupDao.getByKey(targetId); SurveyGroup projectParent = null; if (folderId != null) { projectParent = surveyGroupDao.getByKey(folderId); } if (projectSource == null) { logger.log(Level.WARNING, String.format("Failed to copy project %s to folder %s", targetId, folderId)); return "failed"; } SurveyGroup projectCopy = new SurveyGroup(); BeanUtils.copyProperties(projectSource, projectCopy, Constants.EXCLUDED_PROPERTIES); // if set, projectCopy.newLocaleSurveyId is now wrong projectCopy.setCode(projectSource.getCode() + " copy"); projectCopy.setName(projectSource.getName() + " copy"); String parentPath = null; if (projectParent != null) { parentPath = projectParent.getPath(); } else { parentPath = ""; // root folder } projectCopy.setPath(parentPath + "/" + projectCopy.getName()); projectCopy.setParentId(folderId); boolean isCopiedToDifferentFolder = projectSource.getParentId() != null && folderId != null && !projectSource.getParentId().equals(folderId); if (isCopiedToDifferentFolder) { // reset ancestorIds when copying to a different folder projectCopy.setAncestorIds(SurveyUtils.retrieveAncestorIds(projectCopy)); } projectCopy.setPublished(false); SurveyGroup savedProjectCopy = surveyGroupDao.save(projectCopy); // saves List<Survey> sourceSurveys = surveyDao.listSurveysByGroup(targetId); List<Long> surveysAncestorIds = new ArrayList<Long>(savedProjectCopy.getAncestorIds()); surveysAncestorIds.add(savedProjectCopy.getKey().getId()); for (Survey sourceSurvey : sourceSurveys) { SurveyDto surveyDto = new SurveyDto(); surveyDto.setCode(sourceSurvey.getCode()); surveyDto.setName(sourceSurvey.getName()); surveyDto.setPath(projectCopy.getPath() + "/" + sourceSurvey.getName()); surveyDto.setSurveyGroupId(savedProjectCopy.getKey().getId()); Survey surveyCopy = SurveyUtils.copySurvey(sourceSurvey, surveyDto); surveyCopy.setSurveyGroupId(savedProjectCopy.getKey().getId()); sourceSurvey.setAncestorIds(surveysAncestorIds); long copyId = surveyDao.save(surveyCopy).getKey().getId(); if (isRegistrationFormId(sourceSurvey.getKey().getId(), projectSource.getNewLocaleSurveyId())) { // original was the registration survey for its survey group savedProjectCopy.setNewLocaleSurveyId(copyId); // fix it surveyGroupDao.save(savedProjectCopy); } } return "success"; } private static boolean isRegistrationFormId(Long sourceFormId, Long sourceProjectRegistrationFormId) { return sourceFormId != null && sourceProjectRegistrationFormId != null && sourceFormId.equals(sourceProjectRegistrationFormId); } }