/* * Copyright (C) 2013-2017 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 com.gallatinsystems.survey.dao; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import org.akvo.flow.domain.SecuredObject; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.json.JSONObject; import org.springframework.beans.BeanUtils; import org.waterforpeople.mapping.app.gwt.client.survey.SurveyDto; import org.waterforpeople.mapping.app.web.dto.DataProcessorRequest; import com.gallatinsystems.common.Constants; import com.gallatinsystems.common.util.HttpUtil; import com.gallatinsystems.common.util.PropertyUtil; import com.gallatinsystems.framework.domain.BaseDomain; import com.gallatinsystems.survey.domain.CascadeResource; import com.gallatinsystems.survey.domain.CascadeResource.Status; import com.gallatinsystems.survey.domain.Question; import com.gallatinsystems.survey.domain.QuestionGroup; import com.gallatinsystems.survey.domain.QuestionOption; import com.gallatinsystems.survey.domain.Survey; import com.gallatinsystems.survey.domain.SurveyGroup; import com.gallatinsystems.survey.domain.Translation; import com.gallatinsystems.survey.domain.Translation.ParentType; import com.google.appengine.api.backends.BackendServiceFactory; import com.google.appengine.api.taskqueue.Queue; import com.google.appengine.api.taskqueue.QueueFactory; import com.google.appengine.api.taskqueue.TaskOptions; /** * @author stellan * */ public class SurveyUtils { private static final Logger log = Logger.getLogger(SurveyUtils.class.getName()); public static Survey copySurvey(Survey source, SurveyDto dto) { final SurveyDAO sDao = new SurveyDAO(); final Survey tmp = new Survey(); BeanUtils.copyProperties(source, tmp, Constants.EXCLUDED_PROPERTIES); // set name and surveyGroupId to values we got from the dashboard tmp.setCode(dto.getCode()); tmp.setName(dto.getName()); tmp.setSurveyGroupId(dto.getSurveyGroupId()); tmp.setStatus(Survey.Status.COPYING); tmp.setPath(getPath(tmp)); tmp.setVersion(Double.valueOf("1.0")); log.log(Level.INFO, "Copying `Survey` " + source.getKey().getId()); final Survey newSurvey = sDao.save(tmp); log.log(Level.INFO, "New `Survey` ID: " + newSurvey.getKey().getId()); SurveyUtils.copyTranslation(source.getKey().getId(), newSurvey.getKey() .getId(), newSurvey.getKey().getId(), null, ParentType.SURVEY_NAME, ParentType.SURVEY_DESC); log.log(Level.INFO, "Running rest of copy functionality as a task..."); final Queue queue = QueueFactory.getDefaultQueue(); final TaskOptions options = TaskOptions.Builder .withUrl("/app_worker/dataprocessor") .param(DataProcessorRequest.ACTION_PARAM, DataProcessorRequest.COPY_SURVEY) .param(DataProcessorRequest.SURVEY_ID_PARAM, String.valueOf(newSurvey.getKey().getId())) .param(DataProcessorRequest.SOURCE_PARAM, String.valueOf(source.getKey().getId())) .header("Host", BackendServiceFactory.getBackendService() .getBackendAddress("dataprocessor")); queue.add(options); return newSurvey; } /** * @param sourceGroup * @param copyGroup * @param newSurveyId * @param qDependencyResolutionMap * @return * * copies a question group to another survey or within the same survey (which risks creating duplicated question ids). */ public static QuestionGroup copyQuestionGroup(QuestionGroup sourceGroup, QuestionGroup copyGroup, Long newSurveyId, Map<Long, Long> qDependencyResolutionMap, Set<String> idsInUse) { final QuestionDao qDao = new QuestionDao(); final Long sourceGroupId = sourceGroup.getKey().getId(); final Long copyGroupId = copyGroup.getKey().getId(); SurveyUtils.copyTranslation(sourceGroupId, copyGroupId, newSurveyId, copyGroupId, ParentType.QUESTION_GROUP_NAME, ParentType.QUESTION_GROUP_DESC); List<Question> qList = qDao.listQuestionsInOrderForGroup(sourceGroupId); if (qList == null) { return copyGroup; } log.log(Level.INFO, "Copying " + qList.size() + " `Question`"); int qCount = 1; List<Question> qCopyList = new ArrayList<Question>(); for (Question question : qList) { final Question questionCopy = SurveyUtils.copyQuestion(question, copyGroupId, qCount++, newSurveyId, idsInUse); qCopyList.add(questionCopy); } if (qDependencyResolutionMap == null) { return copyGroup; } // fixing dependencies final List<Question> dependentQuestionList = new ArrayList<Question>(); for (Question questionCopy : qCopyList) { qDependencyResolutionMap.put(questionCopy.getSourceQuestionId(), questionCopy.getKey() .getId()); if (questionCopy.getDependentFlag() == null || !questionCopy.getDependentFlag()) { continue; } Long originalDependentId = questionCopy.getDependentQuestionId(); questionCopy.setDependentQuestionId(qDependencyResolutionMap.get(originalDependentId)); dependentQuestionList.add(questionCopy); } qDao.save(dependentQuestionList); log.log(Level.INFO, "Resolved dependencies for " + dependentQuestionList.size() + " `Question`"); return copyGroup; } /** * @param source * @param newQuestionGroupId * @param order * @param newSurveyId * @param idsInUse the set of all questionIds in use anywhere in the survey group * @return the new question * * copies one question, ensuring that it has a unique questionId */ public static Question copyQuestion(Question source, Long newQuestionGroupId, Integer order, Long newSurveyId, Set<String> idsInUse) { final QuestionDao qDao = new QuestionDao(); final QuestionOptionDao qoDao = new QuestionOptionDao(); final Question tmp = new Question(); final Long sourceQuestionId = source.getKey().getId(); final String[] questionExcludedProps = { "questionOptionMap", "questionHelpMediaMap", "scoringRules", "translationMap", "order", "questionId" }; final String[] allExcludedProps = (String[]) ArrayUtils.addAll( questionExcludedProps, Constants.EXCLUDED_PROPERTIES); log.log(Level.INFO, "Copying `Question` " + sourceQuestionId); BeanUtils.copyProperties(source, tmp, allExcludedProps); tmp.setOrder(order); tmp.setSourceQuestionId(sourceQuestionId); if (source.getQuestionId() != null) { if (idsInUse != null) { //must avoid these String newId = source.getQuestionId() + "_1"; int index = 2; while (idsInUse.contains(newId)) { newId = source.getQuestionId() + "_" + index++; } tmp.setQuestionId(newId); //one more to avoid idsInUse.add(newId); log.log(Level.FINE, "Changing QuestionId from " + source.getQuestionId() + " to " + newId); } else { tmp.setQuestionId(source.getQuestionId()); log.log(Level.FINE, "Keeping QuestionId " + source.getQuestionId()); } } final Question newQuestion = qDao.save(tmp, newQuestionGroupId); log.log(Level.FINE, "New `Question` ID: " + newQuestion.getKey().getId()); log.log(Level.FINE, "Copying question translations"); SurveyUtils.copyTranslation(sourceQuestionId, newQuestion .getKey().getId(), newSurveyId, newQuestionGroupId, ParentType.QUESTION_NAME, ParentType.QUESTION_DESC, ParentType.QUESTION_TEXT, ParentType.QUESTION_TIP); if (!Question.Type.OPTION.equals(newQuestion.getType())) { // Nothing more to do return newQuestion; } final TreeMap<Integer, QuestionOption> options = qoDao .listOptionByQuestion(sourceQuestionId); if (options == null) { return newQuestion; } log.log(Level.FINE, "Copying " + options.values().size() + " `QuestionOption`"); // Copying Question Options for (QuestionOption qo : options.values()) { SurveyUtils.copyQuestionOption(qo, newQuestion.getKey().getId(), newSurveyId, newQuestionGroupId); } return newQuestion; } public static QuestionOption copyQuestionOption(QuestionOption source, Long newQuestionId, Long newSurveyId, Long newQuestionGroupId) { final QuestionOptionDao qDao = new QuestionOptionDao(); final QuestionOption tmp = new QuestionOption(); BeanUtils.copyProperties(source, tmp, Constants.EXCLUDED_PROPERTIES); tmp.setQuestionId(newQuestionId); log.log(Level.INFO, "Copying `QuestionOption` " + source.getKey().getId()); final QuestionOption newQuestionOption = qDao.save(tmp); log.log(Level.INFO, "New `QuestionOption` ID: " + newQuestionOption.getKey().getId()); log.log(Level.INFO, "Copying question option translations"); SurveyUtils.copyTranslation(source.getKey().getId(), newQuestionOption .getKey().getId(), newSurveyId, newQuestionGroupId, ParentType.QUESTION_OPTION); return newQuestionOption; } public static Survey resetSurveyState(Long surveyId) { final SurveyDAO sDao = new SurveyDAO(); final Survey s = sDao.getById(surveyId); s.setStatus(Survey.Status.NOT_PUBLISHED); return sDao.save(s); } public static Survey retrieveSurvey(Long surveyId) { final SurveyDAO sDao = new SurveyDAO(); return sDao.getById(surveyId); } public static SurveyGroup retrieveSurveyGroup(Long surveyGroupId) { final SurveyGroupDAO surveyGroupDAO = new SurveyGroupDAO(); return surveyGroupDAO.getByKey(surveyGroupId); } public static String getPath(Survey s) { if (s == null) { return null; } final SurveyGroupDAO dao = new SurveyGroupDAO(); final SurveyGroup sg = dao.getByKey(s.getSurveyGroupId()); if (sg == null) { return null; } return sg.getPath() + "/" + s.getName(); } public static List<Long> retrieveAncestorIds(SecuredObject s) { List<Long> ancestorIds = new ArrayList<Long>(); if (s.getParentObject() == null) { return null; } SecuredObject parent = s.getParentObject(); if (parent.listAncestorIds() != null) { ancestorIds.addAll(parent.listAncestorIds()); } ancestorIds.add(parent.getObjectId()); // add parent id to returned ancestor list return ancestorIds; } public static String fixPath(String oldPath, String newName) { if (oldPath == null || newName == null) { return oldPath; } int idx = oldPath.lastIndexOf("/"); if (idx >= 0) { return oldPath.substring(0, idx) + "/" + newName; } return oldPath; } public static List<Translation> getTranslations(Long parentId, ParentType... types) { final List<Translation> trs = new ArrayList<Translation>(); final TranslationDao trDao = new TranslationDao(); for (ParentType pt : types) { trs.addAll(trDao.findTranslations(pt, parentId).values()); } return trs; } public static void saveTranslationCopy(List<Translation> trs, Long newParentId, Long newSurveyId, Long newQuestionGroupId) { final TranslationDao trDao = new TranslationDao(); for (Translation t : trs) { Translation copy = new Translation(); BeanUtils.copyProperties(t, copy, Constants.EXCLUDED_PROPERTIES); copy.setParentId(newParentId); copy.setQuestionGroupId(newQuestionGroupId); copy.setSurveyId(newSurveyId); trDao.save(copy); } } public static void copyTranslation(Long sourceParentId, Long copyParentId, Long newSurveyId, Long newQuestionGroupId, ParentType... types) { SurveyUtils.saveTranslationCopy( SurveyUtils.getTranslations(sourceParentId, types), copyParentId, newSurveyId, newQuestionGroupId); } /** * Sends a POST request to publish a cascade resource to a server defined by the `flowServices` * property * * @param cascadeResourceId The id of the cascade resource to publish * @return "failed" or "publishing requested", depending on the success. */ public static String publishCascade(Long cascadeResourceId) { String status = "failed"; CascadeResourceDao crDao = new CascadeResourceDao(); CascadeResource cr = crDao.getByKey(cascadeResourceId); if (cr != null) { final String flowServiceURL = PropertyUtil.getProperty("flowServices"); final String uploadUrl = PropertyUtil.getProperty("surveyuploadurl"); if (flowServiceURL == null || "".equals(flowServiceURL)) { log.log(Level.SEVERE, "Error trying to publish cascade. Check `flowServices` property"); return status; } try { final JSONObject payload = new JSONObject(); payload.put("cascadeResourceId", cascadeResourceId.toString()); payload.put("uploadUrl", uploadUrl); payload.put("version", String.valueOf(cr.getVersion() + 1)); log.log(Level.INFO, "Sending cascade publish request for cascade: " + cascadeResourceId); final String postString = payload.toString(); log.log(Level.INFO, "POSTing to: " + flowServiceURL); final String response = new String(HttpUtil.doPost(flowServiceURL + "/publish_cascade", postString, "application/json"), "UTF-8"); log.log(Level.INFO, "Response from server: " + response); status = "publish requested"; cr.setStatus(Status.PUBLISHING); crDao.save(cr); } catch (Exception e) { log.log(Level.SEVERE, "Error publishing cascade: " + e.getMessage(), e); } } return status; } /** * Sends a POST request of a collection of surveyIds to a server defined by the `flowServices` * property The property `alias` define the baseURL property that is sent in the request * * @param surveyIds Collection of ids (Long) that requires processing * @param action A string indicating the action that will be used, this string is used for * building the URL, with the `flowServices` property + / + action * @return The response from the server or null when `flowServices` is not defined, or an error * in the request happens */ public static String notifyReportService(Collection<Long> surveyIds, String action) { final String flowServiceURL = PropertyUtil.getProperty("flowServices"); final String baseURL = PropertyUtil.getProperty("alias"); if (flowServiceURL == null || "".equals(flowServiceURL)) { log.log(Level.SEVERE, "Error trying to notify server. It's not configured, check `flowServices` property"); return null; } try { final JSONObject payload = new JSONObject(); payload.put("surveyIds", surveyIds); payload.put("baseURL", (baseURL.startsWith("http") ? baseURL : "http://" + baseURL)); log.log(Level.INFO, "Sending notification (" + action + ") for surveys: " + surveyIds); final String postString = "criteria=" + URLEncoder.encode(payload.toString(), "UTF-8"); log.log(Level.FINE, "POST string: " + postString); final String response = new String(HttpUtil.doPost(flowServiceURL + "/" + action, postString), "UTF-8"); log.log(Level.INFO, "Response from server: " + response); return response; } catch (Exception e) { log.log(Level.SEVERE, "Error notifying the report service: " + e.getMessage(), e); } return null; } /** * Given the path of an object, return a list of the paths of all its parent objects * * @param objectPath the path of an object * @param includeRootPath include the root path in the list of parent paths * @return */ public static List<String> listParentPaths(String objectPath, boolean includeRootPath) { List<String> parentPaths = new ArrayList<String>(); StringBuilder path = new StringBuilder(objectPath); while (path.length() > 1) { path.delete(path.lastIndexOf("/"), path.length()); if (StringUtils.isNotBlank(path.toString())) { parentPaths.add(path.toString().trim()); } } if (includeRootPath) { parentPaths.add("/"); } return parentPaths; } /** * Copy publicly visible properties from one BaseDomain subclass to another. Should be used for * two classes of the same type. * * @param source * @param copy */ public static void shallowCopy(BaseDomain source, BaseDomain copy) { BeanUtils.copyProperties(source, copy, Constants.EXCLUDED_PROPERTIES); String kind = source.getKey().getKind(); log.log(Level.INFO, "Copying `" + kind + "` " + source.getKey().getId()); } /** * Set the non-persistent child objects of a SurveyGroup entity * * @param surveyGroup */ public static void setChildObjects(SurveyGroup surveyGroup) { if (surveyGroup == null || surveyGroup.getKey() == null) { return; } Long surveyGroupId = surveyGroup.getKey().getId(); List<SurveyGroup> childFolders = new SurveyGroupDAO().listByProjectFolderId(surveyGroupId); surveyGroup.setChildFolders(childFolders); List<Survey> childForms = new SurveyDAO().listSurveysByGroup(surveyGroupId); surveyGroup.setChildForms(childForms); } /** * to prevent collisions, it is useful to collect all ids already in use in a survey group * @param surveyId * @return */ public static Set<String> listQuestionIdsUsedInSurveyGroup(Long surveyId) { final SurveyDAO sDao = new SurveyDAO(); final QuestionDao qDao = new QuestionDao(); Set<String> idsInUse = new HashSet<>(); Survey s0 = sDao.getById(surveyId); final Long surveyGroupId = s0.getSurveyGroupId(); List<Survey> sList = sDao.listSurveysByGroup(surveyGroupId); for (Survey s : sList) { List<Question> qList = qDao.listQuestionsBySurvey(s.getKey().getId()); for (Question q : qList) { idsInUse.add(q.getQuestionId()); } } return idsInUse; } }