DocumentResourceServlet.java

/*
 * Copyright (c) 2002-2023, 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.document.web;

import fr.paris.lutece.plugins.document.business.Document;
import fr.paris.lutece.plugins.document.business.DocumentHome;
import fr.paris.lutece.plugins.document.business.DocumentResource;
import fr.paris.lutece.plugins.document.service.DocumentEvent;
import fr.paris.lutece.plugins.document.service.DocumentEventListener;
import fr.paris.lutece.plugins.document.service.DocumentException;
import fr.paris.lutece.plugins.document.utils.IntegerUtils;
import fr.paris.lutece.portal.business.resourceenhancer.ResourceEnhancer;
import fr.paris.lutece.portal.service.resource.ExtendableResourceActionHit;
import fr.paris.lutece.portal.service.util.AppPropertiesService;

import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * Servlet serving document file resources
 */
public class DocumentResourceServlet extends HttpServlet implements DocumentEventListener
{
    private static final long serialVersionUID = -7512201287826936428L;
    private static final String PARAMETER_DOCUMENT_ID = "id";
    private static final String PARAMETER_ATTRIBUTE_ID = "id_attribute";
    private static final String PARAMETER_WORKING_CONTENT = "working_content";
    private static final String PARAMETER_NOCACHE = "nocache";
    private static final String DEFAULT_EXPIRES_DELAY = "180000";
    private static final String PROPERTY_RESOURCE_TYPE = "document";
    private static final String PROPERTY_EXPIRES_DELAY = "document.resourceServlet.cacheControl.expires";
    private static final String KEY_DOC_BEGIN = "[doc:";
    private static final String KEY_ATTR_BEGIN = "[attr:";
    private static final String KEY_ITEM_CLOSE = "]";
    private static final String STRING_DELAY_IN_SECOND = AppPropertiesService.getProperty( PROPERTY_EXPIRES_DELAY,
            DEFAULT_EXPIRES_DELAY );
    private static final Long LONG_DELAY_IN_MILLISECOND = Long.parseLong( STRING_DELAY_IN_SECOND ) * 1000;
    private static final ResourceServletCache _cache = new ResourceServletCache(  );

    /**
     * Processes request HTTP <code>GET if-modified-since</code> methods
     * @param request The HTTP request
     * @return document last modified date
     */
    @Override
    public long getLastModified( HttpServletRequest request )
    {
        long lLastModified = -1;
        String strDocumentId = request.getParameter( PARAMETER_DOCUMENT_ID );
        String strAttributeId = request.getParameter( PARAMETER_ATTRIBUTE_ID );

        if ( ( strDocumentId != null ) && ( strAttributeId != null ) )
        {
            int nDocumentId = IntegerUtils.convert( strDocumentId );
            int nAttributeId = IntegerUtils.convert( strAttributeId );
            String strKey = getCacheKey( nDocumentId, nAttributeId );

            ResourceValueObject resource = _cache.get( strKey );

            if ( _cache.isCacheEnable(  ) && ( _cache.get( strKey ) != null ) )
            {
                return resource.getLastModified(  );
            }

            Document document = DocumentHome.loadLastModifiedAttributes( nDocumentId );

            // Because Internet Explorer 6 has bogus behavior with PDF and proxy or HTTPS
            if ( ( document != null ) &&
                    !Document.CODE_DOCUMENT_TYPE_DOWNLOAD.equals( document.getCodeDocumentType(  ) ) &&
                    ( document.getDateModification(  ) != null ) )
            {
                lLastModified = document.getDateModification(  ).getTime(  );
            }
        }

        if ( lLastModified == -1 )
        {
            lLastModified = super.getLastModified( request );
        }

        return lLastModified;
    }

    /**
     * Put the file in cache
     * @param nDocumentId The document id
     * @param nAttributeId The attribut id
     */
    public static void putInCache( int nDocumentId, int nAttributeId )
    {
        if ( !_cache.isCacheEnable(  ) )
        {
            return;
        }

        DocumentResource resource = DocumentHome.getValidatedResource( nDocumentId, nAttributeId );
        ResourceValueObject res = new ResourceValueObject( resource );
        Document document = DocumentHome.loadLastModifiedAttributes( nDocumentId );
        long lLastModified = document.getDateModification(  ).getTime(  );
        res.setLastModified( lLastModified );
        _cache.put( getCacheKey( nDocumentId, nAttributeId ), res );
    }

