// Copyright 2015 The Project Buendia Authors // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at: http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software distrib- // uted under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES // OR CONDITIONS OF ANY KIND, either express or implied. See the License for // specific language governing permissions and limitations under the License. package org.projectbuendia.openmrs.web.controller; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.api.context.Context; import org.openmrs.projectbuendia.webservices.rest.GlobalProperties; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.RedirectView; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** The controller for the profile management page. */ @Controller public class ProfileManager { protected static Log log = LogFactory.getLog(ProfileManager.class); private final String PROFILE_DIR_PATH = "/usr/share/buendia/profiles"; private final String VALIDATE_CMD = "buendia-profile-validate"; private final String APPLY_CMD = "buendia-profile-apply"; private File profileDir; /** This is executed every time a request is made and determines the profileDir to be used. */ @ModelAttribute public void getProfileDir() { Properties config = Context.getRuntimeProperties(); String configProfileDir = config.getProperty("profile_manager.profile_dir", PROFILE_DIR_PATH); profileDir = new File(configProfileDir); } @RequestMapping(value = "/module/projectbuendia/openmrs/profiles", method = RequestMethod.GET) public void get(HttpServletRequest request, ModelMap model) { String currentProfile = getCurrentProfile(); model.addAttribute("currentProfile", currentProfile); model.addAttribute("authorized", authorized()); model = queryParamsToModelAttr(request, model); if (!profileDir.exists()) { model.addAttribute("success", false); model.addAttribute("message", "The Profile Manager directory does not exist. It must reside on " + "/usr/share/buendia/profiles or on a directory specified in openmrs-runtime.properties " + "file under profile_manager.profile_dir directive."); } else { model.addAttribute("profiles", listProfiles()); } } /** This method sends data to jsp in a more consistent way. */ private ModelMap queryParamsToModelAttr(HttpServletRequest request, ModelMap model) { Map params = request.getParameterMap(); Iterator paramIterator = params.entrySet().iterator(); while (paramIterator.hasNext()) { Entry param = (Entry) paramIterator.next(); String key = (String) param.getKey(); String[] value = (String[]) param.getValue(); model.addAttribute(key, value[0]); } return model; } private List<FileInfo> listProfiles() { List<FileInfo> files = new ArrayList<>(); File[] profiles = profileDir.listFiles(); if (profiles != null) { for (File file : profiles) { files.add(new FileInfo(file)); } Collections.sort(files, new Comparator<FileInfo>() { public int compare(FileInfo a, FileInfo b) { return -a.modified.compareTo(b.modified); } }); } return files; } public static boolean authorized() { return Context.hasPrivilege("Manage Concepts") && Context.hasPrivilege("Manage Forms"); } @RequestMapping(value = "/module/projectbuendia/openmrs/profiles", method = RequestMethod.POST) public View post(HttpServletRequest request, HttpServletResponse response, ModelMap model) { if (!authorized()) { return new RedirectView("profiles.form"); } if (request instanceof MultipartHttpServletRequest) { addProfile((MultipartHttpServletRequest) request, model); } else { String filename = request.getParameter("profile"); String op = request.getParameter("op"); if (filename != null) { File file = new File(profileDir, filename); if (file.isFile()) { model.addAttribute("filename", filename); if ("Apply".equals(op)) { applyProfile(file, model); } else if ("Download".equals(op)) { downloadProfile(file, response); return null; // download the file, don't redirect } else if ("Delete".equals(op)) { deleteProfile(file, model); } } } } return new RedirectView("profiles.form"); // reload this page with a GET request } /** Chooses a filename based on the given name, with a "-vN" suffix appended for uniqueness. */ private String getNextVersionedFilename(String name) { // Separate the name into a sanitized base name and an extension. name = sanitizeName(name); String ext = ""; int dot = name.lastIndexOf('.'); if (dot > 0) { ext = name.substring(dot); name = name.substring(0, dot); } // Find the highest version number among all existing files named like "name-vN.ext". // If "name.ext" exists, it counts as version 1. String prefix = name + "-v"; int highestVersion = 0; for (File file : profileDir.listFiles()) { int version = 0; String n = file.getName(); if (n.equals(name + ext)) { version = 1; } else if (n.startsWith(prefix) && n.endsWith(ext)) { try { version = Integer.parseInt( n.substring(prefix.length(), n.length() - ext.length())); } catch (NumberFormatException e) { } } highestVersion = Math.max(version, highestVersion); } // Generate a unique new name, adding the next higher version number if necessary. if (highestVersion == 0) { return name + ext; } else { return prefix + (highestVersion + 1) + ext; } } /** Handles an uploaded profile. */ private void addProfile(MultipartHttpServletRequest request, ModelMap model) { List<String> lines = new ArrayList<>(); MultipartFile mpf = request.getFile("file"); if (mpf != null) { String message = ""; String filename = mpf.getOriginalFilename(); try { File tempFile = File.createTempFile("profile", null); mpf.transferTo(tempFile); boolean exResult = execute(VALIDATE_CMD, tempFile, lines); if (exResult) { filename = getNextVersionedFilename(filename); File newFile = new File(profileDir, filename); FileUtils.moveFile(tempFile, newFile); model.addAttribute("success", true); message = "Success adding profile: "; } else { model.addAttribute("success", false); message = "Error adding profile: "; } } catch (Exception e) { model.addAttribute("success", false); message = "Problem saving uploaded profile: "; lines.add(e.getMessage()); log.error("Problem saving uploaded profile", e); } finally { model.addAttribute("operation", "add"); model.addAttribute("message", message + filename); model.addAttribute("filename", filename); model.addAttribute("output", StringUtils.join(lines, "\n")); } } } /** Applies a profile to the OpenMRS database. */ private void applyProfile(File file, ModelMap model) { List<String> lines = new ArrayList<>(); if (execute(APPLY_CMD, file, lines)) { setCurrentProfile(file.getName()); model.addAttribute("success", true); model.addAttribute("message", "Success applying profile: " + file.getName()); } else { model.addAttribute("success", false); model.addAttribute("message", "Error applying profile: " + file.getName()); model.addAttribute("output", StringUtils.join(lines, "\n")); } } /** Downloads a profile. */ private void downloadProfile(File file, HttpServletResponse response) { response.setContentType("application/octet-stream"); response.setHeader( "Content-Disposition", "attachment; filename=\"" + file.getName() + "\""); try { response.getOutputStream().write( Files.readAllBytes(Paths.get(file.getAbsolutePath()))); } catch (IOException e) { log.error("Error downloading profile: " + file.getName(), e); } } /** Deletes a profile. */ private void deleteProfile(File file, ModelMap model) { if (file.getName().equals(getCurrentProfile())) { model.addAttribute("success", false); model.addAttribute("message", "Cannot delete the currently active profile."); } else if (file.delete()) { model.addAttribute("success", true); model.addAttribute("message", "Success deleting profile: " + file.getName()); } else { model.addAttribute("success", false); model.addAttribute("message", "Error deleting profile: " + file.getName()); log.error("Error deleting profile: " + file.getName()); } } /** * Executes a command with one argument, returning true if the command succeeds. * Gathers the output from stdout and stderr into the provided list of lines. */ private boolean execute(String command, File arg, List<String> lines) { ProcessBuilder pb = new ProcessBuilder(command, arg.getAbsolutePath()); pb.redirectErrorStream(true); // redirect stderr to stdout try { Process proc = pb.start(); BufferedReader reader = new BufferedReader( new InputStreamReader(proc.getInputStream())); String line; while ((line = reader.readLine()) != null) { lines.add(line); } proc.waitFor(); return proc.exitValue() == 0; } catch (Exception e) { log.error("Exception while executing: " + command + " " + arg, e); lines.add(e.getMessage()); return false; } } /** Sanitizes a string to produce a safe filename. */ private String sanitizeName(String filename) { String[] parts = filename.split("/"); return parts[parts.length - 1].replaceAll("[^A-Za-z0-9._-]", " ").replaceAll(" +", " "); } /** Gets the global property for the name of the current profile. */ private String getCurrentProfile() { return Context.getAdministrationService().getGlobalProperty( GlobalProperties.CURRENT_PROFILE); } /** Sets the global property for the name of the current profile. */ private void setCurrentProfile(String name) { Context.getAdministrationService().setGlobalProperty( GlobalProperties.CURRENT_PROFILE, name); } public class FileInfo { String name; Long size; Date modified; public FileInfo(File file) { name = file.getName(); size = file.length(); modified = new Date(file.lastModified()); } public String getName() { return name; } public String getFormattedName() { return (name + " ").substring(0, 30); } public Long getSize() { return size; } public String getFormattedSize() { return String.format("%7d", size); } public Date getModified() { return modified; } } }