FormService.java

/*
 * Copyright (c) 2002-2022, 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.forms.service;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.transaction.annotation.Transactional;

import fr.paris.lutece.api.user.User;
import fr.paris.lutece.plugins.forms.business.CompositeDisplayType;
import fr.paris.lutece.plugins.forms.business.Form;
import fr.paris.lutece.plugins.forms.business.FormDisplay;
import fr.paris.lutece.plugins.forms.business.FormHome;
import fr.paris.lutece.plugins.forms.business.FormMessageHome;
import fr.paris.lutece.plugins.forms.business.FormQuestionResponse;
import fr.paris.lutece.plugins.forms.business.FormQuestionResponseHome;
import fr.paris.lutece.plugins.forms.business.FormResponse;
import fr.paris.lutece.plugins.forms.business.FormResponseHome;
import fr.paris.lutece.plugins.forms.business.FormResponseStep;
import fr.paris.lutece.plugins.forms.business.FormResponseStepHome;
import fr.paris.lutece.plugins.forms.business.Question;
import fr.paris.lutece.plugins.forms.business.QuestionHome;
import fr.paris.lutece.plugins.forms.business.Step;
import fr.paris.lutece.plugins.forms.business.StepHome;
import fr.paris.lutece.plugins.forms.business.export.FormExportConfigHome;
import fr.paris.lutece.plugins.forms.exception.MaxFormResponseException;
import fr.paris.lutece.plugins.forms.service.workflow.IFormWorkflowService;
import fr.paris.lutece.plugins.forms.util.FormsConstants;
import fr.paris.lutece.plugins.forms.util.FormsResponseUtils;
import fr.paris.lutece.plugins.forms.web.CompositeGroupDisplay;
import fr.paris.lutece.plugins.forms.web.CompositeQuestionDisplay;
import fr.paris.lutece.plugins.forms.web.FormResponseManager;
import fr.paris.lutece.plugins.forms.web.ICompositeDisplay;
import fr.paris.lutece.plugins.forms.web.StepDisplayTree;
import fr.paris.lutece.plugins.forms.web.admin.MultiviewFormResponseDetailsJspBean;
import fr.paris.lutece.plugins.forms.web.entrytype.IEntryDataService;
import fr.paris.lutece.plugins.genericattributes.business.Entry;
import fr.paris.lutece.plugins.genericattributes.business.EntryHome;
import fr.paris.lutece.plugins.genericattributes.business.FieldHome;
import fr.paris.lutece.plugins.genericattributes.business.Response;
import fr.paris.lutece.plugins.genericattributes.business.ResponseFilter;
import fr.paris.lutece.plugins.genericattributes.business.ResponseHome;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.AbstractEntryTypeFile;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.AbstractEntryTypeGalleryImage;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.AbstractEntryTypeImage;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.EntryTypeServiceManager;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.IEntryTypeService;
import fr.paris.lutece.plugins.genericattributes.util.GenericAttributesUtils;
import fr.paris.lutece.portal.business.event.ResourceEvent;
import fr.paris.lutece.portal.business.file.FileHome;
import fr.paris.lutece.portal.business.user.AdminUser;
import fr.paris.lutece.portal.service.admin.AdminUserService;
import fr.paris.lutece.portal.service.event.ResourceEventManager;
import fr.paris.lutece.portal.service.rbac.RBACService;
import fr.paris.lutece.portal.service.spring.SpringContextService;
import fr.paris.lutece.portal.service.workgroup.AdminWorkgroupService;
import fr.paris.lutece.util.sql.TransactionManager;

/**
 * This is the service class related to the form
 */
public class FormService
{
    public static final String BEAN_NAME = "forms.formService";

    @Inject
    private IFormWorkflowService _formWorkflowService;

