/*
* Copyright 2014, Red Hat, Inc. and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.zanata.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.persistence.OptimisticLockException;
import javax.persistence.PersistenceException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Response;
import com.google.common.base.Optional;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.hibernate.StaleStateException;
import org.hibernate.exception.ConstraintViolationException;
import org.jboss.seam.servlet.ContextualHttpServletRequest;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zanata.action.AuthenticatedAccountHome;
import org.zanata.file.GlobalDocumentId;
import org.zanata.file.SourceDocumentUpload;
import org.zanata.file.UserFileUploadTracker;
import org.zanata.rest.DocumentFileUploadForm;
import org.zanata.rest.dto.ChunkUploadResponse;
import org.zanata.service.TranslationFileService;
import org.zanata.service.impl.TranslationFileServiceImpl;
import org.zanata.util.ServiceLocator;
import static com.google.common.base.Strings.emptyToNull;
/**
* Endpoint for upload dialogs using multi-file upload forms.
*
* Use GET on the endpoint to check that upload is acceptable, including whether
* the user is signed in and whether they already have an upload in-progress
* in a separate tab.
*/
@Slf4j
public class MultiFileUploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
public void init(ServletConfig config) {
}
/**
* Use GET to check the endpoint before doing a POST.
*
* This allows the browser to check that the upload is allowed before attempting an upload.
*/
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
new ContextualHttpServletRequest(request) {
@Override
public void process() throws Exception {
respondWithUploadAvailability(response);
}
}.run();
}
/**
* Indicate in response any errors that would prevent upload to this endpoint.
*
* @param response to respond with error or success JSON
*/
private void respondWithUploadAvailability(HttpServletResponse response) throws IOException {
Optional<String> reason = getCannotUploadReason();
String responseBody = reason.isPresent()
? "{ \"error\": \"" + reason.get() + "\" }"
: "{ \"success\": \"ok to upload\" }";
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
writer.write(responseBody);
writer.close();
}
/**
* @return the reason that the user cannot upload at this time. Reason will
* be absent if the user is able to upload.
*/
private Optional<String> getCannotUploadReason() {
Optional<Long> accountId = getAccountId();
boolean loggedIn = accountId.isPresent();
if (loggedIn) {
UserFileUploadTracker tracker = ServiceLocator.instance().getInstance(
UserFileUploadTracker.class);
boolean alreadyUploading = tracker.isUserUploading(accountId.get());
if (alreadyUploading) {
return Optional.of("already uploading");
}
} else {
return Optional.of("not logged in");
}
return Optional.absent();
}
private Optional<Long> getAccountId() {
AuthenticatedAccountHome accountHome = ServiceLocator.instance().getInstance(
AuthenticatedAccountHome.class);
return Optional.fromNullable((Long) accountHome.getId());
}
@Override
protected void doPost(final HttpServletRequest request,
final HttpServletResponse response) throws ServletException,
IOException {
new ContextualHttpServletRequest(request) {
@Override
public void process() throws Exception {
processPost(request, response);
}
}.run();
}
/**
* Ensure the request is a Multipart request.
*
* Initiates processing if the request is multipart, otherwise respond with
* an error.
*/
private void processPost(HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (ServletFileUpload.isMultipartContent(request)) {
registerForUploadAndProcessMultipartPost(request, response);
} else {
log.error("File upload received non-multipart request");
response.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE,
"Unsupported request type. File upload supports only multipart requests.");
}
}
/**
* Ensure that upload is allowed at this time, then initiate processing of
* the upload request.
*
* This method is responsible for making sure that a user only has one active
* upload at a time.
*/
private void registerForUploadAndProcessMultipartPost(HttpServletRequest request,
HttpServletResponse response) throws IOException {
// TODO at this point the server will expect a response per-file. The simplistic error responses may not be handled well
Optional<Long> accountId = getAccountId();
if (accountId.isPresent()) {
UserFileUploadTracker tracker = ServiceLocator.instance().getInstance(
UserFileUploadTracker.class);
boolean registeredForUpload = tracker.tryToRegisterUserForFileUpload(accountId.get());
if (!registeredForUpload) {
log.error("User with id {} is already uploading something.", accountId.get());
respondWithError(response, "already uploading");
} else {
try {
processMultipartPost(request, response);
} finally {
tracker.deRegisterUserForFileUpload(accountId.get());
}
}
} else {
log.error("User attempted upload when not logged in.");
respondWithError(response, "not logged in");
}
}
private void respondWithError(HttpServletResponse response, String error) throws IOException {
JSONObject responseObject = new JSONObject();
try {
responseObject.put("error", "upload failed: " + error);
} catch (JSONException e) {
log.error("Error while generating JSON", e);
}
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
writer.write(responseObject.toString());
writer.close();
}
private void processMultipartPost(HttpServletRequest request,
HttpServletResponse response) throws IOException {
FileUploadRequestHandler uploadRequestHandler = new FileUploadRequestHandler(request);
JSONArray filesJson;
try {
filesJson = uploadRequestHandler.process();
} catch (FileUploadException e) {
respondWithError(response, "upload failed: " + e.getMessage());
return;
}
respondWithFiles(response, filesJson);
}
private void respondWithFiles(HttpServletResponse response, JSONArray filesJson) throws IOException {
JSONObject responseObject = new JSONObject();
try {
responseObject.put("files", filesJson);
} catch (JSONException e) {
log.error("error adding files list to JSON", e);
}
String responseString = responseObject.toString();
log.info("response string: " + responseString);
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
writer.write(responseString);
writer.close();
}
private class FileUploadRequestHandler {
private final HttpServletRequest request;
private String projectSlug;
private String versionSlug;
private String path = "";
private String lang = "en-US";
private String fileParams = "";
private SourceDocumentUpload sourceUploader;
private TranslationFileService translationFileServiceImpl;
public FileUploadRequestHandler(HttpServletRequest request) {
this.request = request;
projectSlug = request.getParameter("p");
versionSlug = request.getParameter("v");
sourceUploader = ServiceLocator.instance().getInstance(SourceDocumentUpload.class);
translationFileServiceImpl = ServiceLocator.instance().getInstance(
TranslationFileServiceImpl.class);
}
public JSONArray process() throws FileUploadException {
// FIXME fail with error?
if (translationFileServiceImpl == null) {
log.error("translationFileServiceImpl is null");
}
List<FileItem> items = getRequestItems();
JSONArray filesJson = processFilesFromItems(items);
return filesJson;
}
private List<FileItem> getRequestItems() throws FileUploadException {
List<FileItem> items;// Create a factory for disk-based file items
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload uploadHandler = new ServletFileUpload(factory);
items = uploadHandler.parseRequest(request);
return items;
}
private JSONArray processFilesFromItems(List<FileItem> items) {
// parameters are required before processing files
recordParametersFromItems(items);
JSONArray filesJson = new JSONArray();
for (FileItem item : items) {
if (!item.isFormField()) {
JSONObject jsono = processFileItem(item);
filesJson.put(jsono);
}
}
return filesJson;
}
/**
* Must be called before processing any file items.
*
* @param items all items from multipart request.
*/
private void recordParametersFromItems(List<FileItem> items) {
// Make sure params are available before processing files
for (FileItem item : items) {
if (item.isFormField()) {
String field = item.getFieldName();
String value = item.getString();
if (field.equals("filepath")) {
this.path = value;
} else if (field.equals("filelang")) {
this.lang = value;
} else if (field.equals("fileparams")) {
this.fileParams = value;
}
}
}
}
/**
* Process upload of a single file item from a multipart request.
*
* @return JSON summary of outcome of the attempt.
*/
private JSONObject processFileItem(FileItem item) {
String docId = translationFileServiceImpl.generateDocId(path, item.getName());
GlobalDocumentId id = new GlobalDocumentId(projectSlug, versionSlug, docId);
Optional<String> errorMessage;
Optional<String> successMessage = Optional.absent();
Optional<String> concurrentUploadError = Optional.of("failed: someone else is already uploading this file");
try {
DocumentFileUploadForm form = createUploadFormForItem(item);
Response response = sourceUploader.tryUploadSourceFileWithoutHash(id, form);
ChunkUploadResponse responseEntity = (ChunkUploadResponse) response.getEntity();
errorMessage = optionalStringEmptyIsAbsent(responseEntity.getErrorMessage());
successMessage = optionalStringEmptyIsAbsent(responseEntity.getSuccessMessage());
} catch (IOException e) {
errorMessage = Optional.of("could not access file data");
} catch (OptimisticLockException e) {
errorMessage = concurrentUploadError;
} catch (StaleStateException e) {
// this happens in the same circumstances as OptimisticLockException
// but is thrown because we are using hibernate directly rather than
// through JPA.
errorMessage = concurrentUploadError;
} catch (ConstraintViolationException e) {
errorMessage = concurrentUploadError;
} catch (PersistenceException e) {
errorMessage = Optional.of("Timed out: failed because the file took too long to process");
}
return createJSONInfo(item, docId, errorMessage, successMessage);
}
/**
* Create JSON summary of outcome of an attempt to process an item.
*/
private JSONObject createJSONInfo(FileItem item, String docId, Optional<String> error, Optional<String> success) {
JSONObject jsono = new JSONObject();
try {
jsono.put("name", docId);
jsono.put("size", item.getSize());
if (error.isPresent()) {
if (error.get().equals("Valid combination of username and api-key for this server were not included in the request.")) {
error = Optional.of("not logged in");
}
jsono.put("error", error.get());
} else {
if (success.isPresent()) {
jsono.put("message", success.get());
}
// TODO could provide REST URL for this file
// jsono.put("url", "upload?getfile=" + item.getName());
}
} catch (JSONException e) {
log.error("Error while generating JSON", e);
}
return jsono;
}
/**
* Create the upload form required by sourceUploader for file upload.
*
* @throws IOException if the input stream cannot be opened for the file data.
*/
private DocumentFileUploadForm createUploadFormForItem(FileItem item) throws IOException {
DocumentFileUploadForm form = new DocumentFileUploadForm();
form.setAdapterParams(fileParams);
form.setFirst(true);
form.setLast(true);
form.setSize(item.getSize());
form.setFileType(translationFileServiceImpl.extractExtension(item.getName()));
form.setFileStream(item.getInputStream());
return form;
}
/**
* @return absent if the given String is null or empty, otherwise an
* Option containing the given String.
*/
private Optional<String> optionalStringEmptyIsAbsent(String value) {
return(Optional.fromNullable(emptyToNull(value)));
}
}
}