org.silverpeas.core.webapi.upload.FileUploadResource.java Source code

Java tutorial

Introduction

Here is the source code for org.silverpeas.core.webapi.upload.FileUploadResource.java

Source

/*
 * Copyright (C) 2000 - 2018 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception.  You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/legal/floss_exception.html"
 *
 * 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.silverpeas.core.webapi.upload;

import org.apache.commons.io.FilenameUtils;
import org.silverpeas.core.admin.component.model.ComponentFileFilterParameter;
import org.silverpeas.core.annotation.RequestScoped;
import org.silverpeas.core.annotation.Service;
import org.silverpeas.core.io.upload.UploadSession;
import org.silverpeas.core.io.upload.UploadSessionFile;
import org.silverpeas.core.notification.message.MessageNotifier;
import org.silverpeas.core.security.authorization.ComponentAccessControl;
import org.silverpeas.core.util.JSONCodec;
import org.silverpeas.core.util.LocalizationBundle;
import org.silverpeas.core.util.ResourceLocator;
import org.silverpeas.core.util.StringUtil;
import org.silverpeas.core.util.UnitUtil;
import org.silverpeas.core.util.file.FileRepositoryManager;
import org.silverpeas.core.util.logging.SilverLogger;
import org.silverpeas.core.web.http.RequestParameterDecoder;
import org.silverpeas.core.webapi.base.RESTWebService;
import org.silverpeas.core.webapi.base.annotation.Authenticated;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.MessageFormat;
import java.util.Optional;
import java.util.concurrent.Semaphore;
import java.util.function.Function;

import static org.silverpeas.core.web.util.IFrameAjaxTransportUtil.*;

/**
 * A REST Web resource that permits to upload files. It has to be used with one of the following
 * plugins:
 * <ul>
 * <li>silverpeas-filUpload.js: useful to handle file upload on resource creation</li>
 * <li>silverpeas-ddUpload.js: useful to handle drag & drop file upload on existing resources</li>
 * </ul>
 * @author Yohann Chastagnier
 */
@Service
@RequestScoped
@Path(FileUploadResource.PATH)
@Authenticated
public class FileUploadResource extends RESTWebService {

    private static final Semaphore requestLimit = new Semaphore(50, true);
    static final String PATH = "fileUpload";

    @Inject
    private ComponentAccessControl componentAccessController;

    /**
     * Performs some verifications before starting a file upload.
     * All the verifications are checked again on the effective upload (security).
     * @return the result of the verification: HTTP OK.
     */
    @POST
    @Path("verify")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Response verify() {
        FileUploadVerifyData fileUploadVerifyData = RequestParameterDecoder.decode(getHttpRequest(),
                FileUploadVerifyData.class);
        checkMaximumFileSize(fileUploadVerifyData.getName(), fileUploadVerifyData.getSize());
        checkAuthorizedMimeTypes(fileUploadVerifyData.getName());
        checkSpecificComponentVerifications(fileUploadVerifyData, null);
        return Response.ok().build();
    }

    /**
     * Permits to upload files from multipart http request.
     * If the user isn't authenticated, a 401 HTTP code is returned.
     * If a problem occurs when processing the request, a 503 HTTP code is returned.
     * @return the response in relation with jQuery plugins used on the client side: a html textarea
     * tag that contains a JSON array structure. Each line of this array contains
     * information of an uploaded file :
     * <ul>
     * <li><b>uploadSessionId</b> : the uploaded session identifier</li>
     * <li><b>fullPath</b> : the full path of the uploaded file</li>
     * <li><b>name</b> : the name of the uploaded file (without its path)</li>
     * <li><b>size</b> : the byte size of the uploaded file</li>
     * <li><b>formattedSize</b> : the formatted file size according to the language of user</li>
     * <li><b>iconUrl</b> : the url of the icon that represents the type of the uploaded file</li>
     * </ul>
     */
    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces(MediaType.TEXT_HTML)
    public Response uploadFiles() throws IOException {
        UploadedRequestFile uploadedRequestFile = RequestParameterDecoder.decode(getHttpRequest(),
                UploadedRequestFile.class);

        try {
            Function<JSONCodec.JSONObject, JSONCodec.JSONObject> builder = uploadFile(
                    FileUploadData.from(uploadedRequestFile),
                    uploadedRequestFile.getRequestFile().getInputStream());
            String jsonFiles = packJSonArrayWithHtmlContainer(a -> a.addJSONObject(builder));
            return Response.ok().entity(jsonFiles).build();
        } catch (WebApplicationException ex) {
            if (AJAX_IFRAME_TRANSPORT.equals(uploadedRequestFile.getXRequestedWith())
                    && ex.getResponse().getStatus() == Response.Status.PRECONDITION_FAILED.getStatusCode()) {

                // In case of file upload performed by Ajax IFrame transport way,
                // the exception must also be returned into a text/html response.
                ex = createWebApplicationExceptionWithJSonErrorInHtmlContainer(ex);
            }
            throw ex;
        } catch (final Exception ex) {
            throw new WebApplicationException(ex, Response.Status.SERVICE_UNAVAILABLE);
        }
    }

