Mar 8, 2018 2:39:58 PM seb leridon avatar   1801

Use of asynchronous upload plugin feature

The Asynchronous Upload plugin is a plugin to simplify and centralize the asynchronous upload framework used in a Lutèce application. It currently uses the JQuery File Upload JavaScript framework to handle asynchronous file uploads.


Description of operation

The asynchronous upload plugin provides JavaScript files and Freemarker macros to create "file" type inputs for asynchronous uploads. The HTML code generated by these macros will use JavaScript functions to interact in AJAX with the server to perform uploads.

The requests made in AJAX will be processed by a server-side upload manager (called in the following handler). This handler will have two responsibilities:

  • Manage the dialogue between the client JavaScript and the server.
  • Provide uploaded files to the service that requires them, and allow them to add or remove them.

The dialogue with the client is fully implemented by the plugin. The management of the recorded files is when it is the responsibility of the implementation of the application using this plugin. It is not implemented by default.

The management of the uploaded files (recording, deletion, ...) will have to be realized by the application using the plugin.

The standard operation of a form with asynchronous upload file fields is as follows:

  • Display the form to the user.
  • Input of the form by the user, and possible upload of files asynchronously.
  • Submit the form by the user.
  • Receipt of form data submitted by the server.
  • Retrieve the list of uploaded files associated with each field of the form submitted by the service processing the request via the handler.
  • Business processing of data (validation, backup, etc ...).

Using the plugin

In order to use the Asynchronous Upload plugin, you have to perform 3 operations:

  • Include the Asynchronous Upload plugin and activate it.
  • Implement a server-side upload manager: this is a Java class implementing the interface fr.paris.lutece.plugins.asynchronousupload.service.IAsyncUploadHandler. The abstract class fr.paris.lutece.plugins.asynchronousupload.service.AbstractAsynchronousUploadHandler is a partial implementation of this service that can be completed.
  • Include in the generated HTML a javascript file provided by the JSP accessible at the address jsp / site / plugins / asynchronousupload / GetMainUploadJs.jsp. Eventually,
  • Add file-type inputs to the form (s) allowing for asynchronous uploads using Freemarker macros.

Each input created with the corresponding Freemarker macro is identified by its name (fieldName attribute of the macro). This parameter is also used by the handler to associate the uploaded files with a form field.

Inclusion of the plugin

The sources of the current version of the Asynchronous Upload plugin are available at the following address: http://dev.lutece.paris.fr/svn/lutece/portal/trunk/plugins/technical/plugin-asynchronousupload.

The plugin can be declared as a dependency of another Lutèce plugin in its pom.xml thanks to the following code:

<dependency>
    <groupId>fr.paris.lutece.plugins</groupId>
    <artifactId>plugin-asynchronousupload</artifactId>
    <version>[1.0.1-SNAPSHOT,)</version>
    <type>lutece-plugin</type>
</dependency>

Implementing the upload manager

The upload manager must implement the interface fr.paris.lutece.plugins.asynchronousupload.service.IAsyncUploadHandler. It is strongly recommended to use the abstract class en.paris.lutece.plugins.asynchronousupload .service.AbstractAsynchronousUploadHandler. In the following we will assume that we are expanding this class.

A handler must therefore at least implement the methods below:

public class UploadHandlerDemo extends AbstractAsynchronousUploadHandler
{
    private static final String HANDLER_NAME = "myHandler";

    / **
     * Check if a list of uploaded files for a given field are valid
     * @param request The request made
     * @param strFieldName The name of the field used to upload a file.
     * @param listFileItemsToUpload List of uploaded files to check.
     * @param locale The locale to use to display eventual error messages
     * @return The error message, or null if no error has been detected and the files are valid.
     * /
    @Override
    public String canUploadFiles (HttpServletRequest request, String strFieldName,
            List <FileItem> listFileItemsToUpload, Local Local)
    {
        // TODO Auto-generated method stub
        return null;
    }

    / **
     * Allows you to declare a file as uploaded. The implementation of this method is now responsible for managing the file.
     * @param fileItem The uploaded file
     * @param strFieldName The name of the field to which the file is associated
     * @param request The request
     * /
    @Override
    public void addFileItemToUploadedFilesList (FileItem fileItem, String strFieldName, HttpServletRequest request)
    {
        // TODO Auto-generated method stub
    }