    /**
     * Saves the specified form
     *      * 
     * @param form
     *            the form to save
     * @param formResponse
     *            the form response to save
     * @throws MaxFormResponseException 
     * 			 the MaxFormResponseException Runtime Exception
     * 
     */
    public void saveForm( Form form, FormResponse formResponse )
    {
    	TransactionManager.beginTransaction( FormsPlugin.getPlugin( ) );
	    try
	    {
	    	if (  (form.getMaxNumberResponse( ) != 0 || form.isOneResponseByUser( ))
	    			&& ( formResponse.getId() == 0 || !formResponse.isFromSave( )) )
	        {
	    		synchronized( FormsResponseUtils.getLockOnForm( form ) )
	            {
	            	if ( !(FormsResponseUtils.checkNumberMaxResponseForm( form ) ) )
	                {
	            		throw new MaxFormResponseException( "The maximum number of response has been reached for the form: "+ form.getTitle( ) );
	                }
	            	if(!(FormsResponseUtils.checkIfUserResponseForm( form,  formResponse.getGuid() ) )) {       
		            	
	            		throw new MaxFormResponseException( "The maximum number of response has been reached for the user with the guid: "+formResponse.getGuid() );    
		            }
	            	saveForm(formResponse );
	                if (form.getMaxNumberResponse( ) != 0) 
	                {
	                	FormsResponseUtils.increaseNumberResponse( form );
	                }
	            }	        
	        }
	    	else
	    	{	    		
	        	saveForm( formResponse );
	        }
	    	
	        TransactionManager.commitTransaction( FormsPlugin.getPlugin( ) );

	     }
	     catch( Exception e )
	     {
	    	 TransactionManager.rollBack( FormsPlugin.getPlugin( ) );
	         throw e ;
	     }         
        
	    fireFormResponseEventCreation( formResponse );         
    }
    /**
     * Save the response of form
     * 
     *  @param formResponse
     *            The FormResponse
      * @throws MaxFormResponseException 
     * 			 the MaxFormResponseException Runtime Exception
     */
    public void saveFormResponse( FormResponse formResponse )
    {	
    	saveForm( FormHome.findByPrimaryKey( formResponse.getFormId( ) ), formResponse );
    }

    /**
     * Process action on form creation
     *
     * @param form
     *            the workflow form
     * @param formResponse
     *            the form response
     */
    public void processFormAction( Form form, FormResponse formResponse )
    {
        _formWorkflowService.doProcessActionOnFormCreation( form, formResponse );
        fireFormResponseEventUpdate( formResponse );
    }

    /**
     * Filters the responses to keep the final responses
     * 
     * @param formResponse
     *            the form response containing the responses to filter
     */
    private void filterFinalSteps( FormResponse formResponse )
    {
        formResponse.setSteps(
                formResponse.getSteps( ).stream( ).filter( step -> step.getOrder( ) != FormsConstants.ORDER_NOT_SET ).collect( Collectors.toList( ) ) );
    }
    /**
     * Saves the form
     * @param formResponse
     */
    private void saveForm(FormResponse formResponse) 
    {
    	formResponse.setFromSave( Boolean.FALSE );	
        filterFinalSteps( formResponse );
        save( formResponse );
        saveFormResponseSteps( formResponse );
    }
    /**
     * Saves the form response
     * 
     * @param formResponse
     *            the form response to save
     */
    private void save( FormResponse formResponse )
    {
        if ( formResponse.getId( ) > 0 )
        {
            FormResponseHome.update( formResponse );

            for ( FormQuestionResponse formQuestionResponse : FormQuestionResponseHome.getFormQuestionResponseListByFormResponse( formResponse.getId( ) ) )
            {
                FormQuestionResponseHome.remove( formQuestionResponse );
            }

            FormResponseStepHome.removeByFormResponse( formResponse.getId( ) );
        }
        else
        {
        	FormResponseHome.create( formResponse );
        }
    }
    
    /**
     * Saves the form response
     * 
     * @param formResponse
     *            the form response to save
     */
    public void saveFormResponseWithoutQuestionResponse( FormResponse formResponse )
    {
        FormResponseHome.update( formResponse );
        fireFormResponseEventUpdate( formResponse );
    }

    /**
     * Saves the form response steps
     * 
     * @param formResponse
     *            the form response containing the form response steps to save
     */
    private void saveFormResponseSteps( FormResponse formResponse )
    {
        for ( FormResponseStep formResponseStep : formResponse.getSteps( ) )
        {
            formResponseStep.setFormResponseId( formResponse.getId( ) );

            saveFormQuestionResponse( formResponseStep );

            FormResponseStepHome.create( formResponseStep );
        }
    }

    /**
     * Saves the form question responses of the specified step
     * 
     * @param formResponseStep
     *            the form response step containing the form questions responses to save
     */
    private void saveFormQuestionResponse( FormResponseStep formResponseStep )
    {
        for ( FormQuestionResponse formQuestionResponse : formResponseStep.getQuestions( ) )
        {
            Question question = formQuestionResponse.getQuestion( );

            if ( question != null && question.isVisible( ) )
            {
                IEntryDataService dataService = EntryServiceManager.getInstance( ).getEntryDataService( question.getEntry( ).getEntryType( ) );
                formQuestionResponse.setIdFormResponse( formResponseStep.getFormResponseId( ) );
                dataService.save( formQuestionResponse );
            }
        }
    }

