AbstractAsynchronousUploadHandler.java

/*
 * Copyright (c) 2002-2021, City of Paris
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice
 *     and the following disclaimer.
 *
 *  2. Redistributions in binary form must reproduce the above copyright notice
 *     and the following disclaimer in the documentation and/or other materials
 *     provided with the distribution.
 *
 *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
 *     contributors may be used to endorse or promote products derived from
 *     this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * License 1.0
 */
package fr.paris.lutece.plugins.asynchronousupload.service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import fr.paris.lutece.plugins.asynchronousupload.util.JSONUtils;
import fr.paris.lutece.portal.service.util.AppException;
import fr.paris.lutece.portal.service.util.AppLogService;
import fr.paris.lutece.portal.web.upload.MultipartHttpServletRequest;
import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import net.sf.json.JSONSerializer;

/**
 * AbstractAsynchronousUploadHandler.
 */
public abstract class AbstractAsynchronousUploadHandler implements IAsyncUploadHandler
{
    protected static final String PARAM_CUSTOM_SESSION_ID = "CUSTOM_SESSION";
    private static final String PARAMETER_FIELD_NAME = "fieldname";
    private static final String PARAMETER_FIELD_INDEX = "field_index";
    private static final String PARAMETER_HANDLER = "asynchronousupload.handler";
    private static final String UPLOAD_SUBMIT_PREFIX = "_upload_submit_";
    private static final String UPLOAD_DELETE_PREFIX = "_upload_delete_";
    private static final String UPLOAD_CHECKBOX_PREFIX = "_upload_checkbox_";
    private static final String KEY_FORM_ERROR = "form_error";
    private static final String KEY_FILE_SIZE = "fileSize";
    private static final String KEY_FILE_NAME = "fileName";
    private static final String KEY_FIELD_NAME = "field_name";
    private static final String KEY_FILES = "files";
    private static final String HEADER_CONTENT_RANGE = "Content-Range";
    private static final String REGEXP_CONTENT_RANGE_HEADER = "bytes (\\d*)-(\\d*)\\/(\\d*)";

    @Override
    public void process( HttpServletRequest request, HttpServletResponse response, Map<String, Object> map, List<FileItem> listFileItemsToUpload )
    {
        map.clear( );
        String strFieldName = request.getParameter( PARAMETER_FIELD_NAME );

        if ( StringUtils.isBlank( strFieldName ) )
        {
            throw new AppException( "id entry is not provided for the current file upload" );
        }

        if ( CollectionUtils.isNotEmpty( listFileItemsToUpload ) )
        {
            String strError = canUploadFiles( request, strFieldName, listFileItemsToUpload, request.getLocale( ) );
            if ( strError == null )
            {
                // manage chunk file if there is not multiple file
                if ( isManagePartialContent( ) && isRequestContainsPartialContent( request ) )
                {
                    if ( listFileItemsToUpload.size( ) == 1 )
                    {
                        addFileItemToPartialUploadedFilesList( listFileItemsToUpload.get( 0 ), strFieldName, request );
                        if ( isRequestContainsLastPartialContent( request ) )
                        {
                            PartialFileItemGroup partialFileItemGroup = new PartialFileItemGroup(
                                    getListPartialUploadedFiles( strFieldName, request.getSession( ) ) );
                            addFileItemToUploadedFilesList( partialFileItemGroup, strFieldName, request );
                        }
                    }
                    else
                    {
                        AppLogService.error( "AbstractAsynchronousUploadHandler.process : -Chunk files with  multiple file selected do not deal" );
                        map.put( KEY_FORM_ERROR, "Chunk files with  multiple file selected do not deal" );
                    }
                }
                else
                {
                    for ( FileItem fileItem : listFileItemsToUpload )
                    {
                        addFileItemToUploadedFilesList( fileItem, strFieldName, request );
                    }
                }
            }
            else
            {
                map.put( KEY_FORM_ERROR, strError );
            }
        }
        map.put( KEY_FIELD_NAME, strFieldName );

        List<FileItem> fileItemsSession = getListUploadedFiles( strFieldName, request.getSession( ) );
        List<Map<String, Object>> listJsonFileMap = new ArrayList<>( );
        map.put( KEY_FILES, listJsonFileMap );

        for ( FileItem fileItem : fileItemsSession )
        {
            Map<String, Object> jsonFileMap = new HashMap<>( );
            jsonFileMap.put( KEY_FILE_NAME, fileItem.getName( ) );
            jsonFileMap.put( KEY_FILE_SIZE, fileItem.getSize( ) );
            listJsonFileMap.add( jsonFileMap );
        }

    }