    / **
     * Allows you to retrieve the list of uploaded files for a given field. The list must be ordered chronologically by date of upload.
     * For each file an index will be associated corresponding to the index of the file in the list (the oldest file will have index 0).
     * Two successive calls of this method must therefore return a list ordered in the same way.
     * @param strFieldName The name of the field from which we want to recover the files
     * @param session the session of the user using the file. Use only if the files are saved in session.
     * @return The list of uploaded files for the given field
     * /
    @Override
    public List <FileItem> getListUploadedFiles (StrFieldName String, HttpSession Session)
    {
        // TODO Auto-generated method stub
        return null;
    }

    / **
     * Allows you to delete a previously uploaded file
     * @param strFieldName The name of the field
     * @param session the session of the user using the file. Use only if the files are saved in session.
     * @param nIndex The index of the file in the list of uploaded files.
     * /
    @Override
    public void removeFileItem (strFieldName String, session httpSession, int nIndex)
    {
        // TODO Auto-generated method stub
    }

    / **
     * Allows you to define the name of the handler. This name must be unique, and contain only numeric characters (no points, comma, ...).
     * It is recommended to prefix the name of the plugin, then to suffix a functional name.
     * Be careful, the name of the handler is different from the name of the associated Spring bean!
     * /
    @Override
    public String getHandlerName ()
    {
        return HANDLER_NAME;
    }
}

The handler must then be declared as a Spring bean in a context file.

Example of upload manager implementation

This example is taken from the upload manager implementation in the genericattributes plugin.

package fr.paris.lutece.plugins.genericattributes.service.upload;

import fr.paris.lutece.plugins.asynchronousupload.service.AbstractAsynchronousUploadHandler;
import fr.paris.lutece.plugins.genericattributes.business.Entry;
import fr.paris.lutece.plugins.genericattributes.business.EntryHome;
import fr.paris.lutece.plugins.genericattributes.business.GenericAttributeError;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.EntryTypeServiceManager;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.IEntryTypeService;
import fr.paris.lutece.portal.service.i18n.I18nService;
import fr.paris.lutece.portal.service.util.AppException;
import fr.paris.lutece.util.filesystem.UploadUtil;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.lang.StringUtils;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

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


/**
 * Abstract class to manage uploaded files for generic attributes entries of
 * type files
 */
public abstract class AbstractGenAttUploadHandler extends AbstractAsynchronousUploadHandler
{
    private static final String PREFIX_ENTRY_ID = IEntryTypeService.PREFIX_ATTRIBUTE;

    // Error messages
    private static final String ERROR_MESSAGE_UNKNOWN_ERROR = "genericattributes.message.unknownError";

    /** <sessionId,<fieldName,fileItems>> */
    /** contains uploaded file items */
    private static Map<String, Map<String, List<FileItem>>> _mapAsynchronousUpload = new ConcurrentHashMap<String, Map<String, List<FileItem>>>(  );