    /**
     * Saves the specified form for a backup
     * 
     * @param formResponse
     *            The form response to save
     */
    @Transactional( FormsConstants.BEAN_TRANSACTION_MANAGER )
    public void saveFormForBackup( FormResponse formResponse )
    {
        formResponse.setFromSave( Boolean.TRUE );

        save( formResponse );
        saveFormResponseSteps( formResponse );
    }

    /**
     * Removes the specified form's backup
     * 
     * @param formResponse
     *            The form response to remove
     */
    @Transactional( FormsConstants.BEAN_TRANSACTION_MANAGER )
    public void removeFormBackup( FormResponse formResponse )
    {
        if ( formResponse.isFromSave( ) )
        {
            FormResponseHome.remove( formResponse.getId( ) );

            for ( FormQuestionResponse formQuestionResponse : FormQuestionResponseHome.getFormQuestionResponseListByFormResponse( formResponse.getId( ) ) )
            {
                FormQuestionResponseHome.remove( formQuestionResponse );
            }

            FormResponseStepHome.removeByFormResponse( formResponse.getId( ) );
        }
    }

    /**
     * Get the full children composite list of the given step
     * 
     * @param nIdStep
     *            The step primary key
     * @return the Html of the given step
     */
    public List<ICompositeDisplay> getStepCompositeList( int nIdStep )
    {
        StepDisplayTree displayTree = new StepDisplayTree( nIdStep );

        return displayTree.getCompositeList( );
    }

    /**
     * Get the right composite from the given formDisplay
     * 
     * @param formDisplay
     *            The formDisplay
     * @param formResponse
     *            the form response
     * @param nIterationNumber
     *            the iteration number
     * @return the right composite
     */
    public ICompositeDisplay formDisplayToComposite( FormDisplay formDisplay, FormResponse formResponse, int nIterationNumber )
    {
        ICompositeDisplay composite = null;
        if ( FormsConstants.COMPOSITE_GROUP_TYPE.equals( formDisplay.getCompositeType( ) ) )
        {
            composite = new CompositeGroupDisplay( formDisplay, formResponse, nIterationNumber );

        }
        else
            if ( FormsConstants.COMPOSITE_QUESTION_TYPE.equals( formDisplay.getCompositeType( ) ) )
            {
                composite = new CompositeQuestionDisplay( formDisplay, formResponse, nIterationNumber );
            }

        return composite;
    }

    /**
     * Remove a given Form, all its steps and composites, workflow resources. Also remove all the related formResponses, QuestionsResposes, EntryResponses and
     * entries.
     * 
     * @param nIdForm
     *            The identifier of the form to be deleted
     * @param adminUser
     *            the user
     */
    @Transactional( FormsConstants.BEAN_TRANSACTION_MANAGER )
    public void removeForm( int nIdForm, AdminUser adminUser )
    {
        StepService stepService = SpringContextService.getBean( StepService.BEAN_NAME );

        List<Step> listStep = StepHome.getStepsListByForm( nIdForm );

        for ( Step step : listStep )
        {
            stepService.removeStep( step.getId( ) );
        }

        FormResponseHome.removeByForm( nIdForm );
        FormMessageHome.removeByForm( nIdForm );
        FormExportConfigHome.removeByForm( nIdForm );

        Form form = FormHome.findByPrimaryKey( nIdForm );
        int nIdWorkflow = form.getIdWorkflow( );
        if ( form.getLogo( ) != null )
        {
            FileHome.remove( form.getLogo( ).getIdFile( ) );
        }

        FormHome.remove( nIdForm );

        _formWorkflowService.removeResources( nIdWorkflow, nIdForm, adminUser );
    }

    /**
     * Check if a user is authorized to access a File from its given identifier
     * 
     * @param request
     *            The request to use to retrieve information of the current user
     * @param nIdResponse
     *            The identifier of the Response which have the file
     * @param nIdFile
     *            The identifier of the file to access
     * @return the boolean which tell if the user is authorize to access the given File or not
     */
    public boolean isFileAccessAuthorized( HttpServletRequest request, int nIdResponse, int nIdFile )
    {
        boolean bFileAccessAuthorized = Boolean.FALSE;

        Response response = ResponseHome.findByPrimaryKey( nIdResponse );
        if ( response != null && response.getEntry( ) != null && response.getFile( ) != null && response.getFile( ).getIdFile( ) == nIdFile )
        {
            Entry entryResponse = EntryHome.findByPrimaryKey( response.getEntry( ).getIdEntry( ) );
            IEntryTypeService entryTypeService = EntryTypeServiceManager.getEntryTypeService( entryResponse );

            if ( ( entryTypeService instanceof AbstractEntryTypeFile || entryTypeService instanceof AbstractEntryTypeImage 
                   || entryTypeService instanceof AbstractEntryTypeGalleryImage ) && Form.RESOURCE_TYPE.equals( entryResponse.getResourceType( ) ) )
            {
                bFileAccessAuthorized = canUserAccessFile( request, entryResponse.getIdResource( ) );
            }
        }

        return bFileAccessAuthorized;
    }

