/* * 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 * distributed 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 the specific language governing permissions and * limitations under the License. */ package org.ngrinder.script.controller; import com.google.common.base.Predicate; import com.nhncorp.lucy.security.xss.XssPreventer; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.ngrinder.common.controller.BaseController; import org.ngrinder.common.controller.RestAPI; import org.ngrinder.common.util.EncodingUtils; import org.ngrinder.common.util.HttpContainerContext; import org.ngrinder.common.util.PathUtils; import org.ngrinder.common.util.UrlUtils; import org.ngrinder.infra.spring.RemainedPath; import org.ngrinder.model.User; import org.ngrinder.script.handler.ProjectHandler; import org.ngrinder.script.handler.ScriptHandler; import org.ngrinder.script.handler.ScriptHandlerFactory; import org.ngrinder.script.model.FileCategory; import org.ngrinder.script.model.FileEntry; import org.ngrinder.script.model.FileType; import org.ngrinder.script.service.FileEntryService; import org.ngrinder.script.service.ScriptValidationService; import org.python.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; 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.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.annotation.Nullable; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Lists.newArrayList; import static java.util.Collections.sort; import static org.apache.commons.io.FilenameUtils.getPath; import static org.ngrinder.common.util.EncodingUtils.encodePathWithUTF8; import static org.ngrinder.common.util.ExceptionUtils.processException; import static org.ngrinder.common.util.PathUtils.removePrependedSlash; import static org.ngrinder.common.util.PathUtils.trimPathSeparatorBothSides; import static org.ngrinder.common.util.Preconditions.checkNotNull; /** * FileEntry manipulation controller. * * @author JunHo Yoon * @since 3.0 */ @Controller @RequestMapping("/script") public class FileEntryController extends BaseController { private static final Logger LOG = LoggerFactory.getLogger(FileEntryController.class); @Autowired private FileEntryService fileEntryService; @Autowired private ScriptValidationService scriptValidationService; @Autowired private ScriptHandlerFactory handlerFactory; @Autowired HttpContainerContext httpContainerContext; /** * Get the list of file entries for the given user. * * @param user current user * @param path path looking for. * @param model model. * @return script/list */ @RequestMapping({"/list/**", ""}) public String getAll(User user, final @RemainedPath String path, ModelMap model) { // "fileName" model.addAttribute("files", getAllFiles(user, path)); model.addAttribute("currentPath", path); model.addAttribute("svnUrl", getSvnUrlBreadcrumbs(user, path)); model.addAttribute("handlers", handlerFactory.getVisibleHandlers()); return "script/list"; } /** * Get the SVN url BreadCrumbs HTML string. * * @param user user * @param path path * @return generated HTML */ public String getSvnUrlBreadcrumbs(User user, String path) { String contextPath = httpContainerContext.getCurrentContextUrlFromUserRequest(); String[] parts = StringUtils.split(path, '/'); StringBuilder accumulatedPart = new StringBuilder(contextPath).append("/script/list"); StringBuilder returnHtml = new StringBuilder().append("<a href='").append(accumulatedPart).append("'>") .append(contextPath).append("/svn/").append(user.getUserId()).append("</a>"); for (String each : parts) { returnHtml.append("/"); accumulatedPart.append("/").append(each); returnHtml.append("<a href='").append(accumulatedPart).append("'>").append(each).append("</a>"); } return returnHtml.toString(); } /** * Get the script path BreadCrumbs HTML string. * * @param path path * @return generated HTML */ public String getScriptPathBreadcrumbs(String path) { String contextPath = httpContainerContext.getCurrentContextUrlFromUserRequest(); String[] parts = StringUtils.split(path, '/'); StringBuilder accumulatedPart = new StringBuilder(contextPath).append("/script/list"); StringBuilder returnHtml = new StringBuilder(); for (int i = 0; i < parts.length; i++) { String each = parts[i]; accumulatedPart.append("/").append(each); if (i != parts.length - 1) { returnHtml.append("<a target='_path_view' href='").append(accumulatedPart).append("'>").append(each) .append("</a>").append("/"); } else { returnHtml.append(each); } } return returnHtml.toString(); } /** * Add a folder on the given path. * * @param user current user * @param path path in which folder will be added * @param folderName folderName * @param model model. * @return redirect:/script/list/${path} */ @RequestMapping(value = "/new/**", params = "type=folder", method = RequestMethod.POST) public String addFolder(User user, @RemainedPath String path, @RequestParam("folderName") String folderName, ModelMap model) { // "fileName" fileEntryService.addFolder(user, path, StringUtils.trimToEmpty(folderName), ""); model.clear(); return "redirect:/script/list/" + encodePathWithUTF8(path); } /** * Provide new file creation form data. * * @param user current user * @param path path in which a file will be added * @param testUrl url which the script may use * @param fileName fileName * @param scriptType Type of script. optional * @param createLibAndResources true if libs and resources should be created as well. * @param redirectAttributes redirect attributes storage * @param model model. * @return script/editor" */ @RequestMapping(value = "/new/**", params = "type=script", method = RequestMethod.POST) public String createForm(User user, @RemainedPath String path, @RequestParam(value = "testUrl", required = false) String testUrl, @RequestParam("fileName") String fileName, @RequestParam(value = "scriptType", required = false) String scriptType, @RequestParam(value = "createLibAndResource", defaultValue = "false") boolean createLibAndResources, @RequestParam(value = "options", required = false) String options, RedirectAttributes redirectAttributes, ModelMap model) { fileName = StringUtils.trimToEmpty(fileName); String name = "Test1"; if (StringUtils.isEmpty(testUrl)) { testUrl = StringUtils.defaultIfBlank(testUrl, "http://please_modify_this.com"); } else { name = UrlUtils.getHost(testUrl); } ScriptHandler scriptHandler = fileEntryService.getScriptHandler(scriptType); FileEntry entry = new FileEntry(); entry.setPath(fileName); if (scriptHandler instanceof ProjectHandler) { if (!fileEntryService.hasFileEntry(user, PathUtils.join(path, fileName))) { fileEntryService.prepareNewEntry(user, path, fileName, name, testUrl, scriptHandler, createLibAndResources, options); redirectAttributes.addFlashAttribute("message", fileName + " project is created."); return "redirect:/script/list/" + encodePathWithUTF8(path) + "/" + fileName; } else { redirectAttributes.addFlashAttribute("exception", fileName + " is already existing. Please choose the different name"); return "redirect:/script/list/" + encodePathWithUTF8(path) + "/"; } } else { String fullPath = PathUtils.join(path, fileName); if (fileEntryService.hasFileEntry(user, fullPath)) { model.addAttribute("file", fileEntryService.getOne(user, fullPath)); } else { model.addAttribute("file", fileEntryService.prepareNewEntry(user, path, fileName, name, testUrl, scriptHandler, createLibAndResources, options)); } } model.addAttribute("breadcrumbPath", getScriptPathBreadcrumbs(PathUtils.join(path, fileName))); model.addAttribute("scriptHandler", scriptHandler); model.addAttribute("createLibAndResource", createLibAndResources); return "script/editor"; } /** * Get the details of given path. * * @param user user * @param path user * @param revision revision. -1 if HEAD * @param model model * @return script/editor */ @RequestMapping("/detail/**") public String getOne(User user, @RemainedPath String path, @RequestParam(value = "r", required = false) Long revision, ModelMap model) { FileEntry script = fileEntryService.getOne(user, path, revision); if (script == null || !script.getFileType().isEditable()) { LOG.error("Error while getting file detail on {}. the file does not exist or not editable", path); model.clear(); return "redirect:/script/"; } model.addAttribute("file", script); model.addAttribute("lastRevision", script.getLastRevision()); model.addAttribute("curRevision", script.getRevision()); model.addAttribute("scriptHandler", fileEntryService.getScriptHandler(script)); model.addAttribute("ownerId", user.getUserId()); model.addAttribute("breadcrumbPath", getScriptPathBreadcrumbs(path)); return "script/editor"; } /** * Download file entry of given path. * * @param user current user * @param path user * @param response response */ @RequestMapping("/download/**") public void download(User user, @RemainedPath String path, HttpServletResponse response) { FileEntry fileEntry = fileEntryService.getOne(user, path); if (fileEntry == null) { LOG.error("{} requested to download not existing file entity {}", user.getUserId(), path); return; } response.reset(); try { response.addHeader( "Content-Disposition", "attachment;filename=" + java.net.URLEncoder.encode(FilenameUtils.getName(fileEntry.getPath()), "utf8")); } catch (UnsupportedEncodingException e1) { LOG.error(e1.getMessage(), e1); } response.setContentType("application/octet-stream; charset=UTF-8"); response.addHeader("Content-Length", "" + fileEntry.getFileSize()); byte[] buffer = new byte[4096]; ByteArrayInputStream fis = null; OutputStream toClient = null; try { fis = new ByteArrayInputStream(fileEntry.getContentBytes()); toClient = new BufferedOutputStream(response.getOutputStream()); int readLength; while (((readLength = fis.read(buffer)) != -1)) { toClient.write(buffer, 0, readLength); } } catch (IOException e) { throw processException("error while download file", e); } finally { IOUtils.closeQuietly(fis); IOUtils.closeQuietly(toClient); } } /** * Search files on the query. * * @param user current user * @param query query string * @param model model * @return script/list */ @RequestMapping(value = "/search/**") public String search(User user, @RequestParam(required = true, value = "query") final String query, ModelMap model) { final String trimmedQuery = StringUtils.trimToEmpty(query); List<FileEntry> searchResult = newArrayList(filter(fileEntryService.getAll(user), new Predicate<FileEntry>() { @Override public boolean apply(@Nullable FileEntry input) { return input != null && input.getFileType() != FileType.DIR && StringUtils.containsIgnoreCase(new File(input.getPath()).getName(), trimmedQuery); } })); model.addAttribute("query", query); model.addAttribute("files", searchResult); model.addAttribute("currentPath", ""); return "script/list"; } /** * Save a fileEntry and return to the the path. * * @param user current user * @param fileEntry file to be saved * @param targetHosts target host parameter * @param validated validated the script or not, 1 is validated, 0 is not. * @param createLibAndResource true if lib and resources should be created as well. * @param model model * @return redirect:/script/list/${basePath} */ @RequestMapping(value = "/save/**", method = RequestMethod.POST) public String save(User user, FileEntry fileEntry, @RequestParam String targetHosts, @RequestParam(defaultValue = "0") String validated, @RequestParam(defaultValue = "false") boolean createLibAndResource, ModelMap model) { if (fileEntry.getFileType().getFileCategory() == FileCategory.SCRIPT) { Map<String, String> map = Maps.newHashMap(); map.put("validated", validated); map.put("targetHosts", StringUtils.trim(targetHosts)); fileEntry.setProperties(map); } fileEntryService.save(user, fileEntry); String basePath = getPath(fileEntry.getPath()); if (createLibAndResource) { fileEntryService.addFolder(user, basePath, "lib", getMessages("script.commit.libFolder")); fileEntryService.addFolder(user, basePath, "resources", getMessages("script.commit.resourceFolder")); } model.clear(); return "redirect:/script/list/" + encodePathWithUTF8(basePath); } /** * Upload a file. * * @param user current user * @param path path * @param description description * @param file multi part file * @param model model * @return redirect:/script/list/${path} */ @RequestMapping(value = "/upload/**", method = RequestMethod.POST) public String upload(User user, @RemainedPath String path, @RequestParam("description") String description, @RequestParam("uploadFile") MultipartFile file, ModelMap model) { try { description = XssPreventer.escape(description); upload(user, path, description, file); model.clear(); return "redirect:/script/list/" + encodePathWithUTF8(path); } catch (IOException e) { LOG.error("Error while getting file content: {}", e.getMessage(), e); throw processException("Error while getting file content:" + e.getMessage(), e); } } private void upload(User user, String path, String description, MultipartFile file) throws IOException { FileEntry fileEntry = new FileEntry(); fileEntry.setContentBytes(file.getBytes()); fileEntry.setDescription(description); fileEntry.setPath(FilenameUtils.separatorsToUnix(FilenameUtils.concat(path, file.getOriginalFilename()))); fileEntryService.save(user, fileEntry); } /** * Delete files on the given path. * * @param user user * @param path base path * @param filesString file list delimited by "," * @return json string */ @RequestMapping(value = "/delete/**", method = RequestMethod.POST) @ResponseBody public String delete(User user, @RemainedPath String path, @RequestParam("filesString") String filesString) { String[] files = filesString.split(","); fileEntryService.delete(user, path, files); Map<String, Object> rtnMap = new HashMap<String, Object>(1); rtnMap.put(JSON_SUCCESS, true); return toJson(rtnMap); } /** * Create the given file. * * @param user user * @param fileEntry file entry * @return success json string */ @RestAPI @RequestMapping(value = {"/api/", "/api"}, method = RequestMethod.POST) public HttpEntity<String> create(User user, FileEntry fileEntry) { fileEntryService.save(user, fileEntry); return successJsonHttpEntity(); } /** * Create the given file. * * @param user user * @param path path * @param description description * @param file multi part file * @return success json string */ @RestAPI @RequestMapping(value = "/api/**", params = "action=upload", method = RequestMethod.POST) public HttpEntity<String> uploadForAPI(User user, @RemainedPath String path, @RequestParam("description") String description, @RequestParam("uploadFile") MultipartFile file) throws IOException { upload(user, path, description, file); return successJsonHttpEntity(); } /** * Check the file by given path. * * @param user user * @param path path * @return json string */ @RestAPI @RequestMapping(value = "/api/**", params = "action=view", method = RequestMethod.GET) public HttpEntity<String> viewOne(User user, @RemainedPath String path) { FileEntry fileEntry = fileEntryService.getOne(user, path, -1L); return toJsonHttpEntity(checkNotNull(fileEntry , "%s file is not viewable", path)); } /** * Get all files which belongs to given user. * * @param user user * @return json string */ @RestAPI @RequestMapping(value = {"/api/**", "/api/", "/api"}, params = "action=all", method = RequestMethod.GET) public HttpEntity<String> getAll(User user) { return toJsonHttpEntity(fileEntryService.getAll(user)); } /** * Get all files which belongs to given user and path. * * @param user user * @param path path * @return json string */ @RestAPI @RequestMapping(value = {"/api/**", "/api/", "/api"}, method = RequestMethod.GET) public HttpEntity<String> getAll(User user, @RemainedPath String path) { return toJsonHttpEntity(getAllFiles(user, path)); } private List<FileEntry> getAllFiles(User user, String path) { final String trimmedPath = StringUtils.trimToEmpty(path); List<FileEntry> files = newArrayList(filter(fileEntryService.getAll(user), new Predicate<FileEntry>() { @Override public boolean apply(@Nullable FileEntry input) { return input != null && trimPathSeparatorBothSides(getPath(input.getPath())).equals(trimmedPath); } })); sort(files, new Comparator<FileEntry>() { @Override public int compare(FileEntry o1, FileEntry o2) { if (o1.getFileType() == FileType.DIR && o2.getFileType() != FileType.DIR) { return -1; } return (o1.getFileName().compareTo(o2.getFileName())); } }); for (FileEntry each : files) { each.setPath(removePrependedSlash(each.getPath())); } return files; } /** * Delete file by given user and path. * * @param user user * @param path path * @return json string */ @RestAPI @RequestMapping(value = "/api/**", method = RequestMethod.DELETE) public HttpEntity<String> deleteOne(User user, @RemainedPath String path) { fileEntryService.delete(user, path); return successJsonHttpEntity(); } /** * Validate the script. * * @param user current user * @param fileEntry fileEntry * @param hostString hostString * @return validation Result string */ @RequestMapping(value = "/api/validate", method = RequestMethod.POST) @RestAPI public HttpEntity<String> validate(User user, FileEntry fileEntry, @RequestParam(value = "hostString", required = false) String hostString) { fileEntry.setCreatedUser(user); return toJsonHttpEntity(scriptValidationService.validate(user, fileEntry, false, hostString)); } }