    /**
     * Checks the request parameters to see if an upload submit has been called.
     *
     * @param request
     *            the HTTP request
     * @return the name of the upload action, if any. Null otherwise.
     */
    public String getUploadAction( HttpServletRequest request )
    {
        Enumeration<String> enumParamNames = request.getParameterNames( );

        while ( enumParamNames.hasMoreElements( ) )
        {
            String paramName = enumParamNames.nextElement( );

            if ( paramName.startsWith( getUploadSubmitPrefix( ) ) || paramName.startsWith( getUploadDeletePrefix( ) ) )
            {
                return paramName;
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void doRemoveFile( HttpServletRequest request, String strFieldName )
    {
        if ( hasRemoveFlag( request, strFieldName ) )
        {
            HttpSession session = request.getSession( false );

            if ( session != null )
            {
                // Some previously uploaded files were deleted
                // Build the prefix of the associated checkboxes
                String strPrefix = getUploadCheckboxPrefix( ) + strFieldName;

                // Look for the checkboxes in the request
                Enumeration<String> enumParamNames = request.getParameterNames( );
                List<Integer> listIndexes = new ArrayList<>( );

                while ( enumParamNames.hasMoreElements( ) )
                {
                    String strParamName = enumParamNames.nextElement( );
                    String strParamValue = request.getParameter( strParamName );

                    if ( strParamValue.startsWith( strPrefix ) )
                    {
                        // Get the index from the name of the checkbox
                        listIndexes.add( Integer.parseInt( strParamValue.substring( strPrefix.length( ) ) ) );
                    }
                }

                Collections.sort( listIndexes );
                Collections.reverse( listIndexes );

                for ( int nIndex : listIndexes )
                {
                    removeFileItem( strFieldName, session, nIndex );
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String doRemoveUploadedFile( HttpServletRequest request, String strFieldName, List<Integer> listIndexesFilesToRemove )
    {
        if ( StringUtils.isBlank( strFieldName ) )
        {
            return JSONUtils.buildJsonErrorRemovingFile( request ).toString( );
        }

        if ( CollectionUtils.isNotEmpty( listIndexesFilesToRemove ) )
        {
            // parse json
            JSON jsonFieldIndexes = JSONSerializer.toJSON( listIndexesFilesToRemove );

            if ( !jsonFieldIndexes.isArray( ) )
            {
                return JSONUtils.buildJsonErrorRemovingFile( request ).toString( );
            }

            JSONArray jsonArrayFieldIndexers = (JSONArray) jsonFieldIndexes;
            int [ ] tabFieldIndex = new int [ jsonArrayFieldIndexers.size( )];

            for ( int nIndex = 0; nIndex < jsonArrayFieldIndexers.size( ); nIndex++ )
            {
                try
                {
                    tabFieldIndex [nIndex] = Integer.parseInt( jsonArrayFieldIndexers.getString( nIndex ) );
                }
                catch( NumberFormatException nfe )
                {
                    return JSONUtils.buildJsonErrorRemovingFile( request ).toString( );
                }
            }

            // inverse order (removing using index - remove greater first to keep order)
            Arrays.sort( tabFieldIndex );
            ArrayUtils.reverse( tabFieldIndex );

            List<FileItem> fileItemsSession = getListUploadedFiles( strFieldName, request.getSession( ) );

            List<FileItem> listItemsToRemove = new ArrayList<>( listIndexesFilesToRemove.size( ) );

            for ( int nFieldIndex : tabFieldIndex )
            {
                if ( fileItemsSession.size( ) == 1 && nFieldIndex > 0 )
                {
                    nFieldIndex = nFieldIndex - 1;
                }
                listItemsToRemove.add( fileItemsSession.get( nFieldIndex ) );
                removeFileItem( strFieldName, request.getSession( ), nFieldIndex );
            }
        }

        JSONObject json = new JSONObject( );
        json.element( JSONUtils.JSON_KEY_SUCCESS, JSONUtils.JSON_KEY_SUCCESS );

        json.accumulateAll( JSONUtils.getUploadedFileJSON( getListUploadedFiles( strFieldName, request.getSession( ) ) ) );
        json.element( JSONUtils.JSON_KEY_FIELD_NAME, strFieldName );

        return json.toString( );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasRemoveFlag( HttpServletRequest request, String strFieldName )
    {
        return StringUtils.isNotEmpty( request.getParameter( getUploadDeletePrefix( ) + strFieldName ) );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isInvoked( HttpServletRequest request )
    {
        return StringUtils.equals( getHandlerName( ), request.getParameter( PARAMETER_HANDLER ) );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasAddFileFlag( HttpServletRequest request, String strFieldName )
    {
        return StringUtils.isNotEmpty( request.getParameter( getUploadSubmitPrefix( ) + strFieldName ) );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addFilesUploadedSynchronously( HttpServletRequest request, String strFieldName )
    {
        if ( request instanceof MultipartHttpServletRequest && hasAddFileFlag( request, strFieldName ) )
        {
            MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
            List<FileItem> listFileItem = multipartRequest.getFileList( strFieldName );

            if ( CollectionUtils.isNotEmpty( listFileItem ) )
            {
                for ( FileItem fileItem : listFileItem )
                {
                    if ( ( fileItem.getSize( ) > 0L ) && StringUtils.isNotEmpty( fileItem.getName( ) ) )
                    {
                        addFileItemToUploadedFilesList( fileItem, strFieldName, request );
                    }
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getUploadSubmitPrefix( )
    {
        return getHandlerName( ) + UPLOAD_SUBMIT_PREFIX;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getUploadDeletePrefix( )
    {
        return getHandlerName( ) + UPLOAD_DELETE_PREFIX;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getUploadCheckboxPrefix( )
    {
        return getHandlerName( ) + UPLOAD_CHECKBOX_PREFIX;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public byte [ ] doRetrieveUploadedFile( HttpServletRequest request )
    {
        String strFieldName = request.getParameter( PARAMETER_FIELD_NAME );
        String strFieldIndex = request.getParameter( PARAMETER_FIELD_INDEX );
        int intFieldIndex;
        FileItem itemToDownload = null;
        if ( StringUtils.isNotEmpty( strFieldIndex ) && StringUtils.isNumeric( strFieldIndex ) )
        {
            intFieldIndex = Integer.parseInt( request.getParameter( PARAMETER_FIELD_INDEX ) );
            List<FileItem> fileItemsSession = getListUploadedFiles( strFieldName, request.getSession( ) );
            itemToDownload = fileItemsSession.get( intFieldIndex );
        }
        if ( itemToDownload == null )
        {
            return new byte [ 0];
        }
        return itemToDownload.get( );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<FileItem> getListPartialUploadedFiles( String strFieldName, HttpSession session )
    {
        AppLogService.error( "the Upload Handler do not manage partial content files " );
        return new ArrayList<>( );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addFileItemToPartialUploadedFilesList( FileItem fileItem, String strFieldName, HttpServletRequest request )
    {
        AppLogService.error( "the Upload Handler do not manage partial content files " );
    }

    /**
     * {@inheritDoc}
     */

    @Override
    public boolean isManagePartialContent( )
    {

        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeAllFileItem( HttpSession session )
    {
    }

    /**
     * return true if the content of the request is partial
     * 
     * @param request
     *            the request
     * @return true if the content of the request is partial
     */
    private boolean isRequestContainsPartialContent( HttpServletRequest request )
    {
        return request.getHeader( HEADER_CONTENT_RANGE ) != null;

    }

    /**
     * return true if the request contain the last partial content of the file
     * 
     * @param request
     * @return
     */
    private boolean isRequestContainsLastPartialContent( HttpServletRequest request )
    {

        boolean bLastPartialContent = false;
        String strContentRange = request.getHeader( HEADER_CONTENT_RANGE );
        Pattern r = Pattern.compile( REGEXP_CONTENT_RANGE_HEADER );
        Matcher m = r.matcher( strContentRange );

        if ( m.find( ) )
        {

            String strLatsBytes = m.group( 2 );
            String strTotalBytes = m.group( 3 );
            int nTotalBytes = Integer.parseInt( strTotalBytes );
            int nLastBytes = Integer.parseInt( strLatsBytes );
            if ( nTotalBytes - nLastBytes == 1 )
            {
                bLastPartialContent = true;

            }

        }
        return bLastPartialContent;
    }

    protected String getCustomSessionId( HttpSession session )
    {
        String sessionId = (String) session.getAttribute( PARAM_CUSTOM_SESSION_ID );
        if ( sessionId == null )
        {
            sessionId = UUID.randomUUID( ).toString( );
            session.setAttribute( PARAM_CUSTOM_SESSION_ID, sessionId );
        }
        return sessionId;
    }

    @Override
    public void removeFileItem( String strFieldName, HttpSession session, int nIndex )
    {
        // Remove the file (this will also delete the file physically)
        List<FileItem> uploadedFiles = getListUploadedFiles( strFieldName, session );

        if ( ( uploadedFiles != null ) && !uploadedFiles.isEmpty( ) && ( uploadedFiles.size( ) > nIndex ) )
        {
            // Remove the object from the Hashmap
            FileItem fileItem = uploadedFiles.remove( nIndex );
            fileItem.delete( );
        }
    }
}