    /**
     * Check if a user have all necessaries permissions to access to file in the formResponse details view
     * 
     * @param request
     *            The request to use to retrieve the user
     * @param nIdForm
     *            The identifier of the Form to use to check the permissions
     * @return true if the user can access File false if the user doesn't have necessary permissions
     */
    private boolean canUserAccessFile( HttpServletRequest request, int nIdForm )
    {
        boolean bUserAccessFile = Boolean.FALSE;

        AdminUser adminUser = AdminUserService.getAdminUser( request );
        if ( adminUser != null && adminUser.checkRight( MultiviewFormResponseDetailsJspBean.RIGHT_FORMS_MULTIVIEW ) )
        {
            Form form = FormHome.findByPrimaryKey( nIdForm );
            if ( form != null && AdminWorkgroupService.isAuthorized( form, (User) adminUser ) )
            {
                boolean bRbacModify = RBACService.isAuthorized( Form.RESOURCE_TYPE, Integer.toString( form.getId( ) ),
                        FormsResourceIdService.PERMISSION_MODIFY_FORM_RESPONSE, (User) adminUser );

                boolean bRbacManage = RBACService.isAuthorized( Form.RESOURCE_TYPE, Integer.toString( form.getId( ) ),
                        FormsResourceIdService.PERMISSION_MANAGE_FORM_RESPONSE, (User) adminUser );

                boolean bRbacView = RBACService.isAuthorized( Form.RESOURCE_TYPE, Integer.toString( form.getId( ) ),
                        FormsResourceIdService.PERMISSION_VIEW_FORM_RESPONSE, (User) adminUser );

                bUserAccessFile = bRbacModify || bRbacManage || bRbacView;
            }
        }

        return bUserAccessFile;
    }

    /**
     * Creates a {@code FormResponseManager} object from a back up
     * 
     * @param form
     *            The form
     * @param strUserGuid
     *            The user guid
     * @return the created {@code FormResponseManager} object
     */
    public FormResponseManager createFormResponseManagerFromBackUp( Form form, String strUserGuid )
    {
        FormResponseManager formResponseManager = null;
        List<FormResponse> listFormResponse = FormResponseHome.getFormResponseByGuidAndForm( strUserGuid, form.getId( ), true );
        if ( CollectionUtils.isNotEmpty( listFormResponse ) )
        {
            formResponseManager = new FormResponseManager( listFormResponse.get( 0 ) );
            formResponseManager.setIsResponseLoadedFromBackup(true);
        }
        else
        {
            formResponseManager = new FormResponseManager( form );
        }
        return formResponseManager;
    }

    // FORM RESPONSE CREATION
    /**
     * Fire the create event on given form Response
     * 
     * @param formResponse
     *            the form Response
     */
    public void fireFormResponseEventCreation( FormResponse formResponse )
    {
        ResourceEvent formResponseEvent = new ResourceEvent( );
        formResponseEvent.setIdResource( String.valueOf( formResponse.getId( ) ) );
        formResponseEvent.setTypeResource( FormResponse.RESOURCE_TYPE );

        ResourceEventManager.fireAddedResource( formResponseEvent );
    }

    /**
     * Fire the create event on all the form responses associated to given form
     * 
     * @param form
     *            The form
     */
    public void fireFormResponseEventCreation( Form form )
    {
        new Thread( ( ) -> {
            List<FormResponse> listFormResponse = FormResponseHome.selectAllFormResponsesUncompleteByIdForm( form.getId( ) );

            for ( FormResponse formResponse : listFormResponse )
            {
                fireFormResponseEventCreation( formResponse );
            }
        } ).start( );
    }

    // FORM RESPONSE UPDATE
    /**
     * Fire the form response event update on given formResponse
     * 
     * @param formResponse
     *            the formResponse
     */
    public void fireFormResponseEventUpdate( FormResponse formResponse )
    {
        ResourceEvent formResponseEvent = new ResourceEvent( );
        formResponseEvent.setIdResource( String.valueOf( formResponse.getId( ) ) );
        formResponseEvent.setTypeResource( FormResponse.RESOURCE_TYPE );

        ResourceEventManager.fireUpdatedResource( formResponseEvent );
    }

