package org.sigmah.server.servlet;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import com.google.gwt.http.client.Response;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.NoSuchFileException;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.sigmah.client.page.RequestParameter;
import org.sigmah.client.util.ClientUtils;
import org.sigmah.server.dao.FileDAO;
import org.sigmah.server.dao.MonitoredPointDAO;
import org.sigmah.server.dao.OrganizationDAO;
import org.sigmah.server.dao.ProjectDAO;
import org.sigmah.server.domain.Organization;
import org.sigmah.server.domain.Project;
import org.sigmah.server.domain.reminder.MonitoredPoint;
import org.sigmah.server.domain.reminder.MonitoredPointList;
import org.sigmah.server.domain.value.FileVersion;
import org.sigmah.server.file.BackupArchiveManager;
import org.sigmah.server.file.FileStorageProvider;
import org.sigmah.server.file.LogoManager;
import org.sigmah.server.file.util.MultipartRequest;
import org.sigmah.server.file.util.MultipartRequestCallback;
import org.sigmah.server.handler.util.Conflicts;
import org.sigmah.server.service.util.ImageMinimizer;
import org.sigmah.server.servlet.base.AbstractServlet;
import org.sigmah.server.servlet.base.ServletExecutionContext;
import org.sigmah.server.servlet.base.StatusServletException;
import org.sigmah.server.servlet.util.ResponseHelper;
import org.sigmah.shared.dto.reminder.MonitoredPointDTO;
import org.sigmah.shared.dto.value.FileUploadUtils;
import org.sigmah.shared.dto.value.FileVersionDTO;
import org.sigmah.shared.servlet.FileUploadResponse;
import org.sigmah.shared.servlet.ServletConstants.ServletMethod;
import org.sigmah.shared.util.FileType;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* File upload and download servlet.
*
* @author Denis Colliot (dcolliot@ideia.fr)
*/
@Singleton
public class FileServlet extends AbstractServlet {
/**
* Serial version UID.
*/
private static final long serialVersionUID = -8126580127468427311L;
/**
* Logger.
*/
private static final Logger LOG = LoggerFactory.getLogger(FileServlet.class);
/**
* Injected application {@link FileStorageProvider}.
*/
@Inject
private FileStorageProvider fileStorageProvider;
/**
* Injected application {@link LogoManager}.
*/
@Inject
private LogoManager logoManager;
/**
* Injected {@link OrganizationDAO}.
*/
@Inject
private OrganizationDAO organizationDAO;
/**
* Injected {@link FileDAO}.
*/
@Inject
private FileDAO fileDAO;
/**
* Injected {@link BackupArchiveManager}.
*/
@Inject
private BackupArchiveManager backupArchiveManager;
/**
* Injected {@link ProjectDAO}.
*/
@Inject
private ProjectDAO projectDAO;
/**
* Injected {@link MonitoredPointDAO}.
*/
@Inject
private MonitoredPointDAO monitoredPointDAO;
/**
* Injected {@link Conflicts}.
*/
@Inject
private Conflicts conflicts;
@Inject
private ImageMinimizer imageMinimizer;
// ---------------------------------------------------------------------------------------
//
// DOWNLOAD METHODS.
//
// ---------------------------------------------------------------------------------------
/**
* See {@link ServletMethod#DOWNLOAD_LOGO} for JavaDoc.
*
* @param request
* The HTTP request containing the file id parameter.
* @param response
* The HTTP response on which the file content is written.
* @param context
* The execution context.
* @throws Exception
* If an error occurs during process.
*/
protected void downloadLogo(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {
// Retrieves the file id.
final String id = getParameter(request, RequestParameter.ID, false);
if (LOG.isDebugEnabled()) {
LOG.debug("Downloads logo with id '{}'.", id);
}
try {
downloadBase64(id, fileStorageProvider.open(id), response);
} catch (final NoSuchFileException e) {
if (LOG.isInfoEnabled()) {
LOG.info("No logo found for id '" + id + "'.", e);
}
throw new StatusServletException(Response.SC_NOT_FOUND, e);
}
}
/**
* See {@link ServletMethod#DOWNLOAD_FILE} for JavaDoc.
*
* @param request
* The HTTP request containing the file id parameter.
* @param response
* The HTTP response on which the file content is written.
* @param context
* The execution context.
* @throws Exception
* If an error occurs during process.
*/
protected void downloadFile(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {
// Retrieves the file version id.
final Integer fileVersionId = getIntegerParameter(request, RequestParameter.ID, false);
LOG.debug("Downloads file with version id '{}'.", fileVersionId);
try {
final FileVersion version = fileDAO.getVersion(fileVersionId);
final String name = version.getName() + '.' + version.getExtension();
final String path = version.getPath();
download(path, name, fileStorageProvider.open(path), response);
} catch (final NoSuchFileException e) {
LOG.info("No file found for version id '" + fileVersionId + "'.", e);
throw new StatusServletException(Response.SC_NOT_FOUND, e);
}
}
/**
* See {@link ServletMethod#DOWNLOAD_ARCHIVE} for JavaDoc.
*
* @param request
* The HTTP request containing the file id parameter.
* @param response
* The HTTP response on which the file content is written.
* @param context
* The execution context.
* @throws Exception
* If an error occurs during process.
*/
protected void downloadArchive(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {
// Retrieves the file id.
final String id = getParameter(request, RequestParameter.ID, false);
if (LOG.isDebugEnabled()) {
LOG.debug("Downloads archive with id '{}'.", id);
}
try {
download(id, backupArchiveManager.open(id), response);
} catch (final NoSuchFileException e) {
if (LOG.isInfoEnabled()) {
LOG.info("No archive found for id '" + id + "'.", e);
}
throw new StatusServletException(Response.SC_NOT_FOUND, e);
}
}
/**
* Downloads the given {@code id} file on given {@code response} stream.
*
* @param id
* The file id.
* @param in
* The file input stream.
* @param response
* The HTTP response on which the file content is written.
* @throws Exception
* If an error occurs during process.
*/
private static void download(final String id, final InputStream in, final HttpServletResponse response) throws Exception {
download(id, "file_" + id, in, response);
}
/**
* Downloads the given {@code id} file on given {@code response} stream.
*
* @param id
* The file id.
* @param fileName
* Name sent in the response header.
* @param in
* The file input stream.
* @param response
* The HTTP response on which the file content is written.
* @throws Exception
* If an error occurs during process.
*/
private static void download(final String id, String fileName, final InputStream in, final HttpServletResponse response) throws Exception {
final FileType fileType = fileTypeFromFileId(id);
ResponseHelper.executeDownload(response, in, fileType != null ? fileType.getContentType() : null, fileName, null);
}
private static void downloadBase64(final String id, final InputStream in, final HttpServletResponse response) throws IOException {
final FileType fileType = fileTypeFromFileId(id);
ResponseHelper.executeDownload(response, in, fileType != null ? fileType.getContentType() : null, null, null, ResponseHelper.ContentDisposition.BASE64);
}
private static FileType fileTypeFromFileId(String fileName) {
final String extension = FilenameUtils.getExtension(fileName);
return FileType.fromExtension(extension);
}
// ---------------------------------------------------------------------------------------
//
// UPLOAD METHODS.
//
// ---------------------------------------------------------------------------------------
/**
* See {@link ServletMethod#UPLOAD_ORGANIZATION_LOGO} for JavaDoc.
*
* @param request
* The HTTP request containing the Organization id parameter.
* @param response
* The HTTP response on which the file content is written.
* @param context
* The execution context.
* @throws java.io.IOException
* If an error occured while reading or writing to the socket or if an error occured while storing the
* uploaded file.
* @throws org.sigmah.server.servlet.base.StatusServletException
* If the id parameter was not found or not parseable or if the request type is not MULTIPART or if the file
* exceeded the maximum allowed size.
* @throws org.apache.commons.fileupload.FileUploadException
* If an error occured while reading the uploaded file.
* @throws javax.servlet.ServletException
* If the given organization could not be found.
*/
protected void uploadOrganizationLogo(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context)
throws IOException, StatusServletException, ServletException, FileUploadException {
// --
// Retrieving parameters from request.
// --
final Integer organizationId = getIntegerParameter(request, RequestParameter.ID, false);
// --
// Retrieving Organization entity.
// --
final Organization organization = organizationDAO.findById(organizationId);
if (organization == null) {
throw new ServletException("Cannot find Organization with id '" + organizationId + "'.");
}
final String previousLogoFileName = organization.getLogo();
// --
// Verifying content length.
// --
final int contentLength = request.getContentLength();
if (contentLength == 0) {
LOG.error("Empty logo file.");
throw new StatusServletException(Response.SC_NO_CONTENT);
}
if (contentLength > FileUploadUtils.MAX_UPLOAD_IMAGE_SIZE) {
LOG.error("Logo file's size is too big to be uploaded (size: {}, maximum : {}).", contentLength, FileUploadUtils.MAX_UPLOAD_IMAGE_SIZE);
throw new StatusServletException(Response.SC_REQUEST_ENTITY_TOO_LARGE);
}
// --
// Saving new logo.
// --
organization.setLogo(organization.getId() + "_" + new Date().getTime());
processUpload(new MultipartRequest(request), response, organization.getLogo(), true, null);
organizationDAO.persist(organization, context.getUser());
// --
// Deleting previous logo file.
// --
if (StringUtils.isNotBlank(previousLogoFileName)) {
fileStorageProvider.delete(previousLogoFileName);
}
response.getWriter().write(organization.getLogo());
}
/**
* See {@link ServletMethod#UPLOAD} for JavaDoc.
*
* @param request
* The HTTP request containing the file id parameter.
* @param response
* The HTTP response on which the file content is written.
* @param context
* The execution context.
* @throws Exception
* If an error occurs during process.
*/
protected void upload(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {
// --
// Verify content length.
// --
final int contentLength = request.getContentLength();
if (contentLength == 0) {
LOG.error("Empty file.");
throw new StatusServletException(Response.SC_NO_CONTENT);
}
if (contentLength > FileUploadUtils.MAX_UPLOAD_FILE_SIZE) {
LOG.error("File's size is too big to be uploaded (size: {}, maximum : {}).", contentLength, FileUploadUtils.MAX_UPLOAD_FILE_SIZE);
throw new StatusServletException(Response.SC_REQUEST_ENTITY_TOO_LARGE);
}
final String fileName = generateUniqueName();
// --
// Writing the file.
// --
final MultipartRequest multipartRequest = new MultipartRequest(request);
final long size = this.processUpload(multipartRequest, response, fileName, false, null);
final Map<String, String> properties = multipartRequest.getProperties();
conflicts.searchForFileAddConflicts(properties, context.getLanguage(), context.getUser());
// --
// Create the associated entries in File and FileVersion tables.
// --
final Integer fileId = fileDAO.saveOrUpdate(properties, fileName, (int) size);
final FileVersion fileVersion = fileDAO.getLastVersion(fileId);
// --
// If a monitored point must be created.
// --
final MonitoredPoint monitoredPoint = parseMonitoredPoint(properties);
if (monitoredPoint != null) {
final Integer projectId = ClientUtils.asInt(properties.get(FileUploadUtils.DOCUMENT_PROJECT));
final Project project = projectDAO.findById(projectId);
monitoredPoint.setFile(fileDAO.findById(fileId));
MonitoredPointList list = project.getPointsList();
if (list == null) {
list = new MonitoredPointList();
project.setPointsList(list);
}
if (list.getPoints() == null) {
list.setPoints(new ArrayList<MonitoredPoint>());
}
// Adds the point to the list.
list.addMonitoredPoint(monitoredPoint);
// Saves monitored point.
monitoredPointDAO.persist(monitoredPoint, context.getUser());
}
final MonitoredPointDTO monitoredPointDTO = mapper().map(monitoredPoint, new MonitoredPointDTO(), MonitoredPointDTO.Mode.BASE);
final FileVersionDTO fileVersionDTO = mapper().map(fileVersion, new FileVersionDTO());
response.setContentType(FileType.HTML.getContentType());
response.getWriter().write(FileUploadResponse.serialize(fileVersionDTO, monitoredPointDTO));
}
protected void uploadAvatar(final HttpServletRequest request, final HttpServletResponse response, final ServletExecutionContext context) throws Exception {
final int contentLength = request.getContentLength();
if (contentLength == 0) {
LOG.error("Empty file.");
throw new StatusServletException(Response.SC_NO_CONTENT);
}
if (contentLength > FileUploadUtils.MAX_UPLOAD_FILE_SIZE) {
LOG.error("File's size is too big to be uploaded (size: {}, maximum : {}).", contentLength, FileUploadUtils.MAX_UPLOAD_FILE_SIZE);
throw new StatusServletException(Response.SC_REQUEST_ENTITY_TOO_LARGE);
}
final String fileName = generateUniqueName();
// --
// Writing the file.
// --
final MultipartRequest multipartRequest = new MultipartRequest(request);
processUpload(multipartRequest, response, fileName, false, FileUploadUtils.MAX_AVATAR_SIZE);
response.setStatus(Response.SC_OK);
response.setContentType(FileType.TXT.getContentType());
response.getWriter().write(fileName);
}
// ---------------------------------------------------------------------------------------
//
// UTILITY METHODS.
//
// ---------------------------------------------------------------------------------------
/**
* Processes the file upload.
*
* @param request
* The HTTP request.
* @param response
* The HTTP response.
* @param context
* The execution context.
* @param filename
* The uploaded physical file name.
* @param logo
* {@code true} if the upload concerns an organization logo, {@code false} otherwise.
* @throws java.io.IOException
* If an error occured while reading or writing to the socket or if an error occured while storing the
* uploaded file.
* @throws org.sigmah.server.servlet.base.StatusServletException
* if the request type is not MULTIPART or if the file exceeded the maximum allowed size.
* @throws org.apache.commons.fileupload.FileUploadException
* If an error occured while reading the uploaded file.
*/
private long processUpload(final MultipartRequest multipartRequest, final HttpServletResponse response, final String filename, final boolean logo, final Integer resizeTo)
throws StatusServletException, IOException, FileUploadException {
LOG.debug("Starting file uploading...");
final long[] size = { 0L
};
multipartRequest.parse(new MultipartRequestCallback() {
@Override
public void onInputStream(InputStream inputStream, String itemName, String mimeType) throws IOException {
// Retrieving file name.
// If a name (id) is provided, we use it. If not, using the name of the uploaded file.
final String name = StringUtils.isNotBlank(filename) ? filename : itemName;
try (final InputStream stream = inputStream) {
LOG.debug("Reads image content from the field ; name: '{}'.", name);
if (logo) {
size[0] = logoManager.updateLogo(stream, name);
} else if (resizeTo == null) {
size[0] = fileStorageProvider.copy(stream, name, StandardCopyOption.REPLACE_EXISTING);
} else {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
imageMinimizer.resizeImage(inputStream, byteArrayOutputStream, resizeTo);
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray())) {
size[0] = fileStorageProvider.copy(byteArrayInputStream, name, StandardCopyOption.REPLACE_EXISTING);
}
}
}
response.setStatus(Response.SC_ACCEPTED);
// FIXME : perhaps keep the response above for error catching
// response.getWriter().write("ok");
LOG.debug("File '{}' upload has been successfully processed.", name);
}
}
});
return size[0];
}
/**
* Generates a {@link MonitoredPoint} instance from the given {@code properties}.<br>
* Following attributes of the generated monitored point are set:
* <ul>
* <li>{@code label}</li>
* <li>{@code expectedDate}</li>
* <li>{@code deleted} (set to {@code false})</li>
* </ul>
*
* @param properties
* The properties.
* @return The monitored point instance.
* @throws UnsupportedOperationException
* If the {@code properties} cannot be used to generate a <em>valid</em> {@link MonitoredPoint} instance.
*/
private static MonitoredPoint parseMonitoredPoint(final Map<String, String> properties) {
if (MapUtils.isEmpty(properties)) {
return null;
}
final String label = properties.get(FileUploadUtils.MONITORED_POINT_LABEL);
final String expectedDateTime = properties.get(FileUploadUtils.MONITORED_POINT_DATE);
if (StringUtils.isBlank(label) || StringUtils.isBlank(expectedDateTime)) {
return null;
}
try {
final MonitoredPoint monitoredPoint = new MonitoredPoint();
monitoredPoint.setLabel(label);
monitoredPoint.setExpectedDate(new Date(Long.valueOf(expectedDateTime)));
monitoredPoint.setDeleted(false);
return monitoredPoint;
} catch (final Exception e) {
throw new UnsupportedOperationException("Error occures while generating monitored point from properties.", e);
}
}
/**
* Computes and returns a unique string identifier to name files.
*
* @return A unique string identifier.
*/
public static String generateUniqueName() {
// Adds the timestamp to ensure the id uniqueness.
return UUID.randomUUID().toString() + new Date().getTime();
}
}