    /**
     * Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException the servlet Exception
     * @throws IOException the io exception
     */
    protected void processRequest( HttpServletRequest request, HttpServletResponse response )
        throws ServletException, IOException
    {
        String strDocumentId = request.getParameter( PARAMETER_DOCUMENT_ID );
        int nDocumentId = IntegerUtils.convert( strDocumentId );
        String strAttributeId = request.getParameter( PARAMETER_ATTRIBUTE_ID );
        String strNoCache = request.getParameter( PARAMETER_NOCACHE );

        int nAttributeId = IntegerUtils.convert( strAttributeId );
        Boolean bWorkingContent = ( request.getParameter( PARAMETER_WORKING_CONTENT ) != null );

        String strCacheKey = getCacheKey( nDocumentId, nAttributeId );
        ResourceValueObject res;

        if ( !bWorkingContent && _cache.isCacheEnable(  ) && ( _cache.get( strCacheKey ) != null ) )
        {
            res = _cache.get( strCacheKey );
        }
        else
        {
            DocumentResource resource = getResource( nDocumentId, nAttributeId, bWorkingContent );

            if ( resource == null )
            {
                return;
            }

            res = new ResourceValueObject( resource );

            if ( _cache.isCacheEnable(  ) && !bWorkingContent )
            {
                Document document = DocumentHome.loadLastModifiedAttributes( nDocumentId );
                long lLastModified = document.getDateModification(  ).getTime(  );
                res.setLastModified( lLastModified );
                _cache.put( strCacheKey, res );
            }
        }

        ExtendableResourceActionHit.getInstance(  )
                                   .notifyActionOnResource( strDocumentId, Document.PROPERTY_RESOURCE_TYPE,
            ExtendableResourceActionHit.ACTION_DOWNLOAD );

        ResourceEnhancer.doDownloadResourceAddOn( request, PROPERTY_RESOURCE_TYPE, nDocumentId );

        // Sets content type and filename of the resource into the response
        response.setContentType( res.getContentType(  ) );

        if ( !isGraphicalContent( res.getContentType(  ) ) )
        {
            // Add the filename only if the resource isn't a flash document or an image
            response.setHeader( "Content-Disposition", "attachment;filename=\"" + res.getFilename(  ) + "\"" );
        }

        // Add Cache Control HTTP header
        if ( strNoCache != null )
        {
            response.setHeader( "Cache-Control", "no-cache" ); // HTTP 1.1
            response.setDateHeader( "Expires", 0 ); // HTTP 1.0
        }
        else
        {
            response.setHeader( "Cache-Control", "max-age=" + STRING_DELAY_IN_SECOND ); // HTTP 1.1
            response.setDateHeader( "Expires", System.currentTimeMillis(  ) + LONG_DELAY_IN_MILLISECOND ); // HTTP 1.0           
        }

        response.setContentLength( res.getContent(  ).length ); // Keep Alive connexion

        // Write the resource content
        OutputStream out = response.getOutputStream(  );
        out.write( res.getContent(  ) );

        //out.flush( );        
        //out.close(); Disabled : allow Keep Alive connexion
    }

    /** Handles the HTTP <code>GET</code> method.
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException the servlet Exception
     * @throws IOException the io exception
     */
    @Override
    protected void doGet( HttpServletRequest request, HttpServletResponse response )
        throws ServletException, IOException
    {
        processRequest( request, response );
    }

    /** Handles the HTTP <code>POST</code> method.
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException the servlet Exception
     * @throws IOException the io exception
     */
    @Override
    protected void doPost( HttpServletRequest request, HttpServletResponse response )
        throws ServletException, IOException
    {
        processRequest( request, response );
    }

    /** Returns a short description of the servlet.
     * @return message
     */
    @Override
    public String getServletInfo(  )
    {
        return "Servlet serving file resources of documents";
    }

    /**
     * Caclculate the cache key
     * @param nDocumentId The document id
     * @param nAttributeId The attribute id
     * @return The key
     */
    private static String getCacheKey( int nDocumentId, int nAttributeId )
    {
        StringBuilder sbKey = new StringBuilder(  );
        sbKey.append( KEY_DOC_BEGIN ).append( nDocumentId ).append( KEY_ITEM_CLOSE ).append( KEY_ATTR_BEGIN )
             .append( nAttributeId ).append( KEY_ITEM_CLOSE );

        return sbKey.toString(  );
    }

    /**
     * Get the document resource
     * @param nDocumentId The document id
     * @param nAttributeId The attribute id
     * @param bWorkingContent is a working content
     * @return The document resource
     */
    private DocumentResource getResource( int nDocumentId, int nAttributeId, boolean bWorkingContent )
    {
        DocumentResource resource;

        if ( nAttributeId != -1 )
        {
            if ( bWorkingContent )
            {
                resource = DocumentHome.getWorkingResource( nDocumentId, nAttributeId );

                if ( resource == null )
                {
                    resource = DocumentHome.getValidatedResource( nDocumentId, nAttributeId );
                }
            }
            else
            {
                resource = DocumentHome.getValidatedResource( nDocumentId, nAttributeId );
            }
        }
        else
        {
            resource = DocumentHome.getResource( nDocumentId );
        }

        return resource;
    }

    /**
     * Is the document an image or a flash object according the content type
     * @param strContentType The content type
     * @return True for an image or a flash object, otherwise false
     */
    private boolean isGraphicalContent( String strContentType )
    {
        return ( strContentType.equals( "image/jpeg" ) || strContentType.equals( "image/gif" ) ||
        strContentType.equals( "image/png" ) || strContentType.equals( "application/x-shockwave-flash" ) );
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void processDocumentEvent( DocumentEvent event )
        throws DocumentException
    {
        String strKeyPattern = KEY_DOC_BEGIN + event.getDocument(  ).getId(  ) + KEY_ITEM_CLOSE;
        _cache.removeFromKeyPattern( strKeyPattern );
    }
}