    /**
     * Permits to upload one file from http request.
     * If the user isn't authenticated, a 401 HTTP code is returned.
     * If a problem occurs when processing the request, a 503 HTTP code is returned.
     * @return the response in relation with jQuery plugins used on the client side: a html textarea
     * tag that contains a JSON array structure. Each line of this array contains
     * information of an uploaded file :
     * <ul>
     * <li><b>uploadSessionId</b> : the uploaded session identifier</li>
     * <li><b>fullPath</b> : the full path of the uploaded file</li>
     * <li><b>name</b> : the name of the uploaded file (without its path)</li>
     * <li><b>size</b> : the byte size of the uploaded file</li>
     * <li><b>formattedSize</b> : the formatted file size according to the language of user</li>
     * <li><b>iconUrl</b> : the url of the icon that represents the type of the uploaded file</li>
     * </ul>
     */
    @POST
    @Consumes(MediaType.APPLICATION_OCTET_STREAM)
    @Produces(MediaType.TEXT_HTML)
    public Response uploadFile(InputStream inputStream) {
        try {
            String jsonFile = packJSonObjectWithHtmlContainer(
                    uploadFile(FileUploadData.from(getHttpServletRequest()), inputStream));
            return Response.ok().entity(jsonFile).build();
        } catch (final WebApplicationException ex) {
            throw ex;
        } catch (final Exception ex) {
            throw new WebApplicationException(ex, Response.Status.SERVICE_UNAVAILABLE);
        }
    }

    /**
     * Handles the upload of one file.
     * @param fileUploadData the uploaded file data (except the content)
     * @param inputStream the input stream to upload
     * @return a builder of the JSON representation of the uploaded file (more information on
     * {@link FileUploadResource#uploadFiles()})
     * @throws IOException
     */
    private Function<JSONCodec.JSONObject, JSONCodec.JSONObject> uploadFile(FileUploadData fileUploadData,
            InputStream inputStream) throws Exception {

        if (StringUtil.isNotDefined(fileUploadData.getFullPath())) {
            throw new WebApplicationException(Response.Status.BAD_REQUEST);
        }

        // Avoid server overload
        requestLimit.acquire();

        try {
            UploadSession uploadSession = UploadSession.from(fileUploadData.getUploadSessionId());

            if (StringUtil.isDefined(fileUploadData.getComponentInstanceId()) && !componentAccessController
                    .isUserAuthorized(getUser().getId(), fileUploadData.getComponentInstanceId())) {
                throw new WebApplicationException(Response.Status.FORBIDDEN);
            }

            if (StringUtil.isDefined(uploadSession.getComponentInstanceId())) {
                if (!uploadSession.getComponentInstanceId().equals(fileUploadData.getComponentInstanceId())) {
                    throw new WebApplicationException(Response.Status.FORBIDDEN);
                }
            } else if (StringUtil.isDefined(fileUploadData.getComponentInstanceId())) {
                uploadSession.forComponentInstanceId(fileUploadData.getComponentInstanceId());
            }

            UploadSessionFile uploadSessionFile = uploadSession.getUploadSessionFile(fileUploadData.getFullPath());

            // Writing the file on server
            try {
                uploadSessionFile.write(inputStream);
            } catch (IOException ioe) {
                // The file is written currently by an other process
                throw new WebApplicationException(ioe, Response.Status.CONFLICT);
            }

            try {
                // Maximum size
                checkMaximumFileSize(fileUploadData.getName(), uploadSessionFile.getServerFile().length());
                // Mime-Type verified on real data
                checkAuthorizedMimeTypes(uploadSessionFile.getServerFile().getPath());
                // Specific component instance verification
                checkSpecificComponentVerifications(null, uploadSessionFile.getServerFile());
            } catch (Exception e) {
                uploadSession.remove(fileUploadData.getFullPath());
                throw e;
            }

            // JSON response
            return asJSON(uploadSessionFile);

        } finally {
            requestLimit.release();
        }
    }

