CompositeQuestionDisplay.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.web;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;

import fr.paris.lutece.plugins.forms.business.CompositeDisplayType;
import fr.paris.lutece.plugins.forms.business.Control;
import fr.paris.lutece.plugins.forms.business.ControlGroup;
import fr.paris.lutece.plugins.forms.business.ControlGroupHome;
import fr.paris.lutece.plugins.forms.business.ControlHome;
import fr.paris.lutece.plugins.forms.business.ControlType;
import fr.paris.lutece.plugins.forms.business.FormDisplay;
import fr.paris.lutece.plugins.forms.business.FormDisplayHome;
import fr.paris.lutece.plugins.forms.business.FormQuestionResponse;
import fr.paris.lutece.plugins.forms.business.FormResponse;
import fr.paris.lutece.plugins.forms.business.FormResponseStep;
import fr.paris.lutece.plugins.forms.business.LogicalOperator;
import fr.paris.lutece.plugins.forms.business.Question;
import fr.paris.lutece.plugins.forms.business.QuestionHome;
import fr.paris.lutece.plugins.forms.service.EntryServiceManager;
import fr.paris.lutece.plugins.forms.util.FormsConstants;
import fr.paris.lutece.plugins.forms.validation.IValidator;
import fr.paris.lutece.plugins.forms.web.entrytype.DisplayType;
import fr.paris.lutece.plugins.forms.web.entrytype.IEntryDataService;
import fr.paris.lutece.plugins.forms.web.entrytype.IEntryDisplayService;
import fr.paris.lutece.plugins.genericattributes.business.Entry;
import fr.paris.lutece.plugins.genericattributes.business.Field;
import fr.paris.lutece.plugins.genericattributes.business.FieldHome;
import fr.paris.lutece.plugins.genericattributes.business.Response;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.AbstractEntryTypeComment;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.EntryTypeServiceManager;
import fr.paris.lutece.plugins.genericattributes.service.entrytype.IEntryTypeService;
import fr.paris.lutece.portal.service.image.ImageResourceManager;
import fr.paris.lutece.portal.service.template.AppTemplateService;
import fr.paris.lutece.util.html.HtmlTemplate;

/**
 * 
 * Implementation of ICompositeDisplay for Question
 *
 */
public class CompositeQuestionDisplay implements ICompositeDisplay
{
    // Templates
    private static final String TEMPLATE_QUESTION_EDITION_FRONTOFFICE = "/skin/plugins/forms/composite_template/view_question.html";
    private static final String TEMPLATE_QUESTION_READONLY_BACKOFFICE = "/admin/plugins/forms/composite/view_question_read_only.html";
    private static final String TEMPLATE_QUESTION_RESUBMIT_BACKOFFICE = "/admin/plugins/forms/composite/select_question.html";
    private static final String TEMPLATE_QUESTION_EDITION_BACKOFFICE = "/admin/plugins/forms/composite/view_question.html";
    private static final String TEMPLATE_QUESTION_READONLY_FRONTOFFICE = "/skin/plugins/forms/composite_template/view_question_read_only.html";
    private static final String TEMPLATE_QUESTION_RESUBMIT_FRONTOFFICE = "/skin/plugins/forms/composite_template/view_question_resubmit.html";

    // Marks
    private static final String MARK_QUESTION_ENTRY = "questionEntry";
    private static final String MARK_COMPLETENESS_FO = "is_completeness_bo";
    private static final String MARK_ENTRY_ITERATION_NUMBER = "entry_iteration_number";
    private static final String MARK_FIELDS_LIST_BY_ID_ENTRIES = "fields_list_by_id_entries";

    // Constants
    private static final String PUBLIC_IMAGE_RESOURCE = "public_image_resource";
    private static final String ILLUSTRATION_IMAGE = "illustration_image";
    
    private Question _question;
    private final FormDisplay _formDisplay;
    private String _strIconName;
    private final Map<String, Object> _model;

    /**
     * Constructor
     * 
     * @param formDisplay
     *            the form display
     * @param formResponse
     *            the form response
     * @param nIterationNumber
     *            the iteration number
     */
    public CompositeQuestionDisplay( FormDisplay formDisplay, FormResponse formResponse, int nIterationNumber )
    {
        _formDisplay = formDisplay;
        _model = new HashMap<>( );

        initComposite( formResponse, nIterationNumber );
    }