    /**
     * {@inheritDoc}
     */
    @Override
    public String canUploadFiles( HttpServletRequest request, String strFieldName,
        List<FileItem> listFileItemsToUpload, Locale locale )
    {
        if ( StringUtils.isNotBlank( strFieldName ) && ( strFieldName.length(  ) > PREFIX_ENTRY_ID.length(  ) ) )
        {
            initMap( request.getSession(  ).getId(  ), strFieldName );

            String strIdEntry = getEntryIdFromFieldName( strFieldName );

            if ( StringUtils.isEmpty( strIdEntry ) || !StringUtils.isNumeric( strIdEntry ) )
            {
                return I18nService.getLocalizedString( ERROR_MESSAGE_UNKNOWN_ERROR, locale );
            }

            int nIdEntry = Integer.parseInt( strIdEntry );
            Entry entry = EntryHome.findByPrimaryKey( nIdEntry );

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

            if ( entry != null )
            {
                GenericAttributeError error = EntryTypeServiceManager.getEntryTypeService( entry )
                                                                     .canUploadFiles( entry, listUploadedFileItems,
                        listFileItemsToUpload, locale );

                if ( error != null )
                {
                    return error.getErrorMessage(  );
                }

                return null;
            }
        }

        return I18nService.getLocalizedString( ERROR_MESSAGE_UNKNOWN_ERROR, locale );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<FileItem> getListUploadedFiles( String strFieldName, HttpSession session )
    {
        if ( StringUtils.isBlank( strFieldName ) )
        {
            throw new AppException( "id field name is not provided for the current file upload" );
        }

        initMap( session.getId(  ), strFieldName );

        // find session-related files in the map
        Map<String, List<FileItem>> mapFileItemsSession = _mapAsynchronousUpload.get( session.getId(  ) );

        return mapFileItemsSession.get( strFieldName );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addFileItemToUploadedFilesList( FileItem fileItem, String strFieldName, HttpServletRequest request )
    {
        // This is the name that will be displayed in the form. We keep
        // the original name, but clean it to make it cross-platform.
        String strFileName = UploadUtil.cleanFileName( fileItem.getName(  ).trim(  ) );

        initMap( request.getSession(  ).getId(  ), buildFieldName( strFieldName ) );

        // Check if this file has not already been uploaded
        List<FileItem> uploadedFiles = getListUploadedFiles( strFieldName, request.getSession(  ) );

        if ( uploadedFiles != null )
        {
            boolean bNew = true;

            if ( !uploadedFiles.isEmpty(  ) )
            {
                Iterator<FileItem> iterUploadedFiles = uploadedFiles.iterator(  );

                while ( bNew && iterUploadedFiles.hasNext(  ) )
                {
                    FileItem uploadedFile = iterUploadedFiles.next(  );
                    String strUploadedFileName = UploadUtil.cleanFileName( uploadedFile.getName(  ).trim(  ) );
                    // If we find a file with the same name and the same
                    // length, we consider that the current file has
                    // already been uploaded
                    bNew = !( StringUtils.equals( strUploadedFileName, strFileName ) &&
                        ( uploadedFile.getSize(  ) == fileItem.getSize(  ) ) );
                }
            }

            if ( bNew )
            {
                uploadedFiles.add( fileItem );
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @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(  );
        }
    }

    /**
     * Removes all files associated to the session
     * @param strSessionId the session id
     */
    public void removeSessionFiles( String strSessionId )
    {
        _mapAsynchronousUpload.remove( strSessionId );
    }

    /**
     * Build the field name from a given id entry
     * i.e. : form_1
     * @param strIdEntry the id entry
     * @return the field name
     */
    protected String buildFieldName( String strIdEntry )
    {
        return PREFIX_ENTRY_ID + strIdEntry;
    }

    /**
     * Get the id of the entry associated with a given field name
     * @param strFieldName The name of the field
     * @return The id of the entry
     */
    protected String getEntryIdFromFieldName( String strFieldName )
    {
        if ( StringUtils.isEmpty( strFieldName ) || ( strFieldName.length(  ) < PREFIX_ENTRY_ID.length(  ) ) )
        {
            return null;
        }

        return strFieldName.substring( PREFIX_ENTRY_ID.length(  ) );
    }

    /**
     * Init the map
     * @param strSessionId the session id
     * @param strFieldName the field name
     */
    private void initMap( String strSessionId, String strFieldName )
    {
        // find session-related files in the map
        Map<String, List<FileItem>> mapFileItemsSession = _mapAsynchronousUpload.get( strSessionId );

        // create map if not exists
        if ( mapFileItemsSession == null )
        {
            synchronized ( this )
            {
                // Ignore double check locking error : assignation and instanciation of objects are separated.
                mapFileItemsSession = _mapAsynchronousUpload.get( strSessionId );

                if ( mapFileItemsSession == null )
                {
                    mapFileItemsSession = new ConcurrentHashMap<String, List<FileItem>>(  );
                    _mapAsynchronousUpload.put( strSessionId, mapFileItemsSession );
                }
            }
        }

        List<FileItem> listFileItems = mapFileItemsSession.get( strFieldName );

        if ( listFileItems == null )
        {
            listFileItems = new ArrayList<FileItem>(  );
            mapFileItemsSession.put( strFieldName, listFileItems );
        }
    }
}

Inclusion of the JavaScript file

Once the handler is created, the JavaScript file provided by the JSP jsp / site / plugins / asynchronousupload / GetMainUploadJs.jsp must be included in the HTML page containing the form. This JSP uses the following HTTP settings:

  • handler : Name of the handler (as defined in the service). This parameter is required.
  • maxFileSize : The maximum size (in bytes) of each uploaded file. This parameter is optional, the default is 2097152.

Inclusion example:

<script type="text/javascript" src="jsp/site/plugins/asynchronousupload/GetMainUploadJs.jsp?handler=announceAsynchronousUploadHandler" ></script>

Note that this file must be included for each handler used on the page.

Freemarker macros

All that remains is to create the input that will enable asynchronous uploads.

These inputs can be created thanks to the macros contained in the template skin / plugins / asynchronousupload / upload_commons.html. Macros in this file are accessible by importing them into your templates. To do this, add the following line, for example in the header of your templates:

<#include "/skin/plugins/asynchronousupload/upload_commons.html" />

Once this file is imported, two macros can be used:

/ **
 * Macro to include the JavaScript and CSS files necessary for the operation of asynchronous upload inputs.
 * This macro must be used if the inclusion service of the Lutèce portal does not work (example: in the Back Office, in specific JSPs, ...)
 * /
<#macro addRequiredJsFiles>
/ **
 * Macro to add an input of file type initialized for asynchronous upload.
 * This macro will also create a block to display the list of upload files. This list will be updated during uploads.
 * @param fieldName Name of the input to generate.
 * @param handler Instance of the associated upload manager service.
 * @param listUploadedFiles List of files that have already been uploaded. Each object in this list must have a 'title' or 'name' attribute.
 * @param inputCssClass CSS class to add to the generated input. The default value is an empty string.
 * @param multiple True to allow simultaneous selection of multiple files, false to prohibit it. Requires an HTML5 compatible browser to work.
 * /
<#macro addFileInputAndfilesBox fieldName handler listUploadedFiles inputCssClass = '' multiple = false>

The macro <#macro addFileInputAndfilesBox fieldName handler listUploadedFiles inputCssClass = '' multiple = false> uses the macros below:

/ **
 * Macro allowing to add an input of file type initialized for the asynchronous upload.
 * @param fieldName Name of the input to generate.
 * @param handler Instance of the associated upload manager service.
 * @param inputCssClass CSS class to add to the generated input. The default value is an empty string.
 * @param multiple True to allow simultaneous selection of multiple files, false to prohibit it. Requires an HTML5 compatible browser to work.
* /

<#macro addFileInput fieldName handler multiple inputCssClass = false>

/ **
 * Macro creating a block to display the list of upload files. This list will be updated during uploads.
 * @param fieldName Name of the input to generate.
 * @param handler Instance of the associated upload manager service.
 * @param listUploadedFiles List of files that have already been uploaded. Each object in this list must have a 'title' or 'name' attribute.
 * /

<#macro addUploadedFilesBox fieldName handler listUploadedFiles>

You can use the addFileInputAndfilesBox macro to create as many inputs as needed.

The generated HTML code will therefore have an input allowing the asynchronous upload and using the handler service specified in parameter.

Reloading previously uploaded files

If files that have already been uploaded must be added to the list of files uploaded by a user (example: reloading a form, ...), they must be declared to the upload manager. To do this, simply call the IAsynchUploadHandler.addFileItemToUploadedFilesList (FileItem, String, HttpServletRequest) method of the associated upload manager.

Each of these files will then be considered as having been uploaded by the user. The files will be included in the list of uploaded files, and the user will have the option to delete them as any other file.

Synchronous uploads

Once the asynchronous upload has been set up, all that remains is to set up the possibility for users who do not have JavaScript to use the synchronous upload. This can be set up by server-specific processing.

Adding file

The upload manager's IAsynchUploadHandler.hasAddFileFlag (HttpServletRequest, String) method is used to check whether a flag indicating that a file has been uploaded for a specific input is present in the request. If this is the case, the IAsynchUploadHandler.addFilesUploadedSynchronously (HttpServletRequest, String) method is used to add the corresponding files to the upload manager. These will then be treated like any other uploaded file.

For the synchronous upload to work properly, you must do the following:

  • Show the form with the list of already uploaded files initialized
  • When the form is submitted, check if the upload flag is present for each input file.
  • For each flag found, add the files associated with the upload manager.
  • If at least 1 flag is present, display the form again with the list of updated files. If no flag is present, treat the request as a form validation.

Deleting files

Synchronous file deletion works similarly to upload: the IAsynchUploadHandler.hasRemoveFlag (HttpServletRequest, String) method tests the presence of a file deletion flag for a given input, and the IAsynchUploadHandler.doRemoveFile method (HttpServletRequest , String) allows deletion of specified files.