    /**
     * Fire the update event on all the form responses associated to given form
     * 
     * @param form
     *            The form
     */
    public void fireFormResponseEventUpdate( Form form )
    {
        new Thread( ( ) -> {
            List<FormResponse> listFormResponse = FormResponseHome.selectAllFormResponsesUncompleteByIdForm( form.getId( ) );

            for ( FormResponse formResponse : listFormResponse )
            {
                fireFormResponseEventUpdate( formResponse );
            }
        } ).start( );
    }

    // FORM RESPONSE DELETION

    /**
     * Fire the form response deletion event
     * 
     * @param formResponse
     *            the form response
     */
    public void fireFormResponseEventDelete( FormResponse formResponse )
    {
        ResourceEvent formResponseEvent = new ResourceEvent( );
        formResponseEvent.setIdResource( String.valueOf( formResponse.getId( ) ) );
        formResponseEvent.setTypeResource( FormResponse.RESOURCE_TYPE );

        ResourceEventManager.fireDeletedResource( formResponseEvent );
    }

    /**
     * Fire the delete event of all the form responses associated to given form
     * 
     * @param form
     *            the form
     */
    public void fireFormResponseEventDelete( Form form )
    {
        new Thread( ( ) -> {
            List<FormResponse> listFormResponse = FormResponseHome.selectAllFormResponsesUncompleteByIdForm( form.getId( ) );

            for ( FormResponse formResponse : listFormResponse )
            {
                fireFormResponseEventDelete( formResponse );
            }
        } ).start( );
    }
    
    /**
     * Save or update the field of the entry
     * @param entry
     * @param fieldName
     * @param title
     * @param value
     */
    public void saveOrUpdateField( Entry entry, String fieldName, String title, String value )
    {
        if ( entry.getFieldByCode( fieldName ) == null )
        {
            FieldHome.create( GenericAttributesUtils.createOrUpdateField( entry, fieldName, title, value ) );
        }
        else
        {
            FieldHome.update( GenericAttributesUtils.createOrUpdateField( entry, fieldName, title, value ) );
        }
    }

    /**
     * Check if responses exist for a composite (question or group of questions)
     * 
     * @param formDisplay
     *            the form display
     * @return true if responses exist, false otherwise
     */
    public boolean existCompositeResponses( FormDisplay formDisplay )
    {
        boolean existCompositeResponses = false;
        
        if ( CollectionUtils.isEmpty( FormResponseHome.selectAllFormResponsesUncompleteByIdForm( formDisplay.getFormId( ) ) ) )
        {
            return false;
        }
        
        if ( CompositeDisplayType.GROUP.getLabel( ).equalsIgnoreCase( formDisplay.getCompositeType( ) ) )
        {
            List<Question> questionsList = new ArrayList<>( );
            ICompositeDisplay composite = formDisplayToComposite( formDisplay, null, 0 );
            composite.addQuestions( questionsList );
            
            if ( CollectionUtils.isNotEmpty( questionsList ) )
            {
                List<Integer> idEntryList = questionsList.stream( ).map( Question::getIdEntry ).collect( Collectors.toList( ) );
                ResponseFilter responsefilter = new ResponseFilter( );
                responsefilter.setListIdEntry( idEntryList );
                
                existCompositeResponses = existsFilledResponse( responsefilter );
            }
        }
        else if ( CompositeDisplayType.QUESTION.getLabel( ).equalsIgnoreCase( formDisplay.getCompositeType( ) ) )
        {
            Question question = QuestionHome.findByPrimaryKey( formDisplay.getCompositeId( ) );
            
            if ( question != null && question.getIdEntry( ) > 0 )
            {
                ResponseFilter responsefilter = new ResponseFilter( );
                responsefilter.setIdEntry( question.getIdEntry( ) );
                
                existCompositeResponses = existsFilledResponse( responsefilter );
            }
        }
        
        return existCompositeResponses;
    }
    
    /**
     * Check if any response value filled exists for a question
     * 
     * @param responseFilter
     *            the response filter
     * @return true if a response filled exist, false otherwise
     */
    private boolean existsFilledResponse( ResponseFilter responseFilter )
    {
    	boolean existsFilledResponse = false ;
    	
    	List<Response> responseList = ResponseHome.getResponseList( responseFilter );

		if ( CollectionUtils.isNotEmpty( responseList ) 
				&& responseList.stream( ).anyMatch( response -> StringUtils.isNotBlank( response.getResponseValue( ) ) ) )
		{
			existsFilledResponse = true;
		}
    	
    	return existsFilledResponse;
    }
}