    /**
     * Initializes the composite
     * 
     * @param formResponse
     *            the form response
     * @param nIterationNumber
     *            the iteration number
     */
    private void initComposite( FormResponse formResponse, int nIterationNumber )
    {
        _question = QuestionHome.findByPrimaryKey( _formDisplay.getCompositeId( ) );

        if ( _question.getEntry( ) != null && _question.getEntry( ).getEntryType( ) != null )
        {
            _strIconName = _question.getEntry( ).getEntryType( ).getIconName( );
        }

        _question.setIterationNumber( nIterationNumber );

        Question question = getQuestionFromFormResponse( formResponse );

        if ( question != null )
        {
            _question.setIsVisible( question.isVisible( ) );
        }
    }

    /**
     * 
     * @param formResponse
     *            the formResponse
     * @return the current question from the given formResponse
     */
    private Question getQuestionFromFormResponse( FormResponse formResponse )
    {
        if ( formResponse == null || formResponse.getSteps( ).isEmpty( ) )
        {
            return null;
        }

        List<FormResponseStep> listSteps = formResponse.getSteps( );

        for ( FormResponseStep formResponseStep : listSteps )
        {
            if ( formResponseStep.getStep( ).getId( ) == _question.getIdStep( ) )
            {
                List<FormQuestionResponse> questionsResponse = formResponseStep.getQuestions( );

                for ( FormQuestionResponse questionResponse : questionsResponse )
                {
                    if ( questionResponse.getQuestion( ).getId( ) == _question.getId( )
                            && questionResponse.getQuestion( ).getIterationNumber( ) == _question.getIterationNumber( ) )
                    {
                        return questionResponse.getQuestion( );
                    }
                }
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getCompositeHtml( HttpServletRequest request, List<FormQuestionResponse> listFormQuestionResponse, Locale locale, DisplayType displayType )
    {
        String strQuestionTemplate = StringUtils.EMPTY;
        Map<Integer, String> fieldsList = new HashMap<>( );
        Entry entry = _question.getEntry( );

        if ( entry == null )
        {
            return strQuestionTemplate;
        }

        IEntryDisplayService displayService = EntryServiceManager.getInstance( ).getEntryDisplayService( entry.getEntryType( ) );

        if ( displayService != null && isQuestionEnabled( entry, listFormQuestionResponse, displayType ) )
        {
            List<Response> listResponse = findResponses( listFormQuestionResponse );
            setQuestionVisibility( listResponse, displayType );

            _model.put( MARK_ENTRY_ITERATION_NUMBER, _question.getIterationNumber( ) );
            _model.put( FormsConstants.MARK_QUESTION_LIST_RESPONSES, listResponse );
            _model.put( MARK_QUESTION_ENTRY, _question.getEntry( ) );
            _model.put( MARK_COMPLETENESS_FO, displayType == DisplayType.COMPLETE_FRONTOFFICE );
            _model.put( FormsConstants.MARK_REGEX_URL, FormsConstants.DEFAULT_REGEX_URL );

            for( Field field : FieldHome.getFieldListByIdEntry( _question.getEntry().getIdEntry( ) ) )
            {
            	if( field.getCode( ).equals( ILLUSTRATION_IMAGE ) && field.getValue( ) != null )
            		fieldsList.put( field.getIdField( ), ImageResourceManager.getImageUrl( PUBLIC_IMAGE_RESOURCE, Integer.parseInt( field.getValue( ) ) )  );
            }
            
            _model.put( MARK_FIELDS_LIST_BY_ID_ENTRIES, fieldsList );
            
            strQuestionTemplate = displayService.getEntryTemplateDisplay( request, _question.getEntry( ), locale, _model, displayType );

            _model.put( FormsConstants.MARK_QUESTION_CONTENT, strQuestionTemplate );
            _model.put( FormsConstants.MARK_QUESTION, _question );
            if ( _formDisplay.getDisplayControl( ) != null )
            {
                List<Control> listControl = ControlHome.getControlByControlTargetAndType( _formDisplay.getId( ), ControlType.CONDITIONAL );
                List<Control> listOtherStepControl = new ArrayList<>();
                List<IValidator> listValidator = new ArrayList<>();
                Boolean bOtherStepValidation = null;
                int nIdControlGroup = 0;
                int nValidControlsCount = 0;
                int nNotValidControlsCount = 0;
                for (Control control : listControl) {
                	if (nIdControlGroup == 0) {
            			nIdControlGroup = control.getIdControlGroup();
            		}
                	IValidator validator = EntryServiceManager.getInstance( ).getValidator( control.getValidatorName( ) );
                	listValidator.add(validator);
                	control.setValue(validator.getJavascriptControlValue( control ));
                	if ( CollectionUtils.isNotEmpty( control.getListIdQuestion( ) ) && CollectionUtils.isNotEmpty( listFormQuestionResponse ) )
                    {
                        int questionControlStep = QuestionHome.findByPrimaryKey( control.getListIdQuestion( ).iterator( ).next( ) ).getIdStep( );
                        if ( questionControlStep != _question.getIdStep( ) )
                        {
                            List<FormQuestionResponse> listFormQuestionReponseToCheck = listFormQuestionResponse.stream( )
                                    .filter( questionReponse -> control.getListIdQuestion( ).contains( questionReponse.getQuestion( ).getId( ) ) )
                                    .collect( Collectors.toList( ) );
                            if (validator.validate( listFormQuestionReponseToCheck, control )) {
                            	nValidControlsCount++;
                            } else {
                            	nNotValidControlsCount++;
                            }
                            listOtherStepControl.add(control);
                        }
                    }
                }
                
                // remove controls from other steps
                listControl.removeAll(listOtherStepControl);
                
                _model.put(FormsConstants.MARK_LIST_CONTROL, listControl);
                _model.put( FormsConstants.MARK_LIST_VALIDATOR, listValidator );
                
                ControlGroup controlGroup = ControlGroupHome.findByPrimaryKey(nIdControlGroup).orElse(null);
                _model.put( FormsConstants.MARK_LOGICAL_OPERATOR_LABEL, (controlGroup != null ? controlGroup.getLogicalOperator().getLabel() : LogicalOperator.AND.getLabel()) );
                if (controlGroup != null && LogicalOperator.OR.getLabel().equals(controlGroup.getLogicalOperator().getLabel())) {
                	bOtherStepValidation = nValidControlsCount > 0;
                } else {
                	bOtherStepValidation = nNotValidControlsCount == 0;
                }
                _model.put( FormsConstants.MARK_OTHER_STEP_VALIDATION, bOtherStepValidation);
            }

            HtmlTemplate htmlTemplateQuestion = AppTemplateService.getTemplate( findTemplateFor( displayType ), locale, _model );

            strQuestionTemplate = htmlTemplateQuestion != null ? htmlTemplateQuestion.getHtml( ) : StringUtils.EMPTY;
        }

        return strQuestionTemplate;
    }
    
    /**
     * Check if the question is enabled so that it can be displayed in the form
     * 
     * @param entry
     *            the entry
     * @param listFormQuestionResponse
     *            the list of form question responses
     * @param displayType
     *            the display type
     * @return true if the question is enabled, false otherwise
     */
    private boolean isQuestionEnabled( Entry entry, List<FormQuestionResponse> listFormQuestionResponse, DisplayType displayType )
    {
    	boolean isQuestionEnabled = true;
        Field disabledField = entry.getFieldByCode( IEntryTypeService.FIELD_DISABLED );
        
        if ( disabledField != null && Boolean.parseBoolean( disabledField.getValue( ) ) )
        {
        	switch( displayType.getMode( ) )
            {
                case EDITION:
                	isQuestionEnabled = false;
                    break;
                case READONLY:
                	isQuestionEnabled = existsResponseFilled( listFormQuestionResponse );
                    break;
                default: // Nothing to do
            }
        }

        return isQuestionEnabled;
    }
    
    /**
     * Check if a response value filled exists to a question 
     * 
     * @param listFormQuestionResponse
     *            the list of form question responses
     * @return true if a response value filled exists, false otherwise
     */
    private boolean existsResponseFilled( List<FormQuestionResponse> listFormQuestionResponse )
    {
    	boolean existsResponseFilled = false;
    	
        if ( CollectionUtils.isNotEmpty( listFormQuestionResponse ) )
        {
        	List<Response> listResponse = findResponses( listFormQuestionResponse );
        	
        	if ( CollectionUtils.isNotEmpty( listResponse ) 
        			&& listResponse.stream( ).anyMatch( response -> StringUtils.isNotBlank( response.getResponseValue( ) ) ) )
        	{
        		existsResponseFilled = true;
        	}
        }
        
        return existsResponseFilled;
    }

    /**
     * Finds the responses associated to this instance among the specified list of form question responses
     * 
     * @param listFormQuestionResponse
     *            the list of form question responses
     * @return the responses
     */
    private List<Response> findResponses( List<FormQuestionResponse> listFormQuestionResponse )
    {
        List<Response> listResponse = new ArrayList<>( );

        if ( listFormQuestionResponse != null )
        {
            for ( FormQuestionResponse formQuestionResponse : listFormQuestionResponse )
            {
                Question question = formQuestionResponse.getQuestion( );

                if ( _question.getId( ) == question.getId( ) && _question.getIterationNumber( ) == question.getIterationNumber( ) )
                {
                    listResponse = formQuestionResponse.getEntryResponse( );
                    break;
                }
            }
        }

        return listResponse;
    }

    /**
     * Finds the template to use for the specified display type
     * 
     * @param displayType
     *            the display type
     * @return the template
     */
    private String findTemplateFor( DisplayType displayType )
    {
        String strTemplate = StringUtils.EMPTY;

        if ( displayType == DisplayType.EDITION_FRONTOFFICE )
        {
            strTemplate = TEMPLATE_QUESTION_EDITION_FRONTOFFICE;
        }

        if ( displayType == DisplayType.EDITION_BACKOFFICE || displayType == DisplayType.SUBMIT_BACKOFFICE )
        {
            strTemplate = TEMPLATE_QUESTION_EDITION_BACKOFFICE;
        }

        if ( displayType == DisplayType.READONLY_BACKOFFICE )
        {
            strTemplate = TEMPLATE_QUESTION_READONLY_BACKOFFICE;
        }

        if ( displayType == DisplayType.READONLY_FRONTOFFICE )
        {
            strTemplate = TEMPLATE_QUESTION_READONLY_FRONTOFFICE;
        }
        if ( displayType == DisplayType.RESUBMIT_BACKOFFICE )
        {
            strTemplate = TEMPLATE_QUESTION_RESUBMIT_BACKOFFICE;
        }
        if ( displayType == DisplayType.RESUBMIT_FRONTOFFICE )
        {
            strTemplate = TEMPLATE_QUESTION_RESUBMIT_FRONTOFFICE;
        }
        if ( displayType == DisplayType.COMPLETE_BACKOFFICE )
        {
            strTemplate = TEMPLATE_QUESTION_RESUBMIT_BACKOFFICE;
        }
        if ( displayType == DisplayType.COMPLETE_FRONTOFFICE )
        {
            strTemplate = TEMPLATE_QUESTION_RESUBMIT_FRONTOFFICE;
        }
        return strTemplate;
    }

    private void setQuestionVisibilityReadOnlyBO( List<Response> listResponse )
    {
        List<Control> listConditionalControl = ControlHome.getControlByControlTargetAndType( _formDisplay.getId( ), ControlType.CONDITIONAL );
        Control controlConditionnalDisplay = null;

        if ( !listConditionalControl.isEmpty( ) )
        {
            controlConditionnalDisplay = listConditionalControl.get( 0 );
        }

        // No Conditional Display
        if ( controlConditionnalDisplay == null )
        {
            _question.setIsVisible( true );
        }
        else
        {
            IEntryTypeService entryTypeService = EntryTypeServiceManager.getEntryTypeService( _question.getEntry( ) );
            // If there is a conditional display, shows question if it has responses or if it is a comment
            if ( CollectionUtils.isNotEmpty( listResponse ) || entryTypeService instanceof AbstractEntryTypeComment )
            {
                _question.setIsVisible( true );
            }
        }
    }

    /**
     * 
     * @param listResponse
     *            The current question responses
     * @param displayType
     *            The current displayType
     */
    private void setQuestionVisibility( List<Response> listResponse, DisplayType displayType )
    {
        if ( displayType == DisplayType.READONLY_BACKOFFICE && _formDisplay != null )
        {
            setQuestionVisibilityReadOnlyBO( listResponse );
        }
        if ( _question.getEntry( ) == null )
        {
            return;
        }

        if ( displayType == DisplayType.EDITION_FRONTOFFICE )
        {

            _question.setIsVisible( true );
        }

        if ( displayType == DisplayType.EDITION_BACKOFFICE || displayType == DisplayType.SUBMIT_BACKOFFICE )
        {
            _question.setIsVisible( true );

        }
        if ( displayType == DisplayType.RESUBMIT_BACKOFFICE )
        {
            _question.setIsVisible( CollectionUtils.isNotEmpty( listResponse ) );

        }

        if ( displayType == DisplayType.RESUBMIT_FRONTOFFICE )
        {
            _question.setIsVisible( true );

        }
        if ( displayType == DisplayType.COMPLETE_BACKOFFICE )
        {
            _question.setIsVisible( true );

        }

        if ( displayType == DisplayType.COMPLETE_FRONTOFFICE )
        {
            _question.setIsVisible( true );

        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void iterate( int nIdFormDisplay )
    {
        // Nothing to do
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeIteration( HttpServletRequest request, int nIdGroupParent, int nIndexIterationToRemove, FormResponse formResponse )
    {
        FormDisplay formDisplayParent = FormDisplayHome.findByPrimaryKey( _formDisplay.getParentId( ) );

        if ( formDisplayParent != null && FormsConstants.COMPOSITE_GROUP_TYPE.equals( formDisplayParent.getCompositeType( ) )
                && formDisplayParent.getCompositeId( ) == nIdGroupParent )
        {
            IEntryDataService entryDataService = EntryServiceManager.getInstance( ).getEntryDataService( _question.getEntry( ).getEntryType( ) );

            if ( entryDataService != null )
            {
                if ( _question.getIterationNumber( ) == nIndexIterationToRemove )
                {
                    entryDataService.questionRemoved( request, _question );
                }

                if ( _question.getIterationNumber( ) > nIndexIterationToRemove )
                {
                    entryDataService.questionMoved( request, _question, _question.getIterationNumber( ) - 1 );
                }
            }
        }
    }

    @Override
    public List<ICompositeDisplay> getCompositeList( )
    {
        List<ICompositeDisplay> listICompositeDisplay = new ArrayList<>( );
        listICompositeDisplay.add( this );
        return listICompositeDisplay;
    }

    @Override
    public String getTitle( )
    {
        String strTitle = "";
        if ( _question != null && StringUtils.isNotEmpty( _question.getTitle( ) ) )
        {
            strTitle = _question.getTitle( );
        }
        return strTitle;
    }

    @Override
    public String getType( )
    {
        return _question != null ? CompositeDisplayType.QUESTION.getLabel( ) : StringUtils.EMPTY;
    }

    @Override
    public FormDisplay getFormDisplay( )
    {
        return _formDisplay;
    }

    @Override
    public String getIcon( )
    {
        return _strIconName;
    }

    @Override
    public List<Control> getAllDisplayControls( )
    {
        List<Control> listDisplayControls = new ArrayList<>( );

        if ( _formDisplay.getDisplayControl( ) != null )
        {
            listDisplayControls.add( _formDisplay.getDisplayControl( ) );
        }

        return listDisplayControls;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addQuestions( List<Question> listQuestion )
    {
        listQuestion.add( _question );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addModel( Map<String, Object> model )
    {
        _model.putAll( model );
    }

    @Override
    public boolean isVisible( )
    {
        if ( _question == null )
        {
            return false;
        }
        return _question.isVisible( );
    }

    @Override
    public ICompositeDisplay filter( List<Integer> listQuestionIds )
    {
        if ( listQuestionIds.contains( _question.getId( ) ) )
        {
            return this;
        }
        return null;
    }
}