/* * Copyright (C) 2010-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 org.waterforpeople.mapping.app.web; import com.gallatinsystems.common.util.MailUtil; import com.gallatinsystems.common.util.PropertyUtil; import com.gallatinsystems.common.util.S3Util; import com.gallatinsystems.common.util.ZipUtil; import com.gallatinsystems.framework.rest.AbstractRestApiServlet; import com.gallatinsystems.framework.rest.RestRequest; import com.gallatinsystems.framework.rest.RestResponse; import com.gallatinsystems.survey.dao.CascadeResourceDao; import com.gallatinsystems.survey.dao.QuestionDao; import com.gallatinsystems.survey.dao.SurveyDAO; import com.gallatinsystems.survey.domain.CascadeResource; import com.gallatinsystems.survey.domain.Question; import com.gallatinsystems.survey.domain.Survey; import com.google.gdata.util.common.base.Nullable; import com.google.gdata.util.common.base.StringUtil; import org.apache.commons.io.IOUtils; import org.waterforpeople.mapping.app.web.dto.BootstrapGeneratorRequest; import javax.servlet.http.HttpServletRequest; import java.io.*; import java.net.URLConnection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import static com.gallatinsystems.common.Constants.DEFAULT_SURVEY_FILE_NAME; /** * downloads publishes survey xml files and forms a zip that conforms to the structure of the * bootstrap.zip file expected by the device * * @author Christopher Fagiani */ public class BootstrapGeneratorServlet extends AbstractRestApiServlet { private static final Logger log = Logger.getLogger(BootstrapGeneratorServlet.class.getName()); private static final long serialVersionUID = -6645180848307957119L; private static final String DB_INST_ENTRY = "dbinstructions.sql"; private static final String BOOTSTRAP_UPLOAD_DIR = "bootstrapdir"; private static final String EMAIL_FROM_ADDRESS_KEY = "emailFromAddress"; private static final String EMAIL_SUB = "FLOW Bootstrap File"; private static final String EMAIL_BODY = "Click the link to download the bootstrap file"; private static final String ERROR_BODY = "There were errors while attempting to generate the bootstrap file:"; private static final String FILENAME_REGEX = "[^a-zA-Z0-9_]"; private static final String FILENAME_REPLACEMENT = "_"; private static final int FILENAME_MAX_SIZE = 127; public static final String EXCESS_UNDERSCORE = "__"; private SurveyDAO surveyDao; private CascadeResourceDao cascadeDao; private String bucketName; private String keyPrefix; public BootstrapGeneratorServlet() { super(); surveyDao = new SurveyDAO(); cascadeDao = new CascadeResourceDao(); bucketName = PropertyUtil.getProperty("s3bucket"); keyPrefix = PropertyUtil.getProperty("surveyuploaddir"); } @Override protected RestRequest convertRequest() throws Exception { HttpServletRequest req = getRequest(); RestRequest restRequest = new BootstrapGeneratorRequest(); restRequest.populateFromHttpRequest(req); return restRequest; } @Override protected RestResponse handleRequest(RestRequest req) throws Exception { RestResponse response = new RestResponse(); BootstrapGeneratorRequest bootStrapReq = (BootstrapGeneratorRequest) req; if (BootstrapGeneratorRequest.GEN_ACTION.equalsIgnoreCase(bootStrapReq .getAction())) { generateFile(bootStrapReq); } return response; } @Override protected void writeOkResponse(RestResponse resp) throws Exception { // no-op } private void generateFile(BootstrapGeneratorRequest req) { Map<String, String> contentMap = new HashMap<String, String>(); Set<String> resourcesSet = new HashSet<String>(); StringBuilder errors = new StringBuilder(); if (req.getSurveyIds() != null) { for (Long id : req.getSurveyIds()) { try { Survey s = surveyDao.getById(id); String surveyFilename = generateSanitizedFilename(s.getName()); StringBuilder buf = new StringBuilder(); URLConnection conn = S3Util.getConnection(bucketName, keyPrefix + "/" + s.getKey().getId() + ".xml"); BufferedReader reader = new BufferedReader( new InputStreamReader(conn.getInputStream(), "UTF-8")); String line; while ((line = reader.readLine()) != null) { buf.append(line).append("\n"); } reader.close(); contentMap.put(s.getKey().getId() + "/" + surveyFilename + ".xml", buf.toString()); resourcesSet.addAll(getSurveyResources(id));// Add survey resources } catch (Exception e) { log.log(Level.SEVERE, "Could not include survey id " + id + "\n", e); errors.append("Could not include survey id " + id + "\n"); } } } if (req.getDbInstructions() != null && req.getDbInstructions().trim().length() > 0) { contentMap.put(DB_INST_ENTRY, req.getDbInstructions().trim()); } String filename = System.currentTimeMillis() + "-bs.zip"; String objectKey = PropertyUtil.getProperty(BOOTSTRAP_UPLOAD_DIR) + "/" + filename; try { Map<String, byte[]> resources = fetchResources(resourcesSet); ByteArrayOutputStream os = ZipUtil.generateZip(contentMap, resources); S3Util.put(bucketName, objectKey, os.toByteArray(), "application/zip", false); } catch (Exception e) { log.log(Level.SEVERE, "Error uploading bootstrap file: " + e.getMessage(), e); return; // skip the email } String body = EMAIL_BODY; if (errors.toString().trim().length() > 0) { body = ERROR_BODY + "\n\n" + errors.toString(); } else { body += "\n\n" + S3Util.getBrowserLink(bucketName, objectKey); } MailUtil.sendMail(PropertyUtil.getProperty(EMAIL_FROM_ADDRESS_KEY), "FLOW", req.getEmail(), EMAIL_SUB, body); } /** * Uses name to generate a safe string to use as filename, removing any unsafe chars * @param name * @return */ private String generateSanitizedFilename(@Nullable String name) { String filename; if (StringUtil.isEmpty(name)) { filename = DEFAULT_SURVEY_FILE_NAME; } else { filename = name.trim().replaceAll(FILENAME_REGEX, FILENAME_REPLACEMENT); int maxLength = Math.min(FILENAME_MAX_SIZE, filename.length()); filename = filename.substring(0, maxLength); //make sure we do not have multiple underscores filename = filename.replaceAll(EXCESS_UNDERSCORE, FILENAME_REPLACEMENT); } return filename; } private Set<String> getSurveyResources(Long surveyId) { Set<String> resources = new HashSet<String>(); for (Question q : new QuestionDao().listQuestionsInOrder(surveyId, Question.Type.CASCADE)) { Long cascadeResourceId = q.getCascadeResourceId(); if (cascadeResourceId != null) { CascadeResource cr = cascadeDao.getByKey(cascadeResourceId); if (cr != null) { String resName = cr.getResourceId() + ".zip"; resources.add(resName); } } } return resources; } private Map<String, byte[]> fetchResources(Set<String> resources) throws IOException { Map<String, byte[]> resData = new HashMap<String, byte[]>(); for (String resource : resources) { byte[] data = fetchResource(resource); resData.put(resource, data); } return resData; } private byte[] fetchResource(String res) throws IOException { URLConnection conn = null; InputStream in = null; ByteArrayOutputStream out = null; try { conn = S3Util.getConnection(bucketName, keyPrefix + "/" + res); out = new ByteArrayOutputStream(); in = conn.getInputStream(); IOUtils.copy(in, out); out.flush(); return out.toByteArray(); } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly(out); } } }