    /**
     * Checks the maximum size authorized.
     * @param fileName the name of the file to check.
     * @param fileSize the size of the file to check.
     */
    private void checkMaximumFileSize(final String fileName, long fileSize) {
        long maximumFileSize = FileRepositoryManager.getUploadMaximumFileSize();
        if (fileSize > maximumFileSize) {
            LocalizationBundle bundle = ResourceLocator.getLocalizationBundle(
                    "org.silverpeas.util.attachment.multilang.attachment", getUserPreferences().getLanguage());
            String errorMessage = bundle.getString("attachment.dialog.errorFileSize") + " "
                    + bundle.getString("attachment.dialog.maximumFileSize") + " ("
                    + UnitUtil.formatMemSize(maximumFileSize) + ")";
            errorMessage = MessageFormat.format(errorMessage, fileName);
            MessageNotifier.addError(errorMessage);
            throw new WebApplicationException(
                    Response.status(Response.Status.PRECONDITION_FAILED).entity(errorMessage).build());
        }
    }

    /**
     * Checks the authorized mime-types if {@link #getComponentId()} return a defined value.<br>
     * If no defined value is returned by {@link #getComponentId()}, nothing is verified.
     * @param fileName the file name to test.
     */
    private void checkAuthorizedMimeTypes(final String fileName) {
        String componentInstanceId = getComponentId();
        if (StringUtil.isDefined(componentInstanceId)) {

            // Component file filter that contains authorized and forbidden rules
            final ComponentFileFilterParameter componentFileFilter = ComponentFileFilterParameter
                    .from(getOrganisationController().getComponentInstance(componentInstanceId).orElse(null));

            try {
                componentFileFilter.verifyFileAuthorized(new File(fileName));
            } catch (Exception e) {
                throw new WebApplicationException(e, Response.Status.PRECONDITION_FAILED);
            }
        }
    }

    /**
     * Checks the authorized mime-types if {@link #getComponentId()} return a defined value.<br>
     * If no defined value is returned by {@link #getComponentId()}, nothing is verified.
     * @param fileUploadData the file upload data (filled when file has not been yet uploaded).
     * @param uploadedFile the uploaded file.
     */
    private void checkSpecificComponentVerifications(FileUploadVerifyData fileUploadData, File uploadedFile) {
        String componentInstanceId = getComponentId();
        if (StringUtil.isDefined(componentInstanceId)) {
            try {
                Optional<ComponentInstanceFileUploadVerification> verification = ComponentInstanceFileUploadVerification
                        .get(componentInstanceId);
                if (fileUploadData != null) {
                    verification.ifPresent(i -> i.verify(componentInstanceId, fileUploadData));
                } else {
                    verification.ifPresent(i -> i.verify(componentInstanceId, uploadedFile));
                }
            } catch (Exception e) {
                throw new WebApplicationException(e, Response.Status.PRECONDITION_FAILED);
            }
        }
    }

    /**
     * Builds a JSON representation of the given uploaded file.
     * @param uploadSessionFile the uploaded file into current session.
     * @return a builder of the JSON representation of the uploaded file (more information on
     * {@link FileUploadResource#uploadFiles()})
     */
    private Function<JSONCodec.JSONObject, JSONCodec.JSONObject> asJSON(UploadSessionFile uploadSessionFile) {
        return o -> o.put("uploadSessionId", uploadSessionFile.getUploadSession().getId())
                .put("fullPath", uploadSessionFile.getFullPath())
                .put("name", uploadSessionFile.getServerFile().getName())
                .put("size", uploadSessionFile.getServerFile().length())
                .put("formattedSize",
                        UnitUtil.formatMemSize(
                                new BigDecimal(String.valueOf(uploadSessionFile.getServerFile().length()))))
                .put("iconUrl", FileRepositoryManager
                        .getFileIcon(FilenameUtils.getExtension(uploadSessionFile.getServerFile().getName())));
    }

    @DELETE
    @Produces(MediaType.APPLICATION_JSON)
    public Response delete() {
        try {
            UploadSession uploadSession = UploadSession.from(getHttpServletRequest());
            FileUploadData fileToDelete = FileUploadData.from(getHttpServletRequest());
            if (StringUtil.isDefined(fileToDelete.getFullPath())) {
                if (!uploadSession.remove(fileToDelete.getFullPath())) {
                    SilverLogger.getLogger(this).error("Trying to delete non existing file with session id '"
                            + uploadSession.getId() + "' and fullPath '" + fileToDelete.getFullPath() + "'");
                }
            } else {
                uploadSession.clear();
            }
            return Response.ok().build();
        } catch (final WebApplicationException ex) {
            throw ex;
        } catch (final Exception ex) {
            throw new WebApplicationException(ex, Response.Status.SERVICE_UNAVAILABLE);
        }
    }

    @Override
    protected String getResourceBasePath() {
        return PATH;
    }

    /*
       * (non-Javadoc)
       * @see com.silverpeas.web.RESTWebService#getComponentId()
       */
    @Override
    public String getComponentId() {
        return getHttpRequest().getHeader(FileUploadData.X_COMPONENT_INSTANCE_ID);
    }
}